# HG changeset patch # User Chris Cannam # Date 1371714470 -3600 # Node ID b2ea0641f798e9ac1e30680b17c5f47587cdbfd5 # Parent ab89f95ef405269e82ddb68bd167947eb1294910# Parent 287f201c2802333ae4981ba8179366ffb5d11199 Merge from redmine-2.2-integration branch. We're going live! diff -r ab89f95ef405 -r b2ea0641f798 .gitignore --- a/.gitignore Thu Jun 20 08:46:39 2013 +0100 +++ b/.gitignore Thu Jun 20 08:47:50 2013 +0100 @@ -5,6 +5,7 @@ /config/database.yml /config/email.yml /config/initializers/session_store.rb +/config/initializers/secret_token.rb /coverage /db/*.db /db/*.sqlite3 @@ -18,9 +19,12 @@ /public/plugin_assets /tmp/* /tmp/cache/* +/tmp/pdf/* /tmp/sessions/* /tmp/sockets/* /tmp/test/* +/tmp/thumbnails/* +/vendor/cache /vendor/rails *.rbc diff -r ab89f95ef405 -r b2ea0641f798 .hgignore --- a/.hgignore Thu Jun 20 08:46:39 2013 +0100 +++ b/.hgignore Thu Jun 20 08:47:50 2013 +0100 @@ -7,6 +7,7 @@ config/database.yml config/email.yml config/initializers/session_store.rb +config/initializers/secret_token.rb coverage db/*.db db/*.sqlite3 @@ -20,17 +21,17 @@ public/plugin_assets tmp/* tmp/cache/* +tmp/pdf/* tmp/sessions/* tmp/sockets/* tmp/test/* +tmp/thumbnails/* +vendor/cache vendor/rails *.rbc - -.svn/ .git/ *~ public/themes/soundsoftware/stylesheets/fonts/* - .bundle Gemfile.lock Gemfile.local diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/00/009460d580c5e7ab7c5519f32165ee2250a363d7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/009460d580c5e7ab7c5519f32165ee2250a363d7.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,229 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../test_helper', __FILE__) + +class Redmine::UnifiedDiffTest < ActiveSupport::TestCase + def test_subversion_diff + diff = Redmine::UnifiedDiff.new(read_diff_fixture('subversion.diff')) + # number of files + assert_equal 4, diff.size + assert diff.detect {|file| file.file_name =~ %r{^config/settings.yml}} + end + + def test_truncate_diff + diff = Redmine::UnifiedDiff.new(read_diff_fixture('subversion.diff'), :max_lines => 20) + assert_equal 2, diff.size + end + + def test_inline_partials + diff = Redmine::UnifiedDiff.new(read_diff_fixture('partials.diff')) + assert_equal 1, diff.size + diff = diff.first + assert_equal 43, diff.size + + assert_equal [51, -1], diff[0].offsets + assert_equal [51, -1], diff[1].offsets + assert_equal 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', diff[0].html_line + assert_equal 'Lorem ipsum dolor sit amet, consectetur adipiscing xx', diff[1].html_line + + assert_nil diff[2].offsets + assert_equal 'Praesent et sagittis dui. Vivamus ac diam diam', diff[2].html_line + + assert_equal [0, -14], diff[3].offsets + assert_equal [0, -14], diff[4].offsets + assert_equal 'Ut sed auctor justo', diff[3].html_line + assert_equal 'xxx auctor justo', diff[4].html_line + + assert_equal [13, -19], diff[6].offsets + assert_equal [13, -19], diff[7].offsets + + assert_equal [24, -8], diff[9].offsets + assert_equal [24, -8], diff[10].offsets + + assert_equal [37, -1], diff[12].offsets + assert_equal [37, -1], diff[13].offsets + + assert_equal [0, -38], diff[15].offsets + assert_equal [0, -38], diff[16].offsets + end + + def test_side_by_side_partials + diff = Redmine::UnifiedDiff.new(read_diff_fixture('partials.diff'), :type => 'sbs') + assert_equal 1, diff.size + diff = diff.first + assert_equal 32, diff.size + + assert_equal [51, -1], diff[0].offsets + assert_equal 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', diff[0].html_line_left + assert_equal 'Lorem ipsum dolor sit amet, consectetur adipiscing xx', diff[0].html_line_right + + assert_nil diff[1].offsets + assert_equal 'Praesent et sagittis dui. Vivamus ac diam diam', diff[1].html_line_left + assert_equal 'Praesent et sagittis dui. Vivamus ac diam diam', diff[1].html_line_right + + assert_equal [0, -14], diff[2].offsets + assert_equal 'Ut sed auctor justo', diff[2].html_line_left + assert_equal 'xxx auctor justo', diff[2].html_line_right + + assert_equal [13, -19], diff[4].offsets + assert_equal [24, -8], diff[6].offsets + assert_equal [37, -1], diff[8].offsets + assert_equal [0, -38], diff[10].offsets + + end + + def test_partials_with_html_entities + raw = <<-DIFF +--- test.orig.txt Wed Feb 15 16:10:39 2012 ++++ test.new.txt Wed Feb 15 16:11:25 2012 +@@ -1,5 +1,5 @@ + Semicolons were mysteriously appearing in code diffs in the repository + +-void DoSomething(std::auto_ptr myObj) ++void DoSomething(const MyClass& myObj) + +DIFF + + diff = Redmine::UnifiedDiff.new(raw, :type => 'sbs') + assert_equal 1, diff.size + assert_equal 'void DoSomething(std::auto_ptr<MyClass> myObj)', diff.first[2].html_line_left + assert_equal 'void DoSomething(const MyClass& myObj)', diff.first[2].html_line_right + + diff = Redmine::UnifiedDiff.new(raw, :type => 'inline') + assert_equal 1, diff.size + assert_equal 'void DoSomething(std::auto_ptr<MyClass> myObj)', diff.first[2].html_line + assert_equal 'void DoSomething(const MyClass& myObj)', diff.first[3].html_line + end + + def test_line_starting_with_dashes + diff = Redmine::UnifiedDiff.new(<<-DIFF +--- old.txt Wed Nov 11 14:24:58 2009 ++++ new.txt Wed Nov 11 14:25:02 2009 +@@ -1,8 +1,4 @@ +-Lines that starts with dashes: +- +------------------------- +--- file.c +------------------------- ++A line that starts with dashes: + + and removed. + +@@ -23,4 +19,4 @@ + + + +-Another chunk of change ++Another chunk of changes + +DIFF + ) + assert_equal 1, diff.size + end + + def test_one_line_new_files + diff = Redmine::UnifiedDiff.new(<<-DIFF +diff -r 000000000000 -r ea98b14f75f0 README1 +--- /dev/null ++++ b/README1 +@@ -0,0 +1,1 @@ ++test1 +diff -r 000000000000 -r ea98b14f75f0 README2 +--- /dev/null ++++ b/README2 +@@ -0,0 +1,1 @@ ++test2 +diff -r 000000000000 -r ea98b14f75f0 README3 +--- /dev/null ++++ b/README3 +@@ -0,0 +1,3 @@ ++test4 ++test5 ++test6 +diff -r 000000000000 -r ea98b14f75f0 README4 +--- /dev/null ++++ b/README4 +@@ -0,0 +1,3 @@ ++test4 ++test5 ++test6 +DIFF + ) + assert_equal 4, diff.size + assert_equal "README1", diff[0].file_name + end + + def test_both_git_diff + diff = Redmine::UnifiedDiff.new(<<-DIFF +# HG changeset patch +# User test +# Date 1348014182 -32400 +# Node ID d1c871b8ef113df7f1c56d41e6e3bfbaff976e1f +# Parent 180b6605936cdc7909c5f08b59746ec1a7c99b3e +modify test1.txt + +diff -r 180b6605936c -r d1c871b8ef11 test1.txt +--- a/test1.txt ++++ b/test1.txt +@@ -1,1 +1,1 @@ +-test1 ++modify test1 +DIFF + ) + assert_equal 1, diff.size + assert_equal "test1.txt", diff[0].file_name + end + + def test_include_a_b_slash + diff = Redmine::UnifiedDiff.new(<<-DIFF +--- test1.txt ++++ b/test02.txt +@@ -1 +0,0 @@ +-modify test1 +DIFF + ) + assert_equal 1, diff.size + assert_equal "b/test02.txt", diff[0].file_name + + diff = Redmine::UnifiedDiff.new(<<-DIFF +--- a/test1.txt ++++ a/test02.txt +@@ -1 +0,0 @@ +-modify test1 +DIFF + ) + assert_equal 1, diff.size + assert_equal "a/test02.txt", diff[0].file_name + + diff = Redmine::UnifiedDiff.new(<<-DIFF +--- a/test1.txt ++++ test02.txt +@@ -1 +0,0 @@ +-modify test1 +DIFF + ) + assert_equal 1, diff.size + assert_equal "test02.txt", diff[0].file_name + end + + private + + def read_diff_fixture(filename) + File.new(File.join(File.dirname(__FILE__), '/../../../fixtures/diffs', filename)).read + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/00/0097cb632567412c9ae3b42993114578789fd825.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/0097cb632567412c9ae3b42993114578789fd825.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,22 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class VersionCustomField < CustomField + def type_name + :label_version_plural + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/00/00dbd874dfe947c78eb0cddc2e6a0cbc57c3f815.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/00dbd874dfe947c78eb0cddc2e6a0cbc57c3f815.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,62 @@ +require File.expand_path('../../../test_helper', __FILE__) + +class ApiTest::DisabledRestApiTest < ActionController::IntegrationTest + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :workflows + + def setup + Setting.rest_api_enabled = '0' + Setting.login_required = '1' + end + + def teardown + Setting.rest_api_enabled = '1' + Setting.login_required = '0' + end + + def test_with_a_valid_api_token + @user = User.generate! + @token = Token.create!(:user => @user, :action => 'api') + + get "/news.xml?key=#{@token.value}" + assert_response :unauthorized + assert_equal User.anonymous, User.current + + get "/news.json?key=#{@token.value}" + assert_response :unauthorized + assert_equal User.anonymous, User.current + end + + def test_with_valid_username_password_http_authentication + @user = User.generate! do |user| + user.password = 'my_password' + end + + get "/news.xml", nil, credentials(@user.login, 'my_password') + assert_response :unauthorized + assert_equal User.anonymous, User.current + + get "/news.json", nil, credentials(@user.login, 'my_password') + assert_response :unauthorized + assert_equal User.anonymous, User.current + end + + def test_with_valid_token_http_authentication + @user = User.generate! + @token = Token.create!(:user => @user, :action => 'api') + + get "/news.xml", nil, credentials(@token.value, 'X') + assert_response :unauthorized + assert_equal User.anonymous, User.current + + get "/news.json", nil, credentials(@token.value, 'X') + assert_response :unauthorized + assert_equal User.anonymous, User.current + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/00/00e064a58814bd1506484c9e41b6bc1f8553a834.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/00/00e064a58814bd1506484c9e41b6bc1f8553a834.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,795 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class ApiTest::IssuesTest < ActionController::IntegrationTest + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :issue_relations, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries, + :attachments + + def setup + Setting.rest_api_enabled = '1' + end + + context "/issues" do + # Use a private project to make sure auth is really working and not just + # only showing public issues. + should_allow_api_authentication(:get, "/projects/private-child/issues.xml") + + should "contain metadata" do + get '/issues.xml' + + assert_tag :tag => 'issues', + :attributes => { + :type => 'array', + :total_count => assigns(:issue_count), + :limit => 25, + :offset => 0 + } + end + + context "with offset and limit" do + should "use the params" do + get '/issues.xml?offset=2&limit=3' + + assert_equal 3, assigns(:limit) + assert_equal 2, assigns(:offset) + assert_tag :tag => 'issues', :children => {:count => 3, :only => {:tag => 'issue'}} + end + end + + context "with nometa param" do + should "not contain metadata" do + get '/issues.xml?nometa=1' + + assert_tag :tag => 'issues', + :attributes => { + :type => 'array', + :total_count => nil, + :limit => nil, + :offset => nil + } + end + end + + context "with nometa header" do + should "not contain metadata" do + get '/issues.xml', {}, {'X-Redmine-Nometa' => '1'} + + assert_tag :tag => 'issues', + :attributes => { + :type => 'array', + :total_count => nil, + :limit => nil, + :offset => nil + } + end + end + + context "with relations" do + should "display relations" do + get '/issues.xml?include=relations' + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag 'relations', + :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '3'}}, + :children => {:count => 1}, + :child => { + :tag => 'relation', + :attributes => {:id => '2', :issue_id => '2', :issue_to_id => '3', + :relation_type => 'relates'} + } + assert_tag 'relations', + :parent => {:tag => 'issue', :child => {:tag => 'id', :content => '1'}}, + :children => {:count => 0} + end + end + + context "with invalid query params" do + should "return errors" do + get '/issues.xml', {:f => ['start_date'], :op => {:start_date => '='}} + + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag 'errors', :child => {:tag => 'error', :content => "Start date can't be blank"} + end + end + + context "with custom field filter" do + should "show only issues with the custom field value" do + get '/issues.xml', + {:set_filter => 1, :f => ['cf_1'], :op => {:cf_1 => '='}, + :v => {:cf_1 => ['MySQL']}} + expected_ids = Issue.visible.all( + :include => :custom_values, + :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id) + assert_select 'issues > issue > id', :count => expected_ids.count do |ids| + ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) } + end + end + end + + context "with custom field filter (shorthand method)" do + should "show only issues with the custom field value" do + get '/issues.xml', { :cf_1 => 'MySQL' } + + expected_ids = Issue.visible.all( + :include => :custom_values, + :conditions => {:custom_values => {:custom_field_id => 1, :value => 'MySQL'}}).map(&:id) + + assert_select 'issues > issue > id', :count => expected_ids.count do |ids| + ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) } + end + end + end + end + + context "/index.json" do + should_allow_api_authentication(:get, "/projects/private-child/issues.json") + end + + context "/index.xml with filter" do + should "show only issues with the status_id" do + get '/issues.xml?status_id=5' + + expected_ids = Issue.visible.all(:conditions => {:status_id => 5}).map(&:id) + + assert_select 'issues > issue > id', :count => expected_ids.count do |ids| + ids.each { |id| assert expected_ids.delete(id.children.first.content.to_i) } + end + end + end + + context "/index.json with filter" do + should "show only issues with the status_id" do + get '/issues.json?status_id=5' + + json = ActiveSupport::JSON.decode(response.body) + status_ids_used = json['issues'].collect {|j| j['status']['id'] } + assert_equal 3, status_ids_used.length + assert status_ids_used.all? {|id| id == 5 } + end + + end + + # Issue 6 is on a private project + context "/issues/6.xml" do + should_allow_api_authentication(:get, "/issues/6.xml") + end + + context "/issues/6.json" do + should_allow_api_authentication(:get, "/issues/6.json") + end + + context "GET /issues/:id" do + context "with journals" do + context ".xml" do + should "display journals" do + get '/issues/1.xml?include=journals' + + assert_tag :tag => 'issue', + :child => { + :tag => 'journals', + :attributes => { :type => 'array' }, + :child => { + :tag => 'journal', + :attributes => { :id => '1'}, + :child => { + :tag => 'details', + :attributes => { :type => 'array' }, + :child => { + :tag => 'detail', + :attributes => { :name => 'status_id' }, + :child => { + :tag => 'old_value', + :content => '1', + :sibling => { + :tag => 'new_value', + :content => '2' + } + } + } + } + } + } + end + end + end + + context "with custom fields" do + context ".xml" do + should "display custom fields" do + get '/issues/3.xml' + + assert_tag :tag => 'issue', + :child => { + :tag => 'custom_fields', + :attributes => { :type => 'array' }, + :child => { + :tag => 'custom_field', + :attributes => { :id => '1'}, + :child => { + :tag => 'value', + :content => 'MySQL' + } + } + } + + assert_nothing_raised do + Hash.from_xml(response.body).to_xml + end + end + end + end + + context "with multi custom fields" do + setup do + field = CustomField.find(1) + field.update_attribute :multiple, true + issue = Issue.find(3) + issue.custom_field_values = {1 => ['MySQL', 'Oracle']} + issue.save! + end + + context ".xml" do + should "display custom fields" do + get '/issues/3.xml' + assert_response :success + assert_tag :tag => 'issue', + :child => { + :tag => 'custom_fields', + :attributes => { :type => 'array' }, + :child => { + :tag => 'custom_field', + :attributes => { :id => '1'}, + :child => { + :tag => 'value', + :attributes => { :type => 'array' }, + :children => { :count => 2 } + } + } + } + + xml = Hash.from_xml(response.body) + custom_fields = xml['issue']['custom_fields'] + assert_kind_of Array, custom_fields + field = custom_fields.detect {|f| f['id'] == '1'} + assert_kind_of Hash, field + assert_equal ['MySQL', 'Oracle'], field['value'].sort + end + end + + context ".json" do + should "display custom fields" do + get '/issues/3.json' + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + custom_fields = json['issue']['custom_fields'] + assert_kind_of Array, custom_fields + field = custom_fields.detect {|f| f['id'] == 1} + assert_kind_of Hash, field + assert_equal ['MySQL', 'Oracle'], field['value'].sort + end + end + end + + context "with empty value for multi custom field" do + setup do + field = CustomField.find(1) + field.update_attribute :multiple, true + issue = Issue.find(3) + issue.custom_field_values = {1 => ['']} + issue.save! + end + + context ".xml" do + should "display custom fields" do + get '/issues/3.xml' + assert_response :success + assert_tag :tag => 'issue', + :child => { + :tag => 'custom_fields', + :attributes => { :type => 'array' }, + :child => { + :tag => 'custom_field', + :attributes => { :id => '1'}, + :child => { + :tag => 'value', + :attributes => { :type => 'array' }, + :children => { :count => 0 } + } + } + } + + xml = Hash.from_xml(response.body) + custom_fields = xml['issue']['custom_fields'] + assert_kind_of Array, custom_fields + field = custom_fields.detect {|f| f['id'] == '1'} + assert_kind_of Hash, field + assert_equal [], field['value'] + end + end + + context ".json" do + should "display custom fields" do + get '/issues/3.json' + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + custom_fields = json['issue']['custom_fields'] + assert_kind_of Array, custom_fields + field = custom_fields.detect {|f| f['id'] == 1} + assert_kind_of Hash, field + assert_equal [], field['value'].sort + end + end + end + + context "with attachments" do + context ".xml" do + should "display attachments" do + get '/issues/3.xml?include=attachments' + + assert_tag :tag => 'issue', + :child => { + :tag => 'attachments', + :children => {:count => 5}, + :child => { + :tag => 'attachment', + :child => { + :tag => 'filename', + :content => 'source.rb', + :sibling => { + :tag => 'content_url', + :content => 'http://www.example.com/attachments/download/4/source.rb' + } + } + } + } + end + end + end + + context "with subtasks" do + setup do + @c1 = Issue.create!( + :status_id => 1, :subject => "child c1", + :tracker_id => 1, :project_id => 1, :author_id => 1, + :parent_issue_id => 1 + ) + @c2 = Issue.create!( + :status_id => 1, :subject => "child c2", + :tracker_id => 1, :project_id => 1, :author_id => 1, + :parent_issue_id => 1 + ) + @c3 = Issue.create!( + :status_id => 1, :subject => "child c3", + :tracker_id => 1, :project_id => 1, :author_id => 1, + :parent_issue_id => @c1.id + ) + end + + context ".xml" do + should "display children" do + get '/issues/1.xml?include=children' + + assert_tag :tag => 'issue', + :child => { + :tag => 'children', + :children => {:count => 2}, + :child => { + :tag => 'issue', + :attributes => {:id => @c1.id.to_s}, + :child => { + :tag => 'subject', + :content => 'child c1', + :sibling => { + :tag => 'children', + :children => {:count => 1}, + :child => { + :tag => 'issue', + :attributes => {:id => @c3.id.to_s} + } + } + } + } + } + end + + context ".json" do + should "display children" do + get '/issues/1.json?include=children' + + json = ActiveSupport::JSON.decode(response.body) + assert_equal([ + { + 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'}, + 'children' => [{'id' => @c3.id, 'subject' => 'child c3', + 'tracker' => {'id' => 1, 'name' => 'Bug'} }] + }, + { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} } + ], + json['issue']['children']) + end + end + end + end + end + + context "POST /issues.xml" do + should_allow_api_authentication( + :post, + '/issues.xml', + {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, + {:success_code => :created} + ) + should "create an issue with the attributes" do + assert_difference('Issue.count') do + post '/issues.xml', + {:issue => {:project_id => 1, :subject => 'API test', + :tracker_id => 2, :status_id => 3}}, credentials('jsmith') + end + issue = Issue.first(:order => 'id DESC') + assert_equal 1, issue.project_id + assert_equal 2, issue.tracker_id + assert_equal 3, issue.status_id + assert_equal 'API test', issue.subject + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s} + end + end + + context "POST /issues.xml with failure" do + should "have an errors tag" do + assert_no_difference('Issue.count') do + post '/issues.xml', {:issue => {:project_id => 1}}, credentials('jsmith') + end + + assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"} + end + end + + context "POST /issues.json" do + should_allow_api_authentication(:post, + '/issues.json', + {:issue => {:project_id => 1, :subject => 'API test', + :tracker_id => 2, :status_id => 3}}, + {:success_code => :created}) + + should "create an issue with the attributes" do + assert_difference('Issue.count') do + post '/issues.json', + {:issue => {:project_id => 1, :subject => 'API test', + :tracker_id => 2, :status_id => 3}}, + credentials('jsmith') + end + + issue = Issue.first(:order => 'id DESC') + assert_equal 1, issue.project_id + assert_equal 2, issue.tracker_id + assert_equal 3, issue.status_id + assert_equal 'API test', issue.subject + end + + end + + context "POST /issues.json with failure" do + should "have an errors element" do + assert_no_difference('Issue.count') do + post '/issues.json', {:issue => {:project_id => 1}}, credentials('jsmith') + end + + json = ActiveSupport::JSON.decode(response.body) + assert json['errors'].include?("Subject can't be blank") + end + end + + # Issue 6 is on a private project + context "PUT /issues/6.xml" do + setup do + @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}} + end + + should_allow_api_authentication(:put, + '/issues/6.xml', + {:issue => {:subject => 'API update', :notes => 'A new note'}}, + {:success_code => :ok}) + + should "not create a new issue" do + assert_no_difference('Issue.count') do + put '/issues/6.xml', @parameters, credentials('jsmith') + end + end + + should "create a new journal" do + assert_difference('Journal.count') do + put '/issues/6.xml', @parameters, credentials('jsmith') + end + end + + should "add the note to the journal" do + put '/issues/6.xml', @parameters, credentials('jsmith') + + journal = Journal.last + assert_equal "A new note", journal.notes + end + + should "update the issue" do + put '/issues/6.xml', @parameters, credentials('jsmith') + + issue = Issue.find(6) + assert_equal "API update", issue.subject + end + + end + + context "PUT /issues/3.xml with custom fields" do + setup do + @parameters = { + :issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' }, + {'id' => '2', 'value' => '150'}]} + } + end + + should "update custom fields" do + assert_no_difference('Issue.count') do + put '/issues/3.xml', @parameters, credentials('jsmith') + end + + issue = Issue.find(3) + assert_equal '150', issue.custom_value_for(2).value + assert_equal 'PostgreSQL', issue.custom_value_for(1).value + end + end + + context "PUT /issues/3.xml with multi custom fields" do + setup do + field = CustomField.find(1) + field.update_attribute :multiple, true + @parameters = { + :issue => {:custom_fields => [{'id' => '1', 'value' => ['MySQL', 'PostgreSQL'] }, + {'id' => '2', 'value' => '150'}]} + } + end + + should "update custom fields" do + assert_no_difference('Issue.count') do + put '/issues/3.xml', @parameters, credentials('jsmith') + end + + issue = Issue.find(3) + assert_equal '150', issue.custom_value_for(2).value + assert_equal ['MySQL', 'PostgreSQL'], issue.custom_field_value(1).sort + end + end + + context "PUT /issues/3.xml with project change" do + setup do + @parameters = {:issue => {:project_id => 2, :subject => 'Project changed'}} + end + + should "update project" do + assert_no_difference('Issue.count') do + put '/issues/3.xml', @parameters, credentials('jsmith') + end + + issue = Issue.find(3) + assert_equal 2, issue.project_id + assert_equal 'Project changed', issue.subject + end + end + + context "PUT /issues/6.xml with failed update" do + setup do + @parameters = {:issue => {:subject => ''}} + end + + should "not create a new issue" do + assert_no_difference('Issue.count') do + put '/issues/6.xml', @parameters, credentials('jsmith') + end + end + + should "not create a new journal" do + assert_no_difference('Journal.count') do + put '/issues/6.xml', @parameters, credentials('jsmith') + end + end + + should "have an errors tag" do + put '/issues/6.xml', @parameters, credentials('jsmith') + + assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"} + end + end + + context "PUT /issues/6.json" do + setup do + @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}} + end + + should_allow_api_authentication(:put, + '/issues/6.json', + {:issue => {:subject => 'API update', :notes => 'A new note'}}, + {:success_code => :ok}) + + should "update the issue" do + assert_no_difference('Issue.count') do + assert_difference('Journal.count') do + put '/issues/6.json', @parameters, credentials('jsmith') + + assert_response :ok + assert_equal '', response.body + end + end + + issue = Issue.find(6) + assert_equal "API update", issue.subject + journal = Journal.last + assert_equal "A new note", journal.notes + end + end + + context "PUT /issues/6.json with failed update" do + should "return errors" do + assert_no_difference('Issue.count') do + assert_no_difference('Journal.count') do + put '/issues/6.json', {:issue => {:subject => ''}}, credentials('jsmith') + + assert_response :unprocessable_entity + end + end + + json = ActiveSupport::JSON.decode(response.body) + assert json['errors'].include?("Subject can't be blank") + end + end + + context "DELETE /issues/1.xml" do + should_allow_api_authentication(:delete, + '/issues/6.xml', + {}, + {:success_code => :ok}) + + should "delete the issue" do + assert_difference('Issue.count', -1) do + delete '/issues/6.xml', {}, credentials('jsmith') + + assert_response :ok + assert_equal '', response.body + end + + assert_nil Issue.find_by_id(6) + end + end + + context "DELETE /issues/1.json" do + should_allow_api_authentication(:delete, + '/issues/6.json', + {}, + {:success_code => :ok}) + + should "delete the issue" do + assert_difference('Issue.count', -1) do + delete '/issues/6.json', {}, credentials('jsmith') + + assert_response :ok + assert_equal '', response.body + end + + assert_nil Issue.find_by_id(6) + end + end + + def test_create_issue_with_uploaded_file + set_tmp_attachments_directory + # upload the file + assert_difference 'Attachment.count' do + post '/uploads.xml', 'test_create_with_upload', + {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response :created + end + xml = Hash.from_xml(response.body) + token = xml['upload']['token'] + attachment = Attachment.first(:order => 'id DESC') + + # create the issue with the upload's token + assert_difference 'Issue.count' do + post '/issues.xml', + {:issue => {:project_id => 1, :subject => 'Uploaded file', + :uploads => [{:token => token, :filename => 'test.txt', + :content_type => 'text/plain'}]}}, + credentials('jsmith') + assert_response :created + end + issue = Issue.first(:order => 'id DESC') + assert_equal 1, issue.attachments.count + assert_equal attachment, issue.attachments.first + + attachment.reload + assert_equal 'test.txt', attachment.filename + assert_equal 'text/plain', attachment.content_type + assert_equal 'test_create_with_upload'.size, attachment.filesize + assert_equal 2, attachment.author_id + + # get the issue with its attachments + get "/issues/#{issue.id}.xml", :include => 'attachments' + assert_response :success + xml = Hash.from_xml(response.body) + attachments = xml['issue']['attachments'] + assert_kind_of Array, attachments + assert_equal 1, attachments.size + url = attachments.first['content_url'] + assert_not_nil url + + # download the attachment + get url + assert_response :success + end + + def test_update_issue_with_uploaded_file + set_tmp_attachments_directory + # upload the file + assert_difference 'Attachment.count' do + post '/uploads.xml', 'test_upload_with_upload', + {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response :created + end + xml = Hash.from_xml(response.body) + token = xml['upload']['token'] + attachment = Attachment.first(:order => 'id DESC') + + # update the issue with the upload's token + assert_difference 'Journal.count' do + put '/issues/1.xml', + {:issue => {:notes => 'Attachment added', + :uploads => [{:token => token, :filename => 'test.txt', + :content_type => 'text/plain'}]}}, + credentials('jsmith') + assert_response :ok + assert_equal '', @response.body + end + + issue = Issue.find(1) + assert_include attachment, issue.attachments + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/01/012acdb93702d840e809c52a4f350ace647a5f34.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/012acdb93702d840e809c52a4f350ace647a5f34.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1285 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'forwardable' +require 'cgi' + +module ApplicationHelper + include Redmine::WikiFormatting::Macros::Definitions + include Redmine::I18n + include GravatarHelper::PublicMethods + + extend Forwardable + def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter + + # Return true if user is authorized for controller/action, otherwise false + def authorize_for(controller, action) + User.current.allowed_to?({:controller => controller, :action => action}, @project) + end + + # Display a link if user is authorized + # + # @param [String] name Anchor text (passed to link_to) + # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized + # @param [optional, Hash] html_options Options passed to link_to + # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to + def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) + link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) + end + + # Displays a link to user's account page if active + def link_to_user(user, options={}) + if user.is_a?(User) + name = h(user.name(options[:format])) + if user.active? || (User.current.admin? && user.logged?) + link_to name, user_path(user), :class => user.css_classes + else + name + end + else + h(user.to_s) + end + end + + # Displays a link to +issue+ with its subject. + # Examples: + # + # link_to_issue(issue) # => Defect #6: This is the subject + # link_to_issue(issue, :truncate => 6) # => Defect #6: This i... + # link_to_issue(issue, :subject => false) # => Defect #6 + # link_to_issue(issue, :project => true) # => Foo - Defect #6 + # link_to_issue(issue, :subject => false, :tracker => false) # => #6 + # + def link_to_issue(issue, options={}) + title = nil + subject = nil + text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}" + if options[:subject] == false + title = truncate(issue.subject, :length => 60) + else + subject = issue.subject + if options[:truncate] + subject = truncate(subject, :length => options[:truncate]) + end + end + s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title + s << h(": #{subject}") if subject + s = h("#{issue.project} - ") + s if options[:project] + s + end + + # Generates a link to an attachment. + # Options: + # * :text - Link text (default to attachment filename) + # * :download - Force download (default: false) + def link_to_attachment(attachment, options={}) + text = options.delete(:text) || attachment.filename + action = options.delete(:download) ? 'download' : 'show' + opt_only_path = {} + opt_only_path[:only_path] = (options[:only_path] == false ? false : true) + options.delete(:only_path) + link_to(h(text), + {:controller => 'attachments', :action => action, + :id => attachment, :filename => attachment.filename}.merge(opt_only_path), + options) + end + + # Generates a link to a SCM revision + # Options: + # * :text - Link text (default to the formatted revision) + def link_to_revision(revision, repository, options={}) + if repository.is_a?(Project) + repository = repository.repository + end + text = options.delete(:text) || format_revision(revision) + rev = revision.respond_to?(:identifier) ? revision.identifier : revision + link_to( + h(text), + {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev}, + :title => l(:label_revision_id, format_revision(revision)) + ) + end + + # Generates a link to a message + def link_to_message(message, options={}, html_options = nil) + link_to( + h(truncate(message.subject, :length => 60)), + { :controller => 'messages', :action => 'show', + :board_id => message.board_id, + :id => (message.parent_id || message.id), + :r => (message.parent_id && message.id), + :anchor => (message.parent_id ? "message-#{message.id}" : nil) + }.merge(options), + html_options + ) + end + + # Generates a link to a project if active + # Examples: + # + # link_to_project(project) # => link to the specified project overview + # link_to_project(project, :action=>'settings') # => link to project settings + # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options + # link_to_project(project, {}, :class => "project") # => html options with default url (project overview) + # + def link_to_project(project, options={}, html_options = nil) + if project.archived? + h(project) + else + url = {:controller => 'projects', :action => 'show', :id => project}.merge(options) + link_to(h(project), url, html_options) + end + end + + def wiki_page_path(page, options={}) + url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options)) + end + + def thumbnail_tag(attachment) + link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)), + {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename}, + :title => attachment.filename + end + + def toggle_link(name, id, options={}) + onclick = "$('##{id}').toggle(); " + onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ") + onclick << "return false;" + link_to(name, "#", :onclick => onclick) + end + + def image_to_function(name, function, html_options = {}) + html_options.symbolize_keys! + tag(:input, html_options.merge({ + :type => "image", :src => image_path(name), + :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" + })) + end + + def format_activity_title(text) + h(truncate_single_line(text, :length => 100)) + end + + def format_activity_day(date) + date == User.current.today ? l(:label_today).titleize : format_date(date) + end + + def format_activity_description(text) + h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...') + ).gsub(/[\r\n]+/, "
").html_safe + end + + def format_version_name(version) + if version.project == @project + h(version) + else + h("#{version.project} - #{version}") + end + end + + def due_date_distance_in_words(date) + if date + l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date)) + end + end + + # Renders a tree of projects as a nested set of unordered lists + # The given collection may be a subset of the whole project tree + # (eg. some intermediate nodes are private and can not be seen) + def render_project_nested_lists(projects) + s = '' + if projects.any? + ancestors = [] + original_project = @project + projects.sort_by(&:lft).each do |project| + # set the project environment to please macros. + @project = project + if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) + s << "\n" + end + end + classes = (ancestors.empty? ? 'root' : 'child') + s << "
  • " + s << h(block_given? ? yield(project) : project.name) + s << "
    \n" + ancestors << project + end + s << ("
  • \n" * ancestors.size) + @project = original_project + end + s.html_safe + end + + def render_page_hierarchy(pages, node=nil, options={}) + content = '' + if pages[node] + content << "\n" + end + content.html_safe + end + + # Renders flash messages + def render_flash_messages + s = '' + flash.each do |k,v| + s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}") + end + s.html_safe + end + + # Renders tabs and their content + def render_tabs(tabs) + if tabs.any? + render :partial => 'common/tabs', :locals => {:tabs => tabs} + else + content_tag 'p', l(:label_no_data), :class => "nodata" + end + end + + # Renders the project quick-jump box + def render_project_jump_box + return unless User.current.logged? + projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq + if projects.any? + options = + ("" + + '').html_safe + + options << project_tree_options_for_select(projects, :selected => @project) do |p| + { :value => project_path(:id => p, :jump => current_menu_item) } + end + + select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }') + end + end + + def project_tree_options_for_select(projects, options = {}) + s = '' + project_tree(projects) do |project, level| + name_prefix = (level > 0 ? ' ' * 2 * level + '» ' : '').html_safe + tag_options = {:value => project.id} + if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project)) + tag_options[:selected] = 'selected' + else + tag_options[:selected] = nil + end + tag_options.merge!(yield(project)) if block_given? + s << content_tag('option', name_prefix + h(project), tag_options) + end + s.html_safe + end + + # Yields the given block for each project with its level in the tree + # + # Wrapper for Project#project_tree + def project_tree(projects, &block) + Project.project_tree(projects, &block) + end + + def principals_check_box_tags(name, principals) + s = '' + principals.sort.each do |principal| + s << "\n" + end + s.html_safe + end + + # Returns a string for users/groups option tags + def principals_options_for_select(collection, selected=nil) + s = '' + if collection.include?(User.current) + s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id) + end + groups = '' + collection.sort.each do |element| + selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) + (element.is_a?(Group) ? groups : s) << %() + end + unless groups.empty? + s << %(#{groups}) + end + s.html_safe + end + + # Options for the new membership projects combo-box + def options_for_membership_project_select(principal, projects) + options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") + options << project_tree_options_for_select(projects) do |p| + {:disabled => principal.projects.include?(p)} + end + options + end + + # Truncates and returns the string as a single line + def truncate_single_line(string, *args) + truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ') + end + + # Truncates at line break after 250 characters or options[:length] + def truncate_lines(string, options={}) + length = options[:length] || 250 + if string.to_s =~ /\A(.{#{length}}.*?)$/m + "#{$1}..." + else + string + end + end + + def anchor(text) + text.to_s.gsub(' ', '_') + end + + def html_hours(text) + text.gsub(%r{(\d+)\.(\d+)}, '\1.\2').html_safe + end + + def authoring(created, author, options={}) + l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe + end + + def time_tag(time) + text = distance_of_time_in_words(Time.now, time) + if @project + link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time)) + else + content_tag('acronym', text, :title => format_time(time)) + end + end + + def syntax_highlight_lines(name, content) + lines = [] + syntax_highlight(name, content).each_line { |line| lines << line } + lines + end + + def syntax_highlight(name, content) + Redmine::SyntaxHighlighting.highlight_by_filename(content, name) + end + + def to_path_param(path) + str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/") + str.blank? ? nil : str + end + + def pagination_links_full(paginator, count=nil, options={}) + page_param = options.delete(:page_param) || :page + per_page_links = options.delete(:per_page_links) + url_param = params.dup + + html = '' + if paginator.current.previous + # \xc2\xab(utf-8) = « + html << link_to_content_update( + "\xc2\xab " + l(:label_previous), + url_param.merge(page_param => paginator.current.previous)) + ' ' + end + + html << (pagination_links_each(paginator, options) do |n| + link_to_content_update(n.to_s, url_param.merge(page_param => n)) + end || '') + + if paginator.current.next + # \xc2\xbb(utf-8) = » + html << ' ' + link_to_content_update( + (l(:label_next) + " \xc2\xbb"), + url_param.merge(page_param => paginator.current.next)) + end + + unless count.nil? + html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})" + if per_page_links != false && links = per_page_links(paginator.items_per_page, count) + html << " | #{links}" + end + end + + html.html_safe + end + + def per_page_links(selected=nil, item_count=nil) + values = Setting.per_page_options_array + if item_count && values.any? + if item_count > values.first + max = values.detect {|value| value >= item_count} || item_count + else + max = item_count + end + values = values.select {|value| value <= max || value == selected} + end + if values.empty? || (values.size == 1 && values.first == selected) + return nil + end + links = values.collect do |n| + n == selected ? n : link_to_content_update(n, params.merge(:per_page => n)) + end + l(:label_display_per_page, links.join(', ')) + end + + def reorder_links(name, url, method = :post) + link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), + url.merge({"#{name}[move_to]" => 'highest'}), + :method => method, :title => l(:label_sort_highest)) + + link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), + url.merge({"#{name}[move_to]" => 'higher'}), + :method => method, :title => l(:label_sort_higher)) + + link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), + url.merge({"#{name}[move_to]" => 'lower'}), + :method => method, :title => l(:label_sort_lower)) + + link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), + url.merge({"#{name}[move_to]" => 'lowest'}), + :method => method, :title => l(:label_sort_lowest)) + end + + def breadcrumb(*args) + elements = args.flatten + elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil + end + + def other_formats_links(&block) + concat('

    '.html_safe + l(:label_export_to)) + yield Redmine::Views::OtherFormatsBuilder.new(self) + concat('

    '.html_safe) + end + + def page_header_title + if @project.nil? || @project.new_record? + h(Setting.app_title) + else + b = [] + ancestors = (@project.root? ? [] : @project.ancestors.visible.all) + if ancestors.any? + root = ancestors.shift + b << link_to_project(root, {:jump => current_menu_item}, :class => 'root') + if ancestors.size > 2 + b << "\xe2\x80\xa6" + ancestors = ancestors[-2, 2] + end + b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') } + end + b << h(@project) + b.join(" \xc2\xbb ").html_safe + end + end + + def html_title(*args) + if args.empty? + title = @html_title || [] + title << @project.name if @project + title << Setting.app_title unless Setting.app_title == title.last + title.select {|t| !t.blank? }.join(' - ') + else + @html_title ||= [] + @html_title += args + end + end + + # Returns the theme, controller name, and action as css classes for the + # HTML body. + def body_css_classes + css = [] + if theme = Redmine::Themes.theme(Setting.ui_theme) + css << 'theme-' + theme.name + end + + css << 'controller-' + controller_name + css << 'action-' + action_name + css.join(' ') + end + + def accesskey(s) + Redmine::AccessKeys.key_for s + end + + # Formats text according to system settings. + # 2 ways to call this method: + # * with a String: textilizable(text, options) + # * with an object and one of its attribute: textilizable(issue, :description, options) + def textilizable(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + case args.size + when 1 + obj = options[:object] + text = args.shift + when 2 + obj = args.shift + attr = args.shift + text = obj.send(attr).to_s + else + raise ArgumentError, 'invalid arguments to textilizable' + end + return '' if text.blank? + project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) + only_path = options.delete(:only_path) == false ? false : true + + text = text.dup + macros = catch_macros(text) + text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) + + @parsed_headings = [] + @heading_anchors = {} + @current_section = 0 if options[:edit_section_links] + + parse_sections(text, project, obj, attr, only_path, options) + text = parse_non_pre_blocks(text, obj, macros) do |text| + [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name| + send method_name, text, project, obj, attr, only_path, options + end + end + parse_headings(text, project, obj, attr, only_path, options) + + if @parsed_headings.any? + replace_toc(text, @parsed_headings) + end + + text.html_safe + end + + def parse_non_pre_blocks(text, obj, macros) + s = StringScanner.new(text) + tags = [] + parsed = '' + while !s.eos? + s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im) + text, full_tag, closing, tag = s[1], s[2], s[3], s[4] + if tags.empty? + yield text + inject_macros(text, obj, macros) if macros.any? + else + inject_macros(text, obj, macros, false) if macros.any? + end + parsed << text + if tag + if closing + if tags.last == tag.downcase + tags.pop + end + else + tags << tag.downcase + end + parsed << full_tag + end + end + # Close any non closing tags + while tag = tags.pop + parsed << "" + end + parsed + end + + def parse_inline_attachments(text, project, obj, attr, only_path, options) + # when using an image link, try to use an attachment, if possible + if options[:attachments] || (obj && obj.respond_to?(:attachments)) + attachments = options[:attachments] || obj.attachments + text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| + filename, ext, alt, alttext = $1.downcase, $2, $3, $4 + # search for the picture in attachments + if found = Attachment.latest_attach(attachments, filename) + image_url = url_for :only_path => only_path, :controller => 'attachments', + :action => 'download', :id => found + desc = found.description.to_s.gsub('"', '') + if !desc.blank? && alttext.blank? + alt = " title=\"#{desc}\" alt=\"#{desc}\"" + end + "src=\"#{image_url}\"#{alt}" + else + m + end + end + end + end + + # Wiki links + # + # Examples: + # [[mypage]] + # [[mypage|mytext]] + # wiki links can refer other project wikis, using project name or identifier: + # [[project:]] -> wiki starting page + # [[project:|mytext]] + # [[project:mypage]] + # [[project:mypage|mytext]] + def parse_wiki_links(text, project, obj, attr, only_path, options) + text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m| + link_project = project + esc, all, page, title = $1, $2, $3, $5 + if esc.nil? + if page =~ /^([^\:]+)\:(.*)$/ + link_project = Project.find_by_identifier($1) || Project.find_by_name($1) + page = $2 + title ||= $1 if page.blank? + end + + if link_project && link_project.wiki + # extract anchor + anchor = nil + if page =~ /^(.+?)\#(.+)$/ + page, anchor = $1, $2 + end + anchor = sanitize_anchor_name(anchor) if anchor.present? + # check if page exists + wiki_page = link_project.wiki.find_page(page) + url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page + "##{anchor}" + else + case options[:wiki_links] + when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '') + when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export + else + wiki_page_id = page.present? ? Wiki.titleize(page) : nil + parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil + url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, + :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent) + end + end + link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new'))) + else + # project or wiki doesn't exist + all + end + else + all + end + end + end + + # Redmine links + # + # Examples: + # Issues: + # #52 -> Link to issue #52 + # Changesets: + # r52 -> Link to revision 52 + # commit:a85130f -> Link to scmid starting with a85130f + # Documents: + # document#17 -> Link to document with id 17 + # document:Greetings -> Link to the document with title "Greetings" + # document:"Some document" -> Link to the document with title "Some document" + # Versions: + # version#3 -> Link to version with id 3 + # version:1.0.0 -> Link to version named "1.0.0" + # version:"1.0 beta 2" -> Link to version named "1.0 beta 2" + # Attachments: + # attachment:file.zip -> Link to the attachment of the current object named file.zip + # Source files: + # source:some/file -> Link to the file located at /some/file in the project's repository + # source:some/file@52 -> Link to the file's revision 52 + # source:some/file#L120 -> Link to line 120 of the file + # source:some/file@52#L120 -> Link to line 120 of the file's revision 52 + # export:some/file -> Force the download of the file + # Forum messages: + # message#1218 -> Link to message with id 1218 + # + # Links can refer other objects from other projects, using project identifier: + # identifier:r52 + # identifier:document:"Some document" + # identifier:version:1.0.0 + # identifier:source:some/file + def parse_redmine_links(text, project, obj, attr, only_path, options) + text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m| + leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17 + link = nil + if project_identifier + project = Project.visible.find_by_identifier(project_identifier) + end + if esc.nil? + if prefix.nil? && sep == 'r' + if project + repository = nil + if repo_identifier + repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} + else + repository = project.repository + end + # project.changesets.visible raises an SQL error because of a double join on repositories + if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier)) + link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision}, + :class => 'changeset', + :title => truncate_single_line(changeset.comments, :length => 100)) + end + end + elsif sep == '#' + oid = identifier.to_i + case prefix + when nil + if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status) + anchor = comment_id ? "note-#{comment_id}" : nil + link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor}, + :class => issue.css_classes, + :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})") + end + when 'document' + if document = Document.visible.find_by_id(oid) + link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, + :class => 'document' + end + when 'version' + if version = Version.visible.find_by_id(oid) + link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, + :class => 'version' + end + when 'message' + if message = Message.visible.find_by_id(oid, :include => :parent) + link = link_to_message(message, {:only_path => only_path}, :class => 'message') + end + when 'forum' + if board = Board.visible.find_by_id(oid) + link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, + :class => 'board' + end + when 'news' + if news = News.visible.find_by_id(oid) + link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, + :class => 'news' + end + when 'project' + if p = Project.visible.find_by_id(oid) + link = link_to_project(p, {:only_path => only_path}, :class => 'project') + end + end + elsif sep == ':' + # removes the double quotes if any + name = identifier.gsub(%r{^"(.*)"$}, "\\1") + case prefix + when 'document' + if project && document = project.documents.visible.find_by_title(name) + link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, + :class => 'document' + end + when 'version' + if project && version = project.versions.visible.find_by_name(name) + link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, + :class => 'version' + end + when 'forum' + if project && board = project.boards.visible.find_by_name(name) + link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, + :class => 'board' + end + when 'news' + if project && news = project.news.visible.find_by_title(name) + link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, + :class => 'news' + end + when 'commit', 'source', 'export' + if project + repository = nil + if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$} + repo_prefix, repo_identifier, name = $1, $2, $3 + repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} + else + repository = project.repository + end + if prefix == 'commit' + if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"])) + link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier}, + :class => 'changeset', + :title => truncate_single_line(h(changeset.comments), :length => 100) + end + else + if repository && User.current.allowed_to?(:browse_repository, project) + name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$} + path, rev, anchor = $1, $3, $5 + link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param, + :path => to_path_param(path), + :rev => rev, + :anchor => anchor}, + :class => (prefix == 'export' ? 'source download' : 'source') + end + end + repo_prefix = nil + end + when 'attachment' + attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) + if attachments && attachment = attachments.detect {|a| a.filename == name } + link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment}, + :class => 'attachment' + end + when 'project' + if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}]) + link = link_to_project(p, {:only_path => only_path}, :class => 'project') + end + end + end + end + (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}")) + end + end + + HEADING_RE = /(]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE) + + def parse_sections(text, project, obj, attr, only_path, options) + return unless options[:edit_section_links] + text.gsub!(HEADING_RE) do + heading = $1 + @current_section += 1 + if @current_section > 1 + content_tag('div', + link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)), + :class => 'contextual', + :title => l(:button_edit_section)) + heading.html_safe + else + heading + end + end + end + + # Headings and TOC + # Adds ids and links to headings unless options[:headings] is set to false + def parse_headings(text, project, obj, attr, only_path, options) + return if options[:headings] == false + + text.gsub!(HEADING_RE) do + level, attrs, content = $2.to_i, $3, $4 + item = strip_tags(content).strip + anchor = sanitize_anchor_name(item) + # used for single-file wiki export + anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) + @heading_anchors[anchor] ||= 0 + idx = (@heading_anchors[anchor] += 1) + if idx > 1 + anchor = "#{anchor}-#{idx}" + end + @parsed_headings << [level, anchor, item] + "\n#{content}" + end + end + + MACROS_RE = /( + (!)? # escaping + ( + \{\{ # opening tag + ([\w]+) # macro name + (\(([^\n\r]*?)\))? # optional arguments + ([\n\r].*?[\n\r])? # optional block of text + \}\} # closing tag + ) + )/mx unless const_defined?(:MACROS_RE) + + MACRO_SUB_RE = /( + \{\{ + macro\((\d+)\) + \}\} + )/x unless const_defined?(:MACRO_SUB_RE) + + # Extracts macros from text + def catch_macros(text) + macros = {} + text.gsub!(MACROS_RE) do + all, macro = $1, $4.downcase + if macro_exists?(macro) || all =~ MACRO_SUB_RE + index = macros.size + macros[index] = all + "{{macro(#{index})}}" + else + all + end + end + macros + end + + # Executes and replaces macros in text + def inject_macros(text, obj, macros, execute=true) + text.gsub!(MACRO_SUB_RE) do + all, index = $1, $2.to_i + orig = macros.delete(index) + if execute && orig && orig =~ MACROS_RE + esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip) + if esc.nil? + h(exec_macro(macro, obj, args, block) || all) + else + h(all) + end + elsif orig + h(orig) + else + h(all) + end + end + end + + TOC_RE = /

    \{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE) + + # Renders the TOC with given headings + def replace_toc(text, headings) + text.gsub!(TOC_RE) do + # Keep only the 4 first levels + headings = headings.select{|level, anchor, item| level <= 4} + if headings.empty? + '' + else + div_class = 'toc' + div_class << ' right' if $1 == '>' + div_class << ' left' if $1 == '<' + out = "

    ' * (current - root) + out << '' + end + end + end + + # Same as Rails' simple_format helper without using paragraphs + def simple_format_without_paragraph(text) + text.to_s. + gsub(/\r\n?/, "\n"). # \r\n and \r -> \n + gsub(/\n\n+/, "

    "). # 2+ newline -> 2 br + gsub(/([^\n]\n)(?=[^\n])/, '\1
    '). # 1 newline -> br + html_safe + end + + def lang_options_for_select(blank=true) + (blank ? [["(auto)", ""]] : []) + languages_options + end + + def label_tag_for(name, option_tags = nil, options = {}) + label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") + content_tag("label", label_text) + end + + def labelled_form_for(*args, &proc) + args << {} unless args.last.is_a?(Hash) + options = args.last + if args.first.is_a?(Symbol) + options.merge!(:as => args.shift) + end + options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) + form_for(*args, &proc) + end + + def labelled_fields_for(*args, &proc) + args << {} unless args.last.is_a?(Hash) + options = args.last + options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) + fields_for(*args, &proc) + end + + def labelled_remote_form_for(*args, &proc) + ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2." + args << {} unless args.last.is_a?(Hash) + options = args.last + options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true}) + form_for(*args, &proc) + end + + def error_messages_for(*objects) + html = "" + objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact + errors = objects.map {|o| o.errors.full_messages}.flatten + if errors.any? + html << "
      \n" + errors.each do |error| + html << "
    • #{h error}
    • \n" + end + html << "
    \n" + end + html.html_safe + end + + def delete_link(url, options={}) + options = { + :method => :delete, + :data => {:confirm => l(:text_are_you_sure)}, + :class => 'icon icon-del' + }.merge(options) + + link_to l(:button_delete), url, options + end + + def preview_link(url, form, target='preview', options={}) + content_tag 'a', l(:label_preview), { + :href => "#", + :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|, + :accesskey => accesskey(:preview) + }.merge(options) + end + + def link_to_function(name, function, html_options={}) + content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options)) + end + + # Helper to render JSON in views + def raw_json(arg) + arg.to_json.to_s.gsub('/', '\/').html_safe + end + + def back_url + url = params[:back_url] + if url.nil? && referer = request.env['HTTP_REFERER'] + url = CGI.unescape(referer.to_s) + end + url + end + + def back_url_hidden_field_tag + url = back_url + hidden_field_tag('back_url', url, :id => nil) unless url.blank? + end + + def check_all_links(form_name) + link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + + " | ".html_safe + + link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") + end + + def progress_bar(pcts, options={}) + pcts = [pcts, pcts] unless pcts.is_a?(Array) + pcts = pcts.collect(&:round) + pcts[1] = pcts[1] - pcts[0] + pcts << (100 - pcts[1] - pcts[0]) + width = options[:width] || '100px;' + legend = options[:legend] || '' + content_tag('table', + content_tag('tr', + (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) + + (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) + + (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe) + ), :class => 'progress', :style => "width: #{width};").html_safe + + content_tag('p', legend, :class => 'pourcent').html_safe + end + + def checked_image(checked=true) + if checked + image_tag 'toggle_check.png' + end + end + + def context_menu(url) + unless @context_menu_included + content_for :header_tags do + javascript_include_tag('context_menu') + + stylesheet_link_tag('context_menu') + end + if l(:direction) == 'rtl' + content_for :header_tags do + stylesheet_link_tag('context_menu_rtl') + end + end + @context_menu_included = true + end + javascript_tag "contextMenuInit('#{ url_for(url) }')" + end + + def calendar_for(field_id) + include_calendar_headers_tags + javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });") + end + + def include_calendar_headers_tags + unless @calendar_headers_tags_included + @calendar_headers_tags_included = true + content_for :header_tags do + start_of_week = Setting.start_of_week + start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank? + # Redmine uses 1..7 (monday..sunday) in settings and locales + # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0 + start_of_week = start_of_week.to_i % 7 + + tags = javascript_tag( + "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " + + "showOn: 'button', buttonImageOnly: true, buttonImage: '" + + path_to_image('/images/calendar.png') + + "', showButtonPanel: true};") + jquery_locale = l('jquery.locale', :default => current_language.to_s) + unless jquery_locale == 'en' + tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js") + end + tags + end + end + end + + # Overrides Rails' stylesheet_link_tag with themes and plugins support. + # Examples: + # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults + # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets + # + def stylesheet_link_tag(*sources) + options = sources.last.is_a?(Hash) ? sources.pop : {} + plugin = options.delete(:plugin) + sources = sources.map do |source| + if plugin + "/plugin_assets/#{plugin}/stylesheets/#{source}" + elsif current_theme && current_theme.stylesheets.include?(source) + current_theme.stylesheet_path(source) + else + source + end + end + super sources, options + end + + # Overrides Rails' image_tag with themes and plugins support. + # Examples: + # image_tag('image.png') # => picks image.png from the current theme or defaults + # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets + # + def image_tag(source, options={}) + if plugin = options.delete(:plugin) + source = "/plugin_assets/#{plugin}/images/#{source}" + elsif current_theme && current_theme.images.include?(source) + source = current_theme.image_path(source) + end + super source, options + end + + # Overrides Rails' javascript_include_tag with plugins support + # Examples: + # javascript_include_tag('scripts') # => picks scripts.js from defaults + # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets + # + def javascript_include_tag(*sources) + options = sources.last.is_a?(Hash) ? sources.pop : {} + if plugin = options.delete(:plugin) + sources = sources.map do |source| + if plugin + "/plugin_assets/#{plugin}/javascripts/#{source}" + else + source + end + end + end + super sources, options + end + + def content_for(name, content = nil, &block) + @has_content ||= {} + @has_content[name] = true + super(name, content, &block) + end + + def has_content?(name) + (@has_content && @has_content[name]) || false + end + + def sidebar_content? + has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present? + end + + def view_layouts_base_sidebar_hook_response + @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar) + end + + def email_delivery_enabled? + !!ActionMailer::Base.perform_deliveries + end + + # Returns the avatar image tag for the given +user+ if avatars are enabled + # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe ') + def avatar(user, options = { }) + if Setting.gravatar_enabled? + options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default}) + email = nil + if user.respond_to?(:mail) + email = user.mail + elsif user.to_s =~ %r{<(.+?)>} + email = $1 + end + return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil + else + '' + end + end + + def sanitize_anchor_name(anchor) + if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java' + anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + else + # TODO: remove when ruby1.8 is no longer supported + anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + end + end + + # Returns the javascript tags that are included in the html layout head + def javascript_heads + tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application') + unless User.current.pref.warn_on_leaving_unsaved == '0' + tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });") + end + tags + end + + def favicon + "".html_safe + end + + def robot_exclusion_tag + ''.html_safe + end + + # Returns true if arg is expected in the API response + def include_in_api_response?(arg) + unless @included_in_api_response + param = params[:include] + @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',') + @included_in_api_response.collect!(&:strip) + end + @included_in_api_response.include?(arg.to_s) + end + + # Returns options or nil if nometa param or X-Redmine-Nometa header + # was set in the request + def api_meta(options) + if params[:nometa].present? || request.headers['X-Redmine-Nometa'] + # compatibility mode for activeresource clients that raise + # an error when unserializing an array with attributes + nil + else + options + end + end + + private + + def wiki_helper + helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) + extend helper + return self + end + + def link_to_content_update(text, url_params = {}, html_options = {}) + link_to(text, url_params, html_options) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/01/016b47aef50027cc7a73f1b3fdde3506ed5b4072.svn-base --- a/.svn/pristine/01/016b47aef50027cc7a73f1b3fdde3506ed5b4072.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -class IssueCategory < ActiveRecord::Base - generator_for :name, :start => 'Category 0001' - -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/01/018db29c39216709e43cc1f869161f57aa497455.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/018db29c39216709e43cc1f869161f57aa497455.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,50 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class CustomFieldValue + attr_accessor :custom_field, :customized, :value + + def custom_field_id + custom_field.id + end + + def true? + self.value == '1' + end + + def editable? + custom_field.editable? + end + + def visible? + custom_field.visible? + end + + def required? + custom_field.is_required? + end + + def to_s + value.to_s + end + + def validate_value + custom_field.validate_field_value(value).each do |message| + customized.errors.add(:base, custom_field.name + ' ' + message) + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/01/01b6a7e6fd6dc268ef495a93dfee12a89a78979d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/01b6a7e6fd6dc268ef495a93dfee12a89a78979d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,24 @@ +/* Slovenian initialisation for the jQuery UI date picker plugin. */ +/* Written by Jaka Jancar (jaka@kubje.org). */ +/* c = č, s = š z = ž C = Č S = Š Z = Ž */ +jQuery(function($){ + $.datepicker.regional['sl'] = { + closeText: 'Zapri', + prevText: '<Prejšnji', + nextText: 'Naslednji>', + currentText: 'Trenutni', + monthNames: ['Januar','Februar','Marec','April','Maj','Junij', + 'Julij','Avgust','September','Oktober','November','December'], + monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun', + 'Jul','Avg','Sep','Okt','Nov','Dec'], + dayNames: ['Nedelja','Ponedeljek','Torek','Sreda','Četrtek','Petek','Sobota'], + dayNamesShort: ['Ned','Pon','Tor','Sre','Čet','Pet','Sob'], + dayNamesMin: ['Ne','Po','To','Sr','Če','Pe','So'], + weekHeader: 'Teden', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['sl']); +}); diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/01/01df356dd5562f68f76338bddccfd73e2d039ee5.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/01df356dd5562f68f76338bddccfd73e2d039ee5.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,114 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Acts + module Attachable + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + def acts_as_attachable(options = {}) + cattr_accessor :attachable_options + self.attachable_options = {} + attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym + attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym + + has_many :attachments, options.merge(:as => :container, + :order => "#{Attachment.table_name}.created_on ASC, #{Attachment.table_name}.id ASC", + :dependent => :destroy) + send :include, Redmine::Acts::Attachable::InstanceMethods + before_save :attach_saved_attachments + end + end + + module InstanceMethods + def self.included(base) + base.extend ClassMethods + end + + def attachments_visible?(user=User.current) + (respond_to?(:visible?) ? visible?(user) : true) && + user.allowed_to?(self.class.attachable_options[:view_permission], self.project) + end + + def attachments_deletable?(user=User.current) + (respond_to?(:visible?) ? visible?(user) : true) && + user.allowed_to?(self.class.attachable_options[:delete_permission], self.project) + end + + def saved_attachments + @saved_attachments ||= [] + end + + def unsaved_attachments + @unsaved_attachments ||= [] + end + + def save_attachments(attachments, author=User.current) + if attachments.is_a?(Hash) + attachments = attachments.stringify_keys + attachments = attachments.to_a.sort {|a, b| + if a.first.to_i > 0 && b.first.to_i > 0 + a.first.to_i <=> b.first.to_i + elsif a.first.to_i > 0 + 1 + elsif b.first.to_i > 0 + -1 + else + a.first <=> b.first + end + } + attachments = attachments.map(&:last) + end + if attachments.is_a?(Array) + attachments.each do |attachment| + a = nil + if file = attachment['file'] + next unless file.size > 0 + a = Attachment.create(:file => file, :author => author) + elsif token = attachment['token'] + a = Attachment.find_by_token(token) + next unless a + a.filename = attachment['filename'] unless attachment['filename'].blank? + a.content_type = attachment['content_type'] + end + next unless a + a.description = attachment['description'].to_s.strip + if a.new_record? + unsaved_attachments << a + else + saved_attachments << a + end + end + end + {:files => saved_attachments, :unsaved => unsaved_attachments} + end + + def attach_saved_attachments + saved_attachments.each do |attachment| + self.attachments << attachment + end + end + + module ClassMethods + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/01/01e977490126a0e666397a535bfda934cac273ac.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/01/01e977490126a0e666397a535bfda934cac273ac.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,435 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class ScmFetchError < Exception; end + +class Repository < ActiveRecord::Base + include Redmine::Ciphering + include Redmine::SafeAttributes + + # Maximum length for repository identifiers + IDENTIFIER_MAX_LENGTH = 255 + + belongs_to :project + has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC" + has_many :filechanges, :class_name => 'Change', :through => :changesets + + serialize :extra_info + + before_save :check_default + + # Raw SQL to delete changesets and changes in the database + # has_many :changesets, :dependent => :destroy is too slow for big repositories + before_destroy :clear_changesets + + validates_length_of :password, :maximum => 255, :allow_nil => true + validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true + validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? } + validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true + validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph) + # donwcase letters, digits, dashes, underscores but not digits only + validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :allow_blank => true + # Checks if the SCM is enabled when creating a repository + validate :repo_create_validation, :on => :create + + safe_attributes 'identifier', + 'login', + 'password', + 'path_encoding', + 'log_encoding', + 'is_default' + + safe_attributes 'url', + :if => lambda {|repository, user| repository.new_record?} + + def repo_create_validation + unless Setting.enabled_scm.include?(self.class.name.demodulize) + errors.add(:type, :invalid) + end + end + + def self.human_attribute_name(attribute_key_name, *args) + attr_name = attribute_key_name.to_s + if attr_name == "log_encoding" + attr_name = "commit_logs_encoding" + end + super(attr_name, *args) + end + + # Removes leading and trailing whitespace + def url=(arg) + write_attribute(:url, arg ? arg.to_s.strip : nil) + end + + # Removes leading and trailing whitespace + def root_url=(arg) + write_attribute(:root_url, arg ? arg.to_s.strip : nil) + end + + def password + read_ciphered_attribute(:password) + end + + def password=(arg) + write_ciphered_attribute(:password, arg) + end + + def scm_adapter + self.class.scm_adapter_class + end + + def scm + unless @scm + @scm = self.scm_adapter.new(url, root_url, + login, password, path_encoding) + if root_url.blank? && @scm.root_url.present? + update_attribute(:root_url, @scm.root_url) + end + end + @scm + end + + def scm_name + self.class.scm_name + end + + def name + if identifier.present? + identifier + elsif is_default? + l(:field_repository_is_default) + else + scm_name + end + end + + def identifier=(identifier) + super unless identifier_frozen? + end + + def identifier_frozen? + errors[:identifier].blank? && !(new_record? || identifier.blank?) + end + + def identifier_param + if is_default? + nil + elsif identifier.present? + identifier + else + id.to_s + end + end + + def <=>(repository) + if is_default? + -1 + elsif repository.is_default? + 1 + else + identifier.to_s <=> repository.identifier.to_s + end + end + + def self.find_by_identifier_param(param) + if param.to_s =~ /^\d+$/ + find_by_id(param) + else + find_by_identifier(param) + end + end + + def merge_extra_info(arg) + h = extra_info || {} + return h if arg.nil? + h.merge!(arg) + write_attribute(:extra_info, h) + end + + def report_last_commit + true + end + + def supports_cat? + scm.supports_cat? + end + + def supports_annotate? + scm.supports_annotate? + end + + def supports_all_revisions? + true + end + + def supports_directory_revisions? + false + end + + def supports_revision_graph? + false + end + + def entry(path=nil, identifier=nil) + scm.entry(path, identifier) + end + + def entries(path=nil, identifier=nil) + entries = scm.entries(path, identifier) + load_entries_changesets(entries) + entries + end + + def branches + scm.branches + end + + def tags + scm.tags + end + + def default_branch + nil + end + + def properties(path, identifier=nil) + scm.properties(path, identifier) + end + + def cat(path, identifier=nil) + scm.cat(path, identifier) + end + + def diff(path, rev, rev_to) + scm.diff(path, rev, rev_to) + end + + def diff_format_revisions(cs, cs_to, sep=':') + text = "" + text << cs_to.format_identifier + sep if cs_to + text << cs.format_identifier if cs + text + end + + # Returns a path relative to the url of the repository + def relative_path(path) + path + end + + # Finds and returns a revision with a number or the beginning of a hash + def find_changeset_by_name(name) + return nil if name.blank? + s = name.to_s + changesets.find(:first, :conditions => (s.match(/^\d*$/) ? + ["revision = ?", s] : ["revision LIKE ?", s + '%'])) + end + + def latest_changeset + @latest_changeset ||= changesets.find(:first) + end + + # Returns the latest changesets for +path+ + # Default behaviour is to search in cached changesets + def latest_changesets(path, rev, limit=10) + if path.blank? + changesets.find( + :all, + :include => :user, + :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC", + :limit => limit) + else + filechanges.find( + :all, + :include => {:changeset => :user}, + :conditions => ["path = ?", path.with_leading_slash], + :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC", + :limit => limit + ).collect(&:changeset) + end + end + + def scan_changesets_for_issue_ids + self.changesets.each(&:scan_comment_for_issue_ids) + end + + # Returns an array of committers usernames and associated user_id + def committers + @committers ||= Changeset.connection.select_rows( + "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}") + end + + # Maps committers username to a user ids + def committer_ids=(h) + if h.is_a?(Hash) + committers.each do |committer, user_id| + new_user_id = h[committer] + if new_user_id && (new_user_id.to_i != user_id.to_i) + new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil) + Changeset.update_all( + "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", + ["repository_id = ? AND committer = ?", id, committer]) + end + end + @committers = nil + @found_committer_users = nil + true + else + false + end + end + + # Returns the Redmine User corresponding to the given +committer+ + # It will return nil if the committer is not yet mapped and if no User + # with the same username or email was found + def find_committer_user(committer) + unless committer.blank? + @found_committer_users ||= {} + return @found_committer_users[committer] if @found_committer_users.has_key?(committer) + + user = nil + c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user) + if c && c.user + user = c.user + elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/ + username, email = $1.strip, $3 + u = User.find_by_login(username) + u ||= User.find_by_mail(email) unless email.blank? + user = u + end + @found_committer_users[committer] = user + user + end + end + + def repo_log_encoding + encoding = log_encoding.to_s.strip + encoding.blank? ? 'UTF-8' : encoding + end + + # Fetches new changesets for all repositories of active projects + # Can be called periodically by an external script + # eg. ruby script/runner "Repository.fetch_changesets" + def self.fetch_changesets + Project.active.has_module(:repository).all.each do |project| + project.repositories.each do |repository| + begin + repository.fetch_changesets + rescue Redmine::Scm::Adapters::CommandFailed => e + logger.error "scm: error during fetching changesets: #{e.message}" + end + end + end + end + + # scan changeset comments to find related and fixed issues for all repositories + def self.scan_changesets_for_issue_ids + find(:all).each(&:scan_changesets_for_issue_ids) + end + + def self.scm_name + 'Abstract' + end + + def self.available_scm + subclasses.collect {|klass| [klass.scm_name, klass.name]} + end + + def self.factory(klass_name, *args) + klass = "Repository::#{klass_name}".constantize + klass.new(*args) + rescue + nil + end + + def self.scm_adapter_class + nil + end + + def self.scm_command + ret = "" + begin + ret = self.scm_adapter_class.client_command if self.scm_adapter_class + rescue Exception => e + logger.error "scm: error during get command: #{e.message}" + end + ret + end + + def self.scm_version_string + ret = "" + begin + ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class + rescue Exception => e + logger.error "scm: error during get version string: #{e.message}" + end + ret + end + + def self.scm_available + ret = false + begin + ret = self.scm_adapter_class.client_available if self.scm_adapter_class + rescue Exception => e + logger.error "scm: error during get scm available: #{e.message}" + end + ret + end + + def set_as_default? + new_record? && project && !Repository.first(:conditions => {:project_id => project.id}) + end + + protected + + def check_default + if !is_default? && set_as_default? + self.is_default = true + end + if is_default? && is_default_changed? + Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id]) + end + end + + def load_entries_changesets(entries) + if entries + entries.each do |entry| + if entry.lastrev && entry.lastrev.identifier + entry.changeset = find_changeset_by_name(entry.lastrev.identifier) + end + end + end + end + + private + + # Deletes repository data + def clear_changesets + cs = Changeset.table_name + ch = Change.table_name + ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}" + cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}" + + connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})") + connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})") + connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})") + connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}") + clear_extra_info_of_changesets + end + + def clear_extra_info_of_changesets + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/02/020539cf91037b37b7739ed19a56c058529a8860.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/02/020539cf91037b37b7739ed19a56c058529a8860.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,45 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class WorkflowPermission < WorkflowRule + validates_inclusion_of :rule, :in => %w(readonly required) + validate :validate_field_name + + # Replaces the workflow permissions for the given tracker and role + # + # Example: + # WorkflowPermission.replace_permissions role, tracker, {'due_date' => {'1' => 'readonly', '2' => 'required'}} + def self.replace_permissions(tracker, role, permissions) + destroy_all(:tracker_id => tracker.id, :role_id => role.id) + + permissions.each { |field, rule_by_status_id| + rule_by_status_id.each { |status_id, rule| + if rule.present? + WorkflowPermission.create(:role_id => role.id, :tracker_id => tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule) + end + } + } + end + + protected + + def validate_field_name + unless Tracker::CORE_FIELDS_ALL.include?(field_name) || field_name.to_s.match(/^\d+$/) + errors.add :field_name, :invalid + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/03/03387de6b294853cc867f560276231ad9e9e4136.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/03/03387de6b294853cc867f560276231ad9e9e4136.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,69 @@ +== Redmine upgrade + +Redmine - project management software +Copyright (C) 2006-2012 Jean-Philippe Lang +http://www.redmine.org/ + + +== Upgrading + +1. Uncompress the program archive in a new directory + +2. Copy your database settings (RAILS_ROOT/config/database.yml) + and your configuration file (RAILS_ROOT/config/configuration.yml) + into the new config directory + Note: before Redmine 1.2, SMTP configuration was stored in + config/email.yml. It should now be stored in config/configuration.yml. + +3. Copy the RAILS_ROOT/files directory content into your new installation + This directory contains all the attached files. + +4. Copy the folders of the installed plugins and themes into new installation + Plugins must be stored in the [redmine_root]/plugins directory + Themes must be stored in the [redmine_root]/public/themes directory + + WARNING: plugins from your previous Redmine version may not be compatible + with the Redmine version you're upgrading to. + +5. Install the required gems by running: + bundle install --without development test + + If ImageMagick is not installed on your system, you should skip the installation + of the rmagick gem using: + bundle install --without development test rmagick + +6. Generate a session store secret + + Redmine stores session data in cookies by default, which requires + a secret to be generated. Under the new application directory run: + rake generate_secret_token + + DO NOT REPLACE OR EDIT ANY OTHER FILES. + +7. Migrate your database + + If you are upgrading to Rails 2.3.14 as part of this migration, you + need to upgrade the plugin migrations before running the plugin migrations + using: + rake db:migrate:upgrade_plugin_migrations RAILS_ENV="production" + + Please make a backup before doing this! Under the new application + directory run: + rake db:migrate RAILS_ENV="production" + + If you have installed any plugins, you should also run their database + migrations using: + rake db:migrate_plugins RAILS_ENV="production" + +8. Clear the cache and the existing sessions by running: + rake tmp:cache:clear + rake tmp:sessions:clear + +9. Restart the application server (e.g. mongrel, thin, passenger) + +10. Finally go to "Administration -> Roles & permissions" to check/set permissions + for new features, if any + +== References + +* http://www.redmine.org/wiki/redmine/RedmineUpgrade diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/03/03803ec02697cfe8f89b59eb47e58dc9312a0502.svn-base --- a/.svn/pristine/03/03803ec02697cfe8f89b59eb47e58dc9312a0502.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,156 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -require File.expand_path('../../../../test_helper', __FILE__) - -class Redmine::UnifiedDiffTest < ActiveSupport::TestCase - - def setup - end - - def test_subversion_diff - diff = Redmine::UnifiedDiff.new(read_diff_fixture('subversion.diff')) - # number of files - assert_equal 4, diff.size - assert diff.detect {|file| file.file_name =~ %r{^config/settings.yml}} - end - - def test_truncate_diff - diff = Redmine::UnifiedDiff.new(read_diff_fixture('subversion.diff'), :max_lines => 20) - assert_equal 2, diff.size - end - - def test_inline_partials - diff = Redmine::UnifiedDiff.new(read_diff_fixture('partials.diff')) - assert_equal 1, diff.size - diff = diff.first - assert_equal 43, diff.size - - assert_equal [51, -1], diff[0].offsets - assert_equal [51, -1], diff[1].offsets - assert_equal 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', diff[0].html_line - assert_equal 'Lorem ipsum dolor sit amet, consectetur adipiscing xx', diff[1].html_line - - assert_nil diff[2].offsets - assert_equal 'Praesent et sagittis dui. Vivamus ac diam diam', diff[2].html_line - - assert_equal [0, -14], diff[3].offsets - assert_equal [0, -14], diff[4].offsets - assert_equal 'Ut sed auctor justo', diff[3].html_line - assert_equal 'xxx auctor justo', diff[4].html_line - - assert_equal [13, -19], diff[6].offsets - assert_equal [13, -19], diff[7].offsets - - assert_equal [24, -8], diff[9].offsets - assert_equal [24, -8], diff[10].offsets - - assert_equal [37, -1], diff[12].offsets - assert_equal [37, -1], diff[13].offsets - - assert_equal [0, -38], diff[15].offsets - assert_equal [0, -38], diff[16].offsets - end - - def test_side_by_side_partials - diff = Redmine::UnifiedDiff.new(read_diff_fixture('partials.diff'), :type => 'sbs') - assert_equal 1, diff.size - diff = diff.first - assert_equal 32, diff.size - - assert_equal [51, -1], diff[0].offsets - assert_equal 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', diff[0].html_line_left - assert_equal 'Lorem ipsum dolor sit amet, consectetur adipiscing xx', diff[0].html_line_right - - assert_nil diff[1].offsets - assert_equal 'Praesent et sagittis dui. Vivamus ac diam diam', diff[1].html_line_left - assert_equal 'Praesent et sagittis dui. Vivamus ac diam diam', diff[1].html_line_right - - assert_equal [0, -14], diff[2].offsets - assert_equal 'Ut sed auctor justo', diff[2].html_line_left - assert_equal 'xxx auctor justo', diff[2].html_line_right - - assert_equal [13, -19], diff[4].offsets - assert_equal [24, -8], diff[6].offsets - assert_equal [37, -1], diff[8].offsets - assert_equal [0, -38], diff[10].offsets - - end - - def test_line_starting_with_dashes - diff = Redmine::UnifiedDiff.new(<<-DIFF ---- old.txt Wed Nov 11 14:24:58 2009 -+++ new.txt Wed Nov 11 14:25:02 2009 -@@ -1,8 +1,4 @@ --Lines that starts with dashes: -- -------------------------- ---- file.c -------------------------- -+A line that starts with dashes: - - and removed. - -@@ -23,4 +19,4 @@ - - - --Another chunk of change -+Another chunk of changes - -DIFF - ) - assert_equal 1, diff.size - end - - def test_one_line_new_files - diff = Redmine::UnifiedDiff.new(<<-DIFF -diff -r 000000000000 -r ea98b14f75f0 README1 ---- /dev/null -+++ b/README1 -@@ -0,0 +1,1 @@ -+test1 -diff -r 000000000000 -r ea98b14f75f0 README2 ---- /dev/null -+++ b/README2 -@@ -0,0 +1,1 @@ -+test2 -diff -r 000000000000 -r ea98b14f75f0 README3 ---- /dev/null -+++ b/README3 -@@ -0,0 +1,3 @@ -+test4 -+test5 -+test6 -diff -r 000000000000 -r ea98b14f75f0 README4 ---- /dev/null -+++ b/README4 -@@ -0,0 +1,3 @@ -+test4 -+test5 -+test6 -DIFF - ) - assert_equal 4, diff.size - end - - private - - def read_diff_fixture(filename) - File.new(File.join(File.dirname(__FILE__), '/../../../fixtures/diffs', filename)).read - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/03/03fba5f64f9a20698931cdb93696c59c7041a580.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/03/03fba5f64f9a20698931cdb93696c59c7041a580.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,25 @@ +/* French initialisation for the jQuery UI date picker plugin. */ +/* Written by Keith Wood (kbwood{at}iinet.com.au), + Stéphane Nahmani (sholby@sholby.net), + Stéphane Raimbault */ +jQuery(function($){ + $.datepicker.regional['fr'] = { + closeText: 'Fermer', + prevText: 'Précédent', + nextText: 'Suivant', + currentText: 'Aujourd\'hui', + monthNames: ['Janvier','Février','Mars','Avril','Mai','Juin', + 'Juillet','Août','Septembre','Octobre','Novembre','Décembre'], + monthNamesShort: ['Janv.','Févr.','Mars','Avril','Mai','Juin', + 'Juil.','Août','Sept.','Oct.','Nov.','Déc.'], + dayNames: ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'], + dayNamesShort: ['Dim.','Lun.','Mar.','Mer.','Jeu.','Ven.','Sam.'], + dayNamesMin: ['D','L','M','M','J','V','S'], + weekHeader: 'Sem.', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['fr']); +}); diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/04/0406b4818e714c1d680ae173e23dbc29be7a3556.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/04/0406b4818e714c1d680ae173e23dbc29be7a3556.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1100 @@ +# Polish translations for Ruby on Rails +# by Jacek Becela (jacek.becela@gmail.com, http://github.com/ncr) +# by Krzysztof Podejma (kpodejma@customprojects.pl, http://www.customprojects.pl) + +pl: + number: + format: + separator: "," + delimiter: " " + precision: 2 + currency: + format: + format: "%n %u" + unit: "PLN" + percentage: + format: + delimiter: "" + precision: + format: + delimiter: "" + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "B" + other: "B" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + + direction: ltr + date: + formats: + default: "%Y-%m-%d" + short: "%d %b" + long: "%d %B %Y" + + day_names: [Niedziela, PoniedziaÅ‚ek, Wtorek, Åšroda, Czwartek, PiÄ…tek, Sobota] + abbr_day_names: [nie, pon, wto, Å›ro, czw, pia, sob] + + month_names: [~, StyczeÅ„, Luty, Marzec, KwiecieÅ„, Maj, Czerwiec, Lipiec, SierpieÅ„, WrzesieÅ„, Październik, Listopad, GrudzieÅ„] + abbr_month_names: [~, sty, lut, mar, kwi, maj, cze, lip, sie, wrz, paź, lis, gru] + order: + - :year + - :month + - :day + + time: + formats: + default: "%a, %d %b %Y, %H:%M:%S %z" + time: "%H:%M" + short: "%d %b, %H:%M" + long: "%d %B %Y, %H:%M" + am: "przed poÅ‚udniem" + pm: "po poÅ‚udniu" + + datetime: + distance_in_words: + half_a_minute: "pół minuty" + less_than_x_seconds: + one: "mniej niż sekundÄ™" + few: "mniej niż %{count} sekundy" + other: "mniej niż %{count} sekund" + x_seconds: + one: "sekundÄ™" + few: "%{count} sekundy" + other: "%{count} sekund" + less_than_x_minutes: + one: "mniej niż minutÄ™" + few: "mniej niż %{count} minuty" + other: "mniej niż %{count} minut" + x_minutes: + one: "minutÄ™" + few: "%{count} minuty" + other: "%{count} minut" + about_x_hours: + one: "okoÅ‚o godziny" + other: "okoÅ‚o %{count} godzin" + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "1 dzieÅ„" + other: "%{count} dni" + about_x_months: + one: "okoÅ‚o miesiÄ…ca" + other: "okoÅ‚o %{count} miesiÄ™cy" + x_months: + one: "1 miesiÄ…c" + few: "%{count} miesiÄ…ce" + other: "%{count} miesiÄ™cy" + about_x_years: + one: "okoÅ‚o roku" + other: "okoÅ‚o %{count} lat" + over_x_years: + one: "ponad rok" + few: "ponad %{count} lata" + other: "ponad %{count} lat" + almost_x_years: + one: "prawie rok" + other: "prawie %{count} lata" + + activerecord: + errors: + template: + header: + one: "%{model} nie zostaÅ‚ zachowany z powodu jednego błędu" + other: "%{model} nie zostaÅ‚ zachowany z powodu %{count} błędów" + body: "Błędy dotyczÄ… nastÄ™pujÄ…cych pól:" + messages: + inclusion: "nie znajduje siÄ™ na liÅ›cie dopuszczalnych wartoÅ›ci" + exclusion: "znajduje siÄ™ na liÅ›cie zabronionych wartoÅ›ci" + invalid: "jest nieprawidÅ‚owe" + confirmation: "nie zgadza siÄ™ z potwierdzeniem" + accepted: "musi być zaakceptowane" + empty: "nie może być puste" + blank: "nie może być puste" + too_long: "jest za dÅ‚ugie (maksymalnie %{count} znaków)" + too_short: "jest za krótkie (minimalnie %{count} znaków)" + wrong_length: "jest nieprawidÅ‚owej dÅ‚ugoÅ›ci (powinna wynosić %{count} znaków)" + taken: "jest już zajÄ™te" + not_a_number: "nie jest liczbÄ…" + greater_than: "musi być wiÄ™ksze niż %{count}" + greater_than_or_equal_to: "musi być wiÄ™ksze lub równe %{count}" + equal_to: "musi być równe %{count}" + less_than: "musi być mniejsze niż %{count}" + less_than_or_equal_to: "musi być mniejsze lub równe %{count}" + odd: "musi być nieparzyste" + even: "musi być parzyste" + greater_than_start_date: "musi być wiÄ™ksze niż poczÄ…tkowa data" + not_same_project: "nie należy do tego samego projektu" + circular_dependency: "Ta relacja może wytworzyć koÅ‚owÄ… zależność" + cant_link_an_issue_with_a_descendant: "Zagadnienie nie może zostać powiÄ…zane z jednym z wÅ‚asnych podzagadnieÅ„" + + support: + array: + sentence_connector: "i" + skip_last_comma: true + + # Keep this line in order to avoid problems with Windows Notepad UTF-8 EF-BB-BFidea... + # Best regards from Lublin@Poland :-) + # PL translation by Mariusz@Olejnik.net, + actionview_instancetag_blank_option: ProszÄ™ wybierz + + button_activate: Aktywuj + button_add: Dodaj + button_annotate: Adnotuj + button_apply: Ustaw + button_archive: Archiwizuj + button_back: Wstecz + button_cancel: Anuluj + button_change: ZmieÅ„ + button_change_password: ZmieÅ„ hasÅ‚o + button_check_all: Zaznacz wszystko + button_clear: Wyczyść + button_configure: Konfiguruj + button_copy: Kopia + button_create: Stwórz + button_delete: UsuÅ„ + button_download: Pobierz + button_edit: Edytuj + button_list: Lista + button_lock: Zablokuj + button_log_time: Dziennik + button_login: Login + button_move: PrzenieÅ› + button_quote: Cytuj + button_rename: ZmieÅ„ nazwÄ™ + button_reply: Odpowiedz + button_reset: Resetuj + button_rollback: Przywróć do tej wersji + button_save: Zapisz + button_sort: Sortuj + button_submit: WyÅ›lij + button_test: Testuj + button_unarchive: Przywróć z archiwum + button_uncheck_all: Odznacz wszystko + button_unlock: Odblokuj + button_unwatch: Nie obserwuj + button_update: Uaktualnij + button_view: Pokaż + button_watch: Obserwuj + default_activity_design: Projektowanie + default_activity_development: Rozwój + default_doc_category_tech: Dokumentacja techniczna + default_doc_category_user: Dokumentacja użytkownika + default_issue_status_in_progress: W Toku + default_issue_status_closed: ZamkniÄ™ty + default_issue_status_feedback: Odpowiedź + default_issue_status_new: Nowy + default_issue_status_rejected: Odrzucony + default_issue_status_resolved: RozwiÄ…zany + default_priority_high: Wysoki + default_priority_immediate: Natychmiastowy + default_priority_low: Niski + default_priority_normal: Normalny + default_priority_urgent: Pilny + default_role_developer: Programista + default_role_manager: Kierownik + default_role_reporter: Wprowadzajacy + default_tracker_bug: Błąd + default_tracker_feature: Zadanie + default_tracker_support: Wsparcie + enumeration_activities: DziaÅ‚ania (Å›ledzenie czasu) + enumeration_doc_categories: Kategorie dokumentów + enumeration_issue_priorities: Priorytety zagadnieÅ„ + error_can_t_load_default_data: "DomyÅ›lna konfiguracja nie może być zaÅ‚adowana: %{value}" + error_issue_not_found_in_project: 'Zaganienie nie zostaÅ‚o znalezione lub nie należy do tego projektu' + error_scm_annotate: "Wpis nie istnieje lub nie można do niego dodawać adnotacji." + error_scm_command_failed: "WystÄ…piÅ‚ błąd przy próbie dostÄ™pu do repozytorium: %{value}" + error_scm_not_found: "Obiekt lub wersja nie zostaÅ‚y znalezione w repozytorium." + field_account: Konto + field_activity: Aktywność + field_admin: Administrator + field_assignable: Zagadnienia mogÄ… być przypisane do tej roli + field_assigned_to: Przydzielony do + field_attr_firstname: ImiÄ™ atrybut + field_attr_lastname: Nazwisko atrybut + field_attr_login: Login atrybut + field_attr_mail: Email atrybut + field_auth_source: Tryb identyfikacji + field_author: Autor + field_base_dn: Base DN + field_category: Kategoria + field_column_names: Nazwy kolumn + field_comments: Komentarz + field_comments_sorting: Pokazuj komentarze + field_created_on: Stworzone + field_default_value: DomyÅ›lny + field_delay: Opóźnienie + field_description: Opis + field_done_ratio: "% Wykonane" + field_downloads: PobraÅ„ + field_due_date: Data oddania + field_effective_date: Data + field_estimated_hours: Szacowany czas + field_field_format: Format + field_filename: Plik + field_filesize: Rozmiar + field_firstname: ImiÄ™ + field_fixed_version: Wersja docelowa + field_hide_mail: Ukryj mój adres email + field_homepage: Strona www + field_host: Host + field_hours: Godzin + field_identifier: Identifikator + field_is_closed: Zagadnienie zamkniÄ™te + field_is_default: DomyÅ›lny status + field_is_filter: Atrybut filtrowania + field_is_for_all: Dla wszystkich projektów + field_is_in_roadmap: Zagadnienie pokazywane na mapie + field_is_public: Publiczny + field_is_required: Wymagane + field_issue: Zagadnienie + field_issue_to: PowiÄ…zania zagadnienia + field_language: JÄ™zyk + field_last_login_on: Ostatnie połączenie + field_lastname: Nazwisko + field_login: Login + field_mail: Email + field_mail_notification: Powiadomienia Email + field_max_length: Maksymalna dÅ‚ugość + field_min_length: Minimalna dÅ‚ugość + field_name: Nazwa + field_new_password: Nowe hasÅ‚o + field_notes: Notatki + field_onthefly: Tworzenie użytkownika w locie + field_parent: Nadprojekt + field_parent_title: Strona rodzica + field_password: HasÅ‚o + field_password_confirmation: Potwierdzenie + field_port: Port + field_possible_values: Możliwe wartoÅ›ci + field_priority: Priorytet + field_project: Projekt + field_redirect_existing_links: Przekierowanie istniejÄ…cych odnoÅ›ników + field_regexp: Wyrażenie regularne + field_role: Rola + field_searchable: Przeszukiwalne + field_spent_on: Data + field_start_date: Start + field_start_page: Strona startowa + field_status: Status + field_subject: Temat + field_subproject: Podprojekt + field_summary: Podsumowanie + field_time_zone: Strefa czasowa + field_title: TytuÅ‚ + field_tracker: Typ zagadnienia + field_type: Typ + field_updated_on: Zmienione + field_url: URL + field_user: Użytkownik + field_value: Wartość + field_version: Wersja + field_vf_personnel: Personel + field_vf_watcher: Obserwator + general_csv_decimal_separator: ',' + general_csv_encoding: UTF-8 + general_csv_separator: ';' + general_first_day_of_week: '1' + general_lang_name: 'Polski' + general_pdf_encoding: UTF-8 + general_text_No: 'Nie' + general_text_Yes: 'Tak' + general_text_no: 'nie' + general_text_yes: 'tak' + gui_validation_error: 1 błąd + gui_validation_error_plural234: "%{count} błędy" + gui_validation_error_plural5: "%{count} błędów" + gui_validation_error_plural: "%{count} błędów" + label_activity: Aktywność + label_add_another_file: Dodaj kolejny plik + label_add_note: Dodaj notatkÄ™ + label_added: dodane + label_added_time_by: "Dodane przez %{author} %{age} temu" + label_administration: Administracja + label_age: Wiek + label_ago: dni temu + label_all: wszystko + label_all_time: caÅ‚y czas + label_all_words: Wszystkie sÅ‚owa + label_and_its_subprojects: "%{value} i podprojekty" + label_applied_status: Stosowany status + label_assigned_to_me_issues: Zagadnienia przypisane do mnie + label_associated_revisions: Skojarzone rewizje + label_attachment: Plik + label_attachment_delete: UsuÅ„ plik + label_attachment_new: Nowy plik + label_attachment_plural: Pliki + label_attribute: Atrybut + label_attribute_plural: Atrybuty + label_auth_source: Tryb identyfikacji + label_auth_source_new: Nowy tryb identyfikacji + label_auth_source_plural: Tryby identyfikacji + label_authentication: Identyfikacja + label_blocked_by: zablokowane przez + label_blocks: blokuje + label_board: Forum + label_board_new: Nowe forum + label_board_plural: Fora + label_boolean: Wartość logiczna + label_browse: PrzeglÄ…d + label_bulk_edit_selected_issues: Zbiorowa edycja zagadnieÅ„ + label_calendar: Kalendarz + label_change_plural: Zmiany + label_change_properties: ZmieÅ„ wÅ‚aÅ›ciwoÅ›ci + label_change_status: Status zmian + label_change_view_all: Pokaż wszystkie zmiany + label_changes_details: Szczegóły wszystkich zmian + label_changeset_plural: Zestawienia zmian + label_chronological_order: W kolejnoÅ›ci chronologicznej + label_closed_issues: zamkniÄ™te + label_closed_issues_plural234: zamkniÄ™te + label_closed_issues_plural5: zamkniÄ™te + label_closed_issues_plural: zamkniÄ™te + label_x_open_issues_abbr_on_total: + zero: 0 open / %{total} + one: 1 open / %{total} + other: "%{count} open / %{total}" + label_x_open_issues_abbr: + zero: 0 open + one: 1 open + other: "%{count} open" + label_x_closed_issues_abbr: + zero: 0 closed + one: 1 closed + other: "%{count} closed" + label_comment: Komentarz + label_comment_add: Dodaj komentarz + label_comment_added: Komentarz dodany + label_comment_delete: UsuÅ„ komentarze + label_comment_plural234: Komentarze + label_comment_plural5: Komentarze + label_comment_plural: Komentarze + label_x_comments: + zero: no comments + one: 1 comment + other: "%{count} comments" + label_commits_per_author: Zatwierdzenia wedÅ‚ug autorów + label_commits_per_month: Zatwierdzenia wedÅ‚ug miesiÄ™cy + label_confirmation: Potwierdzenie + label_contains: zawiera + label_copied: skopiowano + label_copy_workflow_from: Kopiuj przepÅ‚yw z + label_current_status: Obecny status + label_current_version: Obecna wersja + label_custom_field: Dowolne pole + label_custom_field_new: Nowe dowolne pole + label_custom_field_plural: Dowolne pola + label_date: Data + label_date_from: Z + label_date_range: Zakres datowy + label_date_to: Do + label_day_plural: dni + label_default: DomyÅ›lne + label_default_columns: DomyÅ›lne kolumny + label_deleted: usuniÄ™te + label_details: Szczegóły + label_diff_inline: w linii + label_diff_side_by_side: obok siebie + label_disabled: zablokowany + label_display_per_page: "Na stronÄ™: %{value}" + label_document: Dokument + label_document_added: Dodano dokument + label_document_new: Nowy dokument + label_document_plural: Dokumenty + label_download: "%{count} Pobranie" + label_download_plural234: "%{count} Pobrania" + label_download_plural5: "%{count} PobraÅ„" + label_download_plural: "%{count} Pobrania" + label_downloads_abbr: Pobieranie + label_duplicated_by: zduplikowane przez + label_duplicates: duplikuje + label_end_to_end: koniec do koÅ„ca + label_end_to_start: koniec do poczÄ…tku + label_enumeration_new: Nowa wartość + label_enumerations: Wyliczenia + label_environment: Åšrodowisko + label_equals: równa siÄ™ + label_example: PrzykÅ‚ad + label_export_to: Eksportuj do + label_f_hour: "%{value} godzina" + label_f_hour_plural: "%{value} godzin" + label_feed_plural: Ilość RSS + label_feeds_access_key_created_on: "Klucz dostÄ™pu RSS stworzony %{value} dni temu" + label_file_added: Dodano plik + label_file_plural: Pliki + label_filter_add: Dodaj filtr + label_filter_plural: Filtry + label_float: Liczba rzeczywista + label_follows: nastÄ™puje po + label_gantt: Gantt + label_general: Ogólne + label_generate_key: Wygeneruj klucz + label_help: Pomoc + label_history: Historia + label_home: Główna + label_in: w + label_in_less_than: mniejsze niż + label_in_more_than: wiÄ™ksze niż + label_incoming_emails: PrzychodzÄ…ca poczta elektroniczna + label_index_by_date: Indeks wg daty + label_index_by_title: Indeks + label_information: Informacja + label_information_plural: Informacje + label_integer: Liczba caÅ‚kowita + label_internal: WewnÄ™trzny + label_issue: Zagadnienie + label_issue_added: Dodano zagadnienie + label_issue_category: Kategoria zagadnienia + label_issue_category_new: Nowa kategoria + label_issue_category_plural: Kategorie zagadnieÅ„ + label_issue_new: Nowe zagadnienie + label_issue_plural: Zagadnienia + label_issue_status: Status zagadnienia + label_issue_status_new: Nowy status + label_issue_status_plural: Statusy zagadnieÅ„ + label_issue_tracking: Åšledzenie zagadnieÅ„ + label_issue_updated: Uaktualniono zagadnienie + label_issue_view_all: Zobacz wszystkie zagadnienia + label_issue_watchers: Obserwatorzy + label_issues_by: "Zagadnienia wprowadzone przez %{value}" + label_jump_to_a_project: Skocz do projektu... + label_language_based: Na podstawie jÄ™zyka + label_last_changes: "ostatnie %{count} zmian" + label_last_login: Ostatnie połączenie + label_last_month: ostatni miesiÄ…c + label_last_n_days: "ostatnie %{count} dni" + label_last_week: ostatni tydzieÅ„ + label_latest_revision: Najnowsza rewizja + label_latest_revision_plural: Najnowsze rewizje + label_ldap_authentication: Autoryzacja LDAP + label_less_than_ago: dni mniej + label_list: Lista + label_loading: Åadowanie... + label_logged_as: Zalogowany jako + label_login: Login + label_logout: Wylogowanie + label_max_size: Maksymalny rozmiar + label_me: ja + label_member: Uczestnik + label_member_new: Nowy uczestnik + label_member_plural: Uczestnicy + label_message_last: Ostatnia wiadomość + label_message_new: Nowa wiadomość + label_message_plural: WiadomoÅ›ci + label_message_posted: Dodano wiadomość + label_min_max_length: Min - Maks dÅ‚ugość + label_modification: "%{count} modyfikacja" + label_modification_plural234: "%{count} modyfikacje" + label_modification_plural5: "%{count} modyfikacji" + label_modification_plural: "%{count} modyfikacje" + label_modified: zmodyfikowane + label_module_plural: ModuÅ‚y + label_month: MiesiÄ…c + label_months_from: miesiÄ…ce od + label_more: WiÄ™cej + label_more_than_ago: dni wiÄ™cej + label_my_account: Moje konto + label_my_page: Moja strona + label_my_projects: Moje projekty + label_new: Nowy + label_new_statuses_allowed: Uprawnione nowe statusy + label_news: Komunikat + label_news_added: Dodano komunikat + label_news_latest: Ostatnie komunikaty + label_news_new: Dodaj komunikat + label_news_plural: Komunikaty + label_news_view_all: Pokaż wszystkie komunikaty + label_next: NastÄ™pne + label_no_change_option: (Bez zmian) + label_no_data: Brak danych do pokazania + label_nobody: nikt + label_none: brak + label_not_contains: nie zawiera + label_not_equals: różni siÄ™ + label_open_issues: otwarte + label_open_issues_plural234: otwarte + label_open_issues_plural5: otwarte + label_open_issues_plural: otwarte + label_optional_description: Opcjonalny opis + label_options: Opcje + label_overall_activity: Ogólna aktywność + label_overview: PrzeglÄ…d + label_password_lost: Zapomniane hasÅ‚o + label_per_page: Na stronÄ™ + label_permissions: Uprawnienia + label_permissions_report: Raport uprawnieÅ„ + label_personalize_page: Personalizuj tÄ… stronÄ™ + label_planning: Planowanie + label_please_login: Zaloguj siÄ™ + label_plugins: Wtyczki + label_precedes: poprzedza + label_preferences: Preferencje + label_preview: PodglÄ…d + label_previous: Poprzednie + label_project: Projekt + label_project_all: Wszystkie projekty + label_project_latest: Ostatnie projekty + label_project_new: Nowy projekt + label_project_plural234: Projekty + label_project_plural5: Projektów + label_project_plural: Projekty + label_x_projects: + zero: brak projektów + one: jeden projekt + other: "%{count} projektów" + label_public_projects: Projekty publiczne + label_query: Kwerenda + label_query_new: Nowa kwerenda + label_query_plural: Kwerendy + label_read: Czytanie... + label_register: Rejestracja + label_registered_on: Zarejestrowany + label_registration_activation_by_email: aktywacja konta przez e-mail + label_registration_automatic_activation: automatyczna aktywacja kont + label_registration_manual_activation: manualna aktywacja kont + label_related_issues: PowiÄ…zane zagadnienia + label_relates_to: powiÄ…zane z + label_relation_delete: UsuÅ„ powiÄ…zanie + label_relation_new: Nowe powiÄ…zanie + label_renamed: przemianowano + label_reply_plural: Odpowiedzi + label_report: Raport + label_report_plural: Raporty + label_reported_issues: Wprowadzone zagadnienia + label_repository: Repozytorium + label_repository_plural: Repozytoria + label_result_plural: Rezultatów + label_reverse_chronological_order: W kolejnoÅ›ci odwrotnej do chronologicznej + label_revision: Rewizja + label_revision_plural: Rewizje + label_roadmap: Mapa + label_roadmap_due_in: W czasie + label_roadmap_no_issues: Brak zagadnieÅ„ do tej wersji + label_roadmap_overdue: "%{value} spóźnienia" + label_role: Rola + label_role_and_permissions: Role i Uprawnienia + label_role_new: Nowa rola + label_role_plural: Role + label_scm: SCM + label_search: Szukaj + label_search_titles_only: Przeszukuj tylko tytuÅ‚y + label_send_information: WyÅ›lij informacjÄ™ użytkownikowi + label_send_test_email: WyÅ›lij próbny email + label_settings: Ustawienia + label_show_completed_versions: Pokaż kompletne wersje + label_sort_by: "Sortuj po %{value}" + label_sort_higher: Do góry + label_sort_highest: PrzesuÅ„ na górÄ™ + label_sort_lower: Do doÅ‚u + label_sort_lowest: PrzesuÅ„ na dół + label_spent_time: Przepracowany czas + label_start_to_end: poczÄ…tek do koÅ„ca + label_start_to_start: poczÄ…tek do poczÄ…tku + label_statistics: Statystyki + label_stay_logged_in: PozostaÅ„ zalogowany + label_string: Tekst + label_subproject_plural: Podprojekty + label_text: DÅ‚ugi tekst + label_theme: Temat + label_this_month: ten miesiÄ…c + label_this_week: ten tydzieÅ„ + label_this_year: ten rok + label_time_tracking: Åšledzenie czasu pracy + label_today: dzisiaj + label_topic_plural: Tematy + label_total: Ogółem + label_tracker: Typ zagadnienia + label_tracker_new: Nowy typ zagadnienia + label_tracker_plural: Typy zagadnieÅ„ + label_updated_time: "Zaktualizowane %{value} temu" + label_used_by: Używane przez + label_user: Użytkownik + label_user_mail_no_self_notified: "Nie chcÄ™ powiadomieÅ„ o zmianach, które sam wprowadzam." + label_user_mail_option_all: "Dla każdego zdarzenia w każdym moim projekcie" + label_user_mail_option_selected: "Tylko dla każdego zdarzenia w wybranych projektach..." + label_user_new: Nowy użytkownik + label_user_plural: Użytkownicy + label_version: Wersja + label_version_new: Nowa wersja + label_version_plural: Wersje + label_view_diff: Pokaż różnice + label_view_revisions: Pokaż rewizje + label_watched_issues: Obserwowane zagadnienia + label_week: TydzieÅ„ + label_wiki: Wiki + label_wiki_edit: Edycja wiki + label_wiki_edit_plural: Edycje wiki + label_wiki_page: Strona wiki + label_wiki_page_plural: Strony wiki + label_workflow: PrzepÅ‚yw + label_year: Rok + label_yesterday: wczoraj + mail_body_account_activation_request: "Zarejestrowano nowego użytkownika: (%{value}). Konto oczekuje na twoje zatwierdzenie:" + mail_body_account_information: Twoje konto + mail_body_account_information_external: "Możesz użyć twojego %{value} konta do zalogowania." + mail_body_lost_password: 'W celu zmiany swojego hasÅ‚a użyj poniższego odnoÅ›nika:' + mail_body_register: 'W celu aktywacji Twojego konta, użyj poniższego odnoÅ›nika:' + mail_body_reminder: "Wykaz przypisanych do Ciebie zagadnieÅ„, których termin wypada w ciÄ…gu nastÄ™pnych %{count} dni" + mail_subject_account_activation_request: "Zapytanie aktywacyjne konta %{value}" + mail_subject_lost_password: "Twoje hasÅ‚o do %{value}" + mail_subject_register: "Aktywacja konta w %{value}" + mail_subject_reminder: "Uwaga na terminy, masz zagadnienia do obsÅ‚użenia w ciÄ…gu nastÄ™pnych %{count} dni! (%{days})" + notice_account_activated: Twoje konto zostaÅ‚o aktywowane. Możesz siÄ™ zalogować. + notice_account_invalid_creditentials: ZÅ‚y użytkownik lub hasÅ‚o + notice_account_lost_email_sent: Email z instrukcjami zmiany hasÅ‚a zostaÅ‚ wysÅ‚any do Ciebie. + notice_account_password_updated: HasÅ‚o prawidÅ‚owo zmienione. + notice_account_pending: "Twoje konto zostaÅ‚o utworzone i oczekuje na zatwierdzenie administratora." + notice_account_register_done: Konto prawidÅ‚owo stworzone. + notice_account_unknown_email: Nieznany użytkownik. + notice_account_updated: Konto prawidÅ‚owo zaktualizowane. + notice_account_wrong_password: ZÅ‚e hasÅ‚o + notice_can_t_change_password: To konto ma zewnÄ™trzne źródÅ‚o identyfikacji. Nie możesz zmienić hasÅ‚a. + notice_default_data_loaded: DomyÅ›lna konfiguracja zostaÅ‚a pomyÅ›lnie zaÅ‚adowana. + notice_email_error: "WystÄ…piÅ‚ błąd w trakcie wysyÅ‚ania maila (%{value})" + notice_email_sent: "Email zostaÅ‚ wysÅ‚any do %{value}" + notice_failed_to_save_issues: "Błąd podczas zapisu zagadnieÅ„ %{count} z %{total} zaznaczonych: %{ids}." + notice_feeds_access_key_reseted: Twój klucz dostÄ™pu RSS zostaÅ‚ zrestetowany. + notice_file_not_found: Strona do której próbujesz siÄ™ dostać nie istnieje lub zostaÅ‚a usuniÄ™ta. + notice_locking_conflict: Dane poprawione przez innego użytkownika. + notice_no_issue_selected: "Nie wybrano zagadnienia! Zaznacz zagadnienie, które chcesz edytować." + notice_not_authorized: Nie jesteÅ› autoryzowany by zobaczyć stronÄ™. + notice_successful_connection: Udane nawiÄ…zanie połączenia. + notice_successful_create: Utworzenie zakoÅ„czone sukcesem. + notice_successful_delete: UsuniÄ™cie zakoÅ„czone sukcesem. + notice_successful_update: Uaktualnienie zakoÅ„czone sukcesem. + notice_unable_delete_version: Nie można usunąć wersji + permission_add_issue_notes: Dodawanie notatek + permission_add_issue_watchers: Dodawanie obserwatorów + permission_add_issues: Dodawanie zagadnieÅ„ + permission_add_messages: Dodawanie wiadomoÅ›ci + permission_browse_repository: PrzeglÄ…danie repozytorium + permission_comment_news: Komentowanie komunikatów + permission_commit_access: Wykonywanie zatwierdzeÅ„ + permission_delete_issues: Usuwanie zagadnieÅ„ + permission_delete_messages: Usuwanie wiadomoÅ›ci + permission_delete_wiki_pages: Usuwanie stron wiki + permission_delete_wiki_pages_attachments: Usuwanie załączników + permission_delete_own_messages: Usuwanie wÅ‚asnych wiadomoÅ›ci + permission_edit_issue_notes: Edycja notatek + permission_edit_issues: Edycja zagadnieÅ„ + permission_edit_messages: Edycja wiadomoÅ›ci + permission_edit_own_issue_notes: Edycja wÅ‚asnych notatek + permission_edit_own_messages: Edycja wÅ‚asnych wiadomoÅ›ci + permission_edit_own_time_entries: Edycja wÅ‚asnego dziennika + permission_edit_project: Edycja projektów + permission_edit_time_entries: Edycja wpisów dziennika + permission_edit_wiki_pages: Edycja stron wiki + permission_log_time: Zapisywanie przepracowanego czasu + permission_manage_boards: ZarzÄ…dzanie forami + permission_manage_categories: ZarzÄ…dzanie kategoriami zaganieÅ„ + permission_manage_documents: ZarzÄ…dzanie dokumentami + permission_manage_files: ZarzÄ…dzanie plikami + permission_manage_issue_relations: ZarzÄ…dzanie powiÄ…zaniami zagadnieÅ„ + permission_manage_members: ZarzÄ…dzanie uczestnikami + permission_manage_news: ZarzÄ…dzanie komunikatami + permission_manage_public_queries: ZarzÄ…dzanie publicznymi kwerendami + permission_manage_repository: ZarzÄ…dzanie repozytorium + permission_manage_versions: ZarzÄ…dzanie wersjami + permission_manage_wiki: ZarzÄ…dzanie wiki + permission_move_issues: Przenoszenie zagadnieÅ„ + permission_protect_wiki_pages: Blokowanie stron wiki + permission_rename_wiki_pages: Zmiana nazw stron wiki + permission_save_queries: Zapisywanie kwerend + permission_select_project_modules: Wybieranie modułów projektu + permission_view_calendar: PodglÄ…d kalendarza + permission_view_changesets: PodglÄ…d zmian + permission_view_documents: PodglÄ…d dokumentów + permission_view_files: PodglÄ…d plików + permission_view_gantt: PodglÄ…d diagramu Gantta + permission_view_issue_watchers: PodglÄ…d listy obserwatorów + permission_view_messages: PodglÄ…d wiadomoÅ›ci + permission_view_time_entries: PodglÄ…d przepracowanego czasu + permission_view_wiki_edits: PodglÄ…d historii wiki + permission_view_wiki_pages: PodglÄ…d wiki + project_module_boards: Fora + project_module_documents: Dokumenty + project_module_files: Pliki + project_module_issue_tracking: Åšledzenie zagadnieÅ„ + project_module_news: Komunikaty + project_module_repository: Repozytorium + project_module_time_tracking: Åšledzenie czasu pracy + project_module_wiki: Wiki + setting_activity_days_default: Dni wyÅ›wietlane w aktywnoÅ›ci projektu + setting_app_subtitle: PodtytuÅ‚ aplikacji + setting_app_title: TytuÅ‚ aplikacji + setting_attachment_max_size: Maks. rozm. załącznika + setting_autofetch_changesets: Automatyczne pobieranie zmian + setting_autologin: Auto logowanie + setting_bcc_recipients: Odbiorcy kopii tajnej (kt/bcc) + setting_commit_fix_keywords: SÅ‚owa zmieniajÄ…ce status + setting_commit_ref_keywords: SÅ‚owa tworzÄ…ce powiÄ…zania + setting_cross_project_issue_relations: Zezwól na powiÄ…zania zagadnieÅ„ miÄ™dzy projektami + setting_date_format: Format daty + setting_default_language: DomyÅ›lny jÄ™zyk + setting_default_projects_public: Nowe projekty sÄ… domyÅ›lnie publiczne + setting_display_subprojects_issues: DomyÅ›lnie pokazuj zagadnienia podprojektów w głównym projekcie + setting_emails_footer: Stopka e-mail + setting_enabled_scm: DostÄ™pny SCM + setting_feeds_limit: Limit danych RSS + setting_gravatar_enabled: Używaj ikon użytkowników Gravatar + setting_host_name: Nazwa hosta i Å›cieżka + setting_issue_list_default_columns: DomyÅ›lne kolumny wyÅ›wietlane na liÅ›cie zagadnieÅ„ + setting_issues_export_limit: Limit eksportu zagadnieÅ„ + setting_login_required: Identyfikacja wymagana + setting_mail_from: Adres email wysyÅ‚ki + setting_mail_handler_api_enabled: Uaktywnij usÅ‚ugi sieciowe (WebServices) dla poczty przychodzÄ…cej + setting_mail_handler_api_key: Klucz API + setting_per_page_options: Opcje iloÅ›ci obiektów na stronie + setting_plain_text_mail: tylko tekst (bez HTML) + setting_protocol: ProtokoÅ‚ + setting_self_registration: Samodzielna rejestracja użytkowników + setting_sequential_project_identifiers: Generuj sekwencyjne identyfikatory projektów + setting_sys_api_enabled: Włączenie WS do zarzÄ…dzania repozytorium + setting_text_formatting: Formatowanie tekstu + setting_time_format: Format czasu + setting_user_format: Personalny format wyÅ›wietlania + setting_welcome_text: Tekst powitalny + setting_wiki_compression: Kompresja historii Wiki + status_active: aktywny + status_locked: zablokowany + status_registered: zarejestrowany + text_are_you_sure: JesteÅ› pewien ? + text_assign_time_entries_to_project: Przypisz wpisy dziennika do projektu + text_caracters_maximum: "%{count} znaków maksymalnie." + text_caracters_minimum: "Musi być nie krótsze niż %{count} znaków." + text_comma_separated: Wielokrotne wartoÅ›ci dozwolone (rozdzielone przecinkami). + text_default_administrator_account_changed: Zmieniono domyÅ›lne hasÅ‚o administratora + text_destroy_time_entries: UsuÅ„ wpisy dziennika + text_destroy_time_entries_question: Przepracowano %{hours} godzin przy zagadnieniu, które chcesz usunąć. Co chcesz zrobić? + text_email_delivery_not_configured: "Dostarczanie poczty elektronicznej nie zostaÅ‚o skonfigurowane, wiÄ™c powiadamianie jest nieaktywne.\nSkonfiguruj serwer SMTP w config/configuration.yml a nastÄ™pnie zrestartuj aplikacjÄ™ i uaktywnij to." + text_enumeration_category_reassign_to: 'ZmieÅ„ przypisanie na tÄ… wartość:' + text_enumeration_destroy_question: "%{count} obiektów jest przypisana do tej wartoÅ›ci." + text_file_repository_writable: Zapisywalne repozytorium plików + text_issue_added: "Zagadnienie %{id} zostaÅ‚o wprowadzone (by %{author})." + text_issue_category_destroy_assignments: UsuÅ„ przydziaÅ‚y kategorii + text_issue_category_destroy_question: "Zagadnienia (%{count}) sÄ… przypisane do tej kategorii. Co chcesz zrobić?" + text_issue_category_reassign_to: Przydziel zagadnienie do tej kategorii + text_issue_updated: "Zagadnienie %{id} zostaÅ‚o zaktualizowane (by %{author})." + text_issues_destroy_confirmation: 'Czy jestes pewien, że chcesz usunąć wskazane zagadnienia?' + text_issues_ref_in_commit_messages: OdwoÅ‚ania do zagadnieÅ„ w komentarzach zatwierdzeÅ„ + text_length_between: "DÅ‚ugość pomiÄ™dzy %{min} i %{max} znaków." + text_load_default_configuration: ZaÅ‚aduj domyÅ›lnÄ… konfiguracjÄ™ + text_min_max_length_info: 0 oznacza brak restrykcji + text_no_configuration_data: "Role użytkowników, typy zagadnieÅ„, statusy zagadnieÅ„ oraz przepÅ‚yw pracy nie zostaÅ‚y jeszcze skonfigurowane.\nJest wysoce rekomendowane by zaÅ‚adować domyÅ›lnÄ… konfiguracjÄ™. Po zaÅ‚adowaniu bÄ™dzie możliwość edycji tych danych." + text_project_destroy_confirmation: JesteÅ› pewien, że chcesz usunąć ten projekt i wszystkie powiÄ…zane dane? + text_reassign_time_entries: 'Przepnij przepracowany czas do tego zagadnienia:' + text_regexp_info: np. ^[A-Z0-9]+$ + text_repository_usernames_mapping: "Wybierz lub uaktualnij przyporzÄ…dkowanie użytkowników Redmine do użytkowników repozytorium.\nUżytkownicy z takÄ… samÄ… nazwÄ… lub adresem email sÄ… przyporzÄ…dkowani automatycznie." + text_rmagick_available: RMagick dostÄ™pne (opcjonalnie) + text_select_mail_notifications: Zaznacz czynnoÅ›ci przy których użytkownik powinien być powiadomiony mailem. + text_select_project_modules: 'Wybierz moduÅ‚y do aktywacji w tym projekcie:' + text_status_changed_by_changeset: "Zastosowane w zmianach %{value}." + text_subprojects_destroy_warning: "Podprojekt(y): %{value} zostanÄ… także usuniÄ™te." + text_tip_issue_begin_day: zadanie zaczynajÄ…ce siÄ™ dzisiaj + text_tip_issue_begin_end_day: zadanie zaczynajÄ…ce i koÅ„czÄ…ce siÄ™ dzisiaj + text_tip_issue_end_day: zadanie koÅ„czÄ…ce siÄ™ dzisiaj + text_tracker_no_workflow: Brak przepÅ‚ywu zdefiniowanego dla tego typu zagadnienia + text_unallowed_characters: Niedozwolone znaki + text_user_mail_option: "W przypadku niezaznaczonych projektów, bÄ™dziesz otrzymywaÅ‚ powiadomienia tylko na temat zagadnieÅ„, które obserwujesz, lub w których bierzesz udziaÅ‚ (np. jesteÅ› autorem lub adresatem)." + text_user_wrote: "%{value} napisaÅ‚:" + text_wiki_destroy_confirmation: JesteÅ› pewien, że chcesz usunąć to wiki i całą jego zawartość ? + text_workflow_edit: Zaznacz rolÄ™ i typ zagadnienia do edycji przepÅ‚ywu + + label_user_activity: "Aktywność: %{value}" + label_updated_time_by: "Uaktualnione przez %{author} %{age} temu" + text_diff_truncated: '... Ten plik różnic zostaÅ‚ przyciÄ™ty ponieważ jest zbyt dÅ‚ugi.' + setting_diff_max_lines_displayed: Maksymalna liczba linii różnicy do pokazania + text_plugin_assets_writable: Zapisywalny katalog zasobów wtyczek + warning_attachments_not_saved: "%{count} załącznik(ów) nie zostaÅ‚o zapisanych." + field_editable: Edytowalne + label_display: WyglÄ…d + button_create_and_continue: Stwórz i dodaj kolejne + text_custom_field_possible_values_info: 'Każda wartość w osobnej linii' + setting_repository_log_display_limit: Maksymalna liczba rewizji pokazywanych w logu pliku + setting_file_max_size_displayed: Maksymalny rozmiar plików tekstowych osadzanych w stronie + field_watcher: Obserwator + setting_openid: Logowanie i rejestracja przy użyciu OpenID + field_identity_url: Identyfikator OpenID (URL) + label_login_with_open_id_option: albo użyj OpenID + field_content: Treść + label_descending: MalejÄ…co + label_sort: Sortuj + label_ascending: RosnÄ…co + label_date_from_to: Od %{start} do %{end} + label_greater_or_equal: ">=" + label_less_or_equal: <= + text_wiki_page_destroy_question: Ta strona posiada podstrony (%{descendants}). Co chcesz zrobić? + text_wiki_page_reassign_children: Podepnij je do strony nadrzÄ™dnej wzglÄ™dem usuwanej + text_wiki_page_nullify_children: PrzesuÅ„ je na szczyt hierarchii + text_wiki_page_destroy_children: UsuÅ„ wszystkie podstrony + setting_password_min_length: Minimalna dÅ‚ugość hasÅ‚a + field_group_by: Grupuj wyniki wg + mail_subject_wiki_content_updated: "Strona wiki '%{id}' zostaÅ‚a uaktualniona" + label_wiki_content_added: Dodano stronÄ™ wiki + mail_subject_wiki_content_added: "Strona wiki '%{id}' zostaÅ‚a dodana" + mail_body_wiki_content_added: Strona wiki '%{id}' zostaÅ‚a dodana przez %{author}. + label_wiki_content_updated: Uaktualniono stronÄ™ wiki + mail_body_wiki_content_updated: Strona wiki '%{id}' zostaÅ‚a uaktualniona przez %{author}. + permission_add_project: Tworzenie projektu + setting_new_project_user_role_id: Rola nadawana twórcom projektów, którzy nie posiadajÄ… uprawnieÅ„ administatora + label_view_all_revisions: Pokaż wszystkie rewizje + label_tag: SÅ‚owo kluczowe + label_branch: Gałąź + error_no_tracker_in_project: Projekt nie posiada powiÄ…zanych typów zagadnieÅ„. Sprawdź ustawienia projektu. + error_no_default_issue_status: Nie zdefiniowano domyÅ›lnego statusu zagadnieÅ„. Sprawdź konfiguracjÄ™ (Przejdź do "Administracja -> Statusy zagadnieÅ„). + text_journal_changed: "Zmieniono %{label} z %{old} na %{new}" + text_journal_set_to: "Ustawiono %{label} na %{value}" + text_journal_deleted: "UsuniÄ™to %{label} (%{old})" + label_group_plural: Grupy + label_group: Grupa + label_group_new: Nowa grupa + label_time_entry_plural: Przepracowany czas + text_journal_added: "Dodano %{label} %{value}" + field_active: Aktywne + enumeration_system_activity: Aktywność Systemowa + button_copy_and_follow: Kopiuj i przejdź do kopii zagadnienia + button_duplicate: Duplikuj + button_move_and_follow: PrzenieÅ› i przejdź do zagadnienia + button_show: Pokaż + error_can_not_archive_project: Ten projekt nie może zostać zarchiwizowany + error_can_not_reopen_issue_on_closed_version: Zagadnienie przydzielone do zakoÅ„czonej wersji nie może zostać ponownie otwarte + error_issue_done_ratios_not_updated: "% wykonania zagadnienia nie zostaÅ‚ uaktualniony." + error_workflow_copy_source: ProszÄ™ wybrać źródÅ‚owy typ zagadnienia lub rolÄ™ + error_workflow_copy_target: ProszÄ™ wybrać docelowe typ(y) zagadnieÅ„ i rolÄ™(e) + field_sharing: Współdzielenie + label_api_access_key: Klucz dostÄ™pu do API + label_api_access_key_created_on: Klucz dostÄ™pu do API zostaÅ‚ utworzony %{value} temu + label_close_versions: Zamknij ukoÅ„czone wersje + label_copy_same_as_target: Jak cel + label_copy_source: ŹródÅ‚o + label_copy_target: Cel + label_display_used_statuses_only: WyÅ›wietlaj tylko statusy używane przez ten typ zagadnienia + label_feeds_access_key: Klucz dostÄ™pu do RSS + label_missing_api_access_key: Brakuje klucza dostÄ™pu do API + label_missing_feeds_access_key: Brakuje klucza dostÄ™pu do RSS + label_revision_id: Rewizja %{value} + label_subproject_new: Nowy podprojekt + label_update_issue_done_ratios: Uaktualnij % wykonania + label_user_anonymous: Anonimowy + label_version_sharing_descendants: Z podprojektami + label_version_sharing_hierarchy: Z hierarchiÄ… projektów + label_version_sharing_none: Brak współdzielenia + label_version_sharing_system: Ze wszystkimi projektami + label_version_sharing_tree: Z drzewem projektów + notice_api_access_key_reseted: Twój klucz dostÄ™pu do API zostaÅ‚ zresetowany. + notice_issue_done_ratios_updated: Uaktualnienie % wykonania zakoÅ„czone sukcesem. + permission_add_subprojects: Tworzenie podprojektów + permission_delete_issue_watchers: UsuÅ„ obserwatorów + permission_view_issues: PrzeglÄ…danie zagadnieÅ„ + setting_default_projects_modules: DomyÅ›lnie włączone moduÅ‚y dla nowo tworzonych projektów + setting_gravatar_default: DomyÅ›lny obraz Gravatar + setting_issue_done_ratio: Obliczaj postÄ™p realizacji zagadnieÅ„ za pomocÄ… + setting_issue_done_ratio_issue_field: "% Wykonania zagadnienia" + setting_issue_done_ratio_issue_status: Statusu zagadnienia + setting_mail_handler_body_delimiters: Przycinaj e-maile po jednej z tych linii + setting_rest_api_enabled: Uaktywnij usÅ‚ugÄ™ sieciowÄ… REST + setting_start_of_week: Pierwszy dzieÅ„ tygodnia + text_line_separated: Dozwolone jest wiele wartoÅ›ci (każda wartość w osobnej linii). + text_own_membership_delete_confirmation: |- + Masz zamiar usunąć niektóre lub wszystkie swoje uprawnienia. Po wykonaniu tej czynnoÅ›ci możesz utracić możliwoÅ›ci edycji tego projektu. + Czy na pewno chcesz kontynuować? + version_status_closed: zamkniÄ™ta + version_status_locked: zablokowana + version_status_open: otwarta + + label_board_sticky: Przyklejona + label_board_locked: ZamkniÄ™ta + permission_export_wiki_pages: Eksport stron wiki + permission_manage_project_activities: ZarzÄ…dzanie aktywnoÅ›ciami projektu + setting_cache_formatted_text: Buforuj sformatowany tekst + error_unable_delete_issue_status: Nie można usunąć statusu zagadnienia + label_profile: Profil + permission_manage_subtasks: ZarzÄ…dzanie podzagadnieniami + field_parent_issue: Zagadnienie nadrzÄ™dne + label_subtask_plural: Podzagadnienia + label_project_copy_notifications: WyÅ›lij powiadomienia mailowe przy kopiowaniu projektu + error_can_not_delete_custom_field: Nie można usunąć tego pola + error_unable_to_connect: Nie można połączyć (%{value}) + error_can_not_remove_role: Ta rola przypisana jest niektórym użytkownikom i nie może zostać usuniÄ™ta. + error_can_not_delete_tracker: Ten typ przypisany jest do części zagadnieÅ„ i nie może zostać usuniÄ™ty. + field_principal: PrzeÅ‚ożony + label_my_page_block: Elementy + notice_failed_to_save_members: "Nie można zapisać uczestników: %{errors}." + text_zoom_out: Zmniejsz czcionkÄ™ + text_zoom_in: PowiÄ™ksz czcionkÄ™ + notice_unable_delete_time_entry: Nie można usunąć wpisu z dziennika. + label_overall_spent_time: Przepracowany czas + field_time_entries: Dziennik + project_module_gantt: Diagram Gantta + project_module_calendar: Kalendarz + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Text field + label_user_mail_option_only_owner: Only for things I am the owner of + setting_default_notification_option: Default notification option + label_user_mail_option_only_my_events: Only for things I watch or I'm involved in + label_user_mail_option_only_assigned: Only for things I am assigned to + label_user_mail_option_none: No events + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + notice_not_authorized_archived_project: The project you're trying to access has been archived. + label_principal_search: "Search for user or group:" + label_user_search: "Search for user:" + field_visible: Visible + setting_emails_header: Emails header + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Enable time logging + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maximum number of items displayed on the gantt chart + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: Priority updated + label_issues_visibility_own: Issues created by or assigned to the user + field_issues_visibility: Issues visibility + label_issues_visibility_all: All issues + permission_set_own_issues_private: Set own issues public or private + field_is_private: Private + permission_set_issues_private: Set issues public or private + label_issues_visibility_public: All non private issues + text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). + field_commit_logs_encoding: Kodowanie komentarzy zatwierdzeÅ„ + field_scm_path_encoding: Path encoding + text_scm_path_encoding_note: "Default: UTF-8" + field_path_to_repository: Path to repository + field_root_directory: Root directory + field_cvs_module: Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Command + text_scm_command_version: Version + label_git_report_last_commit: Report last commit for files and directories + text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 zagadnienie + one: 1 zagadnienie + other: "%{count} zagadnienia" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: wszystko + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Z podprojektami + label_cross_project_tree: Z drzewem projektów + label_cross_project_hierarchy: Z hierarchiÄ… projektów + label_cross_project_system: Ze wszystkimi projektami + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/04/042891243ed00da7c541ac0d9a5f19f90a344ef6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/04/042891243ed00da7c541ac0d9a5f19f90a344ef6.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1133 @@ +# Korean translations for Ruby on Rails +ko: + direction: ltr + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%Y/%m/%d" + short: "%m/%d" + long: "%Yë…„ %mì›” %dì¼ (%a)" + + day_names: [ì¼ìš”ì¼, 월요ì¼, 화요ì¼, 수요ì¼, 목요ì¼, 금요ì¼, 토요ì¼] + abbr_day_names: [ì¼, ì›”, í™”, 수, 목, 금, 토] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, 1ì›”, 2ì›”, 3ì›”, 4ì›”, 5ì›”, 6ì›”, 7ì›”, 8ì›”, 9ì›”, 10ì›”, 11ì›”, 12ì›”] + abbr_month_names: [~, 1ì›”, 2ì›”, 3ì›”, 4ì›”, 5ì›”, 6ì›”, 7ì›”, 8ì›”, 9ì›”, 10ì›”, 11ì›”, 12ì›”] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%Y/%m/%d %H:%M:%S" + time: "%H:%M" + short: "%y/%m/%d %H:%M" + long: "%Yë…„ %Bì›” %dì¼, %H시 %Më¶„ %Sì´ˆ %Z" + am: "오전" + pm: "오후" + + datetime: + distance_in_words: + half_a_minute: "30ì´ˆ" + less_than_x_seconds: + one: "ì¼ì´ˆ ì´í•˜" + other: "%{count}ì´ˆ ì´í•˜" + x_seconds: + one: "ì¼ì´ˆ" + other: "%{count}ì´ˆ" + less_than_x_minutes: + one: "ì¼ë¶„ ì´í•˜" + other: "%{count}ë¶„ ì´í•˜" + x_minutes: + one: "ì¼ë¶„" + other: "%{count}ë¶„" + about_x_hours: + one: "약 한시간" + other: "약 %{count}시간" + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "하루" + other: "%{count}ì¼" + about_x_months: + one: "약 한달" + other: "약 %{count}달" + x_months: + one: "한달" + other: "%{count}달" + about_x_years: + one: "약 ì¼ë…„" + other: "약 %{count}ë…„" + over_x_years: + one: "ì¼ë…„ ì´ìƒ" + other: "%{count}ë…„ ì´ìƒ" + almost_x_years: + one: "약 1ë…„" + other: "약 %{count}ë…„" + prompts: + year: "ë…„" + month: "ì›”" + day: "ì¼" + hour: "시" + minute: "ë¶„" + second: "ì´ˆ" + + number: + # Used in number_with_delimiter() + # These are also the defaults for 'currency', 'percentage', 'precision', and 'human' + format: + # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5) + separator: "." + # Delimets thousands (e.g. 1,000,000 is a million) (always in groups of three) + delimiter: "," + # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00) + precision: 3 + + # Used in number_to_currency() + currency: + format: + # Where is the currency sign? %u is the currency unit, %n the number (default: $5.00) + format: "%u%n" + unit: "â‚©" + # These three are to override number.format and are optional + separator: "." + delimiter: "," + precision: 0 + + # Used in number_to_percentage() + percentage: + format: + # These three are to override number.format and are optional + # separator: + delimiter: "" + # precision: + + # Used in number_to_precision() + precision: + format: + # These three are to override number.format and are optional + # separator: + delimiter: "" + # precision: + + # Used in number_to_human_size() + human: + format: + # These three are to override number.format and are optional + # separator: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + +# Used in array.to_sentence. + support: + array: + words_connector: ", " + two_words_connector: "ê³¼ " + last_word_connector: ", " + sentence_connector: "그리고" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "í•œê°œì˜ ì˜¤ë¥˜ê°€ ë°œìƒí•´ %{model}ì„(를) 저장하지 않았습니다." + other: "%{count}ê°œì˜ ì˜¤ë¥˜ê°€ ë°œìƒí•´ %{model}ì„(를) 저장하지 않았습니다." + # The variable :count is also available + body: "ë‹¤ìŒ í•­ëª©ì— ë¬¸ì œê°€ 발견했습니다:" + + messages: + inclusion: "ì€ ëª©ë¡ì— í¬í•¨ë˜ì–´ 있지 않습니다" + exclusion: "ì€ ì˜ˆì•½ë˜ì–´ 있습니다" + invalid: "ì€ ìœ íš¨í•˜ì§€ 않습니다." + confirmation: "ì€ í™•ì¸ì´ ë˜ì§€ 않았습니다" + accepted: "ì€ ì¸ì •ë˜ì–´ì•¼ 합니다" + empty: "ì€ ê¸¸ì´ê°€ 0ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤." + blank: "ì€ ë¹ˆ ê°’ì´ì–´ì„œëŠ” 안 ë©ë‹ˆë‹¤" + too_long: "ì€ ë„ˆë¬´ ê¹ë‹ˆë‹¤ (최대 %{count}ìž ê¹Œì§€)" + too_short: "ì€ ë„ˆë¬´ 짧습니다 (최소 %{count}ìž ê¹Œì§€)" + wrong_length: "ì€ ê¸¸ì´ê°€ 틀렸습니다 (%{count}ìžì´ì–´ì•¼ 합니다.)" + taken: "ì€ ì´ë¯¸ ì„ íƒëœ ê²ë‹ˆë‹¤" + not_a_number: "ì€ ìˆ«ìžê°€ 아닙니다" + greater_than: "ì€ %{count}보다 커야 합니다." + greater_than_or_equal_to: "ì€ %{count}보다 í¬ê±°ë‚˜ 같아야 합니다" + equal_to: "ì€ %{count}(와)ê³¼ 같아야 합니다" + less_than: "ì€ %{count}보다 작어야 합니다" + less_than_or_equal_to: "ì€ %{count}ê³¼ 같거나 ì´í•˜ì„ 요구합니다" + odd: "ì€ í™€ìˆ˜ì—¬ì•¼ 합니다" + even: "ì€ ì§ìˆ˜ì—¬ì•¼ 합니다" + greater_than_start_date: "는 시작날짜보다 커야 합니다" + not_same_project: "는 ê°™ì€ í”„ë¡œì íŠ¸ì— ì†í•´ 있지 않습니다" + circular_dependency: "ì´ ê´€ê³„ëŠ” 순환 ì˜ì¡´ê´€ê³„를 만들 수 있습니다" + cant_link_an_issue_with_a_descendant: "ì¼ê°ì€ 하위 ì¼ê°ê³¼ ì—°ê²°í•  수 없습니다." + + actionview_instancetag_blank_option: ì„ íƒí•˜ì„¸ìš” + + general_text_No: '아니오' + general_text_Yes: '예' + general_text_no: '아니오' + general_text_yes: '예' + general_lang_name: 'Korean (한국어)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: CP949 + general_pdf_encoding: CP949 + general_first_day_of_week: '7' + + notice_account_updated: ê³„ì •ì´ ì„±ê³µì ìœ¼ë¡œ 변경ë˜ì—ˆìŠµë‹ˆë‹¤. + notice_account_invalid_creditentials: ìž˜ëª»ëœ ê³„ì • ë˜ëŠ” 비밀번호 + notice_account_password_updated: 비밀번호가 잘 변경ë˜ì—ˆìŠµë‹ˆë‹¤. + notice_account_wrong_password: ìž˜ëª»ëœ ë¹„ë°€ë²ˆí˜¸ + notice_account_register_done: ê³„ì •ì´ ìž˜ 만들어졌습니다. ê³„ì •ì„ í™œì„±í™”í•˜ì‹œë ¤ë©´ ë°›ì€ ë©”ì¼ì˜ ë§í¬ë¥¼ í´ë¦­í•´ì£¼ì„¸ìš”. + notice_account_unknown_email: 알려지지 ì•Šì€ ì‚¬ìš©ìž. + notice_can_t_change_password: ì´ ê³„ì •ì€ ì™¸ë¶€ ì¸ì¦ì„ ì´ìš©í•©ë‹ˆë‹¤. 비밀번호를 변경할 수 없습니다. + notice_account_lost_email_sent: 새로운 비밀번호를 위한 ë©”ì¼ì´ 발송ë˜ì—ˆìŠµë‹ˆë‹¤. + notice_account_activated: ê³„ì •ì´ í™œì„±í™”ë˜ì—ˆìŠµë‹ˆë‹¤. ì´ì œ ë¡œê·¸ì¸ í•˜ì‹¤ìˆ˜ 있습니다. + notice_successful_create: ìƒì„± 성공. + notice_successful_update: 변경 성공. + notice_successful_delete: ì‚­ì œ 성공. + notice_successful_connection: ì—°ê²° 성공. + notice_file_not_found: 요청하신 페ì´ì§€ëŠ” ì‚­ì œë˜ì—ˆê±°ë‚˜ 옮겨졌습니다. + notice_locking_conflict: 다른 사용ìžì— ì˜í•´ì„œ ë°ì´í„°ê°€ 변경ë˜ì—ˆìŠµë‹ˆë‹¤. + notice_not_authorized: ì´ íŽ˜ì´ì§€ì— 접근할 ê¶Œí•œì´ ì—†ìŠµë‹ˆë‹¤. + notice_email_sent: "%{value}님ì—게 ë©”ì¼ì´ 발송ë˜ì—ˆìŠµë‹ˆë‹¤." + notice_email_error: "ë©”ì¼ì„ 전송하는 ê³¼ì •ì— ì˜¤ë¥˜ê°€ ë°œìƒí–ˆìŠµë‹ˆë‹¤. (%{value})" + notice_feeds_access_key_reseted: RSSì— ì ‘ê·¼ê°€ëŠ¥í•œ 열쇠(key)ê°€ ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤. + notice_failed_to_save_issues: "ì €ìž¥ì— ì‹¤íŒ¨í•˜ì˜€ìŠµë‹ˆë‹¤: 실패 %{count}(ì„ íƒ %{total}): %{ids}." + notice_no_issue_selected: "ì¼ê°ì´ ì„ íƒë˜ì§€ 않았습니다. 수정하기 ì›í•˜ëŠ” ì¼ê°ì„ ì„ íƒí•˜ì„¸ìš”" + notice_account_pending: "ê³„ì •ì´ ë§Œë“¤ì–´ì¡Œìœ¼ë©° ê´€ë¦¬ìž ìŠ¹ì¸ ëŒ€ê¸°ì¤‘ìž…ë‹ˆë‹¤." + notice_default_data_loaded: ê¸°ë³¸ê°’ì„ ì„±ê³µì ìœ¼ë¡œ ì½ì–´ë“¤ì˜€ìŠµë‹ˆë‹¤. + notice_unable_delete_version: 삭제할 수 없는 버전입니다. + + error_can_t_load_default_data: "ê¸°ë³¸ê°’ì„ ì½ì–´ë“¤ì¼ 수 없습니다.: %{value}" + error_scm_not_found: 항목ì´ë‚˜ ë¦¬ë¹„ì ¼ì´ ì €ìž¥ì†Œì— ì¡´ìž¬í•˜ì§€ 않습니다. + error_scm_command_failed: "ì €ìž¥ì†Œì— ì ‘ê·¼í•˜ëŠ” ë„ì¤‘ì— ì˜¤ë¥˜ê°€ ë°œìƒí•˜ì˜€ìŠµë‹ˆë‹¤.: %{value}" + error_scm_annotate: "í•­ëª©ì´ ì—†ê±°ë‚˜ 행별 ì´ë ¥ì„ ë³¼ 수 없습니다." + error_issue_not_found_in_project: 'ì¼ê°ì´ 없거나 ì´ í”„ë¡œì íŠ¸ì˜ ê²ƒì´ ì•„ë‹™ë‹ˆë‹¤.' + + warning_attachments_not_saved: "%{count}ê°œ 파ì¼ì„ 저장할 수 없습니다." + + mail_subject_lost_password: "%{value} 비밀번호" + mail_body_lost_password: '비밀번호를 변경하려면 ë‹¤ìŒ ë§í¬ë¥¼ í´ë¦­í•˜ì„¸ìš”.' + mail_subject_register: "%{value} 계정 활성화" + mail_body_register: 'ê³„ì •ì„ í™œì„±í™”í•˜ë ¤ë©´ ë§í¬ë¥¼ í´ë¦­í•˜ì„¸ìš”.:' + mail_body_account_information_external: "로그ì¸í•  때 %{value} ê³„ì •ì„ ì‚¬ìš©í•˜ì‹¤ 수 있습니다." + mail_body_account_information: 계정 ì •ë³´ + mail_subject_account_activation_request: "%{value} 계정 활성화 요청" + mail_body_account_activation_request: "새 사용ìž(%{value})ê°€ 등ë¡ë˜ì—ˆìŠµë‹ˆë‹¤. 관리ìžë‹˜ì˜ 승ì¸ì„ 기다리고 있습니다.:" + mail_body_reminder: "ë‹¹ì‹ ì´ ë§¡ê³  있는 ì¼ê° %{count}ê°œì˜ ì™„ë£Œ ê¸°í•œì´ %{days}ì¼ í›„ 입니다." + mail_subject_reminder: "ë‚´ì¼ì´ ë§Œê¸°ì¸ ì¼ê° %{count}ê°œ (%{days})" + mail_subject_wiki_content_added: "위키페ì´ì§€ '%{id}'ì´(ê°€) 추가ë˜ì—ˆìŠµë‹ˆë‹¤." + mail_subject_wiki_content_updated: "'위키페ì´ì§€ %{id}'ì´(ê°€) 수정ë˜ì—ˆìŠµë‹ˆë‹¤." + mail_body_wiki_content_added: "%{author}ì´(ê°€) 위키페ì´ì§€ '%{id}'ì„(를) 추가하였습니다." + mail_body_wiki_content_updated: "%{author}ì´(ê°€) 위키페ì´ì§€ '%{id}'ì„(를) 수정하였습니다." + + gui_validation_error: ì—러 + gui_validation_error_plural: "%{count}ê°œ ì—러" + + field_name: ì´ë¦„ + field_description: 설명 + field_summary: 요약 + field_is_required: 필수 + field_firstname: ì´ë¦„ + field_lastname: 성 + field_mail: ë©”ì¼ + field_filename: íŒŒì¼ + field_filesize: í¬ê¸° + field_downloads: 다운로드 + field_author: ì €ìž + field_created_on: ë“±ë¡ + field_updated_on: 변경 + field_field_format: í˜•ì‹ + field_is_for_all: 모든 프로ì íЏ + field_possible_values: 가능한 값들 + field_regexp: ì •ê·œì‹ + field_min_length: 최소 ê¸¸ì´ + field_max_length: 최대 ê¸¸ì´ + field_value: ê°’ + field_category: 범주 + field_title: 제목 + field_project: 프로ì íЏ + field_issue: ì¼ê° + field_status: ìƒíƒœ + field_notes: ë§ê¸€ + field_is_closed: 완료 ìƒíƒœ + field_is_default: 기본값 + field_tracker: 유형 + field_subject: 제목 + field_due_date: 완료 기한 + field_assigned_to: ë‹´ë‹¹ìž + field_priority: 우선순위 + field_fixed_version: 목표버전 + field_user: ì‚¬ìš©ìž + field_role: ì—­í•  + field_homepage: 홈페ì´ì§€ + field_is_public: 공개 + field_parent: ìƒìœ„ 프로ì íЏ + field_is_in_roadmap: ë¡œë“œë§µì— í‘œì‹œ + field_login: ë¡œê·¸ì¸ + field_mail_notification: ë©”ì¼ ì•Œë¦¼ + field_admin: ê´€ë¦¬ìž + field_last_login_on: 마지막 ë¡œê·¸ì¸ + field_language: 언어 + field_effective_date: ë‚ ì§œ + field_password: 비밀번호 + field_new_password: 새 비밀번호 + field_password_confirmation: 비밀번호 í™•ì¸ + field_version: 버전 + field_type: ë°©ì‹ + field_host: 호스트 + field_port: í¬íЏ + field_account: 계정 + field_base_dn: 기본 DN + field_attr_login: ë¡œê·¸ì¸ ì†ì„± + field_attr_firstname: ì´ë¦„ ì†ì„± + field_attr_lastname: 성 ì†ì„± + field_attr_mail: ë©”ì¼ ì†ì„± + field_onthefly: ë™ì  ì‚¬ìš©ìž ìƒì„± + field_start_date: 시작시간 + field_done_ratio: ì§„ì²™ë„ + field_auth_source: ì¸ì¦ ê³µê¸‰ìž + field_hide_mail: ë©”ì¼ ì£¼ì†Œ 숨기기 + field_comments: 설명 + field_url: URL + field_start_page: 첫 페ì´ì§€ + field_subproject: 하위 프로ì íЏ + field_hours: 시간 + field_activity: 작업종류 + field_spent_on: 작업시간 + field_identifier: ì‹ë³„ìž + field_is_filter: 검색조건으로 ì‚¬ìš©ë¨ + field_issue_to_id: ì—°ê´€ëœ ì¼ê° + field_delay: 지연 + field_assignable: ì´ ì—­í• ì—게 ì¼ê°ì„ 맡길 수 ìžˆìŒ + field_redirect_existing_links: ê¸°ì¡´ì˜ ë§í¬ë¡œ ëŒë ¤ë³´ëƒ„(redirect) + field_estimated_hours: 추정시간 + field_column_names: 컬럼 + field_default_value: 기본값 + field_time_zone: 시간대 + field_searchable: 검색가능 + field_comments_sorting: 댓글 ì •ë ¬ + field_parent_title: ìƒìœ„ 제목 + field_editable: 편집가능 + field_watcher: ì¼ê°ì§€í‚´ì´ + field_identity_url: OpenID URL + field_content: ë‚´ìš© + field_group_by: 결과를 묶어 보여줄 기준 + + setting_app_title: ë ˆë“œë§ˆì¸ ì œëª© + setting_app_subtitle: ë ˆë“œë§ˆì¸ ë¶€ì œëª© + setting_welcome_text: í™˜ì˜ ë©”ì‹œì§€ + setting_default_language: 기본 언어 + setting_login_required: ì¸ì¦ì´ 필요함 + setting_self_registration: ì‚¬ìš©ìž ì§ì ‘ë“±ë¡ + setting_attachment_max_size: 최대 ì²¨ë¶€íŒŒì¼ í¬ê¸° + setting_issues_export_limit: ì¼ê° 내보내기 제한 + setting_mail_from: 발신 ë©”ì¼ ì£¼ì†Œ + setting_bcc_recipients: 참조ìžë“¤ì„ bcc로 숨기기 + setting_plain_text_mail: í…스트만 (HTML ì—†ì´) + setting_host_name: 호스트 ì´ë¦„ê³¼ 경로 + setting_text_formatting: 본문 í˜•ì‹ + setting_wiki_compression: 위키 ì´ë ¥ ì••ì¶• + setting_feeds_limit: í”¼ë“œì— í¬í•¨í•  í•­ëª©ì˜ ìˆ˜ + setting_default_projects_public: 새 프로ì íŠ¸ë¥¼ 공개로 설정 + setting_autofetch_changesets: 커밋(commit)ëœ ë³€ê²½ë¬¶ìŒì„ ìžë™ìœ¼ë¡œ 가져오기 + setting_sys_api_enabled: 저장소 ê´€ë¦¬ì— WS를 사용 + setting_commit_ref_keywords: ì¼ê° ì°¸ì¡°ì— ì‚¬ìš©í•  키워드들 + setting_commit_fix_keywords: ì¼ê° í•´ê²°ì— ì‚¬ìš©í•  키워드들 + setting_autologin: ìžë™ ë¡œê·¸ì¸ + setting_date_format: ë‚ ì§œ í˜•ì‹ + setting_time_format: 시간 í˜•ì‹ + setting_cross_project_issue_relations: 다른 프로ì íŠ¸ì˜ ì¼ê°ê³¼ 연결하는 ê²ƒì„ í—ˆìš© + setting_issue_list_default_columns: ì¼ê° 목ë¡ì— 표시할 항목 + setting_emails_footer: ë©”ì¼ ê¼¬ë¦¬ + setting_protocol: 프로토콜 + setting_per_page_options: 목ë¡ì—서, 한 페ì´ì§€ì— 표시할 í–‰ + setting_user_format: ì‚¬ìš©ìž í‘œì‹œ í˜•ì‹ + setting_activity_days_default: 프로ì íЏ ìž‘ì—…ë‚´ì—­ì— í‘œì‹œí•  기간 + setting_display_subprojects_issues: 하위 프로ì íŠ¸ì˜ ì¼ê°ì„ 함께 표시 + setting_enabled_scm: "ì§€ì›í•  SCM(Source Control Management)" + setting_mail_handler_api_enabled: 수신 ë©”ì¼ì— WS를 허용 + setting_mail_handler_api_key: API 키 + setting_sequential_project_identifiers: 프로ì íЏ ì‹ë³„ìžë¥¼ 순차ì ìœ¼ë¡œ ìƒì„± + setting_gravatar_enabled: ê·¸ë¼ë°”타 ì‚¬ìš©ìž ì•„ì´ì½˜ 사용 + setting_diff_max_lines_displayed: ì°¨ì´ì (diff) ë³´ê¸°ì— í‘œì‹œí•  최대 줄수 + setting_repository_log_display_limit: 저장소 ë³´ê¸°ì— í‘œì‹œí•  ê°œì •íŒ ì´ë ¥ì˜ 최대 갯수 + setting_file_max_size_displayed: 바로 보여줄 í…스트파ì¼ì˜ 최대 í¬ê¸° + setting_openid: OpenID 로그ì¸ê³¼ ë“±ë¡ í—ˆìš© + setting_password_min_length: 최소 암호 ê¸¸ì´ + setting_new_project_user_role_id: 프로ì íŠ¸ë¥¼ 만든 사용ìžì—게 주어질 ì—­í•  + + permission_add_project: 프로ì íЏ ìƒì„± + permission_edit_project: 프로ì íЏ 편집 + permission_select_project_modules: 프로ì íЏ 모듈 ì„ íƒ + permission_manage_members: êµ¬ì„±ì› ê´€ë¦¬ + permission_manage_versions: 버전 관리 + permission_manage_categories: ì¼ê° 범주 관리 + permission_add_issues: ì¼ê° 추가 + permission_edit_issues: ì¼ê° 편집 + permission_manage_issue_relations: ì¼ê° 관계 관리 + permission_add_issue_notes: ë§ê¸€ 추가 + permission_edit_issue_notes: ë§ê¸€ 편집 + permission_edit_own_issue_notes: ë‚´ ë§ê¸€ 편집 + permission_move_issues: ì¼ê° ì´ë™ + permission_delete_issues: ì¼ê° ì‚­ì œ + permission_manage_public_queries: 공용 ê²€ìƒ‰ì–‘ì‹ ê´€ë¦¬ + permission_save_queries: ê²€ìƒ‰ì–‘ì‹ ì €ìž¥ + permission_view_gantt: Gantt차트 보기 + permission_view_calendar: 달력 보기 + permission_view_issue_watchers: ì¼ê°ì§€í‚´ì´ 보기 + permission_add_issue_watchers: ì¼ê°ì§€í‚´ì´ 추가 + permission_log_time: 작업시간 ê¸°ë¡ + permission_view_time_entries: 시간입력 보기 + permission_edit_time_entries: 시간입력 편집 + permission_edit_own_time_entries: ë‚´ 시간입력 편집 + permission_manage_news: 뉴스 관리 + permission_comment_news: ë‰´ìŠ¤ì— ëŒ“ê¸€ë‹¬ê¸° + permission_manage_documents: 문서 관리 + permission_view_documents: 문서 보기 + permission_manage_files: 파ì¼ê´€ë¦¬ + permission_view_files: 파ì¼ë³´ê¸° + permission_manage_wiki: 위키 관리 + permission_rename_wiki_pages: 위키 페ì´ì§€ ì´ë¦„변경 + permission_delete_wiki_pages: 위치 페ì´ì§€ ì‚­ì œ + permission_view_wiki_pages: 위키 보기 + permission_view_wiki_edits: 위키 ê¸°ë¡ ë³´ê¸° + permission_edit_wiki_pages: 위키 페ì´ì§€ 편집 + permission_delete_wiki_pages_attachments: ì²¨ë¶€íŒŒì¼ ì‚­ì œ + permission_protect_wiki_pages: 프로ì íЏ 위키 페ì´ì§€ + permission_manage_repository: 저장소 관리 + permission_browse_repository: 저장소 둘러보기 + permission_view_changesets: 변경묶ìŒë³´ê¸° + permission_commit_access: 변경로그 보기 + permission_manage_boards: ê²Œì‹œíŒ ê´€ë¦¬ + permission_view_messages: 메시지 보기 + permission_add_messages: 메시지 추가 + permission_edit_messages: 메시지 편집 + permission_edit_own_messages: ìžê¸° 메시지 편집 + permission_delete_messages: 메시지 ì‚­ì œ + permission_delete_own_messages: ìžê¸° 메시지 ì‚­ì œ + + project_module_issue_tracking: ì¼ê°ê´€ë¦¬ + project_module_time_tracking: ì‹œê°„ì¶”ì  + project_module_news: 뉴스 + project_module_documents: 문서 + project_module_files: íŒŒì¼ + project_module_wiki: 위키 + project_module_repository: 저장소 + project_module_boards: ê²Œì‹œíŒ + + label_user: ì‚¬ìš©ìž + label_user_plural: ì‚¬ìš©ìž + label_user_new: 새 ì‚¬ìš©ìž + label_project: 프로ì íЏ + label_project_new: 새 프로ì íЏ + label_project_plural: 프로ì íЏ + label_x_projects: + zero: ì—†ìŒ + one: "한 프로ì íЏ" + other: "%{count}ê°œ 프로ì íЏ" + label_project_all: 모든 프로ì íЏ + label_project_latest: 최근 프로ì íЏ + label_issue: ì¼ê° + label_issue_new: 새 ì¼ê°ë§Œë“¤ê¸° + label_issue_plural: ì¼ê° + label_issue_view_all: 모든 ì¼ê° 보기 + label_issues_by: "%{value}별 ì¼ê°" + label_issue_added: ì¼ê° 추가 + label_issue_updated: ì¼ê° 수정 + label_document: 문서 + label_document_new: 새 문서 + label_document_plural: 문서 + label_document_added: 문서 추가 + label_role: ì—­í•  + label_role_plural: ì—­í•  + label_role_new: 새 ì—­í•  + label_role_and_permissions: ì—­í•  ë° ê¶Œí•œ + label_member: ë‹´ë‹¹ìž + label_member_new: 새 ë‹´ë‹¹ìž + label_member_plural: ë‹´ë‹¹ìž + label_tracker: ì¼ê° 유형 + label_tracker_plural: ì¼ê° 유형 + label_tracker_new: 새 ì¼ê° 유형 + label_workflow: 업무í름 + label_issue_status: ì¼ê° ìƒíƒœ + label_issue_status_plural: ì¼ê° ìƒíƒœ + label_issue_status_new: 새 ì¼ê° ìƒíƒœ + label_issue_category: ì¼ê° 범주 + label_issue_category_plural: ì¼ê° 범주 + label_issue_category_new: 새 ì¼ê° 범주 + label_custom_field: ì‚¬ìš©ìž ì •ì˜ í•­ëª© + label_custom_field_plural: ì‚¬ìš©ìž ì •ì˜ í•­ëª© + label_custom_field_new: 새 ì‚¬ìš©ìž ì •ì˜ í•­ëª© + label_enumerations: 코드값 + label_enumeration_new: 새 코드값 + label_information: ì •ë³´ + label_information_plural: ì •ë³´ + label_please_login: 로그ì¸í•˜ì„¸ìš”. + label_register: ë“±ë¡ + label_login_with_open_id_option: ë˜ëŠ” OpenID로 ë¡œê·¸ì¸ + label_password_lost: 비밀번호 찾기 + label_home: 초기화면 + label_my_page: ë‚´ 페ì´ì§€ + label_my_account: ë‚´ 계정 + label_my_projects: ë‚´ 프로ì íЏ + label_administration: 관리 + label_login: ë¡œê·¸ì¸ + label_logout: 로그아웃 + label_help: ë„ì›€ë§ + label_reported_issues: 보고한 ì¼ê° + label_assigned_to_me_issues: ë‚´ê°€ ë§¡ì€ ì¼ê° + label_last_login: 마지막 ì ‘ì† + label_registered_on: 등ë¡ì‹œê° + label_activity: 작업내역 + label_overall_activity: ì „ì²´ 작업내역 + label_user_activity: "%{value}ì˜ ìž‘ì—…ë‚´ì—­" + label_new: 새로 만들기 + label_logged_as: '로그ì¸ê³„ì •:' + label_environment: 환경 + label_authentication: ì¸ì¦ + label_auth_source: ì¸ì¦ ê³µê¸‰ìž + label_auth_source_new: 새 ì¸ì¦ ê³µê¸‰ìž + label_auth_source_plural: ì¸ì¦ ê³µê¸‰ìž + label_subproject_plural: 하위 프로ì íЏ + label_and_its_subprojects: "%{value}와 하위 프로ì íŠ¸ë“¤" + label_min_max_length: 최소 - 최대 ê¸¸ì´ + label_list: ëª©ë¡ + label_date: ë‚ ì§œ + label_integer: 정수 + label_float: ë¶€ë™ì†Œìˆ˜ + label_boolean: 부울린 + label_string: 문ìžì—´ + label_text: í…스트 + label_attribute: ì†ì„± + label_attribute_plural: ì†ì„± + label_download: "%{count}회 다운로드" + label_download_plural: "%{count}회 다운로드" + label_no_data: 표시할 ë°ì´í„°ê°€ 없습니다. + label_change_status: ìƒíƒœ 변경 + label_history: ì´ë ¥ + label_attachment: íŒŒì¼ + label_attachment_new: 파ì¼ì¶”ê°€ + label_attachment_delete: 파ì¼ì‚­ì œ + label_attachment_plural: íŒŒì¼ + label_file_added: íŒŒì¼ ì¶”ê°€ + label_report: 보고서 + label_report_plural: 보고서 + label_news: 뉴스 + label_news_new: 새 뉴스 + label_news_plural: 뉴스 + label_news_latest: 최근 뉴스 + label_news_view_all: 모든 뉴스 + label_news_added: 뉴스 추가 + label_settings: 설정 + label_overview: 개요 + label_version: 버전 + label_version_new: 새 버전 + label_version_plural: 버전 + label_confirmation: í™•ì¸ + label_export_to: 내보내기 + label_read: ì½ê¸°... + label_public_projects: 공개 프로ì íЏ + label_open_issues: 진행중 + label_open_issues_plural: 진행중 + label_closed_issues: ì™„ë£Œë¨ + label_closed_issues_plural: ì™„ë£Œë¨ + label_x_open_issues_abbr_on_total: + zero: "ì´ %{total} ê±´ ëª¨ë‘ ì™„ë£Œ" + one: "한 ê±´ ì§„í–‰ 중 / ì´ %{total} ê±´ 중 " + other: "%{count} ê±´ ì§„í–‰ 중 / ì´ %{total} ê±´" + label_x_open_issues_abbr: + zero: ëª¨ë‘ ì™„ë£Œ + one: 한 ê±´ ì§„í–‰ 중 + other: "%{count} ê±´ ì§„í–‰ 중" + label_x_closed_issues_abbr: + zero: ëª¨ë‘ ë¯¸ì™„ë£Œ + one: 한 ê±´ 완료 + other: "%{count} ê±´ 완료" + label_total: 합계 + label_permissions: 권한 + label_current_status: ì¼ê° ìƒíƒœ + label_new_statuses_allowed: 허용ë˜ëŠ” ì¼ê° ìƒíƒœ + label_all: ëª¨ë‘ + label_none: ì—†ìŒ + label_nobody: 미지정 + label_next: ë‹¤ìŒ + label_previous: 뒤로 + label_used_by: ì‚¬ìš©ë¨ + label_details: ìžì„¸ížˆ + label_add_note: ì¼ê°ë§ê¸€ 추가 + label_per_page: 페ì´ì§€ë³„ + label_calendar: 달력 + label_months_from: 개월 ë™ì•ˆ | 다ìŒë¶€í„° + label_gantt: Gantt 챠트 + label_internal: ë‚´ë¶€ + label_last_changes: "최근 %{count}ê°œì˜ ë³€ê²½ì‚¬í•­" + label_change_view_all: 모든 변경 ë‚´ì—­ 보기 + label_personalize_page: 입맛대로 구성하기 + label_comment: 댓글 + label_comment_plural: 댓글 + label_x_comments: + zero: 댓글 ì—†ìŒ + one: 한 ê°œì˜ ëŒ“ê¸€ + other: "%{count} ê°œì˜ ëŒ“ê¸€" + label_comment_add: 댓글 추가 + label_comment_added: ëŒ“ê¸€ì´ ì¶”ê°€ë˜ì—ˆìŠµë‹ˆë‹¤. + label_comment_delete: 댓글 ì‚­ì œ + label_query: ê²€ìƒ‰ì–‘ì‹ + label_query_plural: ê²€ìƒ‰ì–‘ì‹ + label_query_new: 새 ê²€ìƒ‰ì–‘ì‹ + label_filter_add: 검색조건 추가 + label_filter_plural: 검색조건 + label_equals: ì´ë‹¤ + label_not_equals: 아니다 + label_in_less_than: ì´ë‚´ + label_in_more_than: ì´í›„ + label_greater_or_equal: ">=" + label_less_or_equal: "<=" + label_in: ì´ë‚´ + label_today: 오늘 + label_all_time: 모든 시간 + label_yesterday: ì–´ì œ + label_this_week: ì´ë²ˆì£¼ + label_last_week: 지난 주 + label_last_n_days: "지난 %{count} ì¼" + label_this_month: ì´ë²ˆ 달 + label_last_month: 지난 달 + label_this_year: 올해 + label_date_range: ë‚ ì§œ 범위 + label_less_than_ago: ì´ì „ + label_more_than_ago: ì´í›„ + label_ago: ì¼ ì „ + label_contains: í¬í•¨ë˜ëŠ” 키워드 + label_not_contains: í¬í•¨í•˜ì§€ 않는 키워드 + label_day_plural: ì¼ + label_repository: 저장소 + label_repository_plural: 저장소 + label_browse: 저장소 둘러보기 + label_modification: "%{count} 변경" + label_modification_plural: "%{count} 변경" + label_revision: ê°œì •íŒ + label_revision_plural: ê°œì •íŒ + label_associated_revisions: ê´€ë ¨ëœ ê°œì •íŒë“¤ + label_added: ì¶”ê°€ë¨ + label_modified: ë³€ê²½ë¨ + label_copied: ë³µì‚¬ë¨ + label_renamed: ì´ë¦„바뀜 + label_deleted: ì‚­ì œë¨ + label_latest_revision: 최근 ê°œì •íŒ + label_latest_revision_plural: 최근 ê°œì •íŒ + label_view_revisions: ê°œì •íŒ ë³´ê¸° + label_max_size: 최대 í¬ê¸° + label_sort_highest: 맨 위로 + label_sort_higher: 위로 + label_sort_lower: 아래로 + label_sort_lowest: 맨 아래로 + label_roadmap: 로드맵 + label_roadmap_due_in: "기한 %{value}" + label_roadmap_overdue: "%{value} 지연" + label_roadmap_no_issues: ì´ ë²„ì „ì— í•´ë‹¹í•˜ëŠ” ì¼ê° ì—†ìŒ + label_search: 검색 + label_result_plural: ê²°ê³¼ + label_all_words: 모든 단어 + label_wiki: 위키 + label_wiki_edit: 위키 편집 + label_wiki_edit_plural: 위키 편집 + label_wiki_page: 위키 페ì´ì§€ + label_wiki_page_plural: 위키 페ì´ì§€ + label_index_by_title: 제목별 ìƒ‰ì¸ + label_index_by_date: 날짜별 ìƒ‰ì¸ + label_current_version: 현재 버전 + label_preview: 미리보기 + label_feed_plural: 피드(Feeds) + label_changes_details: 모든 ìƒì„¸ 변경 ë‚´ì—­ + label_issue_tracking: ì¼ê° ì¶”ì  + label_spent_time: 소요 시간 + label_f_hour: "%{value} 시간" + label_f_hour_plural: "%{value} 시간" + label_time_tracking: ì‹œê°„ì¶”ì  + label_change_plural: 변경사항들 + label_statistics: 통계 + label_commits_per_month: 월별 커밋 ë‚´ì—­ + label_commits_per_author: ì €ìžë³„ 커밋 ë‚´ì—­ + label_view_diff: ì°¨ì´ì  보기 + label_diff_inline: 한줄로 + label_diff_side_by_side: ë‘줄로 + label_options: 옵션 + label_copy_workflow_from: 업무í름 복사하기 + label_permissions_report: 권한 보고서 + label_watched_issues: 지켜보고 있는 ì¼ê° + label_related_issues: ì—°ê²°ëœ ì¼ê° + label_applied_status: ì ìš©ëœ ìƒíƒœ + label_loading: ì½ëŠ” 중... + label_relation_new: 새 관계 + label_relation_delete: 관계 지우기 + label_relates_to: "ë‹¤ìŒ ì¼ê°ê³¼ 관련ë¨:" + label_duplicates: "ë‹¤ìŒ ì¼ê°ì— 중복ë¨:" + label_duplicated_by: "ì¤‘ë³µëœ ì¼ê°:" + label_blocks: "ë‹¤ìŒ ì¼ê°ì˜ í•´ê²°ì„ ë§‰ê³  있ìŒ:" + label_blocked_by: "ë‹¤ìŒ ì¼ê°ì—게 막혀 있ìŒ:" + label_precedes: "다ìŒì— 진행할 ì¼ê°:" + label_follows: "ë‹¤ìŒ ì¼ê°ì„ ìš°ì„  ì§„í–‰:" + label_end_to_start: "ëì—서 시작" + label_end_to_end: "ëì—서 ë" + label_start_to_start: "시작ì—서 시작" + label_start_to_end: "시작ì—서 ë" + label_stay_logged_in: ë¡œê·¸ì¸ ìœ ì§€ + label_disabled: 비활성화 + label_show_completed_versions: ì™„ë£Œëœ ë²„ì „ 보기 + label_me: 나 + label_board: ê²Œì‹œíŒ + label_board_new: 새 ê²Œì‹œíŒ + label_board_plural: ê²Œì‹œíŒ + label_topic_plural: 주제 + label_message_plural: 글 + label_message_last: 마지막 글 + label_message_new: 새글쓰기 + label_message_posted: 글 추가 + label_reply_plural: 답글 + label_send_information: 사용ìžì—게 계정정보를 보내기 + label_year: ë…„ + label_month: ì›” + label_week: 주 + label_date_from: '기간:' + label_date_to: ' ~ ' + label_language_based: ì–¸ì–´ì„¤ì •ì— ë”°ë¦„ + label_sort_by: "%{value}(으)로 ì •ë ¬" + label_send_test_email: 테스트 ë©”ì¼ ë³´ë‚´ê¸° + label_feeds_access_key_created_on: "피드 ì ‘ê·¼ 키가 %{value} ì´ì „ì— ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤." + label_module_plural: 모듈 + label_added_time_by: "%{author}ì´(ê°€) %{age} ì „ì— ì¶”ê°€í•¨" + label_updated_time_by: "%{author}ì´(ê°€) %{age} ì „ì— ë³€ê²½" + label_updated_time: "%{value} ì „ì— ìˆ˜ì •ë¨" + label_jump_to_a_project: 프로ì íЏ 바로가기 + label_file_plural: íŒŒì¼ + label_changeset_plural: ë³€ê²½ë¬¶ìŒ + label_default_columns: 기본 컬럼 + label_no_change_option: (수정 안함) + label_bulk_edit_selected_issues: ì„ íƒí•œ ì¼ê°ë“¤ì„ í•œêº¼ë²ˆì— ìˆ˜ì •í•˜ê¸° + label_theme: 테마 + label_default: 기본 + label_search_titles_only: 제목ì—서만 찾기 + label_user_mail_option_all: "ë‚´ê°€ ì†í•œ 프로ì íŠ¸ë¡œë“¤ë¶€í„° 모든 ë©”ì¼ ë°›ê¸°" + label_user_mail_option_selected: "ì„ íƒí•œ 프로ì íŠ¸ë“¤ë¡œë¶€í„° 모든 ë©”ì¼ ë°›ê¸°.." + label_user_mail_no_self_notified: "ë‚´ê°€ 만든 ë³€ê²½ì‚¬í•­ë“¤ì— ëŒ€í•´ì„œëŠ” 알림메ì¼ì„ 받지 않습니다." + label_registration_activation_by_email: ë©”ì¼ë¡œ ê³„ì •ì„ í™œì„±í™”í•˜ê¸° + label_registration_automatic_activation: ìžë™ 계정 활성화 + label_registration_manual_activation: ìˆ˜ë™ ê³„ì • 활성화 + label_display_per_page: "페ì´ì§€ë‹¹ 줄수: %{value}" + label_age: 마지막 ìˆ˜ì •ì¼ + label_change_properties: ì†ì„± 변경 + label_general: ì¼ë°˜ + label_more: 제목 ë° ì„¤ëª… 수정 + label_scm: 형ìƒê´€ë¦¬ì‹œìŠ¤í…œ + label_plugins: í”ŒëŸ¬ê·¸ì¸ + label_ldap_authentication: LDAP ì¸ì¦ + label_downloads_abbr: D/L + label_optional_description: 부가ì ì¸ 설명 + label_add_another_file: 다른 íŒŒì¼ ì¶”ê°€ + label_preferences: 설정 + label_chronological_order: 시간 순으로 ì •ë ¬ + label_reverse_chronological_order: 시간 역순으로 ì •ë ¬ + label_planning: 프로ì íŠ¸ê³„íš + label_incoming_emails: 수신 ë©”ì¼ + label_generate_key: 키 ìƒì„± + label_issue_watchers: ì¼ê°ì§€í‚´ì´ + label_example: 예 + label_display: í‘œì‹œë°©ì‹ + label_sort: ì •ë ¬ + label_ascending: 오름차순 + label_descending: 내림차순 + label_date_from_to: "%{start}부터 %{end}까지" + label_wiki_content_added: 위키페ì´ì§€ 추가 + label_wiki_content_updated: 위키페ì´ì§€ 수정 + + button_login: ë¡œê·¸ì¸ + button_submit: í™•ì¸ + button_save: 저장 + button_check_all: 모ë‘ì„ íƒ + button_uncheck_all: ì„ íƒí•´ì œ + button_delete: ì‚­ì œ + button_create: 만들기 + button_create_and_continue: 만들고 계ì†í•˜ê¸° + button_test: 테스트 + button_edit: 편집 + button_add: 추가 + button_change: 변경 + button_apply: ì ìš© + button_clear: 지우기 + button_lock: 잠금 + button_unlock: 잠금해제 + button_download: 다운로드 + button_list: ëª©ë¡ + button_view: 보기 + button_move: ì´ë™ + button_back: 뒤로 + button_cancel: 취소 + button_activate: 활성화 + button_sort: ì •ë ¬ + button_log_time: 작업시간 ê¸°ë¡ + button_rollback: ì´ ë²„ì „ìœ¼ë¡œ ë˜ëŒë¦¬ê¸° + button_watch: 지켜보기 + button_unwatch: 관심ë„기 + button_reply: 답글 + button_archive: 잠금보관 + button_unarchive: 잠금보관해제 + button_reset: 초기화 + button_rename: ì´ë¦„바꾸기 + button_change_password: 비밀번호 바꾸기 + button_copy: 복사 + button_annotate: ì´ë ¥í•´ì„¤ + button_update: 수정 + button_configure: 설정 + button_quote: 댓글달기 + + status_active: 사용중 + status_registered: 등ë¡ëŒ€ê¸° + status_locked: ìž ê¹€ + + text_select_mail_notifications: 알림메ì¼ì´ 필요한 ìž‘ì—…ì„ ì„ íƒí•˜ì„¸ìš”. + text_regexp_info: 예) ^[A-Z0-9]+$ + text_min_max_length_info: 0 는 ì œí•œì´ ì—†ìŒì„ ì˜ë¯¸í•¨ + text_project_destroy_confirmation: ì´ í”„ë¡œì íŠ¸ë¥¼ 삭제하고 모든 ë°ì´í„°ë¥¼ 지우시겠습니까? + text_subprojects_destroy_warning: "하위 프로ì íЏ(%{value})ì´(ê°€) ìžë™ìœ¼ë¡œ 지워질 것입니다." + text_workflow_edit: 업무íë¦„ì„ ìˆ˜ì •í•˜ë ¤ë©´ ì—­í• ê³¼ ì¼ê° ìœ í˜•ì„ ì„ íƒí•˜ì„¸ìš”. + text_are_you_sure: ê³„ì† ì§„í–‰ 하시겠습니까? + text_tip_issue_begin_day: 오늘 시작하는 업무(task) + text_tip_issue_end_day: 오늘 종료하는 업무(task) + text_tip_issue_begin_end_day: 오늘 시작하고 종료하는 업무(task) + text_caracters_maximum: "최대 %{count} ê¸€ìž ê°€ëŠ¥" + text_caracters_minimum: "최소한 %{count} ê¸€ìž ì´ìƒì´ì–´ì•¼ 합니다." + text_length_between: "%{min} ì—서 %{max} 글ìž" + text_tracker_no_workflow: ì´ ì¼ê° 유형ì—는 업무íë¦„ì´ ì •ì˜ë˜ì§€ 않았습니다. + text_unallowed_characters: 허용ë˜ì§€ 않는 문ìžì—´ + text_comma_separated: "구분ìž','를 ì´ìš©í•´ì„œ 여러 ê°œì˜ ê°’ì„ ìž…ë ¥í•  수 있습니다." + text_issues_ref_in_commit_messages: 커밋 메시지ì—서 ì¼ê°ì„ 참조하거나 해결하기 + text_issue_added: "%{author}ì´(ê°€) ì¼ê° %{id}ì„(를) 보고하였습니다." + text_issue_updated: "%{author}ì´(ê°€) ì¼ê° %{id}ì„(를) 수정하였습니다." + text_wiki_destroy_confirmation: ì´ ìœ„í‚¤ì™€ 모든 ë‚´ìš©ì„ ì§€ìš°ì‹œê² ìŠµë‹ˆê¹Œ? + text_issue_category_destroy_question: "ì¼ë¶€ ì¼ê°ë“¤(%{count}ê°œ)ì´ ì´ ë²”ì£¼ì— ì§€ì •ë˜ì–´ 있습니다. 어떻게 하시겠습니까?" + text_issue_category_destroy_assignments: 범주 지정 지우기 + text_issue_category_reassign_to: ì¼ê°ì„ ì´ ë²”ì£¼ì— ë‹¤ì‹œ 지정하기 + text_user_mail_option: "ì„ íƒí•˜ì§€ ì•Šì€ í”„ë¡œì íЏì—서ë„, 지켜보는 중ì´ê±°ë‚˜ ì†í•´ìžˆëŠ” 사항(ì¼ê°ë¥¼ 발행했거나 í• ë‹¹ëœ ê²½ìš°)ì´ ìžˆìœ¼ë©´ 알림메ì¼ì„ 받게 ë©ë‹ˆë‹¤." + text_no_configuration_data: "ì—­í• , ì¼ê° 유형, ì¼ê° ìƒíƒœë“¤ê³¼ 업무íë¦„ì´ ì•„ì§ ì„¤ì •ë˜ì§€ 않았습니다.\n기본 ì„¤ì •ì„ ì½ì–´ë“¤ì´ëŠ” ê²ƒì„ ê¶Œìž¥í•©ë‹ˆë‹¤. ì½ì–´ë“¤ì¸ í›„ì— ìˆ˜ì •í•  수 있습니다." + text_load_default_configuration: 기본 ì„¤ì •ì„ ì½ì–´ë“¤ì´ê¸° + text_status_changed_by_changeset: "ë³€ê²½ë¬¶ìŒ %{value}ì— ì˜í•˜ì—¬ 변경ë¨" + text_issues_destroy_confirmation: 'ì„ íƒí•œ ì¼ê°ë¥¼ ì •ë§ë¡œ 삭제하시겠습니까?' + text_select_project_modules: 'ì´ í”„ë¡œì íЏì—서 활성화시킬 ëª¨ë“ˆì„ ì„ íƒí•˜ì„¸ìš”:' + text_default_administrator_account_changed: 기본 ê´€ë¦¬ìž ê³„ì •ì´ ë³€ê²½ + text_file_repository_writable: íŒŒì¼ ì €ìž¥ì†Œ 쓰기 가능 + text_plugin_assets_writable: í”ŒëŸ¬ê·¸ì¸ ì „ìš© 디렉토리가 쓰기 가능 + text_rmagick_available: RMagick 사용 가능 (ì„ íƒì ) + text_destroy_time_entries_question: 삭제하려는 ì¼ê°ì— %{hours} ì‹œê°„ì´ ë³´ê³ ë˜ì–´ 있습니다. 어떻게 하시겠습니까? + text_destroy_time_entries: ë³´ê³ ëœ ì‹œê°„ì„ ì‚­ì œí•˜ê¸° + text_assign_time_entries_to_project: ë³´ê³ ëœ ì‹œê°„ì„ í”„ë¡œì íŠ¸ì— í• ë‹¹í•˜ê¸° + text_reassign_time_entries: 'ì´ ì•Œë¦¼ì— ë³´ê³ ëœ ì‹œê°„ì„ ìž¬í• ë‹¹í•˜ê¸°:' + text_user_wrote: "%{value}ì˜ ë§ê¸€:" + text_enumeration_category_reassign_to: '새로운 ê°’ì„ ì„¤ì •:' + text_enumeration_destroy_question: "%{count} ê°œì˜ ì¼ê°ì´ ì´ ê°’ì„ ì‚¬ìš©í•˜ê³  있습니다." + text_email_delivery_not_configured: "ì´ë©”ì¼ ì „ë‹¬ì´ ì„¤ì •ë˜ì§€ 않았습니다. 그래서 ì•Œë¦¼ì´ ë¹„í™œì„±í™”ë˜ì—ˆìŠµë‹ˆë‹¤.\n SMTP서버를 config/configuration.ymlì—서 설정하고 어플리케ì´ì…˜ì„ 다시 시작하십시오. 그러면 ë™ìž‘합니다." + text_repository_usernames_mapping: "저장소 로그ì—서 ë°œê²¬ëœ ê° ì‚¬ìš©ìžì— ë ˆë“œë§ˆì¸ ì‚¬ìš©ìžë¥¼ ì—…ë°ì´íŠ¸í• ë•Œ ì„ íƒí•©ë‹ˆë‹¤.\n레드마ì¸ê³¼ ì €ìž¥ì†Œì˜ ì´ë¦„ì´ë‚˜ ì´ë©”ì¼ì´ ê°™ì€ ì‚¬ìš©ìžê°€ ìžë™ìœ¼ë¡œ ì—°ê²°ë©ë‹ˆë‹¤." + text_diff_truncated: '... ì´ ì°¨ì´ì ì€ 표시할 수 있는 최대 줄수를 초과해서 ì´ ì°¨ì´ì ì€ 잘렸습니다.' + text_custom_field_possible_values_info: 'ê° ê°’ 당 한 줄' + text_wiki_page_destroy_question: ì´ íŽ˜ì´ì§€ëŠ” %{descendants} ê°œì˜ í•˜ìœ„ 페ì´ì§€ì™€ 관련 ë‚´ìš©ì´ ìžˆìŠµë‹ˆë‹¤. ì´ ë‚´ìš©ì„ ì–´ë–»ê²Œ 하시겠습니까? + text_wiki_page_nullify_children: 하위 페ì´ì§€ë¥¼ 최ìƒìœ„ 페ì´ì§€ 아래로 지정 + text_wiki_page_destroy_children: 모든 하위 페ì´ì§€ì™€ 관련 ë‚´ìš©ì„ ì‚­ì œ + text_wiki_page_reassign_children: 하위 페ì´ì§€ë¥¼ ì´ íŽ˜ì´ì§€ 아래로 지정 + + default_role_manager: ê´€ë¦¬ìž + default_role_developer: ê°œë°œìž + default_role_reporter: ë³´ê³ ìž + default_tracker_bug: 결함 + default_tracker_feature: 새기능 + default_tracker_support: ì§€ì› + default_issue_status_new: ì‹ ê·œ + default_issue_status_in_progress: ì§„í–‰ + default_issue_status_resolved: í•´ê²° + default_issue_status_feedback: ì˜ê²¬ + default_issue_status_closed: 완료 + default_issue_status_rejected: ê±°ì ˆ + default_doc_category_user: ì‚¬ìš©ìž ë¬¸ì„œ + default_doc_category_tech: 기술 문서 + default_priority_low: ë‚®ìŒ + default_priority_normal: 보통 + default_priority_high: ë†’ìŒ + default_priority_urgent: 긴급 + default_priority_immediate: 즉시 + default_activity_design: 설계 + default_activity_development: 개발 + + enumeration_issue_priorities: ì¼ê° 우선순위 + enumeration_doc_categories: 문서 범주 + enumeration_activities: 작업분류(시간추ì ) + + field_issue_to: 관련 ì¼ê° + label_view_all_revisions: 모든 ê°œì •íŒ í‘œì‹œ + label_tag: 태그(Tag) + label_branch: 브랜치(Branch) + error_no_tracker_in_project: 사용할 수 있ë„ë¡ ì„¤ì •ëœ ì¼ê° ìœ í˜•ì´ ì—†ìŠµë‹ˆë‹¤. 프로ì íЏ ì„¤ì •ì„ í™•ì¸í•˜ì‹­ì‹œì˜¤. + error_no_default_issue_status: '기본 ìƒíƒœê°€ ì •í•´ì ¸ 있지 않습니다. ì„¤ì •ì„ í™•ì¸í•˜ì‹­ì‹œì˜¤. (주 ë©”ë‰´ì˜ "관리" -> "ì¼ê° ìƒíƒœ")' + text_journal_changed: "%{label}ì„(를) %{old}ì—서 %{new}(으)로 변경ë˜ì—ˆìŠµë‹ˆë‹¤." + text_journal_set_to: "%{label}ì„(를) %{value}(으)로 지정ë˜ì—ˆìŠµë‹ˆë‹¤." + text_journal_deleted: "%{label} ê°’ì´ ì§€ì›Œì¡ŒìŠµë‹ˆë‹¤. (%{old})" + label_group_plural: 그룹 + label_group: 그룹 + label_group_new: 새 그룹 + label_time_entry_plural: 작업시간 + text_journal_added: "%{label}ì— %{value}ì´(ê°€) 추가ë˜ì—ˆìŠµë‹ˆë‹¤." + field_active: 사용중 + enumeration_system_activity: 시스템 작업 + permission_delete_issue_watchers: ì¼ê°ì§€í‚´ì´ 지우기 + version_status_closed: 닫힘 + version_status_locked: ìž ê¹€ + version_status_open: ì§„í–‰ + error_can_not_reopen_issue_on_closed_version: 닫힌 ë²„ì „ì— í• ë‹¹ëœ ì¼ê°ì€ 다시 재발ìƒì‹œí‚¬ 수 없습니다. + label_user_anonymous: ì´ë¦„ì—†ìŒ + button_move_and_follow: ì´ë™í•˜ê³  ë”°ë¼ê°€ê¸° + setting_default_projects_modules: 새 프로ì íŠ¸ì— ê¸°ë³¸ì ìœ¼ë¡œ í™œì„±í™”ë  ëª¨ë“ˆ + setting_gravatar_default: 기본 ê·¸ë¼ë°”타 ì´ë¯¸ì§€ + field_sharing: 공유 + label_version_sharing_hierarchy: ìƒìœ„ ë° í•˜ìœ„ 프로ì íЏ + label_version_sharing_system: 모든 프로ì íЏ + label_version_sharing_descendants: 하위 프로ì íЏ + label_version_sharing_tree: 최ìƒìœ„ ë° ëª¨ë“  하위 프로ì íЏ + label_version_sharing_none: ê³µìœ ì—†ìŒ + error_can_not_archive_project: ì´ í”„ë¡œì íŠ¸ë¥¼ 잠금보관할 수 없습니다. + button_duplicate: 복제 + button_copy_and_follow: 복사하고 ë”°ë¼ê°€ê¸° + label_copy_source: ì›ë³¸ + setting_issue_done_ratio: ì¼ê°ì˜ ì§„ì²™ë„ ê³„ì‚°ë°©ë²• + setting_issue_done_ratio_issue_status: ì¼ê° ìƒíƒœë¥¼ 사용하기 + error_issue_done_ratios_not_updated: ì¼ê° ì§„ì²™ë„ê°€ 수정ë˜ì§€ 않았습니다. + error_workflow_copy_target: ëŒ€ìƒ ì¼ê°ì˜ 유형과 ì—­í• ì„ ì„ íƒí•˜ì„¸ìš”. + setting_issue_done_ratio_issue_field: ì¼ê° 수정ì—서 ì§„ì²™ë„ ìž…ë ¥í•˜ê¸° + label_copy_same_as_target: 대ìƒê³¼ ê°™ìŒ. + label_copy_target: ëŒ€ìƒ + notice_issue_done_ratios_updated: ì¼ê° ì§„ì²™ë„ê°€ 수정ë˜ì—ˆìŠµë‹ˆë‹¤. + error_workflow_copy_source: ì›ë³¸ ì¼ê°ì˜ 유형ì´ë‚˜ ì—­í• ì„ ì„ íƒí•˜ì„¸ìš”. + label_update_issue_done_ratios: 모든 ì¼ê° ì§„ì²™ë„ ê°±ì‹ í•˜ê¸° + setting_start_of_week: 달력 시작 ìš”ì¼ + permission_view_issues: ì¼ê° 보기 + label_display_used_statuses_only: ì´ ì¼ê° 유형ì—서 사용ë˜ëŠ” ìƒíƒœë§Œ 보여주기 + label_revision_id: ê°œì •íŒ %{value} + label_api_access_key: API 접근키 + label_api_access_key_created_on: API 접근키가 %{value} ì „ì— ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤. + label_feeds_access_key: RSS 접근키 + notice_api_access_key_reseted: API 접근키가 초기화ë˜ì—ˆìŠµë‹ˆë‹¤. + setting_rest_api_enabled: REST 웹서비스 활성화 + label_missing_api_access_key: API 접근키가 없습니다. + label_missing_feeds_access_key: RSS 접근키가 없습니다. + button_show: 보기 + text_line_separated: 여러 ê°’ì´ í—ˆìš©ë¨(ê°’ 마다 한 줄씩) + setting_mail_handler_body_delimiters: ë©”ì¼ ë³¸ë¬¸ êµ¬ë¶„ìž + permission_add_subprojects: 하위 프로ì íЏ 만들기 + label_subproject_new: 새 하위 프로ì íЏ + text_own_membership_delete_confirmation: |- + 권한들 ì¼ë¶€ ë˜ëŠ” 전부를 막 삭제하려고 하고 있습니다. 그렇게 ë˜ë©´ ì´ í”„ë¡œì íŠ¸ë¥¼ ë”ì´ìƒ 수정할 수 없게 ë©ë‹ˆë‹¤. + 계ì†í•˜ì‹œê² ìŠµë‹ˆê¹Œ? + label_close_versions: ì™„ë£Œëœ ë²„ì „ 닫기 + label_board_sticky: ë¶™ë°•ì´ + label_board_locked: 잠금 + permission_export_wiki_pages: 위키 페ì´ì§€ 내보내기 + setting_cache_formatted_text: 형ì‹ì„ 가진 í…스트 빠른 임시 기억 + permission_manage_project_activities: 프로ì íЏ 작업내역 관리 + error_unable_delete_issue_status: ì¼ê° ìƒíƒœë¥¼ 지울 수 없습니다. + label_profile: 사용ìžì •ë³´ + permission_manage_subtasks: 하위 ì¼ê° 관리 + field_parent_issue: ìƒìœ„ ì¼ê° + label_subtask_plural: 하위 ì¼ê° + label_project_copy_notifications: 프로ì íЏ 복사 ì¤‘ì— ì´ë©”ì¼ ì•Œë¦¼ 보내기 + error_can_not_delete_custom_field: ì‚¬ìš©ìž ì •ì˜ í•„ë“œë¥¼ 삭제할 수 없습니다. + error_unable_to_connect: ì—°ê²°í•  수 없습니다((%{value}) + error_can_not_remove_role: ì´ ì—­í• ì€ í˜„ìž¬ 사용 중ì´ì´ì„œ 삭제할 수 없습니다. + error_can_not_delete_tracker: ì´ ìœ í˜•ì˜ ì¼ê°ë“¤ì´ 있어서 삭제할 수 없습니다. + field_principal: ì‹ ì› + label_my_page_block: ë‚´ 페ì´ì§€ 출력화면 + notice_failed_to_save_members: "%{errors}:구성ì›ì„ 저장 중 실패하였습니다" + text_zoom_out: ë” ìž‘ê²Œ + text_zoom_in: ë” í¬ê²Œ + notice_unable_delete_time_entry: 시간 ê¸°ë¡ í•­ëª©ì„ ì‚­ì œí•  수 없습니다. + label_overall_spent_time: ì´ ì†Œìš”ì‹œê°„ + field_time_entries: 기ë¡ëœ 시간 + project_module_gantt: Gantt 챠트 + project_module_calendar: 달력 + button_edit_associated_wikipage: "ì—°ê´€ëœ ìœ„í‚¤ 페ì´ì§€ %{page_title} 수정" + field_text: í…스트 ì˜ì—­ + label_user_mail_option_only_owner: ë‚´ê°€ ì €ìžì¸ 사항만 + setting_default_notification_option: 기본 알림 옵션 + label_user_mail_option_only_my_events: ë‚´ê°€ 지켜보거나 ì†í•´ìžˆëŠ” 사항만 + label_user_mail_option_only_assigned: ë‚´ì—게 í• ë‹¹ëœ ì‚¬í•­ë§Œ + label_user_mail_option_none: 알림 ì—†ìŒ + field_member_of_group: í• ë‹¹ëœ ì‚¬ëžŒì˜ ê·¸ë£¹ + field_assigned_to_role: í• ë‹¹ëœ ì‚¬ëžŒì˜ ì—­í•  + notice_not_authorized_archived_project: 접근하려는 프로ì íŠ¸ëŠ” ì´ë¯¸ 잠금보관ë˜ì–´ 있습니다. + label_principal_search: "ì‚¬ìš©ìž ë° ê·¸ë£¹ 찾기:" + label_user_search: "ì‚¬ìš©ìž ì°¾ê¸°::" + field_visible: ë³´ì´ê¸° + setting_emails_header: ì´ë©”ì¼ í—¤ë” + setting_commit_logtime_activity_id: 기ë¡ëœ ì‹œê°„ì— ì ìš©í•  작업분류 + text_time_logged_by_changeset: "ë³€ê²½ë¬¶ìŒ %{value}ì—서 ì ìš©ë˜ì—ˆìŠµë‹ˆë‹¤." + setting_commit_logtime_enabled: 커밋 시ì ì— 작업 시간 ê¸°ë¡ í™œì„±í™” + notice_gantt_chart_truncated: "표시할 수 있는 최대 항목수(%{max})를 초과하여 차트가 잘렸습니다." + setting_gantt_items_limit: "Gantt ì°¨íŠ¸ì— í‘œì‹œë˜ëŠ” 최대 항목수" + field_warn_on_leaving_unsaved: "저장하지 ì•Šì€ íŽ˜ì´ì§€ë¥¼ 빠져나갈 때 나ì—게 알림" + text_warn_on_leaving_unsaved: "현재 페ì´ì§€ëŠ” 저장ë˜ì§€ ì•Šì€ ë¬¸ìžê°€ 있습니다. ì´ íŽ˜ì´ì§€ë¥¼ 빠져나가면 ë‚´ìš©ì„ ìžƒì„것입니다." + label_my_queries: "ë‚´ 검색 ì–‘ì‹" + text_journal_changed_no_detail: "%{label}ì´ ë³€ê²½ë˜ì—ˆìŠµë‹ˆë‹¤." + label_news_comment_added: "ë‰´ìŠ¤ì— ì„¤ëª…ì´ ì¶”ê°€ë˜ì—ˆìŠµë‹ˆë‹¤." + button_expand_all: "ëª¨ë‘ í™•ìž¥" + button_collapse_all: "ëª¨ë‘ ì¶•ì†Œ" + label_additional_workflow_transitions_for_assignee: "사용ìžê°€ 작업ìžì¼ 때 허용ë˜ëŠ” 추가 ìƒíƒœ" + label_additional_workflow_transitions_for_author: "사용ìžê°€ ì €ìžì¼ 때 허용ë˜ëŠ” 추가 ìƒíƒœ" + label_bulk_edit_selected_time_entries: "ì„ íƒëœ 소요 시간 대량 편집" + text_time_entries_destroy_confirmation: "ì„ íƒí•œ 소요 시간 í•­ëª©ì„ ì‚­ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" + label_role_anonymous: Anonymous + label_role_non_member: Non member + + label_issue_note_added: "ë§ê¸€ì´ 추가ë˜ì—ˆìŠµë‹ˆë‹¤." + label_issue_status_updated: "ìƒíƒœê°€ 변경ë˜ì—ˆìŠµë‹ˆë‹¤." + label_issue_priority_updated: "ìš°ì„  순위가 변경ë˜ì—ˆìŠµë‹ˆë‹¤." + label_issues_visibility_own: "ì¼ê°ì„ ìƒì„±í•˜ê±°ë‚˜ ë§¡ì€ ì‚¬ìš©ìž" + field_issues_visibility: "ì¼ê° ë³´ìž„" + label_issues_visibility_all: "모든 ì¼ê°" + permission_set_own_issues_private: "ìžì‹ ì˜ ì¼ê°ì„ 공개나 비공개로 설정" + field_is_private: "비공개" + permission_set_issues_private: "ì¼ê°ì„ 공개나 비공개로 설정" + label_issues_visibility_public: "모든 비공개 ì¼ê°" + text_issues_destroy_descendants_confirmation: "%{count} ê°œì˜ í•˜ìœ„ ì¼ê°ì„ 삭제할 것입니다." + field_commit_logs_encoding: "커밋(commit) ê¸°ë¡ ì¸ì½”딩" + field_scm_path_encoding: "경로 ì¸ì½”딩" + text_scm_path_encoding_note: "기본: UTF-8" + field_path_to_repository: "저장소 경로" + field_root_directory: "루트 경로" + field_cvs_module: "모듈" + field_cvsroot: "CVS 루트" + text_mercurial_repository_note: "로컬 저장소 (예: /hgrepo, c:\\hgrepo)" + text_scm_command: "명령" + text_scm_command_version: "버전" + label_git_report_last_commit: "파ì¼ì´ë‚˜ í´ë”ì˜ ë§ˆì§€ë§‰ 커밋(commit)ì„ ë³´ê³ " + text_scm_config: "SCM ëª…ë ¹ì„ config/configuration.ymlì—서 수정할 수 있습니다. 수정후ì—는 재시작하십시오." + text_scm_command_not_available: "SCM ëª…ë ¹ì„ ì‚¬ìš©í•  수 없습니다. 관리 페ì´ì§€ì˜ ì„¤ì •ì„ ê²€ì‚¬í•˜ì‹­ì‹œì˜¤." + notice_issue_successful_create: "%{id} ì¼ê°ì´ ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤." + label_between: "사ì´" + setting_issue_group_assignment: "ê·¸ë£¹ì— ì¼ê° 할당 허용" + label_diff: "비êµ(diff)" + text_git_repository_note: "ë¡œì»¬ì˜ bare 저장소 (예: /gitrepo, c:\\gitrepo)" + description_query_sort_criteria_direction: "ì •ë ¬ ë°©í–¥" + description_project_scope: "검색 범위" + description_filter: "검색 ì¡°ê±´" + description_user_mail_notification: "ë©”ì¼ ì•Œë¦¼ 설정" + description_date_from: "시작 ë‚ ì§œ ìž…ë ¥" + description_message_content: "메세지 ë‚´ìš©" + description_available_columns: "가능한 컬럼" + description_date_range_interval: "시작과 ë 날짜로 범위를 ì„ íƒí•˜ì‹­ì‹œì˜¤." + description_issue_category_reassign: "ì¼ê° 범주를 ì„ íƒí•˜ì‹­ì‹œì˜¤." + description_search: "검색항목" + description_notes: "ë§ê¸€" + description_date_range_list: "목ë¡ì—서 범위를 ì„ íƒ í•˜ì‹­ì‹œì˜¤." + description_choose_project: "프로ì íЏ" + description_date_to: "종료 ë‚ ì§œ ìž…ë ¥" + description_query_sort_criteria_attribute: "ì •ë ¬ ì†ì„±" + description_wiki_subpages_reassign: "새로운 ìƒìœ„ 페ì´ì§€ë¥¼ ì„ íƒí•˜ì‹­ì‹œì˜¤." + description_selected_columns: "ì„ íƒëœ 컬럼" + label_parent_revision: "ìƒìœ„" + label_child_revision: "하위" + error_scm_annotate_big_text_file: "최대 í…스트 íŒŒì¼ í¬ê¸°ë¥¼ 초과 하면 í•­ëª©ì€ ì´ë ¥í™” ë  ìˆ˜ 없습니다." + setting_default_issue_start_date_to_creation_date: "새로운 ì¼ê°ì˜ 시작 날짜로 오늘 ë‚ ì§œ 사용" + button_edit_section: "ì´ ë¶€ë¶„ 수정" + setting_repositories_encodings: "첨부파ì¼ì´ë‚˜ 저장소 ì¸ì½”딩" + description_all_columns: "모든 컬럼" + button_export: "내보내기" + label_export_options: "내보내기 옵션: %{export_format}" + error_attachment_too_big: "ì´ íŒŒì¼ì€ ì œí•œëœ í¬ê¸°(%{max_size})를 초과하였기 ë•Œë¬¸ì— ì—…ë¡œë“œ í•  수 없습니다." + + notice_failed_to_save_time_entries: "%{total} ê°œì˜ ì‹œê°„ìž…ë ¥ì¤‘ ë‹¤ìŒ %{count} ê°œì˜ ì €ìž¥ì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤:: %{ids}." + label_x_issues: + zero: 0 ì¼ê° + one: 1 ì¼ê° + other: "%{count} ì¼ê°" + label_repository_new: 저장소 추가 + field_repository_is_default: 주 저장소 + label_copy_attachments: ì²¨ë¶€íŒŒì¼ ë³µì‚¬ + label_item_position: "%{position}/%{count}" + label_completed_versions: 완료 버전 + text_project_identifier_info: "소문ìž(a-z),숫ìž,대쉬(-)와 밑줄(_)ë§Œ 가능합니다.
    ì‹ë³„ìžëŠ” 저장후ì—는 수정할 수 없습니다." + field_multiple: 복수선íƒê°€ëŠ¥ + setting_commit_cross_project_ref: 다른 프로ì íŠ¸ì˜ ì¼ê° 참조 ë° ìˆ˜ì • 허용 + text_issue_conflict_resolution_add_notes: ë³€ê²½ë‚´ìš©ì€ ì·¨ì†Œí•˜ê³  ë§ê¸€ë§Œ 추가 + text_issue_conflict_resolution_overwrite: 변경내용 ê°•ì œì ìš© (ì´ì „ ë§ê¸€ì„ 제외하고 ë®ì–´ ì”니다) + notice_issue_update_conflict: ì¼ê°ì´ 수정ë˜ëŠ” ë™ì•ˆ 다른 사용ìžì— ì˜í•´ì„œ 변경ë˜ì—ˆìŠµë‹ˆë‹¤. + text_issue_conflict_resolution_cancel: "ë³€ê²½ë‚´ìš©ì„ ë˜ëŒë¦¬ê³  다시 표시 %{link}" + permission_manage_related_issues: ì—°ê²°ëœ ì¼ê° 관리 + field_auth_source_ldap_filter: LDAP í•„í„° + label_search_for_watchers: 추가할 ì¼ê°ì§€í‚´ì´ 검색 + notice_account_deleted: ë‹¹ì‹ ì˜ ê³„ì •ì´ ì™„ì „ížˆ ì‚­ì œë˜ì—ˆìŠµë‹ˆë‹¤. + setting_unsubscribe: 사용ìžë“¤ì´ ìžì‹ ì˜ ê³„ì •ì„ ì‚­ì œí† ë¡ í—ˆìš© + button_delete_my_account: ë‚˜ì˜ ê³„ì • ì‚­ì œ + text_account_destroy_confirmation: |- + 계ì†í•˜ì‹œê² ìŠµë‹ˆê¹Œ? + ê³„ì •ì´ ì‚­ì œë˜ë©´ 복구할 수 없습니다. + error_session_expired: ë‹¹ì‹ ì˜ ì„¸ì…˜ì´ ë§Œë£Œë˜ì—ˆìŠµë‹ˆë‹¤. 다시 로그ì¸í•˜ì„¸ìš”. + text_session_expiration_settings: "경고: ì´ ì„¤ì •ì„ ë°”ê¾¸ë©´ ë‹¹ì‹ ì„ í¬í•¨í•˜ì—¬ í˜„ìž¬ì˜ ì„¸ì…˜ë“¤ì„ ë§Œë£Œì‹œí‚¬ 수 있습니다." + setting_session_lifetime: 세션 최대 시간 + setting_session_timeout: 세션 비활성화 타임아웃 + label_session_expiration: 세션 만료 + permission_close_project: 프로ì íŠ¸ë¥¼ 닫거나 다시 열기 + label_show_closed_projects: 닫힌 프로ì íЏ 보기 + button_close: 닫기 + button_reopen: 다시 열기 + project_status_active: 사용중 + project_status_closed: 닫힘 + project_status_archived: 잠금보관 + text_project_closed: ì´ í”„ë¡œì íŠ¸ëŠ” 닫혀 있으며 ì½ê¸° 전용입니다. + notice_user_successful_create: ì‚¬ìš©ìž %{id} ì´(ê°€) ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤. + field_core_fields: 표준 항목들 + field_timeout: 타임아웃 (ì´ˆ) + setting_thumbnails_enabled: 첨부파ì¼ì˜ ì¸ë„¤ì¼ì„ 보여줌 + setting_thumbnails_size: ì¸ë„¤ì¼ í¬ê¸° (픽셀) + label_status_transitions: ì¼ê° ìƒíƒœ 변경 + label_fields_permissions: 항목 편집 권한 + label_readonly: ì½ê¸° ì „ìš© + label_required: 필수 + text_repository_identifier_info: "소문ìž(a-z),숫ìž,대쉬(-)와 밑줄(_)ë§Œ 가능합니다.
    ì‹ë³„ìžëŠ” 저장후ì—는 수정할 수 없습니다." + field_board_parent: Parent forum + label_attribute_of_project: "프로ì íŠ¸ì˜ %{name}" + label_attribute_of_author: "ì €ìžì˜ %{name}" + label_attribute_of_assigned_to: "담당ìžì˜ %{name}" + label_attribute_of_fixed_version: "ëª©í‘œë²„ì „ì˜ %{name}" + label_copy_subtasks: 하위 ì¼ê°ë“¤ì„ 복사 + label_copied_to: "ë‹¤ìŒ ì¼ê°ìœ¼ë¡œ 복사ë¨:" + label_copied_from: "ë‹¤ìŒ ì¼ê°ìœ¼ë¡œë¶€í„° 복사ë¨:" + label_any_issues_in_project: ë‹¤ìŒ í”„ë¡œì íŠ¸ì— ì†í•œ 아무 ì¼ê° + label_any_issues_not_in_project: ë‹¤ìŒ í”„ë¡œì íŠ¸ì— ì†í•˜ì§€ ì•Šì€ ì•„ë¬´ ì¼ê° + field_private_notes: 비공개 ë§ê¸€ + permission_view_private_notes: 비공개 ë§ê¸€ 보기 + permission_set_notes_private: ë§ê¸€ì„ 비공개로 설정 + label_no_issues_in_project: ë‹¤ìŒ í”„ë¡œì íЏ ë‚´ì—서 해당 ì¼ê° ì—†ìŒ + label_any: ëª¨ë‘ + label_last_n_weeks: 최근 %{count} 주 + setting_cross_project_subtasks: 다른 프로ì íŠ¸ì˜ ì¼ê°ì„ ìƒìœ„ ì¼ê°ìœ¼ë¡œ 지정하는 ê²ƒì„ í—ˆìš© + label_cross_project_descendants: 하위 프로ì íЏ + label_cross_project_tree: 최ìƒìœ„ ë° ëª¨ë“  하위 프로ì íЏ + label_cross_project_hierarchy: ìƒìœ„ ë° í•˜ìœ„ 프로ì íЏ + label_cross_project_system: 모든 프로ì íЏ + button_hide: 숨기기 + setting_non_working_week_days: ë¹„ê·¼ë¬´ì¼ (non-working days) + label_in_the_next_days: ë‹¤ìŒ + label_in_the_past_days: 지난 diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/04/042b73a7fb39bc9f350c595849fe7907d6d24b4a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/04/042b73a7fb39bc9f350c595849fe7907d6d24b4a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,37 @@ +class RedminePluginGenerator < Rails::Generators::NamedBase + source_root File.expand_path("../templates", __FILE__) + + attr_reader :plugin_path, :plugin_name, :plugin_pretty_name + + def initialize(*args) + super + @plugin_name = file_name.underscore + @plugin_pretty_name = plugin_name.titleize + @plugin_path = "plugins/#{plugin_name}" + end + + def copy_templates + empty_directory "#{plugin_path}/app" + empty_directory "#{plugin_path}/app/controllers" + empty_directory "#{plugin_path}/app/helpers" + empty_directory "#{plugin_path}/app/models" + empty_directory "#{plugin_path}/app/views" + empty_directory "#{plugin_path}/db/migrate" + empty_directory "#{plugin_path}/lib/tasks" + empty_directory "#{plugin_path}/assets/images" + empty_directory "#{plugin_path}/assets/javascripts" + empty_directory "#{plugin_path}/assets/stylesheets" + empty_directory "#{plugin_path}/config/locales" + empty_directory "#{plugin_path}/test" + empty_directory "#{plugin_path}/test/fixtures" + empty_directory "#{plugin_path}/test/unit" + empty_directory "#{plugin_path}/test/functional" + empty_directory "#{plugin_path}/test/integration" + + template 'README.rdoc', "#{plugin_path}/README.rdoc" + template 'init.rb.erb', "#{plugin_path}/init.rb" + template 'routes.rb', "#{plugin_path}/config/routes.rb" + template 'en_rails_i18n.yml', "#{plugin_path}/config/locales/en.yml" + template 'test_helper.rb.erb', "#{plugin_path}/test/test_helper.rb" + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/04/043366b6d2160c545cba1a2e40dbe400262cc0d0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/04/043366b6d2160c545cba1a2e40dbe400262cc0d0.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,72 @@ +class Note < ActiveRecord::Base + acts_as_nested_set :scope => [:notable_id, :notable_type] +end + +class Default < ActiveRecord::Base + self.table_name = 'categories' + acts_as_nested_set +end + +class ScopedCategory < ActiveRecord::Base + self.table_name = 'categories' + acts_as_nested_set :scope => :organization +end + +class RenamedColumns < ActiveRecord::Base + acts_as_nested_set :parent_column => 'mother_id', :left_column => 'red', :right_column => 'black' +end + +class Category < ActiveRecord::Base + acts_as_nested_set + + validates_presence_of :name + + # Setup a callback that we can switch to true or false per-test + set_callback :move, :before, :custom_before_move + cattr_accessor :test_allows_move + @@test_allows_move = true + def custom_before_move + @@test_allows_move + end + + def to_s + name + end + + def recurse &block + block.call self, lambda{ + self.children.each do |child| + child.recurse &block + end + } + end +end + +class Thing < ActiveRecord::Base + acts_as_nested_set :counter_cache => 'children_count' +end + +class DefaultWithCallbacks < ActiveRecord::Base + + self.table_name = 'categories' + + attr_accessor :before_add, :after_add, :before_remove, :after_remove + + acts_as_nested_set :before_add => :do_before_add_stuff, + :after_add => :do_after_add_stuff, + :before_remove => :do_before_remove_stuff, + :after_remove => :do_after_remove_stuff + + private + + [ :before_add, :after_add, :before_remove, :after_remove ].each do |hook_name| + define_method "do_#{hook_name}_stuff" do |child_node| + self.send("#{hook_name}=", child_node) + end + end + +end + +class Broken < ActiveRecord::Base + acts_as_nested_set +end \ No newline at end of file diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/04/04353faa53c81964d7239a9938818ad1596cb331.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/04/04353faa53c81964d7239a9938818ad1596cb331.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,218 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) +require 'messages_controller' + +# Re-raise errors caught by the controller. +class MessagesController; def rescue_action(e) raise e end; end + +class MessagesControllerTest < ActionController::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules + + def setup + @controller = MessagesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_show + get :show, :board_id => 1, :id => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:board) + assert_not_nil assigns(:project) + assert_not_nil assigns(:topic) + end + + def test_show_should_contain_reply_field_tags_for_quoting + @request.session[:user_id] = 2 + get :show, :board_id => 1, :id => 1 + assert_response :success + + # tags required by MessagesController#quote + assert_tag 'input', :attributes => {:id => 'message_subject'} + assert_tag 'textarea', :attributes => {:id => 'message_content'} + assert_tag 'div', :attributes => {:id => 'reply'} + end + + def test_show_with_pagination + message = Message.find(1) + assert_difference 'Message.count', 30 do + 30.times do + message.children << Message.new(:subject => 'Reply', :content => 'Reply body', :author_id => 2, :board_id => 1) + end + end + get :show, :board_id => 1, :id => 1, :r => message.children.last(:order => 'id').id + assert_response :success + assert_template 'show' + replies = assigns(:replies) + assert_not_nil replies + assert !replies.include?(message.children.first(:order => 'id')) + assert replies.include?(message.children.last(:order => 'id')) + end + + def test_show_with_reply_permission + @request.session[:user_id] = 2 + get :show, :board_id => 1, :id => 1 + assert_response :success + assert_template 'show' + assert_tag :div, :attributes => { :id => 'reply' }, + :descendant => { :tag => 'textarea', :attributes => { :id => 'message_content' } } + end + + def test_show_message_not_found + get :show, :board_id => 1, :id => 99999 + assert_response 404 + end + + def test_show_message_from_invalid_board_should_respond_with_404 + get :show, :board_id => 999, :id => 1 + assert_response 404 + end + + def test_get_new + @request.session[:user_id] = 2 + get :new, :board_id => 1 + assert_response :success + assert_template 'new' + end + + def test_post_new + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + + with_settings :notified_events => %w(message_posted) do + post :new, :board_id => 1, + :message => { :subject => 'Test created message', + :content => 'Message body'} + end + message = Message.find_by_subject('Test created message') + assert_not_nil message + assert_redirected_to "/boards/1/topics/#{message.to_param}" + assert_equal 'Message body', message.content + assert_equal 2, message.author_id + assert_equal 1, message.board_id + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert_equal "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] Test created message", mail.subject + assert_mail_body_match 'Message body', mail + # author + assert mail.bcc.include?('jsmith@somenet.foo') + # project member + assert mail.bcc.include?('dlopper@somenet.foo') + end + + def test_get_edit + @request.session[:user_id] = 2 + get :edit, :board_id => 1, :id => 1 + assert_response :success + assert_template 'edit' + end + + def test_post_edit + @request.session[:user_id] = 2 + post :edit, :board_id => 1, :id => 1, + :message => { :subject => 'New subject', + :content => 'New body'} + assert_redirected_to '/boards/1/topics/1' + message = Message.find(1) + assert_equal 'New subject', message.subject + assert_equal 'New body', message.content + end + + def test_post_edit_sticky_and_locked + @request.session[:user_id] = 2 + post :edit, :board_id => 1, :id => 1, + :message => { :subject => 'New subject', + :content => 'New body', + :locked => '1', + :sticky => '1'} + assert_redirected_to '/boards/1/topics/1' + message = Message.find(1) + assert_equal true, message.sticky? + assert_equal true, message.locked? + end + + def test_post_edit_should_allow_to_change_board + @request.session[:user_id] = 2 + post :edit, :board_id => 1, :id => 1, + :message => { :subject => 'New subject', + :content => 'New body', + :board_id => 2} + assert_redirected_to '/boards/2/topics/1' + message = Message.find(1) + assert_equal Board.find(2), message.board + end + + def test_reply + @request.session[:user_id] = 2 + post :reply, :board_id => 1, :id => 1, :reply => { :content => 'This is a test reply', :subject => 'Test reply' } + reply = Message.find(:first, :order => 'id DESC') + assert_redirected_to "/boards/1/topics/1?r=#{reply.id}" + assert Message.find_by_subject('Test reply') + end + + def test_destroy_topic + @request.session[:user_id] = 2 + assert_difference 'Message.count', -3 do + post :destroy, :board_id => 1, :id => 1 + end + assert_redirected_to '/projects/ecookbook/boards/1' + assert_nil Message.find_by_id(1) + end + + def test_destroy_reply + @request.session[:user_id] = 2 + assert_difference 'Message.count', -1 do + post :destroy, :board_id => 1, :id => 2 + end + assert_redirected_to '/boards/1/topics/1?r=2' + assert_nil Message.find_by_id(2) + end + + def test_quote + @request.session[:user_id] = 2 + xhr :get, :quote, :board_id => 1, :id => 3 + assert_response :success + assert_equal 'text/javascript', response.content_type + assert_template 'quote' + assert_include 'RE: First post', response.body + assert_include '> An other reply', response.body + end + + def test_preview_new + @request.session[:user_id] = 2 + post :preview, + :board_id => 1, + :message => {:subject => "", :content => "Previewed text"} + assert_response :success + assert_template 'common/_preview' + end + + def test_preview_edit + @request.session[:user_id] = 2 + post :preview, + :id => 4, + :board_id => 1, + :message => {:subject => "", :content => "Previewed text"} + assert_response :success + assert_template 'common/_preview' + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/04/04389a310fa0201bca85505cd492c1ab5010af2e.svn-base --- a/.svn/pristine/04/04389a310fa0201bca85505cd492c1ab5010af2e.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module DocumentsHelper -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/04/046cbe787072732ca9cd18ffb9fce1aa135f723e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/04/046cbe787072732ca9cd18ffb9fce1aa135f723e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,12 @@ +

    <%= l(@enumeration.option_name) %>: <%=h @enumeration %>

    + +<%= form_tag({}, :method => :delete) do %> +
    +

    <%= l(:text_enumeration_destroy_question, @enumeration.objects_count) %>

    +

    +<%= select_tag 'reassign_to_id', (content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") + options_from_collection_for_select(@enumerations, 'id', 'name')) %>

    +
    + +<%= submit_tag l(:button_apply) %> +<%= link_to l(:button_cancel), enumerations_path %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/04/048c8c7c3fc4fac08662cbdaac59cd6348bb3d14.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/04/048c8c7c3fc4fac08662cbdaac59cd6348bb3d14.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1287 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'forwardable' +require 'cgi' + +module ApplicationHelper + include Redmine::WikiFormatting::Macros::Definitions + include Redmine::I18n + include GravatarHelper::PublicMethods + + extend Forwardable + def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter + + # Return true if user is authorized for controller/action, otherwise false + def authorize_for(controller, action) + User.current.allowed_to?({:controller => controller, :action => action}, @project) + end + + # Display a link if user is authorized + # + # @param [String] name Anchor text (passed to link_to) + # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized + # @param [optional, Hash] html_options Options passed to link_to + # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to + def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) + link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) + end + + # Displays a link to user's account page if active + def link_to_user(user, options={}) + if user.is_a?(User) + name = h(user.name(options[:format])) + if user.active? || (User.current.admin? && user.logged?) + link_to name, user_path(user), :class => user.css_classes + else + name + end + else + h(user.to_s) + end + end + + # Displays a link to +issue+ with its subject. + # Examples: + # + # link_to_issue(issue) # => Defect #6: This is the subject + # link_to_issue(issue, :truncate => 6) # => Defect #6: This i... + # link_to_issue(issue, :subject => false) # => Defect #6 + # link_to_issue(issue, :project => true) # => Foo - Defect #6 + # link_to_issue(issue, :subject => false, :tracker => false) # => #6 + # + def link_to_issue(issue, options={}) + title = nil + subject = nil + text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}" + if options[:subject] == false + title = truncate(issue.subject, :length => 60) + else + subject = issue.subject + if options[:truncate] + subject = truncate(subject, :length => options[:truncate]) + end + end + s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title + s << h(": #{subject}") if subject + s = h("#{issue.project} - ") + s if options[:project] + s + end + + # Generates a link to an attachment. + # Options: + # * :text - Link text (default to attachment filename) + # * :download - Force download (default: false) + def link_to_attachment(attachment, options={}) + text = options.delete(:text) || attachment.filename + action = options.delete(:download) ? 'download' : 'show' + opt_only_path = {} + opt_only_path[:only_path] = (options[:only_path] == false ? false : true) + options.delete(:only_path) + link_to(h(text), + {:controller => 'attachments', :action => action, + :id => attachment, :filename => attachment.filename}.merge(opt_only_path), + options) + end + + # Generates a link to a SCM revision + # Options: + # * :text - Link text (default to the formatted revision) + def link_to_revision(revision, repository, options={}) + if repository.is_a?(Project) + repository = repository.repository + end + text = options.delete(:text) || format_revision(revision) + rev = revision.respond_to?(:identifier) ? revision.identifier : revision + link_to( + h(text), + {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev}, + :title => l(:label_revision_id, format_revision(revision)) + ) + end + + # Generates a link to a message + def link_to_message(message, options={}, html_options = nil) + link_to( + h(truncate(message.subject, :length => 60)), + { :controller => 'messages', :action => 'show', + :board_id => message.board_id, + :id => (message.parent_id || message.id), + :r => (message.parent_id && message.id), + :anchor => (message.parent_id ? "message-#{message.id}" : nil) + }.merge(options), + html_options + ) + end + + # Generates a link to a project if active + # Examples: + # + # link_to_project(project) # => link to the specified project overview + # link_to_project(project, :action=>'settings') # => link to project settings + # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options + # link_to_project(project, {}, :class => "project") # => html options with default url (project overview) + # + def link_to_project(project, options={}, html_options = nil) + if project.archived? + h(project) + else + url = {:controller => 'projects', :action => 'show', :id => project}.merge(options) + link_to(h(project), url, html_options) + end + end + + def wiki_page_path(page, options={}) + url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options)) + end + + def thumbnail_tag(attachment) + link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)), + {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename}, + :title => attachment.filename + end + + def toggle_link(name, id, options={}) + onclick = "$('##{id}').toggle(); " + onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ") + onclick << "return false;" + link_to(name, "#", :onclick => onclick) + end + + def image_to_function(name, function, html_options = {}) + html_options.symbolize_keys! + tag(:input, html_options.merge({ + :type => "image", :src => image_path(name), + :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" + })) + end + + def format_activity_title(text) + h(truncate_single_line(text, :length => 100)) + end + + def format_activity_day(date) + date == User.current.today ? l(:label_today).titleize : format_date(date) + end + + def format_activity_description(text) + h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...') + ).gsub(/[\r\n]+/, "
    ").html_safe + end + + def format_version_name(version) + if version.project == @project + h(version) + else + h("#{version.project} - #{version}") + end + end + + def due_date_distance_in_words(date) + if date + l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date)) + end + end + + # Renders a tree of projects as a nested set of unordered lists + # The given collection may be a subset of the whole project tree + # (eg. some intermediate nodes are private and can not be seen) + def render_project_nested_lists(projects) + s = '' + if projects.any? + ancestors = [] + original_project = @project + projects.sort_by(&:lft).each do |project| + # set the project environment to please macros. + @project = project + if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) + s << "
      \n" + else + ancestors.pop + s << "" + while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) + ancestors.pop + s << "
    \n" + end + end + classes = (ancestors.empty? ? 'root' : 'child') + s << "
  • " + s << h(block_given? ? yield(project) : project.name) + s << "
    \n" + ancestors << project + end + s << ("
  • \n" * ancestors.size) + @project = original_project + end + s.html_safe + end + + def render_page_hierarchy(pages, node=nil, options={}) + content = '' + if pages[node] + content << "
      \n" + pages[node].each do |page| + content << "
    • " + content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil}, + :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil)) + content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id] + content << "
    • \n" + end + content << "
    \n" + end + content.html_safe + end + + # Renders flash messages + def render_flash_messages + s = '' + flash.each do |k,v| + s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}") + end + s.html_safe + end + + # Renders tabs and their content + def render_tabs(tabs) + if tabs.any? + render :partial => 'common/tabs', :locals => {:tabs => tabs} + else + content_tag 'p', l(:label_no_data), :class => "nodata" + end + end + + # Renders the project quick-jump box + def render_project_jump_box + return unless User.current.logged? + projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq + if projects.any? + options = + ("" + + '').html_safe + + options << project_tree_options_for_select(projects, :selected => @project) do |p| + { :value => project_path(:id => p, :jump => current_menu_item) } + end + + select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }') + end + end + + def project_tree_options_for_select(projects, options = {}) + s = '' + project_tree(projects) do |project, level| + name_prefix = (level > 0 ? ' ' * 2 * level + '» ' : '').html_safe + tag_options = {:value => project.id} + if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project)) + tag_options[:selected] = 'selected' + else + tag_options[:selected] = nil + end + tag_options.merge!(yield(project)) if block_given? + s << content_tag('option', name_prefix + h(project), tag_options) + end + s.html_safe + end + + # Yields the given block for each project with its level in the tree + # + # Wrapper for Project#project_tree + def project_tree(projects, &block) + Project.project_tree(projects, &block) + end + + def principals_check_box_tags(name, principals) + s = '' + principals.sort.each do |principal| + s << "\n" + end + s.html_safe + end + + # Returns a string for users/groups option tags + def principals_options_for_select(collection, selected=nil) + s = '' + if collection.include?(User.current) + s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id) + end + groups = '' + collection.sort.each do |element| + selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) + (element.is_a?(Group) ? groups : s) << %() + end + unless groups.empty? + s << %(#{groups}) + end + s.html_safe + end + + # Options for the new membership projects combo-box + def options_for_membership_project_select(principal, projects) + options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") + options << project_tree_options_for_select(projects) do |p| + {:disabled => principal.projects.include?(p)} + end + options + end + + # Truncates and returns the string as a single line + def truncate_single_line(string, *args) + truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ') + end + + # Truncates at line break after 250 characters or options[:length] + def truncate_lines(string, options={}) + length = options[:length] || 250 + if string.to_s =~ /\A(.{#{length}}.*?)$/m + "#{$1}..." + else + string + end + end + + def anchor(text) + text.to_s.gsub(' ', '_') + end + + def html_hours(text) + text.gsub(%r{(\d+)\.(\d+)}, '\1.\2').html_safe + end + + def authoring(created, author, options={}) + l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe + end + + def time_tag(time) + text = distance_of_time_in_words(Time.now, time) + if @project + link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time)) + else + content_tag('acronym', text, :title => format_time(time)) + end + end + + def syntax_highlight_lines(name, content) + lines = [] + syntax_highlight(name, content).each_line { |line| lines << line } + lines + end + + def syntax_highlight(name, content) + Redmine::SyntaxHighlighting.highlight_by_filename(content, name) + end + + def to_path_param(path) + str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/") + str.blank? ? nil : str + end + + def pagination_links_full(paginator, count=nil, options={}) + page_param = options.delete(:page_param) || :page + per_page_links = options.delete(:per_page_links) + url_param = params.dup + + html = '' + if paginator.current.previous + # \xc2\xab(utf-8) = « + html << link_to_content_update( + "\xc2\xab " + l(:label_previous), + url_param.merge(page_param => paginator.current.previous)) + ' ' + end + + html << (pagination_links_each(paginator, options) do |n| + link_to_content_update(n.to_s, url_param.merge(page_param => n)) + end || '') + + if paginator.current.next + # \xc2\xbb(utf-8) = » + html << ' ' + link_to_content_update( + (l(:label_next) + " \xc2\xbb"), + url_param.merge(page_param => paginator.current.next)) + end + + unless count.nil? + html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})" + if per_page_links != false && links = per_page_links(paginator.items_per_page, count) + html << " | #{links}" + end + end + + html.html_safe + end + + def per_page_links(selected=nil, item_count=nil) + values = Setting.per_page_options_array + if item_count && values.any? + if item_count > values.first + max = values.detect {|value| value >= item_count} || item_count + else + max = item_count + end + values = values.select {|value| value <= max || value == selected} + end + if values.empty? || (values.size == 1 && values.first == selected) + return nil + end + links = values.collect do |n| + n == selected ? n : link_to_content_update(n, params.merge(:per_page => n)) + end + l(:label_display_per_page, links.join(', ')) + end + + def reorder_links(name, url, method = :post) + link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), + url.merge({"#{name}[move_to]" => 'highest'}), + :method => method, :title => l(:label_sort_highest)) + + link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), + url.merge({"#{name}[move_to]" => 'higher'}), + :method => method, :title => l(:label_sort_higher)) + + link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), + url.merge({"#{name}[move_to]" => 'lower'}), + :method => method, :title => l(:label_sort_lower)) + + link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), + url.merge({"#{name}[move_to]" => 'lowest'}), + :method => method, :title => l(:label_sort_lowest)) + end + + def breadcrumb(*args) + elements = args.flatten + elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil + end + + def other_formats_links(&block) + concat('

    '.html_safe + l(:label_export_to)) + yield Redmine::Views::OtherFormatsBuilder.new(self) + concat('

    '.html_safe) + end + + def page_header_title + if @project.nil? || @project.new_record? + h(Setting.app_title) + else + b = [] + ancestors = (@project.root? ? [] : @project.ancestors.visible.all) + if ancestors.any? + root = ancestors.shift + b << link_to_project(root, {:jump => current_menu_item}, :class => 'root') + if ancestors.size > 2 + b << "\xe2\x80\xa6" + ancestors = ancestors[-2, 2] + end + b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') } + end + b << h(@project) + b.join(" \xc2\xbb ").html_safe + end + end + + def html_title(*args) + if args.empty? + title = @html_title || [] + title << @project.name if @project + title << Setting.app_title unless Setting.app_title == title.last + title.select {|t| !t.blank? }.join(' - ') + else + @html_title ||= [] + @html_title += args + end + end + + # Returns the theme, controller name, and action as css classes for the + # HTML body. + def body_css_classes + css = [] + if theme = Redmine::Themes.theme(Setting.ui_theme) + css << 'theme-' + theme.name + end + + css << 'controller-' + controller_name + css << 'action-' + action_name + css.join(' ') + end + + def accesskey(s) + Redmine::AccessKeys.key_for s + end + + # Formats text according to system settings. + # 2 ways to call this method: + # * with a String: textilizable(text, options) + # * with an object and one of its attribute: textilizable(issue, :description, options) + def textilizable(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + case args.size + when 1 + obj = options[:object] + text = args.shift + when 2 + obj = args.shift + attr = args.shift + text = obj.send(attr).to_s + else + raise ArgumentError, 'invalid arguments to textilizable' + end + return '' if text.blank? + project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) + only_path = options.delete(:only_path) == false ? false : true + + text = text.dup + macros = catch_macros(text) + text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) + + @parsed_headings = [] + @heading_anchors = {} + @current_section = 0 if options[:edit_section_links] + + parse_sections(text, project, obj, attr, only_path, options) + text = parse_non_pre_blocks(text, obj, macros) do |text| + [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name| + send method_name, text, project, obj, attr, only_path, options + end + end + parse_headings(text, project, obj, attr, only_path, options) + + if @parsed_headings.any? + replace_toc(text, @parsed_headings) + end + + text.html_safe + end + + def parse_non_pre_blocks(text, obj, macros) + s = StringScanner.new(text) + tags = [] + parsed = '' + while !s.eos? + s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im) + text, full_tag, closing, tag = s[1], s[2], s[3], s[4] + if tags.empty? + yield text + inject_macros(text, obj, macros) if macros.any? + else + inject_macros(text, obj, macros, false) if macros.any? + end + parsed << text + if tag + if closing + if tags.last == tag.downcase + tags.pop + end + else + tags << tag.downcase + end + parsed << full_tag + end + end + # Close any non closing tags + while tag = tags.pop + parsed << "" + end + parsed + end + + def parse_inline_attachments(text, project, obj, attr, only_path, options) + # when using an image link, try to use an attachment, if possible + attachments = options[:attachments] || [] + attachments += obj.attachments if obj.respond_to?(:attachments) + if attachments.present? + text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| + filename, ext, alt, alttext = $1.downcase, $2, $3, $4 + # search for the picture in attachments + if found = Attachment.latest_attach(attachments, filename) + image_url = url_for :only_path => only_path, :controller => 'attachments', + :action => 'download', :id => found + desc = found.description.to_s.gsub('"', '') + if !desc.blank? && alttext.blank? + alt = " title=\"#{desc}\" alt=\"#{desc}\"" + end + "src=\"#{image_url}\"#{alt}" + else + m + end + end + end + end + + # Wiki links + # + # Examples: + # [[mypage]] + # [[mypage|mytext]] + # wiki links can refer other project wikis, using project name or identifier: + # [[project:]] -> wiki starting page + # [[project:|mytext]] + # [[project:mypage]] + # [[project:mypage|mytext]] + def parse_wiki_links(text, project, obj, attr, only_path, options) + text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m| + link_project = project + esc, all, page, title = $1, $2, $3, $5 + if esc.nil? + if page =~ /^([^\:]+)\:(.*)$/ + link_project = Project.find_by_identifier($1) || Project.find_by_name($1) + page = $2 + title ||= $1 if page.blank? + end + + if link_project && link_project.wiki + # extract anchor + anchor = nil + if page =~ /^(.+?)\#(.+)$/ + page, anchor = $1, $2 + end + anchor = sanitize_anchor_name(anchor) if anchor.present? + # check if page exists + wiki_page = link_project.wiki.find_page(page) + url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page + "##{anchor}" + else + case options[:wiki_links] + when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '') + when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export + else + wiki_page_id = page.present? ? Wiki.titleize(page) : nil + parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil + url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, + :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent) + end + end + link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new'))) + else + # project or wiki doesn't exist + all + end + else + all + end + end + end + + # Redmine links + # + # Examples: + # Issues: + # #52 -> Link to issue #52 + # Changesets: + # r52 -> Link to revision 52 + # commit:a85130f -> Link to scmid starting with a85130f + # Documents: + # document#17 -> Link to document with id 17 + # document:Greetings -> Link to the document with title "Greetings" + # document:"Some document" -> Link to the document with title "Some document" + # Versions: + # version#3 -> Link to version with id 3 + # version:1.0.0 -> Link to version named "1.0.0" + # version:"1.0 beta 2" -> Link to version named "1.0 beta 2" + # Attachments: + # attachment:file.zip -> Link to the attachment of the current object named file.zip + # Source files: + # source:some/file -> Link to the file located at /some/file in the project's repository + # source:some/file@52 -> Link to the file's revision 52 + # source:some/file#L120 -> Link to line 120 of the file + # source:some/file@52#L120 -> Link to line 120 of the file's revision 52 + # export:some/file -> Force the download of the file + # Forum messages: + # message#1218 -> Link to message with id 1218 + # + # Links can refer other objects from other projects, using project identifier: + # identifier:r52 + # identifier:document:"Some document" + # identifier:version:1.0.0 + # identifier:source:some/file + def parse_redmine_links(text, default_project, obj, attr, only_path, options) + text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m| + leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17 + link = nil + project = default_project + if project_identifier + project = Project.visible.find_by_identifier(project_identifier) + end + if esc.nil? + if prefix.nil? && sep == 'r' + if project + repository = nil + if repo_identifier + repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} + else + repository = project.repository + end + # project.changesets.visible raises an SQL error because of a double join on repositories + if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier)) + link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision}, + :class => 'changeset', + :title => truncate_single_line(changeset.comments, :length => 100)) + end + end + elsif sep == '#' + oid = identifier.to_i + case prefix + when nil + if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status) + anchor = comment_id ? "note-#{comment_id}" : nil + link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor}, + :class => issue.css_classes, + :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})") + end + when 'document' + if document = Document.visible.find_by_id(oid) + link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, + :class => 'document' + end + when 'version' + if version = Version.visible.find_by_id(oid) + link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, + :class => 'version' + end + when 'message' + if message = Message.visible.find_by_id(oid, :include => :parent) + link = link_to_message(message, {:only_path => only_path}, :class => 'message') + end + when 'forum' + if board = Board.visible.find_by_id(oid) + link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, + :class => 'board' + end + when 'news' + if news = News.visible.find_by_id(oid) + link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, + :class => 'news' + end + when 'project' + if p = Project.visible.find_by_id(oid) + link = link_to_project(p, {:only_path => only_path}, :class => 'project') + end + end + elsif sep == ':' + # removes the double quotes if any + name = identifier.gsub(%r{^"(.*)"$}, "\\1") + case prefix + when 'document' + if project && document = project.documents.visible.find_by_title(name) + link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, + :class => 'document' + end + when 'version' + if project && version = project.versions.visible.find_by_name(name) + link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, + :class => 'version' + end + when 'forum' + if project && board = project.boards.visible.find_by_name(name) + link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, + :class => 'board' + end + when 'news' + if project && news = project.news.visible.find_by_title(name) + link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, + :class => 'news' + end + when 'commit', 'source', 'export' + if project + repository = nil + if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$} + repo_prefix, repo_identifier, name = $1, $2, $3 + repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} + else + repository = project.repository + end + if prefix == 'commit' + if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"])) + link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier}, + :class => 'changeset', + :title => truncate_single_line(h(changeset.comments), :length => 100) + end + else + if repository && User.current.allowed_to?(:browse_repository, project) + name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$} + path, rev, anchor = $1, $3, $5 + link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param, + :path => to_path_param(path), + :rev => rev, + :anchor => anchor}, + :class => (prefix == 'export' ? 'source download' : 'source') + end + end + repo_prefix = nil + end + when 'attachment' + attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) + if attachments && attachment = Attachment.latest_attach(attachments, name) + link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment}, + :class => 'attachment' + end + when 'project' + if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}]) + link = link_to_project(p, {:only_path => only_path}, :class => 'project') + end + end + end + end + (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}")) + end + end + + HEADING_RE = /(]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE) + + def parse_sections(text, project, obj, attr, only_path, options) + return unless options[:edit_section_links] + text.gsub!(HEADING_RE) do + heading = $1 + @current_section += 1 + if @current_section > 1 + content_tag('div', + link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)), + :class => 'contextual', + :title => l(:button_edit_section)) + heading.html_safe + else + heading + end + end + end + + # Headings and TOC + # Adds ids and links to headings unless options[:headings] is set to false + def parse_headings(text, project, obj, attr, only_path, options) + return if options[:headings] == false + + text.gsub!(HEADING_RE) do + level, attrs, content = $2.to_i, $3, $4 + item = strip_tags(content).strip + anchor = sanitize_anchor_name(item) + # used for single-file wiki export + anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) + @heading_anchors[anchor] ||= 0 + idx = (@heading_anchors[anchor] += 1) + if idx > 1 + anchor = "#{anchor}-#{idx}" + end + @parsed_headings << [level, anchor, item] + "\n#{content}" + end + end + + MACROS_RE = /( + (!)? # escaping + ( + \{\{ # opening tag + ([\w]+) # macro name + (\(([^\n\r]*?)\))? # optional arguments + ([\n\r].*?[\n\r])? # optional block of text + \}\} # closing tag + ) + )/mx unless const_defined?(:MACROS_RE) + + MACRO_SUB_RE = /( + \{\{ + macro\((\d+)\) + \}\} + )/x unless const_defined?(:MACRO_SUB_RE) + + # Extracts macros from text + def catch_macros(text) + macros = {} + text.gsub!(MACROS_RE) do + all, macro = $1, $4.downcase + if macro_exists?(macro) || all =~ MACRO_SUB_RE + index = macros.size + macros[index] = all + "{{macro(#{index})}}" + else + all + end + end + macros + end + + # Executes and replaces macros in text + def inject_macros(text, obj, macros, execute=true) + text.gsub!(MACRO_SUB_RE) do + all, index = $1, $2.to_i + orig = macros.delete(index) + if execute && orig && orig =~ MACROS_RE + esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip) + if esc.nil? + h(exec_macro(macro, obj, args, block) || all) + else + h(all) + end + elsif orig + h(orig) + else + h(all) + end + end + end + + TOC_RE = /

    \{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE) + + # Renders the TOC with given headings + def replace_toc(text, headings) + text.gsub!(TOC_RE) do + # Keep only the 4 first levels + headings = headings.select{|level, anchor, item| level <= 4} + if headings.empty? + '' + else + div_class = 'toc' + div_class << ' right' if $1 == '>' + div_class << ' left' if $1 == '<' + out = "

    • " + root = headings.map(&:first).min + current = root + started = false + headings.each do |level, anchor, item| + if level > current + out << '
      • ' * (level - current) + elsif level < current + out << "
      \n" * (current - level) + "
    • " + elsif started + out << '
    • ' + end + out << "#{item}" + current = level + started = true + end + out << '
    ' * (current - root) + out << '' + end + end + end + + # Same as Rails' simple_format helper without using paragraphs + def simple_format_without_paragraph(text) + text.to_s. + gsub(/\r\n?/, "\n"). # \r\n and \r -> \n + gsub(/\n\n+/, "

    "). # 2+ newline -> 2 br + gsub(/([^\n]\n)(?=[^\n])/, '\1
    '). # 1 newline -> br + html_safe + end + + def lang_options_for_select(blank=true) + (blank ? [["(auto)", ""]] : []) + languages_options + end + + def label_tag_for(name, option_tags = nil, options = {}) + label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") + content_tag("label", label_text) + end + + def labelled_form_for(*args, &proc) + args << {} unless args.last.is_a?(Hash) + options = args.last + if args.first.is_a?(Symbol) + options.merge!(:as => args.shift) + end + options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) + form_for(*args, &proc) + end + + def labelled_fields_for(*args, &proc) + args << {} unless args.last.is_a?(Hash) + options = args.last + options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) + fields_for(*args, &proc) + end + + def labelled_remote_form_for(*args, &proc) + ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2." + args << {} unless args.last.is_a?(Hash) + options = args.last + options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true}) + form_for(*args, &proc) + end + + def error_messages_for(*objects) + html = "" + objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact + errors = objects.map {|o| o.errors.full_messages}.flatten + if errors.any? + html << "
      \n" + errors.each do |error| + html << "
    • #{h error}
    • \n" + end + html << "
    \n" + end + html.html_safe + end + + def delete_link(url, options={}) + options = { + :method => :delete, + :data => {:confirm => l(:text_are_you_sure)}, + :class => 'icon icon-del' + }.merge(options) + + link_to l(:button_delete), url, options + end + + def preview_link(url, form, target='preview', options={}) + content_tag 'a', l(:label_preview), { + :href => "#", + :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|, + :accesskey => accesskey(:preview) + }.merge(options) + end + + def link_to_function(name, function, html_options={}) + content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options)) + end + + # Helper to render JSON in views + def raw_json(arg) + arg.to_json.to_s.gsub('/', '\/').html_safe + end + + def back_url + url = params[:back_url] + if url.nil? && referer = request.env['HTTP_REFERER'] + url = CGI.unescape(referer.to_s) + end + url + end + + def back_url_hidden_field_tag + url = back_url + hidden_field_tag('back_url', url, :id => nil) unless url.blank? + end + + def check_all_links(form_name) + link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + + " | ".html_safe + + link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") + end + + def progress_bar(pcts, options={}) + pcts = [pcts, pcts] unless pcts.is_a?(Array) + pcts = pcts.collect(&:round) + pcts[1] = pcts[1] - pcts[0] + pcts << (100 - pcts[1] - pcts[0]) + width = options[:width] || '100px;' + legend = options[:legend] || '' + content_tag('table', + content_tag('tr', + (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) + + (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) + + (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe) + ), :class => 'progress', :style => "width: #{width};").html_safe + + content_tag('p', legend, :class => 'pourcent').html_safe + end + + def checked_image(checked=true) + if checked + image_tag 'toggle_check.png' + end + end + + def context_menu(url) + unless @context_menu_included + content_for :header_tags do + javascript_include_tag('context_menu') + + stylesheet_link_tag('context_menu') + end + if l(:direction) == 'rtl' + content_for :header_tags do + stylesheet_link_tag('context_menu_rtl') + end + end + @context_menu_included = true + end + javascript_tag "contextMenuInit('#{ url_for(url) }')" + end + + def calendar_for(field_id) + include_calendar_headers_tags + javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });") + end + + def include_calendar_headers_tags + unless @calendar_headers_tags_included + @calendar_headers_tags_included = true + content_for :header_tags do + start_of_week = Setting.start_of_week + start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank? + # Redmine uses 1..7 (monday..sunday) in settings and locales + # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0 + start_of_week = start_of_week.to_i % 7 + + tags = javascript_tag( + "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " + + "showOn: 'button', buttonImageOnly: true, buttonImage: '" + + path_to_image('/images/calendar.png') + + "', showButtonPanel: true};") + jquery_locale = l('jquery.locale', :default => current_language.to_s) + unless jquery_locale == 'en' + tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js") + end + tags + end + end + end + + # Overrides Rails' stylesheet_link_tag with themes and plugins support. + # Examples: + # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults + # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets + # + def stylesheet_link_tag(*sources) + options = sources.last.is_a?(Hash) ? sources.pop : {} + plugin = options.delete(:plugin) + sources = sources.map do |source| + if plugin + "/plugin_assets/#{plugin}/stylesheets/#{source}" + elsif current_theme && current_theme.stylesheets.include?(source) + current_theme.stylesheet_path(source) + else + source + end + end + super sources, options + end + + # Overrides Rails' image_tag with themes and plugins support. + # Examples: + # image_tag('image.png') # => picks image.png from the current theme or defaults + # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets + # + def image_tag(source, options={}) + if plugin = options.delete(:plugin) + source = "/plugin_assets/#{plugin}/images/#{source}" + elsif current_theme && current_theme.images.include?(source) + source = current_theme.image_path(source) + end + super source, options + end + + # Overrides Rails' javascript_include_tag with plugins support + # Examples: + # javascript_include_tag('scripts') # => picks scripts.js from defaults + # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets + # + def javascript_include_tag(*sources) + options = sources.last.is_a?(Hash) ? sources.pop : {} + if plugin = options.delete(:plugin) + sources = sources.map do |source| + if plugin + "/plugin_assets/#{plugin}/javascripts/#{source}" + else + source + end + end + end + super sources, options + end + + def content_for(name, content = nil, &block) + @has_content ||= {} + @has_content[name] = true + super(name, content, &block) + end + + def has_content?(name) + (@has_content && @has_content[name]) || false + end + + def sidebar_content? + has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present? + end + + def view_layouts_base_sidebar_hook_response + @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar) + end + + def email_delivery_enabled? + !!ActionMailer::Base.perform_deliveries + end + + # Returns the avatar image tag for the given +user+ if avatars are enabled + # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe ') + def avatar(user, options = { }) + if Setting.gravatar_enabled? + options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default}) + email = nil + if user.respond_to?(:mail) + email = user.mail + elsif user.to_s =~ %r{<(.+?)>} + email = $1 + end + return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil + else + '' + end + end + + def sanitize_anchor_name(anchor) + if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java' + anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + else + # TODO: remove when ruby1.8 is no longer supported + anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + end + end + + # Returns the javascript tags that are included in the html layout head + def javascript_heads + tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application') + unless User.current.pref.warn_on_leaving_unsaved == '0' + tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });") + end + tags + end + + def favicon + "".html_safe + end + + def robot_exclusion_tag + ''.html_safe + end + + # Returns true if arg is expected in the API response + def include_in_api_response?(arg) + unless @included_in_api_response + param = params[:include] + @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',') + @included_in_api_response.collect!(&:strip) + end + @included_in_api_response.include?(arg.to_s) + end + + # Returns options or nil if nometa param or X-Redmine-Nometa header + # was set in the request + def api_meta(options) + if params[:nometa].present? || request.headers['X-Redmine-Nometa'] + # compatibility mode for activeresource clients that raise + # an error when unserializing an array with attributes + nil + else + options + end + end + + private + + def wiki_helper + helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) + extend helper + return self + end + + def link_to_content_update(text, url_params = {}, html_options = {}) + link_to(text, url_params, html_options) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/04/04dfe98ce0a611eee30923688999ac146c40ca35.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/04/04dfe98ce0a611eee30923688999ac146c40ca35.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,96 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Principal < ActiveRecord::Base + self.table_name = "#{table_name_prefix}users#{table_name_suffix}" + + has_many :members, :foreign_key => 'user_id', :dependent => :destroy + has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status<>#{Project::STATUS_ARCHIVED}", :order => "#{Project.table_name}.name" + has_many :projects, :through => :memberships + has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify + + # Groups and active users + scope :active, :conditions => "#{Principal.table_name}.status = 1" + + scope :like, lambda {|q| + q = q.to_s + if q.blank? + where({}) + else + pattern = "%#{q}%" + sql = %w(login firstname lastname mail).map {|column| "LOWER(#{table_name}.#{column}) LIKE LOWER(:p)"}.join(" OR ") + params = {:p => pattern} + if q =~ /^(.+)\s+(.+)$/ + a, b = "#{$1}%", "#{$2}%" + sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:a) AND LOWER(#{table_name}.lastname) LIKE LOWER(:b))" + sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:b) AND LOWER(#{table_name}.lastname) LIKE LOWER(:a))" + params.merge!(:a => a, :b => b) + end + where(sql, params) + end + } + + # Principals that are members of a collection of projects + scope :member_of, lambda {|projects| + projects = [projects] unless projects.is_a?(Array) + if projects.empty? + where("1=0") + else + ids = projects.map(&:id) + where("#{Principal.table_name}.status = 1 AND #{Principal.table_name}.id IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids) + end + } + # Principals that are not members of projects + scope :not_member_of, lambda {|projects| + projects = [projects] unless projects.is_a?(Array) + if projects.empty? + where("1=0") + else + ids = projects.map(&:id) + where("#{Principal.table_name}.id NOT IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids) + end + } + + before_create :set_default_empty_values + + def name(formatter = nil) + to_s + end + + def <=>(principal) + if principal.nil? + -1 + elsif self.class.name == principal.class.name + self.to_s.downcase <=> principal.to_s.downcase + else + # groups after users + principal.class.name <=> self.class.name + end + end + + protected + + # Make sure we don't try to insert NULL values (see #4632) + def set_default_empty_values + self.login ||= '' + self.hashed_password ||= '' + self.firstname ||= '' + self.lastname ||= '' + self.mail ||= '' + true + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/04/04fbda284b00bec052d898fc58aac289855e9000.svn-base --- a/.svn/pristine/04/04fbda284b00bec052d898fc58aac289855e9000.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -class Wiki < ActiveRecord::Base - generator_for :start_page => 'Start' - generator_for :project, :method => :generate_project - - def self.generate_project - Project.generate! - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/05/0513c24a062b523037219d9978aab2b119808571.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/05/0513c24a062b523037219d9978aab2b119808571.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,20 @@ +<%= form_for @project, + :url => { :action => 'modules', :id => @project }, + :html => {:id => 'modules-form', + :method => :post} do |f| %> + +
    +
    +<%= l(:text_select_project_modules) %> + +<% Redmine::AccessControl.available_project_modules.each do |m| %> +

    +<% end %> +
    +
    + +

    <%= check_all_links 'modules-form' %>

    +

    <%= submit_tag l(:button_save) %>

    + +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/05/053c71b830593b5a759e13f9676269b7c9bf4645.svn-base --- a/.svn/pristine/05/053c71b830593b5a759e13f9676269b7c9bf4645.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,47 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class IssueCategory < ActiveRecord::Base - belongs_to :project - belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id' - has_many :issues, :foreign_key => 'category_id', :dependent => :nullify - - validates_presence_of :name - validates_uniqueness_of :name, :scope => [:project_id] - validates_length_of :name, :maximum => 30 - - attr_protected :project_id - - named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}} - - alias :destroy_without_reassign :destroy - - # Destroy the category - # If a category is specified, issues are reassigned to this category - def destroy(reassign_to = nil) - if reassign_to && reassign_to.is_a?(IssueCategory) && reassign_to.project == self.project - Issue.update_all("category_id = #{reassign_to.id}", "category_id = #{id}") - end - destroy_without_reassign - end - - def <=>(category) - name <=> category.name - end - - def to_s; name end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/05/053ccbadc6aeeda1db20f88d64520439b568415d.svn-base --- a/.svn/pristine/05/053ccbadc6aeeda1db20f88d64520439b568415d.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module AccountHelper -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/05/0548f38a902595a77747491d864f451fe12f0f47.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/05/0548f38a902595a77747491d864f451fe12f0f47.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,346 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +RedmineApp::Application.routes.draw do + root :to => 'welcome#index', :as => 'home' + + match 'login', :to => 'account#login', :as => 'signin' + match 'logout', :to => 'account#logout', :as => 'signout' + match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register' + match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password' + match 'account/activate', :to => 'account#activate', :via => :get + + match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news' + match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue' + match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue' + match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue' + + match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post + match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post] + + match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post] + get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message' + match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post] + get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit' + + post 'boards/:board_id/topics/preview', :to => 'messages#preview' + post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply' + post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit' + post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy' + + # Misc issue routes. TODO: move into resources + match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues' + match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu' + match '/issues/changes', :to => 'journals#index', :as => 'issue_changes' + match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue' + + match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get + match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post] + + match '/projects/:project_id/issues/gantt', :to => 'gantts#show' + match '/issues/gantt', :to => 'gantts#show' + + match '/projects/:project_id/issues/calendar', :to => 'calendars#show' + match '/issues/calendar', :to => 'calendars#show' + + match 'projects/:id/issues/report', :to => 'reports#issue_report', :via => :get + match 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :via => :get + + match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post] + match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post] + match 'my/page', :controller => 'my', :action => 'page', :via => :get + match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page + match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post + match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post + match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post] + match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get + match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post + match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post + match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post + + resources :users + match 'users/:id/memberships/:membership_id', :to => 'users#edit_membership', :via => :put, :as => 'user_membership' + match 'users/:id/memberships/:membership_id', :to => 'users#destroy_membership', :via => :delete + match 'users/:id/memberships', :to => 'users#edit_membership', :via => :post, :as => 'user_memberships' + + match 'watchers/new', :controller=> 'watchers', :action => 'new', :via => :get + match 'watchers', :controller=> 'watchers', :action => 'create', :via => :post + match 'watchers/append', :controller=> 'watchers', :action => 'append', :via => :post + match 'watchers/destroy', :controller=> 'watchers', :action => 'destroy', :via => :post + match 'watchers/watch', :controller=> 'watchers', :action => 'watch', :via => :post + match 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch', :via => :post + match 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user', :via => :get + + match 'projects/:id/settings/:tab', :to => "projects#settings" + + resources :projects do + member do + get 'settings' + post 'modules' + post 'archive' + post 'unarchive' + post 'close' + post 'reopen' + match 'copy', :via => [:get, :post] + end + + resources :memberships, :shallow => true, :controller => 'members', :only => [:index, :show, :new, :create, :update, :destroy] do + collection do + get 'autocomplete' + end + end + + resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy] + + match 'issues/:copy_from/copy', :to => 'issues#new' + resources :issues, :only => [:index, :new, :create] do + resources :time_entries, :controller => 'timelog' do + collection do + get 'report' + end + end + end + # issue form update + match 'issues/new', :controller => 'issues', :action => 'new', :via => [:put, :post], :as => 'issue_form' + + resources :files, :only => [:index, :new, :create] + + resources :versions, :except => [:index, :show, :edit, :update, :destroy] do + collection do + put 'close_completed' + end + end + match 'versions.:format', :to => 'versions#index' + match 'roadmap', :to => 'versions#index', :format => false + match 'versions', :to => 'versions#index' + + resources :news, :except => [:show, :edit, :update, :destroy] + resources :time_entries, :controller => 'timelog' do + get 'report', :on => :collection + end + resources :queries, :only => [:new, :create] + resources :issue_categories, :shallow => true + resources :documents, :except => [:show, :edit, :update, :destroy] + resources :boards + resources :repositories, :shallow => true, :except => [:index, :show] do + member do + match 'committers', :via => [:get, :post] + end + end + + match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get + resources :wiki, :except => [:index, :new, :create] do + member do + get 'rename' + post 'rename' + get 'history' + get 'diff' + match 'preview', :via => [:post, :put] + post 'protect' + post 'add_attachment' + end + collection do + get 'export' + get 'date_index' + end + end + match 'wiki', :controller => 'wiki', :action => 'show', :via => :get + get 'wiki/:id/:version', :to => 'wiki#show' + delete 'wiki/:id/:version', :to => 'wiki#destroy_version' + get 'wiki/:id/:version/annotate', :to => 'wiki#annotate' + get 'wiki/:id/:version/diff', :to => 'wiki#diff' + end + + resources :issues do + collection do + match 'bulk_edit', :via => [:get, :post] + post 'bulk_update' + end + resources :time_entries, :controller => 'timelog' do + collection do + get 'report' + end + end + resources :relations, :shallow => true, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy] + end + match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete + + resources :queries, :except => [:show] + + resources :news, :only => [:index, :show, :edit, :update, :destroy] + match '/news/:id/comments', :to => 'comments#create', :via => :post + match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete + + resources :versions, :only => [:show, :edit, :update, :destroy] do + post 'status_by', :on => :member + end + + resources :documents, :only => [:show, :edit, :update, :destroy] do + post 'add_attachment', :on => :member + end + + match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu + + resources :time_entries, :controller => 'timelog', :except => :destroy do + collection do + get 'report' + get 'bulk_edit' + post 'bulk_update' + end + end + match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/ + # TODO: delete /time_entries for bulk deletion + match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete + + # TODO: port to be part of the resources route(s) + match 'projects/:id/settings/:tab', :to => 'projects#settings', :via => :get + + get 'projects/:id/activity', :to => 'activities#index' + get 'projects/:id/activity.:format', :to => 'activities#index' + get 'activity', :to => 'activities#index' + + # repositories routes + get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats' + get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph' + + get 'projects/:id/repository/:repository_id/changes(/*path(.:ext))', + :to => 'repositories#changes' + + get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision' + get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision' + post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue' + delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue' + get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions' + get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path(.:ext))', + :controller => 'repositories', + :format => false, + :constraints => { + :action => /(browse|show|entry|raw|annotate|diff)/, + :rev => /[a-z0-9\.\-_]+/ + } + + get 'projects/:id/repository/statistics', :to => 'repositories#stats' + get 'projects/:id/repository/graph', :to => 'repositories#graph' + + get 'projects/:id/repository/changes(/*path(.:ext))', + :to => 'repositories#changes' + + get 'projects/:id/repository/revisions', :to => 'repositories#revisions' + get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision' + get 'projects/:id/repository/revision', :to => 'repositories#revision' + post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue' + delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue' + get 'projects/:id/repository/revisions/:rev/:action(/*path(.:ext))', + :controller => 'repositories', + :format => false, + :constraints => { + :action => /(browse|show|entry|raw|annotate|diff)/, + :rev => /[a-z0-9\.\-_]+/ + } + get 'projects/:id/repository/:repository_id/:action(/*path(.:ext))', + :controller => 'repositories', + :action => /(browse|show|entry|raw|changes|annotate|diff)/ + get 'projects/:id/repository/:action(/*path(.:ext))', + :controller => 'repositories', + :action => /(browse|show|entry|raw|changes|annotate|diff)/ + + get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil + get 'projects/:id/repository', :to => 'repositories#show', :path => nil + + # additional routes for having the file name at the end of url + match 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/, :via => :get + match 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/, :via => :get + match 'attachments/download/:id', :controller => 'attachments', :action => 'download', :id => /\d+/, :via => :get + match 'attachments/thumbnail/:id(/:size)', :controller => 'attachments', :action => 'thumbnail', :id => /\d+/, :via => :get, :size => /\d+/ + resources :attachments, :only => [:show, :destroy] + + resources :groups do + member do + get 'autocomplete_for_user' + end + end + + match 'groups/:id/users', :controller => 'groups', :action => 'add_users', :id => /\d+/, :via => :post, :as => 'group_users' + match 'groups/:id/users/:user_id', :controller => 'groups', :action => 'remove_user', :id => /\d+/, :via => :delete, :as => 'group_user' + match 'groups/destroy_membership/:id', :controller => 'groups', :action => 'destroy_membership', :id => /\d+/, :via => :post + match 'groups/edit_membership/:id', :controller => 'groups', :action => 'edit_membership', :id => /\d+/, :via => :post + + resources :trackers, :except => :show do + collection do + match 'fields', :via => [:get, :post] + end + end + resources :issue_statuses, :except => :show do + collection do + post 'update_issue_done_ratio' + end + end + resources :custom_fields, :except => :show + resources :roles do + collection do + match 'permissions', :via => [:get, :post] + end + end + resources :enumerations, :except => :show + match 'enumerations/:type', :to => 'enumerations#index', :via => :get + + get 'projects/:id/search', :controller => 'search', :action => 'index' + get 'search', :controller => 'search', :action => 'index' + + match 'mail_handler', :controller => 'mail_handler', :action => 'index', :via => :post + + match 'admin', :controller => 'admin', :action => 'index', :via => :get + match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get + match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get + match 'admin/info', :controller => 'admin', :action => 'info', :via => :get + match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get + match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post + + resources :auth_sources do + member do + get 'test_connection' + end + end + + match 'workflows', :controller => 'workflows', :action => 'index', :via => :get + match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post] + match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post] + match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post] + match 'settings', :controller => 'settings', :action => 'index', :via => :get + match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post] + match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post] + + match 'sys/projects', :to => 'sys#projects', :via => :get + match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post + match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => :get + + match 'uploads', :to => 'attachments#upload', :via => :post + + get 'robots.txt', :to => 'welcome#robots' + + Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir| + file = File.join(plugin_dir, "config/routes.rb") + if File.exists?(file) + begin + instance_eval File.read(file) + rescue Exception => e + puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}." + exit 1 + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/05/055e159e867506d297434a808bdc106b5515daab.svn-base Binary file .svn/pristine/05/055e159e867506d297434a808bdc106b5515daab.svn-base has changed diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/05/057b1251b6a5ec11a65817e4473e1f88569bdb9e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/05/057b1251b6a5ec11a65817e4473e1f88569bdb9e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,43 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "John Smith" +To: +Subject: New ticket on a given project +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + +Projet: onlinestore +Tracker: Feature request +catégorie: Stock management +priorité: Urgent diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/05/058be0b3a53e81bfbd2f18f23f7014355fe7cd0a.svn-base --- a/.svn/pristine/05/058be0b3a53e81bfbd2f18f23f7014355fe7cd0a.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,150 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -require File.expand_path('../../test_helper', __FILE__) - -class MemberTest < ActiveSupport::TestCase - fixtures :projects, :trackers, :issue_statuses, :issues, - :enumerations, :users, :issue_categories, - :projects_trackers, - :roles, - :member_roles, - :members, - :enabled_modules, - :workflows, - :groups_users, - :watchers, - :journals, :journal_details, - :messages, - :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, - :boards - - def setup - @jsmith = Member.find(1) - end - - def test_create - member = Member.new(:project_id => 1, :user_id => 4, :role_ids => [1, 2]) - assert member.save - member.reload - - assert_equal 2, member.roles.size - assert_equal Role.find(1), member.roles.sort.first - end - - def test_update - assert_equal "eCookbook", @jsmith.project.name - assert_equal "Manager", @jsmith.roles.first.name - assert_equal "jsmith", @jsmith.user.login - - @jsmith.mail_notification = !@jsmith.mail_notification - assert @jsmith.save - end - - def test_update_roles - assert_equal 1, @jsmith.roles.size - @jsmith.role_ids = [1, 2] - assert @jsmith.save - assert_equal 2, @jsmith.reload.roles.size - end - - def test_validate - member = Member.new(:project_id => 1, :user_id => 2, :role_ids => [2]) - # same use can't have more than one membership for a project - assert !member.save - - member = Member.new(:project_id => 1, :user_id => 2, :role_ids => []) - # must have one role at least - assert !member.save - end - - def test_destroy - assert_difference 'Member.count', -1 do - assert_difference 'MemberRole.count', -1 do - @jsmith.destroy - end - end - - assert_raise(ActiveRecord::RecordNotFound) { Member.find(@jsmith.id) } - end - - context "removing permissions" do - setup do - Watcher.delete_all("user_id = 9") - user = User.find(9) - # public - Watcher.create!(:watchable => Issue.find(1), :user => user) - # private - Watcher.create!(:watchable => Issue.find(4), :user => user) - Watcher.create!(:watchable => Message.find(7), :user => user) - Watcher.create!(:watchable => Wiki.find(2), :user => user) - Watcher.create!(:watchable => WikiPage.find(3), :user => user) - end - - context "of user" do - setup do - @member = Member.create!(:project => Project.find(2), :principal => User.find(9), :role_ids => [1, 2]) - end - - context "by deleting membership" do - should "prune watchers" do - assert_difference 'Watcher.count', -4 do - @member.destroy - end - end - end - - context "by updating roles" do - should "prune watchers" do - Role.find(2).remove_permission! :view_wiki_pages - member = Member.first(:order => 'id desc') - assert_difference 'Watcher.count', -2 do - member.role_ids = [2] - member.save - end - assert !Message.find(7).watched_by?(@user) - end - end - end - - context "of group" do - setup do - group = Group.find(10) - @member = Member.create!(:project => Project.find(2), :principal => group, :role_ids => [1, 2]) - group.users << User.find(9) - end - - context "by deleting membership" do - should "prune watchers" do - assert_difference 'Watcher.count', -4 do - @member.destroy - end - end - end - - context "by updating roles" do - should "prune watchers" do - Role.find(2).remove_permission! :view_wiki_pages - assert_difference 'Watcher.count', -2 do - @member.role_ids = [2] - @member.save - end - end - end - end - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/05/05bd9a477032dec0ee79cadb6f6626cf79ccae69.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/05/05bd9a477032dec0ee79cadb6f6626cf79ccae69.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,431 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingRepositoriesTest < ActionController::IntegrationTest + def setup + @path_hash = repository_path_hash(%w[path to file.c]) + assert_equal "path/to/file.c", @path_hash[:path] + assert_equal "path/to/file.c", @path_hash[:param] + end + + def test_repositories_resources + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repositories/new" }, + { :controller => 'repositories', :action => 'new', :project_id => 'redmine' } + ) + assert_routing( + { :method => 'post', + :path => "/projects/redmine/repositories" }, + { :controller => 'repositories', :action => 'create', :project_id => 'redmine' } + ) + assert_routing( + { :method => 'get', + :path => "/repositories/1/edit" }, + { :controller => 'repositories', :action => 'edit', :id => '1' } + ) + assert_routing( + { :method => 'put', + :path => "/repositories/1" }, + { :controller => 'repositories', :action => 'update', :id => '1' } + ) + assert_routing( + { :method => 'delete', + :path => "/repositories/1" }, + { :controller => 'repositories', :action => 'destroy', :id => '1' } + ) + ["get", "post"].each do |method| + assert_routing( + { :method => method, + :path => "/repositories/1/committers" }, + { :controller => 'repositories', :action => 'committers', :id => '1' } + ) + end + end + + def test_repositories_show + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository" }, + { :controller => 'repositories', :action => 'show', :id => 'redmine' } + ) + end + + def test_repositories + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/statistics" }, + { :controller => 'repositories', :action => 'stats', :id => 'redmine' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/graph" }, + { :controller => 'repositories', :action => 'graph', :id => 'redmine' } + ) + end + + def test_repositories_show_with_repository_id + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo" }, + { :controller => 'repositories', :action => 'show', :id => 'redmine', :repository_id => 'foo' } + ) + end + + def test_repositories_with_repository_id + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/statistics" }, + { :controller => 'repositories', :action => 'stats', :id => 'redmine', :repository_id => 'foo' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/graph" }, + { :controller => 'repositories', :action => 'graph', :id => 'redmine', :repository_id => 'foo' } + ) + end + + def test_repositories_revisions + empty_path_param = [] + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions" }, + { :controller => 'repositories', :action => 'revisions', :id => 'redmine' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions.atom" }, + { :controller => 'repositories', :action => 'revisions', :id => 'redmine', + :format => 'atom' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2457" }, + { :controller => 'repositories', :action => 'revision', :id => 'redmine', + :rev => '2457' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2457/show" }, + { :controller => 'repositories', :action => 'show', :id => 'redmine', + :rev => '2457' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2457/show/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'show', :id => 'redmine', + :path => @path_hash[:param] , :rev => '2457'} + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2457/diff" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', + :rev => '2457' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2457/diff" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', + :rev => '2457', :format => 'diff' }, + {}, + { :format => 'diff' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2/diff/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', + :path => @path_hash[:param], :rev => '2' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2/diff/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', + :path => @path_hash[:param], :rev => '2', :format => 'diff' }, + {}, + { :format => 'diff' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2/entry/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'entry', :id => 'redmine', + :path => @path_hash[:param], :rev => '2' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2/raw/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'raw', :id => 'redmine', + :path => @path_hash[:param], :rev => '2' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revisions/2/annotate/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'annotate', :id => 'redmine', + :path => @path_hash[:param], :rev => '2' } + ) + end + + def test_repositories_revisions_with_repository_id + empty_path_param = [] + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions" }, + { :controller => 'repositories', :action => 'revisions', :id => 'redmine', :repository_id => 'foo' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions.atom" }, + { :controller => 'repositories', :action => 'revisions', :id => 'redmine', :repository_id => 'foo', + :format => 'atom' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2457" }, + { :controller => 'repositories', :action => 'revision', :id => 'redmine', :repository_id => 'foo', + :rev => '2457' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2457/show" }, + { :controller => 'repositories', :action => 'show', :id => 'redmine', :repository_id => 'foo', + :rev => '2457' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2457/show/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'show', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param] , :rev => '2457'} + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2457/diff" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo', + :rev => '2457' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2457/diff" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo', + :rev => '2457', :format => 'diff' }, + {}, + { :format => 'diff' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2/diff/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param], :rev => '2' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2/diff/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param], :rev => '2', :format => 'diff' }, + {}, + { :format => 'diff' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2/entry/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'entry', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param], :rev => '2' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2/raw/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'raw', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param], :rev => '2' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revisions/2/annotate/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'annotate', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param], :rev => '2' } + ) + end + + def test_repositories_non_revisions_path + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/changes" }, + { :controller => 'repositories', :action => 'changes', :id => 'redmine' } + ) + ['2457', 'master', 'slash/slash'].each do |rev| + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/changes" }, + { :controller => 'repositories', :action => 'changes', :id => 'redmine', + :rev => rev }, + {}, + { :rev => rev } + ) + end + ['2457', 'master', 'slash/slash'].each do |rev| + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/changes/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'changes', :id => 'redmine', + :path => @path_hash[:param], :rev => rev }, + {}, + { :rev => rev } + ) + end + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/diff/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/browse/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'browse', :id => 'redmine', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/entry/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'entry', :id => 'redmine', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/raw/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'raw', :id => 'redmine', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/annotate/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'annotate', :id => 'redmine', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/changes/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'changes', :id => 'redmine', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/revision" }, + { :controller => 'repositories', :action => 'revision', :id => 'redmine' } + ) + end + + def test_repositories_non_revisions_path_with_repository_id + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/changes" }, + { :controller => 'repositories', :action => 'changes', + :id => 'redmine', :repository_id => 'foo' } + ) + ['2457', 'master', 'slash/slash'].each do |rev| + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/changes" }, + { :controller => 'repositories', :action => 'changes', + :id => 'redmine', + :repository_id => 'foo', :rev => rev }, + {}, + { :rev => rev } + ) + end + ['2457', 'master', 'slash/slash'].each do |rev| + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/changes/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'changes', :id => 'redmine', + :repository_id => 'foo', :path => @path_hash[:param], :rev => rev }, + {}, + { :rev => rev } + ) + end + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/diff/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'diff', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/browse/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'browse', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/entry/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'entry', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/raw/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'raw', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/annotate/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'annotate', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/changes/#{@path_hash[:path]}" }, + { :controller => 'repositories', :action => 'changes', :id => 'redmine', :repository_id => 'foo', + :path => @path_hash[:param] } + ) + assert_routing( + { :method => 'get', + :path => "/projects/redmine/repository/foo/revision" }, + { :controller => 'repositories', :action => 'revision', :id => 'redmine', :repository_id => 'foo'} + ) + end + + def test_repositories_related_issues + assert_routing( + { :method => 'post', + :path => "/projects/redmine/repository/revisions/123/issues" }, + { :controller => 'repositories', :action => 'add_related_issue', + :id => 'redmine', :rev => '123' } + ) + assert_routing( + { :method => 'delete', + :path => "/projects/redmine/repository/revisions/123/issues/25" }, + { :controller => 'repositories', :action => 'remove_related_issue', + :id => 'redmine', :rev => '123', :issue_id => '25' } + ) + end + + def test_repositories_related_issues_with_repository_id + assert_routing( + { :method => 'post', + :path => "/projects/redmine/repository/foo/revisions/123/issues" }, + { :controller => 'repositories', :action => 'add_related_issue', + :id => 'redmine', :repository_id => 'foo', :rev => '123' } + ) + assert_routing( + { :method => 'delete', + :path => "/projects/redmine/repository/foo/revisions/123/issues/25" }, + { :controller => 'repositories', :action => 'remove_related_issue', + :id => 'redmine', :repository_id => 'foo', :rev => '123', :issue_id => '25' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/05/05cdc085e6ab746bd3734a56f879f137f280e27b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/05/05cdc085e6ab746bd3734a56f879f137f280e27b.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,139 @@ +require 'rexml/document' +require 'SVG/Graph/Graph' + +module SVG + module Graph + # = Synopsis + # + # A superclass for bar-style graphs. Do not attempt to instantiate + # directly; use one of the subclasses instead. + # + # = Author + # + # Sean E. Russell + # + # Copyright 2004 Sean E. Russell + # This software is available under the Ruby license[LICENSE.txt] + # + class BarBase < SVG::Graph::Graph + # Ensures that :fields are provided in the configuration. + def initialize config + raise "fields was not supplied or is empty" unless config[:fields] && + config[:fields].kind_of?(Array) && + config[:fields].length > 0 + super + end + + # In addition to the defaults set in Graph::initialize, sets + # [bar_gap] true + # [stack] :overlap + def set_defaults + init_with( :bar_gap => true, :stack => :overlap ) + end + + # Whether to have a gap between the bars or not, default + # is true, set to false if you don't want gaps. + attr_accessor :bar_gap + # How to stack data sets. :overlap overlaps bars with + # transparent colors, :top stacks bars on top of one another, + # :side stacks the bars side-by-side. Defaults to :overlap. + attr_accessor :stack + + + protected + + def max_value + @data.collect{|x| x[:data].max}.max + end + + def min_value + min = 0 + if min_scale_value.nil? + min = @data.collect{|x| x[:data].min}.min + min = min > 0 ? 0 : min + else + min = min_scale_value + end + return min + end + + def get_css + return < [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}" - has_many :memberships, :class_name => 'Member' - has_many :member_principals, :class_name => 'Member', - :include => :principal, - :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})" - has_many :users, :through => :members - has_many :principals, :through => :member_principals, :source => :principal - - has_many :enabled_modules, :dependent => :delete_all - has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" - has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker] - has_many :issue_changes, :through => :issues, :source => :journals - has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC" - has_many :time_entries, :dependent => :delete_all - has_many :queries, :dependent => :delete_all - has_many :documents, :dependent => :destroy - has_many :news, :dependent => :destroy, :include => :author - has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name" - has_many :boards, :dependent => :destroy, :order => "position ASC" - has_one :repository, :dependent => :destroy - has_many :changesets, :through => :repository - has_one :wiki, :dependent => :destroy - # Custom field for the project issues - has_and_belongs_to_many :issue_custom_fields, - :class_name => 'IssueCustomField', - :order => "#{CustomField.table_name}.position", - :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", - :association_foreign_key => 'custom_field_id' - - acts_as_nested_set :order => 'name', :dependent => :destroy - acts_as_attachable :view_permission => :view_files, - :delete_permission => :manage_files - - acts_as_customizable - acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil - acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, - :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}}, - :author => nil - - attr_protected :status - - validates_presence_of :name, :identifier - validates_uniqueness_of :identifier - validates_associated :repository, :wiki - validates_length_of :name, :maximum => 255 - validates_length_of :homepage, :maximum => 255 - validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH - # donwcase letters, digits, dashes but not digits only - validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? } - # reserved words - validates_exclusion_of :identifier, :in => %w( new ) - - before_destroy :delete_all_members - - named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } } - named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"} - named_scope :all_public, { :conditions => { :is_public => true } } - named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }} - - def initialize(attributes = nil) - super - - initialized = (attributes || {}).stringify_keys - if !initialized.key?('identifier') && Setting.sequential_project_identifiers? - self.identifier = Project.next_identifier - end - if !initialized.key?('is_public') - self.is_public = Setting.default_projects_public? - end - if !initialized.key?('enabled_module_names') - self.enabled_module_names = Setting.default_projects_modules - end - if !initialized.key?('trackers') && !initialized.key?('tracker_ids') - self.trackers = Tracker.all - end - end - - def identifier=(identifier) - super unless identifier_frozen? - end - - def identifier_frozen? - errors[:identifier].nil? && !(new_record? || identifier.blank?) - end - - # returns latest created projects - # non public projects will be returned only if user is a member of those - def self.latest(user=nil, count=5) - visible(user).find(:all, :limit => count, :order => "created_on DESC") - end - - # Returns true if the project is visible to +user+ or to the current user. - def visible?(user=User.current) - user.allowed_to?(:view_project, self) - end - - # Returns a SQL conditions string used to find all projects visible by the specified user. - # - # Examples: - # Project.visible_condition(admin) => "projects.status = 1" - # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))" - # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))" - def self.visible_condition(user, options={}) - allowed_to_condition(user, :view_project, options) - end - - # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+ - # - # Valid options: - # * :project => limit the condition to project - # * :with_subprojects => limit the condition to project and its subprojects - # * :member => limit the condition to the user projects - def self.allowed_to_condition(user, permission, options={}) - base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" - if perm = Redmine::AccessControl.permission(permission) - unless perm.project_module.nil? - # If the permission belongs to a project module, make sure the module is enabled - base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" - end - end - if options[:project] - project_statement = "#{Project.table_name}.id = #{options[:project].id}" - project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects] - base_statement = "(#{project_statement}) AND (#{base_statement})" - end - - if user.admin? - base_statement - else - statement_by_role = {} - unless options[:member] - role = user.logged? ? Role.non_member : Role.anonymous - if role.allowed_to?(permission) - statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}" - end - end - if user.logged? - user.projects_by_role.each do |role, projects| - if role.allowed_to?(permission) - statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})" - end - end - end - if statement_by_role.empty? - "1=0" - else - if block_given? - statement_by_role.each do |role, statement| - if s = yield(role, user) - statement_by_role[role] = "(#{statement} AND (#{s}))" - end - end - end - "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))" - end - end - end - - # Returns the Systemwide and project specific activities - def activities(include_inactive=false) - if include_inactive - return all_activities - else - return active_activities - end - end - - # Will create a new Project specific Activity or update an existing one - # - # This will raise a ActiveRecord::Rollback if the TimeEntryActivity - # does not successfully save. - def update_or_create_time_entry_activity(id, activity_hash) - if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id') - self.create_time_entry_activity_if_needed(activity_hash) - else - activity = project.time_entry_activities.find_by_id(id.to_i) - activity.update_attributes(activity_hash) if activity - end - end - - # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity - # - # This will raise a ActiveRecord::Rollback if the TimeEntryActivity - # does not successfully save. - def create_time_entry_activity_if_needed(activity) - if activity['parent_id'] - - parent_activity = TimeEntryActivity.find(activity['parent_id']) - activity['name'] = parent_activity.name - activity['position'] = parent_activity.position - - if Enumeration.overridding_change?(activity, parent_activity) - project_activity = self.time_entry_activities.create(activity) - - if project_activity.new_record? - raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved" - else - self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id]) - end - end - end - end - - # Returns a :conditions SQL string that can be used to find the issues associated with this project. - # - # Examples: - # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))" - # project.project_condition(false) => "projects.id = 1" - def project_condition(with_subprojects) - cond = "#{Project.table_name}.id = #{id}" - cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects - cond - end - - def self.find(*args) - if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/) - project = find_by_identifier(*args) - raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil? - project - else - super - end - end - - def to_param - # id is used for projects with a numeric identifier (compatibility) - @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier) - end - - def active? - self.status == STATUS_ACTIVE - end - - def archived? - self.status == STATUS_ARCHIVED - end - - # Archives the project and its descendants - def archive - # Check that there is no issue of a non descendant project that is assigned - # to one of the project or descendant versions - v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten - if v_ids.any? && Issue.find(:first, :include => :project, - :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" + - " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids]) - return false - end - Project.transaction do - archive! - end - true - end - - # Unarchives the project - # All its ancestors must be active - def unarchive - return false if ancestors.detect {|a| !a.active?} - update_attribute :status, STATUS_ACTIVE - end - - # Returns an array of projects the project can be moved to - # by the current user - def allowed_parents - return @allowed_parents if @allowed_parents - @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects)) - @allowed_parents = @allowed_parents - self_and_descendants - if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?) - @allowed_parents << nil - end - unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent) - @allowed_parents << parent - end - @allowed_parents - end - - # Sets the parent of the project with authorization check - def set_allowed_parent!(p) - unless p.nil? || p.is_a?(Project) - if p.to_s.blank? - p = nil - else - p = Project.find_by_id(p) - return false unless p - end - end - if p.nil? - if !new_record? && allowed_parents.empty? - return false - end - elsif !allowed_parents.include?(p) - return false - end - set_parent!(p) - end - - # Sets the parent of the project - # Argument can be either a Project, a String, a Fixnum or nil - def set_parent!(p) - unless p.nil? || p.is_a?(Project) - if p.to_s.blank? - p = nil - else - p = Project.find_by_id(p) - return false unless p - end - end - if p == parent && !p.nil? - # Nothing to do - true - elsif p.nil? || (p.active? && move_possible?(p)) - # Insert the project so that target's children or root projects stay alphabetically sorted - sibs = (p.nil? ? self.class.roots : p.children) - to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase } - if to_be_inserted_before - move_to_left_of(to_be_inserted_before) - elsif p.nil? - if sibs.empty? - # move_to_root adds the project in first (ie. left) position - move_to_root - else - move_to_right_of(sibs.last) unless self == sibs.last - end - else - # move_to_child_of adds the project in last (ie.right) position - move_to_child_of(p) - end - Issue.update_versions_from_hierarchy_change(self) - true - else - # Can not move to the given target - false - end - end - - # Returns an array of the trackers used by the project and its active sub projects - def rolled_up_trackers - @rolled_up_trackers ||= - Tracker.find(:all, :joins => :projects, - :select => "DISTINCT #{Tracker.table_name}.*", - :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt], - :order => "#{Tracker.table_name}.position") - end - - # Closes open and locked project versions that are completed - def close_completed_versions - Version.transaction do - versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version| - if version.completed? - version.update_attribute(:status, 'closed') - end - end - end - end - - # Returns a scope of the Versions on subprojects - def rolled_up_versions - @rolled_up_versions ||= - Version.scoped(:include => :project, - :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt]) - end - - # Returns a scope of the Versions used by the project - def shared_versions - @shared_versions ||= begin - r = root? ? self : root - Version.scoped(:include => :project, - :conditions => "#{Project.table_name}.id = #{id}" + - " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" + - " #{Version.table_name}.sharing = 'system'" + - " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" + - " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" + - " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" + - "))") - end - end - - # Returns a hash of project users grouped by role - def users_by_role - members.find(:all, :include => [:user, :roles]).inject({}) do |h, m| - m.roles.each do |r| - h[r] ||= [] - h[r] << m.user - end - h - end - end - - # Deletes all project's members - def delete_all_members - me, mr = Member.table_name, MemberRole.table_name - connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})") - Member.delete_all(['project_id = ?', id]) - end - - # Users/groups issues can be assigned to - def assignable_users - assignable = Setting.issue_group_assignment? ? member_principals : members - assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort - end - - # Returns the mail adresses of users that should be always notified on project events - def recipients - notified_users.collect {|user| user.mail} - end - - # Returns the users that should be notified on project events - def notified_users - # TODO: User part should be extracted to User#notify_about? - members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user} - end - - # Returns an array of all custom fields enabled for project issues - # (explictly associated custom fields and custom fields enabled for all projects) - def all_issue_custom_fields - @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort - end - - # Returns an array of all custom fields enabled for project time entries - # (explictly associated custom fields and custom fields enabled for all projects) - def all_time_entry_custom_fields - @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort - end - - def project - self - end - - def <=>(project) - name.downcase <=> project.name.downcase - end - - def to_s - name - end - - # Returns a short description of the projects (first lines) - def short_description(length = 255) - description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description - end - - def css_classes - s = 'project' - s << ' root' if root? - s << ' child' if child? - s << (leaf? ? ' leaf' : ' parent') - s - end - - # The earliest start date of a project, based on it's issues and versions - def start_date - [ - issues.minimum('start_date'), - shared_versions.collect(&:effective_date), - shared_versions.collect(&:start_date) - ].flatten.compact.min - end - - # The latest due date of an issue or version - def due_date - [ - issues.maximum('due_date'), - shared_versions.collect(&:effective_date), - shared_versions.collect {|v| v.fixed_issues.maximum('due_date')} - ].flatten.compact.max - end - - def overdue? - active? && !due_date.nil? && (due_date < Date.today) - end - - # Returns the percent completed for this project, based on the - # progress on it's versions. - def completed_percent(options={:include_subprojects => false}) - if options.delete(:include_subprojects) - total = self_and_descendants.collect(&:completed_percent).sum - - total / self_and_descendants.count - else - if versions.count > 0 - total = versions.collect(&:completed_pourcent).sum - - total / versions.count - else - 100 - end - end - end - - # Return true if this project is allowed to do the specified action. - # action can be: - # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') - # * a permission Symbol (eg. :edit_project) - def allows_to?(action) - if action.is_a? Hash - allowed_actions.include? "#{action[:controller]}/#{action[:action]}" - else - allowed_permissions.include? action - end - end - - def module_enabled?(module_name) - module_name = module_name.to_s - enabled_modules.detect {|m| m.name == module_name} - end - - def enabled_module_names=(module_names) - if module_names && module_names.is_a?(Array) - module_names = module_names.collect(&:to_s).reject(&:blank?) - self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)} - else - enabled_modules.clear - end - end - - # Returns an array of the enabled modules names - def enabled_module_names - enabled_modules.collect(&:name) - end - - # Enable a specific module - # - # Examples: - # project.enable_module!(:issue_tracking) - # project.enable_module!("issue_tracking") - def enable_module!(name) - enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name) - end - - # Disable a module if it exists - # - # Examples: - # project.disable_module!(:issue_tracking) - # project.disable_module!("issue_tracking") - # project.disable_module!(project.enabled_modules.first) - def disable_module!(target) - target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target) - target.destroy unless target.blank? - end - - safe_attributes 'name', - 'description', - 'homepage', - 'is_public', - 'identifier', - 'custom_field_values', - 'custom_fields', - 'tracker_ids', - 'issue_custom_field_ids' - - safe_attributes 'enabled_module_names', - :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) } - - # Returns an array of projects that are in this project's hierarchy - # - # Example: parents, children, siblings - def hierarchy - parents = project.self_and_ancestors || [] - descendants = project.descendants || [] - project_hierarchy = parents | descendants # Set union - end - - # Returns an auto-generated project identifier based on the last identifier used - def self.next_identifier - p = Project.find(:first, :order => 'created_on DESC') - p.nil? ? nil : p.identifier.to_s.succ - end - - # Copies and saves the Project instance based on the +project+. - # Duplicates the source project's: - # * Wiki - # * Versions - # * Categories - # * Issues - # * Members - # * Queries - # - # Accepts an +options+ argument to specify what to copy - # - # Examples: - # project.copy(1) # => copies everything - # project.copy(1, :only => 'members') # => copies members only - # project.copy(1, :only => ['members', 'versions']) # => copies members and versions - def copy(project, options={}) - project = project.is_a?(Project) ? project : Project.find(project) - - to_be_copied = %w(wiki versions issue_categories issues members queries boards) - to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil? - - Project.transaction do - if save - reload - to_be_copied.each do |name| - send "copy_#{name}", project - end - Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self) - save - end - end - end - - - # Copies +project+ and returns the new instance. This will not save - # the copy - def self.copy_from(project) - begin - project = project.is_a?(Project) ? project : Project.find(project) - if project - # clear unique attributes - attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt') - copy = Project.new(attributes) - copy.enabled_modules = project.enabled_modules - copy.trackers = project.trackers - copy.custom_values = project.custom_values.collect {|v| v.clone} - copy.issue_custom_fields = project.issue_custom_fields - return copy - else - return nil - end - rescue ActiveRecord::RecordNotFound - return nil - end - end - - # Yields the given block for each project with its level in the tree - def self.project_tree(projects, &block) - ancestors = [] - projects.sort_by(&:lft).each do |project| - while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) - ancestors.pop - end - yield project, ancestors.size - ancestors << project - end - end - - private - - # Copies wiki from +project+ - def copy_wiki(project) - # Check that the source project has a wiki first - unless project.wiki.nil? - self.wiki ||= Wiki.new - wiki.attributes = project.wiki.attributes.dup.except("id", "project_id") - wiki_pages_map = {} - project.wiki.pages.each do |page| - # Skip pages without content - next if page.content.nil? - new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on")) - new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id")) - new_wiki_page.content = new_wiki_content - wiki.pages << new_wiki_page - wiki_pages_map[page.id] = new_wiki_page - end - wiki.save - # Reproduce page hierarchy - project.wiki.pages.each do |page| - if page.parent_id && wiki_pages_map[page.id] - wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id] - wiki_pages_map[page.id].save - end - end - end - end - - # Copies versions from +project+ - def copy_versions(project) - project.versions.each do |version| - new_version = Version.new - new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on") - self.versions << new_version - end - end - - # Copies issue categories from +project+ - def copy_issue_categories(project) - project.issue_categories.each do |issue_category| - new_issue_category = IssueCategory.new - new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id") - self.issue_categories << new_issue_category - end - end - - # Copies issues from +project+ - # Note: issues assigned to a closed version won't be copied due to validation rules - def copy_issues(project) - # Stores the source issue id as a key and the copied issues as the - # value. Used to map the two togeather for issue relations. - issues_map = {} - - # Get issues sorted by root_id, lft so that parent issues - # get copied before their children - project.issues.find(:all, :order => 'root_id, lft').each do |issue| - new_issue = Issue.new - new_issue.copy_from(issue) - new_issue.project = self - # Reassign fixed_versions by name, since names are unique per - # project and the versions for self are not yet saved - if issue.fixed_version - new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first - end - # Reassign the category by name, since names are unique per - # project and the categories for self are not yet saved - if issue.category - new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first - end - # Parent issue - if issue.parent_id - if copied_parent = issues_map[issue.parent_id] - new_issue.parent_issue_id = copied_parent.id - end - end - - self.issues << new_issue - if new_issue.new_record? - logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info - else - issues_map[issue.id] = new_issue unless new_issue.new_record? - end - end - - # Relations after in case issues related each other - project.issues.each do |issue| - new_issue = issues_map[issue.id] - unless new_issue - # Issue was not copied - next - end - - # Relations - issue.relations_from.each do |source_relation| - new_issue_relation = IssueRelation.new - new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id") - new_issue_relation.issue_to = issues_map[source_relation.issue_to_id] - if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations? - new_issue_relation.issue_to = source_relation.issue_to - end - new_issue.relations_from << new_issue_relation - end - - issue.relations_to.each do |source_relation| - new_issue_relation = IssueRelation.new - new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id") - new_issue_relation.issue_from = issues_map[source_relation.issue_from_id] - if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations? - new_issue_relation.issue_from = source_relation.issue_from - end - new_issue.relations_to << new_issue_relation - end - end - end - - # Copies members from +project+ - def copy_members(project) - # Copy users first, then groups to handle members with inherited and given roles - members_to_copy = [] - members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)} - members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)} - - members_to_copy.each do |member| - new_member = Member.new - new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on") - # only copy non inherited roles - # inherited roles will be added when copying the group membership - role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id) - next if role_ids.empty? - new_member.role_ids = role_ids - new_member.project = self - self.members << new_member - end - end - - # Copies queries from +project+ - def copy_queries(project) - project.queries.each do |query| - new_query = Query.new - new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria") - new_query.sort_criteria = query.sort_criteria if query.sort_criteria - new_query.project = self - new_query.user_id = query.user_id - self.queries << new_query - end - end - - # Copies boards from +project+ - def copy_boards(project) - project.boards.each do |board| - new_board = Board.new - new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id") - new_board.project = self - self.boards << new_board - end - end - - def allowed_permissions - @allowed_permissions ||= begin - module_names = enabled_modules.all(:select => :name).collect {|m| m.name} - Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name} - end - end - - def allowed_actions - @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten - end - - # Returns all the active Systemwide and project specific activities - def active_activities - overridden_activity_ids = self.time_entry_activities.collect(&:parent_id) - - if overridden_activity_ids.empty? - return TimeEntryActivity.shared.active - else - return system_activities_and_project_overrides - end - end - - # Returns all the Systemwide and project specific activities - # (inactive and active) - def all_activities - overridden_activity_ids = self.time_entry_activities.collect(&:parent_id) - - if overridden_activity_ids.empty? - return TimeEntryActivity.shared - else - return system_activities_and_project_overrides(true) - end - end - - # Returns the systemwide active activities merged with the project specific overrides - def system_activities_and_project_overrides(include_inactive=false) - if include_inactive - return TimeEntryActivity.shared. - find(:all, - :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) + - self.time_entry_activities - else - return TimeEntryActivity.shared.active. - find(:all, - :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) + - self.time_entry_activities.active - end - end - - # Archives subprojects recursively - def archive! - children.each do |subproject| - subproject.send :archive! - end - update_attribute :status, STATUS_ARCHIVED - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/06/06191abb72622bec9f1387de935e8ce335158e32.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/06/06191abb72622bec9f1387de935e8ce335158e32.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,255 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class AttachmentTest < ActiveSupport::TestCase + fixtures :users, :projects, :roles, :members, :member_roles, + :enabled_modules, :issues, :trackers, :attachments + + class MockFile + attr_reader :original_filename, :content_type, :content, :size + + def initialize(attributes) + @original_filename = attributes[:original_filename] + @content_type = attributes[:content_type] + @content = attributes[:content] || "Content" + @size = content.size + end + end + + def setup + set_tmp_attachments_directory + end + + def test_container_for_new_attachment_should_be_nil + assert_nil Attachment.new.container + end + + def test_create + a = Attachment.new(:container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", "text/plain"), + :author => User.find(1)) + assert a.save + assert_equal 'testfile.txt', a.filename + assert_equal 59, a.filesize + assert_equal 'text/plain', a.content_type + assert_equal 0, a.downloads + assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest + assert File.exist?(a.diskfile) + assert_equal 59, File.size(a.diskfile) + end + + def test_size_should_be_validated_for_new_file + with_settings :attachment_max_size => 0 do + a = Attachment.new(:container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", "text/plain"), + :author => User.find(1)) + assert !a.save + end + end + + def test_size_should_not_be_validated_when_copying + a = Attachment.create!(:container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", "text/plain"), + :author => User.find(1)) + with_settings :attachment_max_size => 0 do + copy = a.copy + assert copy.save + end + end + + def test_description_length_should_be_validated + a = Attachment.new(:description => 'a' * 300) + assert !a.save + assert_not_nil a.errors[:description] + end + + def test_destroy + a = Attachment.new(:container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", "text/plain"), + :author => User.find(1)) + assert a.save + assert_equal 'testfile.txt', a.filename + assert_equal 59, a.filesize + assert_equal 'text/plain', a.content_type + assert_equal 0, a.downloads + assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest + diskfile = a.diskfile + assert File.exist?(diskfile) + assert_equal 59, File.size(a.diskfile) + assert a.destroy + assert !File.exist?(diskfile) + end + + def test_destroy_should_not_delete_file_referenced_by_other_attachment + a = Attachment.create!(:container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", "text/plain"), + :author => User.find(1)) + diskfile = a.diskfile + + copy = a.copy + copy.save! + + assert File.exists?(diskfile) + a.destroy + assert File.exists?(diskfile) + copy.destroy + assert !File.exists?(diskfile) + end + + def test_create_should_auto_assign_content_type + a = Attachment.new(:container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", ""), + :author => User.find(1)) + assert a.save + assert_equal 'text/plain', a.content_type + end + + def test_identical_attachments_at_the_same_time_should_not_overwrite + a1 = Attachment.create!(:container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", ""), + :author => User.find(1)) + a2 = Attachment.create!(:container => Issue.find(1), + :file => uploaded_test_file("testfile.txt", ""), + :author => User.find(1)) + assert a1.disk_filename != a2.disk_filename + end + + def test_filename_should_be_basenamed + a = Attachment.new(:file => MockFile.new(:original_filename => "path/to/the/file")) + assert_equal 'file', a.filename + end + + def test_filename_should_be_sanitized + a = Attachment.new(:file => MockFile.new(:original_filename => "valid:[] invalid:?%*|\"'<>chars")) + assert_equal 'valid_[] invalid_chars', a.filename + end + + def test_diskfilename + assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/ + assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1] + assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentué.txt")[13..-1] + assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentué")[13..-1] + assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentué.ça")[13..-1] + end + + def test_title + a = Attachment.new(:filename => "test.png") + assert_equal "test.png", a.title + + a = Attachment.new(:filename => "test.png", :description => "Cool image") + assert_equal "test.png (Cool image)", a.title + end + + def test_prune_should_destroy_old_unattached_attachments + Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago) + Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago) + Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1) + + assert_difference 'Attachment.count', -2 do + Attachment.prune + end + end + + context "Attachmnet.attach_files" do + should "attach the file" do + issue = Issue.first + assert_difference 'Attachment.count' do + Attachment.attach_files(issue, + '1' => { + 'file' => uploaded_test_file('testfile.txt', 'text/plain'), + 'description' => 'test' + }) + end + + attachment = Attachment.first(:order => 'id DESC') + assert_equal issue, attachment.container + assert_equal 'testfile.txt', attachment.filename + assert_equal 59, attachment.filesize + assert_equal 'test', attachment.description + assert_equal 'text/plain', attachment.content_type + assert File.exists?(attachment.diskfile) + assert_equal 59, File.size(attachment.diskfile) + end + + should "add unsaved files to the object as unsaved attachments" do + # Max size of 0 to force Attachment creation failures + with_settings(:attachment_max_size => 0) do + @project = Project.find(1) + response = Attachment.attach_files(@project, { + '1' => {'file' => mock_file, 'description' => 'test'}, + '2' => {'file' => mock_file, 'description' => 'test'} + }) + + assert response[:unsaved].present? + assert_equal 2, response[:unsaved].length + assert response[:unsaved].first.new_record? + assert response[:unsaved].second.new_record? + assert_equal response[:unsaved], @project.unsaved_attachments + end + end + end + + def test_latest_attach + set_fixtures_attachments_directory + a1 = Attachment.find(16) + assert_equal "testfile.png", a1.filename + assert a1.readable? + assert (! a1.visible?(User.anonymous)) + assert a1.visible?(User.find(2)) + a2 = Attachment.find(17) + assert_equal "testfile.PNG", a2.filename + assert a2.readable? + assert (! a2.visible?(User.anonymous)) + assert a2.visible?(User.find(2)) + assert a1.created_on < a2.created_on + + la1 = Attachment.latest_attach([a1, a2], "testfile.png") + assert_equal 17, la1.id + la2 = Attachment.latest_attach([a1, a2], "Testfile.PNG") + assert_equal 17, la2.id + + set_tmp_attachments_directory + end + + def test_thumbnailable_should_be_true_for_images + assert_equal true, Attachment.new(:filename => 'test.jpg').thumbnailable? + end + + def test_thumbnailable_should_be_true_for_non_images + assert_equal false, Attachment.new(:filename => 'test.txt').thumbnailable? + end + + if convert_installed? + def test_thumbnail_should_generate_the_thumbnail + set_fixtures_attachments_directory + attachment = Attachment.find(16) + Attachment.clear_thumbnails + + assert_difference "Dir.glob(File.join(Attachment.thumbnails_storage_path, '*.thumb')).size" do + thumbnail = attachment.thumbnail + assert_equal "16_8e0294de2441577c529f170b6fb8f638_100.thumb", File.basename(thumbnail) + assert File.exists?(thumbnail) + end + end + else + puts '(ImageMagick convert not available)' + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/06/06dd3ee8641385912fee02a84e6de53eed535f28.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/06/06dd3ee8641385912fee02a84e6de53eed535f28.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,31 @@ +
    +<%= link_to l(:label_auth_source_new), {:action => 'new'}, :class => 'icon icon-add' %> +
    + +

    <%=l(:label_auth_source_plural)%>

    + + + + + + + + + + +<% for source in @auth_sources %> + "> + + + + + + +<% end %> + +
    <%=l(:field_name)%><%=l(:field_type)%><%=l(:field_host)%><%=l(:label_user_plural)%>
    <%= link_to(h(source.name), :action => 'edit', :id => source)%><%= h source.auth_method_name %><%= h source.host %><%= h source.users.count %> + <%= link_to l(:button_test), {:action => 'test_connection', :id => source}, :class => 'icon icon-test' %> + <%= delete_link auth_source_path(source) %> +
    + +

    <%= pagination_links_full @auth_source_pages %>

    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/07/071604c5dacdea2b6b0884f22cd68afddd0f1a2a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/071604c5dacdea2b6b0884f22cd68afddd0f1a2a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,99 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../../../test_helper', __FILE__) +begin + require 'mocha' + + class CvsAdapterTest < ActiveSupport::TestCase + REPOSITORY_PATH = Rails.root.join('tmp/test/cvs_repository').to_s + REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin? + MODULE_NAME = 'test' + + if File.directory?(REPOSITORY_PATH) + def setup + @adapter = Redmine::Scm::Adapters::CvsAdapter.new(MODULE_NAME, REPOSITORY_PATH) + end + + def test_scm_version + to_test = { "\nConcurrent Versions System (CVS) 1.12.13 (client/server)\n" => [1,12,13], + "\r\n1.12.12\r\n1.12.11" => [1,12,12], + "1.12.11\r\n1.12.10\r\n" => [1,12,11]} + to_test.each do |s, v| + test_scm_version_for(s, v) + end + end + + def test_revisions_all + cnt = 0 + @adapter.revisions('', nil, nil, :log_encoding => 'UTF-8') do |revision| + cnt += 1 + end + assert_equal 16, cnt + end + + def test_revisions_from_rev3 + rev3_committed_on = Time.gm(2007, 12, 13, 16, 27, 22) + cnt = 0 + @adapter.revisions('', rev3_committed_on, nil, :log_encoding => 'UTF-8') do |revision| + cnt += 1 + end + assert_equal 4, cnt + end + + def test_entries_rev3 + rev3_committed_on = Time.gm(2007, 12, 13, 16, 27, 22) + entries = @adapter.entries('sources', rev3_committed_on) + assert_equal 2, entries.size + assert_equal entries[0].name, "watchers_controller.rb" + assert_equal entries[0].lastrev.time, Time.gm(2007, 12, 13, 16, 27, 22) + end + + def test_path_encoding_default_utf8 + adpt1 = Redmine::Scm::Adapters::CvsAdapter.new( + MODULE_NAME, + REPOSITORY_PATH + ) + assert_equal "UTF-8", adpt1.path_encoding + adpt2 = Redmine::Scm::Adapters::CvsAdapter.new( + MODULE_NAME, + REPOSITORY_PATH, + nil, + nil, + "" + ) + assert_equal "UTF-8", adpt2.path_encoding + end + + private + + def test_scm_version_for(scm_command_version, version) + @adapter.class.expects(:scm_version_from_command_line).returns(scm_command_version) + assert_equal version, @adapter.class.scm_command_version + end + else + puts "Cvs test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end + end + +rescue LoadError + class CvsMochaFake < ActiveSupport::TestCase + def test_fake; assert(false, "Requires mocha to run those tests") end + end +end + diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/07/0727b3896624bd770efa836eec3947b2d7b9d6a7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/0727b3896624bd770efa836eec3947b2d7b9d6a7.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,7 @@ +From: John Smith +To: "redmine@somenet.foo" +Subject: =?iso-2022-jp?b?GyRCJUYlOSVIGyhCCg=?= +Date: Fri, 1 Jun 2012 14:39:38 +0200 +Message-ID: <87C31D42249DD0489D1A1444E3232DD7019D6183@foo.bar> + +Fixture diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/07/072d65dcf460d00cc4770641f038bc5801ca9243.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/072d65dcf460d00cc4770641f038bc5801ca9243.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,118 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class PrincipalTest < ActiveSupport::TestCase + fixtures :users, :projects, :members, :member_roles + + def test_active_scope_should_return_groups_and_active_users + result = Principal.active.all + assert_include Group.first, result + assert_not_nil result.detect {|p| p.is_a?(User)} + assert_nil result.detect {|p| p.is_a?(User) && !p.active?} + assert_nil result.detect {|p| p.is_a?(AnonymousUser)} + end + + def test_member_of_scope_should_return_the_union_of_all_members + projects = Project.find_all_by_id(1, 2) + assert_equal projects.map(&:principals).flatten.sort, Principal.member_of(projects).sort + end + + def test_member_of_scope_should_be_empty_for_no_projects + assert_equal [], Principal.member_of([]).sort + end + + def test_not_member_of_scope_should_return_users_that_have_no_memberships + projects = Project.find_all_by_id(1, 2) + expected = (Principal.all - projects.map(&:memberships).flatten.map(&:principal)).sort + assert_equal expected, Principal.not_member_of(projects).sort + end + + def test_not_member_of_scope_should_be_empty_for_no_projects + assert_equal [], Principal.not_member_of([]).sort + end + + context "#like" do + setup do + Principal.create!(:login => 'login') + Principal.create!(:login => 'login2') + + Principal.create!(:firstname => 'firstname') + Principal.create!(:firstname => 'firstname2') + + Principal.create!(:lastname => 'lastname') + Principal.create!(:lastname => 'lastname2') + + Principal.create!(:mail => 'mail@example.com') + Principal.create!(:mail => 'mail2@example.com') + + @palmer = Principal.create!(:firstname => 'David', :lastname => 'Palmer') + end + + should "search login" do + results = Principal.like('login') + + assert_equal 2, results.count + assert results.all? {|u| u.login.match(/login/) } + end + + should "search firstname" do + results = Principal.like('firstname') + + assert_equal 2, results.count + assert results.all? {|u| u.firstname.match(/firstname/) } + end + + should "search lastname" do + results = Principal.like('lastname') + + assert_equal 2, results.count + assert results.all? {|u| u.lastname.match(/lastname/) } + end + + should "search mail" do + results = Principal.like('mail') + + assert_equal 2, results.count + assert results.all? {|u| u.mail.match(/mail/) } + end + + should "search firstname and lastname" do + results = Principal.like('david palm') + + assert_equal 1, results.count + assert_equal @palmer, results.first + end + + should "search lastname and firstname" do + results = Principal.like('palmer davi') + + assert_equal 1, results.count + assert_equal @palmer, results.first + end + end + + def test_like_scope_with_cyrillic_name + user = User.generate!(:firstname => 'Соболев', :lastname => 'ДениÑ') + results = Principal.like('Собо') + assert_equal 1, results.count + assert_equal user, results.first + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/07/076d12ba0a0e6cd2d89298e8d1723e4c715f64cb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/076d12ba0a0e6cd2d89298e8d1723e4c715f64cb.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1085 @@ +# Serbian translations for Redmine +# by Vladimir Medarović (vlada@medarovic.com) +sr-YU: + direction: ltr + jquery: + locale: "sr" + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%d.%m.%Y." + short: "%e %b" + long: "%B %e, %Y" + + day_names: [nedelja, ponedeljak, utorak, sreda, Äetvrtak, petak, subota] + abbr_day_names: [ned, pon, uto, sre, Äet, pet, sub] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, januar, februar, mart, april, maj, jun, jul, avgust, septembar, oktobar, novembar, decembar] + abbr_month_names: [~, jan, feb, mar, apr, maj, jun, jul, avg, sep, okt, nov, dec] + # Used in date_select and datime_select. + order: + - :day + - :month + - :year + + time: + formats: + default: "%d.%m.%Y. u %H:%M" + time: "%H:%M" + short: "%d. %b u %H:%M" + long: "%d. %B %Y u %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "pola minuta" + less_than_x_seconds: + one: "manje od jedne sekunde" + other: "manje od %{count} sek." + x_seconds: + one: "jedna sekunda" + other: "%{count} sek." + less_than_x_minutes: + one: "manje od minuta" + other: "manje od %{count} min." + x_minutes: + one: "jedan minut" + other: "%{count} min." + about_x_hours: + one: "približno jedan sat" + other: "približno %{count} sati" + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "jedan dan" + other: "%{count} dana" + about_x_months: + one: "približno jedan mesec" + other: "približno %{count} meseci" + x_months: + one: "jedan mesec" + other: "%{count} meseci" + about_x_years: + one: "približno godinu dana" + other: "približno %{count} god." + over_x_years: + one: "preko godinu dana" + other: "preko %{count} god." + almost_x_years: + one: "skoro godinu dana" + other: "skoro %{count} god." + + number: + format: + separator: "," + delimiter: "" + precision: 3 + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + + +# Used in array.to_sentence. + support: + array: + sentence_connector: "i" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + messages: + inclusion: "nije ukljuÄen u spisak" + exclusion: "je rezervisan" + invalid: "je neispravan" + confirmation: "potvrda ne odgovara" + accepted: "mora biti prihvaćen" + empty: "ne može biti prazno" + blank: "ne može biti prazno" + too_long: "je predugaÄka (maksimum znakova je %{count})" + too_short: "je prekratka (minimum znakova je %{count})" + wrong_length: "je pogreÅ¡ne dužine (broj znakova mora biti %{count})" + taken: "je već u upotrebi" + not_a_number: "nije broj" + not_a_date: "nije ispravan datum" + greater_than: "mora biti veći od %{count}" + greater_than_or_equal_to: "mora biti veći ili jednak %{count}" + equal_to: "mora biti jednak %{count}" + less_than: "mora biti manji od %{count}" + less_than_or_equal_to: "mora biti manji ili jednak %{count}" + odd: "mora biti paran" + even: "mora biti neparan" + greater_than_start_date: "mora biti veći od poÄetnog datuma" + not_same_project: "ne pripada istom projektu" + circular_dependency: "Ova veza će stvoriti kružnu referencu" + cant_link_an_issue_with_a_descendant: "Problem ne može biti povezan sa jednim od svojih podzadataka" + + actionview_instancetag_blank_option: Molim odaberite + + general_text_No: 'Ne' + general_text_Yes: 'Da' + general_text_no: 'ne' + general_text_yes: 'da' + general_lang_name: 'Serbian (Srpski)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: Nalog je uspeÅ¡no ažuriran. + notice_account_invalid_creditentials: Neispravno korisniÄko ime ili lozinka. + notice_account_password_updated: Lozinka je uspeÅ¡no ažurirana. + notice_account_wrong_password: PogreÅ¡na lozinka + notice_account_register_done: KorisniÄki nalog je uspeÅ¡no kreiran. Kliknite na link koji ste dobili u e-poruci za aktivaciju. + notice_account_unknown_email: Nepoznat korisnik. + notice_can_t_change_password: Ovaj korisniÄki nalog za potvrdu identiteta koristi spoljni izvor. Nemoguće je promeniti lozinku. + notice_account_lost_email_sent: Poslata vam je e-poruka sa uputstvom za izbor nove lozinke + notice_account_activated: VaÅ¡ korisniÄki nalog je aktiviran. Sada se možete prijaviti. + notice_successful_create: UspeÅ¡no kreiranje. + notice_successful_update: UspeÅ¡no ažuriranje. + notice_successful_delete: UspeÅ¡no brisanje. + notice_successful_connection: UspeÅ¡no povezivanje. + notice_file_not_found: Strana kojoj želite pristupiti ne postoji ili je uklonjena. + notice_locking_conflict: Podatak je ažuriran od strane drugog korisnika. + notice_not_authorized: Niste ovlašćeni za pristup ovoj strani. + notice_email_sent: "E-poruka je poslata na %{value}" + notice_email_error: "Dogodila se greÅ¡ka prilikom slanja e-poruke (%{value})" + notice_feeds_access_key_reseted: VaÅ¡ RSS pristupni kljuÄ je poniÅ¡ten. + notice_api_access_key_reseted: VaÅ¡ API pristupni kljuÄ je poniÅ¡ten. + notice_failed_to_save_issues: "NeuspeÅ¡no snimanje %{count} problema od %{total} odabranih: %{ids}." + notice_failed_to_save_members: "NeuspeÅ¡no snimanje Älana(ova): %{errors}." + notice_no_issue_selected: "Ni jedan problem nije odabran! Molimo, odaberite problem koji želite da menjate." + notice_account_pending: "VaÅ¡ nalog je kreiran i Äeka na odobrenje administratora." + notice_default_data_loaded: Podrazumevano konfigurisanje je uspeÅ¡no uÄitano. + notice_unable_delete_version: Verziju je nemoguće izbrisati. + notice_unable_delete_time_entry: Stavku evidencije vremena je nemoguće izbrisati. + notice_issue_done_ratios_updated: Odnos reÅ¡enih problema je ažuriran. + + error_can_t_load_default_data: "Podrazumevano konfigurisanje je nemoguće uÄitati: %{value}" + error_scm_not_found: "Stavka ili ispravka nisu pronaÄ‘ene u spremiÅ¡tu." + error_scm_command_failed: "GreÅ¡ka se javila prilikom pokuÅ¡aja pristupa spremiÅ¡tu: %{value}" + error_scm_annotate: "Stavka ne postoji ili ne može biti oznaÄena." + error_issue_not_found_in_project: 'Problem nije pronaÄ‘en ili ne pripada ovom projektu.' + error_no_tracker_in_project: 'Ni jedno praćenje nije povezano sa ovim projektom. Molimo proverite podeÅ¡avanja projekta.' + error_no_default_issue_status: 'Podrazumevani status problema nije definisan. Molimo proverite vaÅ¡e konfigurisanje (idite na "Administracija -> Statusi problema").' + error_can_not_delete_custom_field: Nemoguće je izbrisati prilagoÄ‘eno polje + error_can_not_delete_tracker: "Ovo praćenje sadrži probleme i ne može biti obrisano." + error_can_not_remove_role: "Ova uloga je u upotrebi i ne može biti obrisana." + error_can_not_reopen_issue_on_closed_version: 'Problem dodeljen zatvorenoj verziji ne može biti ponovo otvoren' + error_can_not_archive_project: Ovaj projekat se ne može arhivirati + error_issue_done_ratios_not_updated: "Odnos reÅ¡enih problema nije ažuriran." + error_workflow_copy_source: 'Molimo odaberite izvorno praćenje ili ulogu' + error_workflow_copy_target: 'Molimo odaberite odrediÅ¡no praćenje i ulogu' + error_unable_delete_issue_status: 'Status problema je nemoguće obrisati' + error_unable_to_connect: "Povezivanje sa (%{value}) je nemoguće" + warning_attachments_not_saved: "%{count} datoteka ne može biti snimljena." + + mail_subject_lost_password: "VaÅ¡a %{value} lozinka" + mail_body_lost_password: 'Za promenu vaÅ¡e lozinke, kliknite na sledeći link:' + mail_subject_register: "Aktivacija vaÅ¡eg %{value} naloga" + mail_body_register: 'Za aktivaciju vaÅ¡eg naloga, kliknite na sledeći link:' + mail_body_account_information_external: "VaÅ¡ nalog %{value} možete koristiti za prijavu." + mail_body_account_information: Informacije o vaÅ¡em nalogu + mail_subject_account_activation_request: "Zahtev za aktivaciju naloga %{value}" + mail_body_account_activation_request: "Novi korisnik (%{value}) je registrovan. Nalog Äeka na vaÅ¡e odobrenje:" + mail_subject_reminder: "%{count} problema dospeva narednih %{days} dana" + mail_body_reminder: "%{count} problema dodeljenih vama dospeva u narednih %{days} dana:" + mail_subject_wiki_content_added: "Wiki stranica '%{id}' je dodata" + mail_body_wiki_content_added: "%{author} je dodao wiki stranicu '%{id}'." + mail_subject_wiki_content_updated: "Wiki stranica '%{id}' je ažurirana" + mail_body_wiki_content_updated: "%{author} je ažurirao wiki stranicu '%{id}'." + + gui_validation_error: jedna greÅ¡ka + gui_validation_error_plural: "%{count} greÅ¡aka" + + field_name: Naziv + field_description: Opis + field_summary: Rezime + field_is_required: Obavezno + field_firstname: Ime + field_lastname: Prezime + field_mail: E-adresa + field_filename: Datoteka + field_filesize: VeliÄina + field_downloads: Preuzimanja + field_author: Autor + field_created_on: Kreirano + field_updated_on: Ažurirano + field_field_format: Format + field_is_for_all: Za sve projekte + field_possible_values: Moguće vrednosti + field_regexp: Regularan izraz + field_min_length: Minimalna dužina + field_max_length: Maksimalna dužina + field_value: Vrednost + field_category: Kategorija + field_title: Naslov + field_project: Projekat + field_issue: Problem + field_status: Status + field_notes: BeleÅ¡ke + field_is_closed: Zatvoren problem + field_is_default: Podrazumevana vrednost + field_tracker: Praćenje + field_subject: Predmet + field_due_date: Krajnji rok + field_assigned_to: Dodeljeno + field_priority: Prioritet + field_fixed_version: OdrediÅ¡na verzija + field_user: Korisnik + field_principal: Glavni + field_role: Uloga + field_homepage: PoÄetna stranica + field_is_public: Javno objavljivanje + field_parent: Potprojekat od + field_is_in_roadmap: Problemi prikazani u planu rada + field_login: KorisniÄko ime + field_mail_notification: ObaveÅ¡tenja putem e-poÅ¡te + field_admin: Administrator + field_last_login_on: Poslednje povezivanje + field_language: Jezik + field_effective_date: Datum + field_password: Lozinka + field_new_password: Nova lozinka + field_password_confirmation: Potvrda lozinke + field_version: Verzija + field_type: Tip + field_host: Glavni raÄunar + field_port: Port + field_account: KorisniÄki nalog + field_base_dn: Bazni DN + field_attr_login: Atribut prijavljivanja + field_attr_firstname: Atribut imena + field_attr_lastname: Atribut prezimena + field_attr_mail: Atribut e-adrese + field_onthefly: Kreiranje korisnika u toku rada + field_start_date: PoÄetak + field_done_ratio: "% uraÄ‘eno" + field_auth_source: Režim potvrde identiteta + field_hide_mail: Sakrij moju e-adresu + field_comments: Komentar + field_url: URL + field_start_page: PoÄetna stranica + field_subproject: Potprojekat + field_hours: sati + field_activity: Aktivnost + field_spent_on: Datum + field_identifier: Identifikator + field_is_filter: Upotrebi kao filter + field_issue_to: Srodni problemi + field_delay: KaÅ¡njenje + field_assignable: Problem može biti dodeljen ovoj ulozi + field_redirect_existing_links: Preusmeri postojeće veze + field_estimated_hours: Proteklo vreme + field_column_names: Kolone + field_time_zone: Vremenska zona + field_searchable: Može da se pretražuje + field_default_value: Podrazumevana vrednost + field_comments_sorting: Prikaži komentare + field_parent_title: MatiÄna stranica + field_editable: Izmenljivo + field_watcher: PosmatraÄ + field_identity_url: OpenID URL + field_content: Sadržaj + field_group_by: Grupisanje rezultata po + field_sharing: Deljenje + field_parent_issue: MatiÄni zadatak + + setting_app_title: Naslov aplikacije + setting_app_subtitle: Podnaslov aplikacije + setting_welcome_text: Tekst dobrodoÅ¡lice + setting_default_language: Podrazumevani jezik + setting_login_required: Obavezna potvrda identiteta + setting_self_registration: Samoregistracija + setting_attachment_max_size: Maks. veliÄina priložene datoteke + setting_issues_export_limit: OgraniÄenje izvoza „problema“ + setting_mail_from: E-adresa poÅ¡iljaoca + setting_bcc_recipients: Primaoci „Bcc“ kopije + setting_plain_text_mail: Poruka sa Äistim tekstom (bez HTML-a) + setting_host_name: Putanja i naziv glavnog raÄunara + setting_text_formatting: Oblikovanje teksta + setting_wiki_compression: Kompresija Wiki istorije + setting_feeds_limit: OgraniÄenje sadržaja izvora vesti + setting_default_projects_public: Podrazumeva se javno prikazivanje novih projekata + setting_autofetch_changesets: IzvrÅ¡avanje automatskog preuzimanja + setting_sys_api_enabled: Omogućavanje WS za upravljanje spremiÅ¡tem + setting_commit_ref_keywords: Referenciranje kljuÄnih reÄi + setting_commit_fix_keywords: Popravljanje kljuÄnih reÄi + setting_autologin: Automatska prijava + setting_date_format: Format datuma + setting_time_format: Format vremena + setting_cross_project_issue_relations: Dozvoli povezivanje problema iz unakrsnih projekata + setting_issue_list_default_columns: Podrazumevane kolone prikazane na spisku problema + setting_emails_footer: Podnožje stranice e-poruke + setting_protocol: Protokol + setting_per_page_options: Opcije prikaza objekata po stranici + setting_user_format: Format prikaza korisnika + setting_activity_days_default: Broj dana prikazanih na projektnoj aktivnosti + setting_display_subprojects_issues: Prikazuj probleme iz potprojekata na glavnom projektu, ukoliko nije drugaÄije navedeno + setting_enabled_scm: Omogućavanje SCM + setting_mail_handler_body_delimiters: "Skraćivanje e-poruke nakon jedne od ovih linija" + setting_mail_handler_api_enabled: Omogućavanje WS dolazne e-poruke + setting_mail_handler_api_key: API kljuÄ + setting_sequential_project_identifiers: Generisanje sekvencijalnog imena projekta + setting_gravatar_enabled: Koristi Gravatar korisniÄke ikone + setting_gravatar_default: Podrazumevana Gravatar slika + setting_diff_max_lines_displayed: Maks. broj prikazanih razliÄitih linija + setting_file_max_size_displayed: Maks. veliÄina tekst. datoteka prikazanih umetnuto + setting_repository_log_display_limit: Maks. broj revizija prikazanih u datoteci za evidenciju + setting_openid: Dozvoli OpenID prijavu i registraciju + setting_password_min_length: Minimalna dužina lozinke + setting_new_project_user_role_id: Kreatoru projekta (koji nije administrator) dodeljuje je uloga + setting_default_projects_modules: Podrazumevano omogućeni moduli za nove projekte + setting_issue_done_ratio: IzraÄunaj odnos reÅ¡enih problema + setting_issue_done_ratio_issue_field: koristeći polje problema + setting_issue_done_ratio_issue_status: koristeći status problema + setting_start_of_week: Prvi dan u sedmici + setting_rest_api_enabled: Omogući REST web usluge + setting_cache_formatted_text: KeÅ¡iranje obraÄ‘enog teksta + + permission_add_project: Kreiranje projekta + permission_add_subprojects: Kreiranje potpojekta + permission_edit_project: Izmena projekata + permission_select_project_modules: Odabiranje modula projekta + permission_manage_members: Upravljanje Älanovima + permission_manage_project_activities: Upravljanje projektnim aktivnostima + permission_manage_versions: Upravljanje verzijama + permission_manage_categories: Upravljanje kategorijama problema + permission_view_issues: Pregled problema + permission_add_issues: Dodavanje problema + permission_edit_issues: Izmena problema + permission_manage_issue_relations: Upravljanje vezama izmeÄ‘u problema + permission_add_issue_notes: Dodavanje beleÅ¡ki + permission_edit_issue_notes: Izmena beleÅ¡ki + permission_edit_own_issue_notes: Izmena sopstvenih beleÅ¡ki + permission_move_issues: Pomeranje problema + permission_delete_issues: Brisanje problema + permission_manage_public_queries: Upravljanje javnim upitima + permission_save_queries: Snimanje upita + permission_view_gantt: Pregledanje Gantovog dijagrama + permission_view_calendar: Pregledanje kalendara + permission_view_issue_watchers: Pregledanje spiska posmatraÄa + permission_add_issue_watchers: Dodavanje posmatraÄa + permission_delete_issue_watchers: Brisanje posmatraÄa + permission_log_time: Beleženje utroÅ¡enog vremena + permission_view_time_entries: Pregledanje utroÅ¡enog vremena + permission_edit_time_entries: Izmena utroÅ¡enog vremena + permission_edit_own_time_entries: Izmena sopstvenog utroÅ¡enog vremena + permission_manage_news: Upravljanje vestima + permission_comment_news: Komentarisanje vesti + permission_manage_documents: Upravljanje dokumentima + permission_view_documents: Pregledanje dokumenata + permission_manage_files: Upravljanje datotekama + permission_view_files: Pregledanje datoteka + permission_manage_wiki: Upravljanje wiki stranicama + permission_rename_wiki_pages: Promena imena wiki stranicama + permission_delete_wiki_pages: Brisanje wiki stranica + permission_view_wiki_pages: Pregledanje wiki stranica + permission_view_wiki_edits: Pregledanje wiki istorije + permission_edit_wiki_pages: Izmena wiki stranica + permission_delete_wiki_pages_attachments: Brisanje priloženih datoteka + permission_protect_wiki_pages: ZaÅ¡tita wiki stranica + permission_manage_repository: Upravljanje spremiÅ¡tem + permission_browse_repository: Pregledanje spremiÅ¡ta + permission_view_changesets: Pregledanje skupa promena + permission_commit_access: Potvrda pristupa + permission_manage_boards: Upravljanje forumima + permission_view_messages: Pregledanje poruka + permission_add_messages: Slanje poruka + permission_edit_messages: Izmena poruka + permission_edit_own_messages: Izmena sopstvenih poruka + permission_delete_messages: Brisanje poruka + permission_delete_own_messages: Brisanje sopstvenih poruka + permission_export_wiki_pages: Izvoz wiki stranica + permission_manage_subtasks: Upravljanje podzadacima + + project_module_issue_tracking: Praćenje problema + project_module_time_tracking: Praćenje vremena + project_module_news: Vesti + project_module_documents: Dokumenti + project_module_files: Datoteke + project_module_wiki: Wiki + project_module_repository: SpremiÅ¡te + project_module_boards: Forumi + + label_user: Korisnik + label_user_plural: Korisnici + label_user_new: Novi korisnik + label_user_anonymous: Anoniman + label_project: Projekat + label_project_new: Novi projekat + label_project_plural: Projekti + label_x_projects: + zero: nema projekata + one: jedan projekat + other: "%{count} projekata" + label_project_all: Svi projekti + label_project_latest: Poslednji projekti + label_issue: Problem + label_issue_new: Novi problem + label_issue_plural: Problemi + label_issue_view_all: Prikaz svih problema + label_issues_by: "Problemi (%{value})" + label_issue_added: Problem je dodat + label_issue_updated: Problem je ažuriran + label_document: Dokument + label_document_new: Novi dokument + label_document_plural: Dokumenti + label_document_added: Dokument je dodat + label_role: Uloga + label_role_plural: Uloge + label_role_new: Nova uloga + label_role_and_permissions: Uloge i dozvole + label_member: ÄŒlan + label_member_new: Novi Älan + label_member_plural: ÄŒlanovi + label_tracker: Praćenje + label_tracker_plural: Praćenja + label_tracker_new: Novo praćenje + label_workflow: Tok posla + label_issue_status: Status problema + label_issue_status_plural: Statusi problema + label_issue_status_new: Novi status + label_issue_category: Kategorija problema + label_issue_category_plural: Kategorije problema + label_issue_category_new: Nova kategorija + label_custom_field: PrilagoÄ‘eno polje + label_custom_field_plural: PrilagoÄ‘ena polja + label_custom_field_new: Novo prilagoÄ‘eno polje + label_enumerations: Nabrojiva lista + label_enumeration_new: Nova vrednost + label_information: Informacija + label_information_plural: Informacije + label_please_login: Molimo, prijavite se + label_register: Registracija + label_login_with_open_id_option: ili prijava sa OpenID + label_password_lost: Izgubljena lozinka + label_home: PoÄetak + label_my_page: Moja stranica + label_my_account: Moj nalog + label_my_projects: Moji projekti + label_my_page_block: My page block + label_administration: Administracija + label_login: Prijava + label_logout: Odjava + label_help: Pomoć + label_reported_issues: Prijavljeni problemi + label_assigned_to_me_issues: Problemi dodeljeni meni + label_last_login: Poslednje povezivanje + label_registered_on: Registrovan + label_activity: Aktivnost + label_overall_activity: Celokupna aktivnost + label_user_activity: "Aktivnost korisnika %{value}" + label_new: Novo + label_logged_as: Prijavljeni ste kao + label_environment: Okruženje + label_authentication: Potvrda identiteta + label_auth_source: Režim potvrde identiteta + label_auth_source_new: Novi režim potvrde identiteta + label_auth_source_plural: Režimi potvrde identiteta + label_subproject_plural: Potprojekti + label_subproject_new: Novi potprojekat + label_and_its_subprojects: "%{value} i njegovi potprojekti" + label_min_max_length: Min. - Maks. dužina + label_list: Spisak + label_date: Datum + label_integer: Ceo broj + label_float: Sa pokretnim zarezom + label_boolean: LogiÄki operator + label_string: Tekst + label_text: Dugi tekst + label_attribute: Osobina + label_attribute_plural: Osobine + label_download: "%{count} preuzimanje" + label_download_plural: "%{count} preuzimanja" + label_no_data: Nema podataka za prikazivanje + label_change_status: Promena statusa + label_history: Istorija + label_attachment: Datoteka + label_attachment_new: Nova datoteka + label_attachment_delete: Brisanje datoteke + label_attachment_plural: Datoteke + label_file_added: Datoteka je dodata + label_report: IzveÅ¡taj + label_report_plural: IzveÅ¡taji + label_news: Vesti + label_news_new: Dodavanje vesti + label_news_plural: Vesti + label_news_latest: Poslednje vesti + label_news_view_all: Prikaz svih vesti + label_news_added: Vesti su dodate + label_settings: PodeÅ¡avanja + label_overview: Pregled + label_version: Verzija + label_version_new: Nova verzija + label_version_plural: Verzije + label_close_versions: Zatvori zavrÅ¡ene verzije + label_confirmation: Potvrda + label_export_to: 'TakoÄ‘e dostupno i u varijanti:' + label_read: ÄŒitanje... + label_public_projects: Javni projekti + label_open_issues: otvoren + label_open_issues_plural: otvorenih + label_closed_issues: zatvoren + label_closed_issues_plural: zatvorenih + label_x_open_issues_abbr_on_total: + zero: 0 otvorenih / %{total} + one: 1 otvoren / %{total} + other: "%{count} otvorenih / %{total}" + label_x_open_issues_abbr: + zero: 0 otvorenih + one: 1 otvoren + other: "%{count} otvorenih" + label_x_closed_issues_abbr: + zero: 0 zatvorenih + one: 1 zatvoren + other: "%{count} zatvorenih" + label_total: Ukupno + label_permissions: Dozvole + label_current_status: Trenutni status + label_new_statuses_allowed: Novi statusi dozvoljeni + label_all: svi + label_none: nijedan + label_nobody: nikome + label_next: Sledeće + label_previous: Prethodno + label_used_by: Koristio + label_details: Detalji + label_add_note: Dodaj beleÅ¡ku + label_per_page: Po strani + label_calendar: Kalendar + label_months_from: meseci od + label_gantt: Gantov dijagram + label_internal: UnutraÅ¡nji + label_last_changes: "poslednjih %{count} promena" + label_change_view_all: Prikaži sve promene + label_personalize_page: Personalizuj ovu stranu + label_comment: Komentar + label_comment_plural: Komentari + label_x_comments: + zero: bez komentara + one: jedan komentar + other: "%{count} komentara" + label_comment_add: Dodaj komentar + label_comment_added: Komentar dodat + label_comment_delete: ObriÅ¡i komentare + label_query: PrilagoÄ‘en upit + label_query_plural: PrilagoÄ‘eni upiti + label_query_new: Novi upit + label_filter_add: Dodavanje filtera + label_filter_plural: Filteri + label_equals: je + label_not_equals: nije + label_in_less_than: manje od + label_in_more_than: viÅ¡e od + label_greater_or_equal: '>=' + label_less_or_equal: '<=' + label_in: u + label_today: danas + label_all_time: sve vreme + label_yesterday: juÄe + label_this_week: ove sedmice + label_last_week: poslednje sedmice + label_last_n_days: "poslednjih %{count} dana" + label_this_month: ovog meseca + label_last_month: poslednjeg meseca + label_this_year: ove godine + label_date_range: Vremenski period + label_less_than_ago: pre manje od nekoliko dana + label_more_than_ago: pre viÅ¡e od nekoliko dana + label_ago: pre nekoliko dana + label_contains: sadrži + label_not_contains: ne sadrži + label_day_plural: dana + label_repository: SpremiÅ¡te + label_repository_plural: SpremiÅ¡ta + label_browse: Pregledanje + label_modification: "%{count} promena" + label_modification_plural: "%{count} promena" + label_branch: Grana + label_tag: Oznaka + label_revision: Revizija + label_revision_plural: Revizije + label_revision_id: "Revizija %{value}" + label_associated_revisions: Pridružene revizije + label_added: dodato + label_modified: promenjeno + label_copied: kopirano + label_renamed: preimenovano + label_deleted: izbrisano + label_latest_revision: Poslednja revizija + label_latest_revision_plural: Poslednje revizije + label_view_revisions: Pregled revizija + label_view_all_revisions: Pregled svih revizija + label_max_size: Maksimalna veliÄina + label_sort_highest: PremeÅ¡tanje na vrh + label_sort_higher: PremeÅ¡tanje na gore + label_sort_lower: PremeÅ¡tanje na dole + label_sort_lowest: PremeÅ¡tanje na dno + label_roadmap: Plan rada + label_roadmap_due_in: "Dospeva %{value}" + label_roadmap_overdue: "%{value} najkasnije" + label_roadmap_no_issues: Nema problema za ovu verziju + label_search: Pretraga + label_result_plural: Rezultati + label_all_words: Sve reÄi + label_wiki: Wiki + label_wiki_edit: Wiki izmena + label_wiki_edit_plural: Wiki izmene + label_wiki_page: Wiki stranica + label_wiki_page_plural: Wiki stranice + label_index_by_title: Indeksiranje po naslovu + label_index_by_date: Indeksiranje po datumu + label_current_version: Trenutna verzija + label_preview: Pregled + label_feed_plural: Izvori vesti + label_changes_details: Detalji svih promena + label_issue_tracking: Praćenje problema + label_spent_time: UtroÅ¡eno vreme + label_overall_spent_time: Celokupno utroÅ¡eno vreme + label_f_hour: "%{value} sat" + label_f_hour_plural: "%{value} sati" + label_time_tracking: Praćenje vremena + label_change_plural: Promene + label_statistics: Statistika + label_commits_per_month: IzvrÅ¡enja meseÄno + label_commits_per_author: IzvrÅ¡enja po autoru + label_view_diff: Pogledaj razlike + label_diff_inline: unutra + label_diff_side_by_side: uporedo + label_options: Opcije + label_copy_workflow_from: Kopiranje toka posla od + label_permissions_report: IzveÅ¡taj o dozvolama + label_watched_issues: Posmatrani problemi + label_related_issues: Srodni problemi + label_applied_status: Primenjeni statusi + label_loading: UÄitavanje... + label_relation_new: Nova relacija + label_relation_delete: Brisanje relacije + label_relates_to: srodnih sa + label_duplicates: dupliranih + label_duplicated_by: dupliranih od + label_blocks: odbijenih + label_blocked_by: odbijenih od + label_precedes: prethodi + label_follows: praćenih + label_end_to_start: od kraja do poÄetka + label_end_to_end: od kraja do kraja + label_start_to_start: od poÄetka do poÄetka + label_start_to_end: od poÄetka do kraja + label_stay_logged_in: Ostanite prijavljeni + label_disabled: onemogućeno + label_show_completed_versions: Prikazivanje zavrÅ¡ene verzije + label_me: meni + label_board: Forum + label_board_new: Novi forum + label_board_plural: Forumi + label_board_locked: ZakljuÄana + label_board_sticky: Lepljiva + label_topic_plural: Teme + label_message_plural: Poruke + label_message_last: Poslednja poruka + label_message_new: Nova poruka + label_message_posted: Poruka je dodata + label_reply_plural: Odgovori + label_send_information: PoÅ¡alji korisniku detalje naloga + label_year: Godina + label_month: Mesec + label_week: Sedmica + label_date_from: Å alje + label_date_to: Prima + label_language_based: Bazirano na jeziku korisnika + label_sort_by: "Sortirano po %{value}" + label_send_test_email: Slanje probne e-poruke + label_feeds_access_key: RSS pristupni kljuÄ + label_missing_feeds_access_key: RSS pristupni kljuÄ nedostaje + label_feeds_access_key_created_on: "RSS pristupni kljuÄ je napravljen pre %{value}" + label_module_plural: Moduli + label_added_time_by: "Dodao %{author} pre %{age}" + label_updated_time_by: "Ažurirao %{author} pre %{age}" + label_updated_time: "Ažurirano pre %{value}" + label_jump_to_a_project: Skok na projekat... + label_file_plural: Datoteke + label_changeset_plural: Skupovi promena + label_default_columns: Podrazumevane kolone + label_no_change_option: (Bez promena) + label_bulk_edit_selected_issues: Grupna izmena odabranih problema + label_theme: Tema + label_default: Podrazumevano + label_search_titles_only: Pretražuj samo naslove + label_user_mail_option_all: "Za bilo koji dogaÄ‘aj na svim mojim projektima" + label_user_mail_option_selected: "Za bilo koji dogaÄ‘aj na samo odabranim projektima..." + label_user_mail_no_self_notified: "Ne želim biti obaveÅ¡tavan za promene koje sam pravim" + label_registration_activation_by_email: aktivacija naloga putem e-poruke + label_registration_manual_activation: ruÄna aktivacija naloga + label_registration_automatic_activation: automatska aktivacija naloga + label_display_per_page: "Broj stavki po stranici: %{value}" + label_age: Starost + label_change_properties: Promeni svojstva + label_general: OpÅ¡ti + label_more: ViÅ¡e + label_scm: SCM + label_plugins: Dodatne komponente + label_ldap_authentication: LDAP potvrda identiteta + label_downloads_abbr: D/L + label_optional_description: Opciono opis + label_add_another_file: Dodaj joÅ¡ jednu datoteku + label_preferences: PodeÅ¡avanja + label_chronological_order: po hronoloÅ¡kom redosledu + label_reverse_chronological_order: po obrnutom hronoloÅ¡kom redosledu + label_planning: Planiranje + label_incoming_emails: Dolazne e-poruke + label_generate_key: Generisanje kljuÄa + label_issue_watchers: PosmatraÄi + label_example: Primer + label_display: Prikaz + label_sort: Sortiranje + label_ascending: Rastući niz + label_descending: Opadajući niz + label_date_from_to: Od %{start} do %{end} + label_wiki_content_added: Wiki stranica je dodata + label_wiki_content_updated: Wiki stranica je ažurirana + label_group: Grupa + label_group_plural: Grupe + label_group_new: Nova grupa + label_time_entry_plural: UtroÅ¡eno vreme + label_version_sharing_none: Nije deljeno + label_version_sharing_descendants: Sa potprojektima + label_version_sharing_hierarchy: Sa hijerarhijom projekta + label_version_sharing_tree: Sa stablom projekta + label_version_sharing_system: Sa svim projektima + label_update_issue_done_ratios: Ažuriraj odnos reÅ¡enih problema + label_copy_source: Izvor + label_copy_target: OdrediÅ¡te + label_copy_same_as_target: Isto kao odrediÅ¡te + label_display_used_statuses_only: Prikazuj statuse korišćene samo od strane ovog praćenja + label_api_access_key: API pristupni kljuÄ + label_missing_api_access_key: Nedostaje API pristupni kljuÄ + label_api_access_key_created_on: "API pristupni kljuÄ je kreiran pre %{value}" + label_profile: Profil + label_subtask_plural: Podzadatak + label_project_copy_notifications: PoÅ¡alji e-poruku sa obaveÅ¡tenjem prilikom kopiranja projekta + + button_login: Prijava + button_submit: PoÅ¡alji + button_save: Snimi + button_check_all: UkljuÄi sve + button_uncheck_all: IskljuÄi sve + button_delete: IzbriÅ¡i + button_create: Kreiraj + button_create_and_continue: Kreiraj i nastavi + button_test: Test + button_edit: Izmeni + button_add: Dodaj + button_change: Promeni + button_apply: Primeni + button_clear: ObriÅ¡i + button_lock: ZakljuÄaj + button_unlock: OtkljuÄaj + button_download: Preuzmi + button_list: Spisak + button_view: Prikaži + button_move: Pomeri + button_move_and_follow: Pomeri i prati + button_back: Nazad + button_cancel: PoniÅ¡ti + button_activate: Aktiviraj + button_sort: Sortiraj + button_log_time: Evidentiraj vreme + button_rollback: Povratak na ovu verziju + button_watch: Prati + button_unwatch: Ne prati viÅ¡e + button_reply: Odgovori + button_archive: Arhiviraj + button_unarchive: Vrati iz arhive + button_reset: PoniÅ¡ti + button_rename: Preimenuj + button_change_password: Promeni lozinku + button_copy: Kopiraj + button_copy_and_follow: Kopiraj i prati + button_annotate: Pribeleži + button_update: Ažuriraj + button_configure: Podesi + button_quote: Pod navodnicima + button_duplicate: Dupliraj + button_show: Prikaži + + status_active: aktivni + status_registered: registrovani + status_locked: zakljuÄani + + version_status_open: otvoren + version_status_locked: zakljuÄan + version_status_closed: zatvoren + + field_active: Aktivan + + text_select_mail_notifications: Odaberi akcije za koje će obaveÅ¡tenje biti poslato putem e-poÅ¡te. + text_regexp_info: npr. ^[A-Z0-9]+$ + text_min_max_length_info: 0 znaÄi bez ograniÄenja + text_project_destroy_confirmation: Jeste li sigurni da želite da izbriÅ¡ete ovaj projekat i sve pripadajuće podatke? + text_subprojects_destroy_warning: "Potprojekti: %{value} će takoÄ‘e biti izbrisan." + text_workflow_edit: Odaberite ulogu i praćenje za izmenu toka posla + text_are_you_sure: Jeste li sigurni? + text_journal_changed: "%{label} promenjen od %{old} u %{new}" + text_journal_set_to: "%{label} postavljen u %{value}" + text_journal_deleted: "%{label} izbrisano (%{old})" + text_journal_added: "%{label} %{value} dodato" + text_tip_issue_begin_day: zadatak poÄinje ovog dana + text_tip_issue_end_day: zadatak se zavrÅ¡ava ovog dana + text_tip_issue_begin_end_day: zadatak poÄinje i zavrÅ¡ava ovog dana + text_caracters_maximum: "NajviÅ¡e %{count} znak(ova)." + text_caracters_minimum: "Broj znakova mora biti najmanje %{count}." + text_length_between: "Broj znakova mora biti izmeÄ‘u %{min} i %{max}." + text_tracker_no_workflow: Ovo praćenje nema definisan tok posla + text_unallowed_characters: Nedozvoljeni znakovi + text_comma_separated: Dozvoljene su viÅ¡estruke vrednosti (odvojene zarezom). + text_line_separated: Dozvoljene su viÅ¡estruke vrednosti (jedan red za svaku vrednost). + text_issues_ref_in_commit_messages: Referenciranje i popravljanje problema u izvrÅ¡nim porukama + text_issue_added: "%{author} je prijavio problem %{id}." + text_issue_updated: "%{author} je ažurirao problem %{id}." + text_wiki_destroy_confirmation: Jeste li sigurni da želite da obriÅ¡ete wiki i sav sadržaj? + text_issue_category_destroy_question: "Nekoliko problema (%{count}) je dodeljeno ovoj kategoriji. Å ta želite da uradite?" + text_issue_category_destroy_assignments: Ukloni dodeljene kategorije + text_issue_category_reassign_to: Dodeli ponovo probleme ovoj kategoriji + text_user_mail_option: "Za neizabrane projekte, dobićete samo obaveÅ¡tenje o stvarima koje pratite ili ste ukljuÄeni (npr. problemi Äiji ste vi autor ili zastupnik)." + text_no_configuration_data: "Uloge, praćenja, statusi problema i toka posla joÅ¡ uvek nisu podeÅ¡eni.\nPreporuÄljivo je da uÄitate podrazumevano konfigurisanje. Izmena je moguća nakon prvog uÄitavanja." + text_load_default_configuration: UÄitaj podrazumevano konfigurisanje + text_status_changed_by_changeset: "Primenjeno u skupu sa promenama %{value}." + text_issues_destroy_confirmation: 'Jeste li sigurni da želite da izbriÅ¡ete odabrane probleme?' + text_select_project_modules: 'Odaberite module koje želite omogućiti za ovaj projekat:' + text_default_administrator_account_changed: Podrazumevani administratorski nalog je promenjen + text_file_repository_writable: Fascikla priloženih datoteka je upisiva + text_plugin_assets_writable: Fascikla elemenata dodatnih komponenti je upisiva + text_rmagick_available: RMagick je dostupan (opciono) + text_destroy_time_entries_question: "%{hours} sati je prijavljeno za ovaj problem koji želite izbrisati. Å ta želite da uradite?" + text_destroy_time_entries: IzbriÅ¡i prijavljene sate + text_assign_time_entries_to_project: Dodeli prijavljene sate projektu + text_reassign_time_entries: 'Dodeli ponovo prijavljene sate ovom problemu:' + text_user_wrote: "%{value} je napisao:" + text_enumeration_destroy_question: "%{count} objekat(a) je dodeljeno ovoj vrednosti." + text_enumeration_category_reassign_to: 'Dodeli ih ponovo ovoj vrednosti:' + text_email_delivery_not_configured: "Isporuka e-poruka nije konfigurisana i obaveÅ¡tenja su onemogućena.\nPodesite vaÅ¡ SMTP server u config/configuration.yml i pokrenite ponovo aplikaciju za njihovo omogućavanje." + text_repository_usernames_mapping: "Odaberite ili ažurirajte Redmine korisnike mapiranjem svakog korisniÄkog imena pronaÄ‘enog u evidenciji spremiÅ¡ta.\nKorisnici sa istim Redmine imenom i imenom spremiÅ¡ta ili e-adresom su automatski mapirani." + text_diff_truncated: '... Ova razlika je iseÄena jer je dostignuta maksimalna veliÄina prikaza.' + text_custom_field_possible_values_info: 'Jedan red za svaku vrednost' + text_wiki_page_destroy_question: "Ova stranica ima %{descendants} podreÄ‘enih stranica i podstranica. Å ta želite da uradite?" + text_wiki_page_nullify_children: "Zadrži podreÄ‘ene stranice kao korene stranice" + text_wiki_page_destroy_children: "IzbriÅ¡i podreÄ‘ene stranice i sve njihove podstranice" + text_wiki_page_reassign_children: "Dodeli ponovo podreÄ‘ene stranice ovoj matiÄnoj stranici" + text_own_membership_delete_confirmation: "Nakon uklanjanja pojedinih ili svih vaÅ¡ih dozvola nećete viÅ¡e moći da ureÄ‘ujete ovaj projekat.\nŽelite li da nastavite?" + text_zoom_in: Uvećaj + text_zoom_out: Umanji + + default_role_manager: Menadžer + default_role_developer: Programer + default_role_reporter: IzveÅ¡taÄ + default_tracker_bug: GreÅ¡ka + default_tracker_feature: Funkcionalnost + default_tracker_support: PodrÅ¡ka + default_issue_status_new: Novo + default_issue_status_in_progress: U toku + default_issue_status_resolved: ReÅ¡eno + default_issue_status_feedback: Povratna informacija + default_issue_status_closed: Zatvoreno + default_issue_status_rejected: Odbijeno + default_doc_category_user: KorisniÄka dokumentacija + default_doc_category_tech: TehniÄka dokumentacija + default_priority_low: Nizak + default_priority_normal: Normalan + default_priority_high: Visok + default_priority_urgent: Hitno + default_priority_immediate: Neposredno + default_activity_design: Dizajn + default_activity_development: Razvoj + + enumeration_issue_priorities: Prioriteti problema + enumeration_doc_categories: Kategorije dokumenta + enumeration_activities: Aktivnosti (praćenje vremena) + enumeration_system_activity: Sistemska aktivnost + + field_time_entries: Vreme evidencije + project_module_gantt: Gantov dijagram + project_module_calendar: Kalendar + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Text field + label_user_mail_option_only_owner: Only for things I am the owner of + setting_default_notification_option: Default notification option + label_user_mail_option_only_my_events: Only for things I watch or I'm involved in + label_user_mail_option_only_assigned: Only for things I am assigned to + label_user_mail_option_none: No events + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + notice_not_authorized_archived_project: The project you're trying to access has been archived. + label_principal_search: "Search for user or group:" + label_user_search: "Search for user:" + field_visible: Visible + setting_emails_header: Emails header + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Enable time logging + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maximum number of items displayed on the gantt chart + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: Priority updated + label_issues_visibility_own: Issues created by or assigned to the user + field_issues_visibility: Issues visibility + label_issues_visibility_all: All issues + permission_set_own_issues_private: Set own issues public or private + field_is_private: Private + permission_set_issues_private: Set issues public or private + label_issues_visibility_public: All non private issues + text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). + field_commit_logs_encoding: Kodiranje izvrÅ¡nih poruka + field_scm_path_encoding: Path encoding + text_scm_path_encoding_note: "Default: UTF-8" + field_path_to_repository: Path to repository + field_root_directory: Root directory + field_cvs_module: Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Command + text_scm_command_version: Version + label_git_report_last_commit: Report last commit for files and directories + text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 problem + one: 1 problem + other: "%{count} problemi" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: svi + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Sa potprojektima + label_cross_project_tree: Sa stablom projekta + label_cross_project_hierarchy: Sa hijerarhijom projekta + label_cross_project_system: Sa svim projektima + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/07/077e480273d388c124ae7826c285eaf11442be18.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/07/077e480273d388c124ae7826c285eaf11442be18.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,65 @@ +<% roles = Role.find_all_givable %> +<% projects = Project.active.find(:all, :order => 'lft') %> + +
    +<% if @group.memberships.any? %> + + + + + + + + <% @group.memberships.each do |membership| %> + <% next if membership.new_record? %> + + + + + +<% end; reset_cycle %> + +
    <%= l(:label_project) %><%= l(:label_role_plural) %>
    <%=h membership.project %> + <%=h membership.roles.sort.collect(&:to_s).join(', ') %> + <%= form_for(:membership, :remote => true, + :url => { :action => 'edit_membership', :id => @group, :membership_id => membership }, + :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %> +

    <% roles.each do |role| %> +
    + <% end %>

    +

    <%= submit_tag l(:button_change) %> + <%= link_to_function( + l(:button_cancel), + "$('#member-#{membership.id}-roles').show(); $('#member-#{membership.id}-roles-form').hide(); return false;" + ) %>

    + <% end %> +
    + <%= link_to_function( + l(:button_edit), + "$('#member-#{membership.id}-roles').hide(); $('#member-#{membership.id}-roles-form').show(); return false;", + :class => 'icon icon-edit' + ) %> + <%= delete_link({:controller => 'groups', :action => 'destroy_membership', :id => @group, :membership_id => membership}, + :remote => true, + :method => :post) %> +
    +<% else %> +

    <%= l(:label_no_data) %>

    +<% end %> +
    + +
    +<% if projects.any? %> +
    <%=l(:label_project_new)%> +<%= form_for(:membership, :remote => true, :url => { :action => 'edit_membership', :id => @group }) do %> +<%= label_tag "membership_project_id", l(:description_choose_project), :class => "hidden-for-sighted" %> +<%= select_tag 'membership[project_id]', options_for_membership_project_select(@group, projects) %> +

    <%= l(:label_role_plural) %>: +<% roles.each do |role| %> + +<% end %>

    +

    <%= submit_tag l(:button_add) %>

    +<% end %> +
    +<% end %> +
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/08/084dba3511eace99e8953d1d7626d2fa107595d7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/084dba3511eace99e8953d1d7626d2fa107595d7.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,111 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class NewsController < ApplicationController + default_search_scope :news + model_object News + before_filter :find_model_object, :except => [:new, :create, :index] + before_filter :find_project_from_association, :except => [:new, :create, :index] + before_filter :find_project_by_project_id, :only => [:new, :create] + before_filter :authorize, :except => [:index] + before_filter :find_optional_project, :only => :index + accept_rss_auth :index + accept_api_auth :index + + helper :watchers + helper :attachments + + def index + case params[:format] + when 'xml', 'json' + @offset, @limit = api_offset_and_limit + else + @limit = 10 + end + + scope = @project ? @project.news.visible : News.visible + + @news_count = scope.count + @news_pages = Paginator.new self, @news_count, @limit, params['page'] + @offset ||= @news_pages.current.offset + @newss = scope.all(:include => [:author, :project], + :order => "#{News.table_name}.created_on DESC", + :offset => @offset, + :limit => @limit) + + respond_to do |format| + format.html { + @news = News.new # for adding news inline + render :layout => false if request.xhr? + } + format.api + format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") } + end + end + + def show + @comments = @news.comments + @comments.reverse! if User.current.wants_comments_in_reverse_order? + end + + def new + @news = News.new(:project => @project, :author => User.current) + end + + def create + @news = News.new(:project => @project, :author => User.current) + @news.safe_attributes = params[:news] + @news.save_attachments(params[:attachments]) + if @news.save + render_attachment_warning_if_needed(@news) + flash[:notice] = l(:notice_successful_create) + redirect_to :controller => 'news', :action => 'index', :project_id => @project + else + render :action => 'new' + end + end + + def edit + end + + def update + @news.safe_attributes = params[:news] + @news.save_attachments(params[:attachments]) + if @news.save + render_attachment_warning_if_needed(@news) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'show', :id => @news + else + render :action => 'edit' + end + end + + def destroy + @news.destroy + redirect_to :action => 'index', :project_id => @project + end + + private + + def find_optional_project + return true unless params[:project_id] + @project = Project.find(params[:project_id]) + authorize + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/08/085f61120574ccfad3bedefd8a6623102e2bf32b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/085f61120574ccfad3bedefd8a6623102e2bf32b.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,34 @@ +<% content_for :header_tags do %> + <%= javascript_include_tag 'repository_navigation' %> +<% end %> + +<%= link_to l(:label_statistics), + {:action => 'stats', :id => @project, :repository_id => @repository.identifier_param}, + :class => 'icon icon-stats' if @repository.supports_all_revisions? %> + +<%= form_tag({:action => controller.action_name, + :id => @project, + :repository_id => @repository.identifier_param, + :path => to_path_param(@path), + :rev => nil}, + {:method => :get, :id => 'revision_selector'}) do -%> + + <% if !@repository.branches.nil? && @repository.branches.length > 0 -%> + | <%= l(:label_branch) %>: + <%= select_tag :branch, + options_for_select([''] + @repository.branches, @rev), + :id => 'branch' %> + <% end -%> + + <% if !@repository.tags.nil? && @repository.tags.length > 0 -%> + | <%= l(:label_tag) %>: + <%= select_tag :tag, + options_for_select([''] + @repository.tags, @rev), + :id => 'tag' %> + <% end -%> + + <% if @repository.supports_all_revisions? %> + | <%= l(:label_revision) %>: + <%= text_field_tag 'rev', @rev, :size => 8 %> + <% end %> +<% end -%> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/08/08821f4ea63f8913a9301b84afdd1e4c25ebe173.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/08821f4ea63f8913a9301b84afdd1e4c25ebe173.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,7 @@ +

    <%=l(:label_version)%>

    + +<%= labelled_form_for @version do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> + diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/08/08ad4f57c782749abd613605456ebd0acf953d61.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/08ad4f57c782749abd613605456ebd0acf953d61.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,978 @@ +begin + require 'zlib' + @@__have_zlib = true +rescue + @@__have_zlib = false +end + +require 'rexml/document' + +module SVG + module Graph + VERSION = '@ANT_VERSION@' + + # === Base object for generating SVG Graphs + # + # == Synopsis + # + # This class is only used as a superclass of specialized charts. Do not + # attempt to use this class directly, unless creating a new chart type. + # + # For examples of how to subclass this class, see the existing specific + # subclasses, such as SVG::Graph::Pie. + # + # == Examples + # + # For examples of how to use this package, see either the test files, or + # the documentation for the specific class you want to use. + # + # * file:test/plot.rb + # * file:test/single.rb + # * file:test/test.rb + # * file:test/timeseries.rb + # + # == Description + # + # This package should be used as a base for creating SVG graphs. + # + # == Acknowledgements + # + # Leo Lapworth for creating the SVG::TT::Graph package which this Ruby + # port is based on. + # + # Stephen Morgan for creating the TT template and SVG. + # + # == See + # + # * SVG::Graph::BarHorizontal + # * SVG::Graph::Bar + # * SVG::Graph::Line + # * SVG::Graph::Pie + # * SVG::Graph::Plot + # * SVG::Graph::TimeSeries + # + # == Author + # + # Sean E. Russell + # + # Copyright 2004 Sean E. Russell + # This software is available under the Ruby license[LICENSE.txt] + # + class Graph + include REXML + + # Initialize the graph object with the graph settings. You won't + # instantiate this class directly; see the subclass for options. + # [width] 500 + # [height] 300 + # [show_x_guidelines] false + # [show_y_guidelines] true + # [show_data_values] true + # [min_scale_value] 0 + # [show_x_labels] true + # [stagger_x_labels] false + # [rotate_x_labels] false + # [step_x_labels] 1 + # [step_include_first_x_label] true + # [show_y_labels] true + # [rotate_y_labels] false + # [scale_integers] false + # [show_x_title] false + # [x_title] 'X Field names' + # [show_y_title] false + # [y_title_text_direction] :bt + # [y_title] 'Y Scale' + # [show_graph_title] false + # [graph_title] 'Graph Title' + # [show_graph_subtitle] false + # [graph_subtitle] 'Graph Sub Title' + # [key] true, + # [key_position] :right, # bottom or righ + # [font_size] 12 + # [title_font_size] 16 + # [subtitle_font_size] 14 + # [x_label_font_size] 12 + # [x_title_font_size] 14 + # [y_label_font_size] 12 + # [y_title_font_size] 14 + # [key_font_size] 10 + # [no_css] false + # [add_popups] false + def initialize( config ) + @config = config + + self.top_align = self.top_font = self.right_align = self.right_font = 0 + + init_with({ + :width => 500, + :height => 300, + :show_x_guidelines => false, + :show_y_guidelines => true, + :show_data_values => true, + +# :min_scale_value => 0, + + :show_x_labels => true, + :stagger_x_labels => false, + :rotate_x_labels => false, + :step_x_labels => 1, + :step_include_first_x_label => true, + + :show_y_labels => true, + :rotate_y_labels => false, + :stagger_y_labels => false, + :scale_integers => false, + + :show_x_title => false, + :x_title => 'X Field names', + + :show_y_title => false, + :y_title_text_direction => :bt, + :y_title => 'Y Scale', + + :show_graph_title => false, + :graph_title => 'Graph Title', + :show_graph_subtitle => false, + :graph_subtitle => 'Graph Sub Title', + :key => true, + :key_position => :right, # bottom or right + + :font_size =>12, + :title_font_size =>16, + :subtitle_font_size =>14, + :x_label_font_size =>12, + :x_title_font_size =>14, + :y_label_font_size =>12, + :y_title_font_size =>14, + :key_font_size =>10, + + :no_css =>false, + :add_popups =>false, + }) + + set_defaults if respond_to? :set_defaults + + init_with config + end + + + # This method allows you do add data to the graph object. + # It can be called several times to add more data sets in. + # + # data_sales_02 = [12, 45, 21]; + # + # graph.add_data({ + # :data => data_sales_02, + # :title => 'Sales 2002' + # }) + def add_data conf + @data = [] unless defined? @data + + if conf[:data] and conf[:data].kind_of? Array + @data << conf + else + raise "No data provided by #{conf.inspect}" + end + end + + + # This method removes all data from the object so that you can + # reuse it to create a new graph but with the same config options. + # + # graph.clear_data + def clear_data + @data = [] + end + + + # This method processes the template with the data and + # config which has been set and returns the resulting SVG. + # + # This method will croak unless at least one data set has + # been added to the graph object. + # + # print graph.burn + def burn + raise "No data available" unless @data.size > 0 + + calculations if respond_to? :calculations + + start_svg + calculate_graph_dimensions + @foreground = Element.new( "g" ) + draw_graph + draw_titles + draw_legend + draw_data + @graph.add_element( @foreground ) + style + + data = "" + @doc.write( data, 0 ) + + if @config[:compress] + if @@__have_zlib + inp, out = IO.pipe + gz = Zlib::GzipWriter.new( out ) + gz.write data + gz.close + data = inp.read + else + data << ""; + end + end + + return data + end + + + # Set the height of the graph box, this is the total height + # of the SVG box created - not the graph it self which auto + # scales to fix the space. + attr_accessor :height + # Set the width of the graph box, this is the total width + # of the SVG box created - not the graph it self which auto + # scales to fix the space. + attr_accessor :width + # Set the path to an external stylesheet, set to '' if + # you want to revert back to using the defaut internal version. + # + # To create an external stylesheet create a graph using the + # default internal version and copy the stylesheet section to + # an external file and edit from there. + attr_accessor :style_sheet + # (Bool) Show the value of each element of data on the graph + attr_accessor :show_data_values + # The point at which the Y axis starts, defaults to '0', + # if set to nil it will default to the minimum data value. + attr_accessor :min_scale_value + # Whether to show labels on the X axis or not, defaults + # to true, set to false if you want to turn them off. + attr_accessor :show_x_labels + # This puts the X labels at alternative levels so if they + # are long field names they will not overlap so easily. + # Default it false, to turn on set to true. + attr_accessor :stagger_x_labels + # This puts the Y labels at alternative levels so if they + # are long field names they will not overlap so easily. + # Default it false, to turn on set to true. + attr_accessor :stagger_y_labels + # This turns the X axis labels by 90 degrees. + # Default it false, to turn on set to true. + attr_accessor :rotate_x_labels + # This turns the Y axis labels by 90 degrees. + # Default it false, to turn on set to true. + attr_accessor :rotate_y_labels + # How many "steps" to use between displayed X axis labels, + # a step of one means display every label, a step of two results + # in every other label being displayed (label label label), + # a step of three results in every third label being displayed + # (label label label) and so on. + attr_accessor :step_x_labels + # Whether to (when taking "steps" between X axis labels) step from + # the first label (i.e. always include the first label) or step from + # the X axis origin (i.e. start with a gap if step_x_labels is greater + # than one). + attr_accessor :step_include_first_x_label + # Whether to show labels on the Y axis or not, defaults + # to true, set to false if you want to turn them off. + attr_accessor :show_y_labels + # Ensures only whole numbers are used as the scale divisions. + # Default it false, to turn on set to true. This has no effect if + # scale divisions are less than 1. + attr_accessor :scale_integers + # This defines the gap between markers on the Y axis, + # default is a 10th of the max_value, e.g. you will have + # 10 markers on the Y axis. NOTE: do not set this too + # low - you are limited to 999 markers, after that the + # graph won't generate. + attr_accessor :scale_divisions + # Whether to show the title under the X axis labels, + # default is false, set to true to show. + attr_accessor :show_x_title + # What the title under X axis should be, e.g. 'Months'. + attr_accessor :x_title + # Whether to show the title under the Y axis labels, + # default is false, set to true to show. + attr_accessor :show_y_title + # Aligns writing mode for Y axis label. + # Defaults to :bt (Bottom to Top). + # Change to :tb (Top to Bottom) to reverse. + attr_accessor :y_title_text_direction + # What the title under Y axis should be, e.g. 'Sales in thousands'. + attr_accessor :y_title + # Whether to show a title on the graph, defaults + # to false, set to true to show. + attr_accessor :show_graph_title + # What the title on the graph should be. + attr_accessor :graph_title + # Whether to show a subtitle on the graph, defaults + # to false, set to true to show. + attr_accessor :show_graph_subtitle + # What the subtitle on the graph should be. + attr_accessor :graph_subtitle + # Whether to show a key, defaults to false, set to + # true if you want to show it. + attr_accessor :key + # Where the key should be positioned, defaults to + # :right, set to :bottom if you want to move it. + attr_accessor :key_position + # Set the font size (in points) of the data point labels + attr_accessor :font_size + # Set the font size of the X axis labels + attr_accessor :x_label_font_size + # Set the font size of the X axis title + attr_accessor :x_title_font_size + # Set the font size of the Y axis labels + attr_accessor :y_label_font_size + # Set the font size of the Y axis title + attr_accessor :y_title_font_size + # Set the title font size + attr_accessor :title_font_size + # Set the subtitle font size + attr_accessor :subtitle_font_size + # Set the key font size + attr_accessor :key_font_size + # Show guidelines for the X axis + attr_accessor :show_x_guidelines + # Show guidelines for the Y axis + attr_accessor :show_y_guidelines + # Do not use CSS if set to true. Many SVG viewers do not support CSS, but + # not using CSS can result in larger SVGs as well as making it impossible to + # change colors after the chart is generated. Defaults to false. + attr_accessor :no_css + # Add popups for the data points on some graphs + attr_accessor :add_popups + + + protected + + def sort( *arrys ) + sort_multiple( arrys ) + end + + # Overwrite configuration options with supplied options. Used + # by subclasses. + def init_with config + config.each { |key, value| + self.send((key.to_s+"=").to_sym, value ) if respond_to? key.to_sym + } + end + + attr_accessor :top_align, :top_font, :right_align, :right_font + + KEY_BOX_SIZE = 12 + + # Override this (and call super) to change the margin to the left + # of the plot area. Results in @border_left being set. + def calculate_left_margin + @border_left = 7 + # Check for Y labels + max_y_label_height_px = rotate_y_labels ? + y_label_font_size : + get_y_labels.max{|a,b| + a.to_s.length<=>b.to_s.length + }.to_s.length * y_label_font_size * 0.6 + @border_left += max_y_label_height_px if show_y_labels + @border_left += max_y_label_height_px + 10 if stagger_y_labels + @border_left += y_title_font_size + 5 if show_y_title + end + + + # Calculates the width of the widest Y label. This will be the + # character height if the Y labels are rotated + def max_y_label_width_px + return font_size if rotate_y_labels + end + + + # Override this (and call super) to change the margin to the right + # of the plot area. Results in @border_right being set. + def calculate_right_margin + @border_right = 7 + if key and key_position == :right + val = keys.max { |a,b| a.length <=> b.length } + @border_right += val.length * key_font_size * 0.6 + @border_right += KEY_BOX_SIZE + @border_right += 10 # Some padding around the box + end + end + + + # Override this (and call super) to change the margin to the top + # of the plot area. Results in @border_top being set. + def calculate_top_margin + @border_top = 5 + @border_top += title_font_size if show_graph_title + @border_top += 5 + @border_top += subtitle_font_size if show_graph_subtitle + end + + + # Adds pop-up point information to a graph. + def add_popup( x, y, label ) + txt_width = label.length * font_size * 0.6 + 10 + tx = (x+txt_width > width ? x-5 : x+5) + t = @foreground.add_element( "text", { + "x" => tx.to_s, + "y" => (y - font_size).to_s, + "visibility" => "hidden", + }) + t.attributes["style"] = "fill: #000; "+ + (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;") + t.text = label.to_s + t.attributes["id"] = t.object_id.to_s + + @foreground.add_element( "circle", { + "cx" => x.to_s, + "cy" => y.to_s, + "r" => "10", + "style" => "opacity: 0", + "onmouseover" => + "document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )", + "onmouseout" => + "document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )", + }) + + end + + + # Override this (and call super) to change the margin to the bottom + # of the plot area. Results in @border_bottom being set. + def calculate_bottom_margin + @border_bottom = 7 + if key and key_position == :bottom + @border_bottom += @data.size * (font_size + 5) + @border_bottom += 10 + end + if show_x_labels + max_x_label_height_px = (not rotate_x_labels) ? + x_label_font_size : + get_x_labels.max{|a,b| + a.to_s.length<=>b.to_s.length + }.to_s.length * x_label_font_size * 0.6 + @border_bottom += max_x_label_height_px + @border_bottom += max_x_label_height_px + 10 if stagger_x_labels + end + @border_bottom += x_title_font_size + 5 if show_x_title + end + + + # Draws the background, axis, and labels. + def draw_graph + @graph = @root.add_element( "g", { + "transform" => "translate( #@border_left #@border_top )" + }) + + # Background + @graph.add_element( "rect", { + "x" => "0", + "y" => "0", + "width" => @graph_width.to_s, + "height" => @graph_height.to_s, + "class" => "graphBackground" + }) + + # Axis + @graph.add_element( "path", { + "d" => "M 0 0 v#@graph_height", + "class" => "axis", + "id" => "xAxis" + }) + @graph.add_element( "path", { + "d" => "M 0 #@graph_height h#@graph_width", + "class" => "axis", + "id" => "yAxis" + }) + + draw_x_labels + draw_y_labels + end + + + # Where in the X area the label is drawn + # Centered in the field, should be width/2. Start, 0. + def x_label_offset( width ) + 0 + end + + def make_datapoint_text( x, y, value, style="" ) + if show_data_values + @foreground.add_element( "text", { + "x" => x.to_s, + "y" => y.to_s, + "class" => "dataPointLabel", + "style" => "#{style} stroke: #fff; stroke-width: 2;" + }).text = value.to_s + text = @foreground.add_element( "text", { + "x" => x.to_s, + "y" => y.to_s, + "class" => "dataPointLabel" + }) + text.text = value.to_s + text.attributes["style"] = style if style.length > 0 + end + end + + + # Draws the X axis labels + def draw_x_labels + stagger = x_label_font_size + 5 + if show_x_labels + label_width = field_width + + count = 0 + for label in get_x_labels + if step_include_first_x_label == true then + step = count % step_x_labels + else + step = (count + 1) % step_x_labels + end + + if step == 0 then + text = @graph.add_element( "text" ) + text.attributes["class"] = "xAxisLabels" + text.text = label.to_s + + x = count * label_width + x_label_offset( label_width ) + y = @graph_height + x_label_font_size + 3 + t = 0 - (font_size / 2) + + if stagger_x_labels and count % 2 == 1 + y += stagger + @graph.add_element( "path", { + "d" => "M#{x} #@graph_height v#{stagger}", + "class" => "staggerGuideLine" + }) + end + + text.attributes["x"] = x.to_s + text.attributes["y"] = y.to_s + if rotate_x_labels + text.attributes["transform"] = + "rotate( 90 #{x} #{y-x_label_font_size} )"+ + " translate( 0 -#{x_label_font_size/4} )" + text.attributes["style"] = "text-anchor: start" + else + text.attributes["style"] = "text-anchor: middle" + end + end + + draw_x_guidelines( label_width, count ) if show_x_guidelines + count += 1 + end + end + end + + + # Where in the Y area the label is drawn + # Centered in the field, should be width/2. Start, 0. + def y_label_offset( height ) + 0 + end + + + def field_width + (@graph_width.to_f - font_size*2*right_font) / + (get_x_labels.length - right_align) + end + + + def field_height + (@graph_height.to_f - font_size*2*top_font) / + (get_y_labels.length - top_align) + end + + + # Draws the Y axis labels + def draw_y_labels + stagger = y_label_font_size + 5 + if show_y_labels + label_height = field_height + + count = 0 + y_offset = @graph_height + y_label_offset( label_height ) + y_offset += font_size/1.2 unless rotate_y_labels + for label in get_y_labels + y = y_offset - (label_height * count) + x = rotate_y_labels ? 0 : -3 + + if stagger_y_labels and count % 2 == 1 + x -= stagger + @graph.add_element( "path", { + "d" => "M#{x} #{y} h#{stagger}", + "class" => "staggerGuideLine" + }) + end + + text = @graph.add_element( "text", { + "x" => x.to_s, + "y" => y.to_s, + "class" => "yAxisLabels" + }) + text.text = label.to_s + if rotate_y_labels + text.attributes["transform"] = "translate( -#{font_size} 0 ) "+ + "rotate( 90 #{x} #{y} ) " + text.attributes["style"] = "text-anchor: middle" + else + text.attributes["y"] = (y - (y_label_font_size/2)).to_s + text.attributes["style"] = "text-anchor: end" + end + draw_y_guidelines( label_height, count ) if show_y_guidelines + count += 1 + end + end + end + + + # Draws the X axis guidelines + def draw_x_guidelines( label_height, count ) + if count != 0 + @graph.add_element( "path", { + "d" => "M#{label_height*count} 0 v#@graph_height", + "class" => "guideLines" + }) + end + end + + + # Draws the Y axis guidelines + def draw_y_guidelines( label_height, count ) + if count != 0 + @graph.add_element( "path", { + "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width", + "class" => "guideLines" + }) + end + end + + + # Draws the graph title and subtitle + def draw_titles + if show_graph_title + @root.add_element( "text", { + "x" => (width / 2).to_s, + "y" => (title_font_size).to_s, + "class" => "mainTitle" + }).text = graph_title.to_s + end + + if show_graph_subtitle + y_subtitle = show_graph_title ? + title_font_size + 10 : + subtitle_font_size + @root.add_element("text", { + "x" => (width / 2).to_s, + "y" => (y_subtitle).to_s, + "class" => "subTitle" + }).text = graph_subtitle.to_s + end + + if show_x_title + y = @graph_height + @border_top + x_title_font_size + if show_x_labels + y += x_label_font_size + 5 if stagger_x_labels + y += x_label_font_size + 5 + end + x = width / 2 + + @root.add_element("text", { + "x" => x.to_s, + "y" => y.to_s, + "class" => "xAxisTitle", + }).text = x_title.to_s + end + + if show_y_title + x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3) + y = height / 2 + + text = @root.add_element("text", { + "x" => x.to_s, + "y" => y.to_s, + "class" => "yAxisTitle", + }) + text.text = y_title.to_s + if y_title_text_direction == :bt + text.attributes["transform"] = "rotate( -90, #{x}, #{y} )" + else + text.attributes["transform"] = "rotate( 90, #{x}, #{y} )" + end + end + end + + def keys + return @data.collect{ |d| d[:title] } + end + + # Draws the legend on the graph + def draw_legend + if key + group = @root.add_element( "g" ) + + key_count = 0 + for key_name in keys + y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5) + group.add_element( "rect", { + "x" => 0.to_s, + "y" => y_offset.to_s, + "width" => KEY_BOX_SIZE.to_s, + "height" => KEY_BOX_SIZE.to_s, + "class" => "key#{key_count+1}" + }) + group.add_element( "text", { + "x" => (KEY_BOX_SIZE + 5).to_s, + "y" => (y_offset + KEY_BOX_SIZE).to_s, + "class" => "keyText" + }).text = key_name.to_s + key_count += 1 + end + + case key_position + when :right + x_offset = @graph_width + @border_left + 10 + y_offset = @border_top + 20 + when :bottom + x_offset = @border_left + 20 + y_offset = @border_top + @graph_height + 5 + if show_x_labels + max_x_label_height_px = (not rotate_x_labels) ? + x_label_font_size : + get_x_labels.max{|a,b| + a.to_s.length<=>b.to_s.length + }.to_s.length * x_label_font_size * 0.6 + x_label_font_size + y_offset += max_x_label_height_px + y_offset += max_x_label_height_px + 5 if stagger_x_labels + end + y_offset += x_title_font_size + 5 if show_x_title + end + group.attributes["transform"] = "translate(#{x_offset} #{y_offset})" + end + end + + + private + + def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 ) + if lo < hi + p = partition(arrys,lo,hi) + sort_multiple(arrys, lo, p-1) + sort_multiple(arrys, p+1, hi) + end + arrys + end + + def partition( arrys, lo, hi ) + p = arrys[0][lo] + l = lo + z = lo+1 + while z <= hi + if arrys[0][z] < p + l += 1 + arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] } + end + z += 1 + end + arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] } + l + end + + def style + if no_css + styles = parse_css + @root.elements.each("//*[@class]") { |el| + cl = el.attributes["class"] + style = styles[cl] + style += el.attributes["style"] if el.attributes["style"] + el.attributes["style"] = style + } + end + end + + def parse_css + css = get_style + rv = {} + while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m + names_orig = names = $1 + css = $' + css =~ /([^}]+)\}/m + content = $1 + css = $' + + nms = [] + while names =~ /^\s*,?\s*\.(\w+)/ + nms << $1 + names = $' + end + + content = content.tr( "\n\t", " ") + for name in nms + current = rv[name] + current = current ? current+"; "+content : content + rv[name] = current.strip.squeeze(" ") + end + end + return rv + end + + + # Override and place code to add defs here + def add_defs defs + end + + + def start_svg + # Base document + @doc = Document.new + @doc << XMLDecl.new + @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } + + %q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} ) + if style_sheet && style_sheet != '' + @doc << Instruction.new( "xml-stylesheet", + %Q{href="#{style_sheet}" type="text/css"} ) + end + @root = @doc.add_element( "svg", { + "width" => width.to_s, + "height" => height.to_s, + "viewBox" => "0 0 #{width} #{height}", + "xmlns" => "http://www.w3.org/2000/svg", + "xmlns:xlink" => "http://www.w3.org/1999/xlink", + "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/", + "a3:scriptImplementation" => "Adobe" + }) + @root << Comment.new( " "+"\\"*66 ) + @root << Comment.new( " Created with SVG::Graph " ) + @root << Comment.new( " SVG::Graph by Sean E. Russell " ) + @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+ + " Leo Lapworth & Stephan Morgan " ) + @root << Comment.new( " "+"/"*66 ) + + defs = @root.add_element( "defs" ) + add_defs defs + if not(style_sheet && style_sheet != '') and !no_css + @root << Comment.new(" include default stylesheet if none specified ") + style = defs.add_element( "style", {"type"=>"text/css"} ) + style << CData.new( get_style ) + end + + @root << Comment.new( "SVG Background" ) + @root.add_element( "rect", { + "width" => width.to_s, + "height" => height.to_s, + "x" => "0", + "y" => "0", + "class" => "svgBackground" + }) + end + + + def calculate_graph_dimensions + calculate_left_margin + calculate_right_margin + calculate_bottom_margin + calculate_top_margin + @graph_width = width - @border_left - @border_right + @graph_height = height - @border_top - @border_bottom + end + + def get_style + return < 'role_id = 1 AND tracker_id = 2') + assert_tag :tag => 'a', :content => count.to_s, + :attributes => { :href => '/workflows/edit?role_id=1&tracker_id=2' } + end + + def test_get_edit + get :edit + assert_response :success + assert_template 'edit' + assert_not_nil assigns(:roles) + assert_not_nil assigns(:trackers) + end + + def test_get_edit_with_role_and_tracker + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3) + WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5) + + get :edit, :role_id => 2, :tracker_id => 1 + assert_response :success + assert_template 'edit' + + # used status only + assert_not_nil assigns(:statuses) + assert_equal [2, 3, 5], assigns(:statuses).collect(&:id) + + # allowed transitions + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'issue_status[3][5][]', + :value => 'always', + :checked => 'checked' } + # not allowed + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'issue_status[3][2][]', + :value => 'always', + :checked => nil } + # unused + assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'issue_status[1][1][]' } + end + + def test_get_edit_with_role_and_tracker_and_all_statuses + WorkflowTransition.delete_all + + get :edit, :role_id => 2, :tracker_id => 1, :used_statuses_only => '0' + assert_response :success + assert_template 'edit' + + assert_not_nil assigns(:statuses) + assert_equal IssueStatus.count, assigns(:statuses).size + + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'issue_status[1][1][]', + :value => 'always', + :checked => nil } + end + + def test_post_edit + post :edit, :role_id => 2, :tracker_id => 1, + :issue_status => { + '4' => {'5' => ['always']}, + '3' => {'1' => ['always'], '2' => ['always']} + } + assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1' + + assert_equal 3, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) + assert_not_nil WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2}) + assert_nil WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4}) + end + + def test_post_edit_with_additional_transitions + post :edit, :role_id => 2, :tracker_id => 1, + :issue_status => { + '4' => {'5' => ['always']}, + '3' => {'1' => ['author'], '2' => ['assignee'], '4' => ['author', 'assignee']} + } + assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1' + + assert_equal 4, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) + + w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5}) + assert ! w.author + assert ! w.assignee + w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1}) + assert w.author + assert ! w.assignee + w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2}) + assert ! w.author + assert w.assignee + w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4}) + assert w.author + assert w.assignee + end + + def test_clear_workflow + assert WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0 + + post :edit, :role_id => 2, :tracker_id => 1 + assert_equal 0, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) + end + + def test_get_permissions + get :permissions + + assert_response :success + assert_template 'permissions' + assert_not_nil assigns(:roles) + assert_not_nil assigns(:trackers) + end + + def test_get_permissions_with_role_and_tracker + WorkflowPermission.delete_all + WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required') + WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required') + WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly') + + get :permissions, :role_id => 1, :tracker_id => 2 + assert_response :success + assert_template 'permissions' + + assert_select 'input[name=role_id][value=1]' + assert_select 'input[name=tracker_id][value=2]' + + # Required field + assert_select 'select[name=?]', 'permissions[assigned_to_id][2]' do + assert_select 'option[value=]' + assert_select 'option[value=][selected=selected]', 0 + assert_select 'option[value=readonly]', :text => 'Read-only' + assert_select 'option[value=readonly][selected=selected]', 0 + assert_select 'option[value=required]', :text => 'Required' + assert_select 'option[value=required][selected=selected]' + end + + # Read-only field + assert_select 'select[name=?]', 'permissions[fixed_version_id][3]' do + assert_select 'option[value=]' + assert_select 'option[value=][selected=selected]', 0 + assert_select 'option[value=readonly]', :text => 'Read-only' + assert_select 'option[value=readonly][selected=selected]' + assert_select 'option[value=required]', :text => 'Required' + assert_select 'option[value=required][selected=selected]', 0 + end + + # Other field + assert_select 'select[name=?]', 'permissions[due_date][3]' do + assert_select 'option[value=]' + assert_select 'option[value=][selected=selected]', 0 + assert_select 'option[value=readonly]', :text => 'Read-only' + assert_select 'option[value=readonly][selected=selected]', 0 + assert_select 'option[value=required]', :text => 'Required' + assert_select 'option[value=required][selected=selected]', 0 + end + end + + def test_get_permissions_with_required_custom_field_should_not_show_required_option + cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :tracker_ids => [1], :is_required => true) + + get :permissions, :role_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'permissions' + + # Custom field that is always required + # The default option is "(Required)" + assert_select 'select[name=?]', "permissions[#{cf.id}][3]" do + assert_select 'option[value=]' + assert_select 'option[value=readonly]', :text => 'Read-only' + assert_select 'option[value=required]', 0 + end + end + + def test_get_permissions_with_role_and_tracker_and_all_statuses + WorkflowTransition.delete_all + + get :permissions, :role_id => 1, :tracker_id => 2, :used_statuses_only => '0' + assert_response :success + assert_equal IssueStatus.sorted.all, assigns(:statuses) + end + + def test_post_permissions + WorkflowPermission.delete_all + + post :permissions, :role_id => 1, :tracker_id => 2, :permissions => { + 'assigned_to_id' => {'1' => '', '2' => 'readonly', '3' => ''}, + 'fixed_version_id' => {'1' => 'required', '2' => 'readonly', '3' => ''}, + 'due_date' => {'1' => '', '2' => '', '3' => ''}, + } + assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2' + + workflows = WorkflowPermission.all + assert_equal 3, workflows.size + workflows.each do |workflow| + assert_equal 1, workflow.role_id + assert_equal 2, workflow.tracker_id + end + assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'assigned_to_id' && wf.rule == 'readonly'} + assert workflows.detect {|wf| wf.old_status_id == 1 && wf.field_name == 'fixed_version_id' && wf.rule == 'required'} + assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'fixed_version_id' && wf.rule == 'readonly'} + end + + def test_post_permissions_should_clear_permissions + WorkflowPermission.delete_all + WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required') + WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required') + wf1 = WorkflowPermission.create!(:role_id => 1, :tracker_id => 3, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required') + wf2 = WorkflowPermission.create!(:role_id => 2, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly') + + post :permissions, :role_id => 1, :tracker_id => 2 + assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2' + + workflows = WorkflowPermission.all + assert_equal 2, workflows.size + assert wf1.reload + assert wf2.reload + end + + def test_get_copy + get :copy + assert_response :success + assert_template 'copy' + assert_select 'select[name=source_tracker_id]' do + assert_select 'option[value=1]', :text => 'Bug' + end + assert_select 'select[name=source_role_id]' do + assert_select 'option[value=2]', :text => 'Developer' + end + assert_select 'select[name=?]', 'target_tracker_ids[]' do + assert_select 'option[value=3]', :text => 'Support request' + end + assert_select 'select[name=?]', 'target_role_ids[]' do + assert_select 'option[value=1]', :text => 'Manager' + end + end + + def test_post_copy_one_to_one + source_transitions = status_transitions(:tracker_id => 1, :role_id => 2) + + post :copy, :source_tracker_id => '1', :source_role_id => '2', + :target_tracker_ids => ['3'], :target_role_ids => ['1'] + assert_response 302 + assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1) + end + + def test_post_copy_one_to_many + source_transitions = status_transitions(:tracker_id => 1, :role_id => 2) + + post :copy, :source_tracker_id => '1', :source_role_id => '2', + :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3'] + assert_response 302 + assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 1) + assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1) + assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 3) + assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 3) + end + + def test_post_copy_many_to_many + source_t2 = status_transitions(:tracker_id => 2, :role_id => 2) + source_t3 = status_transitions(:tracker_id => 3, :role_id => 2) + + post :copy, :source_tracker_id => 'any', :source_role_id => '2', + :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3'] + assert_response 302 + assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 1) + assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 1) + assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 3) + assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 3) + end + + # Returns an array of status transitions that can be compared + def status_transitions(conditions) + WorkflowTransition.find(:all, :conditions => conditions, + :order => 'tracker_id, role_id, old_status_id, new_status_id').collect {|w| [w.old_status, w.new_status_id]} + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/08/08cbe25d569ad9b66dc5e2416984f568601a717d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/08cbe25d569ad9b66dc5e2416984f568601a717d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,4 @@ +<%= labelled_form_for @group do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/08/08df2c90c0fda275003e285b2727284ba737baa3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/08df2c90c0fda275003e285b2727284ba737baa3.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,8 @@ +Redmine::Plugin.register :<%= plugin_name %> do + name '<%= plugin_pretty_name %> plugin' + author 'Author name' + description 'This is a plugin for Redmine' + version '0.0.1' + url 'http://example.com/path/to/plugin' + author_url 'http://example.com/about' +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/08/08f914636b6cd922b89f9ce8cb2268cea53be558.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/08f914636b6cd922b89f9ce8cb2268cea53be558.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,182 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# Class used to represent the relations of an issue +class IssueRelations < Array + include Redmine::I18n + + def initialize(issue, *args) + @issue = issue + super(*args) + end + + def to_s(*args) + map {|relation| "#{l(relation.label_for(@issue))} ##{relation.other_issue(@issue).id}"}.join(', ') + end +end + +class IssueRelation < ActiveRecord::Base + belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id' + belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id' + + TYPE_RELATES = "relates" + TYPE_DUPLICATES = "duplicates" + TYPE_DUPLICATED = "duplicated" + TYPE_BLOCKS = "blocks" + TYPE_BLOCKED = "blocked" + TYPE_PRECEDES = "precedes" + TYPE_FOLLOWS = "follows" + TYPE_COPIED_TO = "copied_to" + TYPE_COPIED_FROM = "copied_from" + + TYPES = { + TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, + :order => 1, :sym => TYPE_RELATES }, + TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, + :order => 2, :sym => TYPE_DUPLICATED }, + TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates, + :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES }, + TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, + :order => 4, :sym => TYPE_BLOCKED }, + TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks, + :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS }, + TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, + :order => 6, :sym => TYPE_FOLLOWS }, + TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, + :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES }, + TYPE_COPIED_TO => { :name => :label_copied_to, :sym_name => :label_copied_from, + :order => 8, :sym => TYPE_COPIED_FROM }, + TYPE_COPIED_FROM => { :name => :label_copied_from, :sym_name => :label_copied_to, + :order => 9, :sym => TYPE_COPIED_TO, :reverse => TYPE_COPIED_TO } + }.freeze + + validates_presence_of :issue_from, :issue_to, :relation_type + validates_inclusion_of :relation_type, :in => TYPES.keys + validates_numericality_of :delay, :allow_nil => true + validates_uniqueness_of :issue_to_id, :scope => :issue_from_id + validate :validate_issue_relation + + attr_protected :issue_from_id, :issue_to_id + before_save :handle_issue_order + + def visible?(user=User.current) + (issue_from.nil? || issue_from.visible?(user)) && (issue_to.nil? || issue_to.visible?(user)) + end + + def deletable?(user=User.current) + visible?(user) && + ((issue_from.nil? || user.allowed_to?(:manage_issue_relations, issue_from.project)) || + (issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project))) + end + + def initialize(attributes=nil, *args) + super + if new_record? + if relation_type.blank? + self.relation_type = IssueRelation::TYPE_RELATES + end + end + end + + def validate_issue_relation + if issue_from && issue_to + errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id + unless issue_from.project_id == issue_to.project_id || + Setting.cross_project_issue_relations? + errors.add :issue_to_id, :not_same_project + end + # detect circular dependencies depending wether the relation should be reversed + if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse] + errors.add :base, :circular_dependency if issue_from.all_dependent_issues.include? issue_to + else + errors.add :base, :circular_dependency if issue_to.all_dependent_issues.include? issue_from + end + if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to) + errors.add :base, :cant_link_an_issue_with_a_descendant + end + end + end + + def other_issue(issue) + (self.issue_from_id == issue.id) ? issue_to : issue_from + end + + # Returns the relation type for +issue+ + def relation_type_for(issue) + if TYPES[relation_type] + if self.issue_from_id == issue.id + relation_type + else + TYPES[relation_type][:sym] + end + end + end + + def label_for(issue) + TYPES[relation_type] ? + TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : + :unknow + end + + def css_classes_for(issue) + "rel-#{relation_type_for(issue)}" + end + + def handle_issue_order + reverse_if_needed + + if TYPE_PRECEDES == relation_type + self.delay ||= 0 + else + self.delay = nil + end + set_issue_to_dates + end + + def set_issue_to_dates + soonest_start = self.successor_soonest_start + if soonest_start && issue_to + issue_to.reschedule_on!(soonest_start) + end + end + + def successor_soonest_start + if (TYPE_PRECEDES == self.relation_type) && delay && issue_from && + (issue_from.start_date || issue_from.due_date) + (issue_from.due_date || issue_from.start_date) + 1 + delay + end + end + + def <=>(relation) + r = TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order] + r == 0 ? id <=> relation.id : r + end + + private + + # Reverses the relation if needed so that it gets stored in the proper way + # Should not be reversed before validation so that it can be displayed back + # as entered on new relation form + def reverse_if_needed + if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse] + issue_tmp = issue_to + self.issue_to = issue_from + self.issue_from = issue_tmp + self.relation_type = TYPES[relation_type][:reverse] + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/08/08fb7d1dbcb8591c64d100b93daff66b95400b98.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/08/08fb7d1dbcb8591c64d100b93daff66b95400b98.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,185 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module DefaultData + class DataAlreadyLoaded < Exception; end + + module Loader + include Redmine::I18n + + class << self + # Returns true if no data is already loaded in the database + # otherwise false + def no_data? + !Role.find(:first, :conditions => {:builtin => 0}) && + !Tracker.find(:first) && + !IssueStatus.find(:first) && + !Enumeration.find(:first) + end + + # Loads the default data + # Raises a RecordNotSaved exception if something goes wrong + def load(lang=nil) + raise DataAlreadyLoaded.new("Some configuration data is already loaded.") unless no_data? + set_language_if_valid(lang) + + Role.transaction do + # Roles + manager = Role.create! :name => l(:default_role_manager), + :issues_visibility => 'all', + :position => 1 + manager.permissions = manager.setable_permissions.collect {|p| p.name} + manager.save! + + developer = Role.create! :name => l(:default_role_developer), + :position => 2, + :permissions => [:manage_versions, + :manage_categories, + :view_issues, + :add_issues, + :edit_issues, + :view_private_notes, + :set_notes_private, + :manage_issue_relations, + :manage_subtasks, + :add_issue_notes, + :save_queries, + :view_gantt, + :view_calendar, + :log_time, + :view_time_entries, + :comment_news, + :view_documents, + :view_wiki_pages, + :view_wiki_edits, + :edit_wiki_pages, + :delete_wiki_pages, + :add_messages, + :edit_own_messages, + :view_files, + :manage_files, + :browse_repository, + :view_changesets, + :commit_access, + :manage_related_issues] + + reporter = Role.create! :name => l(:default_role_reporter), + :position => 3, + :permissions => [:view_issues, + :add_issues, + :add_issue_notes, + :save_queries, + :view_gantt, + :view_calendar, + :log_time, + :view_time_entries, + :comment_news, + :view_documents, + :view_wiki_pages, + :view_wiki_edits, + :add_messages, + :edit_own_messages, + :view_files, + :browse_repository, + :view_changesets] + + Role.non_member.update_attribute :permissions, [:view_issues, + :add_issues, + :add_issue_notes, + :save_queries, + :view_gantt, + :view_calendar, + :view_time_entries, + :comment_news, + :view_documents, + :view_wiki_pages, + :view_wiki_edits, + :add_messages, + :view_files, + :browse_repository, + :view_changesets] + + Role.anonymous.update_attribute :permissions, [:view_issues, + :view_gantt, + :view_calendar, + :view_time_entries, + :view_documents, + :view_wiki_pages, + :view_wiki_edits, + :view_files, + :browse_repository, + :view_changesets] + + # Trackers + Tracker.create!(:name => l(:default_tracker_bug), :is_in_chlog => true, :is_in_roadmap => false, :position => 1) + Tracker.create!(:name => l(:default_tracker_feature), :is_in_chlog => true, :is_in_roadmap => true, :position => 2) + Tracker.create!(:name => l(:default_tracker_support), :is_in_chlog => false, :is_in_roadmap => false, :position => 3) + + # Issue statuses + new = IssueStatus.create!(:name => l(:default_issue_status_new), :is_closed => false, :is_default => true, :position => 1) + in_progress = IssueStatus.create!(:name => l(:default_issue_status_in_progress), :is_closed => false, :is_default => false, :position => 2) + resolved = IssueStatus.create!(:name => l(:default_issue_status_resolved), :is_closed => false, :is_default => false, :position => 3) + feedback = IssueStatus.create!(:name => l(:default_issue_status_feedback), :is_closed => false, :is_default => false, :position => 4) + closed = IssueStatus.create!(:name => l(:default_issue_status_closed), :is_closed => true, :is_default => false, :position => 5) + rejected = IssueStatus.create!(:name => l(:default_issue_status_rejected), :is_closed => true, :is_default => false, :position => 6) + + # Workflow + Tracker.find(:all).each { |t| + IssueStatus.find(:all).each { |os| + IssueStatus.find(:all).each { |ns| + WorkflowTransition.create!(:tracker_id => t.id, :role_id => manager.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns + } + } + } + + Tracker.find(:all).each { |t| + [new, in_progress, resolved, feedback].each { |os| + [in_progress, resolved, feedback, closed].each { |ns| + WorkflowTransition.create!(:tracker_id => t.id, :role_id => developer.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns + } + } + } + + Tracker.find(:all).each { |t| + [new, in_progress, resolved, feedback].each { |os| + [closed].each { |ns| + WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns + } + } + WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => resolved.id, :new_status_id => feedback.id) + } + + # Enumerations + IssuePriority.create!(:name => l(:default_priority_low), :position => 1) + IssuePriority.create!(:name => l(:default_priority_normal), :position => 2, :is_default => true) + IssuePriority.create!(:name => l(:default_priority_high), :position => 3) + IssuePriority.create!(:name => l(:default_priority_urgent), :position => 4) + IssuePriority.create!(:name => l(:default_priority_immediate), :position => 5) + + DocumentCategory.create!(:name => l(:default_doc_category_user), :position => 1) + DocumentCategory.create!(:name => l(:default_doc_category_tech), :position => 2) + + TimeEntryActivity.create!(:name => l(:default_activity_design), :position => 1) + TimeEntryActivity.create!(:name => l(:default_activity_development), :position => 2) + end + true + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/09/0930e7dcd668d1bc526e085d51f359ac0b2bfdf3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/0930e7dcd668d1bc526e085d51f359ac0b2bfdf3.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,35 @@ +api.user do + api.id @user.id + api.login @user.login if User.current.admin? + api.firstname @user.firstname + api.lastname @user.lastname + api.mail @user.mail if User.current.admin? || !@user.pref.hide_mail + api.created_on @user.created_on + api.last_login_on @user.last_login_on + + render_api_custom_values @user.visible_custom_field_values, api + + api.array :groups do |groups| + @user.groups.each do |group| + api.group :id => group.id, :name => group.name + end + end if User.current.admin? && include_in_api_response?('groups') + + api.array :memberships do + @memberships.each do |membership| + api.membership do + api.id membership.id + api.project :id => membership.project.id, :name => membership.project.name + api.array :roles do + membership.member_roles.each do |member_role| + if member_role.role + attrs = {:id => member_role.role.id, :name => member_role.role.name} + attrs.merge!(:inherited => true) if member_role.inherited_from.present? + api.role attrs + end + end + end + end if membership.project + end + end if include_in_api_response?('memberships') && @memberships +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/09/09319bb04693b301a469674c2043f8c089fbdcbb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/09319bb04693b301a469674c2043f8c089fbdcbb.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,4349 @@ +#============================================================+ +# File name : tcpdf.rb +# Begin : 2002-08-03 +# Last Update : 2007-03-20 +# Author : Nicola Asuni +# Version : 1.53.0.TC031 +# License : GNU LGPL (http://www.gnu.org/copyleft/lesser.html) +# +# Description : This is a Ruby class for generating PDF files +# on-the-fly without requiring external +# extensions. +# +# IMPORTANT: +# This class is an extension and improvement of the Public Domain +# FPDF class by Olivier Plathey (http://www.fpdf.org). +# +# Main changes by Nicola Asuni: +# Ruby porting; +# UTF-8 Unicode support; +# code refactoring; +# source code clean up; +# code style and formatting; +# source code documentation using phpDocumentor (www.phpdoc.org); +# All ISO page formats were included; +# image scale factor; +# includes methods to parse and printsome XHTML code, supporting the following elements: h1, h2, h3, h4, h5, h6, b, u, i, a, img, p, br, strong, em, font, blockquote, li, ul, ol, hr, td, th, tr, table, sup, sub, small; +# includes a method to print various barcode formats using an improved version of "Generic Barcode Render Class" by Karim Mribti (http://www.mribti.com/barcode/) (require GD library: http://www.boutell.com/gd/); +# defines standard Header() and Footer() methods. +# +# Ported to Ruby by Ed Moss 2007-08-06 +# +#============================================================+ + +require 'tempfile' +require 'core/rmagick' + +# +# TCPDF Class. +# @package com.tecnick.tcpdf +# + +@@version = "1.53.0.TC031" +@@fpdf_charwidths = {} + +PDF_PRODUCER = 'TCPDF via RFPDF 1.53.0.TC031 (http://tcpdf.sourceforge.net)' + +module TCPDFFontDescriptor + @@descriptors = { 'freesans' => {} } + @@font_name = 'freesans' + + def self.font(font_name) + @@descriptors[font_name.gsub(".rb", "")] + end + + def self.define(font_name = 'freesans') + @@descriptors[font_name] ||= {} + yield @@descriptors[font_name] + end +end + +# This is a Ruby class for generating PDF files on-the-fly without requiring external extensions.
    +# This class is an extension and improvement of the FPDF class by Olivier Plathey (http://www.fpdf.org).
    +# This version contains some changes: [porting to Ruby, support for UTF-8 Unicode, code style and formatting, php documentation (www.phpdoc.org), ISO page formats, minor improvements, image scale factor]
    +# TCPDF project (http://tcpdf.sourceforge.net) is based on the Public Domain FPDF class by Olivier Plathey (http://www.fpdf.org).
    +# To add your own TTF fonts please read /fonts/README.TXT +# @name TCPDF +# @package com.tecnick.tcpdf +# @@version 1.53.0.TC031 +# @author Nicola Asuni +# @link http://tcpdf.sourceforge.net +# @license http://www.gnu.org/copyleft/lesser.html LGPL +# +class TCPDF + include RFPDF + include Core::RFPDF + include RFPDF::Math + + def logger + Rails.logger + end + + cattr_accessor :k_cell_height_ratio + @@k_cell_height_ratio = 1.25 + + cattr_accessor :k_blank_image + @@k_blank_image = "" + + cattr_accessor :k_small_ratio + @@k_small_ratio = 2/3.0 + + cattr_accessor :k_path_cache + @@k_path_cache = Rails.root.join('tmp') + + cattr_accessor :k_path_url_cache + @@k_path_url_cache = Rails.root.join('tmp') + + attr_accessor :barcode + + attr_accessor :buffer + + attr_accessor :diffs + + attr_accessor :color_flag + + attr_accessor :default_table_columns + + attr_accessor :max_table_columns + + attr_accessor :default_font + + attr_accessor :draw_color + + attr_accessor :encoding + + attr_accessor :fill_color + + attr_accessor :fonts + + attr_accessor :font_family + + attr_accessor :font_files + + cattr_accessor :font_path + + attr_accessor :font_style + + attr_accessor :font_size_pt + + attr_accessor :header_width + + attr_accessor :header_logo + + attr_accessor :header_logo_width + + attr_accessor :header_title + + attr_accessor :header_string + + attr_accessor :images + + attr_accessor :img_scale + + attr_accessor :in_footer + + attr_accessor :is_unicode + + attr_accessor :lasth + + attr_accessor :links + + attr_accessor :list_ordered + + attr_accessor :list_count + + attr_accessor :li_spacer + + attr_accessor :n + + attr_accessor :offsets + + attr_accessor :orientation_changes + + attr_accessor :page + + attr_accessor :page_links + + attr_accessor :pages + + attr_accessor :pdf_version + + attr_accessor :prevfill_color + + attr_accessor :prevtext_color + + attr_accessor :print_header + + attr_accessor :print_footer + + attr_accessor :state + + attr_accessor :tableborder + + attr_accessor :tdbegin + + attr_accessor :tdwidth + + attr_accessor :tdheight + + attr_accessor :tdalign + + attr_accessor :tdfill + + attr_accessor :tempfontsize + + attr_accessor :text_color + + attr_accessor :underline + + attr_accessor :ws + + # + # This is the class constructor. + # It allows to set up the page format, the orientation and + # the measure unit used in all the methods (except for the font sizes). + # @since 1.0 + # @param string :orientation page orientation. Possible values are (case insensitive):
    • P or Portrait (default)
    • L or Landscape
    + # @param string :unit User measure unit. Possible values are:
    • pt: point
    • mm: millimeter (default)
    • cm: centimeter
    • in: inch

    A point equals 1/72 of inch, that is to say about 0.35 mm (an inch being 2.54 cm). This is a very common unit in typography; font sizes are expressed in that unit. + # @param mixed :format The format used for pages. It can be either one of the following values (case insensitive) or a custom format in the form of a two-element array containing the width and the height (expressed in the unit given by unit).
    • 4A0
    • 2A0
    • A0
    • A1
    • A2
    • A3
    • A4 (default)
    • A5
    • A6
    • A7
    • A8
    • A9
    • A10
    • B0
    • B1
    • B2
    • B3
    • B4
    • B5
    • B6
    • B7
    • B8
    • B9
    • B10
    • C0
    • C1
    • C2
    • C3
    • C4
    • C5
    • C6
    • C7
    • C8
    • C9
    • C10
    • RA0
    • RA1
    • RA2
    • RA3
    • RA4
    • SRA0
    • SRA1
    • SRA2
    • SRA3
    • SRA4
    • LETTER
    • LEGAL
    • EXECUTIVE
    • FOLIO
    + # @param boolean :unicode TRUE means that the input text is unicode (default = true) + # @param String :encoding charset encoding; default is UTF-8 + # + def initialize(orientation = 'P', unit = 'mm', format = 'A4', unicode = true, encoding = "UTF-8") + + # Set internal character encoding to ASCII# + #FIXME 2007-05-25 (EJM) Level=0 - + # if (respond_to?("mb_internal_encoding") and mb_internal_encoding()) + # @internal_encoding = mb_internal_encoding(); + # mb_internal_encoding("ASCII"); + # } + + #Some checks + dochecks(); + + #Initialization of properties + @barcode ||= false + @buffer ||= '' + @diffs ||= [] + @color_flag ||= false + @default_table_columns ||= 4 + @table_columns ||= 0 + @max_table_columns ||= [] + @tr_id ||= 0 + @max_td_page ||= [] + @max_td_y ||= [] + @t_columns ||= 0 + @default_font ||= "FreeSans" if unicode + @default_font ||= "Helvetica" + @draw_color ||= '0 G' + @encoding ||= "UTF-8" + @fill_color ||= '0 g' + @fonts ||= {} + @font_family ||= '' + @font_files ||= {} + @font_style ||= '' + @font_size ||= 12 + @font_size_pt ||= 12 + @header_width ||= 0 + @header_logo ||= "" + @header_logo_width ||= 30 + @header_title ||= "" + @header_string ||= "" + @images ||= {} + @img_scale ||= 1 + @in_footer ||= false + @is_unicode = unicode + @lasth ||= 0 + @links ||= [] + @list_ordered ||= [] + @list_count ||= [] + @li_spacer ||= "" + @li_count ||= 0 + @spacer ||= "" + @quote_count ||= 0 + @prevquote_count ||= 0 + @quote_top ||= [] + @quote_page ||= [] + @n ||= 2 + @offsets ||= [] + @orientation_changes ||= [] + @page ||= 0 + @page_links ||= {} + @pages ||= [] + @pdf_version ||= "1.3" + @prevfill_color ||= [255,255,255] + @prevtext_color ||= [0,0,0] + @print_header ||= false + @print_footer ||= false + @state ||= 0 + @tableborder ||= 0 + @tdbegin ||= false + @tdtext ||= '' + @tdwidth ||= 0 + @tdheight ||= 0 + @tdalign ||= "L" + @tdfill ||= 0 + @tempfontsize ||= 10 + @text_color ||= '0 g' + @underline ||= false + @deleted ||= false + @ws ||= 0 + + #Standard Unicode fonts + @core_fonts = { + 'courier'=>'Courier', + 'courierB'=>'Courier-Bold', + 'courierI'=>'Courier-Oblique', + 'courierBI'=>'Courier-BoldOblique', + 'helvetica'=>'Helvetica', + 'helveticaB'=>'Helvetica-Bold', + 'helveticaI'=>'Helvetica-Oblique', + 'helveticaBI'=>'Helvetica-BoldOblique', + 'times'=>'Times-Roman', + 'timesB'=>'Times-Bold', + 'timesI'=>'Times-Italic', + 'timesBI'=>'Times-BoldItalic', + 'symbol'=>'Symbol', + 'zapfdingbats'=>'ZapfDingbats'} + + #Scale factor + case unit.downcase + when 'pt' ; @k=1 + when 'mm' ; @k=72/25.4 + when 'cm' ; @k=72/2.54 + when 'in' ; @k=72 + else Error("Incorrect unit: #{unit}") + end + + #Page format + if format.is_a?(String) + # Page formats (45 standard ISO paper formats and 4 american common formats). + # Paper cordinates are calculated in this way: (inches# 72) where (1 inch = 2.54 cm) + case (format.upcase) + when '4A0' ; format = [4767.87,6740.79] + when '2A0' ; format = [3370.39,4767.87] + when 'A0' ; format = [2383.94,3370.39] + when 'A1' ; format = [1683.78,2383.94] + when 'A2' ; format = [1190.55,1683.78] + when 'A3' ; format = [841.89,1190.55] + when 'A4' ; format = [595.28,841.89] # ; default + when 'A5' ; format = [419.53,595.28] + when 'A6' ; format = [297.64,419.53] + when 'A7' ; format = [209.76,297.64] + when 'A8' ; format = [147.40,209.76] + when 'A9' ; format = [104.88,147.40] + when 'A10' ; format = [73.70,104.88] + when 'B0' ; format = [2834.65,4008.19] + when 'B1' ; format = [2004.09,2834.65] + when 'B2' ; format = [1417.32,2004.09] + when 'B3' ; format = [1000.63,1417.32] + when 'B4' ; format = [708.66,1000.63] + when 'B5' ; format = [498.90,708.66] + when 'B6' ; format = [354.33,498.90] + when 'B7' ; format = [249.45,354.33] + when 'B8' ; format = [175.75,249.45] + when 'B9' ; format = [124.72,175.75] + when 'B10' ; format = [87.87,124.72] + when 'C0' ; format = [2599.37,3676.54] + when 'C1' ; format = [1836.85,2599.37] + when 'C2' ; format = [1298.27,1836.85] + when 'C3' ; format = [918.43,1298.27] + when 'C4' ; format = [649.13,918.43] + when 'C5' ; format = [459.21,649.13] + when 'C6' ; format = [323.15,459.21] + when 'C7' ; format = [229.61,323.15] + when 'C8' ; format = [161.57,229.61] + when 'C9' ; format = [113.39,161.57] + when 'C10' ; format = [79.37,113.39] + when 'RA0' ; format = [2437.80,3458.27] + when 'RA1' ; format = [1729.13,2437.80] + when 'RA2' ; format = [1218.90,1729.13] + when 'RA3' ; format = [864.57,1218.90] + when 'RA4' ; format = [609.45,864.57] + when 'SRA0' ; format = [2551.18,3628.35] + when 'SRA1' ; format = [1814.17,2551.18] + when 'SRA2' ; format = [1275.59,1814.17] + when 'SRA3' ; format = [907.09,1275.59] + when 'SRA4' ; format = [637.80,907.09] + when 'LETTER' ; format = [612.00,792.00] + when 'LEGAL' ; format = [612.00,1008.00] + when 'EXECUTIVE' ; format = [521.86,756.00] + when 'FOLIO' ; format = [612.00,936.00] + #else then Error("Unknown page format: #{format}" + end + @fw_pt = format[0] + @fh_pt = format[1] + else + @fw_pt = format[0]*@k + @fh_pt = format[1]*@k + end + + @fw = @fw_pt/@k + @fh = @fh_pt/@k + + #Page orientation + orientation = orientation.downcase + if orientation == 'p' or orientation == 'portrait' + @def_orientation = 'P' + @w_pt = @fw_pt + @h_pt = @fh_pt + elsif orientation == 'l' or orientation == 'landscape' + @def_orientation = 'L' + @w_pt = @fh_pt + @h_pt = @fw_pt + else + Error("Incorrect orientation: #{orientation}") + end + + @fw = @w_pt/@k + @fh = @h_pt/@k + + @cur_orientation = @def_orientation + @w = @w_pt/@k + @h = @h_pt/@k + #Page margins (1 cm) + margin = 28.35/@k + SetMargins(margin, margin) + #Interior cell margin (1 mm) + @c_margin = margin / 10 + #Line width (0.2 mm) + @line_width = 0.567 / @k + #Automatic page break + SetAutoPageBreak(true, 2 * margin) + #Full width display mode + SetDisplayMode('fullwidth') + #Compression + SetCompression(true) + #Set default PDF version number + @pdf_version = "1.3" + + @encoding = encoding + @b = 0 + @i = 0 + @u = 0 + @href = '' + @fontlist = ["arial", "times", "courier", "helvetica", "symbol"] + @issetfont = false + @issetcolor = false + + SetFillColor(200, 200, 200, true) + SetTextColor(0, 0, 0, true) + end + + # + # Set the image scale. + # @param float :scale image scale. + # @author Nicola Asuni + # @since 1.5.2 + # + def SetImageScale(scale) + @img_scale = scale; + end + alias_method :set_image_scale, :SetImageScale + + # + # Returns the image scale. + # @return float image scale. + # @author Nicola Asuni + # @since 1.5.2 + # + def GetImageScale() + return @img_scale; + end + alias_method :get_image_scale, :GetImageScale + + # + # Returns the page width in units. + # @return int page width. + # @author Nicola Asuni + # @since 1.5.2 + # + def GetPageWidth() + return @w; + end + alias_method :get_page_width, :GetPageWidth + + # + # Returns the page height in units. + # @return int page height. + # @author Nicola Asuni + # @since 1.5.2 + # + def GetPageHeight() + return @h; + end + alias_method :get_page_height, :GetPageHeight + + # + # Returns the page break margin. + # @return int page break margin. + # @author Nicola Asuni + # @since 1.5.2 + # + def GetBreakMargin() + return @b_margin; + end + alias_method :get_break_margin, :GetBreakMargin + + # + # Returns the scale factor (number of points in user unit). + # @return int scale factor. + # @author Nicola Asuni + # @since 1.5.2 + # + def GetScaleFactor() + return @k; + end + alias_method :get_scale_factor, :GetScaleFactor + + # + # Defines the left, top and right margins. By default, they equal 1 cm. Call this method to change them. + # @param float :left Left margin. + # @param float :top Top margin. + # @param float :right Right margin. Default value is the left one. + # @since 1.0 + # @see SetLeftMargin(), SetTopMargin(), SetRightMargin(), SetAutoPageBreak() + # + def SetMargins(left, top, right=-1) + #Set left, top and right margins + @l_margin = left + @t_margin = top + if (right == -1) + right = left + end + @r_margin = right + end + alias_method :set_margins, :SetMargins + + # + # Defines the left margin. The method can be called before creating the first page. If the current abscissa gets out of page, it is brought back to the margin. + # @param float :margin The margin. + # @since 1.4 + # @see SetTopMargin(), SetRightMargin(), SetAutoPageBreak(), SetMargins() + # + def SetLeftMargin(margin) + #Set left margin + @l_margin = margin + if ((@page>0) and (@x < margin)) + @x = margin + end + end + alias_method :set_left_margin, :SetLeftMargin + + # + # Defines the top margin. The method can be called before creating the first page. + # @param float :margin The margin. + # @since 1.5 + # @see SetLeftMargin(), SetRightMargin(), SetAutoPageBreak(), SetMargins() + # + def SetTopMargin(margin) + #Set top margin + @t_margin = margin + end + alias_method :set_top_margin, :SetTopMargin + + # + # Defines the right margin. The method can be called before creating the first page. + # @param float :margin The margin. + # @since 1.5 + # @see SetLeftMargin(), SetTopMargin(), SetAutoPageBreak(), SetMargins() + # + def SetRightMargin(margin) + #Set right margin + @r_margin = margin + end + alias_method :set_right_margin, :SetRightMargin + + # + # Enables or disables the automatic page breaking mode. When enabling, the second parameter is the distance from the bottom of the page that defines the triggering limit. By default, the mode is on and the margin is 2 cm. + # @param boolean :auto Boolean indicating if mode should be on or off. + # @param float :margin Distance from the bottom of the page. + # @since 1.0 + # @see Cell(), MultiCell(), AcceptPageBreak() + # + def SetAutoPageBreak(auto, margin=0) + #Set auto page break mode and triggering margin + @auto_page_break = auto + @b_margin = margin + @page_break_trigger = @h - margin + end + alias_method :set_auto_page_break, :SetAutoPageBreak + + # + # Defines the way the document is to be displayed by the viewer. The zoom level can be set: pages can be displayed entirely on screen, occupy the full width of the window, use real size, be scaled by a specific zooming factor or use viewer default (configured in the Preferences menu of Acrobat). The page layout can be specified too: single at once, continuous display, two columns or viewer default. By default, documents use the full width mode with continuous display. + # @param mixed :zoom The zoom to use. It can be one of the following string values or a number indicating the zooming factor to use.
    • fullpage: displays the entire page on screen
    • fullwidth: uses maximum width of window
    • real: uses real size (equivalent to 100% zoom)
    • default: uses viewer default mode
    + # @param string :layout The page layout. Possible values are:
    • single: displays one page at once
    • continuous: displays pages continuously (default)
    • two: displays two pages on two columns
    • default: uses viewer default mode
    + # @since 1.2 + # + def SetDisplayMode(zoom, layout = 'continuous') + #Set display mode in viewer + if (zoom == 'fullpage' or zoom == 'fullwidth' or zoom == 'real' or zoom == 'default' or !zoom.is_a?(String)) + @zoom_mode = zoom + else + Error("Incorrect zoom display mode: #{zoom}") + end + if (layout == 'single' or layout == 'continuous' or layout == 'two' or layout == 'default') + @layout_mode = layout + else + Error("Incorrect layout display mode: #{layout}") + end + end + alias_method :set_display_mode, :SetDisplayMode + + # + # Activates or deactivates page compression. When activated, the internal representation of each page is compressed, which leads to a compression ratio of about 2 for the resulting document. Compression is on by default. + # Note: the Zlib extension is required for this feature. If not present, compression will be turned off. + # @param boolean :compress Boolean indicating if compression must be enabled. + # @since 1.4 + # + def SetCompression(compress) + #Set page compression + if (respond_to?('gzcompress')) + @compress = compress + else + @compress = false + end + end + alias_method :set_compression, :SetCompression + + # + # Defines the title of the document. + # @param string :title The title. + # @since 1.2 + # @see SetAuthor(), SetCreator(), SetKeywords(), SetSubject() + # + def SetTitle(title) + #Title of document + @title = title + end + alias_method :set_title, :SetTitle + + # + # Defines the subject of the document. + # @param string :subject The subject. + # @since 1.2 + # @see SetAuthor(), SetCreator(), SetKeywords(), SetTitle() + # + def SetSubject(subject) + #Subject of document + @subject = subject + end + alias_method :set_subject, :SetSubject + + # + # Defines the author of the document. + # @param string :author The name of the author. + # @since 1.2 + # @see SetCreator(), SetKeywords(), SetSubject(), SetTitle() + # + def SetAuthor(author) + #Author of document + @author = author + end + alias_method :set_author, :SetAuthor + + # + # Associates keywords with the document, generally in the form 'keyword1 keyword2 ...'. + # @param string :keywords The list of keywords. + # @since 1.2 + # @see SetAuthor(), SetCreator(), SetSubject(), SetTitle() + # + def SetKeywords(keywords) + #Keywords of document + @keywords = keywords + end + alias_method :set_keywords, :SetKeywords + + # + # Defines the creator of the document. This is typically the name of the application that generates the PDF. + # @param string :creator The name of the creator. + # @since 1.2 + # @see SetAuthor(), SetKeywords(), SetSubject(), SetTitle() + # + def SetCreator(creator) + #Creator of document + @creator = creator + end + alias_method :set_creator, :SetCreator + + # + # Defines an alias for the total number of pages. It will be substituted as the document is closed.
    + # Example:
    + #
    +	# class PDF extends TCPDF {
    +	# 	def Footer()
    +	# 		#Go to 1.5 cm from bottom
    +	# 		SetY(-15);
    +	# 		#Select Arial italic 8
    +	# 		SetFont('Arial','I',8);
    +	# 		#Print current and total page numbers
    +	# 		Cell(0,10,'Page '.PageNo().'/{nb}',0,0,'C');
    +	# 	end
    +	# }
    +	# :pdf=new PDF();
    +	# :pdf->alias_nb_pages();
    +	# 
    + # @param string :alias The alias. Default valuenb}. + # @since 1.4 + # @see PageNo(), Footer() + # + def AliasNbPages(alias_nb ='{nb}') + #Define an alias for total number of pages + @alias_nb_pages = escapetext(alias_nb) + end + alias_method :alias_nb_pages, :AliasNbPages + + # + # This method is automatically called in case of fatal error; it simply outputs the message and halts the execution. An inherited class may override it to customize the error handling but should always halt the script, or the resulting document would probably be invalid. + # 2004-06-11 :: Nicola Asuni : changed bold tag with strong + # @param string :msg The error message + # @since 1.0 + # + def Error(msg) + #Fatal error + raise ("TCPDF error: #{msg}") + end + alias_method :error, :Error + + # + # This method begins the generation of the PDF document. It is not necessary to call it explicitly because AddPage() does it automatically. + # Note: no page is created by this method + # @since 1.0 + # @see AddPage(), Close() + # + def Open() + #Begin document + @state = 1 + end + # alias_method :open, :Open + + # + # Terminates the PDF document. It is not necessary to call this method explicitly because Output() does it automatically. If the document contains no page, AddPage() is called to prevent from getting an invalid document. + # @since 1.0 + # @see Open(), Output() + # + def Close() + #Terminate document + if (@state==3) + return; + end + if (@page==0) + AddPage(); + end + #Page footer + @in_footer=true; + Footer(); + @in_footer=false; + #Close page + endpage(); + #Close document + enddoc(); + end + # alias_method :close, :Close + + # + # Adds a new page to the document. If a page is already present, the Footer() method is called first to output the footer. Then the page is added, the current position set to the top-left corner according to the left and top margins, and Header() is called to display the header. + # The font which was set before calling is automatically restored. There is no need to call SetFont() again if you want to continue with the same font. The same is true for colors and line width. + # The origin of the coordinate system is at the top-left corner and increasing ordinates go downwards. + # @param string :orientation Page orientation. Possible values are (case insensitive):
    • P or Portrait
    • L or Landscape
    The default value is the one passed to the constructor. + # @since 1.0 + # @see TCPDF(), Header(), Footer(), SetMargins() + # + def AddPage(orientation='') + #Start a new page + if (@state==0) + Open(); + end + family=@font_family; + style=@font_style + (@underline ? 'U' : '') + (@deleted ? 'D' : ''); + size=@font_size_pt; + lw=@line_width; + dc=@draw_color; + fc=@fill_color; + tc=@text_color; + cf=@color_flag; + if (@page>0) + #Page footer + @in_footer=true; + Footer(); + @in_footer=false; + #Close page + endpage(); + end + #Start new page + beginpage(orientation); + #Set line cap style to square + out('2 J'); + #Set line width + @line_width = lw; + out(sprintf('%.2f w', lw*@k)); + #Set font + if (family) + SetFont(family, style, size); + end + #Set colors + @draw_color = dc; + if (dc!='0 G') + out(dc); + end + @fill_color = fc; + if (fc!='0 g') + out(fc); + end + @text_color = tc; + @color_flag = cf; + #Page header + Header(); + #Restore line width + if (@line_width != lw) + @line_width = lw; + out(sprintf('%.2f w', lw*@k)); + end + #Restore font + if (family) + SetFont(family, style, size); + end + #Restore colors + if (@draw_color != dc) + @draw_color = dc; + out(dc); + end + if (@fill_color != fc) + @fill_color = fc; + out(fc); + end + @text_color = tc; + @color_flag = cf; + end + alias_method :add_page, :AddPage + + # + # Rotate object. + # @param float :angle angle in degrees for counter-clockwise rotation + # @param int :x abscissa of the rotation center. Default is current x position + # @param int :y ordinate of the rotation center. Default is current y position + # + def Rotate(angle, x="", y="") + + if (x == '') + x = @x; + end + + if (y == '') + y = @y; + end + + if (@rtl) + x = @w - x; + angle = -@angle; + end + + y = (@h - y) * @k; + x *= @k; + + # calculate elements of transformation matrix + tm = [] + tm[0] = ::Math::cos(deg2rad(angle)); + tm[1] = ::Math::sin(deg2rad(angle)); + tm[2] = -tm[1]; + tm[3] = tm[0]; + tm[4] = x + tm[1] * y - tm[0] * x; + tm[5] = y - tm[0] * y - tm[1] * x; + + # generate the transformation matrix + Transform(tm); + end + alias_method :rotate, :Rotate + + # + # Starts a 2D tranformation saving current graphic state. + # This function must be called before scaling, mirroring, translation, rotation and skewing. + # Use StartTransform() before, and StopTransform() after the transformations to restore the normal behavior. + # + def StartTransform + out('q'); + end + alias_method :start_transform, :StartTransform + + # + # Stops a 2D tranformation restoring previous graphic state. + # This function must be called after scaling, mirroring, translation, rotation and skewing. + # Use StartTransform() before, and StopTransform() after the transformations to restore the normal behavior. + # + def StopTransform + out('Q'); + end + alias_method :stop_transform, :StopTransform + + # + # Apply graphic transformations. + # @since 2.1.000 (2008-01-07) + # @see StartTransform(), StopTransform() + # + def Transform(tm) + x = out(sprintf('%.3f %.3f %.3f %.3f %.3f %.3f cm', tm[0], tm[1], tm[2], tm[3], tm[4], tm[5])); + end + alias_method :transform, :Transform + + # + # Set header data. + # @param string :ln header image logo + # @param string :lw header image logo width in mm + # @param string :ht string to print as title on document header + # @param string :hs string to print on document header + # + def SetHeaderData(ln="", lw=0, ht="", hs="") + @header_logo = ln || "" + @header_logo_width = lw || 0 + @header_title = ht || "" + @header_string = hs || "" + end + alias_method :set_header_data, :SetHeaderData + + # + # Set header margin. + # (minimum distance between header and top page margin) + # @param int :hm distance in millimeters + # + def SetHeaderMargin(hm=10) + @header_margin = hm; + end + alias_method :set_header_margin, :SetHeaderMargin + + # + # Set footer margin. + # (minimum distance between footer and bottom page margin) + # @param int :fm distance in millimeters + # + def SetFooterMargin(fm=10) + @footer_margin = fm; + end + alias_method :set_footer_margin, :SetFooterMargin + + # + # Set a flag to print page header. + # @param boolean :val set to true to print the page header (default), false otherwise. + # + def SetPrintHeader(val=true) + @print_header = val; + end + alias_method :set_print_header, :SetPrintHeader + + # + # Set a flag to print page footer. + # @param boolean :value set to true to print the page footer (default), false otherwise. + # + def SetPrintFooter(val=true) + @print_footer = val; + end + alias_method :set_print_footer, :SetPrintFooter + + # + # This method is used to render the page header. + # It is automatically called by AddPage() and could be overwritten in your own inherited class. + # + def Header() + if (@print_header) + if (@original_l_margin.nil?) + @original_l_margin = @l_margin; + end + if (@original_r_margin.nil?) + @original_r_margin = @r_margin; + end + + #set current position + SetXY(@original_l_margin, @header_margin); + + if ((@header_logo) and (@header_logo != @@k_blank_image)) + Image(@header_logo, @original_l_margin, @header_margin, @header_logo_width); + else + @img_rb_y = GetY(); + end + + cell_height = ((@@k_cell_height_ratio * @header_font[2]) / @k).round(2) + + header_x = @original_l_margin + (@header_logo_width * 1.05); #set left margin for text data cell + + # header title + SetFont(@header_font[0], 'B', @header_font[2] + 1); + SetX(header_x); + Cell(@header_width, cell_height, @header_title, 0, 1, 'L'); + + # header string + SetFont(@header_font[0], @header_font[1], @header_font[2]); + SetX(header_x); + MultiCell(@header_width, cell_height, @header_string, 0, 'L', 0); + + # print an ending header line + if (@header_width) + #set style for cell border + SetLineWidth(0.3); + SetDrawColor(0, 0, 0); + SetY(1 + (@img_rb_y > GetY() ? @img_rb_y : GetY())); + SetX(@original_l_margin); + Cell(0, 0, '', 'T', 0, 'C'); + end + + #restore position + SetXY(@original_l_margin, @t_margin); + end + end + alias_method :header, :Header + + # + # This method is used to render the page footer. + # It is automatically called by AddPage() and could be overwritten in your own inherited class. + # + def Footer() + if (@print_footer) + + if (@original_l_margin.nil?) + @original_l_margin = @l_margin; + end + if (@original_r_margin.nil?) + @original_r_margin = @r_margin; + end + + #set font + SetFont(@footer_font[0], @footer_font[1] , @footer_font[2]); + #set style for cell border + line_width = 0.3; + SetLineWidth(line_width); + SetDrawColor(0, 0, 0); + + footer_height = ((@@k_cell_height_ratio * @footer_font[2]) / @k).round; #footer height, was , 2) + #get footer y position + footer_y = @h - @footer_margin - footer_height; + #set current position + SetXY(@original_l_margin, footer_y); + + #print document barcode + if (@barcode) + Ln(); + barcode_width = ((@w - @original_l_margin - @original_r_margin)).round; #max width + writeBarcode(@original_l_margin, footer_y + line_width, barcode_width, footer_height - line_width, "C128B", false, false, 2, @barcode); + end + + SetXY(@original_l_margin, footer_y); + + #Print page number + Cell(0, footer_height, @l['w_page'] + " " + PageNo().to_s + ' / {nb}', 'T', 0, 'R'); + end + end + alias_method :footer, :Footer + + # + # Returns the current page number. + # @return int page number + # @since 1.0 + # @see alias_nb_pages() + # + def PageNo() + #Get current page number + return @page; + end + alias_method :page_no, :PageNo + + # + # Defines the color used for all drawing operations (lines, rectangles and cell borders). It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page. + # @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255 + # @param int :g Green component (between 0 and 255) + # @param int :b Blue component (between 0 and 255) + # @since 1.3 + # @see SetFillColor(), SetTextColor(), Line(), Rect(), Cell(), MultiCell() + # + def SetDrawColor(r, g=-1, b=-1) + #Set color for all stroking operations + if ((r==0 and g==0 and b==0) or g==-1) + @draw_color=sprintf('%.3f G', r/255.0); + else + @draw_color=sprintf('%.3f %.3f %.3f RG', r/255.0, g/255.0, b/255.0); + end + if (@page>0) + out(@draw_color); + end + end + alias_method :set_draw_color, :SetDrawColor + + # + # Defines the color used for all filling operations (filled rectangles and cell backgrounds). It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page. + # @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255 + # @param int :g Green component (between 0 and 255) + # @param int :b Blue component (between 0 and 255) + # @param boolean :storeprev if true stores the RGB array on :prevfill_color variable. + # @since 1.3 + # @see SetDrawColor(), SetTextColor(), Rect(), Cell(), MultiCell() + # + def SetFillColor(r, g=-1, b=-1, storeprev=false) + #Set color for all filling operations + if ((r==0 and g==0 and b==0) or g==-1) + @fill_color=sprintf('%.3f g', r/255.0); + else + @fill_color=sprintf('%.3f %.3f %.3f rg', r/255.0, g/255.0, b/255.0); + end + @color_flag=(@fill_color!=@text_color); + if (@page>0) + out(@fill_color); + end + if (storeprev) + # store color as previous value + @prevfill_color = [r, g, b] + end + end + alias_method :set_fill_color, :SetFillColor + + # This hasn't been ported from tcpdf, it's a variation on SetTextColor for setting cmyk colors + def SetCmykFillColor(c, m, y, k, storeprev=false) + #Set color for all filling operations + @fill_color=sprintf('%.3f %.3f %.3f %.3f k', c, m, y, k); + @color_flag=(@fill_color!=@text_color); + if (storeprev) + # store color as previous value + @prevtext_color = [c, m, y, k] + end + if (@page>0) + out(@fill_color); + end + end + alias_method :set_cmyk_fill_color, :SetCmykFillColor + + # + # Defines the color used for text. It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page. + # @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255 + # @param int :g Green component (between 0 and 255) + # @param int :b Blue component (between 0 and 255) + # @param boolean :storeprev if true stores the RGB array on :prevtext_color variable. + # @since 1.3 + # @see SetDrawColor(), SetFillColor(), Text(), Cell(), MultiCell() + # + def SetTextColor(r, g=-1, b=-1, storeprev=false) + #Set color for text + if ((r==0 and :g==0 and :b==0) or :g==-1) + @text_color=sprintf('%.3f g', r/255.0); + else + @text_color=sprintf('%.3f %.3f %.3f rg', r/255.0, g/255.0, b/255.0); + end + @color_flag=(@fill_color!=@text_color); + if (storeprev) + # store color as previous value + @prevtext_color = [r, g, b] + end + end + alias_method :set_text_color, :SetTextColor + + # This hasn't been ported from tcpdf, it's a variation on SetTextColor for setting cmyk colors + def SetCmykTextColor(c, m, y, k, storeprev=false) + #Set color for text + @text_color=sprintf('%.3f %.3f %.3f %.3f k', c, m, y, k); + @color_flag=(@fill_color!=@text_color); + if (storeprev) + # store color as previous value + @prevtext_color = [c, m, y, k] + end + end + alias_method :set_cmyk_text_color, :SetCmykTextColor + + # + # Returns the length of a string in user unit. A font must be selected.
    + # Support UTF-8 Unicode [Nicola Asuni, 2005-01-02] + # @param string :s The string whose length is to be computed + # @return int + # @since 1.2 + # + def GetStringWidth(s) + #Get width of a string in the current font + s = s.to_s; + cw = @current_font['cw'] + w = 0; + if (@is_unicode) + unicode = UTF8StringToArray(s); + unicode.each do |char| + if (!cw[char].nil?) + w += cw[char]; + # This should not happen. UTF8StringToArray should guarentee the array is ascii values. + # elsif (c!cw[char[0]].nil?) + # w += cw[char[0]]; + # elsif (!cw[char.chr].nil?) + # w += cw[char.chr]; + elsif (!@current_font['desc']['MissingWidth'].nil?) + w += @current_font['desc']['MissingWidth']; # set default size + else + w += 500; + end + end + else + s.each_byte do |c| + if cw[c.chr] + w += cw[c.chr]; + elsif cw[?c.chr] + w += cw[?c.chr] + end + end + end + return (w * @font_size / 1000.0); + end + alias_method :get_string_width, :GetStringWidth + + # + # Defines the line width. By default, the value equals 0.2 mm. The method can be called before the first page is created and the value is retained from page to page. + # @param float :width The width. + # @since 1.0 + # @see Line(), Rect(), Cell(), MultiCell() + # + def SetLineWidth(width) + #Set line width + @line_width = width; + if (@page>0) + out(sprintf('%.2f w', width*@k)); + end + end + alias_method :set_line_width, :SetLineWidth + + # + # Draws a line between two points. + # @param float :x1 Abscissa of first point + # @param float :y1 Ordinate of first point + # @param float :x2 Abscissa of second point + # @param float :y2 Ordinate of second point + # @since 1.0 + # @see SetLineWidth(), SetDrawColor() + # + def Line(x1, y1, x2, y2) + #Draw a line + out(sprintf('%.2f %.2f m %.2f %.2f l S', x1 * @k, (@h - y1) * @k, x2 * @k, (@h - y2) * @k)); + end + alias_method :line, :Line + + def Circle(mid_x, mid_y, radius, style='') + mid_y = (@h-mid_y)*@k + out(sprintf("q\n")) # postscript content in pdf + # init line type etc. with /GSD gs G g (grey) RG rg (RGB) w=line witdh etc. + out(sprintf("1 j\n")) # line join + # translate ("move") circle to mid_y, mid_y + out(sprintf("1 0 0 1 %f %f cm", mid_x, mid_y)) + kappa = 0.5522847498307933984022516322796 + # Quadrant 1 + x_s = 0.0 # 12 o'clock + y_s = 0.0 + radius + x_e = 0.0 + radius # 3 o'clock + y_e = 0.0 + out(sprintf("%f %f m\n", x_s, y_s)) # move to 12 o'clock + # cubic bezier control point 1, start height and kappa * radius to the right + bx_e1 = x_s + (radius * kappa) + by_e1 = y_s + # cubic bezier control point 2, end and kappa * radius above + bx_e2 = x_e + by_e2 = y_e + (radius * kappa) + # draw cubic bezier from current point to x_e/y_e with bx_e1/by_e1 and bx_e2/by_e2 as bezier control points + out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e)) + # Quadrant 2 + x_s = x_e + y_s = y_e # 3 o'clock + x_e = 0.0 + y_e = 0.0 - radius # 6 o'clock + bx_e1 = x_s # cubic bezier point 1 + by_e1 = y_s - (radius * kappa) + bx_e2 = x_e + (radius * kappa) # cubic bezier point 2 + by_e2 = y_e + out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e)) + # Quadrant 3 + x_s = x_e + y_s = y_e # 6 o'clock + x_e = 0.0 - radius + y_e = 0.0 # 9 o'clock + bx_e1 = x_s - (radius * kappa) # cubic bezier point 1 + by_e1 = y_s + bx_e2 = x_e # cubic bezier point 2 + by_e2 = y_e - (radius * kappa) + out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e)) + # Quadrant 4 + x_s = x_e + y_s = y_e # 9 o'clock + x_e = 0.0 + y_e = 0.0 + radius # 12 o'clock + bx_e1 = x_s # cubic bezier point 1 + by_e1 = y_s + (radius * kappa) + bx_e2 = x_e - (radius * kappa) # cubic bezier point 2 + by_e2 = y_e + out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e)) + if style=='F' + op='f' + elsif style=='FD' or style=='DF' + op='b' + else + op='s' + end + out(sprintf("#{op}\n")) # stroke circle, do not fill and close path + # for filling etc. b, b*, f, f* + out(sprintf("Q\n")) # finish postscript in PDF + end + alias_method :circle, :Circle + + # + # Outputs a rectangle. It can be drawn (border only), filled (with no border) or both. + # @param float :x Abscissa of upper-left corner + # @param float :y Ordinate of upper-left corner + # @param float :w Width + # @param float :h Height + # @param string :style Style of rendering. Possible values are:
    • D or empty string: draw (default)
    • F: fill
    • DF or FD: draw and fill
    + # @since 1.0 + # @see SetLineWidth(), SetDrawColor(), SetFillColor() + # + def Rect(x, y, w, h, style='') + #Draw a rectangle + if (style=='F') + op='f'; + elsif (style=='FD' or style=='DF') + op='B'; + else + op='S'; + end + out(sprintf('%.2f %.2f %.2f %.2f re %s', x * @k, (@h - y) * @k, w * @k, -h * @k, op)); + end + alias_method :rect, :Rect + + # + # Imports a TrueType or Type1 font and makes it available. It is necessary to generate a font definition file first with the makefont.rb utility. The definition file (and the font file itself when embedding) must be present either in the current directory or in the one indicated by FPDF_FONTPATH if the constant is defined. If it could not be found, the error "Could not include font definition file" is generated. + # Support UTF-8 Unicode [Nicola Asuni, 2005-01-02]. + # Example:
    + #
    +	# :pdf->AddFont('Comic','I');
    +	# # is equivalent to:
    +	# :pdf->AddFont('Comic','I','comici.rb');
    +	# 
    + # @param string :family Font family. The name can be chosen arbitrarily. If it is a standard family name, it will override the corresponding font. + # @param string :style Font style. Possible values are (case insensitive):
    • empty string: regular (default)
    • B: bold
    • I: italic
    • BI or IB: bold italic
    + # @param string :file The font definition file. By default, the name is built from the family and style, in lower case with no space. + # @since 1.5 + # @see SetFont() + # + def AddFont(family, style='', file='') + if (family.empty?) + return; + end + + #Add a TrueType or Type1 font + family = family.downcase + if ((!@is_unicode) and (family == 'arial')) + family = 'helvetica'; + end + + style=style.upcase + style=style.gsub('U',''); + style=style.gsub('D',''); + if (style == 'IB') + style = 'BI'; + end + + fontkey = family + style; + # check if the font has been already added + if !@fonts[fontkey].nil? + return; + end + + if (file=='') + file = family.gsub(' ', '') + style.downcase + '.rb'; + end + font_file_name = getfontpath(file) + if (font_file_name.nil?) + # try to load the basic file without styles + file = family.gsub(' ', '') + '.rb'; + font_file_name = getfontpath(file) + end + if font_file_name.nil? + Error("Could not find font #{file}.") + end + require(getfontpath(file)) + font_desc = TCPDFFontDescriptor.font(file) + + if (font_desc[:name].nil? and @@fpdf_charwidths.nil?) + Error('Could not include font definition file'); + end + + i = @fonts.length+1; + if (@is_unicode) + @fonts[fontkey] = {'i' => i, 'type' => font_desc[:type], 'name' => font_desc[:name], 'desc' => font_desc[:desc], 'up' => font_desc[:up], 'ut' => font_desc[:ut], 'cw' => font_desc[:cw], 'enc' => font_desc[:enc], 'file' => font_desc[:file], 'ctg' => font_desc[:ctg], 'cMap' => font_desc[:cMap], 'registry' => font_desc[:registry]} + @@fpdf_charwidths[fontkey] = font_desc[:cw]; + else + @fonts[fontkey]={'i' => i, 'type'=>'core', 'name'=>@core_fonts[fontkey], 'up'=>-100, 'ut'=>50, 'cw' => font_desc[:cw]} + @@fpdf_charwidths[fontkey] = font_desc[:cw]; + end + + if (!font_desc[:diff].nil? and (!font_desc[:diff].empty?)) + #Search existing encodings + d=0; + nb=@diffs.length; + 1.upto(nb) do |i| + if (@diffs[i]== font_desc[:diff]) + d = i; + break; + end + end + if (d==0) + d = nb+1; + @diffs[d] = font_desc[:diff]; + end + @fonts[fontkey]['diff'] = d; + end + if (font_desc[:file] and font_desc[:file].length > 0) + if (font_desc[:type] == "TrueType") or (font_desc[:type] == "TrueTypeUnicode") + @font_files[font_desc[:file]] = {'length1' => font_desc[:originalsize]} + else + @font_files[font_desc[:file]] = {'length1' => font_desc[:size1], 'length2' => font_desc[:size2]} + end + end + end + alias_method :add_font, :AddFont + + # + # Sets the font used to print character strings. It is mandatory to call this method at least once before printing text or the resulting document would not be valid. + # The font can be either a standard one or a font added via the AddFont() method. Standard fonts use Windows encoding cp1252 (Western Europe). + # The method can be called before the first page is created and the font is retained from page to page. + # If you just wish to change the current font size, it is simpler to call SetFontSize(). + # Note: for the standard fonts, the font metric files must be accessible. There are three possibilities for this:
    • They are in the current directory (the one where the running script lies)
    • They are in one of the directories defined by the include_path parameter
    • They are in the directory defined by the FPDF_FONTPATH constant

    + # Example for the last case (note the trailing slash):
    + #
    +	# define('FPDF_FONTPATH','/home/www/font/');
    +	# require('tcpdf.rb');
    +	#
    +	# #Times regular 12
    +	# :pdf->SetFont('Times');
    +	# #Arial bold 14
    +	# :pdf->SetFont('Arial','B',14);
    +	# #Removes bold
    +	# :pdf->SetFont('');
    +	# #Times bold, italic and underlined 14
    +	# :pdf->SetFont('Times','BIUD');
    +	# 

    + # If the file corresponding to the requested font is not found, the error "Could not include font metric file" is generated. + # @param string :family Family font. It can be either a name defined by AddFont() or one of the standard families (case insensitive):
    • Courier (fixed-width)
    • Helvetica or Arial (synonymous; sans serif)
    • Times (serif)
    • Symbol (symbolic)
    • ZapfDingbats (symbolic)
    It is also possible to pass an empty string. In that case, the current family is retained. + # @param string :style Font style. Possible values are (case insensitive):
    • empty string: regular
    • B: bold
    • I: italic
    • U: underline
    or any combination. The default value is regular. Bold and italic styles do not apply to Symbol and ZapfDingbats + # @param float :size Font size in points. The default value is the current size. If no size has been specified since the beginning of the document, the value taken is 12 + # @since 1.0 + # @see AddFont(), SetFontSize(), Cell(), MultiCell(), Write() + # + def SetFont(family, style='', size=0) + # save previous values + @prevfont_family = @font_family; + @prevfont_style = @font_style; + + family=family.downcase; + if (family=='') + family=@font_family; + end + if ((!@is_unicode) and (family == 'arial')) + family = 'helvetica'; + elsif ((family=="symbol") or (family=="zapfdingbats")) + style=''; + end + + style=style.upcase; + + if (style.include?('U')) + @underline=true; + style= style.gsub('U',''); + else + @underline=false; + end + if (style.include?('D')) + @deleted=true; + style= style.gsub('D',''); + else + @deleted=false; + end + if (style=='IB') + style='BI'; + end + if (size==0) + size=@font_size_pt; + end + + # try to add font (if not already added) + AddFont(family, style); + + #Test if font is already selected + if ((@font_family == family) and (@font_style == style) and (@font_size_pt == size)) + return; + end + + fontkey = family + style; + style = '' if (@fonts[fontkey].nil? and !@fonts[family].nil?) + + #Test if used for the first time + if (@fonts[fontkey].nil?) + #Check if one of the standard fonts + if (!@core_fonts[fontkey].nil?) + if @@fpdf_charwidths[fontkey].nil? + #Load metric file + file = family; + if ((family!='symbol') and (family!='zapfdingbats')) + file += style.downcase; + end + if (getfontpath(file + '.rb').nil?) + # try to load the basic file without styles + file = family; + fontkey = family; + end + require(getfontpath(file + '.rb')); + font_desc = TCPDFFontDescriptor.font(file) + if ((@is_unicode and ctg.nil?) or ((!@is_unicode) and (@@fpdf_charwidths[fontkey].nil?)) ) + Error("Could not include font metric file [" + fontkey + "]: " + getfontpath(file + ".rb")); + end + end + i = @fonts.length + 1; + + if (@is_unicode) + @fonts[fontkey] = {'i' => i, 'type' => font_desc[:type], 'name' => font_desc[:name], 'desc' => font_desc[:desc], 'up' => font_desc[:up], 'ut' => font_desc[:ut], 'cw' => font_desc[:cw], 'enc' => font_desc[:enc], 'file' => font_desc[:file], 'ctg' => font_desc[:ctg]} + @@fpdf_charwidths[fontkey] = font_desc[:cw]; + else + @fonts[fontkey] = {'i' => i, 'type'=>'core', 'name'=>@core_fonts[fontkey], 'up'=>-100, 'ut'=>50, 'cw' => font_desc[:cw]} + @@fpdf_charwidths[fontkey] = font_desc[:cw]; + end + else + Error('Undefined font: ' + family + ' ' + style); + end + end + #Select it + @font_family = family; + @font_style = style; + @font_size_pt = size; + @font_size = size / @k; + @current_font = @fonts[fontkey]; # was & may need deep copy? + if (@page>0) + out(sprintf('BT /F%d %.2f Tf ET', @current_font['i'], @font_size_pt)); + end + end + alias_method :set_font, :SetFont + + # + # Defines the size of the current font. + # @param float :size The size (in points) + # @since 1.0 + # @see SetFont() + # + def SetFontSize(size) + #Set font size in points + if (@font_size_pt== size) + return; + end + @font_size_pt = size; + @font_size = size.to_f / @k; + if (@page > 0) + out(sprintf('BT /F%d %.2f Tf ET', @current_font['i'], @font_size_pt)); + end + end + alias_method :set_font_size, :SetFontSize + + # + # Creates a new internal link and returns its identifier. An internal link is a clickable area which directs to another place within the document.
    + # The identifier can then be passed to Cell(), Write(), Image() or Link(). The destination is defined with SetLink(). + # @since 1.5 + # @see Cell(), Write(), Image(), Link(), SetLink() + # + def AddLink() + #Create a new internal link + n=@links.length+1; + @links[n]=[0,0]; + return n; + end + alias_method :add_link, :AddLink + + # + # Defines the page and position a link points to + # @param int :link The link identifier returned by AddLink() + # @param float :y Ordinate of target position; -1 indicates the current position. The default value is 0 (top of page) + # @param int :page Number of target page; -1 indicates the current page. This is the default value + # @since 1.5 + # @see AddLink() + # + def SetLink(link, y=0, page=-1) + #Set destination of internal link + if (y==-1) + y=@y; + end + if (page==-1) + page=@page; + end + @links[link] = [page, y] + end + alias_method :set_link, :SetLink + + # + # Puts a link on a rectangular area of the page. Text or image links are generally put via Cell(), Write() or Image(), but this method can be useful for instance to define a clickable area inside an image. + # @param float :x Abscissa of the upper-left corner of the rectangle + # @param float :y Ordinate of the upper-left corner of the rectangle + # @param float :w Width of the rectangle + # @param float :h Height of the rectangle + # @param mixed :link URL or identifier returned by AddLink() + # @since 1.5 + # @see AddLink(), Cell(), Write(), Image() + # + def Link(x, y, w, h, link) + #Put a link on the page + @page_links ||= Array.new + @page_links[@page] ||= Array.new + @page_links[@page].push([x * @k, @h_pt - y * @k, w * @k, h*@k, link]); + end + alias_method :link, :Link + + # + # Prints a character string. The origin is on the left of the first charcter, on the baseline. This method allows to place a string precisely on the page, but it is usually easier to use Cell(), MultiCell() or Write() which are the standard methods to print text. + # @param float :x Abscissa of the origin + # @param float :y Ordinate of the origin + # @param string :txt String to print + # @since 1.0 + # @see SetFont(), SetTextColor(), Cell(), MultiCell(), Write() + # + def Text(x, y, txt) + #Output a string + s=sprintf('BT %.2f %.2f Td (%s) Tj ET', x * @k, (@h-y) * @k, escapetext(txt)); + if (@underline and (txt!='')) + s += ' ' + dolinetxt(x, y, txt); + end + if (@color_flag) + s='q ' + @text_color + ' ' + s + ' Q'; + end + out(s); + end + alias_method :text, :Text + + # + # Whenever a page break condition is met, the method is called, and the break is issued or not depending on the returned value. The default implementation returns a value according to the mode selected by SetAutoPageBreak().
    + # This method is called automatically and should not be called directly by the application.
    + # Example:
    + # The method is overriden in an inherited class in order to obtain a 3 column layout:
    + #
    +	# class PDF extends TCPDF {
    +	# 	var :col=0;
    +	#
    +	# 	def SetCol(col)
    +	# 		#Move position to a column
    +	# 		@col = col;
    +	# 		:x=10+:col*65;
    +	# 		SetLeftMargin(x);
    +	# 		SetX(x);
    +	# 	end
    +	#
    +	# 	def AcceptPageBreak()
    +	# 		if (@col<2)
    +	# 			#Go to next column
    +	# 			SetCol(@col+1);
    +	# 			SetY(10);
    +	# 			return false;
    +	# 		end
    +	# 		else
    +	# 			#Go back to first column and issue page break
    +	# 			SetCol(0);
    +	# 			return true;
    +	# 		end
    +	# 	end
    +	# }
    +	#
    +	# :pdf=new PDF();
    +	# :pdf->Open();
    +	# :pdf->AddPage();
    +	# :pdf->SetFont('Arial','',12);
    +	# for(i=1;:i<=300;:i++)
    +	#     :pdf->Cell(0,5,"Line :i",0,1);
    +	# }
    +	# :pdf->Output();
    +	# 
    + # @return boolean + # @since 1.4 + # @see SetAutoPageBreak() + # + def AcceptPageBreak() + #Accept automatic page break or not + return @auto_page_break; + end + alias_method :accept_page_break, :AcceptPageBreak + + def BreakThePage?(h) + if ((@y + h) > @page_break_trigger and !@in_footer and AcceptPageBreak()) + true + else + false + end + end + alias_method :break_the_page?, :BreakThePage? + # + # Prints a cell (rectangular area) with optional borders, background color and character string. The upper-left corner of the cell corresponds to the current position. The text can be aligned or centered. After the call, the current position moves to the right or to the next line. It is possible to put a link on the text.
    + # If automatic page breaking is enabled and the cell goes beyond the limit, a page break is done before outputting. + # @param float :w Cell width. If 0, the cell extends up to the right margin. + # @param float :h Cell height. Default value: 0. + # @param string :txt String to print. Default value: empty string. + # @param mixed :border Indicates if borders must be drawn around the cell. The value can be either a number:
    • 0: no border (default)
    • 1: frame
    or a string containing some or all of the following characters (in any order):
    • L: left
    • T: top
    • R: right
    • B: bottom
    + # @param int :ln Indicates where the current position should go after the call. Possible values are:
    • 0: to the right
    • 1: to the beginning of the next line
    • 2: below
    + # Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: 0. + # @param string :align Allows to center or align the text. Possible values are:
    • L or empty string: left align (default value)
    • C: center
    • R: right align
    + # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. + # @param mixed :link URL or identifier returned by AddLink(). + # @since 1.0 + # @see SetFont(), SetDrawColor(), SetFillColor(), SetTextColor(), SetLineWidth(), AddLink(), Ln(), MultiCell(), Write(), SetAutoPageBreak() + # + def Cell(w, h=0, txt='', border=0, ln=0, align='', fill=0, link=nil) + #Output a cell + k=@k; + if ((@y + h) > @page_break_trigger and !@in_footer and AcceptPageBreak()) + #Automatic page break + if @pages[@page+1].nil? + x = @x; + ws = @ws; + if (ws > 0) + @ws = 0; + out('0 Tw'); + end + AddPage(@cur_orientation); + @x = x; + if (ws > 0) + @ws = ws; + out(sprintf('%.3f Tw', ws * k)); + end + else + @page += 1; + @y=@t_margin; + end + end + + if (w == 0) + w = @w - @r_margin - @x; + end + s = ''; + if ((fill.to_i == 1) or (border.to_i == 1)) + if (fill.to_i == 1) + op = (border.to_i == 1) ? 'B' : 'f'; + else + op = 'S'; + end + s = sprintf('%.2f %.2f %.2f %.2f re %s ', @x * k, (@h - @y) * k, w * k, -h * k, op); + end + if (border.is_a?(String)) + x=@x; + y=@y; + if (border.include?('L')) + s<0) + # Go to next line + @y += h; + if (ln == 1) + @x = @l_margin; + end + else + @x += w; + end + end + alias_method :cell, :Cell + + # + # This method allows printing text with line breaks. They can be automatic (as soon as the text reaches the right border of the cell) or explicit (via the \n character). As many cells as necessary are output, one below the other.
    + # Text can be aligned, centered or justified. The cell block can be framed and the background painted. + # @param float :w Width of cells. If 0, they extend up to the right margin of the page. + # @param float :h Height of cells. + # @param string :txt String to print + # @param mixed :border Indicates if borders must be drawn around the cell block. The value can be either a number:
    • 0: no border (default)
    • 1: frame
    or a string containing some or all of the following characters (in any order):
    • L: left
    • T: top
    • R: right
    • B: bottom
    + # @param string :align Allows to center or align the text. Possible values are:
    • L or empty string: left align
    • C: center
    • R: right align
    • J: justification (default value)
    + # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. + # @param int :ln Indicates where the current position should go after the call. Possible values are:
    • 0: to the right
    • 1: to the beginning of the next line [DEFAULT]
    • 2: below
    + # @since 1.3 + # @see SetFont(), SetDrawColor(), SetFillColor(), SetTextColor(), SetLineWidth(), Cell(), Write(), SetAutoPageBreak() + # + def MultiCell(w, h, txt, border=0, align='J', fill=0, ln=1) + + # save current position + prevx = @x; + prevy = @y; + prevpage = @page; + + #Output text with automatic or explicit line breaks + + if (w == 0) + w = @w - @r_margin - @x; + end + + wmax = (w - 3 * @c_margin); + + s = txt.gsub("\r", ''); # remove carriage returns + nb = s.length; + + b=0; + if (border) + if (border==1) + border='LTRB'; + b='LRT'; + b2='LR'; + elsif border.is_a?(String) + b2=''; + if (border.include?('L')) + b2<<'L'; + end + if (border.include?('R')) + b2<<'R'; + end + b=(border.include?('T')) ? b2 + 'T' : b2; + end + end + sep=-1; + to_index=0; + from_j=0; + l=0; + ns=0; + nl=1; + + while to_index < nb + #Get next character + c = s[to_index]; + if c == "\n"[0] + #Explicit line break + if @ws > 0 + @ws = 0 + out('0 Tw') + end + #Ed Moss - change begin + end_i = to_index == 0 ? 0 : to_index - 1 + # Changed from s[from_j..to_index] to fix bug reported by Hans Allis. + from_j = to_index == 0 ? 1 : from_j + Cell(w, h, s[from_j..end_i], b, 2, align, fill) + #change end + to_index += 1 + sep=-1 + from_j=to_index + l=0 + ns=0 + nl += 1 + b = b2 if border and nl==2 + next + end + if (c == " "[0]) + sep = to_index; + ls = l; + ns += 1; + end + + l = GetStringWidth(s[from_j, to_index - from_j]); + + if (l > wmax) + #Automatic line break + if (sep == -1) + if (to_index == from_j) + to_index += 1; + end + if (@ws > 0) + @ws = 0; + out('0 Tw'); + end + Cell(w, h, s[from_j..to_index-1], b, 2, align, fill) # my FPDF version + else + if (align=='J' || align=='justify' || align=='justified') + @ws = (ns>1) ? (wmax-ls)/(ns-1) : 0; + out(sprintf('%.3f Tw', @ws * @k)); + end + Cell(w, h, s[from_j..sep], b, 2, align, fill); + to_index = sep + 1; + end + sep=-1; + from_j = to_index; + l=0; + ns=0; + nl += 1; + if (border and (nl==2)) + b = b2; + end + else + to_index += 1; + end + end + #Last chunk + if (@ws>0) + @ws=0; + out('0 Tw'); + end + if (border.is_a?(String) and border.include?('B')) + b<<'B'; + end + Cell(w, h, s[from_j, to_index-from_j], b, 2, align, fill); + + # move cursor to specified position + # since 2007-03-03 + if (ln == 1) + # go to the beginning of the next line + @x = @l_margin; + elsif (ln == 0) + # go to the top-right of the cell + @page = prevpage; + @y = prevy; + @x = prevx + w; + elsif (ln == 2) + # go to the bottom-left of the cell + @x = prevx; + end + end + alias_method :multi_cell, :MultiCell + + # + # This method prints text from the current position. When the right margin is reached (or the \n character is met) a line break occurs and text continues from the left margin. Upon method exit, the current position is left just at the end of the text. It is possible to put a link on the text.
    + # Example:
    + #
    +	# #Begin with regular font
    +	# :pdf->SetFont('Arial','',14);
    +	# :pdf->Write(5,'Visit ');
    +	# #Then put a blue underlined link
    +	# :pdf->SetTextColor(0,0,255);
    +	# :pdf->SetFont('','U');
    +	# :pdf->Write(5,'www.tecnick.com','http://www.tecnick.com');
    +	# 
    + # @param float :h Line height + # @param string :txt String to print + # @param mixed :link URL or identifier returned by AddLink() + # @param int :fill Indicates if the background must be painted (1) or transparent (0). Default value: 0. + # @since 1.5 + # @see SetFont(), SetTextColor(), AddLink(), MultiCell(), SetAutoPageBreak() + # + def Write(h, txt, link=nil, fill=0) + + #Output text in flowing mode + w = @w - @r_margin - @x; + wmax = (w - 3 * @c_margin); + + s = txt.gsub("\r", ''); + nb = s.length; + + # handle single space character + if ((nb==1) and (s == " ")) + @x += GetStringWidth(s); + return; + end + + sep=-1; + i=0; + j=0; + l=0; + nl=1; + while(i wmax) + #Automatic line break (word wrapping) + if (sep == -1) + if (@x > @l_margin) + #Move to next line + @x = @l_margin; + @y += h; + w=@w - @r_margin - @x; + wmax=(w - 3 * @c_margin); + i += 1 + nl += 1 + next + end + if (i == j) + i += 1 + end + Cell(w, h, s[j, (i-1)], 0, 2, '', fill, link); + else + Cell(w, h, s[j, (sep-j)], 0, 2, '', fill, link); + i = sep+1; + end + sep = -1; + j = i; + l = 0; + if (nl==1) + @x = @l_margin; + w = @w - @r_margin - @x; + wmax = (w - 3 * @c_margin); + end + nl += 1; + else + i += 1; + end + end + #Last chunk + if (i != j) + Cell(GetStringWidth(s[j..i]), h, s[j..i], 0, 0, '', fill, link); + end + end + alias_method :write, :Write + + # + # Puts an image in the page. The upper-left corner must be given. The dimensions can be specified in different ways:
    • explicit width and height (expressed in user unit)
    • one explicit dimension, the other being calculated automatically in order to keep the original proportions
    • no explicit dimension, in which case the image is put at 72 dpi
    + # Supported formats are JPEG and PNG. + # For JPEG, all flavors are allowed:
    • gray scales
    • true colors (24 bits)
    • CMYK (32 bits)
    + # For PNG, are allowed:
    • gray scales on at most 8 bits (256 levels)
    • indexed colors
    • true colors (24 bits)
    + # but are not supported:
    • Interlacing
    • Alpha channel
    + # If a transparent color is defined, it will be taken into account (but will be only interpreted by Acrobat 4 and above).
    + # The format can be specified explicitly or inferred from the file extension.
    + # It is possible to put a link on the image.
    + # Remark: if an image is used several times, only one copy will be embedded in the file.
    + # @param string :file Name of the file containing the image. + # @param float :x Abscissa of the upper-left corner. + # @param float :y Ordinate of the upper-left corner. + # @param float :w Width of the image in the page. If not specified or equal to zero, it is automatically calculated. + # @param float :h Height of the image in the page. If not specified or equal to zero, it is automatically calculated. + # @param string :type Image format. Possible values are (case insensitive): JPG, JPEG, PNG. If not specified, the type is inferred from the file extension. + # @param mixed :link URL or identifier returned by AddLink(). + # @since 1.1 + # @see AddLink() + # + def Image(file, x, y, w=0, h=0, type='', link=nil) + #Put an image on the page + if (@images[file].nil?) + #First use of image, get info + if (type == '') + pos = File::basename(file).rindex('.'); + if (pos.nil? or pos == 0) + Error('Image file has no extension and no type was specified: ' + file); + end + pos = file.rindex('.'); + type = file[pos+1..-1]; + end + type.downcase! + if (type == 'jpg' or type == 'jpeg') + info=parsejpg(file); + elsif (type == 'png') + info=parsepng(file); + elsif (type == 'gif') + tmpFile = imageToPNG(file); + info=parsepng(tmpFile.path); + tmpFile.delete + else + #Allow for additional formats + mtd='parse' + type; + if (!self.respond_to?(mtd)) + Error('Unsupported image type: ' + type); + end + info=send(mtd, file); + end + info['i']=@images.length+1; + @images[file] = info; + else + info=@images[file]; + end + #Automatic width and height calculation if needed + if ((w == 0) and (h == 0)) + rescale_x = (@w - @r_margin - x) / (info['w'] / (@img_scale * @k)) + rescale_x = 1 if rescale_x >= 1 + if (y + info['h'] * rescale_x / (@img_scale * @k) > @page_break_trigger and !@in_footer and AcceptPageBreak()) + #Automatic page break + if @pages[@page+1].nil? + ws = @ws; + if (ws > 0) + @ws = 0; + out('0 Tw'); + end + AddPage(@cur_orientation); + if (ws > 0) + @ws = ws; + out(sprintf('%.3f Tw', ws * @k)); + end + else + @page += 1; + end + y=@t_margin; + end + rescale_y = (@page_break_trigger - y) / (info['h'] / (@img_scale * @k)) + rescale_y = 1 if rescale_y >= 1 + rescale = rescale_y >= rescale_x ? rescale_x : rescale_y + + #Put image at 72 dpi + # 2004-06-14 :: Nicola Asuni, scale factor where added + w = info['w'] * rescale / (@img_scale * @k); + h = info['h'] * rescale / (@img_scale * @k); + elsif (w == 0) + w = h * info['w'] / info['h']; + elsif (h == 0) + h = w * info['h'] / info['w']; + end + out(sprintf('q %.2f 0 0 %.2f %.2f %.2f cm /I%d Do Q', w*@k, h*@k, x*@k, (@h-(y+h))*@k, info['i'])); + if (link) + Link(x, y, w, h, link); + end + + #2002-07-31 - Nicola Asuni + # set right-bottom corner coordinates + @img_rb_x = x + w; + @img_rb_y = y + h; + end + alias_method :image, :Image + + # + # Performs a line break. The current abscissa goes back to the left margin and the ordinate increases by the amount passed in parameter. + # @param float :h The height of the break. By default, the value equals the height of the last printed cell. + # @since 1.0 + # @see Cell() + # + def Ln(h='') + #Line feed; default value is last cell height + @x=@l_margin; + if (h.is_a?(String)) + @y += @lasth; + else + @y += h; + end + + k=@k; + if (@y > @page_break_trigger and !@in_footer and AcceptPageBreak()) + #Automatic page break + if @pages[@page+1].nil? + x = @x; + ws = @ws; + if (ws > 0) + @ws = 0; + out('0 Tw'); + end + AddPage(@cur_orientation); + @x = x; + if (ws > 0) + @ws = ws; + out(sprintf('%.3f Tw', ws * k)); + end + else + @page += 1; + @y=@t_margin; + end + end + + end + alias_method :ln, :Ln + + # + # Returns the abscissa of the current position. + # @return float + # @since 1.2 + # @see SetX(), GetY(), SetY() + # + def GetX() + #Get x position + return @x; + end + alias_method :get_x, :GetX + + # + # Defines the abscissa of the current position. If the passed value is negative, it is relative to the right of the page. + # @param float :x The value of the abscissa. + # @since 1.2 + # @see GetX(), GetY(), SetY(), SetXY() + # + def SetX(x) + #Set x position + if (x>=0) + @x = x; + else + @x=@w+x; + end + end + alias_method :set_x, :SetX + + # + # Returns the ordinate of the current position. + # @return float + # @since 1.0 + # @see SetY(), GetX(), SetX() + # + def GetY() + #Get y position + return @y; + end + alias_method :get_y, :GetY + + # + # Moves the current abscissa back to the left margin and sets the ordinate. If the passed value is negative, it is relative to the bottom of the page. + # @param float :y The value of the ordinate. + # @since 1.0 + # @see GetX(), GetY(), SetY(), SetXY() + # + def SetY(y) + #Set y position and reset x + @x=@l_margin; + if (y>=0) + @y = y; + else + @y=@h+y; + end + end + alias_method :set_y, :SetY + + # + # Defines the abscissa and ordinate of the current position. If the passed values are negative, they are relative respectively to the right and bottom of the page. + # @param float :x The value of the abscissa + # @param float :y The value of the ordinate + # @since 1.2 + # @see SetX(), SetY() + # + def SetXY(x, y) + #Set x and y positions + SetY(y); + SetX(x); + end + alias_method :set_xy, :SetXY + + # + # Send the document to a given destination: string, local file or browser. In the last case, the plug-in may be used (if present) or a download ("Save as" dialog box) may be forced.
    + # The method first calls Close() if necessary to terminate the document. + # @param string :name The name of the file. If not given, the document will be sent to the browser (destination I) with the name doc.pdf. + # @param string :dest Destination where to send the document. It can take one of the following values:
    • I: send the file inline to the browser. The plug-in is used if available. The name given by name is used when one selects the "Save as" option on the link generating the PDF.
    • D: send to the browser and force a file download with the name given by name.
    • F: save to a local file with the name given by name.
    • S: return the document as a string. name is ignored.
    If the parameter is not specified but a name is given, destination is F. If no parameter is specified at all, destination is I.
    + # @since 1.0 + # @see Close() + # + def Output(name='', dest='') + #Output PDF to some destination + #Finish document if necessary + if (@state < 3) + Close(); + end + #Normalize parameters + # Boolean no longer supported + # if (dest.is_a?(Boolean)) + # dest = dest ? 'D' : 'F'; + # end + dest = dest.upcase + if (dest=='') + if (name=='') + name='doc.pdf'; + dest='I'; + else + dest='F'; + end + end + case (dest) + when 'I' + # This is PHP specific code + ##Send to standard output + # if (ob_get_contents()) + # Error('Some data has already been output, can\'t send PDF file'); + # end + # if (php_sapi_name()!='cli') + # #We send to a browser + # header('Content-Type: application/pdf'); + # if (headers_sent()) + # Error('Some data has already been output to browser, can\'t send PDF file'); + # end + # header('Content-Length: ' + @buffer.length); + # header('Content-disposition: inline; filename="' + name + '"'); + # end + return @buffer; + + when 'D' + # PHP specific + #Download file + # if (ob_get_contents()) + # Error('Some data has already been output, can\'t send PDF file'); + # end + # if (!_SERVER['HTTP_USER_AGENT'].nil? && SERVER['HTTP_USER_AGENT'].include?('MSIE')) + # header('Content-Type: application/force-download'); + # else + # header('Content-Type: application/octet-stream'); + # end + # if (headers_sent()) + # Error('Some data has already been output to browser, can\'t send PDF file'); + # end + # header('Content-Length: '+ @buffer.length); + # header('Content-disposition: attachment; filename="' + name + '"'); + return @buffer; + + when 'F' + open(name,'wb') do |f| + f.write(@buffer) + end + # PHP code + # #Save to local file + # f=open(name,'wb'); + # if (!f) + # Error('Unable to create output file: ' + name); + # end + # fwrite(f,@buffer,@buffer.length); + # f.close + + when 'S' + #Return as a string + return @buffer; + else + Error('Incorrect output destination: ' + dest); + + end + return ''; + end + alias_method :output, :Output + + # Protected methods + + # + # Check for locale-related bug + # @access protected + # + def dochecks() + #Check for locale-related bug + if (1.1==1) + Error('Don\'t alter the locale before including class file'); + end + #Check for decimal separator + if (sprintf('%.1f',1.0)!='1.0') + setlocale(LC_NUMERIC,'C'); + end + end + + # + # Return fonts path + # @access protected + # + def getfontpath(file) + # Is it in the @@font_path? + if @@font_path + fpath = File.join @@font_path, file + if File.exists?(fpath) + return fpath + end + end + # Is it in this plugin's font folder? + fpath = File.join File.dirname(__FILE__), 'fonts', file + if File.exists?(fpath) + return fpath + end + # Could not find it. + nil + end + + # + # Start document + # @access protected + # + def begindoc() + #Start document + @state=1; + out('%PDF-1.3'); + end + + # + # putpages + # @access protected + # + def putpages() + nb = @page; + if (@alias_nb_pages) + nbstr = UTF8ToUTF16BE(nb.to_s, false); + #Replace number of pages + 1.upto(nb) do |n| + @pages[n].gsub!(@alias_nb_pages, nbstr) + end + end + if @def_orientation=='P' + w_pt=@fw_pt + h_pt=@fh_pt + else + w_pt=@fh_pt + h_pt=@fw_pt + end + filter=(@compress) ? '/Filter /FlateDecode ' : '' + 1.upto(nb) do |n| + #Page + newobj + out('<>>>'; + else + l=@links[pl[4]]; + h=!@orientation_changes[l[0]].nil? ? w_pt : h_pt; + annots<>',1+2*l[0], h-l[1]*@k); + end + end + out(annots + ']'); + end + out('/Contents ' + (@n+1).to_s + ' 0 R>>'); + out('endobj'); + #Page content + p=(@compress) ? gzcompress(@pages[n]) : @pages[n]; + newobj(); + out('<<' + filter + '/Length '+ p.length.to_s + '>>'); + putstream(p); + out('endobj'); + end + #Pages root + @offsets[1]=@buffer.length; + out('1 0 obj'); + out('<>'); + out('endobj'); + end + + # + # Adds fonts + # putfonts + # @access protected + # + def putfonts() + nf=@n; + @diffs.each do |diff| + #Encodings + newobj(); + out('<>'); + out('endobj'); + end + @font_files.each do |file, info| + #Font file embedding + newobj(); + @font_files[file]['n']=@n; + font=''; + open(getfontpath(file),'rb') do |f| + font = f.read(); + end + compressed=(file[-2,2]=='.z'); + if (!compressed && !info['length2'].nil?) + header=((font[0][0])==128); + if (header) + #Strip first binary header + font=font[6]; + end + if header && (font[info['length1']][0] == 128) + #Strip second binary header + font=font[0..info['length1']] + font[info['length1']+6]; + end + end + out('<>'); + open(getfontpath(file),'rb') do |f| + putstream(font) + end + out('endobj'); + end + @fonts.each do |k, font| + #Font objects + @fonts[k]['n']=@n+1; + type = font['type']; + name = font['name']; + if (type=='core') + #Standard font + newobj(); + out('<>'); + out('endobj'); + elsif type == 'Type0' + putType0(font) + elsif (type=='Type1' || type=='TrueType') + #Additional Type1 or TrueType font + newobj(); + out('<>'); + out('endobj'); + #Widths + newobj(); + cw=font['cw']; # & + s='['; + 32.upto(255) do |i| + s << cw[i.chr] + ' '; + end + out(s + ']'); + out('endobj'); + #Descriptor + newobj(); + s='<>'); + out('endobj'); + else + #Allow for additional types + mtd='put' + type.downcase; + if (!self.respond_to?(mtd)) + Error('Unsupported font type: ' + type) + else + self.send(mtd,font) + end + end + end + end + + def putType0(font) + #Type0 + newobj(); + out('<>') + out('endobj') + #CIDFont + newobj() + out('<>') + out('/FontDescriptor '+(@n+1).to_s+' 0 R') + w='/W [1 [' + font['cw'].keys.sort.each {|key| + w+=font['cw'][key].to_s + " " +# ActionController::Base::logger.debug key.to_s +# ActionController::Base::logger.debug font['cw'][key].to_s + } + out(w+'] 231 325 500 631 [500] 326 389 500]') + out('>>') + out('endobj') + #Font descriptor + newobj() + out('<>') + out('endobj') + end + + # + # putimages + # @access protected + # + def putimages() + filter=(@compress) ? '/Filter /FlateDecode ' : ''; + @images.each do |file, info| # was while(list(file, info)=each(@images)) + newobj(); + @images[file]['n']=@n; + out('<>'); + putstream(info['data']); + @images[file]['data']=nil + out('endobj'); + #Palette + if (info['cs']=='Indexed') + newobj(); + pal=(@compress) ? gzcompress(info['pal']) : info['pal']; + out('<<' + filter + '/Length ' + pal.length.to_s + '>>'); + putstream(pal); + out('endobj'); + end + end + end + + # + # putxobjectdict + # @access protected + # + def putxobjectdict() + @images.each_value do |image| + out('/I' + image['i'].to_s + ' ' + image['n'].to_s + ' 0 R'); + end + end + + # + # putresourcedict + # @access protected + # + def putresourcedict() + out('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); + out('/Font <<'); + @fonts.each_value do |font| + out('/F' + font['i'].to_s + ' ' + font['n'].to_s + ' 0 R'); + end + out('>>'); + out('/XObject <<'); + putxobjectdict(); + out('>>'); + end + + # + # putresources + # @access protected + # + def putresources() + putfonts(); + putimages(); + #Resource dictionary + @offsets[2]=@buffer.length; + out('2 0 obj'); + out('<<'); + putresourcedict(); + out('>>'); + out('endobj'); + end + + # + # putinfo + # @access protected + # + def putinfo() + out('/Producer ' + textstring(PDF_PRODUCER)); + if (!@title.nil?) + out('/Title ' + textstring(@title)); + end + if (!@subject.nil?) + out('/Subject ' + textstring(@subject)); + end + if (!@author.nil?) + out('/Author ' + textstring(@author)); + end + if (!@keywords.nil?) + out('/Keywords ' + textstring(@keywords)); + end + if (!@creator.nil?) + out('/Creator ' + textstring(@creator)); + end + out('/CreationDate ' + textstring('D:' + Time.now.strftime('%Y%m%d%H%M%S'))); + end + + # + # putcatalog + # @access protected + # + def putcatalog() + out('/Type /Catalog'); + out('/Pages 1 0 R'); + if (@zoom_mode=='fullpage') + out('/OpenAction [3 0 R /Fit]'); + elsif (@zoom_mode=='fullwidth') + out('/OpenAction [3 0 R /FitH null]'); + elsif (@zoom_mode=='real') + out('/OpenAction [3 0 R /XYZ null null 1]'); + elsif (!@zoom_mode.is_a?(String)) + out('/OpenAction [3 0 R /XYZ null null ' + (@zoom_mode/100) + ']'); + end + if (@layout_mode=='single') + out('/PageLayout /SinglePage'); + elsif (@layout_mode=='continuous') + out('/PageLayout /OneColumn'); + elsif (@layout_mode=='two') + out('/PageLayout /TwoColumnLeft'); + end + end + + # + # puttrailer + # @access protected + # + def puttrailer() + out('/Size ' + (@n+1).to_s); + out('/Root ' + @n.to_s + ' 0 R'); + out('/Info ' + (@n-1).to_s + ' 0 R'); + end + + # + # putheader + # @access protected + # + def putheader() + out('%PDF-' + @pdf_version); + end + + # + # enddoc + # @access protected + # + def enddoc() + putheader(); + putpages(); + putresources(); + #Info + newobj(); + out('<<'); + putinfo(); + out('>>'); + out('endobj'); + #Catalog + newobj(); + out('<<'); + putcatalog(); + out('>>'); + out('endobj'); + #Cross-ref + o=@buffer.length; + out('xref'); + out('0 ' + (@n+1).to_s); + out('0000000000 65535 f '); + 1.upto(@n) do |i| + out(sprintf('%010d 00000 n ',@offsets[i])); + end + #Trailer + out('trailer'); + out('<<'); + puttrailer(); + out('>>'); + out('startxref'); + out(o); + out('%%EOF'); + @state=3; + end + + # + # beginpage + # @access protected + # + def beginpage(orientation) + @page += 1; + @pages[@page]=''; + @state=2; + @x=@l_margin; + @y=@t_margin; + @font_family=''; + #Page orientation + if (orientation.empty?) + orientation=@def_orientation; + else + orientation.upcase! + if (orientation!=@def_orientation) + @orientation_changes[@page]=true; + end + end + if (orientation!=@cur_orientation) + #Change orientation + if (orientation=='P') + @w_pt=@fw_pt; + @h_pt=@fh_pt; + @w=@fw; + @h=@fh; + else + @w_pt=@fh_pt; + @h_pt=@fw_pt; + @w=@fh; + @h=@fw; + end + @page_break_trigger=@h-@b_margin; + @cur_orientation = orientation; + end + end + + # + # End of page contents + # @access protected + # + def endpage() + @state=1; + end + + # + # Begin a new object + # @access protected + # + def newobj() + @n += 1; + @offsets[@n]=@buffer.length; + out(@n.to_s + ' 0 obj'); + end + + # + # Underline and Deleted text + # @access protected + # + def dolinetxt(x, y, txt) + up = @current_font['up']; + ut = @current_font['ut']; + w = GetStringWidth(txt) + @ws * txt.count(' '); + sprintf('%.2f %.2f %.2f %.2f re f', x * @k, (@h - (y - up / 1000.0 * @font_size)) * @k, w * @k, -ut / 1000.0 * @font_size_pt); + end + + # + # Extract info from a JPEG file + # @access protected + # + def parsejpg(file) + a=getimagesize(file); + if (a.empty?) + Error('Missing or incorrect image file: ' + file); + end + if (!a[2].nil? and a[2]!='JPEG') + Error('Not a JPEG file: ' + file); + end + if (a['channels'].nil? or a['channels']==3) + colspace='DeviceRGB'; + elsif (a['channels']==4) + colspace='DeviceCMYK'; + else + colspace='DeviceGray'; + end + bpc=!a['bits'].nil? ? a['bits'] : 8; + #Read whole file + data=''; + + open(file,'rb') do |f| + data< a[0],'h' => a[1],'cs' => colspace,'bpc' => bpc,'f'=>'DCTDecode','data' => data} + end + + def imageToPNG(file) + return unless Object.const_defined?(:Magick) + + img = Magick::ImageList.new(file) + img.format = 'PNG' # convert to PNG from gif + img.opacity = 0 # PNG alpha channel delete + + #use a temporary file.... + tmpFile = Tempfile.new(['', '_' + File::basename(file) + '.png'], @@k_path_cache); + tmpFile.binmode + tmpFile.print img.to_blob + tmpFile + ensure + tmpFile.close + end + + # + # Extract info from a PNG file + # @access protected + # + def parsepng(file) + f=open(file,'rb'); + #Check signature + if (f.read(8)!=137.chr + 'PNG' + 13.chr + 10.chr + 26.chr + 10.chr) + Error('Not a PNG file: ' + file); + end + #Read header chunk + f.read(4); + if (f.read(4)!='IHDR') + Error('Incorrect PNG file: ' + file); + end + w=freadint(f); + h=freadint(f); + bpc=f.read(1).unpack('C')[0]; + if (bpc>8) + Error('16-bit depth not supported: ' + file); + end + ct=f.read(1).unpack('C')[0]; + if (ct==0) + colspace='DeviceGray'; + elsif (ct==2) + colspace='DeviceRGB'; + elsif (ct==3) + colspace='Indexed'; + else + Error('Alpha channel not supported: ' + file); + end + if (f.read(1).unpack('C')[0] != 0) + Error('Unknown compression method: ' + file); + end + if (f.read(1).unpack('C')[0] != 0) + Error('Unknown filter method: ' + file); + end + if (f.read(1).unpack('C')[0] != 0) + Error('Interlacing not supported: ' + file); + end + f.read(4); + parms='/DecodeParms <>'; + #Scan chunks looking for palette, transparency and image data + pal=''; + trns=''; + data=''; + begin + n=freadint(f); + type=f.read(4); + if (type=='PLTE') + #Read palette + pal=f.read( n); + f.read(4); + elsif (type=='tRNS') + #Read transparency info + t=f.read( n); + if (ct==0) + trns = t[1].unpack('C')[0] + elsif (ct==2) + trns = t[[1].unpack('C')[0], t[3].unpack('C')[0], t[5].unpack('C')[0]] + else + pos=t.index(0.chr); + unless (pos.nil?) + trns = [pos] + end + end + f.read(4); + elsif (type=='IDAT') + #Read image data block + data< w, 'h' => h, 'cs' => colspace, 'bpc' => bpc, 'f'=>'FlateDecode', 'parms' => parms, 'pal' => pal, 'trns' => trns, 'data' => data} + ensure + f.close + end + + # + # Read a 4-byte integer from file + # @access protected + # + def freadint(f) + # Read a 4-byte integer from file + a = f.read(4).unpack('N') + return a[0] + end + + # + # Format a text string + # @access protected + # + def textstring(s) + if (@is_unicode) + #Convert string to UTF-16BE + s = UTF8ToUTF16BE(s, true); + end + return '(' + escape(s) + ')'; + end + + # + # Format a text string + # @access protected + # + def escapetext(s) + if (@is_unicode) + #Convert string to UTF-16BE + s = UTF8ToUTF16BE(s, false); + end + return escape(s); + end + + # + # Add \ before \, ( and ) + # @access protected + # + def escape(s) + # Add \ before \, ( and ) + s.gsub('\\','\\\\\\').gsub('(','\\(').gsub(')','\\)').gsub(13.chr, '\r') + end + + # + # + # @access protected + # + def putstream(s) + out('stream'); + out(s); + out('endstream'); + end + + # + # Add a line to the document + # @access protected + # + def out(s) + if (@state==2) + @pages[@page] << s.to_s + "\n"; + else + @buffer << s.to_s + "\n"; + end + end + + # + # Adds unicode fonts.
    + # Based on PDF Reference 1.3 (section 5) + # @access protected + # @author Nicola Asuni + # @since 1.52.0.TC005 (2005-01-05) + # + def puttruetypeunicode(font) + # Type0 Font + # A composite font composed of other fonts, organized hierarchically + newobj(); + out('<>'); + out('endobj'); + + # CIDFontType2 + # A CIDFont whose glyph descriptions are based on TrueType font technology + newobj(); + out('<>'); + out('endobj'); + + # ToUnicode + # is a stream object that contains the definition of the CMap + # (PDF Reference 1.3 chap. 5.9) + newobj(); + out('<>'); + out('stream'); + out('/CIDInit /ProcSet findresource begin'); + out('12 dict begin'); + out('begincmap'); + out('/CIDSystemInfo'); + out('<> def'); + out('/CMapName /Adobe-Identity-UCS def'); + out('/CMapType 2 def'); + out('1 begincodespacerange'); + out('<0000> '); + out('endcodespacerange'); + out('1 beginbfrange'); + out('<0000> <0000>'); + out('endbfrange'); + out('endcmap'); + out('CMapName currentdict /CMap defineresource pop'); + out('end'); + out('end'); + out('endstream'); + out('endobj'); + + # CIDSystemInfo dictionary + # A dictionary containing entries that define the character collection of the CIDFont. + newobj(); + out('<>'); + out('endobj'); + + # Font descriptor + # A font descriptor describing the CIDFont default metrics other than its glyph widths + newobj(); + out('<>'); + out('endobj'); + + # Embed CIDToGIDMap + # A specification of the mapping from CIDs to glyph indices + newobj(); + ctgfile = getfontpath(font['ctg']) + if (!ctgfile) + Error('Font file not found: ' + ctgfile); + end + size = File.size(ctgfile); + out('<>'); + open(ctgfile, "rb") do |f| + putstream(f.read()) + end + out('endobj'); + end + + # + # Converts UTF-8 strings to codepoints array.
    + # Invalid byte sequences will be replaced with 0xFFFD (replacement character)
    + # Based on: http://www.faqs.org/rfcs/rfc3629.html + #
    +	# 	  Char. number range  |        UTF-8 octet sequence
    +	#       (hexadecimal)    |              (binary)
    +	#    --------------------+-----------------------------------------------
    +	#    0000 0000-0000 007F | 0xxxxxxx
    +	#    0000 0080-0000 07FF | 110xxxxx 10xxxxxx
    +	#    0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
    +	#    0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
    +	#    ---------------------------------------------------------------------
    +	#
    +	#   ABFN notation:
    +	#   ---------------------------------------------------------------------
    +	#   UTF8-octets =#( UTF8-char )
    +	#   UTF8-char   = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
    +	#   UTF8-1      = %x00-7F
    +	#   UTF8-2      = %xC2-DF UTF8-tail
    +	#
    +	#   UTF8-3      = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /
    +	#                 %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
    +	#   UTF8-4      = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /
    +	#                 %xF4 %x80-8F 2( UTF8-tail )
    +	#   UTF8-tail   = %x80-BF
    +	#   ---------------------------------------------------------------------
    +	# 
    + # @param string :str string to process. + # @return array containing codepoints (UTF-8 characters values) + # @access protected + # @author Nicola Asuni + # @since 1.53.0.TC005 (2005-01-05) + # + def UTF8StringToArray(str) + if (!@is_unicode) + return str; # string is not in unicode + end + + unicode = [] # array containing unicode values + bytes = [] # array containing single character byte sequences + numbytes = 1; # number of octetc needed to represent the UTF-8 character + + str = str.to_s; # force :str to be a string + + str.each_byte do |char| + if (bytes.length == 0) # get starting octect + if (char <= 0x7F) + unicode << char # use the character "as is" because is ASCII + numbytes = 1 + elsif ((char >> 0x05) == 0x06) # 2 bytes character (0x06 = 110 BIN) + bytes << ((char - 0xC0) << 0x06) + numbytes = 2 + elsif ((char >> 0x04) == 0x0E) # 3 bytes character (0x0E = 1110 BIN) + bytes << ((char - 0xE0) << 0x0C) + numbytes = 3 + elsif ((char >> 0x03) == 0x1E) # 4 bytes character (0x1E = 11110 BIN) + bytes << ((char - 0xF0) << 0x12) + numbytes = 4 + else + # use replacement character for other invalid sequences + unicode << 0xFFFD + bytes = [] + numbytes = 1 + end + elsif ((char >> 0x06) == 0x02) # bytes 2, 3 and 4 must start with 0x02 = 10 BIN + bytes << (char - 0x80) + if (bytes.length == numbytes) + # compose UTF-8 bytes to a single unicode value + char = bytes[0] + 1.upto(numbytes-1) do |j| + char += (bytes[j] << ((numbytes - j - 1) * 0x06)) + end + if (((char >= 0xD800) and (char <= 0xDFFF)) or (char >= 0x10FFFF)) + # The definition of UTF-8 prohibits encoding character numbers between + # U+D800 and U+DFFF, which are reserved for use with the UTF-16 + # encoding form (as surrogate pairs) and do not directly represent + # characters + unicode << 0xFFFD; # use replacement character + else + unicode << char; # add char to array + end + # reset data for next char + bytes = [] + numbytes = 1; + end + else + # use replacement character for other invalid sequences + unicode << 0xFFFD; + bytes = [] + numbytes = 1; + end + end + return unicode; + end + + # + # Converts UTF-8 strings to UTF16-BE.
    + # Based on: http://www.faqs.org/rfcs/rfc2781.html + #
    +	#   Encoding UTF-16:
    +	# 
    +		#   Encoding of a single character from an ISO 10646 character value to
    +	#    UTF-16 proceeds as follows. Let U be the character number, no greater
    +	#    than 0x10FFFF.
    +	# 
    +	#    1) If U < 0x10000, encode U as a 16-bit unsigned integer and
    +	#       terminate.
    +	# 
    +	#    2) Let U' = U - 0x10000. Because U is less than or equal to 0x10FFFF,
    +	#       U' must be less than or equal to 0xFFFFF. That is, U' can be
    +	#       represented in 20 bits.
    +	# 
    +	#    3) Initialize two 16-bit unsigned integers, W1 and W2, to 0xD800 and
    +	#       0xDC00, respectively. These integers each have 10 bits free to
    +	#       encode the character value, for a total of 20 bits.
    +	# 
    +	#    4) Assign the 10 high-order bits of the 20-bit U' to the 10 low-order
    +	#       bits of W1 and the 10 low-order bits of U' to the 10 low-order
    +	#       bits of W2. Terminate.
    +	# 
    +	#    Graphically, steps 2 through 4 look like:
    +	#    U' = yyyyyyyyyyxxxxxxxxxx
    +	#    W1 = 110110yyyyyyyyyy
    +	#    W2 = 110111xxxxxxxxxx
    +	# 
    + # @param string :str string to process. + # @param boolean :setbom if true set the Byte Order Mark (BOM = 0xFEFF) + # @return string + # @access protected + # @author Nicola Asuni + # @since 1.53.0.TC005 (2005-01-05) + # @uses UTF8StringToArray + # + def UTF8ToUTF16BE(str, setbom=true) + if (!@is_unicode) + return str; # string is not in unicode + end + outstr = ""; # string to be returned + unicode = UTF8StringToArray(str); # array containing UTF-8 unicode values + numitems = unicode.length; + + if (setbom) + outstr << "\xFE\xFF"; # Byte Order Mark (BOM) + end + unicode.each do |char| + if (char == 0xFFFD) + outstr << "\xFF\xFD"; # replacement character + elsif (char < 0x10000) + outstr << (char >> 0x08).chr; + outstr << (char & 0xFF).chr; + else + char -= 0x10000; + w1 = 0xD800 | (char >> 0x10); + w2 = 0xDC00 | (char & 0x3FF); + outstr << (w1 >> 0x08).chr; + outstr << (w1 & 0xFF).chr; + outstr << (w2 >> 0x08).chr; + outstr << (w2 & 0xFF).chr; + end + end + return outstr; + end + + # ==================================================== + + # + # Set header font. + # @param array :font font + # @since 1.1 + # + def SetHeaderFont(font) + @header_font = font; + end + alias_method :set_header_font, :SetHeaderFont + + # + # Set footer font. + # @param array :font font + # @since 1.1 + # + def SetFooterFont(font) + @footer_font = font; + end + alias_method :set_footer_font, :SetFooterFont + + # + # Set language array. + # @param array :language + # @since 1.1 + # + def SetLanguageArray(language) + @l = language; + end + alias_method :set_language_array, :SetLanguageArray + # + # Set document barcode. + # @param string :bc barcode + # + def SetBarcode(bc="") + @barcode = bc; + end + + # + # Print Barcode. + # @param int :x x position in user units + # @param int :y y position in user units + # @param int :w width in user units + # @param int :h height position in user units + # @param string :type type of barcode (I25, C128A, C128B, C128C, C39) + # @param string :style barcode style + # @param string :font font for text + # @param int :xres x resolution + # @param string :code code to print + # + def writeBarcode(x, y, w, h, type, style, font, xres, code) + require(File.dirname(__FILE__) + "/barcode/barcode.rb"); + require(File.dirname(__FILE__) + "/barcode/i25object.rb"); + require(File.dirname(__FILE__) + "/barcode/c39object.rb"); + require(File.dirname(__FILE__) + "/barcode/c128aobject.rb"); + require(File.dirname(__FILE__) + "/barcode/c128bobject.rb"); + require(File.dirname(__FILE__) + "/barcode/c128cobject.rb"); + + if (code.empty?) + return; + end + + if (style.empty?) + style = BCS_ALIGN_LEFT; + style |= BCS_IMAGE_PNG; + style |= BCS_TRANSPARENT; + #:style |= BCS_BORDER; + #:style |= BCS_DRAW_TEXT; + #:style |= BCS_STRETCH_TEXT; + #:style |= BCS_REVERSE_COLOR; + end + if (font.empty?) then font = BCD_DEFAULT_FONT; end + if (xres.empty?) then xres = BCD_DEFAULT_XRES; end + + scale_factor = 1.5 * xres * @k; + bc_w = (w * scale_factor).round #width in points + bc_h = (h * scale_factor).round #height in points + + case (type.upcase) + when "I25" + obj = I25Object.new(bc_w, bc_h, style, code); + when "C128A" + obj = C128AObject.new(bc_w, bc_h, style, code); + when "C128B" + obj = C128BObject.new(bc_w, bc_h, style, code); + when "C128C" + obj = C128CObject.new(bc_w, bc_h, style, code); + when "C39" + obj = C39Object.new(bc_w, bc_h, style, code); + end + + obj.SetFont(font); + obj.DrawObject(xres); + + #use a temporary file.... + tmpName = tempnam(@@k_path_cache,'img'); + imagepng(obj.getImage(), tmpName); + Image(tmpName, x, y, w, h, 'png'); + obj.DestroyObject(); + obj = nil + unlink(tmpName); + end + + # + # Returns the PDF data. + # + def GetPDFData() + if (@state < 3) + Close(); + end + return @buffer; + end + + # --- HTML PARSER FUNCTIONS --- + + # + # Allows to preserve some HTML formatting.
    + # Supports: h1, h2, h3, h4, h5, h6, b, u, i, a, img, p, br, strong, em, ins, del, font, blockquote, li, ul, ol, hr, td, th, tr, table, sup, sub, small + # @param string :html text to display + # @param boolean :ln if true add a new line after text (default = true) + # @param int :fill Indicates if the background must be painted (1) or transparent (0). Default value: 0. + # + def writeHTML(html, ln=true, fill=0, h=0) + + @lasth = h if h > 0 + if (@lasth == 0) + #set row height + @lasth = @font_size * @@k_cell_height_ratio; + end + + @href = nil + @style = ""; + @t_cells = [[]]; + @table_id = 0; + + # pre calculate + html.split(/(<[^>]+>)/).each do |element| + if "<" == element[0,1] + #Tag + if (element[1, 1] == '/') + closedHTMLTagCalc(element[2..-2].downcase); + else + #Extract attributes + # get tag name + tag = element.scan(/([a-zA-Z0-9]*)/).flatten.delete_if {|x| x.length == 0} + tag = tag[0].to_s.downcase; + + # get attributes + attr_array = element.scan(/([^=\s]*)=["\']?([^"\']*)["\']?/) + attrs = {} + attr_array.each do |name, value| + attrs[name.downcase] = value; + end + openHTMLTagCalc(tag, attrs); + end + end + end + @table_id = 0; + + html.split(/(<[A-Za-z!?\/][^>]*?>)/).each do |element| + if "<" == element[0,1] + #Tag + if (element[1, 1] == '/') + closedHTMLTagHandler(element[2..-2].downcase); + else + #Extract attributes + # get tag name + tag = element.scan(/([a-zA-Z0-9]*)/).flatten.delete_if {|x| x.length == 0} + tag = tag[0].to_s.downcase; + + # get attributes + attr_array = element.scan(/([^=\s]*)=["\']?([^"\']*)["\']?/) + attrs = {} + attr_array.each do |name, value| + attrs[name.downcase] = value; + end + openHTMLTagHandler(tag, attrs, fill); + end + + else + #Text + if (@tdbegin) + element.gsub!(/[\t\r\n\f]/, ""); + @tdtext << element.gsub(/ /, " "); + elsif (@href) + element.gsub!(/[\t\r\n\f]/, ""); + addHtmlLink(@href, element, fill); + elsif (@pre_state == true and element.length > 0) + Write(@lasth, unhtmlentities(element), '', fill); + elsif (element.strip.length > 0) + element.gsub!(/[\t\r\n\f]/, ""); + element.gsub!(/ /, " "); + Write(@lasth, unhtmlentities(element), '', fill); + end + end + end + + if (ln) + Ln(@lasth); + end + end + alias_method :write_html, :writeHTML + + # + # Prints a cell (rectangular area) with optional borders, background color and html text string. The upper-left corner of the cell corresponds to the current position. After the call, the current position moves to the right or to the next line.
    + # If automatic page breaking is enabled and the cell goes beyond the limit, a page break is done before outputting. + # @param float :w Cell width. If 0, the cell extends up to the right margin. + # @param float :h Cell minimum height. The cell extends automatically if needed. + # @param float :x upper-left corner X coordinate + # @param float :y upper-left corner Y coordinate + # @param string :html html text to print. Default value: empty string. + # @param mixed :border Indicates if borders must be drawn around the cell. The value can be either a number:
    • 0: no border (default)
    • 1: frame
    or a string containing some or all of the following characters (in any order):
    • L: left
    • T: top
    • R: right
    • B: bottom
    + # @param int :ln Indicates where the current position should go after the call. Possible values are:
    • 0: to the right
    • 1: to the beginning of the next line
    • 2: below
    +# Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: 0. + # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. + # @see Cell() + # + def writeHTMLCell(w, h, x, y, html='', border=0, ln=1, fill=0) + + if (@lasth == 0) + #set row height + @lasth = @font_size * @@k_cell_height_ratio; + end + + if (x == 0) + x = GetX(); + end + if (y == 0) + y = GetY(); + end + + # get current page number + pagenum = @page; + + SetX(x); + SetY(y); + + if (w == 0) + w = @fw - x - @r_margin; + end + + b=0; + if (border) + if (border==1) + border='LTRB'; + b='LRT'; + b2='LR'; + elsif border.is_a?(String) + b2=''; + if (border.include?('L')) + b2<<'L'; + end + if (border.include?('R')) + b2<<'R'; + end + b=(border.include?('T')) ? b2 + 'T' : b2; + end + end + + # store original margin values + l_margin = @l_margin; + r_margin = @r_margin; + + # set new margin values + SetLeftMargin(x); + SetRightMargin(@fw - x - w); + + # calculate remaining vertical space on page + restspace = GetPageHeight() - GetY() - GetBreakMargin(); + + writeHTML(html, true, fill); # write html text + SetX(x) + + currentY = GetY(); + @auto_page_break = false; + # check if a new page has been created + if (@page > pagenum) + # design a cell around the text on first page + currentpage = @page; + @page = pagenum; + SetY(GetPageHeight() - restspace - GetBreakMargin()); + SetX(x) + Cell(w, restspace - 1, "", b, 0, 'L', 0); + b = b2; + @page += 1; + while @page < currentpage + SetY(@t_margin); # put cursor at the beginning of text + SetX(x) + Cell(w, @page_break_trigger - @t_margin, "", b, 0, 'L', 0); + @page += 1; + end + if (border.is_a?(String) and border.include?('B')) + b<<'B'; + end + # design a cell around the text on last page + SetY(@t_margin); # put cursor at the beginning of text + SetX(x) + Cell(w, currentY - @t_margin, "", b, 0, 'L', 0); + else + SetY(y); # put cursor at the beginning of text + # design a cell around the text + SetX(x) + Cell(w, [h, (currentY - y)].max, "", border, 0, 'L', 0); + end + @auto_page_break = true; + + # restore original margin values + SetLeftMargin(l_margin); + SetRightMargin(r_margin); + + @lasth = h + + # move cursor to specified position + if (ln == 0) + # go to the top-right of the cell + @x = x + w; + @y = y; + elsif (ln == 1) + # go to the beginning of the next line + @x = @l_margin; + @y = currentY; + elsif (ln == 2) + # go to the bottom-left of the cell (below) + @x = x; + @y = currentY; + end + end + alias_method :write_html_cell, :writeHTMLCell + + # + # Check html table tag position. + # + # @param array :table potision array + # @param int :current tr tag id number + # @param int :current td tag id number + # @access private + # @return int : next td_id position. + # value 0 mean that can use position. + # + def checkTableBlockingCellPosition(table, tr_id, td_id ) + 0.upto(tr_id) do |j| + 0.upto(@t_cells[table][j].size - 1) do |i| + if @t_cells[table][j][i]['i0'] <= td_id and td_id <= @t_cells[table][j][i]['i1'] + if @t_cells[table][j][i]['j0'] <= tr_id and tr_id <= @t_cells[table][j][i]['j1'] + return @t_cells[table][j][i]['i1'] - td_id + 1; + end + end + end + end + return 0; + end + + # + # Calculate opening tags. + # + # html table cell array : @t_cells + # + # i0: table cell start position + # i1: table cell end position + # j0: table row start position + # j1: table row end position + # + # +------+ + # |i0,j0 | + # | i1,j1| + # +------+ + # + # example html: + # + # + # + # + # + #
    + # + # i: 0 1 2 + # j+----+----+----+ + # :|0,0 |1,0 |2,0 | + # 0| 0,0| 1,0| 2,0| + # +----+----+----+ + # |0,1 |2,1 | + # 1| 1,1| 2,1| + # +----+----+----+ + # |0,2 |1,2 |2,2 | + # 2| | 1,2| 2,2| + # + +----+----+ + # | |1,3 |2,3 | + # 3| 0,3| 1,3| 2,3| + # +----+----+----+ + # + # html table cell array : + # [[[i0=>0,j0=>0,i1=>0,j1=>0],[i0=>1,j0=>0,i1=>1,j1=>0],[i0=>2,j0=>0,i1=>2,j1=>0]], + # [[i0=>0,j0=>1,i1=>1,j1=>1],[i0=>2,j0=>1,i1=>2,j1=>1]], + # [[i0=>0,j0=>2,i1=>0,j1=>3],[i0=>1,j0=>2,i1=>1,j1=>2],[i0=>2,j0=>2,i1=>2,j1=>2]] + # [[i0=>1,j0=>3,i1=>1,j1=>3],[i0=>2,j0=>3,i1=>2,j1=>3]]] + # + # @param string :tag tag name (in upcase) + # @param string :attr tag attribute (in upcase) + # @access private + # + def openHTMLTagCalc(tag, attrs) + #Opening tag + case (tag) + when 'table' + @max_table_columns[@table_id] = 0; + @t_columns = 0; + @tr_id = -1; + when 'tr' + if @max_table_columns[@table_id] < @t_columns + @max_table_columns[@table_id] = @t_columns; + end + @t_columns = 0; + @tr_id += 1; + @td_id = -1; + @t_cells[@table_id].push [] + when 'td', 'th' + @td_id += 1; + if attrs['colspan'].nil? or attrs['colspan'] == '' + colspan = 1; + else + colspan = attrs['colspan'].to_i; + end + if attrs['rowspan'].nil? or attrs['rowspan'] == '' + rowspan = 1; + else + rowspan = attrs['rowspan'].to_i; + end + + i = 0; + while true + next_i_distance = checkTableBlockingCellPosition(@table_id, @tr_id, @td_id + i); + if next_i_distance == 0 + @t_cells[@table_id][@tr_id].push "i0"=>@td_id + i, "j0"=>@tr_id, "i1"=>(@td_id + i + colspan - 1), "j1"=>@tr_id + rowspan - 1 + break; + end + i += next_i_distance; + end + + @t_columns += colspan; + end + end + + # + # Calculate closing tags. + # @param string :tag tag name (in upcase) + # @access private + # + def closedHTMLTagCalc(tag) + #Closing tag + case (tag) + when 'table' + if @max_table_columns[@table_id] < @t_columns + @max_table_columns[@table_id] = @t_columns; + end + @table_id += 1; + @t_cells.push [] + end + end + + # + # Convert to accessible file path + # @param string :attrname image file name + # + def getImageFilename( attrname ) + nil + end + + # + # Process opening tags. + # @param string :tag tag name (in upcase) + # @param string :attr tag attribute (in upcase) + # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. + # @access private + # + def openHTMLTagHandler(tag, attrs, fill=0) + #Opening tag + case (tag) + when 'pre' + @pre_state = true; + @l_margin += 5; + @r_margin += 5; + @x += 5; + + when 'table' + Ln(); + if @default_table_columns < @max_table_columns[@table_id] + @table_columns = @max_table_columns[@table_id]; + else + @table_columns = @default_table_columns; + end + @l_margin += 5; + @r_margin += 5; + @x += 5; + + if attrs['border'].nil? or attrs['border'] == '' + @tableborder = 0; + else + @tableborder = attrs['border']; + end + @tr_id = -1; + @max_td_page[0] = @page; + @max_td_y[0] = @y; + + when 'tr', 'td', 'th' + if tag == 'th' + SetStyle('b', true); + @tdalign = "C"; + end + if ((!attrs['width'].nil?) and (attrs['width'] != '')) + @tdwidth = (attrs['width'].to_i/4); + else + @tdwidth = ((@w - @l_margin - @r_margin) / @table_columns); + end + + if tag == 'tr' + @tr_id += 1; + @td_id = -1; + else + @td_id += 1; + @x = @l_margin + @tdwidth * @t_cells[@table_id][@tr_id][@td_id]['i0']; + end + + if attrs['colspan'].nil? or attrs['border'] == '' + @colspan = 1; + else + @colspan = attrs['colspan'].to_i; + end + @tdwidth *= @colspan; + if ((!attrs['height'].nil?) and (attrs['height'] != '')) + @tdheight=(attrs['height'].to_i / @k); + else + @tdheight = @lasth; + end + if ((!attrs['align'].nil?) and (attrs['align'] != '')) + case (attrs['align']) + when 'center' + @tdalign = "C"; + when 'right' + @tdalign = "R"; + when 'left' + @tdalign = "L"; + end + end + if ((!attrs['bgcolor'].nil?) and (attrs['bgcolor'] != '')) + coul = convertColorHexToDec(attrs['bgcolor']); + SetFillColor(coul['R'], coul['G'], coul['B']); + @tdfill=1; + end + @tdbegin=true; + + when 'hr' + margin = 1; + if ((!attrs['width'].nil?) and (attrs['width'] != '')) + hrWidth = attrs['width']; + else + hrWidth = @w - @l_margin - @r_margin - margin; + end + SetLineWidth(0.2); + Line(@x + margin, @y, @x + hrWidth, @y); + Ln(); + + when 'strong' + SetStyle('b', true); + + when 'em' + SetStyle('i', true); + + when 'ins' + SetStyle('u', true); + + when 'del' + SetStyle('d', true); + + when 'b', 'i', 'u' + SetStyle(tag, true); + + when 'a' + @href = attrs['href']; + + when 'img' + if (!attrs['src'].nil?) + # Don't generates image inside table tag + if (@tdbegin) + @tdtext << attrs['src']; + return + end + # Only generates image include a pdf if RMagick is avalaible + unless Object.const_defined?(:Magick) + Write(@lasth, attrs['src'], '', fill); + return + end + file = getImageFilename(attrs['src']) + if (file.nil?) + Write(@lasth, attrs['src'], '', fill); + return + end + + if (attrs['width'].nil?) + attrs['width'] = 0; + end + if (attrs['height'].nil?) + attrs['height'] = 0; + end + + begin + Image(file, GetX(),GetY(), pixelsToMillimeters(attrs['width']), pixelsToMillimeters(attrs['height'])); + #SetX(@img_rb_x); + SetY(@img_rb_y); + rescue => err + logger.error "pdf: Image: error: #{err.message}" + Write(@lasth, attrs['src'], '', fill); + end + end + + when 'ul', 'ol' + if @li_count == 0 + Ln() if @prevquote_count == @quote_count; # insert Ln for keeping quote lines + @prevquote_count = @quote_count; + end + if @li_state == true + Ln(); + @li_state = false; + end + if tag == 'ul' + @list_ordered[@li_count] = false; + else + @list_ordered[@li_count] = true; + end + @list_count[@li_count] = 0; + @li_count += 1 + + when 'li' + Ln() if @li_state == true + if (@list_ordered[@li_count - 1]) + @list_count[@li_count - 1] += 1; + @li_spacer = " " * @li_count + (@list_count[@li_count - 1]).to_s + ". "; + else + #unordered list simbol + @li_spacer = " " * @li_count + "- "; + end + Write(@lasth, @spacer + @li_spacer, '', fill); + @li_state = true; + + when 'blockquote' + if (@quote_count == 0) + SetStyle('i', true); + @l_margin += 5; + else + @l_margin += 5 / 2; + end + @x = @l_margin; + @quote_top[@quote_count] = @y; + @quote_page[@quote_count] = @page; + @quote_count += 1 + when 'br' + if @tdbegin + @tdtext << "\n" + return + end + Ln(); + + if (@li_spacer.length > 0) + @x += GetStringWidth(@li_spacer); + end + + when 'p' + Ln(); + 0.upto(@quote_count - 1) do |i| + if @quote_page[i] == @page; + if @quote_top[i] == @y - @lasth; # fix start line + @quote_top[i] = @y; + end + else + if @quote_page[i] == @page - 1; + @quote_page[i] = @page; # fix start line + @quote_top[i] = @t_margin; + end + end + end + + when 'sup' + currentfont_size = @font_size; + @tempfontsize = @font_size_pt; + SetFontSize(@font_size_pt * @@k_small_ratio); + SetXY(GetX(), GetY() - ((currentfont_size - @font_size)*(@@k_small_ratio))); + + when 'sub' + currentfont_size = @font_size; + @tempfontsize = @font_size_pt; + SetFontSize(@font_size_pt * @@k_small_ratio); + SetXY(GetX(), GetY() + ((currentfont_size - @font_size)*(@@k_small_ratio))); + + when 'small' + currentfont_size = @font_size; + @tempfontsize = @font_size_pt; + SetFontSize(@font_size_pt * @@k_small_ratio); + SetXY(GetX(), GetY() + ((currentfont_size - @font_size)/3)); + + when 'font' + if (!attrs['color'].nil? and attrs['color']!='') + coul = convertColorHexToDec(attrs['color']); + SetTextColor(coul['R'], coul['G'], coul['B']); + @issetcolor=true; + end + if (!attrs['face'].nil? and @fontlist.include?(attrs['face'].downcase)) + SetFont(attrs['face'].downcase); + @issetfont=true; + end + if (!attrs['size'].nil?) + headsize = attrs['size'].to_i; + else + headsize = 0; + end + currentfont_size = @font_size; + @tempfontsize = @font_size_pt; + SetFontSize(@font_size_pt + headsize); + @lasth = @font_size * @@k_cell_height_ratio; + + when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' + Ln(); + headsize = (4 - tag[1,1].to_f) * 2 + @tempfontsize = @font_size_pt; + SetFontSize(@font_size_pt + headsize); + SetStyle('b', true); + @lasth = @font_size * @@k_cell_height_ratio; + + end + end + + # + # Process closing tags. + # @param string :tag tag name (in upcase) + # @access private + # + def closedHTMLTagHandler(tag) + #Closing tag + case (tag) + when 'pre' + @pre_state = false; + @l_margin -= 5; + @r_margin -= 5; + @x = @l_margin; + Ln(); + + when 'td','th' + base_page = @page; + base_x = @x; + base_y = @y; + + MultiCell(@tdwidth, @tdheight, unhtmlentities(@tdtext.strip), @tableborder, @tdalign, @tdfill, 1); + tr_end = @t_cells[@table_id][@tr_id][@td_id]['j1'] + 1; + if @max_td_page[tr_end].nil? or (@max_td_page[tr_end] < @page) + @max_td_page[tr_end] = @page + @max_td_y[tr_end] = @y + elsif (@max_td_page[tr_end] == @page) + @max_td_y[tr_end] = @y if @max_td_y[tr_end].nil? or (@max_td_y[tr_end] < @y) + end + + @page = base_page; + @x = base_x + @tdwidth; + @y = base_y; + @tdtext = ''; + @tdbegin = false; + @tdwidth = 0; + @tdheight = 0; + @tdalign = "L"; + SetStyle('b', false); + @tdfill = 0; + SetFillColor(@prevfill_color[0], @prevfill_color[1], @prevfill_color[2]); + + when 'tr' + @y = @max_td_y[@tr_id + 1]; + @x = @l_margin; + @page = @max_td_page[@tr_id + 1]; + + when 'table' + # Write Table Line + width = (@w - @l_margin - @r_margin) / @table_columns; + 0.upto(@t_cells[@table_id].size - 1) do |j| + 0.upto(@t_cells[@table_id][j].size - 1) do |i| + @page = @max_td_page[j] + i0=@t_cells[@table_id][j][i]['i0']; + j0=@t_cells[@table_id][j][i]['j0']; + i1=@t_cells[@table_id][j][i]['i1']; + j1=@t_cells[@table_id][j][i]['j1']; + + Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * (i1+1), @max_td_y[j0]) # top + if ( @page == @max_td_page[j1 + 1]) + Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * i0, @max_td_y[j1+1]) # left + Line(@l_margin + width * (i1+1), @max_td_y[j0], @l_margin + width * (i1+1), @max_td_y[j1+1]) # right + else + Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * i0, @page_break_trigger) # left + Line(@l_margin + width * (i1+1), @max_td_y[j0], @l_margin + width * (i1+1), @page_break_trigger) # right + @page += 1; + while @page < @max_td_page[j1 + 1] + Line(@l_margin + width * i0, @t_margin, @l_margin + width * i0, @page_break_trigger) # left + Line(@l_margin + width * (i1+1), @t_margin, @l_margin + width * (i1+1), @page_break_trigger) # right + @page += 1; + end + Line(@l_margin + width * i0, @t_margin, @l_margin + width * i0, @max_td_y[j1+1]) # left + Line(@l_margin + width * (i1+1), @t_margin, @l_margin + width * (i1+1), @max_td_y[j1+1]) # right + end + Line(@l_margin + width * i0, @max_td_y[j1+1], @l_margin + width * (i1+1), @max_td_y[j1+1]) # bottom + end + end + + @l_margin -= 5; + @r_margin -= 5; + @tableborder=0; + @table_id += 1; + + when 'strong' + SetStyle('b', false); + + when 'em' + SetStyle('i', false); + + when 'ins' + SetStyle('u', false); + + when 'del' + SetStyle('d', false); + + when 'b', 'i', 'u' + SetStyle(tag, false); + + when 'a' + @href = nil; + + when 'p' + Ln(); + + when 'sup' + currentfont_size = @font_size; + SetFontSize(@tempfontsize); + @tempfontsize = @font_size_pt; + SetXY(GetX(), GetY() - ((currentfont_size - @font_size)*(@@k_small_ratio))); + + when 'sub' + currentfont_size = @font_size; + SetFontSize(@tempfontsize); + @tempfontsize = @font_size_pt; + SetXY(GetX(), GetY() + ((currentfont_size - @font_size)*(@@k_small_ratio))); + + when 'small' + currentfont_size = @font_size; + SetFontSize(@tempfontsize); + @tempfontsize = @font_size_pt; + SetXY(GetX(), GetY() - ((@font_size - currentfont_size)/3)); + + when 'font' + if (@issetcolor == true) + SetTextColor(@prevtext_color[0], @prevtext_color[1], @prevtext_color[2]); + end + if (@issetfont) + @font_family = @prevfont_family; + @font_style = @prevfont_style; + SetFont(@font_family); + @issetfont = false; + end + currentfont_size = @font_size; + SetFontSize(@tempfontsize); + @tempfontsize = @font_size_pt; + #@text_color = @prevtext_color; + @lasth = @font_size * @@k_cell_height_ratio; + + when 'blockquote' + @quote_count -= 1 + if (@quote_page[@quote_count] == @page) + Line(@l_margin - 1, @quote_top[@quote_count], @l_margin - 1, @y) # quoto line + else + cur_page = @page; + cur_y = @y; + @page = @quote_page[@quote_count]; + if (@quote_top[@quote_count] < @page_break_trigger) + Line(@l_margin - 1, @quote_top[@quote_count], @l_margin - 1, @page_break_trigger) # quoto line + end + @page += 1; + while @page < cur_page + Line(@l_margin - 1, @t_margin, @l_margin - 1, @page_break_trigger) # quoto line + @page += 1; + end + @y = cur_y; + Line(@l_margin - 1, @t_margin, @l_margin - 1, @y) # quoto line + end + if (@quote_count <= 0) + SetStyle('i', false); + @l_margin -= 5; + else + @l_margin -= 5 / 2; + end + @x = @l_margin; + Ln() if @quote_count == 0 + + when 'ul', 'ol' + @li_count -= 1 + if @li_state == true + Ln(); + @li_state = false; + end + + when 'li' + @li_spacer = ""; + if @li_state == true + Ln(); + @li_state = false; + end + + when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' + SetFontSize(@tempfontsize); + @tempfontsize = @font_size_pt; + SetStyle('b', false); + Ln(); + @lasth = @font_size * @@k_cell_height_ratio; + + if tag == 'h1' or tag == 'h2' or tag == 'h3' or tag == 'h4' + margin = 1; + hrWidth = @w - @l_margin - @r_margin - margin; + if tag == 'h1' or tag == 'h2' + SetLineWidth(0.2); + else + SetLineWidth(0.1); + end + Line(@x + margin, @y, @x + hrWidth, @y); + end + end + end + + # + # Sets font style. + # @param string :tag tag name (in lowercase) + # @param boolean :enable + # @access private + # + def SetStyle(tag, enable) + #Modify style and select corresponding font + ['b', 'i', 'u', 'd'].each do |s| + if tag.downcase == s + if enable + @style << s if ! @style.include?(s) + else + @style = @style.gsub(s,'') + end + end + end + SetFont('', @style); + end + + # + # Output anchor link. + # @param string :url link URL + # @param string :name link name + # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. + # @access public + # + def addHtmlLink(url, name, fill=0) + #Put a hyperlink + SetTextColor(0, 0, 255); + SetStyle('u', true); + Write(@lasth, name, url, fill); + SetStyle('u', false); + SetTextColor(0); + end + + # + # Returns an associative array (keys: R,G,B) from + # a hex html code (e.g. #3FE5AA). + # @param string :color hexadecimal html color [#rrggbb] + # @return array + # @access private + # + def convertColorHexToDec(color = "#000000") + tbl_color = {} + tbl_color['R'] = color[1,2].hex.to_i; + tbl_color['G'] = color[3,2].hex.to_i; + tbl_color['B'] = color[5,2].hex.to_i; + return tbl_color; + end + + # + # Converts pixels to millimeters in 72 dpi. + # @param int :px pixels + # @return float millimeters + # @access private + # + def pixelsToMillimeters(px) + return px.to_f * 25.4 / 72; + end + + # + # Reverse function for htmlentities. + # Convert entities in UTF-8. + # + # @param :text_to_convert Text to convert. + # @return string converted + # + def unhtmlentities(string) + CGI.unescapeHTML(string) + end + +end # END OF CLASS + +#TODO 2007-05-25 (EJM) Level=0 - +#Handle special IE contype request +# if (!_SERVER['HTTP_USER_AGENT'].nil? and (_SERVER['HTTP_USER_AGENT']=='contype')) +# header('Content-Type: application/pdf'); +# exit; +# } diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/09/09db2139c5f23fc2a9c87d7a056e38c3f53388a9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/09db2139c5f23fc2a9c87d7a056e38c3f53388a9.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1096 @@ +# Estonian localization for Redmine +# Copyright (C) 2012 Kaitseministeerium +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +et: + # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) + direction: ltr + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%d.%m.%Y" + short: "%d.%b" + long: "%d. %B %Y" + + day_names: [Pühapäev, Esmaspäev, Teisipäev, Kolmapäev, Neljapäev, Reede, Laupäev] + abbr_day_names: [P, E, T, K, N, R, L] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, Jaanuar, Veebruar, Märts, Aprill, Mai, Juuni, Juuli, August, September, Oktoober, November, Detsember] + abbr_month_names: [~, jaan, veebr, märts, apr, mai, juuni, juuli, aug, sept, okt, nov, dets] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%d.%m.%Y %H:%M" + time: "%H:%M" + short: "%d.%b %H:%M" + long: "%d. %B %Y %H:%M %z" + am: "enne lõunat" + pm: "peale lõunat" + + datetime: + distance_in_words: + half_a_minute: "pool minutit" + less_than_x_seconds: + one: "vähem kui sekund" + other: "vähem kui %{count} sekundit" + x_seconds: + one: "1 sekund" + other: "%{count} sekundit" + less_than_x_minutes: + one: "vähem kui minut" + other: "vähem kui %{count} minutit" + x_minutes: + one: "1 minut" + other: "%{count} minutit" + about_x_hours: + one: "umbes tund" + other: "umbes %{count} tundi" + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "1 päev" + other: "%{count} päeva" + about_x_months: + one: "umbes kuu" + other: "umbes %{count} kuud" + x_months: + one: "1 kuu" + other: "%{count} kuud" + about_x_years: + one: "umbes aasta" + other: "umbes %{count} aastat" + over_x_years: + one: "rohkem kui aasta" + other: "rohkem kui %{count} aastat" + almost_x_years: + one: "peaaegu aasta" + other: "peaaegu %{count} aastat" + + number: + format: + separator: "." + delimiter: "" + precision: 3 + + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "bait" + other: "baiti" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + +# Used in array.to_sentence. + support: + array: + sentence_connector: "ja" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 viga ei võimaldanud selle %{model} salvestamist" + other: "%{count} viga ei võimaldanud selle %{model} salvestamist" + messages: + inclusion: "ei ole nimekirjas" + exclusion: "on reserveeritud" + invalid: "ei sobi" + confirmation: "ei lange kinnitusega kokku" + accepted: "peab olema aktsepteeritud" + empty: "ei või olla tühi" + blank: "ei või olla täitmata" + too_long: "on liiga pikk (lubatud on kuni %{count} märki)" + too_short: "on liiga lühike (vaja on vähemalt %{count} märki)" + wrong_length: "on vale pikkusega (peaks olema %{count} märki)" + taken: "on juba võetud" + not_a_number: "ei ole arv" + not_a_date: "ei ole korrektne kuupäev" + greater_than: "peab olema suurem kui %{count}" + greater_than_or_equal_to: "peab olema võrdne või suurem kui %{count}" + equal_to: "peab võrduma %{count}-ga" + less_than: "peab olema väiksem kui %{count}" + less_than_or_equal_to: "peab olema võrdne või väiksem kui %{count}" + odd: "peab olema paaritu arv" + even: "peab olema paarisarv" + greater_than_start_date: "peab olema suurem kui alguskuupäev" + not_same_project: "ei kuulu sama projekti juurde" + circular_dependency: "See suhe looks vastastikuse sõltuvuse" + cant_link_an_issue_with_a_descendant: "Teemat ei saa sidustada tema enda alamteemaga" + + actionview_instancetag_blank_option: "Palun vali" + + general_text_No: "Ei" + general_text_Yes: "Jah" + general_text_no: "ei" + general_text_yes: "jah" + general_lang_name: "Eesti" + general_csv_separator: "," + general_csv_decimal_separator: "." + general_csv_encoding: ISO-8859-13 + general_pdf_encoding: UTF-8 + general_first_day_of_week: "1" + + notice_account_updated: "Konto uuendamine õnnestus." + notice_account_invalid_creditentials: "Sobimatu kasutajanimi või parool" + notice_account_password_updated: "Parooli uuendamine õnnestus." + notice_account_wrong_password: "Vale parool" + notice_account_register_done: "Konto loomine õnnestus. Konto aktiveerimiseks vajuta vastaval lingil Sulle saadetud e-kirjas." + notice_account_unknown_email: "Tundmatu kasutaja." + notice_can_t_change_password: "See konto kasutab välist autentimisallikat. Siin ei saa selle konto parooli vahetada." + notice_account_lost_email_sent: "Sulle saadeti e-kiri parooli vahetamise juhistega." + notice_account_activated: "Su konto on aktiveeritud. Saad nüüd sisse logida." + notice_successful_create: "Loomine õnnestus." + notice_successful_update: "Uuendamine õnnestus." + notice_successful_delete: "Kustutamine õnnestus." + notice_successful_connection: "Ühenduse loomine õnnestus." + notice_file_not_found: "Sellist lehte ei leitud." + notice_locking_conflict: "Teine kasutaja uuendas vahepeal neid andmeid." + notice_not_authorized: "Sul ei ole sellele lehele ligipääsuks õigusi." + notice_not_authorized_archived_project: "See projekt on arhiveeritud." + notice_email_sent: "%{value}-le saadeti kiri" + notice_email_error: "Kirja saatmisel tekkis viga (%{value})" + notice_feeds_access_key_reseted: "Sinu RSS juurdepääsuvõti nulliti." + notice_api_access_key_reseted: "Sinu API juurdepääsuvõti nulliti." + notice_failed_to_save_issues: "%{count} teemat %{total}-st ei õnnestunud salvestada: %{ids}." + notice_failed_to_save_time_entries: "%{count} ajakulu kannet %{total}-st ei õnnestunud salvestada: %{ids}." + notice_failed_to_save_members: "Liiget/liikmeid ei õnnestunud salvestada: %{errors}." + notice_no_issue_selected: "Ühtegi teemat ei ole valitud! Palun vali teema(d), mida soovid muuta." + notice_account_pending: "Sinu konto on loodud ja ootab nüüd administraatori kinnitust." + notice_default_data_loaded: "Algseadistuste laadimine õnnestus." + notice_unable_delete_version: "Versiooni kustutamine ei õnnestunud." + notice_unable_delete_time_entry: "Ajakulu kande kustutamine ei õnnestunud." + notice_issue_done_ratios_updated: "Teema edenemise astmed on uuendatud." + notice_gantt_chart_truncated: "Diagrammi kärbiti kuna ületati kuvatavate objektide suurim hulk (%{max})" + notice_issue_successful_create: "Teema %{id} loodud." + notice_issue_update_conflict: "Teine kasutaja uuendas seda teemat Sinuga samaaegselt." + notice_account_deleted: "Sinu konto on lõplikult kustutatud." + + error_can_t_load_default_data: "Algseadistusi ei saanud laadida: %{value}" + error_scm_not_found: "Seda sissekannet hoidlast ei leitud." + error_scm_command_failed: "Hoidla poole pöördumisel tekkis viga: %{value}" + error_scm_annotate: "Sissekannet ei eksisteeri või ei saa annoteerida." + error_scm_annotate_big_text_file: "Sissekannet ei saa annoteerida, kuna see on liiga pikk." + error_issue_not_found_in_project: "Teemat ei leitud või see ei kuulu siia projekti" + error_no_tracker_in_project: "Selle projektiga ei ole seostatud ühtegi valdkonda. Palun vaata üle projekti seaded." + error_no_default_issue_status: 'Teema algolek on määramata. Palun vaata asetused üle ("Seadistused -> Olekud").' + error_can_not_delete_custom_field: "Omaloodud välja kustutamine ei õnnestunud" + error_can_not_delete_tracker: "See valdkond on mõnes teemas kasutusel ja seda ei saa kustutada." + error_can_not_remove_role: "See roll on mõnes projektis kasutusel ja seda ei saa kustutada." + error_can_not_reopen_issue_on_closed_version: "Suletud versiooni juurde kuulunud teemat ei saa taasavada" + error_can_not_archive_project: "Seda projekti ei saa arhiveerida" + error_issue_done_ratios_not_updated: "Teema edenemise astmed jäid uuendamata." + error_workflow_copy_source: "Palun vali algne valdkond või roll" + error_workflow_copy_target: "Palun vali sihtvaldkon(na)d või -roll(id)" + error_unable_delete_issue_status: "Oleku kustutamine ei õnnestunud" + error_unable_to_connect: "Ühenduse loomine ei õnnestunud (%{value})" + error_attachment_too_big: "Faili ei saa üles laadida, sest see on lubatust (%{max_size}) pikem" + warning_attachments_not_saved: "%{count} faili salvestamine ei õnnestunud." + + mail_subject_lost_password: "Sinu %{value} parool" + mail_body_lost_password: "Et vahetada oma parooli, vajuta järgmisele lingile:" + mail_subject_register: "Sinu %{value} konto aktiveerimine" + mail_body_register: "Et aktiveerida oma kontot, vajuta järgmisele lingile:" + mail_body_account_information_external: "Sisse logimiseks saad kasutada oma %{value} kontot." + mail_body_account_information: "Sinu konto teave" + mail_subject_account_activation_request: "%{value} konto aktiveerimise nõue" + mail_body_account_activation_request: "Registreerus uus kasutaja (%{value}). Konto avamine ootab Sinu kinnitust:" + mail_subject_reminder: "%{count} teema tähtaeg jõuab kätte järgmise %{days} päeva jooksul" + mail_body_reminder: "%{count} Sulle määratud teema tähtaeg jõuab kätte järgmise %{days} päeva jooksul:" + mail_subject_wiki_content_added: "Lisati '%{id}' vikileht" + mail_body_wiki_content_added: "'%{id}' vikileht lisati %{author} poolt." + mail_subject_wiki_content_updated: "Uuendati '%{id}' vikilehte" + mail_body_wiki_content_updated: "'%{id}' vikilehte uuendati %{author} poolt." + + gui_validation_error: "1 viga" + gui_validation_error_plural: "%{count} viga" + + field_name: "Nimi" + field_description: "Kirjeldus" + field_summary: "Kokkuvõte" + field_is_required: "Kohustuslik" + field_firstname: "Eesnimi" + field_lastname: "Perekonnanimi" + field_mail: "E-post" + field_filename: "Fail" + field_filesize: "Pikkus" + field_downloads: "Allalaadimist" + field_author: "Autor" + field_created_on: "Loodud" + field_updated_on: "Uuendatud" + field_field_format: "Tüüp" + field_is_for_all: "Kõigile projektidele" + field_possible_values: "Võimalikud väärtused" + field_regexp: "Regulaarne avaldis" + field_min_length: "Vähim pikkus" + field_max_length: "Suurim pikkus" + field_value: "Väärtus" + field_category: "Kategooria" + field_title: "Pealkiri" + field_project: "Projekt" + field_issue: "Teema" + field_status: "Olek" + field_notes: "Märkused" + field_is_closed: "Sulgeb teema" + field_is_default: "Algolek" + field_tracker: "Valdkond" + field_subject: "Teema" + field_due_date: "Tähtaeg" + field_assigned_to: "Tegeleja" + field_priority: "Prioriteet" + field_fixed_version: "Sihtversioon" + field_user: "Kasutaja" + field_principal: "Vastutav isik" + field_role: "Roll" + field_homepage: "Koduleht" + field_is_public: "Avalik" + field_parent: "Emaprojekt" + field_is_in_roadmap: "Teemad on teekaardil näha" + field_login: "Kasutajanimi" + field_mail_notification: "Teated e-kirjaga" + field_admin: "Admin" + field_last_login_on: "Viimane ühendus" + field_language: "Keel" + field_effective_date: "Tähtaeg" + field_password: "Parool" + field_new_password: "Uus parool" + field_password_confirmation: "Kinnitus" + field_version: "Versioon" + field_type: "Tüüp" + field_host: "Server" + field_port: "Port" + field_account: "Konto" + field_base_dn: "Baas DN" + field_attr_login: "Kasutajanime atribuut" + field_attr_firstname: "Eesnime atribuut" + field_attr_lastname: "Perekonnanime atribuut" + field_attr_mail: "E-posti atribuut" + field_onthefly: "Kasutaja automaatne loomine" + field_start_date: "Alguskuupäev" + field_done_ratio: "% tehtud" + field_auth_source: "Autentimise viis" + field_hide_mail: "Peida e-posti aadress" + field_comments: "Kommentaar" + field_url: "URL" + field_start_page: "Esileht" + field_subproject: "Alamprojekt" + field_hours: "tundi" + field_activity: "Tegevus" + field_spent_on: "Kuupäev" + field_identifier: "Tunnus" + field_is_filter: "Kasutatakse filtrina" + field_issue_to: "Seotud teema" + field_delay: "Viivitus" + field_assignable: "Saab määrata teemadega tegelema" + field_redirect_existing_links: "Suuna olemasolevad lingid ringi" + field_estimated_hours: "Eeldatav ajakulu" + field_column_names: "Veerud" + field_time_entries: "Ajakulu" + field_time_zone: "Ajatsoon" + field_searchable: "Otsitav" + field_default_value: "Vaikimisi" + field_comments_sorting: "Kommentaaride järjestus" + field_parent_title: "Pärineb lehest" + field_editable: "Muudetav" + field_watcher: "Jälgija" + field_identity_url: "OpenID URL" + field_content: "Sisu" + field_group_by: "Grupeeri tulemus" + field_sharing: "Teemade jagamine" + field_parent_issue: "Pärineb teemast" + field_member_of_group: "Tegeleja grupp" + field_assigned_to_role: "Tegeleja roll" + field_text: "Tekstiväli" + field_visible: "Nähtav" + field_warn_on_leaving_unsaved: "Hoiata salvestamata sisuga lehtedelt lahkumisel" + field_issues_visibility: "See roll näeb" + field_is_private: "Privaatne" + field_commit_logs_encoding: "Sissekannete kodeering" + field_scm_path_encoding: "Teeraja märkide kodeering" + field_path_to_repository: "Hoidla teerada" + field_root_directory: "Juurkataloog" + field_cvsroot: "CVSROOT" + field_cvs_module: "Moodul" + field_repository_is_default: "Peamine hoidla" + field_multiple: "Korraga mitu väärtust" + field_auth_source_ldap_filter: "LDAP filter" + + setting_app_title: "Veebilehe pealkiri" + setting_app_subtitle: "Veebilehe alampealkiri" + setting_welcome_text: "Tervitustekst" + setting_default_language: "Vaikimisi keel" + setting_login_required: "Autentimine kohustuslik" + setting_self_registration: "Omaloodud konto aktiveerimine" + setting_attachment_max_size: "Manuse suurim pikkus" + setting_issues_export_limit: "Teemade ekspordi limiit" + setting_mail_from: "Saatja e-posti aadress" + setting_bcc_recipients: "Saajaid ei näidata (lähevad BCC reale)" + setting_plain_text_mail: "E-kiri tavalise tekstina (ilma HTML-ta)" + setting_host_name: "Serveri nimi ja teerada" + setting_text_formatting: "Vormindamise abi" + setting_wiki_compression: "Viki ajaloo pakkimine" + setting_feeds_limit: "Atom voogude suurim objektide arv" + setting_default_projects_public: "Uued projektid on vaikimisi avalikud" + setting_autofetch_changesets: "Lae uuendused automaatselt" + setting_sys_api_enabled: "Hoidlate haldamine veebiteenuse kaudu" + setting_commit_ref_keywords: "Viitade võtmesõnad" + setting_commit_fix_keywords: "Paranduste võtmesõnad" + setting_autologin: "Automaatne sisselogimine" + setting_date_format: "Kuupäevaformaat" + setting_time_format: "Ajaformaat" + setting_cross_project_issue_relations: "Luba siduda eri projektide teemasid" + setting_issue_list_default_columns: "Teemade nimekirja vaikimisi veerud" + setting_repositories_encodings: "Manuste ja hoidlate kodeering" + setting_emails_header: "E-kirja päis" + setting_emails_footer: "E-kirja jalus" + setting_protocol: "Protokoll" + setting_per_page_options: "Objekte lehe kohta variandid" + setting_user_format: "Kasutaja nime esitamise vorm" + setting_activity_days_default: "Projektide ajalugu näidatakse" + setting_display_subprojects_issues: "Näita projektis vaikimisi ka alamprojektide teemasid" + setting_enabled_scm: "Kasutatavad lähtekoodi haldusvahendid" + setting_mail_handler_body_delimiters: "Kärbi e-kirja lõpp peale sellist rida" + setting_mail_handler_api_enabled: "E-kirjade vastuvõtt veebiteenuse kaudu" + setting_mail_handler_api_key: "Veebiteenuse API võti" + setting_sequential_project_identifiers: "Genereeri järjestikused projektitunnused" + setting_gravatar_enabled: "Kasuta Gravatari kasutajaikoone" + setting_gravatar_default: "Vaikimisi kasutatav ikoon" + setting_diff_max_lines_displayed: "Enim korraga näidatavaid erinevusi" + setting_file_max_size_displayed: "Kuvatava tekstifaili suurim pikkus" + setting_repository_log_display_limit: "Enim ajaloos näidatavaid sissekandeid" + setting_openid: "Luba OpenID-ga registreerimine ja sisselogimine" + setting_password_min_length: "Lühima lubatud parooli pikkus" + setting_new_project_user_role_id: "Projekti looja roll oma projektis" + setting_default_projects_modules: "Vaikimisi moodulid uutes projektides" + setting_issue_done_ratio: "Määra teema edenemise aste" + setting_issue_done_ratio_issue_field: "kasutades vastavat välja" + setting_issue_done_ratio_issue_status: "kasutades teema olekut" + setting_start_of_week: "Nädala alguspäev" + setting_rest_api_enabled: "Luba REST API kasutamine" + setting_cache_formatted_text: "Puhverda vormindatud teksti" + setting_default_notification_option: "Vaikimisi teavitatakse" + setting_commit_logtime_enabled: "Luba ajakulu sisestamine" + setting_commit_logtime_activity_id: "Tegevus kulunud ajal" + setting_gantt_items_limit: "Gantti diagrammi objektide suurim hulk" + setting_issue_group_assignment: "Luba teemade andmine gruppidele" + setting_default_issue_start_date_to_creation_date: "Uute teemade alguskuupäevaks teema loomise päev" + setting_commit_cross_project_ref: "Luba viiteid ja parandusi ka kõigi teiste projektide teemadele" + setting_unsubscribe: "Luba kasutajal oma konto kustutada" + + permission_add_project: "Projekte luua" + permission_add_subprojects: "Alamprojekte luua" + permission_edit_project: "Projekte muuta" + permission_select_project_modules: "Projektimooduleid valida" + permission_manage_members: "Liikmeid hallata" + permission_manage_project_activities: "Projekti tegevusi hallata" + permission_manage_versions: "Versioone hallata" + permission_manage_categories: "Kategooriaid hallata" + permission_view_issues: "Teemasid näha" + permission_add_issues: "Teemasid lisada" + permission_edit_issues: "Teemasid uuendada" + permission_manage_issue_relations: "Teemade seoseid hallata" + permission_set_issues_private: "Teemasid avalikeks või privaatseiks seada" + permission_set_own_issues_private: "Omi teemasid avalikeks või privaatseiks seada" + permission_add_issue_notes: "Märkusi lisada" + permission_edit_issue_notes: "Märkusi muuta" + permission_edit_own_issue_notes: "Omi märkusi muuta" + permission_move_issues: "Teemasid teise projekti tõsta" + permission_delete_issues: "Teemasid kustutada" + permission_manage_public_queries: "Avalikke päringuid hallata" + permission_save_queries: "Päringuid salvestada" + permission_view_gantt: "Gantti diagramme näha" + permission_view_calendar: "Kalendrit näha" + permission_view_issue_watchers: "Jälgijate nimekirja näha" + permission_add_issue_watchers: "Jälgijaid lisada" + permission_delete_issue_watchers: "Jälgijaid kustutada" + permission_log_time: "Ajakulu sisestada" + permission_view_time_entries: "Ajakulu näha" + permission_edit_time_entries: "Ajakulu kandeid muuta" + permission_edit_own_time_entries: "Omi ajakulu kandeid muuta" + permission_manage_news: "Uudiseid hallata" + permission_comment_news: "Uudiseid kommenteerida" + permission_manage_documents: "Dokumente hallata" + permission_view_documents: "Dokumente näha" + permission_manage_files: "Faile hallata" + permission_view_files: "Faile näha" + permission_manage_wiki: "Vikit hallata" + permission_rename_wiki_pages: "Vikilehti ümber nimetada" + permission_delete_wiki_pages: "Vikilehti kustutada" + permission_view_wiki_pages: "Vikit näha" + permission_view_wiki_edits: "Viki ajalugu näha" + permission_edit_wiki_pages: "Vikilehti muuta" + permission_delete_wiki_pages_attachments: "Manuseid kustutada" + permission_protect_wiki_pages: "Vikilehti kaitsta" + permission_manage_repository: "Hoidlaid hallata" + permission_browse_repository: "Hoidlaid sirvida" + permission_view_changesets: "Sissekandeid näha" + permission_commit_access: "Sissekandeid teha" + permission_manage_boards: "Foorumeid hallata" + permission_view_messages: "Postitusi näha" + permission_add_messages: "Postitusi lisada" + permission_edit_messages: "Postitusi muuta" + permission_edit_own_messages: "Omi postitusi muuta" + permission_delete_messages: "Postitusi kustutada" + permission_delete_own_messages: "Omi postitusi kustutada" + permission_export_wiki_pages: "Vikilehti eksportida" + permission_manage_subtasks: "Alamteemasid hallata" + permission_manage_related_issues: "Seotud teemasid hallata" + + project_module_issue_tracking: "Teemade jälgimine" + project_module_time_tracking: "Ajakulu arvestus" + project_module_news: "Uudised" + project_module_documents: "Dokumendid" + project_module_files: "Failid" + project_module_wiki: "Viki" + project_module_repository: "Hoidlad" + project_module_boards: "Foorumid" + project_module_calendar: "Kalender" + project_module_gantt: "Gantt" + + label_user: "Kasutaja" + label_user_plural: "Kasutajad" + label_user_new: "Uus kasutaja" + label_user_anonymous: "Anonüümne" + label_project: "Projekt" + label_project_new: "Uus projekt" + label_project_plural: "Projektid" + label_x_projects: + zero: "pole projekte" + one: "1 projekt" + other: "%{count} projekti" + label_project_all: "Kõik projektid" + label_project_latest: "Viimased projektid" + label_issue: "Teema" + label_issue_new: "Uus teema" + label_issue_plural: "Teemad" + label_issue_view_all: "Teemade nimekiri" + label_issues_by: "Teemad %{value} järgi" + label_issue_added: "Teema lisatud" + label_issue_updated: "Teema uuendatud" + label_issue_note_added: "Märkus lisatud" + label_issue_status_updated: "Olek uuendatud" + label_issue_priority_updated: "Prioriteet uuendatud" + label_document: "Dokument" + label_document_new: "Uus dokument" + label_document_plural: "Dokumendid" + label_document_added: "Dokument lisatud" + label_role: "Roll" + label_role_plural: "Rollid" + label_role_new: "Uus roll" + label_role_and_permissions: "Rollid ja õigused" + label_role_anonymous: "Anonüümne" + label_role_non_member: "Mitteliige" + label_member: "Liige" + label_member_new: "Uus liige" + label_member_plural: "Liikmed" + label_tracker: "Valdkond" + label_tracker_plural: "Valdkonnad" + label_tracker_new: "Uus valdkond" + label_workflow: "Töövood" + label_issue_status: "Olek" + label_issue_status_plural: "Olekud" + label_issue_status_new: "Uus olek" + label_issue_category: "Kategooria" + label_issue_category_plural: "Kategooriad" + label_issue_category_new: "Uus kategooria" + label_custom_field: "Omaloodud väli" + label_custom_field_plural: "Omaloodud väljad" + label_custom_field_new: "Uus väli" + label_enumerations: "Loetelud" + label_enumeration_new: "Uus väärtus" + label_information: "Teave" + label_information_plural: "Teave" + label_please_login: "Palun logi sisse" + label_register: "Registreeru" + label_login_with_open_id_option: "või logi sisse OpenID-ga" + label_password_lost: "Kui parool on ununud..." + label_home: "Kodu" + label_my_page: "Oma leht" + label_my_account: "Oma konto" + label_my_projects: "Oma projektid" + label_my_page_block: "Uus blokk" + label_administration: "Seadistused" + label_login: "Logi sisse" + label_logout: "Logi välja" + label_help: "Abi" + label_reported_issues: "Minu poolt lisatud teemad" + label_assigned_to_me_issues: "Minu teha olevad teemad" + label_last_login: "Viimane ühendus" + label_registered_on: "Registreeritud" + label_activity: "Ajalugu" + label_overall_activity: "Üldine tegevuste ajalugu" + label_user_activity: "%{value} tegevuste ajalugu" + label_new: "Uus" + label_logged_as: "Sisse logitud kui" + label_environment: "Keskkond" + label_authentication: "Autentimine" + label_auth_source: "Autentimisallikas" + label_auth_source_new: "Uus autentimisallikas" + label_auth_source_plural: "Autentimisallikad" + label_subproject_plural: "Alamprojektid" + label_subproject_new: "Uus alamprojekt" + label_and_its_subprojects: "%{value} ja selle alamprojektid" + label_min_max_length: "Min.-maks. pikkus" + label_list: "Nimekiri" + label_date: "Kuupäev" + label_integer: "Täisarv" + label_float: "Ujukomaarv" + label_boolean: "Tõeväärtus" + label_string: "Tekst" + label_text: "Pikk tekst" + label_attribute: "Atribuut" + label_attribute_plural: "Atribuudid" + label_download: "%{count} allalaadimine" + label_download_plural: "%{count} allalaadimist" + label_no_data: "Pole" + label_change_status: "Muuda olekut" + label_history: "Ajalugu" + label_attachment: "Fail" + label_attachment_new: "Uus fail" + label_attachment_delete: "Kustuta fail" + label_attachment_plural: "Failid" + label_file_added: "Fail lisatud" + label_report: "Aruanne" + label_report_plural: "Aruanded" + label_news: "Uudised" + label_news_new: "Lisa uudis" + label_news_plural: "Uudised" + label_news_latest: "Viimased uudised" + label_news_view_all: "Kõik uudised" + label_news_added: "Uudis lisatud" + label_news_comment_added: "Kommentaar uudisele lisatud" + label_settings: "Seaded" + label_overview: "Ülevaade" + label_version: "Versioon" + label_version_new: "Uus versioon" + label_version_plural: "Versioonid" + label_close_versions: "Sulge lõpetatud versioonid" + label_confirmation: "Kinnitus" + label_export_to: "Samuti saadaval kujul:" + label_read: "Loe..." + label_public_projects: "Avalikud projektid" + label_open_issues: "avatud" + label_open_issues_plural: "avatud" + label_closed_issues: "suletud" + label_closed_issues_plural: "suletud" + label_x_open_issues_abbr_on_total: + zero: "0 avatud / %{total}" + one: "1 avatud / %{total}" + other: "%{count} avatud / %{total}" + label_x_open_issues_abbr: + zero: "0 avatud" + one: "1 avatud" + other: "%{count} avatud" + label_x_closed_issues_abbr: + zero: "0 suletud" + one: "1 suletud" + other: "%{count} suletud" + label_x_issues: + zero: "0 teemat" + one: "1 teema" + other: "%{count} teemat" + label_total: "Kokku" + label_permissions: "Õigused" + label_current_status: "Praegune olek" + label_new_statuses_allowed: "Uued lubatud olekud" + label_all: "kõik" + label_none: "pole" + label_nobody: "eikeegi" + label_next: "Järgmine" + label_previous: "Eelmine" + label_used_by: "Kasutab" + label_details: "Üksikasjad" + label_add_note: "Lisa märkus" + label_per_page: "Lehe kohta" + label_calendar: "Kalender" + label_months_from: "kuu kaugusel" + label_gantt: "Gantt" + label_internal: "Sisemine" + label_last_changes: "viimased %{count} muudatust" + label_change_view_all: "Kõik muudatused" + label_personalize_page: "Kujunda leht ümber" + label_comment: "Kommentaar" + label_comment_plural: "Kommentaarid" + label_x_comments: + zero: "kommentaare pole" + one: "1 kommentaar" + other: "%{count} kommentaari" + label_comment_add: "Lisa kommentaar" + label_comment_added: "Kommentaar lisatud" + label_comment_delete: "Kustuta kommentaar" + label_query: "Omaloodud päring" + label_query_plural: "Omaloodud päringud" + label_query_new: "Uus päring" + label_my_queries: "Mu omaloodud päringud" + label_filter_add: "Lisa filter" + label_filter_plural: "Filtrid" + label_equals: "on" + label_not_equals: "ei ole" + label_in_less_than: "on väiksem kui" + label_in_more_than: "on suurem kui" + label_greater_or_equal: "suurem-võrdne" + label_less_or_equal: "väiksem-võrdne" + label_between: "vahemikus" + label_in: "sisaldub hulgas" + label_today: "täna" + label_all_time: "piirideta" + label_yesterday: "eile" + label_this_week: "sel nädalal" + label_last_week: "eelmisel nädalal" + label_last_n_days: "viimase %{count} päeva jooksul" + label_this_month: "sel kuul" + label_last_month: "eelmisel kuul" + label_this_year: "sel aastal" + label_date_range: "Kuupäevavahemik" + label_less_than_ago: "uuem kui" + label_more_than_ago: "vanem kui" + label_ago: "vanus" + label_contains: "sisaldab" + label_not_contains: "ei sisalda" + label_day_plural: "päeva" + label_repository: "Hoidla" + label_repository_new: "Uus hoidla" + label_repository_plural: "Hoidlad" + label_browse: "Sirvi" + label_modification: "%{count} muudatus" + label_modification_plural: "%{count} muudatust" + label_branch: "Haru" + label_tag: "Sildiga" + label_revision: "Sissekanne" + label_revision_plural: "Sissekanded" + label_revision_id: "Sissekande kood %{value}" + label_associated_revisions: "Seotud sissekanded" + label_added: "lisatud" + label_modified: "muudetud" + label_copied: "kopeeritud" + label_renamed: "ümber nimetatud" + label_deleted: "kustutatud" + label_latest_revision: "Viimane sissekanne" + label_latest_revision_plural: "Viimased sissekanded" + label_view_revisions: "Haru ajalugu" + label_view_all_revisions: "Kogu ajalugu" + label_max_size: "Suurim pikkus" + label_sort_highest: "Nihuta esimeseks" + label_sort_higher: "Nihuta üles" + label_sort_lower: "Nihuta alla" + label_sort_lowest: "Nihuta viimaseks" + label_roadmap: "Teekaart" + label_roadmap_due_in: "Tähtaeg %{value}" + label_roadmap_overdue: "%{value} hiljaks jäänud" + label_roadmap_no_issues: "Selles versioonis ei ole teemasid" + label_search: "Otsi" + label_result_plural: "Tulemused" + label_all_words: "Kõik sõnad" + label_wiki: "Viki" + label_wiki_edit: "Viki muutmine" + label_wiki_edit_plural: "Viki muutmised" + label_wiki_page: "Vikileht" + label_wiki_page_plural: "Vikilehed" + label_index_by_title: "Järjesta pealkirja järgi" + label_index_by_date: "Järjesta kuupäeva järgi" + label_current_version: "Praegune versioon" + label_preview: "Eelvaade" + label_feed_plural: "Vood" + label_changes_details: "Kõigi muudatuste üksikasjad" + label_issue_tracking: "Teemade jälgimine" + label_spent_time: "Kulutatud aeg" + label_overall_spent_time: "Kokku kulutatud aeg" + label_f_hour: "%{value} tund" + label_f_hour_plural: "%{value} tundi" + label_time_tracking: "Ajakulu arvestus" + label_change_plural: "Muudatused" + label_statistics: "Statistika" + label_commits_per_month: "Sissekandeid kuu kohta" + label_commits_per_author: "Sissekandeid autori kohta" + label_diff: "erinevused" + label_view_diff: "Vaata erinevusi" + label_diff_inline: "teksti sees" + label_diff_side_by_side: "kõrvuti" + label_options: "Valikud" + label_copy_workflow_from: "Kopeeri see töövoog" + label_permissions_report: "Õiguste aruanne" + label_watched_issues: "Jälgitud teemad" + label_related_issues: "Seotud teemad" + label_applied_status: "Kehtestatud olek" + label_loading: "Laadimas..." + label_relation_new: "Uus seos" + label_relation_delete: "Kustuta seos" + label_relates_to: "seostub" + label_duplicates: "duplitseerib" + label_duplicated_by: "duplitseerija" + label_blocks: "blokeerib" + label_blocked_by: "blokeerija" + label_precedes: "eelneb" + label_follows: "järgneb" + label_end_to_start: "lõpust alguseni" + label_end_to_end: "lõpust lõpuni" + label_start_to_start: "algusest alguseni" + label_start_to_end: "algusest lõpuni" + label_stay_logged_in: "Püsi sisselogituna" + label_disabled: "pole võimalik" + label_show_completed_versions: "Näita lõpetatud versioone" + label_me: "mina" + label_board: "Foorum" + label_board_new: "Uus foorum" + label_board_plural: "Foorumid" + label_board_locked: "Lukus" + label_board_sticky: "Püsiteema" + label_topic_plural: "Teemad" + label_message_plural: "Postitused" + label_message_last: "Viimane postitus" + label_message_new: "Uus postitus" + label_message_posted: "Postitus lisatud" + label_reply_plural: "Vastused" + label_send_information: "Saada teave konto kasutajale" + label_year: "Aasta" + label_month: "Kuu" + label_week: "Nädal" + label_date_from: "Alates" + label_date_to: "Kuni" + label_language_based: "Kasutaja keele põhjal" + label_sort_by: "Sorteeri %{value} järgi" + label_send_test_email: "Saada kontrollkiri" + label_feeds_access_key: "RSS juurdepääsuvõti" + label_missing_feeds_access_key: "RSS juurdepääsuvõti on puudu" + label_feeds_access_key_created_on: "RSS juurdepääsuvõti loodi %{value} tagasi" + label_module_plural: "Moodulid" + label_added_time_by: "Lisatud %{author} poolt %{age} tagasi" + label_updated_time_by: "Uuendatud %{author} poolt %{age} tagasi" + label_updated_time: "Uuendatud %{value} tagasi" + label_jump_to_a_project: "Ava projekt..." + label_file_plural: "Failid" + label_changeset_plural: "Muudatused" + label_default_columns: "Vaikimisi veerud" + label_no_change_option: "(Ei muutu)" + label_bulk_edit_selected_issues: "Muuda valitud teemasid korraga" + label_bulk_edit_selected_time_entries: "Muuda valitud ajakandeid korraga" + label_theme: "Visuaalne teema" + label_default: "Tavaline" + label_search_titles_only: "Ainult pealkirjadest" + label_user_mail_option_all: "Kõigist tegevustest kõigis mu projektides" + label_user_mail_option_selected: "Kõigist tegevustest ainult valitud projektides..." + label_user_mail_option_none: "Teavitusi ei saadeta" + label_user_mail_option_only_my_events: "Ainult mu jälgitavatest või minuga seotud tegevustest" + label_user_mail_option_only_assigned: "Ainult minu teha olevate asjade kohta" + label_user_mail_option_only_owner: "Ainult mu oma asjade kohta" + label_user_mail_no_self_notified: "Ära teavita mind mu enda tehtud muudatustest" + label_registration_activation_by_email: "e-kirjaga" + label_registration_manual_activation: "käsitsi" + label_registration_automatic_activation: "automaatselt" + label_display_per_page: "Lehe kohta: %{value}" + label_age: "Vanus" + label_change_properties: "Muuda omadusi" + label_general: "Üldine" + label_more: "Rohkem" + label_scm: "Lähtekoodi haldusvahend" + label_plugins: "Lisamoodulid" + label_ldap_authentication: "LDAP autentimine" + label_downloads_abbr: "A/L" + label_optional_description: "Teave" + label_add_another_file: "Lisa veel üks fail" + label_preferences: "Eelistused" + label_chronological_order: "kronoloogiline" + label_reverse_chronological_order: "tagurpidi kronoloogiline" + label_planning: "Planeerimine" + label_incoming_emails: "Sissetulevad e-kirjad" + label_generate_key: "Genereeri võti" + label_issue_watchers: "Jälgijad" + label_example: "Näide" + label_display: "Kujundus" + label_sort: "Sorteeri" + label_ascending: "Kasvavalt" + label_descending: "Kahanevalt" + label_date_from_to: "Alates %{start} kuni %{end}" + label_wiki_content_added: "Vikileht lisatud" + label_wiki_content_updated: "Vikileht uuendatud" + label_group: "Grupp" + label_group_plural: "Grupid" + label_group_new: "Uus grupp" + label_time_entry_plural: "Kulutatud aeg" + label_version_sharing_none: "ei toimu" + label_version_sharing_descendants: "alamprojektidega" + label_version_sharing_hierarchy: "projektihierarhiaga" + label_version_sharing_tree: "projektipuuga" + label_version_sharing_system: "kõigi projektidega" + label_update_issue_done_ratios: "Uuenda edenemise astmeid" + label_copy_source: "Allikas" + label_copy_target: "Sihtkoht" + label_copy_same_as_target: "Sama mis sihtkoht" + label_display_used_statuses_only: "Näita ainult selles valdkonnas kasutusel olekuid" + label_api_access_key: "API juurdepääsuvõti" + label_missing_api_access_key: "API juurdepääsuvõti on puudu" + label_api_access_key_created_on: "API juurdepääsuvõti loodi %{value} tagasi" + label_profile: "Profiil" + label_subtask_plural: "Alamteemad" + label_project_copy_notifications: "Saada projekti kopeerimise kohta teavituskiri" + label_principal_search: "Otsi kasutajat või gruppi:" + label_user_search: "Otsi kasutajat:" + label_additional_workflow_transitions_for_author: "Luba ka järgmisi üleminekuid kui kasutaja on teema looja" + label_additional_workflow_transitions_for_assignee: "Luba ka järgmisi üleminekuid kui kasutaja on teemaga tegeleja" + label_issues_visibility_all: "kõiki teemasid" + label_issues_visibility_public: "kõiki mitteprivaatseid teemasid" + label_issues_visibility_own: "enda poolt loodud või enda teha teemasid" + label_git_report_last_commit: "Viimase sissekande teave otse failinimekirja" + label_parent_revision: "Eellane" + label_child_revision: "Järglane" + label_export_options: "%{export_format} ekspordivalikud" + label_copy_attachments: "Kopeeri manused" + label_item_position: "%{position}/%{count}" + label_completed_versions: "Lõpetatud versioonid" + label_search_for_watchers: "Otsi lisamiseks jälgijaid" + + button_login: "Logi sisse" + button_submit: "Sisesta" + button_save: "Salvesta" + button_check_all: "Märgi kõik" + button_uncheck_all: "Nulli valik" + button_collapse_all: "Voldi kõik kokku" + button_expand_all: "Voldi kõik lahti" + button_delete: "Kustuta" + button_create: "Loo" + button_create_and_continue: "Loo ja jätka" + button_test: "Testi" + button_edit: "Muuda" + button_edit_associated_wikipage: "Muuda seotud vikilehte: %{page_title}" + button_add: "Lisa" + button_change: "Muuda" + button_apply: "Lae" + button_clear: "Puhasta" + button_lock: "Lukusta" + button_unlock: "Ava lukust" + button_download: "Lae alla" + button_list: "Listi" + button_view: "Vaata" + button_move: "Tõsta" + button_move_and_follow: "Tõsta ja järgne" + button_back: "Tagasi" + button_cancel: "Katkesta" + button_activate: "Aktiveeri" + button_sort: "Sorteeri" + button_log_time: "Ajakulu" + button_rollback: "Rulli tagasi sellesse versiooni" + button_watch: "Jälgi" + button_unwatch: "Ära jälgi" + button_reply: "Vasta" + button_archive: "Arhiveeri" + button_unarchive: "Arhiivist tagasi" + button_reset: "Nulli" + button_rename: "Nimeta ümber" + button_change_password: "Vaheta parool" + button_copy: "Kopeeri" + button_copy_and_follow: "Kopeeri ja järgne" + button_annotate: "Annoteeri" + button_update: "Muuda" + button_configure: "Konfigureeri" + button_quote: "Tsiteeri" + button_duplicate: "Duplitseeri" + button_show: "Näita" + button_edit_section: "Muuda seda sektsiooni" + button_export: "Ekspordi" + button_delete_my_account: "Kustuta oma konto" + + status_active: "aktiivne" + status_registered: "registreeritud" + status_locked: "lukus" + + version_status_open: "avatud" + version_status_locked: "lukus" + version_status_closed: "suletud" + + field_active: "Aktiivne" + + text_select_mail_notifications: "Tegevused, millest peaks e-kirjaga teavitama" + text_regexp_info: "nt. ^[A-Z0-9]+$" + text_min_max_length_info: "0 tähendab, et piiranguid ei ole" + text_project_destroy_confirmation: "Oled Sa kindel oma soovis see projekt täielikult kustutada?" + text_subprojects_destroy_warning: "Alamprojekt(id) - %{value} - kustutatakse samuti." + text_workflow_edit: "Töövoo muutmiseks vali roll ja valdkond" + text_are_you_sure: "Oled Sa kindel?" + text_journal_changed: "%{label} muudetud %{old} -> %{new}" + text_journal_changed_no_detail: "%{label} uuendatud" + text_journal_set_to: "%{label} uus väärtus on %{value}" + text_journal_deleted: "%{label} kustutatud (%{old})" + text_journal_added: "%{label} %{value} lisatud" + text_tip_issue_begin_day: "teema avamise päev" + text_tip_issue_end_day: "teema sulgemise päev" + text_tip_issue_begin_end_day: "teema avati ja sulgeti samal päeval" + text_project_identifier_info: "Lubatud on ainult väikesed tähed (a-z), numbrid ja kriipsud.
    Peale salvestamist ei saa tunnust enam muuta." + text_caracters_maximum: "%{count} märki kõige rohkem." + text_caracters_minimum: "Peab olema vähemalt %{count} märki pikk." + text_length_between: "Pikkus %{min} kuni %{max} märki." + text_tracker_no_workflow: "Selle valdkonna jaoks ei ole ühtegi töövoogu kirjeldatud" + text_unallowed_characters: "Lubamatud märgid" + text_comma_separated: "Lubatud erinevad väärtused (komaga eraldatult)." + text_line_separated: "Lubatud erinevad väärtused (igaüks eraldi real)." + text_issues_ref_in_commit_messages: "Teemadele ja parandustele viitamine sissekannete märkustes" + text_issue_added: "%{author} lisas uue teema %{id}." + text_issue_updated: "%{author} uuendas teemat %{id}." + text_wiki_destroy_confirmation: "Oled Sa kindel oma soovis kustutada see Viki koos kogu sisuga?" + text_issue_category_destroy_question: "Kustutatavat kategooriat kasutab %{count} teema(t). Mis Sa soovid nendega ette võtta?" + text_issue_category_destroy_assignments: "Jäta teemadel kategooria määramata" + text_issue_category_reassign_to: "Määra teemad teise kategooriasse" + text_user_mail_option: "Valimata projektidest saad teavitusi ainult jälgitavate või Sinuga seotud asjade kohta (nt. Sinu loodud või teha teemad)." + text_no_configuration_data: "Rollid, valdkonnad, olekud ja töövood ei ole veel seadistatud.\nVäga soovitav on laadida vaikeasetused. Peale laadimist saad neid ise muuta." + text_load_default_configuration: "Lae vaikeasetused" + text_status_changed_by_changeset: "Kehtestatakse muudatuses %{value}." + text_time_logged_by_changeset: "Kehtestatakse muudatuses %{value}." + text_issues_destroy_confirmation: "Oled Sa kindel oma soovis valitud teema(d) kustutada?" + text_issues_destroy_descendants_confirmation: "See kustutab samuti %{count} alamteemat." + text_time_entries_destroy_confirmation: "Oled Sa kindel oma soovis valitud ajakulu kanne/kanded kustutada?" + text_select_project_modules: "Projektis kasutatavad moodulid" + text_default_administrator_account_changed: "Algne administraatori konto on muudetud" + text_file_repository_writable: "Manuste kataloog on kirjutatav" + text_plugin_assets_writable: "Lisamoodulite abifailide kataloog on kirjutatav" + text_rmagick_available: "RMagick on kasutatav (mittekohustuslik)" + text_destroy_time_entries_question: "Kustutatavatele teemadele oli kirja pandud %{hours} tundi. Mis Sa soovid ette võtta?" + text_destroy_time_entries: "Kustuta need tunnid" + text_assign_time_entries_to_project: "Vii tunnid üle teise projekti" + text_reassign_time_entries: "Määra tunnid sellele teemale:" + text_user_wrote: "%{value} kirjutas:" + text_enumeration_destroy_question: "Selle väärtusega on seotud %{count} objekt(i)." + text_enumeration_category_reassign_to: "Seo nad teise väärtuse külge:" + text_email_delivery_not_configured: "E-kirjade saatmine ei ole seadistatud ja teavitusi ei saadeta.\nKonfigureeri oma SMTP server failis config/configuration.yml ja taaskäivita Redmine." + text_repository_usernames_mapping: "Seosta Redmine kasutaja hoidlasse sissekannete tegijaga.\nSama nime või e-postiga kasutajad seostatakse automaatselt." + text_diff_truncated: "... Osa erinevusi jäi välja, sest neid on näitamiseks liiga palju." + text_custom_field_possible_values_info: "Üks rida iga väärtuse jaoks" + text_wiki_page_destroy_question: "Sel lehel on %{descendants} järglasleht(e) ja järeltulija(t). Mis Sa soovid ette võtta?" + text_wiki_page_nullify_children: "Muuda järglaslehed uuteks juurlehtedeks" + text_wiki_page_destroy_children: "Kustuta järglaslehed ja kõik nende järglased" + text_wiki_page_reassign_children: "Määra järglaslehed teise lehe külge" + text_own_membership_delete_confirmation: "Sa võtad endalt ära osa või kõik õigused ega saa edaspidi seda projekti võib-olla enam muuta.\nOled Sa jätkamises kindel?" + text_zoom_in: "Vaata lähemalt" + text_zoom_out: "Vaata kaugemalt" + text_warn_on_leaving_unsaved: "Sel lehel on salvestamata teksti, mis läheb kaduma, kui siit lehelt lahkud." + text_scm_path_encoding_note: "Vaikimisi UTF-8" + text_git_repository_note: "Hoidla peab olema paljas (bare) ja kohalik (nt. /gitrepo, c:\\gitrepo)" + text_mercurial_repository_note: "Hoidla peab olema kohalik (nt. /hgrepo, c:\\hgrepo)" + text_scm_command: "Hoidla poole pöördumise käsk" + text_scm_command_version: "Versioon" + text_scm_config: "Hoidlate poole pöördumist saab konfigureerida failis config/configuration.yml. Peale selle muutmist taaskäivita Redmine." + text_scm_command_not_available: "Hoidla poole pöördumine ebaõnnestus. Palun kontrolli seadistusi." + text_issue_conflict_resolution_overwrite: "Kehtesta oma muudatused (kõik märkused jäävad, ent muu võidakse üle kirjutada)" + text_issue_conflict_resolution_add_notes: "Lisa oma märkused, aga loobu teistest muudatustest" + text_issue_conflict_resolution_cancel: "Loobu kõigist muudatustest ja lae %{link} uuesti" + text_account_destroy_confirmation: "Oled Sa kindel?\nSu konto kustutatakse jäädavalt ja seda pole võimalik taastada." + + default_role_manager: "Haldaja" + default_role_developer: "Arendaja" + default_role_reporter: "Edastaja" + default_tracker_bug: "Veaparandus" + default_tracker_feature: "Täiendus" + default_tracker_support: "Klienditugi" + default_issue_status_new: "Avatud" + default_issue_status_in_progress: "Töös" + default_issue_status_resolved: "Lahendatud" + default_issue_status_feedback: "Tagasiside" + default_issue_status_closed: "Suletud" + default_issue_status_rejected: "Tagasi lükatud" + default_doc_category_user: "Juhend lõppkasutajale" + default_doc_category_tech: "Tehniline dokumentatsioon" + default_priority_low: "Aega on" + default_priority_normal: "Tavaline" + default_priority_high: "Pakiline" + default_priority_urgent: "Täna vaja" + default_priority_immediate: "Kohe vaja" + default_activity_design: "Kavandamine" + default_activity_development: "Arendamine" + + enumeration_issue_priorities: "Teemade prioriteedid" + enumeration_doc_categories: "Dokumentide kategooriad" + enumeration_activities: "Tegevused (ajakulu)" + enumeration_system_activity: "Süsteemi aktiivsus" + description_filter: "Filter" + description_search: "Otsinguväli" + description_choose_project: "Projektid" + description_project_scope: "Otsingu ulatus" + description_notes: "Märkused" + description_message_content: "Postituse sisu" + description_query_sort_criteria_attribute: "Sorteerimise kriteerium" + description_query_sort_criteria_direction: "Sorteerimise suund" + description_user_mail_notification: "E-kirjaga teavitamise seaded" + description_available_columns: "Kasutatavad veerud" + description_selected_columns: "Valitud veerud" + description_all_columns: "Kõik veerud" + description_issue_category_reassign: "Vali uus kategooria" + description_wiki_subpages_reassign: "Vali lehele uus vanem" + description_date_range_list: "Vali vahemik nimekirjast" + description_date_range_interval: "Vali vahemik algus- ja lõpukuupäeva abil" + description_date_from: "Sisesta alguskuupäev" + description_date_to: "Sisesta lõpukuupäev" + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: "Lubatud on ainult väikesed tähed (a-z), numbrid ja kriipsud.
    Peale salvestamist ei saa tunnust enam muuta." + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: "kõik" + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: "alamprojektidega" + label_cross_project_tree: "projektipuuga" + label_cross_project_hierarchy: "projektihierarhiaga" + label_cross_project_system: "kõigi projektidega" + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/09/09ea8a2e3065e6d0cfba8be5ebe32c3e0767f60e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/09/09ea8a2e3065e6d0cfba8be5ebe32c3e0767f60e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,46 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Activity + + mattr_accessor :available_event_types, :default_event_types, :providers + + @@available_event_types = [] + @@default_event_types = [] + @@providers = Hash.new {|h,k| h[k]=[] } + + class << self + def map(&block) + yield self + end + + # Registers an activity provider + def register(event_type, options={}) + options.assert_valid_keys(:class_name, :default) + + event_type = event_type.to_s + providers = options[:class_name] || event_type.classify + providers = ([] << providers) unless providers.is_a?(Array) + + @@available_event_types << event_type unless @@available_event_types.include?(event_type) + @@default_event_types << event_type unless options[:default] == false + @@providers[event_type] += providers + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0a/0a055f44f53eaf38a0291650c6688831162451af.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0a/0a055f44f53eaf38a0291650c6688831162451af.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,4 @@ +$('#message_subject').val("<%= raw escape_javascript(@subject) %>"); +$('#message_content').val("<%= raw escape_javascript(@content) %>"); +showAndScrollTo("reply", "message_content"); +$('#message_content').scrollTop = $('#message_content').scrollHeight - $('#message_content').clientHeight; diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0a/0a53e228ad2ca76e975fdb76cc443721ba07788d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0a/0a53e228ad2ca76e975fdb76cc443721ba07788d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,86 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Group < Principal + include Redmine::SafeAttributes + + has_and_belongs_to_many :users, :after_add => :user_added, + :after_remove => :user_removed + + acts_as_customizable + + validates_presence_of :lastname + validates_uniqueness_of :lastname, :case_sensitive => false + validates_length_of :lastname, :maximum => 30 + + before_destroy :remove_references_before_destroy + + scope :sorted, order("#{table_name}.lastname ASC") + + safe_attributes 'name', + 'user_ids', + 'custom_field_values', + 'custom_fields', + :if => lambda {|group, user| user.admin?} + + def to_s + lastname.to_s + end + + def name + lastname + end + + def name=(arg) + self.lastname = arg + end + + def user_added(user) + members.each do |member| + next if member.project.nil? + user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id) + member.member_roles.each do |member_role| + user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id) + end + user_member.save! + end + end + + def user_removed(user) + members.each do |member| + MemberRole.find(:all, :include => :member, + :conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy) + end + end + + def self.human_attribute_name(attribute_key_name, *args) + attr_name = attribute_key_name.to_s + if attr_name == 'lastname' + attr_name = "name" + end + super(attr_name, *args) + end + + private + + # Removes references that are not handled by associations + def remove_references_before_destroy + return if self.id.nil? + + Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id] + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0a/0a7193067757b43aa4a973fedb71a5a6507643df.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0a/0a7193067757b43aa4a973fedb71a5a6507643df.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,103 @@ +--- +changesets_001: + commit_date: 2007-04-11 + committed_on: 2007-04-11 15:14:44 +02:00 + revision: 1 + id: 100 + comments: My very first commit + repository_id: 10 + committer: dlopper + user_id: 3 +changesets_002: + commit_date: 2007-04-12 + committed_on: 2007-04-12 15:14:44 +02:00 + revision: 2 + id: 101 + comments: 'This commit fixes #1, #2 and references #1 & #3' + repository_id: 10 + committer: dlopper + user_id: 3 +changesets_003: + commit_date: 2007-04-12 + committed_on: 2007-04-12 15:14:44 +02:00 + revision: 3 + id: 102 + comments: |- + A commit with wrong issue ids + IssueID #666 #3 + repository_id: 10 + committer: dlopper + user_id: 3 +changesets_004: + commit_date: 2007-04-12 + committed_on: 2007-04-12 15:14:44 +02:00 + revision: 4 + id: 103 + comments: |- + A commit with an issue id of an other project + IssueID 4 2 + repository_id: 10 + committer: dlopper + user_id: 3 +changesets_005: + commit_date: "2007-09-10" + comments: Modified one file in the folder. + committed_on: 2007-09-10 19:01:08 + revision: "5" + id: 104 + scmid: + user_id: 3 + repository_id: 10 + committer: dlopper +changesets_006: + commit_date: "2007-09-10" + comments: Moved helloworld.rb from / to /folder. + committed_on: 2007-09-10 19:01:47 + revision: "6" + id: 105 + scmid: + user_id: 3 + repository_id: 10 + committer: dlopper +changesets_007: + commit_date: "2007-09-10" + comments: Removed one file. + committed_on: 2007-09-10 19:02:16 + revision: "7" + id: 106 + scmid: + user_id: 3 + repository_id: 10 + committer: dlopper +changesets_008: + commit_date: "2007-09-10" + comments: |- + This commits references an issue. + Refs #2 + committed_on: 2007-09-10 19:04:35 + revision: "8" + id: 107 + scmid: + user_id: 3 + repository_id: 10 + committer: dlopper +changesets_009: + commit_date: "2009-09-10" + comments: One file added. + committed_on: 2009-09-10 19:04:35 + revision: "9" + id: 108 + scmid: + user_id: 3 + repository_id: 10 + committer: dlopper +changesets_010: + commit_date: "2009-09-10" + comments: Same file modified. + committed_on: 2009-09-10 19:04:35 + revision: "10" + id: 109 + scmid: + user_id: 3 + repository_id: 10 + committer: dlopper diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0a/0ac45c1808f5ee15f2487069dcbe64385e65e79f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0a/0ac45c1808f5ee15f2487069dcbe64385e65e79f.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,115 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Tracker < ActiveRecord::Base + + CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze + # Fields that can be disabled + # Other (future) fields should be appended, not inserted! + CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio).freeze + CORE_FIELDS_ALL = (CORE_FIELDS_UNDISABLABLE + CORE_FIELDS).freeze + + before_destroy :check_integrity + has_many :issues + has_many :workflow_rules, :dependent => :delete_all do + def copy(source_tracker) + WorkflowRule.copy(source_tracker, nil, proxy_association.owner, nil) + end + end + + has_and_belongs_to_many :projects + has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id' + acts_as_list + + attr_protected :field_bits + + validates_presence_of :name + validates_uniqueness_of :name + validates_length_of :name, :maximum => 30 + + scope :sorted, order("#{table_name}.position ASC") + scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)} + + def to_s; name end + + def <=>(tracker) + position <=> tracker.position + end + + # Returns an array of IssueStatus that are used + # in the tracker's workflows + def issue_statuses + if @issue_statuses + return @issue_statuses + elsif new_record? + return [] + end + + ids = WorkflowTransition. + connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{WorkflowTransition.table_name} WHERE tracker_id = #{id} AND type = 'WorkflowTransition'"). + flatten. + uniq + + @issue_statuses = IssueStatus.find_all_by_id(ids).sort + end + + def disabled_core_fields + i = -1 + @disabled_core_fields ||= CORE_FIELDS.select { i += 1; (fields_bits || 0) & (2 ** i) != 0} + end + + def core_fields + CORE_FIELDS - disabled_core_fields + end + + def core_fields=(fields) + raise ArgumentError.new("Tracker.core_fields takes an array") unless fields.is_a?(Array) + + bits = 0 + CORE_FIELDS.each_with_index do |field, i| + unless fields.include?(field) + bits |= 2 ** i + end + end + self.fields_bits = bits + @disabled_core_fields = nil + core_fields + end + + # Returns the fields that are disabled for all the given trackers + def self.disabled_core_fields(trackers) + if trackers.present? + trackers.uniq.map(&:disabled_core_fields).reduce(:&) + else + [] + end + end + + # Returns the fields that are enabled for one tracker at least + def self.core_fields(trackers) + if trackers.present? + trackers.uniq.map(&:core_fields).reduce(:|) + else + CORE_FIELDS.dup + end + end + +private + def check_integrity + raise Exception.new("Can't delete tracker") if Issue.where(:tracker_id => self.id).any? + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0b/0b1931e7310d3bdeabe0a2c171beeaecda67ff41.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0b/0b1931e7310d3bdeabe0a2c171beeaecda67ff41.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,27 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Platform + class << self + def mswin? + (RUBY_PLATFORM =~ /(:?mswin|mingw)/) || + (RUBY_PLATFORM == 'java' && (ENV['OS'] || ENV['os']) =~ /windows/i) + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0b/0b2cfb5e453d4c5b12b6d8b9ca9e37acb99e7715.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0b/0b2cfb5e453d4c5b12b6d8b9ca9e37acb99e7715.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,58 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingDocumentsTest < ActionController::IntegrationTest + def test_documents_scoped_under_project + assert_routing( + { :method => 'get', :path => "/projects/567/documents" }, + { :controller => 'documents', :action => 'index', :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/documents/new" }, + { :controller => 'documents', :action => 'new', :project_id => '567' } + ) + assert_routing( + { :method => 'post', :path => "/projects/567/documents" }, + { :controller => 'documents', :action => 'create', :project_id => '567' } + ) + end + + def test_documents + assert_routing( + { :method => 'get', :path => "/documents/22" }, + { :controller => 'documents', :action => 'show', :id => '22' } + ) + assert_routing( + { :method => 'get', :path => "/documents/22/edit" }, + { :controller => 'documents', :action => 'edit', :id => '22' } + ) + assert_routing( + { :method => 'put', :path => "/documents/22" }, + { :controller => 'documents', :action => 'update', :id => '22' } + ) + assert_routing( + { :method => 'delete', :path => "/documents/22" }, + { :controller => 'documents', :action => 'destroy', :id => '22' } + ) + assert_routing( + { :method => 'post', :path => "/documents/22/add_attachment" }, + { :controller => 'documents', :action => 'add_attachment', :id => '22' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0b/0bec89d5c0dbedaa5bc5e8099c531dc64fe9abe2.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0b/0bec89d5c0dbedaa5bc5e8099c531dc64fe9abe2.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,3 @@ +<%= l(:text_issue_added, :id => "##{@issue.id}", :author => h(@issue.author)) %> +
    +<%= render :partial => 'issue', :formats => [:html], :locals => { :issue => @issue, :issue_url => @issue_url } %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0b/0bf8f19f5486edff5718369b36db7c358c6ec343.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0b/0bf8f19f5486edff5718369b36db7c358c6ec343.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,73 @@ +<%= labelled_fields_for :issue, @issue do |f| %> + +
    +
    +<% if @issue.safe_attribute?('status_id') && @allowed_statuses.present? %> +

    <%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), {:required => true}, + :onchange => "updateIssueFrom('#{escape_javascript project_issue_form_path(@project, :id => @issue, :format => 'js')}')" %>

    + +<% else %> +

    <%= h(@issue.status.name) %>

    +<% end %> + +<% if @issue.safe_attribute? 'priority_id' %> +

    <%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true}, :disabled => !@issue.leaf? %>

    +<% end %> + +<% if @issue.safe_attribute? 'assigned_to_id' %> +

    <%= f.select :assigned_to_id, principals_options_for_select(@issue.assignable_users, @issue.assigned_to), :include_blank => true, :required => @issue.required_attribute?('assigned_to_id') %>

    +<% end %> + +<% if @issue.safe_attribute?('category_id') && @issue.project.issue_categories.any? %> +

    <%= f.select :category_id, (@issue.project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true, :required => @issue.required_attribute?('category_id') %> +<%= link_to(image_tag('add.png', :style => 'vertical-align: middle;'), + new_project_issue_category_path(@issue.project), + :remote => true, + :method => 'get', + :title => l(:label_issue_category_new), + :tabindex => 200) if User.current.allowed_to?(:manage_categories, @issue.project) %>

    +<% end %> + +<% if @issue.safe_attribute?('fixed_version_id') && @issue.assignable_versions.any? %> +

    <%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true, :required => @issue.required_attribute?('fixed_version_id') %> +<%= link_to(image_tag('add.png', :style => 'vertical-align: middle;'), + new_project_version_path(@issue.project), + :remote => true, + :method => 'get', + :title => l(:label_version_new), + :tabindex => 200) if User.current.allowed_to?(:manage_versions, @issue.project) %> +

    +<% end %> +
    + +
    +<% if @issue.safe_attribute? 'parent_issue_id' %> +

    <%= f.text_field :parent_issue_id, :size => 10, :required => @issue.required_attribute?('parent_issue_id') %>

    +<%= javascript_tag "observeAutocompleteField('issue_parent_issue_id', '#{escape_javascript auto_complete_issues_path}')" %> +<% end %> + +<% if @issue.safe_attribute? 'start_date' %> +

    <%= f.text_field :start_date, :size => 10, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('start_date') %><%= calendar_for('issue_start_date') if @issue.leaf? %>

    +<% end %> + +<% if @issue.safe_attribute? 'due_date' %> +

    <%= f.text_field :due_date, :size => 10, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('due_date') %><%= calendar_for('issue_due_date') if @issue.leaf? %>

    +<% end %> + +<% if @issue.safe_attribute? 'estimated_hours' %> +

    <%= f.text_field :estimated_hours, :size => 3, :disabled => !@issue.leaf?, :required => @issue.required_attribute?('estimated_hours') %> <%= l(:field_hours) %>

    +<% end %> + +<% if @issue.safe_attribute?('done_ratio') && @issue.leaf? && Issue.use_field_for_done_ratio? %> +

    <%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :required => @issue.required_attribute?('done_ratio') %>

    +<% end %> +
    +
    + +<% if @issue.safe_attribute? 'custom_field_values' %> +<%= render :partial => 'issues/form_custom_fields' %> +<% end %> + +<% end %> + +<% include_calendar_headers_tags %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0c/0c006adf29e0abdbaa9644cee502d6882d303828.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0c006adf29e0abdbaa9644cee502d6882d303828.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,98 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module MimeType + + MIME_TYPES = { + 'text/plain' => 'txt,tpl,properties,patch,diff,ini,readme,install,upgrade', + 'text/css' => 'css', + 'text/html' => 'html,htm,xhtml', + 'text/jsp' => 'jsp', + 'text/x-c' => 'c,cpp,cc,h,hh', + 'text/x-csharp' => 'cs', + 'text/x-java' => 'java', + 'text/x-html-template' => 'rhtml', + 'text/x-perl' => 'pl,pm', + 'text/x-php' => 'php,php3,php4,php5', + 'text/x-python' => 'py', + 'text/x-ruby' => 'rb,rbw,ruby,rake,erb', + 'text/x-csh' => 'csh', + 'text/x-sh' => 'sh', + 'text/xml' => 'xml,xsd,mxml', + 'text/yaml' => 'yml,yaml', + 'text/csv' => 'csv', + 'text/x-po' => 'po', + 'image/gif' => 'gif', + 'image/jpeg' => 'jpg,jpeg,jpe', + 'image/png' => 'png', + 'image/tiff' => 'tiff,tif', + 'image/x-ms-bmp' => 'bmp', + 'image/x-xpixmap' => 'xpm', + 'image/svg+xml'=> 'svg', + 'application/javascript' => 'js', + 'application/pdf' => 'pdf', + 'application/rtf' => 'rtf', + 'application/msword' => 'doc', + 'application/vnd.ms-excel' => 'xls', + 'application/vnd.ms-powerpoint' => 'ppt,pps', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => 'ppsx', + 'application/vnd.oasis.opendocument.spreadsheet' => 'ods', + 'application/vnd.oasis.opendocument.text' => 'odt', + 'application/vnd.oasis.opendocument.presentation' => 'odp', + 'application/x-7z-compressed' => '7z', + 'application/x-rar-compressed' => 'rar', + 'application/x-tar' => 'tar', + 'application/zip' => 'zip', + 'application/x-gzip' => 'gz', + }.freeze + + EXTENSIONS = MIME_TYPES.inject({}) do |map, (type, exts)| + exts.split(',').each {|ext| map[ext.strip] = type} + map + end + + # returns mime type for name or nil if unknown + def self.of(name) + return nil unless name + m = name.to_s.match(/(^|\.)([^\.]+)$/) + EXTENSIONS[m[2].downcase] if m + end + + # Returns the css class associated to + # the mime type of name + def self.css_class_of(name) + mime = of(name) + mime && mime.gsub('/', '-') + end + + def self.main_mimetype_of(name) + mimetype = of(name) + mimetype.split('/').first if mimetype + end + + # return true if mime-type for name is type/* + # otherwise false + def self.is_type?(type, name) + main_mimetype = main_mimetype_of(name) + type.to_s == main_mimetype + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0c/0c20b053aa02a18ebfa242903b54c5d2423345a1.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0c20b053aa02a18ebfa242903b54c5d2423345a1.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,61 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingRolesTest < ActionController::IntegrationTest + def test_roles + assert_routing( + { :method => 'get', :path => "/roles" }, + { :controller => 'roles', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/roles.xml" }, + { :controller => 'roles', :action => 'index', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/roles/2.xml" }, + { :controller => 'roles', :action => 'show', :id => '2', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/roles/new" }, + { :controller => 'roles', :action => 'new' } + ) + assert_routing( + { :method => 'post', :path => "/roles" }, + { :controller => 'roles', :action => 'create' } + ) + assert_routing( + { :method => 'get', :path => "/roles/2/edit" }, + { :controller => 'roles', :action => 'edit', :id => '2' } + ) + assert_routing( + { :method => 'put', :path => "/roles/2" }, + { :controller => 'roles', :action => 'update', :id => '2' } + ) + assert_routing( + { :method => 'delete', :path => "/roles/2" }, + { :controller => 'roles', :action => 'destroy', :id => '2' } + ) + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/roles/permissions" }, + { :controller => 'roles', :action => 'permissions' } + ) + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0c/0c42f93f79c01cc0c9d6a9263bd85bc764f22f6f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0c42f93f79c01cc0c9d6a9263bd85bc764f22f6f.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1088 @@ +# Portuguese localization for Ruby on Rails +# by Ricardo Otero +# by Alberto Ferreira +pt: + support: + array: + sentence_connector: "e" + skip_last_comma: true + + direction: ltr + date: + formats: + default: "%d/%m/%Y" + short: "%d de %B" + long: "%d de %B de %Y" + only_day: "%d" + day_names: [Domingo, Segunda, Terça, Quarta, Quinta, Sexta, Sábado] + abbr_day_names: [Dom, Seg, Ter, Qua, Qui, Sex, Sáb] + month_names: [~, Janeiro, Fevereiro, Março, Abril, Maio, Junho, Julho, Agosto, Setembro, Outubro, Novembro, Dezembro] + abbr_month_names: [~, Jan, Fev, Mar, Abr, Mai, Jun, Jul, Ago, Set, Out, Nov, Dez] + order: + - :day + - :month + - :year + + time: + formats: + default: "%A, %d de %B de %Y, %H:%Mh" + time: "%H:%M" + short: "%d/%m, %H:%M hs" + long: "%A, %d de %B de %Y, %H:%Mh" + am: '' + pm: '' + + datetime: + distance_in_words: + half_a_minute: "meio minuto" + less_than_x_seconds: + one: "menos de 1 segundo" + other: "menos de %{count} segundos" + x_seconds: + one: "1 segundo" + other: "%{count} segundos" + less_than_x_minutes: + one: "menos de um minuto" + other: "menos de %{count} minutos" + x_minutes: + one: "1 minuto" + other: "%{count} minutos" + about_x_hours: + one: "aproximadamente 1 hora" + other: "aproximadamente %{count} horas" + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "1 dia" + other: "%{count} dias" + about_x_months: + one: "aproximadamente 1 mês" + other: "aproximadamente %{count} meses" + x_months: + one: "1 mês" + other: "%{count} meses" + about_x_years: + one: "aproximadamente 1 ano" + other: "aproximadamente %{count} anos" + over_x_years: + one: "mais de 1 ano" + other: "mais de %{count} anos" + almost_x_years: + one: "almost 1 year" + other: "almost %{count} years" + + number: + format: + precision: 3 + separator: ',' + delimiter: '.' + currency: + format: + unit: '€' + precision: 2 + format: "%u %n" + separator: ',' + delimiter: '.' + percentage: + format: + delimiter: '' + precision: + format: + delimiter: '' + human: + format: + precision: 3 + delimiter: '' + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + + activerecord: + errors: + template: + header: + one: "Não foi possível guardar %{model}: 1 erro" + other: "Não foi possível guardar %{model}: %{count} erros" + body: "Por favor, verifique os seguintes campos:" + messages: + inclusion: "não está incluído na lista" + exclusion: "não está disponível" + invalid: "não é válido" + confirmation: "não está de acordo com a confirmação" + accepted: "precisa de ser aceite" + empty: "não pode estar em branco" + blank: "não pode estar em branco" + too_long: "tem demasiados caracteres (máximo: %{count} caracteres)" + too_short: "tem poucos caracteres (mínimo: %{count} caracteres)" + wrong_length: "não é do tamanho correcto (necessita de ter %{count} caracteres)" + taken: "não está disponível" + not_a_number: "não é um número" + greater_than: "tem de ser maior do que %{count}" + greater_than_or_equal_to: "tem de ser maior ou igual a %{count}" + equal_to: "tem de ser igual a %{count}" + less_than: "tem de ser menor do que %{count}" + less_than_or_equal_to: "tem de ser menor ou igual a %{count}" + odd: "tem de ser ímpar" + even: "tem de ser par" + greater_than_start_date: "deve ser maior que a data inicial" + not_same_project: "não pertence ao mesmo projecto" + circular_dependency: "Esta relação iria criar uma dependência circular" + cant_link_an_issue_with_a_descendant: "Não é possível ligar uma tarefa a uma sub-tarefa que lhe é pertencente" + + ## Translated by: Pedro Araújo + actionview_instancetag_blank_option: Seleccione + + general_text_No: 'Não' + general_text_Yes: 'Sim' + general_text_no: 'não' + general_text_yes: 'sim' + general_lang_name: 'Português' + general_csv_separator: ';' + general_csv_decimal_separator: ',' + general_csv_encoding: ISO-8859-15 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: A conta foi actualizada com sucesso. + notice_account_invalid_creditentials: Utilizador ou palavra-chave inválidos. + notice_account_password_updated: A palavra-chave foi alterada com sucesso. + notice_account_wrong_password: Palavra-chave errada. + notice_account_register_done: A conta foi criada com sucesso. + notice_account_unknown_email: Utilizador desconhecido. + notice_can_t_change_password: Esta conta utiliza uma fonte de autenticação externa. Não é possível alterar a palavra-chave. + notice_account_lost_email_sent: Foi-lhe enviado um e-mail com as instruções para escolher uma nova palavra-chave. + notice_account_activated: A sua conta foi activada. É agora possível autenticar-se. + notice_successful_create: Criado com sucesso. + notice_successful_update: Alterado com sucesso. + notice_successful_delete: Apagado com sucesso. + notice_successful_connection: Ligado com sucesso. + notice_file_not_found: A página que está a tentar aceder não existe ou foi removida. + notice_locking_conflict: Os dados foram actualizados por outro utilizador. + notice_not_authorized: Não está autorizado a visualizar esta página. + notice_email_sent: "Foi enviado um e-mail para %{value}" + notice_email_error: "Ocorreu um erro ao enviar o e-mail (%{value})" + notice_feeds_access_key_reseted: A sua chave de RSS foi inicializada. + notice_failed_to_save_issues: "Não foi possível guardar %{count} tarefa(s) das %{total} seleccionadas: %{ids}." + notice_no_issue_selected: "Nenhuma tarefa seleccionada! Por favor, seleccione as tarefas que quer editar." + notice_account_pending: "A sua conta foi criada e está agora à espera de aprovação do administrador." + notice_default_data_loaded: Configuração padrão carregada com sucesso. + notice_unable_delete_version: Não foi possível apagar a versão. + + error_can_t_load_default_data: "Não foi possível carregar a configuração padrão: %{value}" + error_scm_not_found: "A entrada ou revisão não foi encontrada no repositório." + error_scm_command_failed: "Ocorreu um erro ao tentar aceder ao repositório: %{value}" + error_scm_annotate: "A entrada não existe ou não pode ser anotada." + error_issue_not_found_in_project: 'A tarefa não foi encontrada ou não pertence a este projecto.' + + mail_subject_lost_password: "Palavra-chave de %{value}" + mail_body_lost_password: 'Para mudar a sua palavra-chave, clique na ligação abaixo:' + mail_subject_register: "Activação de conta de %{value}" + mail_body_register: 'Para activar a sua conta, clique na ligação abaixo:' + mail_body_account_information_external: "Pode utilizar a conta %{value} para autenticar-se." + mail_body_account_information: Informação da sua conta + mail_subject_account_activation_request: "Pedido de activação da conta %{value}" + mail_body_account_activation_request: "Um novo utilizador (%{value}) registou-se. A sua conta está à espera de aprovação:" + mail_subject_reminder: "%{count} tarefa(s) para entregar nos próximos %{days} dias" + mail_body_reminder: "%{count} tarefa(s) que estão atribuídas a si estão agendadas para estarem completas nos próximos %{days} dias:" + + gui_validation_error: 1 erro + gui_validation_error_plural: "%{count} erros" + + field_name: Nome + field_description: Descrição + field_summary: Sumário + field_is_required: Obrigatório + field_firstname: Nome + field_lastname: Apelido + field_mail: E-mail + field_filename: Ficheiro + field_filesize: Tamanho + field_downloads: Downloads + field_author: Autor + field_created_on: Criado + field_updated_on: Alterado + field_field_format: Formato + field_is_for_all: Para todos os projectos + field_possible_values: Valores possíveis + field_regexp: Expressão regular + field_min_length: Tamanho mínimo + field_max_length: Tamanho máximo + field_value: Valor + field_category: Categoria + field_title: Título + field_project: Projecto + field_issue: Tarefa + field_status: Estado + field_notes: Notas + field_is_closed: Tarefa fechada + field_is_default: Valor por omissão + field_tracker: Tipo + field_subject: Assunto + field_due_date: Data fim + field_assigned_to: Atribuído a + field_priority: Prioridade + field_fixed_version: Versão + field_user: Utilizador + field_role: Função + field_homepage: Página + field_is_public: Público + field_parent: Sub-projecto de + field_is_in_roadmap: Tarefas mostradas no mapa de planificação + field_login: Nome de utilizador + field_mail_notification: Notificações por e-mail + field_admin: Administrador + field_last_login_on: Última visita + field_language: Língua + field_effective_date: Data + field_password: Palavra-chave + field_new_password: Nova palavra-chave + field_password_confirmation: Confirmação + field_version: Versão + field_type: Tipo + field_host: Servidor + field_port: Porta + field_account: Conta + field_base_dn: Base DN + field_attr_login: Atributo utilizador + field_attr_firstname: Atributo nome próprio + field_attr_lastname: Atributo último nome + field_attr_mail: Atributo e-mail + field_onthefly: Criação imediata de utilizadores + field_start_date: Data início + field_done_ratio: "% Completo" + field_auth_source: Modo de autenticação + field_hide_mail: Esconder endereço de e-mail + field_comments: Comentário + field_url: URL + field_start_page: Página inicial + field_subproject: Subprojecto + field_hours: Horas + field_activity: Actividade + field_spent_on: Data + field_identifier: Identificador + field_is_filter: Usado como filtro + field_issue_to: Tarefa relacionada + field_delay: Atraso + field_assignable: As tarefas podem ser associadas a esta função + field_redirect_existing_links: Redireccionar ligações existentes + field_estimated_hours: Tempo estimado + field_column_names: Colunas + field_time_zone: Fuso horário + field_searchable: Procurável + field_default_value: Valor por omissão + field_comments_sorting: Mostrar comentários + field_parent_title: Página pai + + setting_app_title: Título da aplicação + setting_app_subtitle: Sub-título da aplicação + setting_welcome_text: Texto de boas vindas + setting_default_language: Língua por omissão + setting_login_required: Autenticação obrigatória + setting_self_registration: Auto-registo + setting_attachment_max_size: Tamanho máximo do anexo + setting_issues_export_limit: Limite de exportação das tarefas + setting_mail_from: E-mail enviado de + setting_bcc_recipients: Recipientes de BCC + setting_host_name: Hostname + setting_text_formatting: Formatação do texto + setting_wiki_compression: Compressão do histórico do Wiki + setting_feeds_limit: Limite de conteúdo do feed + setting_default_projects_public: Projectos novos são públicos por omissão + setting_autofetch_changesets: Buscar automaticamente commits + setting_sys_api_enabled: Activar Web Service para gestão do repositório + setting_commit_ref_keywords: Palavras-chave de referência + setting_commit_fix_keywords: Palavras-chave de fecho + setting_autologin: Login automático + setting_date_format: Formato da data + setting_time_format: Formato do tempo + setting_cross_project_issue_relations: Permitir relações entre tarefas de projectos diferentes + setting_issue_list_default_columns: Colunas na lista de tarefas por omissão + setting_emails_footer: Rodapé do e-mails + setting_protocol: Protocolo + setting_per_page_options: Opções de objectos por página + setting_user_format: Formato de apresentaão de utilizadores + setting_activity_days_default: Dias mostrados na actividade do projecto + setting_display_subprojects_issues: Mostrar as tarefas dos sub-projectos nos projectos principais + setting_enabled_scm: Activar SCM + setting_mail_handler_api_enabled: Activar Web Service para e-mails recebidos + setting_mail_handler_api_key: Chave da API + setting_sequential_project_identifiers: Gerar identificadores de projecto sequênciais + + project_module_issue_tracking: Tarefas + project_module_time_tracking: Registo de tempo + project_module_news: Notícias + project_module_documents: Documentos + project_module_files: Ficheiros + project_module_wiki: Wiki + project_module_repository: Repositório + project_module_boards: Forum + + label_user: Utilizador + label_user_plural: Utilizadores + label_user_new: Novo utilizador + label_project: Projecto + label_project_new: Novo projecto + label_project_plural: Projectos + label_x_projects: + zero: no projects + one: 1 project + other: "%{count} projects" + label_project_all: Todos os projectos + label_project_latest: Últimos projectos + label_issue: Tarefa + label_issue_new: Nova tarefa + label_issue_plural: Tarefas + label_issue_view_all: Ver todas as tarefas + label_issues_by: "Tarefas por %{value}" + label_issue_added: Tarefa adicionada + label_issue_updated: Tarefa actualizada + label_document: Documento + label_document_new: Novo documento + label_document_plural: Documentos + label_document_added: Documento adicionado + label_role: Função + label_role_plural: Funções + label_role_new: Nova função + label_role_and_permissions: Funções e permissões + label_member: Membro + label_member_new: Novo membro + label_member_plural: Membros + label_tracker: Tipo + label_tracker_plural: Tipos + label_tracker_new: Novo tipo + label_workflow: Fluxo de trabalho + label_issue_status: Estado da tarefa + label_issue_status_plural: Estados da tarefa + label_issue_status_new: Novo estado + label_issue_category: Categoria de tarefa + label_issue_category_plural: Categorias de tarefa + label_issue_category_new: Nova categoria + label_custom_field: Campo personalizado + label_custom_field_plural: Campos personalizados + label_custom_field_new: Novo campo personalizado + label_enumerations: Enumerações + label_enumeration_new: Novo valor + label_information: Informação + label_information_plural: Informações + label_please_login: Por favor autentique-se + label_register: Registar + label_password_lost: Perdi a palavra-chave + label_home: Página Inicial + label_my_page: Página Pessoal + label_my_account: Minha conta + label_my_projects: Meus projectos + label_administration: Administração + label_login: Entrar + label_logout: Sair + label_help: Ajuda + label_reported_issues: Tarefas criadas + label_assigned_to_me_issues: Tarefas atribuídas a mim + label_last_login: Último acesso + label_registered_on: Registado em + label_activity: Actividade + label_overall_activity: Actividade geral + label_new: Novo + label_logged_as: Ligado como + label_environment: Ambiente + label_authentication: Autenticação + label_auth_source: Modo de autenticação + label_auth_source_new: Novo modo de autenticação + label_auth_source_plural: Modos de autenticação + label_subproject_plural: Sub-projectos + label_and_its_subprojects: "%{value} e sub-projectos" + label_min_max_length: Tamanho mínimo-máximo + label_list: Lista + label_date: Data + label_integer: Inteiro + label_float: Decimal + label_boolean: Booleano + label_string: Texto + label_text: Texto longo + label_attribute: Atributo + label_attribute_plural: Atributos + label_download: "%{count} Download" + label_download_plural: "%{count} Downloads" + label_no_data: Sem dados para mostrar + label_change_status: Mudar estado + label_history: Histórico + label_attachment: Ficheiro + label_attachment_new: Novo ficheiro + label_attachment_delete: Apagar ficheiro + label_attachment_plural: Ficheiros + label_file_added: Ficheiro adicionado + label_report: Relatório + label_report_plural: Relatórios + label_news: Notícia + label_news_new: Nova notícia + label_news_plural: Notícias + label_news_latest: Últimas notícias + label_news_view_all: Ver todas as notícias + label_news_added: Notícia adicionada + label_settings: Configurações + label_overview: Visão geral + label_version: Versão + label_version_new: Nova versão + label_version_plural: Versões + label_confirmation: Confirmação + label_export_to: 'Também disponível em:' + label_read: Ler... + label_public_projects: Projectos públicos + label_open_issues: aberto + label_open_issues_plural: abertos + label_closed_issues: fechado + label_closed_issues_plural: fechados + label_x_open_issues_abbr_on_total: + zero: 0 open / %{total} + one: 1 open / %{total} + other: "%{count} open / %{total}" + label_x_open_issues_abbr: + zero: 0 open + one: 1 open + other: "%{count} open" + label_x_closed_issues_abbr: + zero: 0 closed + one: 1 closed + other: "%{count} closed" + label_total: Total + label_permissions: Permissões + label_current_status: Estado actual + label_new_statuses_allowed: Novos estados permitidos + label_all: todos + label_none: nenhum + label_nobody: ninguém + label_next: Próximo + label_previous: Anterior + label_used_by: Usado por + label_details: Detalhes + label_add_note: Adicionar nota + label_per_page: Por página + label_calendar: Calendário + label_months_from: meses de + label_gantt: Gantt + label_internal: Interno + label_last_changes: "últimas %{count} alterações" + label_change_view_all: Ver todas as alterações + label_personalize_page: Personalizar esta página + label_comment: Comentário + label_comment_plural: Comentários + label_x_comments: + zero: no comments + one: 1 comment + other: "%{count} comments" + label_comment_add: Adicionar comentário + label_comment_added: Comentário adicionado + label_comment_delete: Apagar comentários + label_query: Consulta personalizada + label_query_plural: Consultas personalizadas + label_query_new: Nova consulta + label_filter_add: Adicionar filtro + label_filter_plural: Filtros + label_equals: é + label_not_equals: não é + label_in_less_than: em menos de + label_in_more_than: em mais de + label_in: em + label_today: hoje + label_all_time: sempre + label_yesterday: ontem + label_this_week: esta semana + label_last_week: semana passada + label_last_n_days: "últimos %{count} dias" + label_this_month: este mês + label_last_month: mês passado + label_this_year: este ano + label_date_range: Date range + label_less_than_ago: menos de dias atrás + label_more_than_ago: mais de dias atrás + label_ago: dias atrás + label_contains: contém + label_not_contains: não contém + label_day_plural: dias + label_repository: Repositório + label_repository_plural: Repositórios + label_browse: Navegar + label_modification: "%{count} alteração" + label_modification_plural: "%{count} alterações" + label_revision: Revisão + label_revision_plural: Revisões + label_associated_revisions: Revisões associadas + label_added: adicionado + label_modified: modificado + label_copied: copiado + label_renamed: renomeado + label_deleted: apagado + label_latest_revision: Última revisão + label_latest_revision_plural: Últimas revisões + label_view_revisions: Ver revisões + label_max_size: Tamanho máximo + label_sort_highest: Mover para o início + label_sort_higher: Mover para cima + label_sort_lower: Mover para baixo + label_sort_lowest: Mover para o fim + label_roadmap: Planificação + label_roadmap_due_in: "Termina em %{value}" + label_roadmap_overdue: "Atrasado %{value}" + label_roadmap_no_issues: Sem tarefas para esta versão + label_search: Procurar + label_result_plural: Resultados + label_all_words: Todas as palavras + label_wiki: Wiki + label_wiki_edit: Edição da Wiki + label_wiki_edit_plural: Edições da Wiki + label_wiki_page: Página da Wiki + label_wiki_page_plural: Páginas da Wiki + label_index_by_title: Ãndice por título + label_index_by_date: Ãndice por data + label_current_version: Versão actual + label_preview: Pré-visualizar + label_feed_plural: Feeds + label_changes_details: Detalhes de todas as mudanças + label_issue_tracking: Tarefas + label_spent_time: Tempo gasto + label_f_hour: "%{value} hora" + label_f_hour_plural: "%{value} horas" + label_time_tracking: Registo de tempo + label_change_plural: Mudanças + label_statistics: Estatísticas + label_commits_per_month: Commits por mês + label_commits_per_author: Commits por autor + label_view_diff: Ver diferenças + label_diff_inline: inline + label_diff_side_by_side: lado a lado + label_options: Opções + label_copy_workflow_from: Copiar fluxo de trabalho de + label_permissions_report: Relatório de permissões + label_watched_issues: Tarefas observadas + label_related_issues: Tarefas relacionadas + label_applied_status: Estado aplicado + label_loading: A carregar... + label_relation_new: Nova relação + label_relation_delete: Apagar relação + label_relates_to: relacionado a + label_duplicates: duplica + label_duplicated_by: duplicado por + label_blocks: bloqueia + label_blocked_by: bloqueado por + label_precedes: precede + label_follows: segue + label_end_to_start: fim a início + label_end_to_end: fim a fim + label_start_to_start: início a início + label_start_to_end: início a fim + label_stay_logged_in: Guardar sessão + label_disabled: desactivado + label_show_completed_versions: Mostrar versões acabadas + label_me: eu + label_board: Forum + label_board_new: Novo forum + label_board_plural: Forums + label_topic_plural: Tópicos + label_message_plural: Mensagens + label_message_last: Última mensagem + label_message_new: Nova mensagem + label_message_posted: Mensagem adicionada + label_reply_plural: Respostas + label_send_information: Enviar dados da conta para o utilizador + label_year: Ano + label_month: mês + label_week: Semana + label_date_from: De + label_date_to: Para + label_language_based: Baseado na língua do utilizador + label_sort_by: "Ordenar por %{value}" + label_send_test_email: enviar um e-mail de teste + label_feeds_access_key_created_on: "Chave RSS criada há %{value} atrás" + label_module_plural: Módulos + label_added_time_by: "Adicionado por %{author} há %{age} atrás" + label_updated_time: "Alterado há %{value} atrás" + label_jump_to_a_project: Ir para o projecto... + label_file_plural: Ficheiros + label_changeset_plural: Changesets + label_default_columns: Colunas por omissão + label_no_change_option: (sem alteração) + label_bulk_edit_selected_issues: Editar tarefas seleccionadas em conjunto + label_theme: Tema + label_default: Padrão + label_search_titles_only: Procurar apenas em títulos + label_user_mail_option_all: "Para qualquer evento em todos os meus projectos" + label_user_mail_option_selected: "Para qualquer evento apenas nos projectos seleccionados..." + label_user_mail_no_self_notified: "Não quero ser notificado de alterações feitas por mim" + label_registration_activation_by_email: Activação da conta por e-mail + label_registration_manual_activation: Activação manual da conta + label_registration_automatic_activation: Activação automática da conta + label_display_per_page: "Por página: %{value}" + label_age: Idade + label_change_properties: Mudar propriedades + label_general: Geral + label_more: Mais + label_scm: SCM + label_plugins: Extensões + label_ldap_authentication: Autenticação LDAP + label_downloads_abbr: D/L + label_optional_description: Descrição opcional + label_add_another_file: Adicionar outro ficheiro + label_preferences: Preferências + label_chronological_order: Em ordem cronológica + label_reverse_chronological_order: Em ordem cronológica inversa + label_planning: Planeamento + label_incoming_emails: E-mails a chegar + label_generate_key: Gerar uma chave + label_issue_watchers: Observadores + + button_login: Entrar + button_submit: Submeter + button_save: Guardar + button_check_all: Marcar tudo + button_uncheck_all: Desmarcar tudo + button_delete: Apagar + button_create: Criar + button_test: Testar + button_edit: Editar + button_add: Adicionar + button_change: Alterar + button_apply: Aplicar + button_clear: Limpar + button_lock: Bloquear + button_unlock: Desbloquear + button_download: Download + button_list: Listar + button_view: Ver + button_move: Mover + button_back: Voltar + button_cancel: Cancelar + button_activate: Activar + button_sort: Ordenar + button_log_time: Tempo de trabalho + button_rollback: Voltar para esta versão + button_watch: Observar + button_unwatch: Deixar de observar + button_reply: Responder + button_archive: Arquivar + button_unarchive: Desarquivar + button_reset: Reinicializar + button_rename: Renomear + button_change_password: Mudar palavra-chave + button_copy: Copiar + button_annotate: Anotar + button_update: Actualizar + button_configure: Configurar + button_quote: Citar + + status_active: activo + status_registered: registado + status_locked: bloqueado + + text_select_mail_notifications: Seleccionar as acções que originam uma notificação por e-mail. + text_regexp_info: ex. ^[A-Z0-9]+$ + text_min_max_length_info: 0 siginifica sem restrição + text_project_destroy_confirmation: Tem a certeza que deseja apagar o projecto e todos os dados relacionados? + text_subprojects_destroy_warning: "O(s) seu(s) sub-projecto(s): %{value} também será/serão apagado(s)." + text_workflow_edit: Seleccione uma função e um tipo de tarefa para editar o fluxo de trabalho + text_are_you_sure: Tem a certeza? + text_tip_issue_begin_day: tarefa a começar neste dia + text_tip_issue_end_day: tarefa a acabar neste dia + text_tip_issue_begin_end_day: tarefa a começar e acabar neste dia + text_caracters_maximum: "máximo %{count} caracteres." + text_caracters_minimum: "Deve ter pelo menos %{count} caracteres." + text_length_between: "Deve ter entre %{min} e %{max} caracteres." + text_tracker_no_workflow: Sem fluxo de trabalho definido para este tipo de tarefa. + text_unallowed_characters: Caracteres não permitidos + text_comma_separated: Permitidos múltiplos valores (separados por vírgula). + text_issues_ref_in_commit_messages: Referenciando e fechando tarefas em mensagens de commit + text_issue_added: "Tarefa %{id} foi criada por %{author}." + text_issue_updated: "Tarefa %{id} foi actualizada por %{author}." + text_wiki_destroy_confirmation: Tem a certeza que deseja apagar este wiki e todo o seu conteúdo? + text_issue_category_destroy_question: "Algumas tarefas (%{count}) estão atribuídas a esta categoria. O que quer fazer?" + text_issue_category_destroy_assignments: Remover as atribuições à categoria + text_issue_category_reassign_to: Re-atribuir as tarefas para esta categoria + text_user_mail_option: "Para projectos não seleccionados, apenas receberá notificações acerca de coisas que está a observar ou está envolvido (ex. tarefas das quais foi o criador ou lhes foram atribuídas)." + text_no_configuration_data: "Perfis, tipos de tarefas, estados das tarefas e workflows ainda não foram configurados.\nÉ extremamente recomendado carregar as configurações padrão. Será capaz de as modificar depois de estarem carregadas." + text_load_default_configuration: Carregar as configurações padrão + text_status_changed_by_changeset: "Aplicado no changeset %{value}." + text_issues_destroy_confirmation: 'Tem a certeza que deseja apagar a(s) tarefa(s) seleccionada(s)?' + text_select_project_modules: 'Seleccione os módulos a activar para este projecto:' + text_default_administrator_account_changed: Conta default de administrador alterada. + text_file_repository_writable: Repositório de ficheiros com permissões de escrita + text_rmagick_available: RMagick disponível (opcional) + text_destroy_time_entries_question: "%{hours} horas de trabalho foram atribuídas a estas tarefas que vai apagar. O que deseja fazer?" + text_destroy_time_entries: Apagar as horas + text_assign_time_entries_to_project: Atribuir as horas ao projecto + text_reassign_time_entries: 'Re-atribuir as horas para esta tarefa:' + text_user_wrote: "%{value} escreveu:" + text_enumeration_destroy_question: "%{count} objectos estão atribuídos a este valor." + text_enumeration_category_reassign_to: 'Re-atribuí-los para este valor:' + text_email_delivery_not_configured: "Entrega por e-mail não está configurada, e as notificação estão desactivadas.\nConfigure o seu servidor de SMTP em config/configuration.yml e reinicie a aplicação para activar estas funcionalidades." + + default_role_manager: Gestor + default_role_developer: Programador + default_role_reporter: Repórter + default_tracker_bug: Bug + default_tracker_feature: Funcionalidade + default_tracker_support: Suporte + default_issue_status_new: Novo + default_issue_status_in_progress: Em curso + default_issue_status_resolved: Resolvido + default_issue_status_feedback: Feedback + default_issue_status_closed: Fechado + default_issue_status_rejected: Rejeitado + default_doc_category_user: Documentação de utilizador + default_doc_category_tech: Documentação técnica + default_priority_low: Baixa + default_priority_normal: Normal + default_priority_high: Alta + default_priority_urgent: Urgente + default_priority_immediate: Imediata + default_activity_design: Planeamento + default_activity_development: Desenvolvimento + + enumeration_issue_priorities: Prioridade de tarefas + enumeration_doc_categories: Categorias de documentos + enumeration_activities: Actividades (Registo de tempo) + setting_plain_text_mail: Apenas texto simples (sem HTML) + permission_view_files: Ver ficheiros + permission_edit_issues: Editar tarefas + permission_edit_own_time_entries: Editar horas pessoais + permission_manage_public_queries: Gerir queries públicas + permission_add_issues: Adicionar tarefas + permission_log_time: Registar tempo gasto + permission_view_changesets: Ver changesets + permission_view_time_entries: Ver tempo gasto + permission_manage_versions: Gerir versões + permission_manage_wiki: Gerir wiki + permission_manage_categories: Gerir categorias de tarefas + permission_protect_wiki_pages: Proteger páginas de wiki + permission_comment_news: Comentar notícias + permission_delete_messages: Apagar mensagens + permission_select_project_modules: Seleccionar módulos do projecto + permission_manage_documents: Gerir documentos + permission_edit_wiki_pages: Editar páginas de wiki + permission_add_issue_watchers: Adicionar observadores + permission_view_gantt: ver diagrama de Gantt + permission_move_issues: Mover tarefas + permission_manage_issue_relations: Gerir relações de tarefas + permission_delete_wiki_pages: Apagar páginas de wiki + permission_manage_boards: Gerir forums + permission_delete_wiki_pages_attachments: Apagar anexos + permission_view_wiki_edits: Ver histórico da wiki + permission_add_messages: Submeter mensagens + permission_view_messages: Ver mensagens + permission_manage_files: Gerir ficheiros + permission_edit_issue_notes: Editar notas de tarefas + permission_manage_news: Gerir notícias + permission_view_calendar: Ver calendário + permission_manage_members: Gerir membros + permission_edit_messages: Editar mensagens + permission_delete_issues: Apagar tarefas + permission_view_issue_watchers: Ver lista de observadores + permission_manage_repository: Gerir repositório + permission_commit_access: Acesso a submissão + permission_browse_repository: Navegar em repositório + permission_view_documents: Ver documentos + permission_edit_project: Editar projecto + permission_add_issue_notes: Adicionar notas a tarefas + permission_save_queries: Guardar queries + permission_view_wiki_pages: Ver wiki + permission_rename_wiki_pages: Renomear páginas de wiki + permission_edit_time_entries: Editar entradas de tempo + permission_edit_own_issue_notes: Editar as prórpias notas + setting_gravatar_enabled: Utilizar ícones Gravatar + label_example: Exemplo + text_repository_usernames_mapping: "Seleccionar ou actualizar o utilizador de Redmine mapeado a cada nome de utilizador encontrado no repositório.\nUtilizadores com o mesmo nome de utilizador ou email no Redmine e no repositório são mapeados automaticamente." + permission_edit_own_messages: Editar as próprias mensagens + permission_delete_own_messages: Apagar as próprias mensagens + label_user_activity: "Actividade de %{value}" + label_updated_time_by: "Actualizado por %{author} há %{age}" + text_diff_truncated: '... Este diff foi truncado porque excede o tamanho máximo que pode ser mostrado.' + setting_diff_max_lines_displayed: Número máximo de linhas de diff mostradas + text_plugin_assets_writable: Escrita na pasta de activos dos módulos de extensão possível + warning_attachments_not_saved: "Não foi possível gravar %{count} ficheiro(s) ." + button_create_and_continue: Criar e continuar + text_custom_field_possible_values_info: 'Uma linha para cada valor' + label_display: Mostrar + field_editable: Editável + setting_repository_log_display_limit: Número máximo de revisões exibido no relatório de ficheiro + setting_file_max_size_displayed: Tamanho máximo dos ficheiros de texto exibidos inline + field_watcher: Observador + setting_openid: Permitir início de sessão e registo com OpenID + field_identity_url: URL do OpenID + label_login_with_open_id_option: ou início de sessão com OpenID + field_content: Conteúdo + label_descending: Descendente + label_sort: Ordenar + label_ascending: Ascendente + label_date_from_to: De %{start} a %{end} + label_greater_or_equal: ">=" + label_less_or_equal: <= + text_wiki_page_destroy_question: Esta página tem %{descendants} página(s) subordinada(s) e descendente(s). O que deseja fazer? + text_wiki_page_reassign_children: Reatribuir páginas subordinadas a esta página principal + text_wiki_page_nullify_children: Manter páginas subordinadas como páginas raíz + text_wiki_page_destroy_children: Apagar as páginas subordinadas e todos os seus descendentes + setting_password_min_length: Tamanho mínimo de palavra-chave + field_group_by: Agrupar resultados por + mail_subject_wiki_content_updated: "A página Wiki '%{id}' foi actualizada" + label_wiki_content_added: Página Wiki adicionada + mail_subject_wiki_content_added: "A página Wiki '%{id}' foi adicionada" + mail_body_wiki_content_added: A página Wiki '%{id}' foi adicionada por %{author}. + label_wiki_content_updated: Página Wiki actualizada + mail_body_wiki_content_updated: A página Wiki '%{id}' foi actualizada por %{author}. + permission_add_project: Criar projecto + setting_new_project_user_role_id: Função atribuída a um utilizador não-administrador que cria um projecto + label_view_all_revisions: Ver todas as revisões + label_tag: Etiqueta + label_branch: Ramo + error_no_tracker_in_project: Este projecto não tem associado nenhum tipo de tarefas. Verifique as definições do projecto. + error_no_default_issue_status: Não está definido um estado padrão para as tarefas. Verifique a sua configuração (dirija-se a "Administração -> Estados da tarefa"). + label_group_plural: Grupos + label_group: Grupo + label_group_new: Novo grupo + label_time_entry_plural: Tempo registado + text_journal_changed: "%{label} alterado de %{old} para %{new}" + text_journal_set_to: "%{label} configurado como %{value}" + text_journal_deleted: "%{label} apagou (%{old})" + text_journal_added: "%{label} %{value} adicionado" + field_active: Activo + enumeration_system_activity: Actividade de sistema + permission_delete_issue_watchers: Apagar observadores + version_status_closed: fechado + version_status_locked: protegido + version_status_open: aberto + error_can_not_reopen_issue_on_closed_version: Não é possível voltar a abrir uma tarefa atribuída a uma versão fechada + label_user_anonymous: Anónimo + button_move_and_follow: Mover e seguir + setting_default_projects_modules: Módulos activos por predefinição para novos projectos + setting_gravatar_default: Imagem Gravatar predefinida + field_sharing: Partilha + label_version_sharing_hierarchy: Com hierarquia do projecto + label_version_sharing_system: Com todos os projectos + label_version_sharing_descendants: Com os sub-projectos + label_version_sharing_tree: Com árvore do projecto + label_version_sharing_none: Não partilhado + error_can_not_archive_project: Não é possível arquivar este projecto + button_duplicate: Duplicar + button_copy_and_follow: Copiar e seguir + label_copy_source: Origem + setting_issue_done_ratio: Calcular a percentagem de progresso da tarefa + setting_issue_done_ratio_issue_status: Através do estado da tarefa + error_issue_done_ratios_not_updated: Percentagens de progresso da tarefa não foram actualizadas. + error_workflow_copy_target: Seleccione os tipos de tarefas e funções desejadas + setting_issue_done_ratio_issue_field: Através do campo da tarefa + label_copy_same_as_target: Mesmo que o alvo + label_copy_target: Alvo + notice_issue_done_ratios_updated: Percentagens de progresso da tarefa actualizadas. + error_workflow_copy_source: Seleccione um tipo de tarefa ou função de origem + label_update_issue_done_ratios: Actualizar percentagens de progresso da tarefa + setting_start_of_week: Iniciar calendários a + permission_view_issues: Ver tarefas + label_display_used_statuses_only: Só exibir estados empregues por este tipo de tarefa + label_revision_id: Revisão %{value} + label_api_access_key: Chave de acesso API + label_api_access_key_created_on: Chave de acesso API criada há %{value} + label_feeds_access_key: Chave de acesso RSS + notice_api_access_key_reseted: A sua chave de acesso API foi reinicializada. + setting_rest_api_enabled: Activar serviço Web REST + label_missing_api_access_key: Chave de acesso API em falta + label_missing_feeds_access_key: Chave de acesso RSS em falta + button_show: Mostrar + text_line_separated: Vários valores permitidos (uma linha para cada valor). + setting_mail_handler_body_delimiters: Truncar mensagens de correio electrónico após uma destas linhas + permission_add_subprojects: Criar sub-projectos + label_subproject_new: Novo sub-projecto + text_own_membership_delete_confirmation: |- + Está prestes a eliminar parcial ou totalmente as suas permissões. É possível que não possa editar o projecto após esta acção. + Tem a certeza de que deseja continuar? + label_close_versions: Fechar versões completas + label_board_sticky: Fixar mensagem + label_board_locked: Proteger + permission_export_wiki_pages: Exportar páginas Wiki + setting_cache_formatted_text: Colocar formatação do texto na memória cache + permission_manage_project_activities: Gerir actividades do projecto + error_unable_delete_issue_status: Não foi possível apagar o estado da tarefa + label_profile: Perfil + permission_manage_subtasks: Gerir sub-tarefas + field_parent_issue: Tarefa principal + label_subtask_plural: Sub-tarefa + label_project_copy_notifications: Enviar notificações por e-mail durante a cópia do projecto + error_can_not_delete_custom_field: Não foi possível apagar o campo personalizado + error_unable_to_connect: Não foi possível ligar (%{value}) + error_can_not_remove_role: Esta função está actualmente em uso e não pode ser apagada. + error_can_not_delete_tracker: Existem ainda tarefas nesta categoria. Não é possível apagar este tipo de tarefa. + field_principal: Principal + label_my_page_block: Bloco da minha página + notice_failed_to_save_members: "Erro ao guardar o(s) membro(s): %{errors}." + text_zoom_out: Ampliar + text_zoom_in: Reduzir + notice_unable_delete_time_entry: Não foi possível apagar a entrada de tempo registado. + label_overall_spent_time: Total de tempo registado + field_time_entries: Tempo registado + project_module_gantt: Gantt + project_module_calendar: Calendário + button_edit_associated_wikipage: "Editar página Wiki associada: %{page_title}" + field_text: Campo de texto + label_user_mail_option_only_owner: Apenas para tarefas das quais sou proprietário + setting_default_notification_option: Opção predefinida de notificação + label_user_mail_option_only_my_events: Apenas para tarefas que observo ou em que estou envolvido + label_user_mail_option_only_assigned: Apenas para tarefas que me foram atribuídas + label_user_mail_option_none: Sem eventos + field_member_of_group: Grupo do detentor de atribuição + field_assigned_to_role: Papel do detentor de atribuição + notice_not_authorized_archived_project: O projecto a que tentou aceder foi arquivado. + label_principal_search: "Procurar utilizador ou grupo:" + label_user_search: "Procurar utilizador:" + field_visible: Visível + setting_emails_header: Cabeçalho dos e-mails + setting_commit_logtime_activity_id: Actividade para tempo registado + text_time_logged_by_changeset: Aplicado no conjunto de alterações %{value}. + setting_commit_logtime_enabled: Activar registo de tempo + notice_gantt_chart_truncated: O gráfico foi truncado porque excede o número máximo de itens visível (%{máx.}) + setting_gantt_items_limit: Número máximo de itens exibidos no gráfico Gantt + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: Priority updated + label_issues_visibility_own: Issues created by or assigned to the user + field_issues_visibility: Issues visibility + label_issues_visibility_all: All issues + permission_set_own_issues_private: Set own issues public or private + field_is_private: Private + permission_set_issues_private: Set issues public or private + label_issues_visibility_public: All non private issues + text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). + field_commit_logs_encoding: Encoding das mensagens de commit + field_scm_path_encoding: Path encoding + text_scm_path_encoding_note: "Default: UTF-8" + field_path_to_repository: Path to repository + field_root_directory: Root directory + field_cvs_module: Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Command + text_scm_command_version: Version + label_git_report_last_commit: Report last commit for files and directories + text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 tarefa + one: 1 tarefa + other: "%{count} tarefas" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: todos + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Com os sub-projectos + label_cross_project_tree: Com árvore do projecto + label_cross_project_hierarchy: Com hierarquia do projecto + label_cross_project_system: Com todos os projectos + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0c/0c52d5a82094593c338acef265e5dd3106b35b1d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0c52d5a82094593c338acef265e5dd3106b35b1d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,68 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class ApplicationTest < ActionController::IntegrationTest + include Redmine::I18n + + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :workflows + + def test_set_localization + Setting.default_language = 'en' + + # a french user + get 'projects', { }, 'HTTP_ACCEPT_LANGUAGE' => 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3' + assert_response :success + assert_tag :tag => 'h2', :content => 'Projets' + assert_equal :fr, current_language + + # then an italien user + get 'projects', { }, 'HTTP_ACCEPT_LANGUAGE' => 'it;q=0.8,en-us;q=0.5,en;q=0.3' + assert_response :success + assert_tag :tag => 'h2', :content => 'Progetti' + assert_equal :it, current_language + + # not a supported language: default language should be used + get 'projects', { }, 'HTTP_ACCEPT_LANGUAGE' => 'zz' + assert_response :success + assert_tag :tag => 'h2', :content => 'Projects' + end + + def test_token_based_access_should_not_start_session + # issue of a private project + get 'issues/4.atom' + assert_response 302 + + rss_key = User.find(2).rss_key + get "issues/4.atom?key=#{rss_key}" + assert_response 200 + assert_nil session[:user_id] + end + + def test_missing_template_should_respond_with_404 + get '/login.png' + assert_response 404 + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0c/0c62ded19e437ec59035548bf5b8cb434884827a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0c62ded19e437ec59035548bf5b8cb434884827a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1395 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Issue < ActiveRecord::Base + include Redmine::SafeAttributes + include Redmine::Utils::DateCalculation + + belongs_to :project + belongs_to :tracker + belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' + belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id' + belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id' + belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id' + belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' + + has_many :journals, :as => :journalized, :dependent => :destroy + has_many :visible_journals, + :class_name => 'Journal', + :as => :journalized, + :conditions => Proc.new { + ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false] + }, + :readonly => true + + has_many :time_entries, :dependent => :delete_all + has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC" + + has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all + has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all + + acts_as_nested_set :scope => 'root_id', :dependent => :destroy + acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed + acts_as_customizable + acts_as_watchable + acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"], + :include => [:project, :visible_journals], + # sort by id so that limited eager loading doesn't break with postgresql + :order_column => "#{table_name}.id" + acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"}, + :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}, + :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') } + + acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}, + :author_key => :author_id + + DONE_RATIO_OPTIONS = %w(issue_field issue_status) + + attr_reader :current_journal + delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true + + validates_presence_of :subject, :priority, :project, :tracker, :author, :status + + validates_length_of :subject, :maximum => 255 + validates_inclusion_of :done_ratio, :in => 0..100 + validates_numericality_of :estimated_hours, :allow_nil => true + validate :validate_issue, :validate_required_fields + + scope :visible, + lambda {|*args| { :include => :project, + :conditions => Issue.visible_condition(args.shift || User.current, *args) } } + + scope :open, lambda {|*args| + is_closed = args.size > 0 ? !args.first : false + {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status} + } + + scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC" + scope :on_active_project, :include => [:status, :project, :tracker], + :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"] + + before_create :default_assign + before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change + after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?} + after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal + # Should be after_create but would be called before previous after_save callbacks + after_save :after_create_from_copy + after_destroy :update_parent_attributes + + # Returns a SQL conditions string used to find all issues visible by the specified user + def self.visible_condition(user, options={}) + Project.allowed_to_condition(user, :view_issues, options) do |role, user| + if user.logged? + case role.issues_visibility + when 'all' + nil + when 'default' + user_ids = [user.id] + user.groups.map(&:id) + "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))" + when 'own' + user_ids = [user.id] + user.groups.map(&:id) + "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))" + else + '1=0' + end + else + "(#{table_name}.is_private = #{connection.quoted_false})" + end + end + end + + # Returns true if usr or current user is allowed to view the issue + def visible?(usr=nil) + (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user| + if user.logged? + case role.issues_visibility + when 'all' + true + when 'default' + !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to)) + when 'own' + self.author == user || user.is_or_belongs_to?(assigned_to) + else + false + end + else + !self.is_private? + end + end + end + + def initialize(attributes=nil, *args) + super + if new_record? + # set default values for new records only + self.status ||= IssueStatus.default + self.priority ||= IssuePriority.default + self.watcher_user_ids = [] + end + end + + # AR#Persistence#destroy would raise and RecordNotFound exception + # if the issue was already deleted or updated (non matching lock_version). + # This is a problem when bulk deleting issues or deleting a project + # (because an issue may already be deleted if its parent was deleted + # first). + # The issue is reloaded by the nested_set before being deleted so + # the lock_version condition should not be an issue but we handle it. + def destroy + super + rescue ActiveRecord::RecordNotFound + # Stale or already deleted + begin + reload + rescue ActiveRecord::RecordNotFound + # The issue was actually already deleted + @destroyed = true + return freeze + end + # The issue was stale, retry to destroy + super + end + + def reload(*args) + @workflow_rule_by_attribute = nil + @assignable_versions = nil + super + end + + # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields + def available_custom_fields + (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : [] + end + + # Copies attributes from another issue, arg can be an id or an Issue + def copy_from(arg, options={}) + issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg) + self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on") + self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} + self.status = issue.status + self.author = User.current + unless options[:attachments] == false + self.attachments = issue.attachments.map do |attachement| + attachement.copy(:container => self) + end + end + @copied_from = issue + @copy_options = options + self + end + + # Returns an unsaved copy of the issue + def copy(attributes=nil, copy_options={}) + copy = self.class.new.copy_from(self, copy_options) + copy.attributes = attributes if attributes + copy + end + + # Returns true if the issue is a copy + def copy? + @copied_from.present? + end + + # Moves/copies an issue to a new project and tracker + # Returns the moved/copied issue on success, false on failure + def move_to_project(new_project, new_tracker=nil, options={}) + ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead." + + if options[:copy] + issue = self.copy + else + issue = self + end + + issue.init_journal(User.current, options[:notes]) + + # Preserve previous behaviour + # #move_to_project doesn't change tracker automatically + issue.send :project=, new_project, true + if new_tracker + issue.tracker = new_tracker + end + # Allow bulk setting of attributes on the issue + if options[:attributes] + issue.attributes = options[:attributes] + end + + issue.save ? issue : false + end + + def status_id=(sid) + self.status = nil + result = write_attribute(:status_id, sid) + @workflow_rule_by_attribute = nil + result + end + + def priority_id=(pid) + self.priority = nil + write_attribute(:priority_id, pid) + end + + def category_id=(cid) + self.category = nil + write_attribute(:category_id, cid) + end + + def fixed_version_id=(vid) + self.fixed_version = nil + write_attribute(:fixed_version_id, vid) + end + + def tracker_id=(tid) + self.tracker = nil + result = write_attribute(:tracker_id, tid) + @custom_field_values = nil + @workflow_rule_by_attribute = nil + result + end + + def project_id=(project_id) + if project_id.to_s != self.project_id.to_s + self.project = (project_id.present? ? Project.find_by_id(project_id) : nil) + end + end + + def project=(project, keep_tracker=false) + project_was = self.project + write_attribute(:project_id, project ? project.id : nil) + association_instance_set('project', project) + if project_was && project && project_was != project + @assignable_versions = nil + + unless keep_tracker || project.trackers.include?(tracker) + self.tracker = project.trackers.first + end + # Reassign to the category with same name if any + if category + self.category = project.issue_categories.find_by_name(category.name) + end + # Keep the fixed_version if it's still valid in the new_project + if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version) + self.fixed_version = nil + end + # Clear the parent task if it's no longer valid + unless valid_parent_project? + self.parent_issue_id = nil + end + @custom_field_values = nil + end + end + + def description=(arg) + if arg.is_a?(String) + arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n") + end + write_attribute(:description, arg) + end + + # Overrides assign_attributes so that project and tracker get assigned first + def assign_attributes_with_project_and_tracker_first(new_attributes, *args) + return if new_attributes.nil? + attrs = new_attributes.dup + attrs.stringify_keys! + + %w(project project_id tracker tracker_id).each do |attr| + if attrs.has_key?(attr) + send "#{attr}=", attrs.delete(attr) + end + end + send :assign_attributes_without_project_and_tracker_first, attrs, *args + end + # Do not redefine alias chain on reload (see #4838) + alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first) + + def estimated_hours=(h) + write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) + end + + safe_attributes 'project_id', + :if => lambda {|issue, user| + if issue.new_record? + issue.copy? + elsif user.allowed_to?(:move_issues, issue.project) + projects = Issue.allowed_target_projects_on_move(user) + projects.include?(issue.project) && projects.size > 1 + end + } + + safe_attributes 'tracker_id', + 'status_id', + 'category_id', + 'assigned_to_id', + 'priority_id', + 'fixed_version_id', + 'subject', + 'description', + 'start_date', + 'due_date', + 'done_ratio', + 'estimated_hours', + 'custom_field_values', + 'custom_fields', + 'lock_version', + 'notes', + :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) } + + safe_attributes 'status_id', + 'assigned_to_id', + 'fixed_version_id', + 'done_ratio', + 'lock_version', + 'notes', + :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? } + + safe_attributes 'notes', + :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)} + + safe_attributes 'private_notes', + :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)} + + safe_attributes 'watcher_user_ids', + :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)} + + safe_attributes 'is_private', + :if => lambda {|issue, user| + user.allowed_to?(:set_issues_private, issue.project) || + (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project)) + } + + safe_attributes 'parent_issue_id', + :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) && + user.allowed_to?(:manage_subtasks, issue.project)} + + def safe_attribute_names(user=nil) + names = super + names -= disabled_core_fields + names -= read_only_attribute_names(user) + names + end + + # Safely sets attributes + # Should be called from controllers instead of #attributes= + # attr_accessible is too rough because we still want things like + # Issue.new(:project => foo) to work + def safe_attributes=(attrs, user=User.current) + return unless attrs.is_a?(Hash) + + attrs = attrs.dup + + # Project and Tracker must be set before since new_statuses_allowed_to depends on it. + if (p = attrs.delete('project_id')) && safe_attribute?('project_id') + if allowed_target_projects(user).collect(&:id).include?(p.to_i) + self.project_id = p + end + end + + if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id') + self.tracker_id = t + end + + if (s = attrs.delete('status_id')) && safe_attribute?('status_id') + if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i) + self.status_id = s + end + end + + attrs = delete_unsafe_attributes(attrs, user) + return if attrs.empty? + + unless leaf? + attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)} + end + + if attrs['parent_issue_id'].present? + s = attrs['parent_issue_id'].to_s + unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1])) + @invalid_parent_issue_id = attrs.delete('parent_issue_id') + end + end + + if attrs['custom_field_values'].present? + attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s} + end + + if attrs['custom_fields'].present? + attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s} + end + + # mass-assignment security bypass + assign_attributes attrs, :without_protection => true + end + + def disabled_core_fields + tracker ? tracker.disabled_core_fields : [] + end + + # Returns the custom_field_values that can be edited by the given user + def editable_custom_field_values(user=nil) + custom_field_values.reject do |value| + read_only_attribute_names(user).include?(value.custom_field_id.to_s) + end + end + + # Returns the names of attributes that are read-only for user or the current user + # For users with multiple roles, the read-only fields are the intersection of + # read-only fields of each role + # The result is an array of strings where sustom fields are represented with their ids + # + # Examples: + # issue.read_only_attribute_names # => ['due_date', '2'] + # issue.read_only_attribute_names(user) # => [] + def read_only_attribute_names(user=nil) + workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys + end + + # Returns the names of required attributes for user or the current user + # For users with multiple roles, the required fields are the intersection of + # required fields of each role + # The result is an array of strings where sustom fields are represented with their ids + # + # Examples: + # issue.required_attribute_names # => ['due_date', '2'] + # issue.required_attribute_names(user) # => [] + def required_attribute_names(user=nil) + workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys + end + + # Returns true if the attribute is required for user + def required_attribute?(name, user=nil) + required_attribute_names(user).include?(name.to_s) + end + + # Returns a hash of the workflow rule by attribute for the given user + # + # Examples: + # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'} + def workflow_rule_by_attribute(user=nil) + return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil? + + user_real = user || User.current + roles = user_real.admin ? Role.all : user_real.roles_for_project(project) + return {} if roles.empty? + + result = {} + workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all + if workflow_permissions.any? + workflow_rules = workflow_permissions.inject({}) do |h, wp| + h[wp.field_name] ||= [] + h[wp.field_name] << wp.rule + h + end + workflow_rules.each do |attr, rules| + next if rules.size < roles.size + uniq_rules = rules.uniq + if uniq_rules.size == 1 + result[attr] = uniq_rules.first + else + result[attr] = 'required' + end + end + end + @workflow_rule_by_attribute = result if user.nil? + result + end + private :workflow_rule_by_attribute + + def done_ratio + if Issue.use_status_for_done_ratio? && status && status.default_done_ratio + status.default_done_ratio + else + read_attribute(:done_ratio) + end + end + + def self.use_status_for_done_ratio? + Setting.issue_done_ratio == 'issue_status' + end + + def self.use_field_for_done_ratio? + Setting.issue_done_ratio == 'issue_field' + end + + def validate_issue + if due_date.nil? && @attributes['due_date'].present? + errors.add :due_date, :not_a_date + end + + if start_date.nil? && @attributes['start_date'].present? + errors.add :start_date, :not_a_date + end + + if due_date && start_date && due_date < start_date + errors.add :due_date, :greater_than_start_date + end + + if start_date && soonest_start && start_date < soonest_start + errors.add :start_date, :invalid + end + + if fixed_version + if !assignable_versions.include?(fixed_version) + errors.add :fixed_version_id, :inclusion + elsif reopened? && fixed_version.closed? + errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version) + end + end + + # Checks that the issue can not be added/moved to a disabled tracker + if project && (tracker_id_changed? || project_id_changed?) + unless project.trackers.include?(tracker) + errors.add :tracker_id, :inclusion + end + end + + # Checks parent issue assignment + if @invalid_parent_issue_id.present? + errors.add :parent_issue_id, :invalid + elsif @parent_issue + if !valid_parent_project?(@parent_issue) + errors.add :parent_issue_id, :invalid + elsif !new_record? + # moving an existing issue + if @parent_issue.root_id != root_id + # we can always move to another tree + elsif move_possible?(@parent_issue) + # move accepted inside tree + else + errors.add :parent_issue_id, :invalid + end + end + end + end + + # Validates the issue against additional workflow requirements + def validate_required_fields + user = new_record? ? author : current_journal.try(:user) + + required_attribute_names(user).each do |attribute| + if attribute =~ /^\d+$/ + attribute = attribute.to_i + v = custom_field_values.detect {|v| v.custom_field_id == attribute } + if v && v.value.blank? + errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank') + end + else + if respond_to?(attribute) && send(attribute).blank? + errors.add attribute, :blank + end + end + end + end + + # Set the done_ratio using the status if that setting is set. This will keep the done_ratios + # even if the user turns off the setting later + def update_done_ratio_from_issue_status + if Issue.use_status_for_done_ratio? && status && status.default_done_ratio + self.done_ratio = status.default_done_ratio + end + end + + def init_journal(user, notes = "") + @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) + if new_record? + @current_journal.notify = false + else + @attributes_before_change = attributes.dup + @custom_values_before_change = {} + self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value } + end + @current_journal + end + + # Returns the id of the last journal or nil + def last_journal_id + if new_record? + nil + else + journals.maximum(:id) + end + end + + # Returns a scope for journals that have an id greater than journal_id + def journals_after(journal_id) + scope = journals.reorder("#{Journal.table_name}.id ASC") + if journal_id.present? + scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i) + end + scope + end + + # Return true if the issue is closed, otherwise false + def closed? + self.status.is_closed? + end + + # Return true if the issue is being reopened + def reopened? + if !new_record? && status_id_changed? + status_was = IssueStatus.find_by_id(status_id_was) + status_new = IssueStatus.find_by_id(status_id) + if status_was && status_new && status_was.is_closed? && !status_new.is_closed? + return true + end + end + false + end + + # Return true if the issue is being closed + def closing? + if !new_record? && status_id_changed? + status_was = IssueStatus.find_by_id(status_id_was) + status_new = IssueStatus.find_by_id(status_id) + if status_was && status_new && !status_was.is_closed? && status_new.is_closed? + return true + end + end + false + end + + # Returns true if the issue is overdue + def overdue? + !due_date.nil? && (due_date < Date.today) && !status.is_closed? + end + + # Is the amount of work done less than it should for the due date + def behind_schedule? + return false if start_date.nil? || due_date.nil? + done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor + return done_date <= Date.today + end + + # Does this issue have children? + def children? + !leaf? + end + + # Users the issue can be assigned to + def assignable_users + users = project.assignable_users + users << author if author + users << assigned_to if assigned_to + users.uniq.sort + end + + # Versions that the issue can be assigned to + def assignable_versions + return @assignable_versions if @assignable_versions + + versions = project.shared_versions.open.all + if fixed_version + if fixed_version_id_changed? + # nothing to do + elsif project_id_changed? + if project.shared_versions.include?(fixed_version) + versions << fixed_version + end + else + versions << fixed_version + end + end + @assignable_versions = versions.uniq.sort + end + + # Returns true if this issue is blocked by another issue that is still open + def blocked? + !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil? + end + + # Returns an array of statuses that user is able to apply + def new_statuses_allowed_to(user=User.current, include_default=false) + if new_record? && @copied_from + [IssueStatus.default, @copied_from.status].compact.uniq.sort + else + initial_status = nil + if new_record? + initial_status = IssueStatus.default + elsif status_id_was + initial_status = IssueStatus.find_by_id(status_id_was) + end + initial_status ||= status + + statuses = initial_status.find_new_statuses_allowed_to( + user.admin ? Role.all : user.roles_for_project(project), + tracker, + author == user, + assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id + ) + statuses << initial_status unless statuses.empty? + statuses << IssueStatus.default if include_default + statuses = statuses.compact.uniq.sort + blocked? ? statuses.reject {|s| s.is_closed?} : statuses + end + end + + def assigned_to_was + if assigned_to_id_changed? && assigned_to_id_was.present? + @assigned_to_was ||= User.find_by_id(assigned_to_id_was) + end + end + + # Returns the users that should be notified + def notified_users + notified = [] + # Author and assignee are always notified unless they have been + # locked or don't want to be notified + notified << author if author + if assigned_to + notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to]) + end + if assigned_to_was + notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was]) + end + notified = notified.select {|u| u.active? && u.notify_about?(self)} + + notified += project.notified_users + notified.uniq! + # Remove users that can not view the issue + notified.reject! {|user| !visible?(user)} + notified + end + + # Returns the email addresses that should be notified + def recipients + notified_users.collect(&:mail) + end + + # Returns the number of hours spent on this issue + def spent_hours + @spent_hours ||= time_entries.sum(:hours) || 0 + end + + # Returns the total number of hours spent on this issue and its descendants + # + # Example: + # spent_hours => 0.0 + # spent_hours => 50.2 + def total_spent_hours + @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", + :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0 + end + + def relations + @relations ||= IssueRelations.new(self, (relations_from + relations_to).sort) + end + + # Preloads relations for a collection of issues + def self.load_relations(issues) + if issues.any? + relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}]) + issues.each do |issue| + issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id} + end + end + end + + # Preloads visible spent time for a collection of issues + def self.load_visible_spent_hours(issues, user=User.current) + if issues.any? + hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id) + issues.each do |issue| + issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0) + end + end + end + + # Preloads visible relations for a collection of issues + def self.load_visible_relations(issues, user=User.current) + if issues.any? + issue_ids = issues.map(&:id) + # Relations with issue_from in given issues and visible issue_to + relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all + # Relations with issue_to in given issues and visible issue_from + relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all + + issues.each do |issue| + relations = + relations_from.select {|relation| relation.issue_from_id == issue.id} + + relations_to.select {|relation| relation.issue_to_id == issue.id} + + issue.instance_variable_set "@relations", IssueRelations.new(issue, relations.sort) + end + end + end + + # Finds an issue relation given its id. + def find_relation(relation_id) + IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id]) + end + + def all_dependent_issues(except=[]) + except << self + dependencies = [] + relations_from.each do |relation| + if relation.issue_to && !except.include?(relation.issue_to) + dependencies << relation.issue_to + dependencies += relation.issue_to.all_dependent_issues(except) + end + end + dependencies + end + + # Returns an array of issues that duplicate this one + def duplicates + relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from} + end + + # Returns the due date or the target due date if any + # Used on gantt chart + def due_before + due_date || (fixed_version ? fixed_version.effective_date : nil) + end + + # Returns the time scheduled for this issue. + # + # Example: + # Start Date: 2/26/09, End Date: 3/04/09 + # duration => 6 + def duration + (start_date && due_date) ? due_date - start_date : 0 + end + + # Returns the duration in working days + def working_duration + (start_date && due_date) ? working_days(start_date, due_date) : 0 + end + + def soonest_start(reload=false) + @soonest_start = nil if reload + @soonest_start ||= ( + relations_to(reload).collect{|relation| relation.successor_soonest_start} + + ancestors.collect(&:soonest_start) + ).compact.max + end + + # Sets start_date on the given date or the next working day + # and changes due_date to keep the same working duration. + def reschedule_on(date) + wd = working_duration + date = next_working_date(date) + self.start_date = date + self.due_date = add_working_days(date, wd) + end + + # Reschedules the issue on the given date or the next working day and saves the record. + # If the issue is a parent task, this is done by rescheduling its subtasks. + def reschedule_on!(date) + return if date.nil? + if leaf? + if start_date.nil? || start_date != date + if start_date && start_date > date + # Issue can not be moved earlier than its soonest start date + date = [soonest_start(true), date].compact.max + end + reschedule_on(date) + begin + save + rescue ActiveRecord::StaleObjectError + reload + reschedule_on(date) + save + end + end + else + leaves.each do |leaf| + if leaf.start_date + # Only move subtask if it starts at the same date as the parent + # or if it starts before the given date + if start_date == leaf.start_date || date > leaf.start_date + leaf.reschedule_on!(date) + end + else + leaf.reschedule_on!(date) + end + end + end + end + + def <=>(issue) + if issue.nil? + -1 + elsif root_id != issue.root_id + (root_id || 0) <=> (issue.root_id || 0) + else + (lft || 0) <=> (issue.lft || 0) + end + end + + def to_s + "#{tracker} ##{id}: #{subject}" + end + + # Returns a string of css classes that apply to the issue + def css_classes + s = "issue status-#{status_id} #{priority.try(:css_classes)}" + s << ' closed' if closed? + s << ' overdue' if overdue? + s << ' child' if child? + s << ' parent' unless leaf? + s << ' private' if is_private? + s << ' created-by-me' if User.current.logged? && author_id == User.current.id + s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id + s + end + + # Saves an issue and a time_entry from the parameters + def save_issue_with_child_records(params, existing_time_entry=nil) + Issue.transaction do + if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project) + @time_entry = existing_time_entry || TimeEntry.new + @time_entry.project = project + @time_entry.issue = self + @time_entry.user = User.current + @time_entry.spent_on = User.current.today + @time_entry.attributes = params[:time_entry] + self.time_entries << @time_entry + end + + # TODO: Rename hook + Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) + if save + # TODO: Rename hook + Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) + else + raise ActiveRecord::Rollback + end + end + end + + # Unassigns issues from +version+ if it's no longer shared with issue's project + def self.update_versions_from_sharing_change(version) + # Update issues assigned to the version + update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id]) + end + + # Unassigns issues from versions that are no longer shared + # after +project+ was moved + def self.update_versions_from_hierarchy_change(project) + moved_project_ids = project.self_and_descendants.reload.collect(&:id) + # Update issues of the moved projects and issues assigned to a version of a moved project + Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids]) + end + + def parent_issue_id=(arg) + s = arg.to_s.strip.presence + if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1])) + @parent_issue.id + else + @parent_issue = nil + @invalid_parent_issue_id = arg + end + end + + def parent_issue_id + if @invalid_parent_issue_id + @invalid_parent_issue_id + elsif instance_variable_defined? :@parent_issue + @parent_issue.nil? ? nil : @parent_issue.id + else + parent_id + end + end + + # Returns true if issue's project is a valid + # parent issue project + def valid_parent_project?(issue=parent) + return true if issue.nil? || issue.project_id == project_id + + case Setting.cross_project_subtasks + when 'system' + true + when 'tree' + issue.project.root == project.root + when 'hierarchy' + issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project) + when 'descendants' + issue.project.is_or_is_ancestor_of?(project) + else + false + end + end + + # Extracted from the ReportsController. + def self.by_tracker(project) + count_and_group_by(:project => project, + :field => 'tracker_id', + :joins => Tracker.table_name) + end + + def self.by_version(project) + count_and_group_by(:project => project, + :field => 'fixed_version_id', + :joins => Version.table_name) + end + + def self.by_priority(project) + count_and_group_by(:project => project, + :field => 'priority_id', + :joins => IssuePriority.table_name) + end + + def self.by_category(project) + count_and_group_by(:project => project, + :field => 'category_id', + :joins => IssueCategory.table_name) + end + + def self.by_assigned_to(project) + count_and_group_by(:project => project, + :field => 'assigned_to_id', + :joins => User.table_name) + end + + def self.by_author(project) + count_and_group_by(:project => project, + :field => 'author_id', + :joins => User.table_name) + end + + def self.by_subproject(project) + ActiveRecord::Base.connection.select_all("select s.id as status_id, + s.is_closed as closed, + #{Issue.table_name}.project_id as project_id, + count(#{Issue.table_name}.id) as total + from + #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s + where + #{Issue.table_name}.status_id=s.id + and #{Issue.table_name}.project_id = #{Project.table_name}.id + and #{visible_condition(User.current, :project => project, :with_subprojects => true)} + and #{Issue.table_name}.project_id <> #{project.id} + group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any? + end + # End ReportsController extraction + + # Returns an array of projects that user can assign the issue to + def allowed_target_projects(user=User.current) + if new_record? + Project.all(:conditions => Project.allowed_to_condition(user, :add_issues)) + else + self.class.allowed_target_projects_on_move(user) + end + end + + # Returns an array of projects that user can move issues to + def self.allowed_target_projects_on_move(user=User.current) + Project.all(:conditions => Project.allowed_to_condition(user, :move_issues)) + end + + private + + def after_project_change + # Update project_id on related time entries + TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id}) + + # Delete issue relations + unless Setting.cross_project_issue_relations? + relations_from.clear + relations_to.clear + end + + # Move subtasks that were in the same project + children.each do |child| + next unless child.project_id == project_id_was + # Change project and keep project + child.send :project=, project, true + unless child.save + raise ActiveRecord::Rollback + end + end + end + + # Callback for after the creation of an issue by copy + # * adds a "copied to" relation with the copied issue + # * copies subtasks from the copied issue + def after_create_from_copy + return unless copy? && !@after_create_from_copy_handled + + if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false + relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO) + unless relation.save + logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger + end + end + + unless @copied_from.leaf? || @copy_options[:subtasks] == false + @copied_from.children.each do |child| + unless child.visible? + # Do not copy subtasks that are not visible to avoid potential disclosure of private data + logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger + next + end + copy = Issue.new.copy_from(child, @copy_options) + copy.author = author + copy.project = project + copy.parent_issue_id = id + # Children subtasks are copied recursively + unless copy.save + logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger + end + end + end + @after_create_from_copy_handled = true + end + + def update_nested_set_attributes + if root_id.nil? + # issue was just created + self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) + set_default_left_and_right + Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id]) + if @parent_issue + move_to_child_of(@parent_issue) + end + reload + elsif parent_issue_id != parent_id + former_parent_id = parent_id + # moving an existing issue + if @parent_issue && @parent_issue.root_id == root_id + # inside the same tree + move_to_child_of(@parent_issue) + else + # to another tree + unless root? + move_to_right_of(root) + reload + end + old_root_id = root_id + self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id ) + target_maxright = nested_set_scope.maximum(right_column_name) || 0 + offset = target_maxright + 1 - lft + Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}", + ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt]) + self[left_column_name] = lft + offset + self[right_column_name] = rgt + offset + if @parent_issue + move_to_child_of(@parent_issue) + end + end + reload + # delete invalid relations of all descendants + self_and_descendants.each do |issue| + issue.relations.each do |relation| + relation.destroy unless relation.valid? + end + end + # update former parent + recalculate_attributes_for(former_parent_id) if former_parent_id + end + remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue) + end + + def update_parent_attributes + recalculate_attributes_for(parent_id) if parent_id + end + + def recalculate_attributes_for(issue_id) + if issue_id && p = Issue.find_by_id(issue_id) + # priority = highest priority of children + if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority) + p.priority = IssuePriority.find_by_position(priority_position) + end + + # start/due dates = lowest/highest dates of children + p.start_date = p.children.minimum(:start_date) + p.due_date = p.children.maximum(:due_date) + if p.start_date && p.due_date && p.due_date < p.start_date + p.start_date, p.due_date = p.due_date, p.start_date + end + + # done ratio = weighted average ratio of leaves + unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio + leaves_count = p.leaves.count + if leaves_count > 0 + average = p.leaves.average(:estimated_hours).to_f + if average == 0 + average = 1 + end + done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f + progress = done / (average * leaves_count) + p.done_ratio = progress.round + end + end + + # estimate = sum of leaves estimates + p.estimated_hours = p.leaves.sum(:estimated_hours).to_f + p.estimated_hours = nil if p.estimated_hours == 0.0 + + # ancestors will be recursively updated + p.save(:validate => false) + end + end + + # Update issues so their versions are not pointing to a + # fixed_version that is not shared with the issue's project + def self.update_versions(conditions=nil) + # Only need to update issues with a fixed_version from + # a different project and that is not systemwide shared + Issue.scoped(:conditions => conditions).all( + :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" + + " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" + + " AND #{Version.table_name}.sharing <> 'system'", + :include => [:project, :fixed_version] + ).each do |issue| + next if issue.project.nil? || issue.fixed_version.nil? + unless issue.project.shared_versions.include?(issue.fixed_version) + issue.init_journal(User.current) + issue.fixed_version = nil + issue.save + end + end + end + + # Callback on file attachment + def attachment_added(obj) + if @current_journal && !obj.new_record? + @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename) + end + end + + # Callback on attachment deletion + def attachment_removed(obj) + if @current_journal && !obj.new_record? + @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename) + @current_journal.save + end + end + + # Default assignment based on category + def default_assign + if assigned_to.nil? && category && category.assigned_to + self.assigned_to = category.assigned_to + end + end + + # Updates start/due dates of following issues + def reschedule_following_issues + if start_date_changed? || due_date_changed? + relations_from.each do |relation| + relation.set_issue_to_dates + end + end + end + + # Closes duplicates if the issue is being closed + def close_duplicates + if closing? + duplicates.each do |duplicate| + # Reload is need in case the duplicate was updated by a previous duplicate + duplicate.reload + # Don't re-close it if it's already closed + next if duplicate.closed? + # Same user and notes + if @current_journal + duplicate.init_journal(@current_journal.user, @current_journal.notes) + end + duplicate.update_attribute :status, self.status + end + end + end + + # Make sure updated_on is updated when adding a note + def force_updated_on_change + if @current_journal + self.updated_on = current_time_from_proper_timezone + end + end + + # Saves the changes in a Journal + # Called after_save + def create_journal + if @current_journal + # attributes changes + if @attributes_before_change + (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c| + before = @attributes_before_change[c] + after = send(c) + next if before == after || (before.blank? && after.blank?) + @current_journal.details << JournalDetail.new(:property => 'attr', + :prop_key => c, + :old_value => before, + :value => after) + } + end + if @custom_values_before_change + # custom fields changes + custom_field_values.each {|c| + before = @custom_values_before_change[c.custom_field_id] + after = c.value + next if before == after || (before.blank? && after.blank?) + + if before.is_a?(Array) || after.is_a?(Array) + before = [before] unless before.is_a?(Array) + after = [after] unless after.is_a?(Array) + + # values removed + (before - after).reject(&:blank?).each do |value| + @current_journal.details << JournalDetail.new(:property => 'cf', + :prop_key => c.custom_field_id, + :old_value => value, + :value => nil) + end + # values added + (after - before).reject(&:blank?).each do |value| + @current_journal.details << JournalDetail.new(:property => 'cf', + :prop_key => c.custom_field_id, + :old_value => nil, + :value => value) + end + else + @current_journal.details << JournalDetail.new(:property => 'cf', + :prop_key => c.custom_field_id, + :old_value => before, + :value => after) + end + } + end + @current_journal.save + # reset current journal + init_journal @current_journal.user, @current_journal.notes + end + end + + # Query generator for selecting groups of issue counts for a project + # based on specific criteria + # + # Options + # * project - Project to search in. + # * field - String. Issue field to key off of in the grouping. + # * joins - String. The table name to join against. + def self.count_and_group_by(options) + project = options.delete(:project) + select_field = options.delete(:field) + joins = options.delete(:joins) + + where = "#{Issue.table_name}.#{select_field}=j.id" + + ActiveRecord::Base.connection.select_all("select s.id as status_id, + s.is_closed as closed, + j.id as #{select_field}, + count(#{Issue.table_name}.id) as total + from + #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j + where + #{Issue.table_name}.status_id=s.id + and #{where} + and #{Issue.table_name}.project_id=#{Project.table_name}.id + and #{visible_condition(User.current, :project => project)} + group by s.id, s.is_closed, j.id") + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0c/0cb33e746ce4443d0cb3bfc0a14c8cc1559914ac.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0cb33e746ce4443d0cb3bfc0a14c8cc1559914ac.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,4 @@ +<%= l(:text_issue_added, :id => "##{@issue.id}", :author => @issue.author) %> + +---------------------------------------- +<%= render :partial => 'issue', :formats => [:text], :locals => { :issue => @issue, :issue_url => @issue_url } %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0c/0cc35d9ebd9809126c44031aad1c4acc705d8aec.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0cc35d9ebd9809126c44031aad1c4acc705d8aec.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,22 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class IssueObserver < ActiveRecord::Observer + def after_create(issue) + Mailer.issue_add(issue).deliver if Setting.notified_events.include?('issue_added') + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0c/0ccef3aea9c789ba7ff6975c6015bb677b5a97a9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0c/0ccef3aea9c789ba7ff6975c6015bb677b5a97a9.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,73 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Helpers + class Diff + include ERB::Util + include ActionView::Helpers::TagHelper + include ActionView::Helpers::TextHelper + attr_reader :diff, :words + + def initialize(content_to, content_from) + @words = content_to.to_s.split(/(\s+)/) + @words = @words.select {|word| word != ' '} + words_from = content_from.to_s.split(/(\s+)/) + words_from = words_from.select {|word| word != ' '} + @diff = words_from.diff @words + end + + def to_html + words = self.words.collect{|word| h(word)} + words_add = 0 + words_del = 0 + dels = 0 + del_off = 0 + diff.diffs.each do |diff| + add_at = nil + add_to = nil + del_at = nil + deleted = "" + diff.each do |change| + pos = change[1] + if change[0] == "+" + add_at = pos + dels unless add_at + add_to = pos + dels + words_add += 1 + else + del_at = pos unless del_at + deleted << ' ' unless deleted.empty? + deleted << h(change[2]) + words_del += 1 + end + end + if add_at + words[add_at] = ''.html_safe + words[add_at] + words[add_to] = words[add_to] + ''.html_safe + end + if del_at + words.insert del_at - del_off + dels + words_add, ''.html_safe + deleted + ''.html_safe + dels += 1 + del_off += words_del + words_del = 0 + end + end + words.join(' ').html_safe + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0d/0d2311ec4452abd2d98cbb99aaf512d8571b9e28.svn-base --- a/.svn/pristine/0d/0d2311ec4452abd2d98cbb99aaf512d8571b9e28.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ -class IssuePriority < Enumeration - generator_for :name, :start => 'IssuePriority0' - generator_for :type => 'IssuePriority' - -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0e/0e54508d3830a714379a5549a38e89ff26176202.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0e/0e54508d3830a714379a5549a38e89ff26176202.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,59 @@ +

    <%= @author.nil? ? l(:label_activity) : l(:label_user_activity, link_to_user(@author)).html_safe %>

    +

    <%= l(:label_date_from_to, :start => format_date(@date_to - @days), :end => format_date(@date_to-1)) %>

    + +
    +<% @events_by_day.keys.sort.reverse.each do |day| %> +

    <%= format_activity_day(day) %>

    +
    +<% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%> +
    + <%= avatar(e.event_author, :size => "24") if e.respond_to?(:event_author) %> + <%= format_time(e.event_datetime, false) %> + <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %> + <%= link_to format_activity_title(e.event_title), e.event_url %>
    +
    <%= format_activity_description(e.event_description) %> + <%= link_to_user(e.event_author) if e.respond_to?(:event_author) %>
    +<% end -%> +
    +<% end -%> +
    + +<%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %> + +
    +<%= link_to_content_update("\xc2\xab " + l(:label_previous), + params.merge(:from => @date_to - @days - 1), + :title => l(:label_date_from_to, :start => format_date(@date_to - 2*@days), :end => format_date(@date_to - @days - 1))) %> +
    +
    +<%= link_to_content_update(l(:label_next) + " \xc2\xbb", + params.merge(:from => @date_to + @days - 1), + :title => l(:label_date_from_to, :start => format_date(@date_to), :end => format_date(@date_to + @days - 1))) unless @date_to >= Date.today %> +
    +  +<% other_formats_links do |f| %> + <%= f.link_to 'Atom', :url => params.merge(:from => nil, :key => User.current.rss_key) %> +<% end %> + +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:atom, params.merge(:format => 'atom', :from => nil, :key => User.current.rss_key)) %> +<% end %> + +<% content_for :sidebar do %> +<%= form_tag({}, :method => :get) do %> +

    <%= l(:label_activity) %>

    +

    <% @activity.event_types.each do |t| %> +<%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %> + +
    +<% end %>

    +<% if @project && @project.descendants.active.any? %> + <%= hidden_field_tag 'with_subprojects', 0 %> +

    +<% end %> +<%= hidden_field_tag('user_id', params[:user_id]) unless params[:user_id].blank? %> +

    <%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %>

    +<% end %> +<% end %> + +<% html_title(l(:label_activity), @author) -%> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0e/0e59eac074a412c3150c3135d0a1eec08baa4fba.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0e/0e59eac074a412c3150c3135d0a1eec08baa4fba.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,265 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) +require 'issues_controller' + +class IssuesControllerTransactionTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + self.use_transactional_fixtures = false + + def setup + @controller = IssuesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_update_stale_issue_should_not_update_the_issue + issue = Issue.find(2) + @request.session[:user_id] = 2 + + assert_no_difference 'Journal.count' do + assert_no_difference 'TimeEntry.count' do + put :update, + :id => issue.id, + :issue => { + :fixed_version_id => 4, + :notes => 'My notes', + :lock_version => (issue.lock_version - 1) + }, + :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id } + end + end + + assert_response :success + assert_template 'edit' + + assert_select 'div.conflict' + assert_select 'input[name=?][value=?]', 'conflict_resolution', 'overwrite' + assert_select 'input[name=?][value=?]', 'conflict_resolution', 'add_notes' + assert_select 'label' do + assert_select 'input[name=?][value=?]', 'conflict_resolution', 'cancel' + assert_select 'a[href=/issues/2]' + end + end + + def test_update_stale_issue_should_save_attachments + set_tmp_attachments_directory + issue = Issue.find(2) + @request.session[:user_id] = 2 + + assert_no_difference 'Journal.count' do + assert_no_difference 'TimeEntry.count' do + assert_difference 'Attachment.count' do + put :update, + :id => issue.id, + :issue => { + :fixed_version_id => 4, + :notes => 'My notes', + :lock_version => (issue.lock_version - 1) + }, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}, + :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id } + end + end + end + + assert_response :success + assert_template 'edit' + attachment = Attachment.first(:order => 'id DESC') + assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token} + assert_tag 'span', :content => /testfile.txt/ + end + + def test_update_stale_issue_without_notes_should_not_show_add_notes_option + issue = Issue.find(2) + @request.session[:user_id] = 2 + + put :update, :id => issue.id, + :issue => { + :fixed_version_id => 4, + :notes => '', + :lock_version => (issue.lock_version - 1) + } + + assert_tag 'div', :attributes => {:class => 'conflict'} + assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'overwrite'} + assert_no_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'add_notes'} + assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'cancel'} + end + + def test_update_stale_issue_should_show_conflicting_journals + @request.session[:user_id] = 2 + + put :update, :id => 1, + :issue => { + :fixed_version_id => 4, + :notes => '', + :lock_version => 2 + }, + :last_journal_id => 1 + + assert_not_nil assigns(:conflict_journals) + assert_equal 1, assigns(:conflict_journals).size + assert_equal 2, assigns(:conflict_journals).first.id + assert_tag 'div', :attributes => {:class => 'conflict'}, + :descendant => {:content => /Some notes with Redmine links/} + end + + def test_update_stale_issue_without_previous_journal_should_show_all_journals + @request.session[:user_id] = 2 + + put :update, :id => 1, + :issue => { + :fixed_version_id => 4, + :notes => '', + :lock_version => 2 + }, + :last_journal_id => '' + + assert_not_nil assigns(:conflict_journals) + assert_equal 2, assigns(:conflict_journals).size + assert_tag 'div', :attributes => {:class => 'conflict'}, + :descendant => {:content => /Some notes with Redmine links/} + assert_tag 'div', :attributes => {:class => 'conflict'}, + :descendant => {:content => /Journal notes/} + end + + def test_update_stale_issue_should_show_private_journals_with_permission_only + journal = Journal.create!(:journalized => Issue.find(1), :notes => 'Privates notes', :private_notes => true, :user_id => 1) + + @request.session[:user_id] = 2 + put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => '' + assert_include journal, assigns(:conflict_journals) + + Role.find(1).remove_permission! :view_private_notes + put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => '' + assert_not_include journal, assigns(:conflict_journals) + end + + def test_update_stale_issue_with_overwrite_conflict_resolution_should_update + @request.session[:user_id] = 2 + + assert_difference 'Journal.count' do + put :update, :id => 1, + :issue => { + :fixed_version_id => 4, + :notes => 'overwrite_conflict_resolution', + :lock_version => 2 + }, + :conflict_resolution => 'overwrite' + end + + assert_response 302 + issue = Issue.find(1) + assert_equal 4, issue.fixed_version_id + journal = Journal.first(:order => 'id DESC') + assert_equal 'overwrite_conflict_resolution', journal.notes + assert journal.details.any? + end + + def test_update_stale_issue_with_add_notes_conflict_resolution_should_update + @request.session[:user_id] = 2 + + assert_difference 'Journal.count' do + put :update, :id => 1, + :issue => { + :fixed_version_id => 4, + :notes => 'add_notes_conflict_resolution', + :lock_version => 2 + }, + :conflict_resolution => 'add_notes' + end + + assert_response 302 + issue = Issue.find(1) + assert_nil issue.fixed_version_id + journal = Journal.first(:order => 'id DESC') + assert_equal 'add_notes_conflict_resolution', journal.notes + assert journal.details.empty? + end + + def test_update_stale_issue_with_cancel_conflict_resolution_should_redirect_without_updating + @request.session[:user_id] = 2 + + assert_no_difference 'Journal.count' do + put :update, :id => 1, + :issue => { + :fixed_version_id => 4, + :notes => 'add_notes_conflict_resolution', + :lock_version => 2 + }, + :conflict_resolution => 'cancel' + end + + assert_redirected_to '/issues/1' + issue = Issue.find(1) + assert_nil issue.fixed_version_id + end + + def test_put_update_with_spent_time_and_failure_should_not_add_spent_time + @request.session[:user_id] = 2 + + assert_no_difference('TimeEntry.count') do + put :update, + :id => 1, + :issue => { :subject => '' }, + :time_entry => { :hours => '2.5', :comments => 'should not be added', :activity_id => TimeEntryActivity.first.id } + assert_response :success + end + + assert_select 'input[name=?][value=?]', 'time_entry[hours]', '2.5' + assert_select 'input[name=?][value=?]', 'time_entry[comments]', 'should not be added' + assert_select 'select[name=?]', 'time_entry[activity_id]' do + assert_select 'option[value=?][selected=selected]', TimeEntryActivity.first.id + end + end + + def test_index_should_rescue_invalid_sql_query + Query.any_instance.stubs(:statement).returns("INVALID STATEMENT") + + get :index + assert_response 500 + assert_tag 'p', :content => /An error occurred/ + assert_nil session[:query] + assert_nil session[:issues_index_sort] + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0e/0e6d154c5ead84b5d126f01a8ce10bcd16d83cbd.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/0e/0e6d154c5ead84b5d126f01a8ce10bcd16d83cbd.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,36 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class WikisController < ApplicationController + menu_item :settings + before_filter :find_project, :authorize + + # Create or update a project's wiki + def edit + @wiki = @project.wiki || Wiki.new(:project => @project) + @wiki.safe_attributes = params[:wiki] + @wiki.save if request.post? + end + + # Delete a project's wiki + def destroy + if request.post? && params[:confirm] && @project.wiki + @project.wiki.destroy + redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'wiki' + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0f/0f12084a6a3192cb0d61cbd2f3ef681df419cac4.svn-base --- a/.svn/pristine/0f/0f12084a6a3192cb0d61cbd2f3ef681df419cac4.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,104 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class Message < ActiveRecord::Base - belongs_to :board - belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' - acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC" - acts_as_attachable - belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id' - - acts_as_searchable :columns => ['subject', 'content'], - :include => {:board => :project}, - :project_key => "#{Board.table_name}.project_id", - :date_column => "#{table_name}.created_on" - acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"}, - :description => :content, - :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'}, - :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} : - {:id => o.parent_id, :r => o.id, :anchor => "message-#{o.id}"})} - - acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]}, - :author_key => :author_id - acts_as_watchable - - attr_protected :locked, :sticky - validates_presence_of :board, :subject, :content - validates_length_of :subject, :maximum => 255 - validate :cannot_reply_to_locked_topic, :on => :create - - after_create :add_author_as_watcher, :update_parent_last_reply - after_update :update_messages_board - after_destroy :reset_board_counters - - named_scope :visible, lambda {|*args| { :include => {:board => :project}, - :conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } } - - def visible?(user=User.current) - !user.nil? && user.allowed_to?(:view_messages, project) - end - - def cannot_reply_to_locked_topic - # Can not reply to a locked topic - errors.add :base, 'Topic is locked' if root.locked? && self != root - end - - def update_parent_last_reply - if parent - parent.reload.update_attribute(:last_reply_id, self.id) - end - board.reset_counters! - end - - def update_messages_board - if board_id_changed? - Message.update_all("board_id = #{board_id}", ["id = ? OR parent_id = ?", root.id, root.id]) - Board.reset_counters!(board_id_was) - Board.reset_counters!(board_id) - end - end - - def reset_board_counters - board.reset_counters! - end - - def sticky=(arg) - write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0) - end - - def sticky? - sticky == 1 - end - - def project - board.project - end - - def editable_by?(usr) - usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project))) - end - - def destroyable_by?(usr) - usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project))) - end - - private - - def add_author_as_watcher - Watcher.create(:watchable => self.root, :user => author) - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/0f/0f71516e3a6532ada8045e94e42972a11451afc4.svn-base --- a/.svn/pristine/0f/0f71516e3a6532ada8045e94e42972a11451afc4.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,36 +0,0 @@ -class CommentsController < ApplicationController - default_search_scope :news - model_object News - before_filter :find_model_object - before_filter :find_project_from_association - before_filter :authorize - - verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed } - def create - @comment = Comment.new(params[:comment]) - @comment.author = User.current - if @news.comments << @comment - flash[:notice] = l(:label_comment_added) - end - - redirect_to :controller => 'news', :action => 'show', :id => @news - end - - verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed } - def destroy - @news.comments.find(params[:comment_id]).destroy - redirect_to :controller => 'news', :action => 'show', :id => @news - end - - private - - # ApplicationController's find_model_object sets it based on the controller - # name so it needs to be overriden and set to @news instead - def find_model_object - super - @news = @object - @comment = nil - @news - end - -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/10/105399d73cdd64dbe994d874bdf78e986ecd631d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/10/105399d73cdd64dbe994d874bdf78e986ecd631d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,101 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class TrackersController < ApplicationController + layout 'admin' + + before_filter :require_admin, :except => :index + before_filter :require_admin_or_api_request, :only => :index + accept_api_auth :index + + def index + respond_to do |format| + format.html { + @tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position' + render :action => "index", :layout => false if request.xhr? + } + format.api { + @trackers = Tracker.sorted.all + } + end + end + + def new + @tracker ||= Tracker.new(params[:tracker]) + @trackers = Tracker.find :all, :order => 'position' + @projects = Project.find(:all) + end + + def create + @tracker = Tracker.new(params[:tracker]) + if request.post? and @tracker.save + # workflow copy + if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from])) + @tracker.workflow_rules.copy(copy_from) + end + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'index' + return + end + new + render :action => 'new' + end + + def edit + @tracker ||= Tracker.find(params[:id]) + @projects = Project.find(:all) + end + + def update + @tracker = Tracker.find(params[:id]) + if request.put? and @tracker.update_attributes(params[:tracker]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'index' + return + end + edit + render :action => 'edit' + end + + def destroy + @tracker = Tracker.find(params[:id]) + unless @tracker.issues.empty? + flash[:error] = l(:error_can_not_delete_tracker) + else + @tracker.destroy + end + redirect_to :action => 'index' + end + + def fields + if request.post? && params[:trackers] + params[:trackers].each do |tracker_id, tracker_params| + tracker = Tracker.find_by_id(tracker_id) + if tracker + tracker.core_fields = tracker_params[:core_fields] + tracker.custom_field_ids = tracker_params[:custom_field_ids] + tracker.save + end + end + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'fields' + return + end + @trackers = Tracker.sorted.all + @custom_fields = IssueCustomField.all.sort + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/11/11389dcb8c49b70343e3b80fa2a021abf64753ca.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/11/11389dcb8c49b70343e3b80fa2a021abf64753ca.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,38 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingContextMenusTest < ActionController::IntegrationTest + def test_context_menus_time_entries + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/time_entries/context_menu" }, + { :controller => 'context_menus', :action => 'time_entries' } + ) + end + end + + def test_context_menus_issues + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/issues/context_menu" }, + { :controller => 'context_menus', :action => 'issues' } + ) + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/11/1143136ec6943c91201441e4e743a0264ddfa810.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/11/1143136ec6943c91201441e4e743a0264ddfa810.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,23 @@ +<%= form_tag({:action => 'edit', :tab => 'mail_handler'}) do %> + +
    +

    + <%= setting_text_area :mail_handler_body_delimiters, :rows => 5 %> + <%= l(:text_line_separated) %> +

    +
    + +
    +

    <%= setting_check_box :mail_handler_api_enabled, + :onclick => "if (this.checked) { $('#settings_mail_handler_api_key').removeAttr('disabled'); } else { $('#settings_mail_handler_api_key').attr('disabled', true); }"%>

    + +

    <%= setting_text_field :mail_handler_api_key, :size => 30, + :id => 'settings_mail_handler_api_key', + :disabled => !Setting.mail_handler_api_enabled? %> + <%= link_to_function l(:label_generate_key), "if (!$('#settings_mail_handler_api_key').attr('disabled')) { $('#settings_mail_handler_api_key').val(randomKey(20)) }" %> +

    +
    + +<%= submit_tag l(:button_save) %> + +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/11/115477077313264feb09635aa1e17387b5e3f9d3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/11/115477077313264feb09635aa1e17387b5e3f9d3.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,263 @@ +require File.expand_path('../../test_helper', __FILE__) + +class ContextMenusControllerTest < ActionController::TestCase + fixtures :projects, + :trackers, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :workflows, + :journals, :journal_details, + :versions, + :issues, :issue_statuses, :issue_categories, + :users, + :enumerations, + :time_entries + + def test_context_menu_one_issue + @request.session[:user_id] = 2 + get :issues, :ids => [1] + assert_response :success + assert_template 'context_menu' + assert_tag :tag => 'a', :content => 'Edit', + :attributes => { :href => '/issues/1/edit', + :class => 'icon-edit' } + assert_tag :tag => 'a', :content => 'Closed', + :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&issue%5Bstatus_id%5D=5', + :class => '' } + assert_tag :tag => 'a', :content => 'Immediate', + :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&issue%5Bpriority_id%5D=8', + :class => '' } + assert_no_tag :tag => 'a', :content => 'Inactive Priority' + # Versions + assert_tag :tag => 'a', :content => '2.0', + :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&issue%5Bfixed_version_id%5D=3', + :class => '' } + assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0', + :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&issue%5Bfixed_version_id%5D=4', + :class => '' } + + assert_tag :tag => 'a', :content => 'Dave Lopper', + :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&issue%5Bassigned_to_id%5D=3', + :class => '' } + assert_tag :tag => 'a', :content => 'Copy', + :attributes => { :href => '/projects/ecookbook/issues/1/copy', + :class => 'icon-copy' } + assert_no_tag :tag => 'a', :content => 'Move' + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => '/issues?ids%5B%5D=1', + :class => 'icon-del' } + end + + def test_context_menu_one_issue_by_anonymous + get :issues, :ids => [1] + assert_response :success + assert_template 'context_menu' + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => '#', + :class => 'icon-del disabled' } + end + + def test_context_menu_multiple_issues_of_same_project + @request.session[:user_id] = 2 + get :issues, :ids => [1, 2] + assert_response :success + assert_template 'context_menu' + assert_not_nil assigns(:issues) + assert_equal [1, 2], assigns(:issues).map(&:id).sort + + ids = assigns(:issues).map(&:id).sort.map {|i| "ids%5B%5D=#{i}"}.join('&') + assert_tag :tag => 'a', :content => 'Edit', + :attributes => { :href => "/issues/bulk_edit?#{ids}", + :class => 'icon-edit' } + assert_tag :tag => 'a', :content => 'Closed', + :attributes => { :href => "/issues/bulk_update?#{ids}&issue%5Bstatus_id%5D=5", + :class => '' } + assert_tag :tag => 'a', :content => 'Immediate', + :attributes => { :href => "/issues/bulk_update?#{ids}&issue%5Bpriority_id%5D=8", + :class => '' } + assert_tag :tag => 'a', :content => 'Dave Lopper', + :attributes => { :href => "/issues/bulk_update?#{ids}&issue%5Bassigned_to_id%5D=3", + :class => '' } + assert_tag :tag => 'a', :content => 'Copy', + :attributes => { :href => "/issues/bulk_edit?copy=1&#{ids}", + :class => 'icon-copy' } + assert_no_tag :tag => 'a', :content => 'Move' + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => "/issues?#{ids}", + :class => 'icon-del' } + end + + def test_context_menu_multiple_issues_of_different_projects + @request.session[:user_id] = 2 + get :issues, :ids => [1, 2, 6] + assert_response :success + assert_template 'context_menu' + assert_not_nil assigns(:issues) + assert_equal [1, 2, 6], assigns(:issues).map(&:id).sort + + ids = assigns(:issues).map(&:id).sort.map {|i| "ids%5B%5D=#{i}"}.join('&') + assert_tag :tag => 'a', :content => 'Edit', + :attributes => { :href => "/issues/bulk_edit?#{ids}", + :class => 'icon-edit' } + assert_tag :tag => 'a', :content => 'Closed', + :attributes => { :href => "/issues/bulk_update?#{ids}&issue%5Bstatus_id%5D=5", + :class => '' } + assert_tag :tag => 'a', :content => 'Immediate', + :attributes => { :href => "/issues/bulk_update?#{ids}&issue%5Bpriority_id%5D=8", + :class => '' } + assert_tag :tag => 'a', :content => 'John Smith', + :attributes => { :href => "/issues/bulk_update?#{ids}&issue%5Bassigned_to_id%5D=2", + :class => '' } + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => "/issues?#{ids}", + :class => 'icon-del' } + end + + def test_context_menu_should_include_list_custom_fields + field = IssueCustomField.create!(:name => 'List', :field_format => 'list', + :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3]) + @request.session[:user_id] = 2 + get :issues, :ids => [1] + + assert_tag 'a', + :content => 'List', + :attributes => {:href => '#'}, + :sibling => {:tag => 'ul', :children => {:count => 3}} + + assert_tag 'a', + :content => 'Foo', + :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=Foo"} + assert_tag 'a', + :content => 'none', + :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D="} + end + + def test_context_menu_should_not_include_null_value_for_required_custom_fields + field = IssueCustomField.create!(:name => 'List', :is_required => true, :field_format => 'list', + :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3]) + @request.session[:user_id] = 2 + get :issues, :ids => [1, 2] + + assert_tag 'a', + :content => 'List', + :attributes => {:href => '#'}, + :sibling => {:tag => 'ul', :children => {:count => 2}} + end + + def test_context_menu_on_single_issue_should_select_current_custom_field_value + field = IssueCustomField.create!(:name => 'List', :field_format => 'list', + :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3]) + issue = Issue.find(1) + issue.custom_field_values = {field.id => 'Bar'} + issue.save! + @request.session[:user_id] = 2 + get :issues, :ids => [1] + + assert_tag 'a', + :content => 'List', + :attributes => {:href => '#'}, + :sibling => {:tag => 'ul', :children => {:count => 3}} + assert_tag 'a', + :content => 'Bar', + :attributes => {:class => /icon-checked/} + end + + def test_context_menu_should_include_bool_custom_fields + field = IssueCustomField.create!(:name => 'Bool', :field_format => 'bool', + :is_for_all => true, :tracker_ids => [1, 2, 3]) + @request.session[:user_id] = 2 + get :issues, :ids => [1] + + assert_tag 'a', + :content => 'Bool', + :attributes => {:href => '#'}, + :sibling => {:tag => 'ul', :children => {:count => 3}} + + assert_tag 'a', + :content => 'Yes', + :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=1"} + end + + def test_context_menu_should_include_user_custom_fields + field = IssueCustomField.create!(:name => 'User', :field_format => 'user', + :is_for_all => true, :tracker_ids => [1, 2, 3]) + @request.session[:user_id] = 2 + get :issues, :ids => [1] + + assert_tag 'a', + :content => 'User', + :attributes => {:href => '#'}, + :sibling => {:tag => 'ul', :children => {:count => Project.find(1).members.count + 1}} + + assert_tag 'a', + :content => 'John Smith', + :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=2"} + end + + def test_context_menu_should_include_version_custom_fields + field = IssueCustomField.create!(:name => 'Version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1, 2, 3]) + @request.session[:user_id] = 2 + get :issues, :ids => [1] + + assert_tag 'a', + :content => 'Version', + :attributes => {:href => '#'}, + :sibling => {:tag => 'ul', :children => {:count => Project.find(1).shared_versions.count + 1}} + + assert_tag 'a', + :content => '2.0', + :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=3"} + end + + def test_context_menu_by_assignable_user_should_include_assigned_to_me_link + @request.session[:user_id] = 2 + get :issues, :ids => [1] + assert_response :success + assert_template 'context_menu' + + assert_tag :tag => 'a', :content => / me /, + :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&issue%5Bassigned_to_id%5D=2', + :class => '' } + end + + def test_context_menu_should_propose_shared_versions_for_issues_from_different_projects + @request.session[:user_id] = 2 + version = Version.create!(:name => 'Shared', :sharing => 'system', :project_id => 1) + + get :issues, :ids => [1, 4] + assert_response :success + assert_template 'context_menu' + + assert_include version, assigns(:versions) + assert_tag :tag => 'a', :content => 'eCookbook - Shared' + end + + def test_context_menu_issue_visibility + get :issues, :ids => [1, 4] + assert_response :success + assert_template 'context_menu' + assert_equal [1], assigns(:issues).collect(&:id) + end + + def test_time_entries_context_menu + @request.session[:user_id] = 2 + get :time_entries, :ids => [1, 2] + assert_response :success + assert_template 'time_entries' + assert_tag 'a', :content => 'Edit' + assert_no_tag 'a', :content => 'Edit', :attributes => {:class => /disabled/} + end + + def test_time_entries_context_menu_without_edit_permission + @request.session[:user_id] = 2 + Role.find_by_name('Manager').remove_permission! :edit_time_entries + + get :time_entries, :ids => [1, 2] + assert_response :success + assert_template 'time_entries' + assert_tag 'a', :content => 'Edit', :attributes => {:class => /disabled/} + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/11/1155c5733fc69fa605b0b34e8b29755f3beb4693.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/11/1155c5733fc69fa605b0b34e8b29755f3beb4693.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,9 @@ +<%= form_for(:user, :url => { :action => 'update' }, :html => {:method => :put}) do %> +
    +<% Group.all.sort.each do |group| %> +
    +<% end %> +<%= hidden_field_tag 'user[group_ids][]', '' %> +
    +<%= submit_tag l(:button_save) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/11/11a5507f6b6077a6e73fb852952ddb0da8dc089e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/11/11a5507f6b6077a6e73fb852952ddb0da8dc089e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,76 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class CustomFieldVersionFormatTest < ActiveSupport::TestCase + fixtures :custom_fields, :projects, :members, :users, :member_roles, :trackers, :issues, :versions + + def setup + @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'version') + end + + def test_possible_values_with_no_arguments + assert_equal [], @field.possible_values + assert_equal [], @field.possible_values(nil) + end + + def test_possible_values_with_project_resource + project = Project.find(1) + possible_values = @field.possible_values(project.issues.first) + assert possible_values.any? + assert_equal project.shared_versions.sort.collect(&:id).map(&:to_s), possible_values + end + + def test_possible_values_with_nil_project_resource + assert_equal [], @field.possible_values(Issue.new) + end + + def test_possible_values_options_with_no_arguments + assert_equal [], @field.possible_values_options + assert_equal [], @field.possible_values_options(nil) + end + + def test_possible_values_options_with_project_resource + project = Project.find(1) + possible_values_options = @field.possible_values_options(project.issues.first) + assert possible_values_options.any? + assert_equal project.shared_versions.sort.map {|u| [u.name, u.id.to_s]}, possible_values_options + end + + def test_possible_values_options_with_array + projects = Project.find([1, 2]) + possible_values_options = @field.possible_values_options(projects) + assert possible_values_options.any? + assert_equal (projects.first.shared_versions & projects.last.shared_versions).sort.map {|u| [u.name, u.id.to_s]}, possible_values_options + end + + def test_cast_blank_value + assert_equal nil, @field.cast_value(nil) + assert_equal nil, @field.cast_value("") + end + + def test_cast_valid_value + version = @field.cast_value("2") + assert_kind_of Version, version + assert_equal Version.find(2), version + end + + def test_cast_invalid_value + assert_equal nil, @field.cast_value("187") + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/11/11c49a871d148b89615d8c37f2abca7b6149921b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/11/11c49a871d148b89615d8c37f2abca7b6149921b.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,377 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class IssueNestedSetTest < ActiveSupport::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, + :trackers, :projects_trackers, + :versions, + :issue_statuses, :issue_categories, :issue_relations, :workflows, + :enumerations, + :issues, + :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values, + :time_entries + + def test_create_root_issue + issue1 = Issue.generate! + issue2 = Issue.generate! + issue1.reload + issue2.reload + + assert_equal [issue1.id, nil, 1, 2], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt] + assert_equal [issue2.id, nil, 1, 2], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt] + end + + def test_create_child_issue + parent = Issue.generate! + child = Issue.generate!(:parent_issue_id => parent.id) + parent.reload + child.reload + + assert_equal [parent.id, nil, 1, 4], [parent.root_id, parent.parent_id, parent.lft, parent.rgt] + assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt] + end + + def test_creating_a_child_in_a_subproject_should_validate + issue = Issue.generate! + child = Issue.new(:project_id => 3, :tracker_id => 2, :author_id => 1, + :subject => 'child', :parent_issue_id => issue.id) + assert_save child + assert_equal issue, child.reload.parent + end + + def test_creating_a_child_in_an_invalid_project_should_not_validate + issue = Issue.generate! + child = Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1, + :subject => 'child', :parent_issue_id => issue.id) + assert !child.save + assert_not_nil child.errors[:parent_issue_id] + end + + def test_move_a_root_to_child + parent1 = Issue.generate! + parent2 = Issue.generate! + child = Issue.generate!(:parent_issue_id => parent1.id) + + parent2.parent_issue_id = parent1.id + parent2.save! + child.reload + parent1.reload + parent2.reload + + assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt] + assert_equal [parent1.id, 4, 5], [parent2.root_id, parent2.lft, parent2.rgt] + assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt] + end + + def test_move_a_child_to_root + parent1 = Issue.generate! + parent2 = Issue.generate! + child = Issue.generate!(:parent_issue_id => parent1.id) + + child.parent_issue_id = nil + child.save! + child.reload + parent1.reload + parent2.reload + + assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt] + assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt] + assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt] + end + + def test_move_a_child_to_another_issue + parent1 = Issue.generate! + parent2 = Issue.generate! + child = Issue.generate!(:parent_issue_id => parent1.id) + + child.parent_issue_id = parent2.id + child.save! + child.reload + parent1.reload + parent2.reload + + assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt] + assert_equal [parent2.id, 1, 4], [parent2.root_id, parent2.lft, parent2.rgt] + assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt] + end + + def test_move_a_child_with_descendants_to_another_issue + parent1 = Issue.generate! + parent2 = Issue.generate! + child = Issue.generate!(:parent_issue_id => parent1.id) + grandchild = Issue.generate!(:parent_issue_id => child.id) + + parent1.reload + parent2.reload + child.reload + grandchild.reload + + assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt] + assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt] + assert_equal [parent1.id, 2, 5], [child.root_id, child.lft, child.rgt] + assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt] + + child.reload.parent_issue_id = parent2.id + child.save! + child.reload + grandchild.reload + parent1.reload + parent2.reload + + assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt] + assert_equal [parent2.id, 1, 6], [parent2.root_id, parent2.lft, parent2.rgt] + assert_equal [parent2.id, 2, 5], [child.root_id, child.lft, child.rgt] + assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt] + end + + def test_move_a_child_with_descendants_to_another_project + parent1 = Issue.generate! + child = Issue.generate!(:parent_issue_id => parent1.id) + grandchild = Issue.generate!(:parent_issue_id => child.id) + + child.reload + child.project = Project.find(2) + assert child.save + child.reload + grandchild.reload + parent1.reload + + assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt] + assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt] + assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt] + end + + def test_moving_an_issue_to_a_descendant_should_not_validate + parent1 = Issue.generate! + parent2 = Issue.generate! + child = Issue.generate!(:parent_issue_id => parent1.id) + grandchild = Issue.generate!(:parent_issue_id => child.id) + + child.reload + child.parent_issue_id = grandchild.id + assert !child.save + assert_not_nil child.errors[:parent_issue_id] + end + + def test_moving_an_issue_should_keep_valid_relations_only + issue1 = Issue.generate! + issue2 = Issue.generate! + issue3 = Issue.generate!(:parent_issue_id => issue2.id) + issue4 = Issue.generate! + r1 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES) + r2 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES) + r3 = IssueRelation.create!(:issue_from => issue2, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES) + issue2.reload + issue2.parent_issue_id = issue1.id + issue2.save! + assert !IssueRelation.exists?(r1.id) + assert !IssueRelation.exists?(r2.id) + assert IssueRelation.exists?(r3.id) + end + + def test_destroy_should_destroy_children + issue1 = Issue.generate! + issue2 = Issue.generate! + issue3 = Issue.generate!(:parent_issue_id => issue2.id) + issue4 = Issue.generate!(:parent_issue_id => issue1.id) + + issue3.init_journal(User.find(2)) + issue3.subject = 'child with journal' + issue3.save! + + assert_difference 'Issue.count', -2 do + assert_difference 'Journal.count', -1 do + assert_difference 'JournalDetail.count', -1 do + Issue.find(issue2.id).destroy + end + end + end + + issue1.reload + issue4.reload + assert !Issue.exists?(issue2.id) + assert !Issue.exists?(issue3.id) + assert_equal [issue1.id, 1, 4], [issue1.root_id, issue1.lft, issue1.rgt] + assert_equal [issue1.id, 2, 3], [issue4.root_id, issue4.lft, issue4.rgt] + end + + def test_destroy_child_should_update_parent + issue = Issue.generate! + child1 = Issue.generate!(:parent_issue_id => issue.id) + child2 = Issue.generate!(:parent_issue_id => issue.id) + + issue.reload + assert_equal [issue.id, 1, 6], [issue.root_id, issue.lft, issue.rgt] + + child2.reload.destroy + + issue.reload + assert_equal [issue.id, 1, 4], [issue.root_id, issue.lft, issue.rgt] + end + + def test_destroy_parent_issue_updated_during_children_destroy + parent = Issue.generate! + Issue.generate!(:start_date => Date.today, :parent_issue_id => parent.id) + Issue.generate!(:start_date => 2.days.from_now, :parent_issue_id => parent.id) + + assert_difference 'Issue.count', -3 do + Issue.find(parent.id).destroy + end + end + + def test_destroy_child_issue_with_children + root = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'root') + child = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => root.id) + leaf = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'leaf', :parent_issue_id => child.id) + leaf.init_journal(User.find(2)) + leaf.subject = 'leaf with journal' + leaf.save! + + assert_difference 'Issue.count', -2 do + assert_difference 'Journal.count', -1 do + assert_difference 'JournalDetail.count', -1 do + Issue.find(child.id).destroy + end + end + end + + root = Issue.find(root.id) + assert root.leaf?, "Root issue is not a leaf (lft: #{root.lft}, rgt: #{root.rgt})" + end + + def test_destroy_issue_with_grand_child + parent = Issue.generate! + issue = Issue.generate!(:parent_issue_id => parent.id) + child = Issue.generate!(:parent_issue_id => issue.id) + grandchild1 = Issue.generate!(:parent_issue_id => child.id) + grandchild2 = Issue.generate!(:parent_issue_id => child.id) + + assert_difference 'Issue.count', -4 do + Issue.find(issue.id).destroy + parent.reload + assert_equal [1, 2], [parent.lft, parent.rgt] + end + end + + def test_parent_priority_should_be_the_highest_child_priority + parent = Issue.generate!(:priority => IssuePriority.find_by_name('Normal')) + # Create children + child1 = Issue.generate!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id) + assert_equal 'High', parent.reload.priority.name + child2 = Issue.generate!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id) + assert_equal 'Immediate', child1.reload.priority.name + assert_equal 'Immediate', parent.reload.priority.name + child3 = Issue.generate!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id) + assert_equal 'Immediate', parent.reload.priority.name + # Destroy a child + child1.destroy + assert_equal 'Low', parent.reload.priority.name + # Update a child + child3.reload.priority = IssuePriority.find_by_name('Normal') + child3.save! + assert_equal 'Normal', parent.reload.priority.name + end + + def test_parent_dates_should_be_lowest_start_and_highest_due_dates + parent = Issue.generate! + Issue.generate!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id) + Issue.generate!( :due_date => '2010-02-13', :parent_issue_id => parent.id) + Issue.generate!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id) + parent.reload + assert_equal Date.parse('2010-01-25'), parent.start_date + assert_equal Date.parse('2010-02-22'), parent.due_date + end + + def test_parent_done_ratio_should_be_average_done_ratio_of_leaves + parent = Issue.generate! + Issue.generate!(:done_ratio => 20, :parent_issue_id => parent.id) + assert_equal 20, parent.reload.done_ratio + Issue.generate!(:done_ratio => 70, :parent_issue_id => parent.id) + assert_equal 45, parent.reload.done_ratio + + child = Issue.generate!(:done_ratio => 0, :parent_issue_id => parent.id) + assert_equal 30, parent.reload.done_ratio + + Issue.generate!(:done_ratio => 30, :parent_issue_id => child.id) + assert_equal 30, child.reload.done_ratio + assert_equal 40, parent.reload.done_ratio + end + + def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any + parent = Issue.generate! + Issue.generate!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id) + assert_equal 20, parent.reload.done_ratio + Issue.generate!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id) + assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio + end + + def test_parent_estimate_should_be_sum_of_leaves + parent = Issue.generate! + Issue.generate!(:estimated_hours => nil, :parent_issue_id => parent.id) + assert_equal nil, parent.reload.estimated_hours + Issue.generate!(:estimated_hours => 5, :parent_issue_id => parent.id) + assert_equal 5, parent.reload.estimated_hours + Issue.generate!(:estimated_hours => 7, :parent_issue_id => parent.id) + assert_equal 12, parent.reload.estimated_hours + end + + def test_move_parent_updates_old_parent_attributes + first_parent = Issue.generate! + second_parent = Issue.generate! + child = Issue.generate!(:estimated_hours => 5, :parent_issue_id => first_parent.id) + assert_equal 5, first_parent.reload.estimated_hours + child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id) + assert_equal 7, second_parent.reload.estimated_hours + assert_nil first_parent.reload.estimated_hours + end + + def test_reschuling_a_parent_should_reschedule_subtasks + parent = Issue.generate! + c1 = Issue.generate!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id) + c2 = Issue.generate!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id) + parent.reload + parent.reschedule_on!(Date.parse('2010-06-02')) + c1.reload + assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date] + c2.reload + assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change + parent.reload + assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date] + end + + def test_project_copy_should_copy_issue_tree + p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2]) + i1 = Issue.generate!(:project => p, :subject => 'i1') + i2 = Issue.generate!(:project => p, :subject => 'i2', :parent_issue_id => i1.id) + i3 = Issue.generate!(:project => p, :subject => 'i3', :parent_issue_id => i1.id) + i4 = Issue.generate!(:project => p, :subject => 'i4', :parent_issue_id => i2.id) + i5 = Issue.generate!(:project => p, :subject => 'i5') + c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2]) + c.copy(p, :only => 'issues') + c.reload + + assert_equal 5, c.issues.count + ic1, ic2, ic3, ic4, ic5 = c.issues.find(:all, :order => 'subject') + assert ic1.root? + assert_equal ic1, ic2.parent + assert_equal ic1, ic3.parent + assert_equal ic2, ic4.parent + assert ic5.root? + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/12/127326e6d209a7195a4e636ab9db2fcdcafc6686.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/12/127326e6d209a7195a4e636ab9db2fcdcafc6686.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,10 @@ +$('#issue_notes').val("<%= raw escape_javascript(@content) %>"); +<% + # when quoting a private journal, check the private checkbox + if @journal && @journal.private_notes? +%> +$('#issue_private_notes').attr('checked', true); +<% end %> + +showAndScrollTo("update", "notes"); +$('#notes').scrollTop = $('#notes').scrollHeight - $('#notes').clientHeight; diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/12/1274b8f28c749e735c8c75f99681e8f1e536c19a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/12/1274b8f28c749e735c8c75f99681e8f1e536c19a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,100 @@ += AwesomeNestedSet + +Awesome Nested Set is an implementation of the nested set pattern for ActiveRecord models. It is replacement for acts_as_nested_set and BetterNestedSet, but more awesome. + +Version 2 supports Rails 3. Gem versions prior to 2.0 support Rails 2. + +== What makes this so awesome? + +This is a new implementation of nested set based off of BetterNestedSet that fixes some bugs, removes tons of duplication, adds a few useful methods, and adds STI support. + +== Installation + + Add to your Gemfile: + + gem 'awesome_nested_set' + +== Usage + +To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id: + + class CreateCategories < ActiveRecord::Migration + def self.up + create_table :categories do |t| + t.string :name + t.integer :parent_id + t.integer :lft + t.integer :rgt + end + end + + def self.down + drop_table :categories + end + end + +Enable the nested set functionality by declaring acts_as_nested_set on your model + + class Category < ActiveRecord::Base + acts_as_nested_set + end + +Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet for more info. + +== Protecting attributes from mass assignment + +It's generally best to "white list" the attributes that can be used in mass assignment: + + class Category < ActiveRecord::Base + acts_as_nested_set + attr_accessible :name, :parent_id + end + +If for some reason that is not possible, you will probably want to protect the lft and rgt attributes: + + class Category < ActiveRecord::Base + acts_as_nested_set + attr_protected :lft, :rgt + end + +== Conversion from other trees + +Coming from acts_as_tree or another system where you only have a parent_id? No problem. Simply add the lft & rgt fields as above, and then run + + Category.rebuild! + +Your tree will be converted to a valid nested set. Awesome! + +== View Helper + +The view helper is called #nested_set_options. + +Example usage: + + <%= f.select :parent_id, nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" } %> + + <%= select_tag 'parent_id', options_for_select(nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" } ) %> + +See CollectiveIdea::Acts::NestedSet::Helper for more information about the helpers. + +== References + +You can learn more about nested sets at: http://threebit.net/tutorials/nestedset/tutorial1.html + +== How to contribute + +If you find what you might think is a bug: + +1. Check the GitHub issue tracker to see if anyone else has had the same issue. + http://github.com/collectiveidea/awesome_nested_set/issues/ +2. If you don't see anything, create an issue with information on how to reproduce it. + +If you want to contribute an enhancement or a fix: + +1. Fork the project on github. + http://github.com/collectiveidea/awesome_nested_set/ +2. Make your changes with tests. +3. Commit the changes without making changes to the Rakefile, VERSION, or any other files that aren't related to your enhancement or fix +4. Send a pull request. + +Copyright ©2008 Collective Idea, released under the MIT license diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/12/12b52cd60621e5d4f549e4bf4aad6c176256d275.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/12/12b52cd60621e5d4f549e4bf4aad6c176256d275.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,7 @@ +# Plugin's routes +# See: http://guides.rubyonrails.org/routing.html + +match 'projects/:id/hello', :to => 'example#say_hello', :via => 'get' +match 'projects/:id/bye', :to => 'example#say_goodbye', :via => 'get' + +resources 'meetings' diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/13/13746fa19acb8e1deab5a668a00a719d06273da8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/13/13746fa19acb8e1deab5a668a00a719d06273da8.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,73 @@ +--- +projects_001: + created_on: 2006-07-19 19:13:59 +02:00 + name: eCookbook + updated_on: 2006-07-19 22:53:01 +02:00 + id: 1 + description: Recipes management application + homepage: http://ecookbook.somenet.foo/ + is_public: true + identifier: ecookbook + parent_id: + lft: 1 + rgt: 10 +projects_002: + created_on: 2006-07-19 19:14:19 +02:00 + name: OnlineStore + updated_on: 2006-07-19 19:14:19 +02:00 + id: 2 + description: E-commerce web site + homepage: "" + is_public: false + identifier: onlinestore + parent_id: + lft: 11 + rgt: 12 +projects_003: + created_on: 2006-07-19 19:15:21 +02:00 + name: eCookbook Subproject 1 + updated_on: 2006-07-19 19:18:12 +02:00 + id: 3 + description: eCookBook Subproject 1 + homepage: "" + is_public: true + identifier: subproject1 + parent_id: 1 + lft: 6 + rgt: 7 +projects_004: + created_on: 2006-07-19 19:15:51 +02:00 + name: eCookbook Subproject 2 + updated_on: 2006-07-19 19:17:07 +02:00 + id: 4 + description: eCookbook Subproject 2 + homepage: "" + is_public: true + identifier: subproject2 + parent_id: 1 + lft: 8 + rgt: 9 +projects_005: + created_on: 2006-07-19 19:15:51 +02:00 + name: Private child of eCookbook + updated_on: 2006-07-19 19:17:07 +02:00 + id: 5 + description: This is a private subproject of a public project + homepage: "" + is_public: false + identifier: private-child + parent_id: 1 + lft: 2 + rgt: 5 +projects_006: + created_on: 2006-07-19 19:15:51 +02:00 + name: Child of private child + updated_on: 2006-07-19 19:17:07 +02:00 + id: 6 + description: This is a public subproject of a private project + homepage: "" + is_public: true + identifier: project6 + parent_id: 5 + lft: 3 + rgt: 4 diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/13/13a70fe72c228213d71cc1e6f52274fd3abf3062.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/13/13a70fe72c228213d71cc1e6f52274fd3abf3062.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,141 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Enumeration < ActiveRecord::Base + include Redmine::SubclassFactory + + default_scope :order => "#{Enumeration.table_name}.position ASC" + + belongs_to :project + + acts_as_list :scope => 'type = \'#{type}\'' + acts_as_customizable + acts_as_tree :order => 'position ASC' + + before_destroy :check_integrity + before_save :check_default + + attr_protected :type + + validates_presence_of :name + validates_uniqueness_of :name, :scope => [:type, :project_id] + validates_length_of :name, :maximum => 30 + + scope :shared, where(:project_id => nil) + scope :sorted, order("#{table_name}.position ASC") + scope :active, where(:active => true) + scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)} + + def self.default + # Creates a fake default scope so Enumeration.default will check + # it's type. STI subclasses will automatically add their own + # types to the finder. + if self.descends_from_active_record? + where(:is_default => true, :type => 'Enumeration').first + else + # STI classes are + where(:is_default => true).first + end + end + + # Overloaded on concrete classes + def option_name + nil + end + + def check_default + if is_default? && is_default_changed? + Enumeration.update_all({:is_default => false}, {:type => type}) + end + end + + # Overloaded on concrete classes + def objects_count + 0 + end + + def in_use? + self.objects_count != 0 + end + + # Is this enumeration overiding a system level enumeration? + def is_override? + !self.parent.nil? + end + + alias :destroy_without_reassign :destroy + + # Destroy the enumeration + # If a enumeration is specified, objects are reassigned + def destroy(reassign_to = nil) + if reassign_to && reassign_to.is_a?(Enumeration) + self.transfer_relations(reassign_to) + end + destroy_without_reassign + end + + def <=>(enumeration) + position <=> enumeration.position + end + + def to_s; name end + + # Returns the Subclasses of Enumeration. Each Subclass needs to be + # required in development mode. + # + # Note: subclasses is protected in ActiveRecord + def self.get_subclasses + subclasses + end + + # Does the +new+ Hash override the previous Enumeration? + def self.overridding_change?(new, previous) + if (same_active_state?(new['active'], previous.active)) && same_custom_values?(new,previous) + return false + else + return true + end + end + + # Does the +new+ Hash have the same custom values as the previous Enumeration? + def self.same_custom_values?(new, previous) + previous.custom_field_values.each do |custom_value| + if custom_value.value != new["custom_field_values"][custom_value.custom_field_id.to_s] + return false + end + end + + return true + end + + # Are the new and previous fields equal? + def self.same_active_state?(new, previous) + new = (new == "1" ? true : false) + return new == previous + end + +private + def check_integrity + raise "Can't delete enumeration" if self.in_use? + end + +end + +# Force load the subclasses in development mode +require_dependency 'time_entry_activity' +require_dependency 'document_category' +require_dependency 'issue_priority' diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/14/1411d2a90c9093fc6ada5ec42e5d250259f58396.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/14/1411d2a90c9093fc6ada5ec42e5d250259f58396.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,66 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class SettingsController < ApplicationController + layout 'admin' + menu_item :plugins, :only => :plugin + + before_filter :require_admin + + def index + edit + render :action => 'edit' + end + + def edit + @notifiables = Redmine::Notifiable.all + if request.post? && params[:settings] && params[:settings].is_a?(Hash) + settings = (params[:settings] || {}).dup.symbolize_keys + settings.each do |name, value| + # remove blank values in array settings + value.delete_if {|v| v.blank? } if value.is_a?(Array) + Setting[name] = value + end + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'edit', :tab => params[:tab] + else + @options = {} + user_format = User::USER_FORMATS.collect{|key, value| [key, value[:setting_order]]}.sort{|a, b| a[1] <=> b[1]} + @options[:user_format] = user_format.collect{|f| [User.current.name(f[0]), f[0].to_s]} + @deliveries = ActionMailer::Base.perform_deliveries + + @guessed_host_and_path = request.host_with_port.dup + @guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank? + + Redmine::Themes.rescan + end + end + + def plugin + @plugin = Redmine::Plugin.find(params[:id]) + if request.post? + Setting.send "plugin_#{@plugin.id}=", params[:settings] + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'plugin', :id => @plugin.id + else + @partial = @plugin.settings[:partial] + @settings = Setting.send "plugin_#{@plugin.id}" + end + rescue Redmine::PluginNotFound + render_404 + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/14/14309d64e537e5e954feb549559e6200d128036a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/14/14309d64e537e5e954feb549559e6200d128036a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,326 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class CustomField < ActiveRecord::Base + include Redmine::SubclassFactory + + has_many :custom_values, :dependent => :delete_all + acts_as_list :scope => 'type = \'#{self.class}\'' + serialize :possible_values + + validates_presence_of :name, :field_format + validates_uniqueness_of :name, :scope => :type + validates_length_of :name, :maximum => 30 + validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats + + validate :validate_custom_field + before_validation :set_searchable + + CUSTOM_FIELDS_TABS = [ + {:name => 'IssueCustomField', :partial => 'custom_fields/index', + :label => :label_issue_plural}, + {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', + :label => :label_spent_time}, + {:name => 'ProjectCustomField', :partial => 'custom_fields/index', + :label => :label_project_plural}, + {:name => 'VersionCustomField', :partial => 'custom_fields/index', + :label => :label_version_plural}, + {:name => 'UserCustomField', :partial => 'custom_fields/index', + :label => :label_user_plural}, + {:name => 'GroupCustomField', :partial => 'custom_fields/index', + :label => :label_group_plural}, + {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', + :label => TimeEntryActivity::OptionName}, + {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', + :label => IssuePriority::OptionName}, + {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', + :label => DocumentCategory::OptionName} + ] + + CUSTOM_FIELDS_NAMES = CUSTOM_FIELDS_TABS.collect{|v| v[:name]} + + def field_format=(arg) + # cannot change format of a saved custom field + super if new_record? + end + + def set_searchable + # make sure these fields are not searchable + self.searchable = false if %w(int float date bool).include?(field_format) + # make sure only these fields can have multiple values + self.multiple = false unless %w(list user version).include?(field_format) + true + end + + def validate_custom_field + if self.field_format == "list" + errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty? + errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array + end + + if regexp.present? + begin + Regexp.new(regexp) + rescue + errors.add(:regexp, :invalid) + end + end + + if default_value.present? && !valid_field_value?(default_value) + errors.add(:default_value, :invalid) + end + end + + def possible_values_options(obj=nil) + case field_format + when 'user', 'version' + if obj.respond_to?(:project) && obj.project + case field_format + when 'user' + obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]} + when 'version' + obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]} + end + elsif obj.is_a?(Array) + obj.collect {|o| possible_values_options(o)}.reduce(:&) + else + [] + end + when 'bool' + [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']] + else + possible_values || [] + end + end + + def possible_values(obj=nil) + case field_format + when 'user', 'version' + possible_values_options(obj).collect(&:last) + when 'bool' + ['1', '0'] + else + values = super() + if values.is_a?(Array) + values.each do |value| + value.force_encoding('UTF-8') if value.respond_to?(:force_encoding) + end + end + values || [] + end + end + + # Makes possible_values accept a multiline string + def possible_values=(arg) + if arg.is_a?(Array) + super(arg.compact.collect(&:strip).select {|v| !v.blank?}) + else + self.possible_values = arg.to_s.split(/[\n\r]+/) + end + end + + def cast_value(value) + casted = nil + unless value.blank? + case field_format + when 'string', 'text', 'list' + casted = value + when 'date' + casted = begin; value.to_date; rescue; nil end + when 'bool' + casted = (value == '1' ? true : false) + when 'int' + casted = value.to_i + when 'float' + casted = value.to_f + when 'user', 'version' + casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i)) + end + end + casted + end + + def value_from_keyword(keyword, customized) + possible_values_options = possible_values_options(customized) + if possible_values_options.present? + keyword = keyword.to_s.downcase + if v = possible_values_options.detect {|text, id| text.downcase == keyword} + if v.is_a?(Array) + v.last + else + v + end + end + else + keyword + end + end + + # Returns a ORDER BY clause that can used to sort customized + # objects by their value of the custom field. + # Returns nil if the custom field can not be used for sorting. + def order_statement + return nil if multiple? + case field_format + when 'string', 'text', 'list', 'date', 'bool' + # COALESCE is here to make sure that blank and NULL values are sorted equally + "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" + + " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" + + " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" + + " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')" + when 'int', 'float' + # Make the database cast values into numeric + # Postgresql will raise an error if a value can not be casted! + # CustomValue validations should ensure that it doesn't occur + "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" + + " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" + + " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" + + " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)" + when 'user', 'version' + value_class.fields_for_order_statement(value_join_alias) + else + nil + end + end + + # Returns a GROUP BY clause that can used to group by custom value + # Returns nil if the custom field can not be used for grouping. + def group_statement + return nil if multiple? + case field_format + when 'list', 'date', 'bool', 'int' + order_statement + when 'user', 'version' + "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" + + " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" + + " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" + + " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')" + else + nil + end + end + + def join_for_order_statement + case field_format + when 'user', 'version' + "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" + + " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" + + " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" + + " AND #{join_alias}.custom_field_id = #{id}" + + " AND #{join_alias}.value <> ''" + + " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" + + " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" + + " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" + + " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" + + " LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" + + " ON CAST(#{join_alias}.value as decimal(60,0)) = #{value_join_alias}.id" + else + nil + end + end + + def join_alias + "cf_#{id}" + end + + def value_join_alias + join_alias + "_" + field_format + end + + def <=>(field) + position <=> field.position + end + + # Returns the class that values represent + def value_class + case field_format + when 'user', 'version' + field_format.classify.constantize + else + nil + end + end + + def self.customized_class + self.name =~ /^(.+)CustomField$/ + begin; $1.constantize; rescue nil; end + end + + # to move in project_custom_field + def self.for_all + find(:all, :conditions => ["is_for_all=?", true], :order => 'position') + end + + def type_name + nil + end + + # Returns the error messages for the given value + # or an empty array if value is a valid value for the custom field + def validate_field_value(value) + errs = [] + if value.is_a?(Array) + if !multiple? + errs << ::I18n.t('activerecord.errors.messages.invalid') + end + if is_required? && value.detect(&:present?).nil? + errs << ::I18n.t('activerecord.errors.messages.blank') + end + value.each {|v| errs += validate_field_value_format(v)} + else + if is_required? && value.blank? + errs << ::I18n.t('activerecord.errors.messages.blank') + end + errs += validate_field_value_format(value) + end + errs + end + + # Returns true if value is a valid value for the custom field + def valid_field_value?(value) + validate_field_value(value).empty? + end + + def format_in?(*args) + args.include?(field_format) + end + + protected + + # Returns the error message for the given value regarding its format + def validate_field_value_format(value) + errs = [] + if value.present? + errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp) + errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length + errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length + + # Format specific validations + case field_format + when 'int' + errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/ + when 'float' + begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end + when 'date' + errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end + when 'list' + errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value) + end + end + errs + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/14/146e04b787ce58184275bdd027f7e85c4c9ff45f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/14/146e04b787ce58184275bdd027f7e85c4c9ff45f.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,19 @@ +

    <%=l(:label_information_plural)%>

    + +

    <%= Redmine::Info.versioned_name %>

    + + +<% @checklist.each do |label, result| %> + + + + +<% end %> +
    <%= l(label) %><%= image_tag((result ? 'true.png' : 'exclamation.png'), + :style => "vertical-align:bottom;") %>
    +
    +
    +
    <%= Redmine::Info.environment %>
    +
    + +<% html_title(l(:label_information_plural)) -%> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/14/149fe7d9ecdbec895382a22950f9b721ccb3953a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/14/149fe7d9ecdbec895382a22950f9b721ccb3953a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,98 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class EnumerationsController < ApplicationController + layout 'admin' + + before_filter :require_admin, :except => :index + before_filter :require_admin_or_api_request, :only => :index + before_filter :build_new_enumeration, :only => [:new, :create] + before_filter :find_enumeration, :only => [:edit, :update, :destroy] + accept_api_auth :index + + helper :custom_fields + + def index + respond_to do |format| + format.html + format.api { + @klass = Enumeration.get_subclass(params[:type]) + if @klass + @enumerations = @klass.shared.sorted.all + else + render_404 + end + } + end + end + + def new + end + + def create + if request.post? && @enumeration.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'index' + else + render :action => 'new' + end + end + + def edit + end + + def update + if request.put? && @enumeration.update_attributes(params[:enumeration]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'index' + else + render :action => 'edit' + end + end + + def destroy + if !@enumeration.in_use? + # No associated objects + @enumeration.destroy + redirect_to :action => 'index' + return + elsif params[:reassign_to_id] + if reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id]) + @enumeration.destroy(reassign_to) + redirect_to :action => 'index' + return + end + end + @enumerations = @enumeration.class.all - [@enumeration] + end + + private + + def build_new_enumeration + class_name = params[:enumeration] && params[:enumeration][:type] || params[:type] + @enumeration = Enumeration.new_subclass_instance(class_name, params[:enumeration]) + if @enumeration.nil? + render_404 + end + end + + def find_enumeration + @enumeration = Enumeration.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/14/14b30558ff27714592e93e747cd5345cdd0c4589.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/14/14b30558ff27714592e93e747cd5345cdd0c4589.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1163 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class ApplicationHelperTest < ActionView::TestCase + include ERB::Util + + fixtures :projects, :roles, :enabled_modules, :users, + :repositories, :changesets, + :trackers, :issue_statuses, :issues, :versions, :documents, + :wikis, :wiki_pages, :wiki_contents, + :boards, :messages, :news, + :attachments, :enumerations + + def setup + super + set_tmp_attachments_directory + end + + context "#link_to_if_authorized" do + context "authorized user" do + should "be tested" + end + + context "unauthorized user" do + should "be tested" + end + + should "allow using the :controller and :action for the target link" do + User.current = User.find_by_login('admin') + + @project = Issue.first.project # Used by helper + response = link_to_if_authorized("By controller/action", + {:controller => 'issues', :action => 'edit', :id => Issue.first.id}) + assert_match /href/, response + end + + end + + def test_auto_links + to_test = { + 'http://foo.bar' => 'http://foo.bar', + 'http://foo.bar/~user' => 'http://foo.bar/~user', + 'http://foo.bar.' => 'http://foo.bar.', + 'https://foo.bar.' => 'https://foo.bar.', + 'This is a link: http://foo.bar.' => 'This is a link: http://foo.bar.', + 'A link (eg. http://foo.bar).' => 'A link (eg. http://foo.bar).', + 'http://foo.bar/foo.bar#foo.bar.' => 'http://foo.bar/foo.bar#foo.bar.', + 'http://www.foo.bar/Test_(foobar)' => 'http://www.foo.bar/Test_(foobar)', + '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : http://www.foo.bar/Test_(foobar))', + '(see inline link : http://www.foo.bar/Test)' => '(see inline link : http://www.foo.bar/Test)', + '(see inline link : http://www.foo.bar/Test).' => '(see inline link : http://www.foo.bar/Test).', + '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see inline link)', + '(see "inline link":http://www.foo.bar/Test)' => '(see inline link)', + '(see "inline link":http://www.foo.bar/Test).' => '(see inline link).', + 'www.foo.bar' => 'www.foo.bar', + 'http://foo.bar/page?p=1&t=z&s=' => 'http://foo.bar/page?p=1&t=z&s=', + 'http://foo.bar/page#125' => 'http://foo.bar/page#125', + 'http://foo@www.bar.com' => 'http://foo@www.bar.com', + 'http://foo:bar@www.bar.com' => 'http://foo:bar@www.bar.com', + 'ftp://foo.bar' => 'ftp://foo.bar', + 'ftps://foo.bar' => 'ftps://foo.bar', + 'sftp://foo.bar' => 'sftp://foo.bar', + # two exclamation marks + 'http://example.net/path!602815048C7B5C20!302.html' => 'http://example.net/path!602815048C7B5C20!302.html', + # escaping + 'http://foo"bar' => 'http://foo"bar', + # wrap in angle brackets + '' => '<http://foo.bar>' + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + if 'ruby'.respond_to?(:encoding) + def test_auto_links_with_non_ascii_characters + to_test = { + 'http://foo.bar/теÑÑ‚' => 'http://foo.bar/теÑÑ‚' + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + else + puts 'Skipping test_auto_links_with_non_ascii_characters, unsupported ruby version' + end + + def test_auto_mailto + assert_equal '

    ', + textilizable('test@foo.bar') + end + + def test_inline_images + to_test = { + '!http://foo.bar/image.jpg!' => '', + 'floating !>http://foo.bar/image.jpg!' => 'floating
    ', + 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class ', + 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style ', + 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title This is a title', + 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title This is a double-quoted "title"', + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_inline_images_inside_tags + raw = <<-RAW +h1. !foo.png! Heading + +Centered image: + +p=. !bar.gif! +RAW + + assert textilizable(raw).include?('') + assert textilizable(raw).include?('') + end + + def test_attached_images + to_test = { + 'Inline image: !logo.gif!' => 'Inline image: This is a logo', + 'Inline image: !logo.GIF!' => 'Inline image: This is a logo', + 'No match: !ogo.gif!' => 'No match: ', + 'No match: !ogo.GIF!' => 'No match: ', + # link image + '!logo.gif!:http://foo.bar/' => 'This is a logo', + } + attachments = Attachment.find(:all) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => attachments) } + end + + def test_attached_images_filename_extension + set_tmp_attachments_directory + a1 = Attachment.new( + :container => Issue.find(1), + :file => mock_file_with_options({:original_filename => "testtest.JPG"}), + :author => User.find(1)) + assert a1.save + assert_equal "testtest.JPG", a1.filename + assert_equal "image/jpeg", a1.content_type + assert a1.image? + + a2 = Attachment.new( + :container => Issue.find(1), + :file => mock_file_with_options({:original_filename => "testtest.jpeg"}), + :author => User.find(1)) + assert a2.save + assert_equal "testtest.jpeg", a2.filename + assert_equal "image/jpeg", a2.content_type + assert a2.image? + + a3 = Attachment.new( + :container => Issue.find(1), + :file => mock_file_with_options({:original_filename => "testtest.JPE"}), + :author => User.find(1)) + assert a3.save + assert_equal "testtest.JPE", a3.filename + assert_equal "image/jpeg", a3.content_type + assert a3.image? + + a4 = Attachment.new( + :container => Issue.find(1), + :file => mock_file_with_options({:original_filename => "Testtest.BMP"}), + :author => User.find(1)) + assert a4.save + assert_equal "Testtest.BMP", a4.filename + assert_equal "image/x-ms-bmp", a4.content_type + assert a4.image? + + to_test = { + 'Inline image: !testtest.jpg!' => + 'Inline image: ', + 'Inline image: !testtest.jpeg!' => + 'Inline image: ', + 'Inline image: !testtest.jpe!' => + 'Inline image: ', + 'Inline image: !testtest.bmp!' => + 'Inline image: ', + } + + attachments = [a1, a2, a3, a4] + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => attachments) } + end + + def test_attached_images_should_read_later + set_fixtures_attachments_directory + a1 = Attachment.find(16) + assert_equal "testfile.png", a1.filename + assert a1.readable? + assert (! a1.visible?(User.anonymous)) + assert a1.visible?(User.find(2)) + a2 = Attachment.find(17) + assert_equal "testfile.PNG", a2.filename + assert a2.readable? + assert (! a2.visible?(User.anonymous)) + assert a2.visible?(User.find(2)) + assert a1.created_on < a2.created_on + + to_test = { + 'Inline image: !testfile.png!' => + 'Inline image: ', + 'Inline image: !Testfile.PNG!' => + 'Inline image: ', + } + attachments = [a1, a2] + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => attachments) } + set_tmp_attachments_directory + end + + def test_textile_external_links + to_test = { + 'This is a "link":http://foo.bar' => 'This is a link', + 'This is an intern "link":/foo/bar' => 'This is an intern link', + '"link (Link title)":http://foo.bar' => 'link', + '"link (Link title with "double-quotes")":http://foo.bar' => 'link', + "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":

    \n\n\n\t

    Another paragraph", + # no multiline link text + "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line
    and another on a second line\":test", + # mailto link + "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "system administrator", + # two exclamation marks + '"a link":http://example.net/path!602815048C7B5C20!302.html' => 'a link', + # escaping + '"test":http://foo"bar' => 'test', + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + if 'ruby'.respond_to?(:encoding) + def test_textile_external_links_with_non_ascii_characters + to_test = { + 'This is a "link":http://foo.bar/теÑÑ‚' => 'This is a link' + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + else + puts 'Skipping test_textile_external_links_with_non_ascii_characters, unsupported ruby version' + end + + def test_redmine_links + issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3}, + :class => 'issue status-1 priority-4 priority-lowest overdue', :title => 'Error 281 when updating a recipe (New)') + note_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'}, + :class => 'issue status-1 priority-4 priority-lowest overdue', :title => 'Error 281 when updating a recipe (New)') + + changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1}, + :class => 'changeset', :title => 'My very first commit') + changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, + :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') + + document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1}, + :class => 'document') + + version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2}, + :class => 'version') + + board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'} + + message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4} + + news_url = {:controller => 'news', :action => 'show', :id => 1} + + project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'} + + source_url = '/projects/ecookbook/repository/entry/some/file' + source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file' + source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext' + source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext' + + export_url = '/projects/ecookbook/repository/raw/some/file' + export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file' + export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext' + export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext' + + to_test = { + # tickets + '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.", + # ticket notes + '#3-14' => note_link, + '#3#note-14' => note_link, + # should not ignore leading zero + '#03' => '#03', + # changesets + 'r1' => changeset_link, + 'r1.' => "#{changeset_link}.", + 'r1, r2' => "#{changeset_link}, #{changeset_link2}", + 'r1,r2' => "#{changeset_link},#{changeset_link2}", + # documents + 'document#1' => document_link, + 'document:"Test document"' => document_link, + # versions + 'version#2' => version_link, + 'version:1.0' => version_link, + 'version:"1.0"' => version_link, + # source + 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'), + 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'), + 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".", + 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".", + 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".", + 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".", + 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",", + 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'), + 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'), + 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'), + 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'), + 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'), + # export + 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'), + 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'), + 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'), + 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'), + # forum + 'forum#2' => link_to('Discussion', board_url, :class => 'board'), + 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'), + # message + 'message#4' => link_to('Post 2', message_url, :class => 'message'), + 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'), + # news + 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'), + 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'), + # project + 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'), + 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'), + 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'), + # not found + '#0123456789' => '#0123456789', + # invalid expressions + 'source:' => 'source:', + # url hash + "http://foo.bar/FAQ#3" => 'http://foo.bar/FAQ#3', + } + @project = Project.find(1) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text), "#{text} failed" } + end + + def test_escaped_redmine_links_should_not_be_parsed + to_test = [ + '#3.', + '#3-14.', + '#3#-note14.', + 'r1', + 'document#1', + 'document:"Test document"', + 'version#2', + 'version:1.0', + 'version:"1.0"', + 'source:/some/file' + ] + @project = Project.find(1) + to_test.each { |text| assert_equal "

    #{text}

    ", textilizable("!" + text), "#{text} failed" } + end + + def test_cross_project_redmine_links + source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, + :class => 'source') + + changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, + :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') + + to_test = { + # documents + 'document:"Test document"' => 'document:"Test document"', + 'ecookbook:document:"Test document"' => 'Test document', + 'invalid:document:"Test document"' => 'invalid:document:"Test document"', + # versions + 'version:"1.0"' => 'version:"1.0"', + 'ecookbook:version:"1.0"' => '1.0', + 'invalid:version:"1.0"' => 'invalid:version:"1.0"', + # changeset + 'r2' => 'r2', + 'ecookbook:r2' => changeset_link, + 'invalid:r2' => 'invalid:r2', + # source + 'source:/some/file' => 'source:/some/file', + 'ecookbook:source:/some/file' => source_link, + 'invalid:source:/some/file' => 'invalid:source:/some/file', + } + @project = Project.find(3) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text), "#{text} failed" } + end + + def test_multiple_repositories_redmine_links + svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg') + Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123') + hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg') + Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd') + + changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, + :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') + svn_changeset_link = link_to('svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123}, + :class => 'changeset', :title => '') + hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'}, + :class => 'changeset', :title => '') + + source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source') + hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source') + + to_test = { + 'r2' => changeset_link, + 'svn1|r123' => svn_changeset_link, + 'invalid|r123' => 'invalid|r123', + 'commit:hg1|abcd' => hg_changeset_link, + 'commit:invalid|abcd' => 'commit:invalid|abcd', + # source + 'source:some/file' => source_link, + 'source:hg1|some/file' => hg_source_link, + 'source:invalid|some/file' => 'source:invalid|some/file', + } + + @project = Project.find(1) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text), "#{text} failed" } + end + + def test_cross_project_multiple_repositories_redmine_links + svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg') + Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123') + hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg') + Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd') + + changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, + :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') + svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123}, + :class => 'changeset', :title => '') + hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'}, + :class => 'changeset', :title => '') + + source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source') + hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source') + + to_test = { + 'ecookbook:r2' => changeset_link, + 'ecookbook:svn1|r123' => svn_changeset_link, + 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123', + 'ecookbook:commit:hg1|abcd' => hg_changeset_link, + 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd', + 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd', + # source + 'ecookbook:source:some/file' => source_link, + 'ecookbook:source:hg1|some/file' => hg_source_link, + 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file', + 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file', + } + + @project = Project.find(3) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text), "#{text} failed" } + end + + def test_redmine_links_git_commit + changeset_link = link_to('abcd', + { + :controller => 'repositories', + :action => 'revision', + :id => 'subproject1', + :rev => 'abcd', + }, + :class => 'changeset', :title => 'test commit') + to_test = { + 'commit:abcd' => changeset_link, + } + @project = Project.find(3) + r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git') + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => 'abcd', + :scmid => 'abcd', + :comments => 'test commit') + assert( c.save ) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'. + def test_redmine_links_darcs_commit + changeset_link = link_to('20080308225258-98289-abcd456efg.gz', + { + :controller => 'repositories', + :action => 'revision', + :id => 'subproject1', + :rev => '123', + }, + :class => 'changeset', :title => 'test commit') + to_test = { + 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link, + } + @project = Project.find(3) + r = Repository::Darcs.create!( + :project => @project, :url => '/tmp/test/darcs', + :log_encoding => 'UTF-8') + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '20080308225258-98289-abcd456efg.gz', + :comments => 'test commit') + assert( c.save ) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_redmine_links_mercurial_commit + changeset_link_rev = link_to('r123', + { + :controller => 'repositories', + :action => 'revision', + :id => 'subproject1', + :rev => '123' , + }, + :class => 'changeset', :title => 'test commit') + changeset_link_commit = link_to('abcd', + { + :controller => 'repositories', + :action => 'revision', + :id => 'subproject1', + :rev => 'abcd' , + }, + :class => 'changeset', :title => 'test commit') + to_test = { + 'r123' => changeset_link_rev, + 'commit:abcd' => changeset_link_commit, + } + @project = Project.find(3) + r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test') + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => 'abcd', + :comments => 'test commit') + assert( c.save ) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_attachment_links + attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment') + to_test = { + 'attachment:error281.txt' => attachment_link + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" } + end + + def test_wiki_links + to_test = { + '[[CookBook documentation]]' => 'CookBook documentation', + '[[Another page|Page]]' => 'Page', + # title content should be formatted + '[[Another page|With _styled_ *title*]]' => 'With styled title', + '[[Another page|With title containing HTML entities & markups]]' => 'With title containing <strong>HTML entities & markups</strong>', + # link with anchor + '[[CookBook documentation#One-section]]' => 'CookBook documentation', + '[[Another page#anchor|Page]]' => 'Page', + # UTF8 anchor + '[[Another_page#ТеÑÑ‚|ТеÑÑ‚]]' => %|ТеÑÑ‚|, + # page that doesn't exist + '[[Unknown page]]' => 'Unknown page', + '[[Unknown page|404]]' => '404', + # link to another project wiki + '[[onlinestore:]]' => 'onlinestore', + '[[onlinestore:|Wiki]]' => 'Wiki', + '[[onlinestore:Start page]]' => 'Start page', + '[[onlinestore:Start page|Text]]' => 'Text', + '[[onlinestore:Unknown page]]' => 'Unknown page', + # striked through link + '-[[Another page|Page]]-' => 'Page', + '-[[Another page|Page]] link-' => 'Page link', + # escaping + '![[Another page|Page]]' => '[[Another page|Page]]', + # project does not exist + '[[unknowproject:Start]]' => '[[unknowproject:Start]]', + '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]', + } + + @project = Project.find(1) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_wiki_links_within_local_file_generation_context + + to_test = { + # link to a page + '[[CookBook documentation]]' => 'CookBook documentation', + '[[CookBook documentation|documentation]]' => 'documentation', + '[[CookBook documentation#One-section]]' => 'CookBook documentation', + '[[CookBook documentation#One-section|documentation]]' => 'documentation', + # page that doesn't exist + '[[Unknown page]]' => 'Unknown page', + '[[Unknown page|404]]' => '404', + '[[Unknown page#anchor]]' => 'Unknown page', + '[[Unknown page#anchor|404]]' => '404', + } + + @project = Project.find(1) + + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :wiki_links => :local) } + end + + def test_wiki_links_within_wiki_page_context + + page = WikiPage.find_by_title('Another_page' ) + + to_test = { + # link to another page + '[[CookBook documentation]]' => 'CookBook documentation', + '[[CookBook documentation|documentation]]' => 'documentation', + '[[CookBook documentation#One-section]]' => 'CookBook documentation', + '[[CookBook documentation#One-section|documentation]]' => 'documentation', + # link to the current page + '[[Another page]]' => 'Another page', + '[[Another page|Page]]' => 'Page', + '[[Another page#anchor]]' => 'Another page', + '[[Another page#anchor|Page]]' => 'Page', + # page that doesn't exist + '[[Unknown page]]' => 'Unknown page', + '[[Unknown page|404]]' => '404', + '[[Unknown page#anchor]]' => 'Unknown page', + '[[Unknown page#anchor|404]]' => '404', + } + + @project = Project.find(1) + + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(WikiContent.new( :text => text, :page => page ), :text) } + end + + def test_wiki_links_anchor_option_should_prepend_page_title_to_href + + to_test = { + # link to a page + '[[CookBook documentation]]' => 'CookBook documentation', + '[[CookBook documentation|documentation]]' => 'documentation', + '[[CookBook documentation#One-section]]' => 'CookBook documentation', + '[[CookBook documentation#One-section|documentation]]' => 'documentation', + # page that doesn't exist + '[[Unknown page]]' => 'Unknown page', + '[[Unknown page|404]]' => '404', + '[[Unknown page#anchor]]' => 'Unknown page', + '[[Unknown page#anchor|404]]' => '404', + } + + @project = Project.find(1) + + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :wiki_links => :anchor) } + end + + def test_html_tags + to_test = { + "
    content
    " => "

    <div>content</div>

    ", + "
    content
    " => "

    <div class=\"bold\">content</div>

    ", + "" => "

    <script>some script;</script>

    ", + # do not escape pre/code tags + "
    \nline 1\nline2
    " => "
    \nline 1\nline2
    ", + "
    \nline 1\nline2
    " => "
    \nline 1\nline2
    ", + "
    content
    " => "
    <div>content</div>
    ", + "HTML comment: " => "

    HTML comment: <!-- no comments -->

    ", + " +


    +<%= f.text_field :subject, :size => 120, :id => "message_subject" %> + +<% unless replying %> + <% if @message.safe_attribute? 'sticky' %> + <%= f.check_box :sticky %> <%= label_tag 'message_sticky', l(:label_board_sticky) %> + <% end %> + <% if @message.safe_attribute? 'locked' %> + <%= f.check_box :locked %> <%= label_tag 'message_locked', l(:label_board_locked) %> + <% end %> +<% end %> +

    + +<% if !replying && !@message.new_record? && @message.safe_attribute?('board_id') %> +


    + <%= f.select :board_id, boards_options_for_select(@message.project.boards) %>

    +<% end %> + +

    +<%= label_tag "message_content", l(:description_message_content), :class => "hidden-for-sighted" %> +<%= f.text_area :content, :cols => 80, :rows => 15, :class => 'wiki-edit', :id => 'message_content' %>

    +<%= wikitoolbar_for 'message_content' %> + + +

    <%= l(:label_attachment_plural) %>
    +<%= render :partial => 'attachments/form', :locals => {:container => @message} %>

    + diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/18/18d4843fce40636d9559a1afe5d262720f5f6601.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/18/18d4843fce40636d9559a1afe5d262720f5f6601.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,20 @@ +Copyright (c) 2007-2011 Collective Idea + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/18/18ef0fecd81e51c21a37f84582d5c50bacf3c380.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/18/18ef0fecd81e51c21a37f84582d5c50bacf3c380.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,68 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class WikiContentVersionTest < ActiveSupport::TestCase + fixtures :projects, :users, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions + + def setup + end + + def test_destroy + v = WikiContent::Version.find(2) + + assert_difference 'WikiContent::Version.count', -1 do + v.destroy + end + end + + def test_destroy_last_version_should_revert_content + v = WikiContent::Version.find(3) + + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_difference 'WikiContent::Version.count', -1 do + assert v.destroy + end + end + end + c = WikiContent.find(1) + v = c.versions.last + assert_equal 2, c.version + assert_equal v.version, c.version + assert_equal v.comments, c.comments + assert_equal v.text, c.text + assert_equal v.author, c.author + assert_equal v.updated_on, c.updated_on + end + + def test_destroy_all_versions_should_delete_page + WikiContent::Version.find(1).destroy + WikiContent::Version.find(2).destroy + v = WikiContent::Version.find(3) + + assert_difference 'WikiPage.count', -1 do + assert_difference 'WikiContent.count', -1 do + assert_difference 'WikiContent::Version.count', -1 do + assert v.destroy + end + end + end + assert_nil WikiPage.find_by_id(1) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/19/1905c04f7ae0c6da24070033b03c6ae55e5dcb3e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/19/1905c04f7ae0c6da24070033b03c6ae55e5dcb3e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,95 @@ +<%= render :partial => 'action_menu' %> + +

    <%=l(:label_workflow)%>

    + +
    +
      +
    • <%= link_to l(:label_status_transitions), {:action => 'edit', :role_id => @role, :tracker_id => @tracker} %>
    • +
    • <%= link_to l(:label_fields_permissions), {:action => 'permissions', :role_id => @role, :tracker_id => @tracker}, :class => 'selected' %>
    • +
    +
    + +

    <%=l(:text_workflow_edit)%>:

    + +<%= form_tag({}, :method => 'get') do %> +

    + + + + + <%= submit_tag l(:button_edit), :name => nil %> + + <%= hidden_field_tag 'used_statuses_only', '0' %> + +

    +<% end %> + +<% if @tracker && @role && @statuses.any? %> + <%= form_tag({}, :id => 'workflow_form' ) do %> + <%= hidden_field_tag 'tracker_id', @tracker.id %> + <%= hidden_field_tag 'role_id', @role.id %> + <%= hidden_field_tag 'used_statuses_only', params[:used_statuses_only] %> +
    + + + + + + + + + <% for status in @statuses %> + + <% end %> + + + + + + + <% @fields.each do |field, name| %> + "> + + <% for status in @statuses -%> + + <% end -%> + + <% end %> + <% if @custom_fields.any? %> + + + + <% @custom_fields.each do |field| %> + "> + + <% for status in @statuses -%> + + <% end -%> + + <% end %> + <% end %> + +
    + <%=l(:label_issue_status)%>
    + <%=h status.name %> +
    +   + <%= l(:field_core_fields) %> +
    + <%=h name %> <%= content_tag('span', '*', :class => 'required') if field_required?(field) %> + + <%= field_permission_tag(@permissions, status, field) %> +
    +   + <%= l(:label_custom_field_plural) %> +
    + <%=h field.name %> <%= content_tag('span', '*', :class => 'required') if field_required?(field) %> + + <%= field_permission_tag(@permissions, status, field) %> +
    +
    + <%= submit_tag l(:button_save) %> + <% end %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/19/1959b21aed080e49e7eb19db5f9d559c39f78f7a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/19/1959b21aed080e49e7eb19db5f9d559c39f78f7a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,154 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) +require 'journals_controller' + +# Re-raise errors caught by the controller. +class JournalsController; def rescue_action(e) raise e end; end + +class JournalsControllerTest < ActionController::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, :issues, :journals, :journal_details, :enabled_modules, + :trackers, :issue_statuses, :enumerations, :custom_fields, :custom_values, :custom_fields_projects + + def setup + @controller = JournalsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_index + get :index, :project_id => 1 + assert_response :success + assert_not_nil assigns(:journals) + assert_equal 'application/atom+xml', @response.content_type + end + + def test_index_should_return_privates_notes_with_permission_only + journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true, :user_id => 1) + @request.session[:user_id] = 2 + + get :index, :project_id => 1 + assert_response :success + assert_include journal, assigns(:journals) + + Role.find(1).remove_permission! :view_private_notes + get :index, :project_id => 1 + assert_response :success + assert_not_include journal, assigns(:journals) + end + + def test_diff + get :diff, :id => 3, :detail_id => 4 + assert_response :success + assert_template 'diff' + + assert_tag 'span', + :attributes => {:class => 'diff_out'}, + :content => /removed/ + assert_tag 'span', + :attributes => {:class => 'diff_in'}, + :content => /added/ + end + + def test_reply_to_issue + @request.session[:user_id] = 2 + xhr :get, :new, :id => 6 + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + assert_include '> This is an issue', response.body + end + + def test_reply_to_issue_without_permission + @request.session[:user_id] = 7 + xhr :get, :new, :id => 6 + assert_response 403 + end + + def test_reply_to_note + @request.session[:user_id] = 2 + xhr :get, :new, :id => 6, :journal_id => 4 + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + assert_include '> A comment with a private version', response.body + end + + def test_reply_to_private_note_should_fail_without_permission + journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true) + @request.session[:user_id] = 2 + + xhr :get, :new, :id => 2, :journal_id => journal.id + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + assert_include '> Privates notes', response.body + + Role.find(1).remove_permission! :view_private_notes + xhr :get, :new, :id => 2, :journal_id => journal.id + assert_response 404 + end + + def test_edit_xhr + @request.session[:user_id] = 1 + xhr :get, :edit, :id => 2 + assert_response :success + assert_template 'edit' + assert_equal 'text/javascript', response.content_type + assert_include 'textarea', response.body + end + + def test_edit_private_note_should_fail_without_permission + journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Privates notes', :private_notes => true) + @request.session[:user_id] = 2 + Role.find(1).add_permission! :edit_issue_notes + + xhr :get, :edit, :id => journal.id + assert_response :success + assert_template 'edit' + assert_equal 'text/javascript', response.content_type + assert_include 'textarea', response.body + + Role.find(1).remove_permission! :view_private_notes + xhr :get, :edit, :id => journal.id + assert_response 404 + end + + def test_update_xhr + @request.session[:user_id] = 1 + xhr :post, :edit, :id => 2, :notes => 'Updated notes' + assert_response :success + assert_template 'update' + assert_equal 'text/javascript', response.content_type + assert_equal 'Updated notes', Journal.find(2).notes + assert_include 'journal-2-notes', response.body + end + + def test_update_xhr_with_empty_notes_should_delete_the_journal + @request.session[:user_id] = 1 + assert_difference 'Journal.count', -1 do + xhr :post, :edit, :id => 2, :notes => '' + assert_response :success + assert_template 'update' + assert_equal 'text/javascript', response.content_type + end + assert_nil Journal.find_by_id(2) + assert_include 'change-2', response.body + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/19/196c7c4e4ef7e50cda7944736d264f90424a0cab.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/19/196c7c4e4ef7e50cda7944736d264f90424a0cab.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,145 @@ +#!/usr/bin/env ruby + +require 'net/http' +require 'net/https' +require 'uri' +require 'optparse' + +module Net + class HTTPS < HTTP + def self.post_form(url, params, headers, options={}) + request = Post.new(url.path) + request.form_data = params + request.initialize_http_header(headers) + request.basic_auth url.user, url.password if url.user + http = new(url.host, url.port) + http.use_ssl = (url.scheme == 'https') + if options[:no_check_certificate] + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + http.start {|h| h.request(request) } + end + end +end + +class RedmineMailHandler + VERSION = '0.2.1' + + attr_accessor :verbose, :issue_attributes, :allow_override, :unknown_user, :no_permission_check, :url, :key, :no_check_certificate + + def initialize + self.issue_attributes = {} + + optparse = OptionParser.new do |opts| + opts.banner = "Usage: rdm-mailhandler.rb [options] --url= --key=" + opts.separator("") + opts.separator("Reads an email from standard input and forward it to a Redmine server through a HTTP request.") + opts.separator("") + opts.separator("Required arguments:") + opts.on("-u", "--url URL", "URL of the Redmine server") {|v| self.url = v} + opts.on("-k", "--key KEY", "Redmine API key") {|v| self.key = v} + opts.separator("") + opts.separator("General options:") + opts.on("--unknown-user ACTION", "how to handle emails from an unknown user", + "ACTION can be one of the following values:", + "* ignore: email is ignored (default)", + "* accept: accept as anonymous user", + "* create: create a user account") {|v| self.unknown_user = v} + opts.on("--no-permission-check", "disable permission checking when receiving", + "the email") {self.no_permission_check = '1'} + opts.on("--key-file FILE", "path to a file that contains the Redmine", + "API key (use this option instead of --key", + "if you don't the key to appear in the", + "command line)") {|v| read_key_from_file(v)} + opts.on("--no-check-certificate", "do not check server certificate") {self.no_check_certificate = true} + opts.on("-h", "--help", "show this help") {puts opts; exit 1} + opts.on("-v", "--verbose", "show extra information") {self.verbose = true} + opts.on("-V", "--version", "show version information and exit") {puts VERSION; exit} + opts.separator("") + opts.separator("Issue attributes control options:") + opts.on("-p", "--project PROJECT", "identifier of the target project") {|v| self.issue_attributes['project'] = v} + opts.on("-s", "--status STATUS", "name of the target status") {|v| self.issue_attributes['status'] = v} + opts.on("-t", "--tracker TRACKER", "name of the target tracker") {|v| self.issue_attributes['tracker'] = v} + opts.on( "--category CATEGORY", "name of the target category") {|v| self.issue_attributes['category'] = v} + opts.on( "--priority PRIORITY", "name of the target priority") {|v| self.issue_attributes['priority'] = v} + opts.on("-o", "--allow-override ATTRS", "allow email content to override attributes", + "specified by previous options", + "ATTRS is a comma separated list of attributes") {|v| self.allow_override = v} + opts.separator("") + opts.separator("Examples:") + opts.separator("No project specified. Emails MUST contain the 'Project' keyword:") + opts.separator(" rdm-mailhandler.rb --url http://redmine.domain.foo --key secret") + opts.separator("") + opts.separator("Fixed project and default tracker specified, but emails can override") + opts.separator("both tracker and priority attributes using keywords:") + opts.separator(" rdm-mailhandler.rb --url https://domain.foo/redmine --key secret \\") + opts.separator(" --project foo \\") + opts.separator(" --tracker bug \\") + opts.separator(" --allow-override tracker,priority") + + opts.summary_width = 27 + end + optparse.parse! + + unless url && key + puts "Some arguments are missing. Use `rdm-mailhandler.rb --help` for getting help." + exit 1 + end + end + + def submit(email) + uri = url.gsub(%r{/*$}, '') + '/mail_handler' + + headers = { 'User-Agent' => "Redmine mail handler/#{VERSION}" } + + data = { 'key' => key, 'email' => email, + 'allow_override' => allow_override, + 'unknown_user' => unknown_user, + 'no_permission_check' => no_permission_check} + issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value } + + debug "Posting to #{uri}..." + response = Net::HTTPS.post_form(URI.parse(uri), data, headers, :no_check_certificate => no_check_certificate) + debug "Response received: #{response.code}" + + case response.code.to_i + when 403 + warn "Request was denied by your Redmine server. " + + "Make sure that 'WS for incoming emails' is enabled in application settings and that you provided the correct API key." + return 77 + when 422 + warn "Request was denied by your Redmine server. " + + "Possible reasons: email is sent from an invalid email address or is missing some information." + return 77 + when 400..499 + warn "Request was denied by your Redmine server (#{response.code})." + return 77 + when 500..599 + warn "Failed to contact your Redmine server (#{response.code})." + return 75 + when 201 + debug "Proccessed successfully" + return 0 + else + return 1 + end + end + + private + + def debug(msg) + puts msg if verbose + end + + def read_key_from_file(filename) + begin + self.key = File.read(filename).strip + rescue Exception => e + $stderr.puts "Unable to read the key from #{filename}:\n#{e.message}" + exit 1 + end + end +end + +handler = RedmineMailHandler.new +exit(handler.submit(STDIN.read)) diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/19/19c2933293d5536cb86e9ab59b2a501d3507d1a0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/19/19c2933293d5536cb86e9ab59b2a501d3507d1a0.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,182 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingWikiTest < ActionController::IntegrationTest + def test_wiki_matching + assert_routing( + { :method => 'get', :path => "/projects/567/wiki" }, + { :controller => 'wiki', :action => 'show', :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/lalala" }, + { :controller => 'wiki', :action => 'show', :project_id => '567', + :id => 'lalala' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/lalala.pdf" }, + { :controller => 'wiki', :action => 'show', :project_id => '567', + :id => 'lalala', :format => 'pdf' } + ) + assert_routing( + { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/diff" }, + { :controller => 'wiki', :action => 'diff', :project_id => '1', + :id => 'CookBook_documentation' } + ) + assert_routing( + { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2" }, + { :controller => 'wiki', :action => 'show', :project_id => '1', + :id => 'CookBook_documentation', :version => '2' } + ) + assert_routing( + { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2/diff" }, + { :controller => 'wiki', :action => 'diff', :project_id => '1', + :id => 'CookBook_documentation', :version => '2' } + ) + assert_routing( + { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2/annotate" }, + { :controller => 'wiki', :action => 'annotate', :project_id => '1', + :id => 'CookBook_documentation', :version => '2' } + ) + end + + def test_wiki_misc + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/date_index" }, + { :controller => 'wiki', :action => 'date_index', :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/export" }, + { :controller => 'wiki', :action => 'export', :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/export.pdf" }, + { :controller => 'wiki', :action => 'export', :project_id => '567', :format => 'pdf' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/index" }, + { :controller => 'wiki', :action => 'index', :project_id => '567' } + ) + end + + def test_wiki_resources + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/my_page/edit" }, + { :controller => 'wiki', :action => 'edit', :project_id => '567', + :id => 'my_page' } + ) + assert_routing( + { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/history" }, + { :controller => 'wiki', :action => 'history', :project_id => '1', + :id => 'CookBook_documentation' } + ) + assert_routing( + { :method => 'get', :path => "/projects/22/wiki/ladida/rename" }, + { :controller => 'wiki', :action => 'rename', :project_id => '22', + :id => 'ladida' } + ) + ["post", "put"].each do |method| + assert_routing( + { :method => method, :path => "/projects/567/wiki/CookBook_documentation/preview" }, + { :controller => 'wiki', :action => 'preview', :project_id => '567', + :id => 'CookBook_documentation' } + ) + end + assert_routing( + { :method => 'post', :path => "/projects/22/wiki/ladida/rename" }, + { :controller => 'wiki', :action => 'rename', :project_id => '22', + :id => 'ladida' } + ) + assert_routing( + { :method => 'post', :path => "/projects/22/wiki/ladida/protect" }, + { :controller => 'wiki', :action => 'protect', :project_id => '22', + :id => 'ladida' } + ) + assert_routing( + { :method => 'post', :path => "/projects/22/wiki/ladida/add_attachment" }, + { :controller => 'wiki', :action => 'add_attachment', :project_id => '22', + :id => 'ladida' } + ) + assert_routing( + { :method => 'put', :path => "/projects/567/wiki/my_page" }, + { :controller => 'wiki', :action => 'update', :project_id => '567', + :id => 'my_page' } + ) + assert_routing( + { :method => 'delete', :path => "/projects/22/wiki/ladida" }, + { :controller => 'wiki', :action => 'destroy', :project_id => '22', + :id => 'ladida' } + ) + assert_routing( + { :method => 'delete', :path => "/projects/22/wiki/ladida/3" }, + { :controller => 'wiki', :action => 'destroy_version', :project_id => '22', + :id => 'ladida', :version => '3' } + ) + end + + def test_api + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/my_page.xml" }, + { :controller => 'wiki', :action => 'show', :project_id => '567', + :id => 'my_page', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/my_page.json" }, + { :controller => 'wiki', :action => 'show', :project_id => '567', + :id => 'my_page', :format => 'json' } + ) + assert_routing( + { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2.xml" }, + { :controller => 'wiki', :action => 'show', :project_id => '1', + :id => 'CookBook_documentation', :version => '2', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2.json" }, + { :controller => 'wiki', :action => 'show', :project_id => '1', + :id => 'CookBook_documentation', :version => '2', :format => 'json' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/index.xml" }, + { :controller => 'wiki', :action => 'index', :project_id => '567', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/wiki/index.json" }, + { :controller => 'wiki', :action => 'index', :project_id => '567', :format => 'json' } + ) + assert_routing( + { :method => 'put', :path => "/projects/567/wiki/my_page.xml" }, + { :controller => 'wiki', :action => 'update', :project_id => '567', + :id => 'my_page', :format => 'xml' } + ) + assert_routing( + { :method => 'put', :path => "/projects/567/wiki/my_page.json" }, + { :controller => 'wiki', :action => 'update', :project_id => '567', + :id => 'my_page', :format => 'json' } + ) + assert_routing( + { :method => 'delete', :path => "/projects/567/wiki/my_page.xml" }, + { :controller => 'wiki', :action => 'destroy', :project_id => '567', + :id => 'my_page', :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/projects/567/wiki/my_page.json" }, + { :controller => 'wiki', :action => 'destroy', :project_id => '567', + :id => 'my_page', :format => 'json' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1a/1a39e16716c41d7a0a0fcc3821bef50938cf1bfa.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1a39e16716c41d7a0a0fcc3821bef50938cf1bfa.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,22 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class GroupCustomField < CustomField + def type_name + :label_group_plural + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1a/1a9c163fd859224f6bc9ce5b56705d82e3f87035.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1a9c163fd859224f6bc9ce5b56705d82e3f87035.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,6 @@ +

    <%= link_to l(@enumeration.option_name), enumerations_path %> » <%=h @enumeration %>

    + +<%= labelled_form_for :enumeration, @enumeration, :url => enumeration_path(@enumeration), :html => {:method => :put} do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_save) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1a/1a9dc03dd504c9d145f93f49fdb8e42950ff1c74.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1a9dc03dd504c9d145f93f49fdb8e42950ff1c74.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,81 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingIssueRelationsTest < ActionController::IntegrationTest + def test_issue_relations + assert_routing( + { :method => 'get', :path => "/issues/1/relations" }, + { :controller => 'issue_relations', :action => 'index', + :issue_id => '1' } + ) + assert_routing( + { :method => 'get', :path => "/issues/1/relations.xml" }, + { :controller => 'issue_relations', :action => 'index', + :issue_id => '1', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/issues/1/relations.json" }, + { :controller => 'issue_relations', :action => 'index', + :issue_id => '1', :format => 'json' } + ) + assert_routing( + { :method => 'post', :path => "/issues/1/relations" }, + { :controller => 'issue_relations', :action => 'create', + :issue_id => '1' } + ) + assert_routing( + { :method => 'post', :path => "/issues/1/relations.xml" }, + { :controller => 'issue_relations', :action => 'create', + :issue_id => '1', :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/issues/1/relations.json" }, + { :controller => 'issue_relations', :action => 'create', + :issue_id => '1', :format => 'json' } + ) + assert_routing( + { :method => 'get', :path => "/relations/23" }, + { :controller => 'issue_relations', :action => 'show', :id => '23' } + ) + assert_routing( + { :method => 'get', :path => "/relations/23.xml" }, + { :controller => 'issue_relations', :action => 'show', :id => '23', + :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/relations/23.json" }, + { :controller => 'issue_relations', :action => 'show', :id => '23', + :format => 'json' } + ) + assert_routing( + { :method => 'delete', :path => "/relations/23" }, + { :controller => 'issue_relations', :action => 'destroy', :id => '23' } + ) + assert_routing( + { :method => 'delete', :path => "/relations/23.xml" }, + { :controller => 'issue_relations', :action => 'destroy', :id => '23', + :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/relations/23.json" }, + { :controller => 'issue_relations', :action => 'destroy', :id => '23', + :format => 'json' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1a/1abc7d9bb63cfe6a27d8a0852d2c50e8db93327c.svn-base --- a/.svn/pristine/1a/1abc7d9bb63cfe6a27d8a0852d2c50e8db93327c.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,502 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -require File.expand_path('../../test_helper', __FILE__) - -class MailerTest < ActiveSupport::TestCase - include Redmine::I18n - include ActionController::Assertions::SelectorAssertions - fixtures :projects, :enabled_modules, :issues, :users, :members, - :member_roles, :roles, :documents, :attachments, :news, - :tokens, :journals, :journal_details, :changesets, :trackers, - :issue_statuses, :enumerations, :messages, :boards, :repositories, - :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, - :versions, - :comments - - def setup - ActionMailer::Base.deliveries.clear - Setting.host_name = 'mydomain.foo' - Setting.protocol = 'http' - Setting.plain_text_mail = '0' - end - - def test_generated_links_in_emails - Setting.host_name = 'mydomain.foo' - Setting.protocol = 'https' - - journal = Journal.find(2) - assert Mailer.deliver_issue_edit(journal) - - mail = ActionMailer::Base.deliveries.last - assert_kind_of TMail::Mail, mail - - assert_select_email do - # link to the main ticket - assert_select "a[href=?]", - "https://mydomain.foo/issues/1#change-2", - :text => "Bug #1: Can't print recipes" - # link to a referenced ticket - assert_select "a[href=?][title=?]", - "https://mydomain.foo/issues/2", - "Add ingredients categories (Assigned)", - :text => "#2" - # link to a changeset - assert_select "a[href=?][title=?]", - "https://mydomain.foo/projects/ecookbook/repository/revisions/2", - "This commit fixes #1, #2 and references #1 & #3", - :text => "r2" - end - end - - def test_generated_links_with_prefix - relative_url_root = Redmine::Utils.relative_url_root - Setting.host_name = 'mydomain.foo/rdm' - Setting.protocol = 'http' - Redmine::Utils.relative_url_root = '/rdm' - - journal = Journal.find(2) - assert Mailer.deliver_issue_edit(journal) - - mail = ActionMailer::Base.deliveries.last - assert_kind_of TMail::Mail, mail - - assert_select_email do - # link to the main ticket - assert_select "a[href=?]", - "http://mydomain.foo/rdm/issues/1#change-2", - :text => "Bug #1: Can't print recipes" - # link to a referenced ticket - assert_select "a[href=?][title=?]", - "http://mydomain.foo/rdm/issues/2", - "Add ingredients categories (Assigned)", - :text => "#2" - # link to a changeset - assert_select "a[href=?][title=?]", - "http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2", - "This commit fixes #1, #2 and references #1 & #3", - :text => "r2" - end - ensure - # restore it - Redmine::Utils.relative_url_root = relative_url_root - end - - def test_generated_links_with_prefix_and_no_relative_url_root - relative_url_root = Redmine::Utils.relative_url_root - Setting.host_name = 'mydomain.foo/rdm' - Setting.protocol = 'http' - Redmine::Utils.relative_url_root = nil - - journal = Journal.find(2) - assert Mailer.deliver_issue_edit(journal) - - mail = ActionMailer::Base.deliveries.last - assert_kind_of TMail::Mail, mail - - assert_select_email do - # link to the main ticket - assert_select "a[href=?]", - "http://mydomain.foo/rdm/issues/1#change-2", - :text => "Bug #1: Can't print recipes" - # link to a referenced ticket - assert_select "a[href=?][title=?]", - "http://mydomain.foo/rdm/issues/2", - "Add ingredients categories (Assigned)", - :text => "#2" - # link to a changeset - assert_select "a[href=?][title=?]", - "http://mydomain.foo/rdm/projects/ecookbook/repository/revisions/2", - "This commit fixes #1, #2 and references #1 & #3", - :text => "r2" - end - ensure - # restore it - Redmine::Utils.relative_url_root = relative_url_root - end - - def test_email_headers - issue = Issue.find(1) - Mailer.deliver_issue_add(issue) - mail = ActionMailer::Base.deliveries.last - assert_not_nil mail - assert_equal 'OOF', mail.header_string('X-Auto-Response-Suppress') - assert_equal 'auto-generated', mail.header_string('Auto-Submitted') - end - - def test_plain_text_mail - Setting.plain_text_mail = 1 - journal = Journal.find(2) - Mailer.deliver_issue_edit(journal) - mail = ActionMailer::Base.deliveries.last - assert_equal "text/plain", mail.content_type - assert_equal 0, mail.parts.size - assert !mail.encoded.include?('href') - end - - def test_html_mail - Setting.plain_text_mail = 0 - journal = Journal.find(2) - Mailer.deliver_issue_edit(journal) - mail = ActionMailer::Base.deliveries.last - assert_equal 2, mail.parts.size - assert mail.encoded.include?('href') - end - - def test_from_header - with_settings :mail_from => 'redmine@example.net' do - Mailer.deliver_test(User.find(1)) - end - mail = ActionMailer::Base.deliveries.last - assert_not_nil mail - assert_equal 'redmine@example.net', mail.from_addrs.first.address - end - - def test_from_header_with_phrase - with_settings :mail_from => 'Redmine app ' do - Mailer.deliver_test(User.find(1)) - end - mail = ActionMailer::Base.deliveries.last - assert_not_nil mail - assert_equal 'redmine@example.net', mail.from_addrs.first.address - assert_equal 'Redmine app', mail.from_addrs.first.name - end - - def test_should_not_send_email_without_recipient - news = News.find(:first) - user = news.author - # Remove members except news author - news.project.memberships.each {|m| m.destroy unless m.user == user} - - user.pref[:no_self_notified] = false - user.pref.save - User.current = user - Mailer.deliver_news_added(news.reload) - assert_equal 1, last_email.bcc.size - - # nobody to notify - user.pref[:no_self_notified] = true - user.pref.save - User.current = user - ActionMailer::Base.deliveries.clear - Mailer.deliver_news_added(news.reload) - assert ActionMailer::Base.deliveries.empty? - end - - def test_issue_add_message_id - issue = Issue.find(1) - Mailer.deliver_issue_add(issue) - mail = ActionMailer::Base.deliveries.last - assert_not_nil mail - assert_equal Mailer.message_id_for(issue), mail.message_id - assert_nil mail.references - end - - def test_issue_edit_message_id - journal = Journal.find(1) - Mailer.deliver_issue_edit(journal) - mail = ActionMailer::Base.deliveries.last - assert_not_nil mail - assert_equal Mailer.message_id_for(journal), mail.message_id - assert_equal Mailer.message_id_for(journal.issue), mail.references.first.to_s - assert_select_email do - # link to the update - assert_select "a[href=?]", - "http://mydomain.foo/issues/#{journal.journalized_id}#change-#{journal.id}" - end - end - - def test_message_posted_message_id - message = Message.find(1) - Mailer.deliver_message_posted(message) - mail = ActionMailer::Base.deliveries.last - assert_not_nil mail - assert_equal Mailer.message_id_for(message), mail.message_id - assert_nil mail.references - assert_select_email do - # link to the message - assert_select "a[href=?]", - "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.id}", - :text => message.subject - end - end - - def test_reply_posted_message_id - message = Message.find(3) - Mailer.deliver_message_posted(message) - mail = ActionMailer::Base.deliveries.last - assert_not_nil mail - assert_equal Mailer.message_id_for(message), mail.message_id - assert_equal Mailer.message_id_for(message.parent), mail.references.first.to_s - assert_select_email do - # link to the reply - assert_select "a[href=?]", - "http://mydomain.foo/boards/#{message.board.id}/topics/#{message.root.id}?r=#{message.id}#message-#{message.id}", - :text => message.subject - end - end - - context("#issue_add") do - setup do - ActionMailer::Base.deliveries.clear - Setting.bcc_recipients = '1' - @issue = Issue.find(1) - end - - should "notify project members" do - assert Mailer.deliver_issue_add(@issue) - assert last_email.bcc.include?('dlopper@somenet.foo') - end - - should "not notify project members that are not allow to view the issue" do - Role.find(2).remove_permission!(:view_issues) - assert Mailer.deliver_issue_add(@issue) - assert !last_email.bcc.include?('dlopper@somenet.foo') - end - - should "notify issue watchers" do - user = User.find(9) - # minimal email notification options - user.pref[:no_self_notified] = '1' - user.pref.save - user.mail_notification = false - user.save - - Watcher.create!(:watchable => @issue, :user => user) - assert Mailer.deliver_issue_add(@issue) - assert last_email.bcc.include?(user.mail) - end - - should "not notify watchers not allowed to view the issue" do - user = User.find(9) - Watcher.create!(:watchable => @issue, :user => user) - Role.non_member.remove_permission!(:view_issues) - assert Mailer.deliver_issue_add(@issue) - assert !last_email.bcc.include?(user.mail) - end - end - - # test mailer methods for each language - def test_issue_add - issue = Issue.find(1) - valid_languages.each do |lang| - Setting.default_language = lang.to_s - assert Mailer.deliver_issue_add(issue) - end - end - - def test_issue_edit - journal = Journal.find(1) - valid_languages.each do |lang| - Setting.default_language = lang.to_s - assert Mailer.deliver_issue_edit(journal) - end - end - - def test_document_added - document = Document.find(1) - valid_languages.each do |lang| - Setting.default_language = lang.to_s - assert Mailer.deliver_document_added(document) - end - end - - def test_attachments_added - attachements = [ Attachment.find_by_container_type('Document') ] - valid_languages.each do |lang| - Setting.default_language = lang.to_s - assert Mailer.deliver_attachments_added(attachements) - end - end - - def test_version_file_added - attachements = [ Attachment.find_by_container_type('Version') ] - assert Mailer.deliver_attachments_added(attachements) - assert_not_nil last_email.bcc - assert last_email.bcc.any? - assert_select_email do - assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files" - end - end - - def test_project_file_added - attachements = [ Attachment.find_by_container_type('Project') ] - assert Mailer.deliver_attachments_added(attachements) - assert_not_nil last_email.bcc - assert last_email.bcc.any? - assert_select_email do - assert_select "a[href=?]", "http://mydomain.foo/projects/ecookbook/files" - end - end - - def test_news_added - news = News.find(:first) - valid_languages.each do |lang| - Setting.default_language = lang.to_s - assert Mailer.deliver_news_added(news) - end - end - - def test_news_comment_added - comment = Comment.find(2) - valid_languages.each do |lang| - Setting.default_language = lang.to_s - assert Mailer.deliver_news_comment_added(comment) - end - end - - def test_message_posted - message = Message.find(:first) - recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author} - recipients = recipients.compact.uniq - valid_languages.each do |lang| - Setting.default_language = lang.to_s - assert Mailer.deliver_message_posted(message) - end - end - - def test_wiki_content_added - content = WikiContent.find(:first) - valid_languages.each do |lang| - Setting.default_language = lang.to_s - assert_difference 'ActionMailer::Base.deliveries.size' do - assert Mailer.deliver_wiki_content_added(content) - end - end - end - - def test_wiki_content_updated - content = WikiContent.find(:first) - valid_languages.each do |lang| - Setting.default_language = lang.to_s - assert_difference 'ActionMailer::Base.deliveries.size' do - assert Mailer.deliver_wiki_content_updated(content) - end - end - end - - def test_account_information - user = User.find(2) - valid_languages.each do |lang| - user.update_attribute :language, lang.to_s - user.reload - assert Mailer.deliver_account_information(user, 'pAsswORd') - end - end - - def test_lost_password - token = Token.find(2) - valid_languages.each do |lang| - token.user.update_attribute :language, lang.to_s - token.reload - assert Mailer.deliver_lost_password(token) - end - end - - def test_register - token = Token.find(1) - Setting.host_name = 'redmine.foo' - Setting.protocol = 'https' - - valid_languages.each do |lang| - token.user.update_attribute :language, lang.to_s - token.reload - ActionMailer::Base.deliveries.clear - assert Mailer.deliver_register(token) - mail = ActionMailer::Base.deliveries.last - assert mail.body.include?("https://redmine.foo/account/activate?token=#{token.value}") - end - end - - def test_test - user = User.find(1) - valid_languages.each do |lang| - user.update_attribute :language, lang.to_s - assert Mailer.deliver_test(user) - end - end - - def test_reminders - Mailer.reminders(:days => 42) - assert_equal 1, ActionMailer::Base.deliveries.size - mail = ActionMailer::Base.deliveries.last - assert mail.bcc.include?('dlopper@somenet.foo') - assert mail.body.include?('Bug #3: Error 281 when updating a recipe') - assert_equal '1 issue(s) due in the next 42 days', mail.subject - end - - def test_reminders_for_users - Mailer.reminders(:days => 42, :users => ['5']) - assert_equal 0, ActionMailer::Base.deliveries.size # No mail for dlopper - Mailer.reminders(:days => 42, :users => ['3']) - assert_equal 1, ActionMailer::Base.deliveries.size # No mail for dlopper - mail = ActionMailer::Base.deliveries.last - assert mail.bcc.include?('dlopper@somenet.foo') - assert mail.body.include?('Bug #3: Error 281 when updating a recipe') - end - - def last_email - mail = ActionMailer::Base.deliveries.last - assert_not_nil mail - mail - end - - def test_mailer_should_not_change_locale - Setting.default_language = 'en' - # Set current language to italian - set_language_if_valid 'it' - # Send an email to a french user - user = User.find(1) - user.language = 'fr' - Mailer.deliver_account_activated(user) - mail = ActionMailer::Base.deliveries.last - assert mail.body.include?('Votre compte') - - assert_equal :it, current_language - end - - def test_with_deliveries_off - Mailer.with_deliveries false do - Mailer.deliver_test(User.find(1)) - end - assert ActionMailer::Base.deliveries.empty? - # should restore perform_deliveries - assert ActionMailer::Base.perform_deliveries - end - - def test_tmail_to_header_field_should_not_include_blank_lines - mail = TMail::Mail.new - mail.to = ["a.user@example.com", "v.user2@example.com", "e.smith@example.com", "info@example.com", "v.pupkin@example.com", - "b.user@example.com", "w.user2@example.com", "f.smith@example.com", "info2@example.com", "w.pupkin@example.com"] - - assert !mail.encoded.strip.split("\r\n").detect(&:blank?), "#{mail.encoded} malformed" - end - - context "layout" do - should "include the emails_header" do - with_settings(:emails_header => "*Header content*") do - assert Mailer.deliver_test(User.find(1)) - - assert_select_email do - assert_select ".header" do - assert_select "strong", :text => "Header content" - end - end - end - end - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1a/1ac615393481d79666a02bb2bca6e76628416d64.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1a/1ac615393481d79666a02bb2bca6e76628416d64.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,34 @@ +
    +<%= form_tag( + {:controller => 'repositories', :action => 'revision', :id => @project, + :repository_id => @repository.identifier_param}, + :method => :get + ) do %> + <%= l(:label_revision) %>: <%= text_field_tag 'rev', nil, :size => 8 %> + <%= submit_tag 'OK' %> +<% end %> +
    + +

    <%= l(:label_revision_plural) %>

    + +<%= render :partial => 'revisions', + :locals => {:project => @project, + :path => '', + :revisions => @changesets, + :entry => nil } %> + +

    <%= pagination_links_full @changeset_pages,@changeset_count %>

    + +<% content_for :header_tags do %> + <%= stylesheet_link_tag "scm" %> + <%= auto_discovery_link_tag( + :atom, + params.merge( + {:format => 'atom', :page => nil, :key => User.current.rss_key})) %> +<% end %> + +<% other_formats_links do |f| %> + <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %> +<% end %> + +<% html_title(l(:label_revision_plural)) -%> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1b/1b07c4eac87f7bab081e88a0df76e015b1e72a69.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1b/1b07c4eac87f7bab081e88a0df76e015b1e72a69.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,7 @@ +

    <%=l(:label_issue_category_new)%>

    + +<%= labelled_form_for @category, :as => :issue_category, + :url => project_issue_categories_path(@project) do |f| %> +<%= render :partial => 'issue_categories/form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1b/1b1f0da73986061f36bbd7cd1895fc7ac1182885.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1b/1b1f0da73986061f36bbd7cd1895fc7ac1182885.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,140 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class MessagesController < ApplicationController + menu_item :boards + default_search_scope :messages + before_filter :find_board, :only => [:new, :preview] + before_filter :find_message, :except => [:new, :preview] + before_filter :authorize, :except => [:preview, :edit, :destroy] + + helper :boards + helper :watchers + helper :attachments + include AttachmentsHelper + + REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE) + + # Show a topic and its replies + def show + page = params[:page] + # Find the page of the requested reply + if params[:r] && page.nil? + offset = @topic.children.count(:conditions => ["#{Message.table_name}.id < ?", params[:r].to_i]) + page = 1 + offset / REPLIES_PER_PAGE + end + + @reply_count = @topic.children.count + @reply_pages = Paginator.new self, @reply_count, REPLIES_PER_PAGE, page + @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}], + :order => "#{Message.table_name}.created_on ASC", + :limit => @reply_pages.items_per_page, + :offset => @reply_pages.current.offset) + + @reply = Message.new(:subject => "RE: #{@message.subject}") + render :action => "show", :layout => false if request.xhr? + end + + # Create a new topic + def new + @message = Message.new + @message.author = User.current + @message.board = @board + @message.safe_attributes = params[:message] + if request.post? + @message.save_attachments(params[:attachments]) + if @message.save + call_hook(:controller_messages_new_after_save, { :params => params, :message => @message}) + render_attachment_warning_if_needed(@message) + redirect_to board_message_path(@board, @message) + end + end + end + + # Reply to a topic + def reply + @reply = Message.new + @reply.author = User.current + @reply.board = @board + @reply.safe_attributes = params[:reply] + @topic.children << @reply + if !@reply.new_record? + call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply}) + attachments = Attachment.attach_files(@reply, params[:attachments]) + render_attachment_warning_if_needed(@reply) + end + redirect_to board_message_path(@board, @topic, :r => @reply) + end + + # Edit a message + def edit + (render_403; return false) unless @message.editable_by?(User.current) + @message.safe_attributes = params[:message] + if request.post? && @message.save + attachments = Attachment.attach_files(@message, params[:attachments]) + render_attachment_warning_if_needed(@message) + flash[:notice] = l(:notice_successful_update) + @message.reload + redirect_to board_message_path(@message.board, @message.root, :r => (@message.parent_id && @message.id)) + end + end + + # Delete a messages + def destroy + (render_403; return false) unless @message.destroyable_by?(User.current) + r = @message.to_param + @message.destroy + if @message.parent + redirect_to board_message_path(@board, @message.parent, :r => r) + else + redirect_to project_board_path(@project, @board) + end + end + + def quote + @subject = @message.subject + @subject = "RE: #{@subject}" unless @subject.starts_with?('RE:') + + @content = "#{ll(Setting.default_language, :text_user_wrote, @message.author)}\n> " + @content << @message.content.to_s.strip.gsub(%r{
    ((.|\s)*?)
    }m, '[...]').gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n" + end + + def preview + message = @board.messages.find_by_id(params[:id]) + @attachements = message.attachments if message + @text = (params[:message] || params[:reply])[:content] + @previewed = message + render :partial => 'common/preview' + end + +private + def find_message + return unless find_board + @message = @board.messages.find(params[:id], :include => :parent) + @topic = @message.root + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_board + @board = Board.find(params[:board_id], :include => :project) + @project = @board.project + rescue ActiveRecord::RecordNotFound + render_404 + nil + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1b/1b29dadeac443afcb6fd27a4a9c92482c01b0a1c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1b/1b29dadeac443afcb6fd27a4a9c92482c01b0a1c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1084 @@ +# Redmine EU language +# Author: Ales Zabala Alava (Shagi), +# 2010-01-25 +# Distributed under the same terms as the Redmine itself. +eu: + direction: ltr + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%Y/%m/%d" + short: "%b %d" + long: "%Y %B %d" + + day_names: [Igandea, Astelehena, Asteartea, Asteazkena, Osteguna, Ostirala, Larunbata] + abbr_day_names: [Ig., Al., Ar., Az., Og., Or., La.] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, Urtarrila, Otsaila, Martxoa, Apirila, Maiatza, Ekaina, Uztaila, Abuztua, Iraila, Urria, Azaroa, Abendua] + abbr_month_names: [~, Urt, Ots, Mar, Api, Mai, Eka, Uzt, Abu, Ira, Urr, Aza, Abe] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%Y/%m/%d %H:%M" + time: "%H:%M" + short: "%b %d %H:%M" + long: "%Y %B %d %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "minutu erdi" + less_than_x_seconds: + one: "segundu bat baino gutxiago" + other: "%{count} segundu baino gutxiago" + x_seconds: + one: "segundu 1" + other: "%{count} segundu" + less_than_x_minutes: + one: "minutu bat baino gutxiago" + other: "%{count} minutu baino gutxiago" + x_minutes: + one: "minutu 1" + other: "%{count} minutu" + about_x_hours: + one: "ordu 1 inguru" + other: "%{count} ordu inguru" + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "egun 1" + other: "%{count} egun" + about_x_months: + one: "hilabete 1 inguru" + other: "%{count} hilabete inguru" + x_months: + one: "hilabete 1" + other: "%{count} hilabete" + about_x_years: + one: "urte 1 inguru" + other: "%{count} urte inguru" + over_x_years: + one: "urte 1 baino gehiago" + other: "%{count} urte baino gehiago" + almost_x_years: + one: "ia urte 1" + other: "ia %{count} urte" + + number: + format: + separator: "." + delimiter: "" + precision: 3 + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Byte" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + +# Used in array.to_sentence. + support: + array: + sentence_connector: "eta" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "Errore batek %{model} hau godetzea galarazi du." + other: "%{count} errorek %{model} hau gordetzea galarazi dute." + messages: + inclusion: "ez dago zerrendan" + exclusion: "erreserbatuta dago" + invalid: "baliogabea da" + confirmation: "ez du berrespenarekin bat egiten" + accepted: "onartu behar da" + empty: "ezin da hutsik egon" + blank: "ezin da hutsik egon" + too_long: "luzeegia da (maximoa %{count} karaktere dira)" + too_short: "laburregia da (minimoa %{count} karaktere dira)" + wrong_length: "luzera ezegokia da (%{count} karakter izan beharko litzake)" + taken: "dagoeneko hartuta dago" + not_a_number: "ez da zenbaki bat" + not_a_date: "ez da baliozko data" + greater_than: "%{count} baino handiagoa izan behar du" + greater_than_or_equal_to: "%{count} edo handiagoa izan behar du" + equal_to: "%{count} izan behar du" + less_than: "%{count} baino gutxiago izan behar du" + less_than_or_equal_to: "%{count} edo gutxiago izan behar du" + odd: "bakoitia izan behar du" + even: "bikoitia izan behar du" + greater_than_start_date: "hasiera data baino handiagoa izan behar du" + not_same_project: "ez dago proiektu berdinean" + circular_dependency: "Erlazio honek mendekotasun zirkular bat sortuko luke" + cant_link_an_issue_with_a_descendant: "Zeregin bat ezin da bere azpiataza batekin estekatu." + + actionview_instancetag_blank_option: Hautatu mesedez + + general_text_No: 'Ez' + general_text_Yes: 'Bai' + general_text_no: 'ez' + general_text_yes: 'bai' + general_lang_name: 'Euskara' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: Kontua ongi eguneratu da. + notice_account_invalid_creditentials: Erabiltzaile edo pasahitz ezegokia + notice_account_password_updated: Pasahitza ongi eguneratu da. + notice_account_wrong_password: Pasahitz ezegokia. + notice_account_register_done: Kontua ongi sortu da. Kontua gaitzeko klikatu epostan adierazi zaizun estekan. + notice_account_unknown_email: Erabiltzaile ezezaguna. + notice_can_t_change_password: Kontu honek kanpoko autentikazio bat erabiltzen du. Ezinezkoa da pasahitza aldatzea. + notice_account_lost_email_sent: Pasahitz berria aukeratzeko jarraibideak dituen eposta bat bidali zaizu. + notice_account_activated: Zure kontua gaituta dago. Orain saioa has dezakezu + notice_successful_create: Sortze arrakastatsua. + notice_successful_update: Eguneratze arrakastatsua. + notice_successful_delete: Ezabaketa arrakastatsua. + notice_successful_connection: Konexio arrakastatsua. + notice_file_not_found: Atzitu nahi duzun orria ez da exisitzen edo ezabatua izan da. + notice_locking_conflict: Beste erabiltzaile batek datuak eguneratu ditu. + notice_not_authorized: Ez duzu orri hau atzitzeko baimenik. + notice_email_sent: "%{value} helbidera eposta bat bidali da" + notice_email_error: "Errorea eposta bidaltzean (%{value})" + notice_feeds_access_key_reseted: Zure RSS atzipen giltza berrezarri da. + notice_api_access_key_reseted: Zure API atzipen giltza berrezarri da. + notice_failed_to_save_issues: "Hautatutako %{total} zereginetatik %{count} ezin izan dira konpondu: %{ids}." + notice_no_issue_selected: "Ez da zereginik hautatu! Mesedez, editatu nahi dituzun arazoak markatu." + notice_account_pending: "Zure kontua sortu da, orain kudeatzailearen onarpenaren zain dago." + notice_default_data_loaded: Lehenetsitako konfigurazioa ongi kargatu da. + notice_unable_delete_version: Ezin da bertsioa ezabatu. + notice_issue_done_ratios_updated: Burututako zereginen erlazioa eguneratu da. + + error_can_t_load_default_data: "Ezin izan da lehenetsitako konfigurazioa kargatu: %{value}" + error_scm_not_found: "Sarrera edo berrikuspena ez da biltegian topatu." + error_scm_command_failed: "Errorea gertatu da biltegia atzitzean: %{value}" + error_scm_annotate: "Sarrera ez da existitzen edo ezin da anotatu." + error_issue_not_found_in_project: 'Zeregina ez da topatu edo ez da proiektu honetakoa' + error_no_tracker_in_project: 'Proiektu honek ez du aztarnaririk esleituta. Mesedez egiaztatu Proiektuaren ezarpenak.' + error_no_default_issue_status: 'Zereginek ez dute lehenetsitako egoerarik. Mesedez egiaztatu zure konfigurazioa ("Kudeaketa -> Arazoen egoerak" atalera joan).' + error_can_not_reopen_issue_on_closed_version: 'Itxitako bertsio batera esleitutako zereginak ezin dira berrireki' + error_can_not_archive_project: Proiektu hau ezin da artxibatu + error_issue_done_ratios_not_updated: "Burututako zereginen erlazioa ez da eguneratu." + error_workflow_copy_source: 'Mesedez hautatu iturburuko aztarnari edo rola' + error_workflow_copy_target: 'Mesedez hautatu helburuko aztarnari(ak) edo rola(k)' + + warning_attachments_not_saved: "%{count} fitxategi ezin izan d(ir)a gorde." + + mail_subject_lost_password: "Zure %{value} pasahitza" + mail_body_lost_password: 'Zure pasahitza aldatzeko hurrengo estekan klikatu:' + mail_subject_register: "Zure %{value} kontuaren gaitzea" + mail_body_register: 'Zure kontua gaitzeko hurrengo estekan klikatu:' + mail_body_account_information_external: "Zure %{value} kontua erabil dezakezu saioa hasteko." + mail_body_account_information: Zure kontuaren informazioa + mail_subject_account_activation_request: "%{value} kontu gaitzeko eskaera" + mail_body_account_activation_request: "Erabiltzaile berri bat (%{value}) erregistratu da. Kontua zure onarpenaren zain dago:" + mail_subject_reminder: "%{count} arazo hurrengo %{days} egunetan amaitzen d(ir)a" + mail_body_reminder: "Zuri esleituta dauden %{count} arazo hurrengo %{days} egunetan amaitzen d(ir)a:" + mail_subject_wiki_content_added: "'%{id}' wiki orria gehitu da" + mail_body_wiki_content_added: "%{author}-(e)k '%{id}' wiki orria gehitu du." + mail_subject_wiki_content_updated: "'%{id}' wiki orria eguneratu da" + mail_body_wiki_content_updated: "%{author}-(e)k '%{id}' wiki orria eguneratu du." + + gui_validation_error: akats 1 + gui_validation_error_plural: "%{count} akats" + + field_name: Izena + field_description: Deskribapena + field_summary: Laburpena + field_is_required: Beharrezkoa + field_firstname: Izena + field_lastname: Abizenak + field_mail: Eposta + field_filename: Fitxategia + field_filesize: Tamaina + field_downloads: Deskargak + field_author: Egilea + field_created_on: Sortuta + field_updated_on: Eguneratuta + field_field_format: Formatua + field_is_for_all: Proiektu guztietarako + field_possible_values: Balio posibleak + field_regexp: Expresio erregularra + field_min_length: Luzera minimoa + field_max_length: Luzera maxioma + field_value: Balioa + field_category: Kategoria + field_title: Izenburua + field_project: Proiektua + field_issue: Zeregina + field_status: Egoera + field_notes: Oharrak + field_is_closed: Itxitako arazoa + field_is_default: Lehenetsitako balioa + field_tracker: Aztarnaria + field_subject: Gaia + field_due_date: Amaiera data + field_assigned_to: Esleituta + field_priority: Lehentasuna + field_fixed_version: Helburuko bertsioa + field_user: Erabiltzilea + field_role: Rola + field_homepage: Orri nagusia + field_is_public: Publikoa + field_parent: "Honen azpiproiektua:" + field_is_in_roadmap: Arazoak ibilbide-mapan erakutsi + field_login: Erabiltzaile izena + field_mail_notification: Eposta jakinarazpenak + field_admin: Kudeatzailea + field_last_login_on: Azken konexioa + field_language: Hizkuntza + field_effective_date: Data + field_password: Pasahitza + field_new_password: Pasahitz berria + field_password_confirmation: Berrespena + field_version: Bertsioa + field_type: Mota + field_host: Ostalaria + field_port: Portua + field_account: Kontua + field_base_dn: Base DN + field_attr_login: Erabiltzaile atributua + field_attr_firstname: Izena atributua + field_attr_lastname: Abizenak atributua + field_attr_mail: Eposta atributua + field_onthefly: Zuzeneko erabiltzaile sorrera + field_start_date: Hasiera + field_done_ratio: Egindako % + field_auth_source: Autentikazio modua + field_hide_mail: Nire eposta helbidea ezkutatu + field_comments: Iruzkina + field_url: URL + field_start_page: Hasierako orria + field_subproject: Azpiproiektua + field_hours: Ordu + field_activity: Jarduera + field_spent_on: Data + field_identifier: Identifikatzailea + field_is_filter: Iragazki moduan erabilita + field_issue_to: Erlazionatutako zereginak + field_delay: Atzerapena + field_assignable: Arazoak rol honetara esleitu daitezke + field_redirect_existing_links: Existitzen diren estekak berbideratu + field_estimated_hours: Estimatutako denbora + field_column_names: Zutabeak + field_time_zone: Ordu zonaldea + field_searchable: Bilagarria + field_default_value: Lehenetsitako balioa + field_comments_sorting: Iruzkinak erakutsi + field_parent_title: Orri gurasoa + field_editable: Editagarria + field_watcher: Behatzailea + field_identity_url: OpenID URLa + field_content: Edukia + field_group_by: Emaitzak honegatik taldekatu + field_sharing: Partekatzea + + setting_app_title: Aplikazioaren izenburua + setting_app_subtitle: Aplikazioaren azpizenburua + setting_welcome_text: Ongietorriko testua + setting_default_language: Lehenetsitako hizkuntza + setting_login_required: Autentikazioa derrigorrezkoa + setting_self_registration: Norberak erregistratu + setting_attachment_max_size: Eranskinen tamaina max. + setting_issues_export_limit: Zereginen esportatze limitea + setting_mail_from: Igorlearen eposta helbidea + setting_bcc_recipients: Hartzaileak ezkutuko kopian (bcc) + setting_plain_text_mail: Testu soileko epostak (HTML-rik ez) + setting_host_name: Ostalari izena eta bidea + setting_text_formatting: Testu formatua + setting_wiki_compression: Wikiaren historia konprimitu + setting_feeds_limit: Jarioaren edukiera limitea + setting_default_projects_public: Proiektu berriak defektuz publikoak dira + setting_autofetch_changesets: Commit-ak automatikoki hartu + setting_sys_api_enabled: Biltegien kudeaketarako WS gaitu + setting_commit_ref_keywords: Erreferentzien gako-hitzak + setting_commit_fix_keywords: Konpontze gako-hitzak + setting_autologin: Saioa automatikoki hasi + setting_date_format: Data formatua + setting_time_format: Ordu formatua + setting_cross_project_issue_relations: Zereginak proiektuen artean erlazionatzea baimendu + setting_issue_list_default_columns: Zereginen zerrendan defektuz ikusten diren zutabeak + setting_emails_footer: Eposten oina + setting_protocol: Protokoloa + setting_per_page_options: Orriko objektuen aukerak + setting_user_format: Erabiltzaileak erakusteko formatua + setting_activity_days_default: Proiektuen jardueran erakusteko egunak + setting_display_subprojects_issues: Azpiproiektuen zereginak proiektu nagusian erakutsi defektuz + setting_enabled_scm: Gaitutako IKKak + setting_mail_handler_body_delimiters: "Lerro hauteko baten ondoren epostak moztu" + setting_mail_handler_api_enabled: Sarrerako epostentzako WS gaitu + setting_mail_handler_api_key: API giltza + setting_sequential_project_identifiers: Proiektuen identifikadore sekuentzialak sortu + setting_gravatar_enabled: Erabili Gravatar erabiltzaile ikonoak + setting_gravatar_default: Lehenetsitako Gravatar irudia + setting_diff_max_lines_displayed: Erakutsiko diren diff lerro kopuru maximoa + setting_file_max_size_displayed: Barnean erakuzten diren testu fitxategien tamaina maximoa + setting_repository_log_display_limit: Egunkari fitxategian erakutsiko diren berrikuspen kopuru maximoa. + setting_openid: Baimendu OpenID saio hasiera eta erregistatzea + setting_password_min_length: Pasahitzen luzera minimoa + setting_new_project_user_role_id: Proiektu berriak sortzerakoan kudeatzaile ez diren erabiltzaileei esleitutako rola + setting_default_projects_modules: Proiektu berrientzako defektuz gaitutako moduluak + setting_issue_done_ratio: "Zereginen burututako tasa kalkulatzean erabili:" + setting_issue_done_ratio_issue_field: Zeregin eremua erabili + setting_issue_done_ratio_issue_status: Zeregin egoera erabili + setting_start_of_week: "Egutegiak noiz hasi:" + setting_rest_api_enabled: Gaitu REST web zerbitzua + + permission_add_project: Proiektua sortu + permission_add_subprojects: Azpiproiektuak sortu + permission_edit_project: Proiektua editatu + permission_select_project_modules: Proiektuaren moduluak hautatu + permission_manage_members: Kideak kudeatu + permission_manage_versions: Bertsioak kudeatu + permission_manage_categories: Arazoen kategoriak kudeatu + permission_view_issues: Zereginak ikusi + permission_add_issues: Zereginak gehitu + permission_edit_issues: Zereginak aldatu + permission_manage_issue_relations: Zereginen erlazioak kudeatu + permission_add_issue_notes: Oharrak gehitu + permission_edit_issue_notes: Oharrak aldatu + permission_edit_own_issue_notes: Nork bere oharrak aldatu + permission_move_issues: Zereginak mugitu + permission_delete_issues: Zereginak ezabatu + permission_manage_public_queries: Galdera publikoak kudeatu + permission_save_queries: Galderak gorde + permission_view_gantt: Gantt grafikoa ikusi + permission_view_calendar: Egutegia ikusi + permission_view_issue_watchers: Behatzaileen zerrenda ikusi + permission_add_issue_watchers: Behatzaileak gehitu + permission_delete_issue_watchers: Behatzaileak ezabatu + permission_log_time: Igarotako denbora erregistratu + permission_view_time_entries: Igarotako denbora ikusi + permission_edit_time_entries: Denbora egunkariak editatu + permission_edit_own_time_entries: Nork bere denbora egunkariak editatu + permission_manage_news: Berriak kudeatu + permission_comment_news: Berrien iruzkinak egin + permission_manage_documents: Dokumentuak kudeatu + permission_view_documents: Dokumentuak ikusi + permission_manage_files: Fitxategiak kudeatu + permission_view_files: Fitxategiak ikusi + permission_manage_wiki: Wikia kudeatu + permission_rename_wiki_pages: Wiki orriak berrizendatu + permission_delete_wiki_pages: Wiki orriak ezabatu + permission_view_wiki_pages: Wikia ikusi + permission_view_wiki_edits: Wikiaren historia ikusi + permission_edit_wiki_pages: Wiki orriak editatu + permission_delete_wiki_pages_attachments: Eranskinak ezabatu + permission_protect_wiki_pages: Wiki orriak babestu + permission_manage_repository: Biltegiak kudeatu + permission_browse_repository: Biltegia arakatu + permission_view_changesets: Aldaketak ikusi + permission_commit_access: Commit atzipena + permission_manage_boards: Foroak kudeatu + permission_view_messages: Mezuak ikusi + permission_add_messages: Mezuak bidali + permission_edit_messages: Mezuak aldatu + permission_edit_own_messages: Nork bere mezuak aldatu + permission_delete_messages: Mezuak ezabatu + permission_delete_own_messages: Nork bere mezuak ezabatu + + project_module_issue_tracking: Zereginen jarraipena + project_module_time_tracking: Denbora jarraipena + project_module_news: Berriak + project_module_documents: Dokumentuak + project_module_files: Fitxategiak + project_module_wiki: Wiki + project_module_repository: Biltegia + project_module_boards: Foroak + + label_user: Erabiltzailea + label_user_plural: Erabiltzaileak + label_user_new: Erabiltzaile berria + label_user_anonymous: Ezezaguna + label_project: Proiektua + label_project_new: Proiektu berria + label_project_plural: Proiektuak + label_x_projects: + zero: proiekturik ez + one: proiektu bat + other: "%{count} proiektu" + label_project_all: Proiektu guztiak + label_project_latest: Azken proiektuak + label_issue: Zeregina + label_issue_new: Zeregin berria + label_issue_plural: Zereginak + label_issue_view_all: Zeregin guztiak ikusi + label_issues_by: "Zereginak honengatik: %{value}" + label_issue_added: Zeregina gehituta + label_issue_updated: Zeregina eguneratuta + label_document: Dokumentua + label_document_new: Dokumentu berria + label_document_plural: Dokumentuak + label_document_added: Dokumentua gehituta + label_role: Rola + label_role_plural: Rolak + label_role_new: Rol berria + label_role_and_permissions: Rolak eta baimenak + label_member: Kidea + label_member_new: Kide berria + label_member_plural: Kideak + label_tracker: Aztarnaria + label_tracker_plural: Aztarnariak + label_tracker_new: Aztarnari berria + label_workflow: Lan-fluxua + label_issue_status: Zeregin egoera + label_issue_status_plural: Zeregin egoerak + label_issue_status_new: Egoera berria + label_issue_category: Zeregin kategoria + label_issue_category_plural: Zeregin kategoriak + label_issue_category_new: Kategoria berria + label_custom_field: Eremu pertsonalizatua + label_custom_field_plural: Eremu pertsonalizatuak + label_custom_field_new: Eremu pertsonalizatu berria + label_enumerations: Enumerazioak + label_enumeration_new: Balio berria + label_information: Informazioa + label_information_plural: Informazioa + label_please_login: Saioa hasi mesedez + label_register: Erregistratu + label_login_with_open_id_option: edo OpenID-rekin saioa hasi + label_password_lost: Pasahitza galduta + label_home: Hasiera + label_my_page: Nire orria + label_my_account: Nire kontua + label_my_projects: Nire proiektuak + label_administration: Kudeaketa + label_login: Saioa hasi + label_logout: Saioa bukatu + label_help: Laguntza + label_reported_issues: Berri emandako zereginak + label_assigned_to_me_issues: Niri esleitutako arazoak + label_last_login: Azken konexioa + label_registered_on: Noiz erregistratuta + label_activity: Jarduerak + label_overall_activity: Jarduera guztiak + label_user_activity: "%{value}-(r)en jarduerak" + label_new: Berria + label_logged_as: "Sartutako erabiltzailea:" + label_environment: Ingurune + label_authentication: Autentikazioa + label_auth_source: Autentikazio modua + label_auth_source_new: Autentikazio modu berria + label_auth_source_plural: Autentikazio moduak + label_subproject_plural: Azpiproiektuak + label_subproject_new: Azpiproiektu berria + label_and_its_subprojects: "%{value} eta bere azpiproiektuak" + label_min_max_length: Luzera min - max + label_list: Zerrenda + label_date: Data + label_integer: Osokoa + label_float: Koma higikorrekoa + label_boolean: Boolearra + label_string: Testua + label_text: Testu luzea + label_attribute: Atributua + label_attribute_plural: Atributuak + label_download: "Deskarga %{count}" + label_download_plural: "%{count} Deskarga" + label_no_data: Ez dago erakusteko daturik + label_change_status: Egoera aldatu + label_history: Historikoa + label_attachment: Fitxategia + label_attachment_new: Fitxategi berria + label_attachment_delete: Fitxategia ezabatu + label_attachment_plural: Fitxategiak + label_file_added: Fitxategia gehituta + label_report: Berri ematea + label_report_plural: Berri emateak + label_news: Berria + label_news_new: Berria gehitu + label_news_plural: Berriak + label_news_latest: Azken berriak + label_news_view_all: Berri guztiak ikusi + label_news_added: Berria gehituta + label_settings: Ezarpenak + label_overview: Gainbegirada + label_version: Bertsioa + label_version_new: Bertsio berria + label_version_plural: Bertsioak + label_close_versions: Burututako bertsioak itxi + label_confirmation: Baieztapena + label_export_to: 'Eskuragarri baita:' + label_read: Irakurri... + label_public_projects: Proiektu publikoak + label_open_issues: irekita + label_open_issues_plural: irekiak + label_closed_issues: itxita + label_closed_issues_plural: itxiak + label_x_open_issues_abbr_on_total: + zero: 0 irekita / %{total} + one: 1 irekita / %{total} + other: "%{count} irekiak / %{total}" + label_x_open_issues_abbr: + zero: 0 irekita + one: 1 irekita + other: "%{count} irekiak" + label_x_closed_issues_abbr: + zero: 0 itxita + one: 1 itxita + other: "%{count} itxiak" + label_total: Guztira + label_permissions: Baimenak + label_current_status: Uneko egoera + label_new_statuses_allowed: Baimendutako egoera berriak + label_all: guztiak + label_none: ezer + label_nobody: inor + label_next: Hurrengoa + label_previous: Aurrekoak + label_used_by: Erabilita + label_details: Xehetasunak + label_add_note: Oharra gehitu + label_per_page: Orriko + label_calendar: Egutegia + label_months_from: hilabete noiztik + label_gantt: Gantt + label_internal: Barnekoa + label_last_changes: "azken %{count} aldaketak" + label_change_view_all: Aldaketa guztiak ikusi + label_personalize_page: Orri hau pertsonalizatu + label_comment: Iruzkin + label_comment_plural: Iruzkinak + label_x_comments: + zero: iruzkinik ez + one: iruzkin 1 + other: "%{count} iruzkin" + label_comment_add: Iruzkina gehitu + label_comment_added: Iruzkina gehituta + label_comment_delete: Iruzkinak ezabatu + label_query: Galdera pertsonalizatua + label_query_plural: Pertsonalizatutako galderak + label_query_new: Galdera berria + label_filter_add: Iragazkia gehitu + label_filter_plural: Iragazkiak + label_equals: da + label_not_equals: ez da + label_in_less_than: baino gutxiagotan + label_in_more_than: baino gehiagotan + label_greater_or_equal: '>=' + label_less_or_equal: '<=' + label_in: hauetan + label_today: gaur + label_all_time: denbora guztia + label_yesterday: atzo + label_this_week: aste honetan + label_last_week: pasadan astean + label_last_n_days: "azken %{count} egunetan" + label_this_month: hilabete hau + label_last_month: pasadan hilabetea + label_this_year: urte hau + label_date_range: Data tartea + label_less_than_ago: egun hauek baino gutxiago + label_more_than_ago: egun hauek baino gehiago + label_ago: orain dela + label_contains: dauka + label_not_contains: ez dauka + label_day_plural: egun + label_repository: Biltegia + label_repository_plural: Biltegiak + label_browse: Arakatu + label_modification: "aldaketa %{count}" + label_modification_plural: "%{count} aldaketa" + label_branch: Adarra + label_tag: Etiketa + label_revision: Berrikuspena + label_revision_plural: Berrikuspenak + label_revision_id: "%{value} berrikuspen" + label_associated_revisions: Elkartutako berrikuspenak + label_added: gehituta + label_modified: aldatuta + label_copied: kopiatuta + label_renamed: berrizendatuta + label_deleted: ezabatuta + label_latest_revision: Azken berrikuspena + label_latest_revision_plural: Azken berrikuspenak + label_view_revisions: Berrikuspenak ikusi + label_view_all_revisions: Berrikuspen guztiak ikusi + label_max_size: Tamaina maximoa + label_sort_highest: Goraino mugitu + label_sort_higher: Gora mugitu + label_sort_lower: Behera mugitu + label_sort_lowest: Beheraino mugitu + label_roadmap: Ibilbide-mapa + label_roadmap_due_in: "Epea: %{value}" + label_roadmap_overdue: "%{value} berandu" + label_roadmap_no_issues: Ez dago zereginik bertsio honetan + label_search: Bilatu + label_result_plural: Emaitzak + label_all_words: hitz guztiak + label_wiki: Wikia + label_wiki_edit: Wiki edizioa + label_wiki_edit_plural: Wiki edizioak + label_wiki_page: Wiki orria + label_wiki_page_plural: Wiki orriak + label_index_by_title: Izenburuaren araberako indizea + label_index_by_date: Dataren araberako indizea + label_current_version: Uneko bertsioa + label_preview: Aurreikusi + label_feed_plural: Jarioak + label_changes_details: Aldaketa guztien xehetasunak + label_issue_tracking: Zeregin jarraipena + label_spent_time: Igarotako denbora + label_f_hour: "ordu %{value}" + label_f_hour_plural: "%{value} ordu" + label_time_tracking: Denbora jarraipena + label_change_plural: Aldaketak + label_statistics: Estatistikak + label_commits_per_month: Commit-ak hilabeteka + label_commits_per_author: Commit-ak egileka + label_view_diff: Ezberdintasunak ikusi + label_diff_inline: barnean + label_diff_side_by_side: aldez alde + label_options: Aukerak + label_copy_workflow_from: Kopiatu workflow-a hemendik + label_permissions_report: Baimenen txostena + label_watched_issues: Behatutako zereginak + label_related_issues: Erlazionatutako zereginak + label_applied_status: Aplikatutako egoera + label_loading: Kargatzen... + label_relation_new: Erlazio berria + label_relation_delete: Erlazioa ezabatu + label_relates_to: erlazionatuta dago + label_duplicates: bikoizten du + label_duplicated_by: honek bikoiztuta + label_blocks: blokeatzen du + label_blocked_by: honek blokeatuta + label_precedes: aurretik doa + label_follows: jarraitzen du + label_end_to_start: bukaeratik hasierara + label_end_to_end: bukaeratik bukaerara + label_start_to_start: hasieratik hasierhasieratik bukaerara + label_start_to_end: hasieratik bukaerara + label_stay_logged_in: Saioa mantendu + label_disabled: ezgaituta + label_show_completed_versions: Bukatutako bertsioak ikusi + label_me: ni + label_board: Foroa + label_board_new: Foro berria + label_board_plural: Foroak + label_topic_plural: Gaiak + label_message_plural: Mezuak + label_message_last: Azken mezua + label_message_new: Mezu berria + label_message_posted: Mezua gehituta + label_reply_plural: Erantzunak + label_send_information: Erabiltzaileai kontuaren informazioa bidali + label_year: Urtea + label_month: Hilabetea + label_week: Astea + label_date_from: Nork + label_date_to: Nori + label_language_based: Erabiltzailearen hizkuntzaren arabera + label_sort_by: "Ordenazioa: %{value}" + label_send_test_email: Frogako mezua bidali + label_feeds_access_key: RSS atzipen giltza + label_missing_feeds_access_key: RSS atzipen giltza falta da + label_feeds_access_key_created_on: "RSS atzipen giltza orain dela %{value} sortuta" + label_module_plural: Moduluak + label_added_time_by: "%{author}, orain dela %{age} gehituta" + label_updated_time_by: "%{author}, orain dela %{age} eguneratuta" + label_updated_time: "Orain dela %{value} eguneratuta" + label_jump_to_a_project: Joan proiektura... + label_file_plural: Fitxategiak + label_changeset_plural: Aldaketak + label_default_columns: Lehenetsitako zutabeak + label_no_change_option: (Aldaketarik ez) + label_bulk_edit_selected_issues: Hautatutako zereginak batera editatu + label_theme: Itxura + label_default: Lehenetsia + label_search_titles_only: Izenburuetan bakarrik bilatu + label_user_mail_option_all: "Nire proiektu guztietako gertakari guztientzat" + label_user_mail_option_selected: "Hautatutako proiektuetako edozein gertakarientzat..." + label_user_mail_no_self_notified: "Ez dut nik egiten ditudan aldeketen jakinarazpenik jaso nahi" + label_registration_activation_by_email: kontuak epostaz gaitu + label_registration_manual_activation: kontuak eskuz gaitu + label_registration_automatic_activation: kontuak automatikoki gaitu + label_display_per_page: "Orriko: %{value}" + label_age: Adina + label_change_properties: Propietateak aldatu + label_general: Orokorra + label_more: Gehiago + label_scm: IKK + label_plugins: Pluginak + label_ldap_authentication: LDAP autentikazioa + label_downloads_abbr: Desk. + label_optional_description: Aukerako deskribapena + label_add_another_file: Beste fitxategia gehitu + label_preferences: Hobespenak + label_chronological_order: Orden kronologikoan + label_reverse_chronological_order: Alderantzizko orden kronologikoan + label_planning: Planifikazioa + label_incoming_emails: Sarrerako epostak + label_generate_key: Giltza sortu + label_issue_watchers: Behatzaileak + label_example: Adibidea + label_display: Bistaratzea + label_sort: Ordenatu + label_ascending: Gorantz + label_descending: Beherantz + label_date_from_to: "%{start}-tik %{end}-ra" + label_wiki_content_added: Wiki orria gehituta + label_wiki_content_updated: Wiki orria eguneratuta + label_group: Taldea + label_group_plural: Taldeak + label_group_new: Talde berria + label_time_entry_plural: Igarotako denbora + label_version_sharing_none: Ez partekatuta + label_version_sharing_descendants: Azpiproiektuekin + label_version_sharing_hierarchy: Proiektu Hierarkiarekin + label_version_sharing_tree: Proiektu zuhaitzarekin + label_version_sharing_system: Proiektu guztiekin + label_update_issue_done_ratios: Zereginen burututako erlazioa eguneratu + label_copy_source: Iturburua + label_copy_target: Helburua + label_copy_same_as_target: Helburuaren berdina + label_display_used_statuses_only: Aztarnari honetan erabiltzen diren egoerak bakarrik erakutsi + label_api_access_key: API atzipen giltza + label_missing_api_access_key: API atzipen giltza falta da + label_api_access_key_created_on: "API atzipen giltza sortuta orain dela %{value}" + + button_login: Saioa hasi + button_submit: Bidali + button_save: Gorde + button_check_all: Guztiak markatu + button_uncheck_all: Guztiak desmarkatu + button_delete: Ezabatu + button_create: Sortu + button_create_and_continue: Sortu eta jarraitu + button_test: Frogatu + button_edit: Editatu + button_add: Gehitu + button_change: Aldatu + button_apply: Aplikatu + button_clear: Garbitu + button_lock: Blokeatu + button_unlock: Desblokeatu + button_download: Deskargatu + button_list: Zerrenda + button_view: Ikusi + button_move: Mugitu + button_move_and_follow: Mugitu eta jarraitu + button_back: Atzera + button_cancel: Ezeztatu + button_activate: Gahitu + button_sort: Ordenatu + button_log_time: Denbora erregistratu + button_rollback: Itzuli bertsio honetara + button_watch: Behatu + button_unwatch: Behatzen utzi + button_reply: Erantzun + button_archive: Artxibatu + button_unarchive: Desartxibatu + button_reset: Berrezarri + button_rename: Berrizendatu + button_change_password: Pasahitza aldatu + button_copy: Kopiatu + button_copy_and_follow: Kopiatu eta jarraitu + button_annotate: Anotatu + button_update: Eguneratu + button_configure: Konfiguratu + button_quote: Aipatu + button_duplicate: Bikoiztu + button_show: Ikusi + + status_active: gaituta + status_registered: izena emanda + status_locked: blokeatuta + + version_status_open: irekita + version_status_locked: blokeatuta + version_status_closed: itxita + + field_active: Gaituta + + text_select_mail_notifications: Jakinarazpenak zein ekintzetarako bidaliko diren hautatu. + text_regexp_info: adib. ^[A-Z0-9]+$ + text_min_max_length_info: 0k mugarik gabe esan nahi du + text_project_destroy_confirmation: Ziur zaude proiektu hau eta erlazionatutako datu guztiak ezabatu nahi dituzula? + text_subprojects_destroy_warning: "%{value} azpiproiektuak ere ezabatuko dira." + text_workflow_edit: Hautatu rola eta aztarnaria workflow-a editatzeko + text_are_you_sure: Ziur zaude? + text_journal_changed: "%{label} %{old}-(e)tik %{new}-(e)ra aldatuta" + text_journal_set_to: "%{label}-k %{value} balioa hartu du" + text_journal_deleted: "%{label} ezabatuta (%{old})" + text_journal_added: "%{label} %{value} gehituta" + text_tip_issue_begin_day: gaur hasten diren zereginak + text_tip_issue_end_day: gaur bukatzen diren zereginak + text_tip_issue_begin_end_day: gaur hasi eta bukatzen diren zereginak + text_caracters_maximum: "%{count} karaktere gehienez." + text_caracters_minimum: "Gutxienez %{count} karaktereetako luzerakoa izan behar du." + text_length_between: "Luzera %{min} eta %{max} karaktereen artekoa." + text_tracker_no_workflow: Ez da workflow-rik definitu aztarnari honentzako + text_unallowed_characters: Debekatutako karaktereak + text_comma_separated: Balio anitz izan daitezke (komaz banatuta). + text_line_separated: Balio anitz izan daitezke (balio bakoitza lerro batean). + text_issues_ref_in_commit_messages: Commit-en mezuetan zereginak erlazionatu eta konpontzen + text_issue_added: "%{id} zeregina %{author}-(e)k jakinarazi du." + text_issue_updated: "%{id} zeregina %{author}-(e)k eguneratu du." + text_wiki_destroy_confirmation: Ziur zaude wiki hau eta bere eduki guztiak ezabatu nahi dituzula? + text_issue_category_destroy_question: "Zeregin batzuk (%{count}) kategoria honetara esleituta daude. Zer egin nahi duzu?" + text_issue_category_destroy_assignments: Kategoria esleipenak kendu + text_issue_category_reassign_to: Zereginak kategoria honetara esleitu + text_user_mail_option: "Hautatu gabeko proiektuetan, behatzen edo parte hartzen duzun gauzei buruzko jakinarazpenak jasoko dituzu (adib. zu egile zaren edo esleituta dituzun zereginak)." + text_no_configuration_data: "Rolak, aztarnariak, zeregin egoerak eta workflow-ak ez dira oraindik konfiguratu.\nOso gomendagarria de lehenetsitako kkonfigurazioa kargatzea. Kargatu eta gero aldatu ahalko duzu." + text_load_default_configuration: Lehenetsitako konfigurazioa kargatu + text_status_changed_by_changeset: "%{value} aldaketan aplikatuta." + text_issues_destroy_confirmation: 'Ziur zaude hautatutako zeregina(k) ezabatu nahi dituzula?' + text_select_project_modules: 'Hautatu proiektu honetan gaitu behar diren moduluak:' + text_default_administrator_account_changed: Lehenetsitako kudeatzaile kontua aldatuta + text_file_repository_writable: Eranskinen direktorioan idatz daiteke + text_plugin_assets_writable: Pluginen baliabideen direktorioan idatz daiteke + text_rmagick_available: RMagick eskuragarri (aukerazkoa) + text_destroy_time_entries_question: "%{hours} orduei buruz berri eman zen zuk ezabatzera zoazen zereginean. Zer egin nahi duzu?" + text_destroy_time_entries: Ezabatu berri emandako orduak + text_assign_time_entries_to_project: Berri emandako orduak proiektura esleitu + text_reassign_time_entries: 'Berri emandako orduak zeregin honetara esleitu:' + text_user_wrote: "%{value}-(e)k idatzi zuen:" + text_enumeration_destroy_question: "%{count} objetu balio honetara esleituta daude." + text_enumeration_category_reassign_to: 'Beste balio honetara esleitu:' + text_email_delivery_not_configured: "Eposta bidalketa ez dago konfiguratuta eta jakinarazpenak ezgaituta daude.\nKonfiguratu zure SMTP zerbitzaria config/configuration.yml-n eta aplikazioa berrabiarazi hauek gaitzeko." + text_repository_usernames_mapping: "Hautatu edo eguneratu Redmineko erabiltzailea biltegiko egunkarietan topatzen diren erabiltzaile izenekin erlazionatzeko.\nRedmine-n eta biltegian erabiltzaile izen edo eposta berdina duten erabiltzaileak automatikoki erlazionatzen dira." + text_diff_truncated: '... Diff hau moztua izan da erakus daitekeen tamaina maximoa gainditu duelako.' + text_custom_field_possible_values_info: 'Lerro bat balio bakoitzeko' + text_wiki_page_destroy_question: "Orri honek %{descendants} orri seme eta ondorengo ditu. Zer egin nahi duzu?" + text_wiki_page_nullify_children: "Orri semeak erro orri moduan mantendu" + text_wiki_page_destroy_children: "Orri semeak eta beraien ondorengo guztiak ezabatu" + text_wiki_page_reassign_children: "Orri semeak orri guraso honetara esleitu" + text_own_membership_delete_confirmation: "Zure baimen batzuk (edo guztiak) kentzera zoaz eta baliteke horren ondoren proiektu hau ezin editatzea.\n Ziur zaude jarraitu nahi duzula?" + + default_role_manager: Kudeatzailea + default_role_developer: Garatzailea + default_role_reporter: Berriemailea + default_tracker_bug: Errorea + default_tracker_feature: Eginbidea + default_tracker_support: Laguntza + default_issue_status_new: Berria + default_issue_status_in_progress: Lanean + default_issue_status_resolved: Ebatzita + default_issue_status_feedback: Berrelikadura + default_issue_status_closed: Itxita + default_issue_status_rejected: Baztertua + default_doc_category_user: Erabiltzaile dokumentazioa + default_doc_category_tech: Dokumentazio teknikoa + default_priority_low: Baxua + default_priority_normal: Normala + default_priority_high: Altua + default_priority_urgent: Larria + default_priority_immediate: Berehalakoa + default_activity_design: Diseinua + default_activity_development: Garapena + + enumeration_issue_priorities: Zeregin lehentasunak + enumeration_doc_categories: Dokumentu kategoriak + enumeration_activities: Jarduerak (denbora kontrola)) + enumeration_system_activity: Sistemako Jarduera + label_board_sticky: Itsaskorra + label_board_locked: Blokeatuta + permission_export_wiki_pages: Wiki orriak esportatu + setting_cache_formatted_text: Formatudun testua katxeatu + permission_manage_project_activities: Proiektuaren jarduerak kudeatu + error_unable_delete_issue_status: Ezine da zereginaren egoera ezabatu + label_profile: Profila + permission_manage_subtasks: Azpiatazak kudeatu + field_parent_issue: Zeregin gurasoa + label_subtask_plural: Azpiatazak + label_project_copy_notifications: Proiektua kopiatzen den bitartean eposta jakinarazpenak bidali + error_can_not_delete_custom_field: Ezin da eremu pertsonalizatua ezabatu + error_unable_to_connect: Ezin da konektatu (%{value}) + error_can_not_remove_role: Rol hau erabiltzen hari da eta ezin da ezabatu. + error_can_not_delete_tracker: Aztarnari honek zereginak ditu eta ezin da ezabatu. + field_principal: Ekintzaile + label_my_page_block: "Nire orriko blokea" + notice_failed_to_save_members: "Kidea(k) gordetzean errorea: %{errors}." + text_zoom_out: Zooma txikiagotu + text_zoom_in: Zooma handiagotu + notice_unable_delete_time_entry: "Ezin da hautatutako denbora erregistroa ezabatu." + label_overall_spent_time: Igarotako denbora guztira + field_time_entries: "Denbora erregistratu" + project_module_gantt: Gantt + project_module_calendar: Egutegia + button_edit_associated_wikipage: "Esleitutako wiki orria editatu: %{page_title}" + field_text: Testu eremua + label_user_mail_option_only_owner: "Jabea naizen gauzetarako barrarik" + setting_default_notification_option: "Lehenetsitako ohartarazpen aukera" + label_user_mail_option_only_my_events: "Behatzen ditudan edo partaide naizen gauzetarako bakarrik" + label_user_mail_option_only_assigned: "Niri esleitutako gauzentzat bakarrik" + label_user_mail_option_none: "Gertakaririk ez" + field_member_of_group: "Esleituta duenaren taldea" + field_assigned_to_role: "Esleituta duenaren rola" + notice_not_authorized_archived_project: "Atzitu nahi duzun proiektua artxibatua izan da." + label_principal_search: "Bilatu erabiltzaile edo taldea:" + label_user_search: "Erabiltzailea bilatu:" + field_visible: Ikusgai + setting_emails_header: "Eposten goiburua" + setting_commit_logtime_activity_id: "Erregistratutako denboraren jarduera" + text_time_logged_by_changeset: "%{value} aldaketan egindakoa." + setting_commit_logtime_enabled: "Erregistrutako denbora gaitu" + notice_gantt_chart_truncated: Grafikoa moztu da bistara daitekeen elementuen kopuru maximoa gainditu delako (%{max}) + setting_gantt_items_limit: "Gantt grafikoan bistara daitekeen elementu kopuru maximoa" + field_warn_on_leaving_unsaved: Gorde gabeko testua duen orri batetik ateratzen naizenean ohartarazi + text_warn_on_leaving_unsaved: Uneko orritik joaten bazara gorde gabeko testua galduko da. + label_my_queries: Nire galdera pertsonalizatuak + text_journal_changed_no_detail: "%{label} eguneratuta" + label_news_comment_added: Berri batera iruzkina gehituta + button_expand_all: Guztia zabaldu + button_collapse_all: Guztia tolestu + label_additional_workflow_transitions_for_assignee: Erabiltzaileak esleitua duenean baimendutako transtsizio gehigarriak + label_additional_workflow_transitions_for_author: Erabiltzailea egilea denean baimendutako transtsizio gehigarriak + label_bulk_edit_selected_time_entries: Hautatutako denbora egunkariak batera editatu + text_time_entries_destroy_confirmation: Ziur zaude hautatutako denbora egunkariak ezabatu nahi dituzula? + label_role_anonymous: Ezezaguna + label_role_non_member: Ez kidea + label_issue_note_added: Oharra gehituta + label_issue_status_updated: Egoera eguneratuta + label_issue_priority_updated: Lehentasuna eguneratuta + label_issues_visibility_own: Erabiltzaileak sortu edo esleituta dituen zereginak + field_issues_visibility: Zeregin ikusgarritasuna + label_issues_visibility_all: Zeregin guztiak + permission_set_own_issues_private: Nork bere zereginak publiko edo pribatu jarri + field_is_private: Pribatu + permission_set_issues_private: Zereginak publiko edo pribatu jarri + label_issues_visibility_public: Pribatu ez diren zeregin guztiak + text_issues_destroy_descendants_confirmation: Honek %{count} azpiataza ezabatuko ditu baita ere. + field_commit_logs_encoding: Commit-en egunkarien kodetzea + field_scm_path_encoding: Bidearen kodeketa + text_scm_path_encoding_note: "Lehentsita: UTF-8" + field_path_to_repository: Biltegirako bidea + field_root_directory: Erro direktorioa + field_cvs_module: Modulua + field_cvsroot: CVSROOT + text_mercurial_repository_note: Biltegi locala (adib. /hgrepo, c:\hgrepo) + text_scm_command: Komandoa + text_scm_command_version: Bertsioa + label_git_report_last_commit: Report last commit for files and directories + text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 zeregina + one: 1 zeregina + other: "%{count} zereginak" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: guztiak + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Azpiproiektuekin + label_cross_project_tree: Proiektu zuhaitzarekin + label_cross_project_hierarchy: Proiektu Hierarkiarekin + label_cross_project_system: Proiektu guztiekin + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1b/1b89f697be7379218e41c746c519419fe6b5879e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1b/1b89f697be7379218e41c746c519419fe6b5879e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,24 @@ +/* Ukrainian (UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* Written by Maxim Drogobitskiy (maxdao@gmail.com). */ +/* Corrected by Igor Milla (igor.fsp.milla@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['uk'] = { + closeText: 'Закрити', + prevText: '<', + nextText: '>', + currentText: 'Сьогодні', + monthNames: ['Січень','Лютий','Березень','Квітень','Травень','Червень', + 'Липень','Серпень','ВереÑень','Жовтень','ЛиÑтопад','Грудень'], + monthNamesShort: ['Січ','Лют','Бер','Кві','Тра','Чер', + 'Лип','Сер','Вер','Жов','ЛиÑ','Гру'], + dayNames: ['неділÑ','понеділок','вівторок','Ñереда','четвер','п’ÑтницÑ','Ñубота'], + dayNamesShort: ['нед','пнд','вів','Ñрд','чтв','птн','Ñбт'], + dayNamesMin: ['Ðд','Пн','Ð’Ñ‚','Ср','Чт','Пт','Сб'], + weekHeader: 'Тиж', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['uk']); +}); \ No newline at end of file diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1b/1bbdb7187bcb352854ea61beba1bb597d2b8fddb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1b/1bbdb7187bcb352854ea61beba1bb597d2b8fddb.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1080 @@ +th: + direction: ltr + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%Y-%m-%d" + short: "%b %d" + long: "%B %d, %Y" + + day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday] + abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December] + abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%a, %d %b %Y %H:%M:%S %z" + time: "%H:%M" + short: "%d %b %H:%M" + long: "%B %d, %Y %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "half a minute" + less_than_x_seconds: + one: "less than 1 second" + other: "less than %{count} seconds" + x_seconds: + one: "1 second" + other: "%{count} seconds" + less_than_x_minutes: + one: "less than a minute" + other: "less than %{count} minutes" + x_minutes: + one: "1 minute" + other: "%{count} minutes" + about_x_hours: + one: "about 1 hour" + other: "about %{count} hours" + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "1 day" + other: "%{count} days" + about_x_months: + one: "about 1 month" + other: "about %{count} months" + x_months: + one: "1 month" + other: "%{count} months" + about_x_years: + one: "about 1 year" + other: "about %{count} years" + over_x_years: + one: "over 1 year" + other: "over %{count} years" + almost_x_years: + one: "almost 1 year" + other: "almost %{count} years" + + number: + format: + separator: "." + delimiter: "" + precision: 3 + human: + format: + precision: 3 + delimiter: "" + storage_units: + format: "%n %u" + units: + kb: KB + tb: TB + gb: GB + byte: + one: Byte + other: Bytes + mb: MB + +# Used in array.to_sentence. + support: + array: + sentence_connector: "and" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + messages: + inclusion: "ไม่อยู่ในรายà¸à¸²à¸£" + exclusion: "ถูà¸à¸ªà¸‡à¸§à¸™à¹„ว้" + invalid: "ไม่ถูà¸à¸•้อง" + confirmation: "พิมพ์ไม่เหมือนเดิม" + accepted: "ต้องยอมรับ" + empty: "ต้องเติม" + blank: "ต้องเติม" + too_long: "ยาวเà¸à¸´à¸™à¹„ป" + too_short: "สั้นเà¸à¸´à¸™à¹„ป" + wrong_length: "ความยาวไม่ถูà¸à¸•้อง" + taken: "ถูà¸à¹ƒà¸Šà¹‰à¹„ปà¹à¸¥à¹‰à¸§" + not_a_number: "ไม่ใช่ตัวเลข" + not_a_date: "ไม่ใช่วันที่ ที่ถูà¸à¸•้อง" + greater_than: "must be greater than %{count}" + greater_than_or_equal_to: "must be greater than or equal to %{count}" + equal_to: "must be equal to %{count}" + less_than: "must be less than %{count}" + less_than_or_equal_to: "must be less than or equal to %{count}" + odd: "must be odd" + even: "must be even" + greater_than_start_date: "ต้องมาà¸à¸à¸§à¹ˆà¸²à¸§à¸±à¸™à¹€à¸£à¸´à¹ˆà¸¡" + not_same_project: "ไม่ได้อยู่ในโครงà¸à¸²à¸£à¹€à¸”ียวà¸à¸±à¸™" + circular_dependency: "ความสัมพันธ์อ้างอิงเป็นวงà¸à¸¥à¸¡" + cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks" + + actionview_instancetag_blank_option: à¸à¸£à¸¸à¸“าเลือภ+ + general_text_No: 'ไม่' + general_text_Yes: 'ใช่' + general_text_no: 'ไม่' + general_text_yes: 'ใช่' + general_lang_name: 'Thai (ไทย)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: Windows-874 + general_pdf_encoding: cp874 + general_first_day_of_week: '1' + + notice_account_updated: บัà¸à¸Šà¸µà¹„ด้ถูà¸à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡à¹à¸¥à¹‰à¸§. + notice_account_invalid_creditentials: ชื้ผู้ใช้หรือรหัสผ่านไม่ถูà¸à¸•้อง + notice_account_password_updated: รหัสได้ถูà¸à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡à¹à¸¥à¹‰à¸§. + notice_account_wrong_password: รหัสผ่านไม่ถูà¸à¸•้อง + notice_account_register_done: บัà¸à¸Šà¸µà¸–ูà¸à¸ªà¸£à¹‰à¸²à¸‡à¹à¸¥à¹‰à¸§. à¸à¸£à¸¸à¸“าเช็คเมล์ à¹à¸¥à¹‰à¸§à¸„ลิ๊à¸à¸—ี่ลิงค์ในอีเมล์เพื่อเปิดใช้บัà¸à¸Šà¸µ + notice_account_unknown_email: ไม่มีผู้ใช้ที่ใช้อีเมล์นี้. + notice_can_t_change_password: บัà¸à¸Šà¸µà¸™à¸µà¹‰à¹ƒà¸Šà¹‰à¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตนจาà¸à¹à¸«à¸¥à¹ˆà¸‡à¸ à¸²à¸¢à¸™à¸­à¸. ไม่สามารถปลี่ยนรหัสผ่านได้. + notice_account_lost_email_sent: เราได้ส่งอีเมล์พร้อมวิธีà¸à¸²à¸£à¸ªà¸£à¹‰à¸²à¸‡à¸£à¸«à¸±à¸µà¸ªà¸œà¹ˆà¸²à¸™à¹ƒà¸«à¸¡à¹ˆà¹ƒà¸«à¹‰à¸„ุณà¹à¸¥à¹‰à¸§ à¸à¸£à¸¸à¸“าเช็คเมล์. + notice_account_activated: บัà¸à¸Šà¸µà¸‚องคุณได้เปิดใช้à¹à¸¥à¹‰à¸§. ตอนนี้คุณสามารถเข้าสู่ระบบได้à¹à¸¥à¹‰à¸§. + notice_successful_create: สร้างเสร็จà¹à¸¥à¹‰à¸§. + notice_successful_update: ปรับปรุงเสร็จà¹à¸¥à¹‰à¸§. + notice_successful_delete: ลบเสร็จà¹à¸¥à¹‰à¸§. + notice_successful_connection: ติดต่อสำเร็จà¹à¸¥à¹‰à¸§. + notice_file_not_found: หน้าที่คุณต้องà¸à¸²à¸£à¸”ูไม่มีอยู่จริง หรือถูà¸à¸¥à¸šà¹„ปà¹à¸¥à¹‰à¸§. + notice_locking_conflict: ข้อมูลถูà¸à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡à¹‚ดยผู้ใช้คนอื่น. + notice_not_authorized: คุณไม่มีสิทธิเข้าถึงหน้านี้. + notice_email_sent: "อีเมล์ได้ถูà¸à¸ªà¹ˆà¸‡à¸–ึง %{value}" + notice_email_error: "เà¸à¸´à¸”ความผิดพลาดขณะà¸à¸³à¸ªà¹ˆà¸‡à¸­à¸µà¹€à¸¡à¸¥à¹Œ (%{value})" + notice_feeds_access_key_reseted: RSS access key ของคุณถูภreset à¹à¸¥à¹‰à¸§. + notice_failed_to_save_issues: "%{count} ปัà¸à¸«à¸²à¸ˆà¸²à¸ %{total} ปัà¸à¸«à¸²à¸—ี่ถูà¸à¹€à¸¥à¸·à¸­à¸à¹„ม่สามารถจัดเà¸à¹‡à¸š: %{ids}." + notice_no_issue_selected: "ไม่มีปัà¸à¸«à¸²à¸—ี่ถูà¸à¹€à¸¥à¸·à¸­à¸! à¸à¸£à¸¸à¸“าเลือà¸à¸›à¸±à¸à¸«à¸²à¸—ี่คุณต้องà¸à¸²à¸£à¹à¸à¹‰à¹„ข." + notice_account_pending: "บัà¸à¸Šà¸µà¸‚องคุณสร้างเสร็จà¹à¸¥à¹‰à¸§ ขณะนี้รอà¸à¸²à¸£à¸­à¸™à¸¸à¸¡à¸±à¸•ิจาà¸à¸œà¸¹à¹‰à¸šà¸£à¸´à¸«à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£." + notice_default_data_loaded: ค่าเริ่มต้นโหลดเสร็จà¹à¸¥à¹‰à¸§. + + error_can_t_load_default_data: "ค่าเริ่มต้นโหลดไม่สำเร็จ: %{value}" + error_scm_not_found: "ไม่พบรุ่นที่ต้องà¸à¸²à¸£à¹ƒà¸™à¹à¸«à¸¥à¹ˆà¸‡à¹€à¸à¹‡à¸šà¸•้นฉบับ." + error_scm_command_failed: "เà¸à¸´à¸”ความผิดพลาดในà¸à¸²à¸£à¹€à¸‚้าถึงà¹à¸«à¸¥à¹ˆà¸‡à¹€à¸à¹‡à¸šà¸•้นฉบับ: %{value}" + error_scm_annotate: "entry ไม่มีอยู่จริง หรือไม่สามารถเขียนหมายเหตุประà¸à¸­à¸š." + error_issue_not_found_in_project: 'ไม่พบปัà¸à¸«à¸²à¸™à¸µà¹‰ หรือปัà¸à¸«à¸²à¹„ม่ได้อยู่ในโครงà¸à¸²à¸£à¸™à¸µà¹‰' + + mail_subject_lost_password: "รหัสผ่าน %{value} ของคุณ" + mail_body_lost_password: 'คลิ๊à¸à¸—ี่ลิงค์ต่อไปนี้เพื่อเปลี่ยนรหัสผ่าน:' + mail_subject_register: "เปิดบัà¸à¸Šà¸µ %{value} ของคุณ" + mail_body_register: 'คลิ๊à¸à¸—ี่ลิงค์ต่อไปนี้เพื่อเปลี่ยนรหัสผ่าน:' + mail_body_account_information_external: "คุณสามารถใช้บัà¸à¸Šà¸µ %{value} เพื่อเข้าสู่ระบบ." + mail_body_account_information: ข้อมูลบัà¸à¸Šà¸µà¸‚องคุณ + mail_subject_account_activation_request: "à¸à¸£à¸¸à¸“าเปิดบัà¸à¸Šà¸µ %{value}" + mail_body_account_activation_request: "ผู้ใช้ใหม่ (%{value}) ได้ลงทะเบียน. บัà¸à¸Šà¸µà¸‚องเขาà¸à¸³à¸¥à¸±à¸‡à¸£à¸­à¸­à¸™à¸¸à¸¡à¸±à¸•ิ:" + + gui_validation_error: 1 ข้อผิดพลาด + gui_validation_error_plural: "%{count} ข้อผิดพลาด" + + field_name: ชื่อ + field_description: รายละเอียด + field_summary: สรุปย่อ + field_is_required: ต้องใส่ + field_firstname: ชื่อ + field_lastname: นามสà¸à¸¸à¸¥ + field_mail: อีเมล์ + field_filename: à¹à¸Ÿà¹‰à¸¡ + field_filesize: ขนาด + field_downloads: ดาวน์โหลด + field_author: ผู้à¹à¸•่ง + field_created_on: สร้าง + field_updated_on: ปรับปรุง + field_field_format: รูปà¹à¸šà¸š + field_is_for_all: สำหรับทุà¸à¹‚ครงà¸à¸²à¸£ + field_possible_values: ค่าที่เป็นไปได้ + field_regexp: Regular expression + field_min_length: สั้นสุด + field_max_length: ยาวสุด + field_value: ค่า + field_category: ประเภท + field_title: ชื่อเรื่อง + field_project: โครงà¸à¸²à¸£ + field_issue: ปัà¸à¸«à¸² + field_status: สถานะ + field_notes: บันทึภ+ field_is_closed: ปัà¸à¸«à¸²à¸ˆà¸š + field_is_default: ค่าเริ่มต้น + field_tracker: à¸à¸²à¸£à¸•ิดตาม + field_subject: เรื่อง + field_due_date: วันครบà¸à¸³à¸«à¸™à¸” + field_assigned_to: มอบหมายให้ + field_priority: ความสำคัภ+ field_fixed_version: รุ่น + field_user: ผู้ใช้ + field_role: บทบาท + field_homepage: หน้าà¹à¸£à¸ + field_is_public: สาธารณะ + field_parent: โครงà¸à¸²à¸£à¸¢à¹ˆà¸­à¸¢à¸‚อง + field_is_in_roadmap: ปัà¸à¸«à¸²à¹à¸ªà¸”งใน à¹à¸œà¸™à¸‡à¸²à¸™ + field_login: ชื่อที่ใช้เข้าระบบ + field_mail_notification: à¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•ือนทางอีเมล์ + field_admin: ผู้บริหารจัดà¸à¸²à¸£ + field_last_login_on: เข้าระบบครั้งสุดท้าย + field_language: ภาษา + field_effective_date: วันที่ + field_password: รหัสผ่าน + field_new_password: รหัสผ่านใหม่ + field_password_confirmation: ยืนยันรหัสผ่าน + field_version: รุ่น + field_type: ชนิด + field_host: โฮสต์ + field_port: พอร์ต + field_account: บัà¸à¸Šà¸µ + field_base_dn: Base DN + field_attr_login: เข้าระบบ attribute + field_attr_firstname: ชื่อ attribute + field_attr_lastname: นามสà¸à¸¸à¸¥ attribute + field_attr_mail: อีเมล์ attribute + field_onthefly: สร้างผู้ใช้ทันที + field_start_date: เริ่ม + field_done_ratio: "% สำเร็จ" + field_auth_source: วิธีà¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตน + field_hide_mail: ซ่อนอีเมล์ของฉัน + field_comments: ความเห็น + field_url: URL + field_start_page: หน้าเริ่มต้น + field_subproject: โครงà¸à¸²à¸£à¸¢à¹ˆà¸­à¸¢ + field_hours: ชั่วโมง + field_activity: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡ + field_spent_on: วันที่ + field_identifier: ชื่อเฉพาะ + field_is_filter: ใช้เป็นตัวà¸à¸£à¸­à¸‡ + field_issue_to: ปัà¸à¸«à¸²à¸—ี่เà¸à¸µà¹ˆà¸¢à¸§à¸‚้อง + field_delay: เลื่อน + field_assignable: ปัà¸à¸«à¸²à¸ªà¸²à¸¡à¸²à¸£à¸–มอบหมายให้คนที่ทำบทบาทนี้ + field_redirect_existing_links: ย้ายจุดเชื่อมโยงนี้ + field_estimated_hours: เวลาที่ใช้โดยประมาณ + field_column_names: สดมภ์ + field_time_zone: ย่านเวลา + field_searchable: ค้นหาได้ + field_default_value: ค่าเริ่มต้น + field_comments_sorting: à¹à¸ªà¸”งความเห็น + + setting_app_title: ชื่อโปรà¹à¸à¸£à¸¡ + setting_app_subtitle: ชื่อโปรà¹à¸à¸£à¸¡à¸£à¸­à¸‡ + setting_welcome_text: ข้อความต้อนรับ + setting_default_language: ภาษาเริ่มต้น + setting_login_required: ต้องป้อนผู้ใช้-รหัสผ่าน + setting_self_registration: ลงทะเบียนด้วยตนเอง + setting_attachment_max_size: ขนาดà¹à¸Ÿà¹‰à¸¡à¹à¸™à¸šà¸ªà¸¹à¸‡à¸ªà¸¸à¸” + setting_issues_export_limit: à¸à¸²à¸£à¸ªà¹ˆà¸‡à¸­à¸­à¸à¸›à¸±à¸à¸«à¸²à¸ªà¸¹à¸‡à¸ªà¸¸à¸” + setting_mail_from: อีเมล์ที่ใช้ส่ง + setting_bcc_recipients: ไม่ระบุชื่อผู้รับ (bcc) + setting_host_name: ชื่อโฮสต์ + setting_text_formatting: à¸à¸²à¸£à¸ˆà¸±à¸”รูปà¹à¸šà¸šà¸‚้อความ + setting_wiki_compression: บีบอัดประวัติ Wiki + setting_feeds_limit: จำนวน Feed + setting_default_projects_public: โครงà¸à¸²à¸£à¹ƒà¸«à¸¡à¹ˆà¸¡à¸µà¸„่าเริ่มต้นเป็น สาธารณะ + setting_autofetch_changesets: ดึง commits อัตโนมัติ + setting_sys_api_enabled: เปิดใช้ WS สำหรับà¸à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£à¸—ี่เà¸à¹‡à¸šà¸•้นฉบับ + setting_commit_ref_keywords: คำสำคัภReferencing + setting_commit_fix_keywords: คำสำคัภFixing + setting_autologin: เข้าระบบอัตโนมัติ + setting_date_format: รูปà¹à¸šà¸šà¸§à¸±à¸™à¸—ี่ + setting_time_format: รูปà¹à¸šà¸šà¹€à¸§à¸¥à¸² + setting_cross_project_issue_relations: อนุà¸à¸²à¸•ให้ระบุปัà¸à¸«à¸²à¸‚้ามโครงà¸à¸²à¸£ + setting_issue_list_default_columns: สดมภ์เริ่มต้นà¹à¸ªà¸”งในรายà¸à¸²à¸£à¸›à¸±à¸à¸«à¸² + setting_emails_footer: คำลงท้ายอีเมล์ + setting_protocol: Protocol + setting_per_page_options: ตัวเลือà¸à¸ˆà¸³à¸™à¸§à¸™à¸•่อหน้า + setting_user_format: รูปà¹à¸šà¸šà¸à¸²à¸£à¹à¸ªà¸”งชื่อผู้ใช้ + setting_activity_days_default: จำนวนวันที่à¹à¸ªà¸”งในà¸à¸´à¸ˆà¸à¸£à¸£à¸¡à¸‚องโครงà¸à¸²à¸£ + setting_display_subprojects_issues: à¹à¸ªà¸”งปัà¸à¸«à¸²à¸‚องโครงà¸à¸²à¸£à¸¢à¹ˆà¸­à¸¢à¹ƒà¸™à¹‚ครงà¸à¸²à¸£à¸«à¸¥à¸±à¸ + + project_module_issue_tracking: à¸à¸²à¸£à¸•ิดตามปัà¸à¸«à¸² + project_module_time_tracking: à¸à¸²à¸£à¹ƒà¸Šà¹‰à¹€à¸§à¸¥à¸² + project_module_news: ข่าว + project_module_documents: เอà¸à¸ªà¸²à¸£ + project_module_files: à¹à¸Ÿà¹‰à¸¡ + project_module_wiki: Wiki + project_module_repository: ที่เà¸à¹‡à¸šà¸•้นฉบับ + project_module_boards: à¸à¸£à¸°à¸”านข้อความ + + label_user: ผู้ใช้ + label_user_plural: ผู้ใช้ + label_user_new: ผู้ใช้ใหม่ + label_project: โครงà¸à¸²à¸£ + label_project_new: โครงà¸à¸²à¸£à¹ƒà¸«à¸¡à¹ˆ + label_project_plural: โครงà¸à¸²à¸£ + label_x_projects: + zero: no projects + one: 1 project + other: "%{count} projects" + label_project_all: โครงà¸à¸²à¸£à¸—ั้งหมด + label_project_latest: โครงà¸à¸²à¸£à¸¥à¹ˆà¸²à¸ªà¸¸à¸” + label_issue: ปัà¸à¸«à¸² + label_issue_new: ปัà¸à¸«à¸²à¹ƒà¸«à¸¡à¹ˆ + label_issue_plural: ปัà¸à¸«à¸² + label_issue_view_all: ดูปัà¸à¸«à¸²à¸—ั้งหมด + label_issues_by: "ปัà¸à¸«à¸²à¹‚ดย %{value}" + label_issue_added: ปัà¸à¸«à¸²à¸–ูà¸à¹€à¸žà¸´à¹ˆà¸¡ + label_issue_updated: ปัà¸à¸«à¸²à¸–ูà¸à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡ + label_document: เอà¸à¸ªà¸²à¸£ + label_document_new: เอà¸à¸ªà¸²à¸£à¹ƒà¸«à¸¡à¹ˆ + label_document_plural: เอà¸à¸ªà¸²à¸£ + label_document_added: เอà¸à¸ªà¸²à¸£à¸–ูà¸à¹€à¸žà¸´à¹ˆà¸¡ + label_role: บทบาท + label_role_plural: บทบาท + label_role_new: บทบาทใหม่ + label_role_and_permissions: บทบาทà¹à¸¥à¸°à¸ªà¸´à¸—ธิ + label_member: สมาชิภ+ label_member_new: สมาชิà¸à¹ƒà¸«à¸¡à¹ˆ + label_member_plural: สมาชิภ+ label_tracker: à¸à¸²à¸£à¸•ิดตาม + label_tracker_plural: à¸à¸²à¸£à¸•ิดตาม + label_tracker_new: à¸à¸²à¸£à¸•ิดตามใหม่ + label_workflow: ลำดับงาน + label_issue_status: สถานะของปัà¸à¸«à¸² + label_issue_status_plural: สถานะของปัà¸à¸«à¸² + label_issue_status_new: สถานะใหม + label_issue_category: ประเภทของปัà¸à¸«à¸² + label_issue_category_plural: ประเภทของปัà¸à¸«à¸² + label_issue_category_new: ประเภทใหม่ + label_custom_field: เขตข้อมูลà¹à¸šà¸šà¸£à¸°à¸šà¸¸à¹€à¸­à¸‡ + label_custom_field_plural: เขตข้อมูลà¹à¸šà¸šà¸£à¸°à¸šà¸¸à¹€à¸­à¸‡ + label_custom_field_new: สร้างเขตข้อมูลà¹à¸šà¸šà¸£à¸°à¸šà¸¸à¹€à¸­à¸‡ + label_enumerations: รายà¸à¸²à¸£ + label_enumeration_new: สร้างใหม่ + label_information: ข้อมูล + label_information_plural: ข้อมูล + label_please_login: à¸à¸£à¸¸à¸“าเข้าระบบà¸à¹ˆà¸­à¸™ + label_register: ลงทะเบียน + label_password_lost: ลืมรหัสผ่าน + label_home: หน้าà¹à¸£à¸ + label_my_page: หน้าของฉัน + label_my_account: บัà¸à¸Šà¸µà¸‚องฉัน + label_my_projects: โครงà¸à¸²à¸£à¸‚องฉัน + label_administration: บริหารจัดà¸à¸²à¸£ + label_login: เข้าระบบ + label_logout: ออà¸à¸£à¸°à¸šà¸š + label_help: ช่วยเหลือ + label_reported_issues: ปัà¸à¸«à¸²à¸—ี่à¹à¸ˆà¹‰à¸‡à¹„ว้ + label_assigned_to_me_issues: ปัà¸à¸«à¸²à¸—ี่มอบหมายให้ฉัน + label_last_login: ติดต่อครั้งสุดท้าย + label_registered_on: ลงทะเบียนเมื่อ + label_activity: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡ + label_activity_plural: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡ + label_activity_latest: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡à¸¥à¹ˆà¸²à¸ªà¸¸à¸” + label_overall_activity: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡à¹‚ดยรวม + label_new: ใหม่ + label_logged_as: เข้าระบบในชื่อ + label_environment: สภาพà¹à¸§à¸”ล้อม + label_authentication: à¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตน + label_auth_source: วิธีà¸à¸²à¸£à¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตน + label_auth_source_new: สร้างวิธีà¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตนใหม่ + label_auth_source_plural: วิธีà¸à¸²à¸£ Authentication + label_subproject_plural: โครงà¸à¸²à¸£à¸¢à¹ˆà¸­à¸¢ + label_min_max_length: สั้น-ยาว สุดที่ + label_list: รายà¸à¸²à¸£ + label_date: วันที่ + label_integer: จำนวนเต็ม + label_float: จำนวนจริง + label_boolean: ถูà¸à¸œà¸´à¸” + label_string: ข้อความ + label_text: ข้อความขนาดยาว + label_attribute: คุณลัà¸à¸©à¸“ะ + label_attribute_plural: คุณลัà¸à¸©à¸“ะ + label_download: "%{count} ดาวน์โหลด" + label_download_plural: "%{count} ดาวน์โหลด" + label_no_data: จำนวนข้อมูลที่à¹à¸ªà¸”ง + label_change_status: เปลี่ยนสถานะ + label_history: ประวัติ + label_attachment: à¹à¸Ÿà¹‰à¸¡ + label_attachment_new: à¹à¸Ÿà¹‰à¸¡à¹ƒà¸«à¸¡à¹ˆ + label_attachment_delete: ลบà¹à¸Ÿà¹‰à¸¡ + label_attachment_plural: à¹à¸Ÿà¹‰à¸¡ + label_file_added: à¹à¸Ÿà¹‰à¸¡à¸–ูà¸à¹€à¸žà¸´à¹ˆà¸¡ + label_report: รายงาน + label_report_plural: รายงาน + label_news: ข่าว + label_news_new: เพิ่มข่าว + label_news_plural: ข่าว + label_news_latest: ข่าวล่าสุด + label_news_view_all: ดูข่าวทั้งหมด + label_news_added: ข่าวถูà¸à¹€à¸žà¸´à¹ˆà¸¡ + label_settings: ปรับà¹à¸•่ง + label_overview: ภาพรวม + label_version: รุ่น + label_version_new: รุ่นใหม่ + label_version_plural: รุ่น + label_confirmation: ยืนยัน + label_export_to: 'รูปà¹à¸šà¸šà¸­à¸·à¹ˆà¸™à¹† :' + label_read: อ่าน... + label_public_projects: โครงà¸à¸²à¸£à¸ªà¸²à¸˜à¸²à¸£à¸“ะ + label_open_issues: เปิด + label_open_issues_plural: เปิด + label_closed_issues: ปิด + label_closed_issues_plural: ปิด + label_x_open_issues_abbr_on_total: + zero: 0 open / %{total} + one: 1 open / %{total} + other: "%{count} open / %{total}" + label_x_open_issues_abbr: + zero: 0 open + one: 1 open + other: "%{count} open" + label_x_closed_issues_abbr: + zero: 0 closed + one: 1 closed + other: "%{count} closed" + label_total: จำนวนรวม + label_permissions: สิทธิ + label_current_status: สถานะปัจจุบัน + label_new_statuses_allowed: อนุà¸à¸²à¸•ให้มีสถานะใหม่ + label_all: ทั้งหมด + label_none: ไม่มี + label_nobody: ไม่มีใคร + label_next: ต่อไป + label_previous: à¸à¹ˆà¸­à¸™à¸«à¸™à¹‰à¸² + label_used_by: ถูà¸à¹ƒà¸Šà¹‰à¹‚ดย + label_details: รายละเอียด + label_add_note: เพิ่มบันทึภ+ label_per_page: ต่อหน้า + label_calendar: ปà¸à¸´à¸—ิน + label_months_from: เดือนจาภ+ label_gantt: Gantt + label_internal: ภายใน + label_last_changes: "last %{count} เปลี่ยนà¹à¸›à¸¥à¸‡" + label_change_view_all: ดูà¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡à¸—ั้งหมด + label_personalize_page: ปรับà¹à¸•่งหน้านี้ + label_comment: ความเห็น + label_comment_plural: ความเห็น + label_x_comments: + zero: no comments + one: 1 comment + other: "%{count} comments" + label_comment_add: เพิ่มความเห็น + label_comment_added: ความเห็นถูà¸à¹€à¸žà¸´à¹ˆà¸¡ + label_comment_delete: ลบความเห็น + label_query: à¹à¸šà¸šà¸ªà¸­à¸šà¸–ามà¹à¸šà¸šà¸à¸³à¸«à¸™à¸”เอง + label_query_plural: à¹à¸šà¸šà¸ªà¸­à¸šà¸–ามà¹à¸šà¸šà¸à¸³à¸«à¸™à¸”เอง + label_query_new: à¹à¸šà¸šà¸ªà¸­à¸šà¸–ามใหม่ + label_filter_add: เพิ่มตัวà¸à¸£à¸­à¸‡ + label_filter_plural: ตัวà¸à¸£à¸­à¸‡ + label_equals: คือ + label_not_equals: ไม่ใช่ + label_in_less_than: น้อยà¸à¸§à¹ˆà¸² + label_in_more_than: มาà¸à¸à¸§à¹ˆà¸² + label_in: ในช่วง + label_today: วันนี้ + label_all_time: ตลอดเวลา + label_yesterday: เมื่อวาน + label_this_week: อาทิตย์นี้ + label_last_week: อาทิตย์ที่à¹à¸¥à¹‰à¸§ + label_last_n_days: "%{count} วันย้อนหลัง" + label_this_month: เดือนนี้ + label_last_month: เดือนที่à¹à¸¥à¹‰à¸§ + label_this_year: ปีนี้ + label_date_range: ช่วงวันที่ + label_less_than_ago: น้อยà¸à¸§à¹ˆà¸²à¸«à¸™à¸¶à¹ˆà¸‡à¸§à¸±à¸™ + label_more_than_ago: มาà¸à¸à¸§à¹ˆà¸²à¸«à¸™à¸¶à¹ˆà¸‡à¸§à¸±à¸™ + label_ago: วันผ่านมาà¹à¸¥à¹‰à¸§ + label_contains: มี... + label_not_contains: ไม่มี... + label_day_plural: วัน + label_repository: ที่เà¸à¹‡à¸šà¸•้นฉบับ + label_repository_plural: ที่เà¸à¹‡à¸šà¸•้นฉบับ + label_browse: เปิดหา + label_modification: "%{count} เปลี่ยนà¹à¸›à¸¥à¸‡" + label_modification_plural: "%{count} เปลี่ยนà¹à¸›à¸¥à¸‡" + label_revision: à¸à¸²à¸£à¹à¸à¹‰à¹„ข + label_revision_plural: à¸à¸²à¸£à¹à¸à¹‰à¹„ข + label_associated_revisions: à¸à¸²à¸£à¹à¸à¹‰à¹„ขที่เà¸à¸µà¹ˆà¸¢à¸§à¸‚้อง + label_added: ถูà¸à¹€à¸žà¸´à¹ˆà¸¡ + label_modified: ถูà¸à¹à¸à¹‰à¹„ข + label_deleted: ถูà¸à¸¥à¸š + label_latest_revision: รุ่นà¸à¸²à¸£à¹à¸à¹‰à¹„ขล่าสุด + label_latest_revision_plural: รุ่นà¸à¸²à¸£à¹à¸à¹‰à¹„ขล่าสุด + label_view_revisions: ดูà¸à¸²à¸£à¹à¸à¹‰à¹„ข + label_max_size: ขนาดใหà¸à¹ˆà¸ªà¸¸à¸” + label_sort_highest: ย้ายไปบนสุด + label_sort_higher: ย้ายขึ้น + label_sort_lower: ย้ายลง + label_sort_lowest: ย้ายไปล่างสุด + label_roadmap: à¹à¸œà¸™à¸‡à¸²à¸™ + label_roadmap_due_in: "ถึงà¸à¸³à¸«à¸™à¸”ใน %{value}" + label_roadmap_overdue: "%{value} ช้าà¸à¸§à¹ˆà¸²à¸à¸³à¸«à¸™à¸”" + label_roadmap_no_issues: ไม่มีปัà¸à¸«à¸²à¸ªà¸³à¸«à¸£à¸±à¸šà¸£à¸¸à¹ˆà¸™à¸™à¸µà¹‰ + label_search: ค้นหา + label_result_plural: ผลà¸à¸²à¸£à¸„้นหา + label_all_words: ทุà¸à¸„ำ + label_wiki: Wiki + label_wiki_edit: à¹à¸à¹‰à¹„ข Wiki + label_wiki_edit_plural: à¹à¸à¹‰à¹„ข Wiki + label_wiki_page: หน้า Wiki + label_wiki_page_plural: หน้า Wiki + label_index_by_title: เรียงตามชื่อเรื่อง + label_index_by_date: เรียงตามวัน + label_current_version: รุ่นปัจจุบัน + label_preview: ตัวอย่างà¸à¹ˆà¸­à¸™à¸ˆà¸±à¸”เà¸à¹‡à¸š + label_feed_plural: Feeds + label_changes_details: รายละเอียดà¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡à¸—ั้งหมด + label_issue_tracking: ติดตามปัà¸à¸«à¸² + label_spent_time: เวลาที่ใช้ + label_f_hour: "%{value} ชั่วโมง" + label_f_hour_plural: "%{value} ชั่วโมง" + label_time_tracking: ติดตามà¸à¸²à¸£à¹ƒà¸Šà¹‰à¹€à¸§à¸¥à¸² + label_change_plural: เปลี่ยนà¹à¸›à¸¥à¸‡ + label_statistics: สถิติ + label_commits_per_month: Commits ต่อเดือน + label_commits_per_author: Commits ต่อผู้à¹à¸•่ง + label_view_diff: ดูความà¹à¸•à¸à¸•่าง + label_diff_inline: inline + label_diff_side_by_side: side by side + label_options: ตัวเลือภ+ label_copy_workflow_from: คัดลอà¸à¸¥à¸³à¸”ับงานจาภ+ label_permissions_report: รายงานสิทธิ + label_watched_issues: เà¸à¹‰à¸²à¸”ูปัà¸à¸«à¸² + label_related_issues: ปัà¸à¸«à¸²à¸—ี่เà¸à¸µà¹ˆà¸¢à¸§à¸‚้อง + label_applied_status: จัดเà¸à¹‡à¸šà¸ªà¸–านะ + label_loading: à¸à¸³à¸¥à¸±à¸‡à¹‚หลด... + label_relation_new: ความสัมพันธ์ใหม่ + label_relation_delete: ลบความสัมพันธ์ + label_relates_to: สัมพันธ์à¸à¸±à¸š + label_duplicates: ซ้ำ + label_blocks: à¸à¸µà¸”à¸à¸±à¸™ + label_blocked_by: à¸à¸µà¸”à¸à¸±à¸™à¹‚ดย + label_precedes: นำหน้า + label_follows: ตามหลัง + label_end_to_start: จบ-เริ่ม + label_end_to_end: จบ-จบ + label_start_to_start: เริ่ม-เริ่ม + label_start_to_end: เริ่ม-จบ + label_stay_logged_in: อยู่ในระบบต่อ + label_disabled: ไม่ใช้งาน + label_show_completed_versions: à¹à¸ªà¸”งรุ่นที่สมบูรณ์ + label_me: ฉัน + label_board: สภาà¸à¸²à¹à¸Ÿ + label_board_new: สร้างสภาà¸à¸²à¹à¸Ÿ + label_board_plural: สภาà¸à¸²à¹à¸Ÿ + label_topic_plural: หัวข้อ + label_message_plural: ข้อความ + label_message_last: ข้อความล่าสุด + label_message_new: เขียนข้อความใหม่ + label_message_posted: ข้อความถูà¸à¹€à¸žà¸´à¹ˆà¸¡à¹à¸¥à¹‰à¸§ + label_reply_plural: ตอบà¸à¸¥à¸±à¸š + label_send_information: ส่งรายละเอียดของบัà¸à¸Šà¸µà¹ƒà¸«à¹‰à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰ + label_year: ปี + label_month: เดือน + label_week: สัปดาห์ + label_date_from: จาภ+ label_date_to: ถึง + label_language_based: ขึ้นอยู่à¸à¸±à¸šà¸ à¸²à¸©à¸²à¸‚องผู้ใช้ + label_sort_by: "เรียงโดย %{value}" + label_send_test_email: ส่งจดหมายทดสอบ + label_feeds_access_key_created_on: "RSS access key สร้างเมื่อ %{value} ที่ผ่านมา" + label_module_plural: ส่วนประà¸à¸­à¸š + label_added_time_by: "เพิ่มโดย %{author} %{age} ที่ผ่านมา" + label_updated_time: "ปรับปรุง %{value} ที่ผ่านมา" + label_jump_to_a_project: ไปที่โครงà¸à¸²à¸£... + label_file_plural: à¹à¸Ÿà¹‰à¸¡ + label_changeset_plural: à¸à¸¥à¸¸à¹ˆà¸¡à¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡ + label_default_columns: สดมภ์เริ่มต้น + label_no_change_option: (ไม่เปลี่ยนà¹à¸›à¸¥à¸‡) + label_bulk_edit_selected_issues: à¹à¸à¹‰à¹„ขปัà¸à¸«à¸²à¸—ี่เลือà¸à¸—ั้งหมด + label_theme: ชุดรูปà¹à¸šà¸š + label_default: ค่าเริ่มต้น + label_search_titles_only: ค้นหาจาà¸à¸Šà¸·à¹ˆà¸­à¹€à¸£à¸·à¹ˆà¸­à¸‡à¹€à¸—่านั้น + label_user_mail_option_all: "ทุà¸à¹† เหตุà¸à¸²à¸£à¸“์ในโครงà¸à¸²à¸£à¸‚องฉัน" + label_user_mail_option_selected: "ทุà¸à¹† เหตุà¸à¸²à¸£à¸“์ในโครงà¸à¸²à¸£à¸—ี่เลือà¸..." + label_user_mail_no_self_notified: "ฉันไม่ต้องà¸à¸²à¸£à¹„ด้รับà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•ือนในสิ่งที่ฉันทำเอง" + label_registration_activation_by_email: เปิดบัà¸à¸Šà¸µà¸œà¹ˆà¸²à¸™à¸­à¸µà¹€à¸¡à¸¥à¹Œ + label_registration_manual_activation: อนุมัติโดยผู้บริหารจัดà¸à¸²à¸£ + label_registration_automatic_activation: เปิดบัà¸à¸Šà¸µà¸­à¸±à¸•โนมัติ + label_display_per_page: "ต่อหน้า: %{value}" + label_age: อายุ + label_change_properties: เปลี่ยนคุณสมบัติ + label_general: ทั่วๆ ไป + label_more: อื่น ๆ + label_scm: ตัวจัดà¸à¸²à¸£à¸•้นฉบับ + label_plugins: ส่วนเสริม + label_ldap_authentication: à¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตนโดยใช้ LDAP + label_downloads_abbr: D/L + label_optional_description: รายละเอียดเพิ่มเติม + label_add_another_file: เพิ่มà¹à¸Ÿà¹‰à¸¡à¸­à¸·à¹ˆà¸™à¹† + label_preferences: ค่าที่ชอบใจ + label_chronological_order: เรียงจาà¸à¹€à¸à¹ˆà¸²à¹„ปใหม่ + label_reverse_chronological_order: เรียงจาà¸à¹ƒà¸«à¸¡à¹ˆà¹„ปเà¸à¹ˆà¸² + label_planning: à¸à¸²à¸£à¸§à¸²à¸‡à¹à¸œà¸™ + + button_login: เข้าระบบ + button_submit: จัดส่งข้อมูล + button_save: จัดเà¸à¹‡à¸š + button_check_all: เลือà¸à¸—ั้งหมด + button_uncheck_all: ไม่เลือà¸à¸—ั้งหมด + button_delete: ลบ + button_create: สร้าง + button_test: ทดสอบ + button_edit: à¹à¸à¹‰à¹„ข + button_add: เพิ่ม + button_change: เปลี่ยนà¹à¸›à¸¥à¸‡ + button_apply: ประยุà¸à¸•์ใช้ + button_clear: ล้างข้อความ + button_lock: ล็อค + button_unlock: ยà¸à¹€à¸¥à¸´à¸à¸à¸²à¸£à¸¥à¹‡à¸­à¸„ + button_download: ดาวน์โหลด + button_list: รายà¸à¸²à¸£ + button_view: มุมมอง + button_move: ย้าย + button_back: à¸à¸¥à¸±à¸š + button_cancel: ยà¸à¹€à¸¥à¸´à¸ + button_activate: เปิดใช้ + button_sort: จัดเรียง + button_log_time: บันทึà¸à¹€à¸§à¸¥à¸² + button_rollback: ถอยà¸à¸¥à¸±à¸šà¸¡à¸²à¸—ี่รุ่นนี้ + button_watch: เà¸à¹‰à¸²à¸”ู + button_unwatch: เลิà¸à¹€à¸à¹‰à¸²à¸”ู + button_reply: ตอบà¸à¸¥à¸±à¸š + button_archive: เà¸à¹‡à¸šà¹€à¸‚้าโà¸à¸”ัง + button_unarchive: เอาออà¸à¸ˆà¸²à¸à¹‚à¸à¸”ัง + button_reset: เริ่มใหมท + button_rename: เปลี่ยนชื่อ + button_change_password: เปลี่ยนรหัสผ่าน + button_copy: คัดลอภ+ button_annotate: หมายเหตุประà¸à¸­à¸š + button_update: ปรับปรุง + button_configure: ปรับà¹à¸•่ง + + status_active: เปิดใช้งานà¹à¸¥à¹‰à¸§ + status_registered: รอà¸à¸²à¸£à¸­à¸™à¸¸à¸¡à¸±à¸•ิ + status_locked: ล็อค + + text_select_mail_notifications: เลือà¸à¸à¸²à¸£à¸à¸£à¸°à¸—ำที่ต้องà¸à¸²à¸£à¹ƒà¸«à¹‰à¸ªà¹ˆà¸‡à¸­à¸µà¹€à¸¡à¸¥à¹Œà¹à¸ˆà¹‰à¸‡. + text_regexp_info: ตัวอย่าง ^[A-Z0-9]+$ + text_min_max_length_info: 0 หมายถึงไม่จำà¸à¸±à¸” + text_project_destroy_confirmation: คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¹„หมว่าต้องà¸à¸²à¸£à¸¥à¸šà¹‚ครงà¸à¸²à¸£à¹à¸¥à¸°à¸‚้อมูลที่เà¸à¸µà¹ˆà¸¢à¸§à¸‚้่อง ? + text_subprojects_destroy_warning: "โครงà¸à¸²à¸£à¸¢à¹ˆà¸­à¸¢: %{value} จะถูà¸à¸¥à¸šà¸”้วย." + text_workflow_edit: เลือà¸à¸šà¸—บาทà¹à¸¥à¸°à¸à¸²à¸£à¸•ิดตาม เพื่อà¹à¸à¹‰à¹„ขลำดับงาน + text_are_you_sure: คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¹„หม ? + text_tip_issue_begin_day: งานที่เริ่มวันนี้ + text_tip_issue_end_day: งานที่จบวันนี้ + text_tip_issue_begin_end_day: งานที่เริ่มà¹à¸¥à¸°à¸ˆà¸šà¸§à¸±à¸™à¸™à¸µà¹‰ + text_caracters_maximum: "สูงสุด %{count} ตัวอัà¸à¸©à¸£." + text_caracters_minimum: "ต้องยาวอย่างน้อย %{count} ตัวอัà¸à¸©à¸£." + text_length_between: "ความยาวระหว่าง %{min} ถึง %{max} ตัวอัà¸à¸©à¸£." + text_tracker_no_workflow: ไม่ได้บัà¸à¸à¸±à¸•ิลำดับงานสำหรับà¸à¸²à¸£à¸•ิดตามนี้ + text_unallowed_characters: ตัวอัà¸à¸©à¸£à¸•้องห้าม + text_comma_separated: ใส่ได้หลายค่า โดยคั่นด้วยลูà¸à¸™à¹‰à¸³( ,). + text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages + text_issue_added: "ปัà¸à¸«à¸² %{id} ถูà¸à¹à¸ˆà¹‰à¸‡à¹‚ดย %{author}." + text_issue_updated: "ปัà¸à¸«à¸² %{id} ถูà¸à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡à¹‚ดย %{author}." + text_wiki_destroy_confirmation: คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¸«à¸£à¸·à¸­à¸§à¹ˆà¸²à¸•้องà¸à¸²à¸£à¸¥à¸š wiki นี้พร้อมทั้งเนี้อหา? + text_issue_category_destroy_question: "บางปัà¸à¸«à¸² (%{count}) อยู่ในประเภทนี้. คุณต้องà¸à¸²à¸£à¸—ำอย่างไร ?" + text_issue_category_destroy_assignments: ลบประเภทนี้ + text_issue_category_reassign_to: ระบุปัà¸à¸«à¸²à¹ƒà¸™à¸›à¸£à¸°à¹€à¸ à¸—นี้ + text_user_mail_option: "ในโครงà¸à¸²à¸£à¸—ี่ไม่ได้เลือà¸, คุณจะได้รับà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¸ªà¸´à¹ˆà¸‡à¸—ี่คุณเà¸à¹‰à¸²à¸”ูหรือมีส่วนเà¸à¸µà¹ˆà¸¢à¸§à¸‚้อง (เช่นปัà¸à¸«à¸²à¸—ี่คุณà¹à¸ˆà¹‰à¸‡à¹„ว้หรือได้รับมอบหมาย)." + text_no_configuration_data: "บทบาท, à¸à¸²à¸£à¸•ิดตาม, สถานะปัà¸à¸«à¸² à¹à¸¥à¸°à¸¥à¸³à¸”ับงานยังไม่ได้ถูà¸à¸•ั้งค่า.\nขอà¹à¸™à¸°à¸™à¸³à¹ƒà¸«à¹‰à¹‚หลดค่าเริ่มต้น. คุณสามารถà¹à¸à¹‰à¹„ขค่าได้หลังจาà¸à¹‚หลดà¹à¸¥à¹‰à¸§." + text_load_default_configuration: โหลดค่าเริ่มต้น + text_status_changed_by_changeset: "ประยุà¸à¸•์ใช้ในà¸à¸¥à¸¸à¹ˆà¸¡à¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡ %{value}." + text_issues_destroy_confirmation: 'คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¹„หมว่าต้องà¸à¸²à¸£à¸¥à¸šà¸›à¸±à¸à¸«à¸²(ทั้งหลาย)ที่เลือà¸à¹„ว้?' + text_select_project_modules: 'เลือà¸à¸ªà¹ˆà¸§à¸™à¸›à¸£à¸°à¸à¸­à¸šà¸—ี่ต้องà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸ªà¸³à¸«à¸£à¸±à¸šà¹‚ครงà¸à¸²à¸£à¸™à¸µà¹‰:' + text_default_administrator_account_changed: ค่าเริ่มต้นของบัà¸à¸Šà¸µà¸œà¸¹à¹‰à¸šà¸£à¸´à¸«à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£à¸–ูà¸à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡ + text_file_repository_writable: ที่เà¸à¹‡à¸šà¸•้นฉบับสามารถเขียนได้ + text_rmagick_available: RMagick มีให้ใช้ (เป็นตัวเลือà¸) + text_destroy_time_entries_question: "%{hours} ชั่วโมงที่ถูà¸à¹à¸ˆà¹‰à¸‡à¹ƒà¸™à¸›à¸±à¸à¸«à¸²à¸™à¸µà¹‰à¸ˆà¸°à¹‚ดนลบ. คุณต้องà¸à¸²à¸£à¸—ำอย่างไร?" + text_destroy_time_entries: ลบเวลาที่รายงานไว้ + text_assign_time_entries_to_project: ระบุเวลาที่ใช้ในโครงà¸à¸²à¸£à¸™à¸µà¹‰ + text_reassign_time_entries: 'ระบุเวลาที่ใช้ในโครงà¸à¸²à¸£à¸™à¸µà¹ˆà¸­à¸µà¸à¸„รั้ง:' + + default_role_manager: ผู้จัดà¸à¸²à¸£ + default_role_developer: ผู้พัฒนา + default_role_reporter: ผู้รายงาน + default_tracker_bug: บั๊ภ+ default_tracker_feature: ลัà¸à¸©à¸“ะเด่น + default_tracker_support: สนับสนุน + default_issue_status_new: เà¸à¸´à¸”ขึ้น + default_issue_status_in_progress: In Progress + default_issue_status_resolved: ดำเนินà¸à¸²à¸£ + default_issue_status_feedback: รอคำตอบ + default_issue_status_closed: จบ + default_issue_status_rejected: ยà¸à¹€à¸¥à¸´à¸ + default_doc_category_user: เอà¸à¸ªà¸²à¸£à¸‚องผู้ใช้ + default_doc_category_tech: เอà¸à¸ªà¸²à¸£à¸—างเทคนิค + default_priority_low: ต่ำ + default_priority_normal: ปà¸à¸•ิ + default_priority_high: สูง + default_priority_urgent: เร่งด่วน + default_priority_immediate: ด่วนมาภ+ default_activity_design: ออà¸à¹à¸šà¸š + default_activity_development: พัฒนา + + enumeration_issue_priorities: ความสำคัà¸à¸‚องปัà¸à¸«à¸² + enumeration_doc_categories: ประเภทเอà¸à¸ªà¸²à¸£ + enumeration_activities: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡ (ใช้ในà¸à¸²à¸£à¸•ิดตามเวลา) + label_and_its_subprojects: "%{value} and its subprojects" + mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:" + mail_subject_reminder: "%{count} issue(s) due in the next %{days} days" + text_user_wrote: "%{value} wrote:" + label_duplicated_by: duplicated by + setting_enabled_scm: Enabled SCM + text_enumeration_category_reassign_to: 'Reassign them to this value:' + text_enumeration_destroy_question: "%{count} objects are assigned to this value." + label_incoming_emails: Incoming emails + label_generate_key: Generate a key + setting_mail_handler_api_enabled: Enable WS for incoming emails + setting_mail_handler_api_key: API key + text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/configuration.yml and restart the application to enable them." + field_parent_title: Parent page + label_issue_watchers: Watchers + button_quote: Quote + setting_sequential_project_identifiers: Generate sequential project identifiers + notice_unable_delete_version: Unable to delete version + label_renamed: renamed + label_copied: copied + setting_plain_text_mail: plain text only (no HTML) + permission_view_files: View files + permission_edit_issues: Edit issues + permission_edit_own_time_entries: Edit own time logs + permission_manage_public_queries: Manage public queries + permission_add_issues: Add issues + permission_log_time: Log spent time + permission_view_changesets: View changesets + permission_view_time_entries: View spent time + permission_manage_versions: Manage versions + permission_manage_wiki: Manage wiki + permission_manage_categories: Manage issue categories + permission_protect_wiki_pages: Protect wiki pages + permission_comment_news: Comment news + permission_delete_messages: Delete messages + permission_select_project_modules: Select project modules + permission_manage_documents: Manage documents + permission_edit_wiki_pages: Edit wiki pages + permission_add_issue_watchers: Add watchers + permission_view_gantt: View gantt chart + permission_move_issues: Move issues + permission_manage_issue_relations: Manage issue relations + permission_delete_wiki_pages: Delete wiki pages + permission_manage_boards: Manage boards + permission_delete_wiki_pages_attachments: Delete attachments + permission_view_wiki_edits: View wiki history + permission_add_messages: Post messages + permission_view_messages: View messages + permission_manage_files: Manage files + permission_edit_issue_notes: Edit notes + permission_manage_news: Manage news + permission_view_calendar: View calendrier + permission_manage_members: Manage members + permission_edit_messages: Edit messages + permission_delete_issues: Delete issues + permission_view_issue_watchers: View watchers list + permission_manage_repository: Manage repository + permission_commit_access: Commit access + permission_browse_repository: Browse repository + permission_view_documents: View documents + permission_edit_project: Edit project + permission_add_issue_notes: Add notes + permission_save_queries: Save queries + permission_view_wiki_pages: View wiki + permission_rename_wiki_pages: Rename wiki pages + permission_edit_time_entries: Edit time logs + permission_edit_own_issue_notes: Edit own notes + setting_gravatar_enabled: Use Gravatar user icons + label_example: Example + text_repository_usernames_mapping: "Select ou update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped." + permission_edit_own_messages: Edit own messages + permission_delete_own_messages: Delete own messages + label_user_activity: "%{value}'s activity" + label_updated_time_by: "Updated by %{author} %{age} ago" + text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.' + setting_diff_max_lines_displayed: Max number of diff lines displayed + text_plugin_assets_writable: Plugin assets directory writable + warning_attachments_not_saved: "%{count} file(s) could not be saved." + button_create_and_continue: Create and continue + text_custom_field_possible_values_info: 'One line for each value' + label_display: Display + field_editable: Editable + setting_repository_log_display_limit: Maximum number of revisions displayed on file log + setting_file_max_size_displayed: Max size of text files displayed inline + field_watcher: Watcher + setting_openid: Allow OpenID login and registration + field_identity_url: OpenID URL + label_login_with_open_id_option: or login with OpenID + field_content: Content + label_descending: Descending + label_sort: Sort + label_ascending: Ascending + label_date_from_to: From %{start} to %{end} + label_greater_or_equal: ">=" + label_less_or_equal: <= + text_wiki_page_destroy_question: This page has %{descendants} child page(s) and descendant(s). What do you want to do? + text_wiki_page_reassign_children: Reassign child pages to this parent page + text_wiki_page_nullify_children: Keep child pages as root pages + text_wiki_page_destroy_children: Delete child pages and all their descendants + setting_password_min_length: Minimum password length + field_group_by: Group results by + mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated" + label_wiki_content_added: Wiki page added + mail_subject_wiki_content_added: "'%{id}' wiki page has been added" + mail_body_wiki_content_added: The '%{id}' wiki page has been added by %{author}. + label_wiki_content_updated: Wiki page updated + mail_body_wiki_content_updated: The '%{id}' wiki page has been updated by %{author}. + permission_add_project: Create project + setting_new_project_user_role_id: Role given to a non-admin user who creates a project + label_view_all_revisions: View all revisions + label_tag: Tag + label_branch: Branch + error_no_tracker_in_project: No tracker is associated to this project. Please check the Project settings. + error_no_default_issue_status: No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses"). + text_journal_changed: "%{label} changed from %{old} to %{new}" + text_journal_set_to: "%{label} set to %{value}" + text_journal_deleted: "%{label} deleted (%{old})" + label_group_plural: Groups + label_group: Group + label_group_new: New group + label_time_entry_plural: Spent time + text_journal_added: "%{label} %{value} added" + field_active: Active + enumeration_system_activity: System Activity + permission_delete_issue_watchers: Delete watchers + version_status_closed: closed + version_status_locked: locked + version_status_open: open + error_can_not_reopen_issue_on_closed_version: An issue assigned to a closed version can not be reopened + label_user_anonymous: Anonymous + button_move_and_follow: Move and follow + setting_default_projects_modules: Default enabled modules for new projects + setting_gravatar_default: Default Gravatar image + field_sharing: Sharing + label_version_sharing_hierarchy: With project hierarchy + label_version_sharing_system: With all projects + label_version_sharing_descendants: With subprojects + label_version_sharing_tree: With project tree + label_version_sharing_none: Not shared + error_can_not_archive_project: This project can not be archived + button_duplicate: Duplicate + button_copy_and_follow: Copy and follow + label_copy_source: Source + setting_issue_done_ratio: Calculate the issue done ratio with + setting_issue_done_ratio_issue_status: Use the issue status + error_issue_done_ratios_not_updated: Issue done ratios not updated. + error_workflow_copy_target: Please select target tracker(s) and role(s) + setting_issue_done_ratio_issue_field: Use the issue field + label_copy_same_as_target: Same as target + label_copy_target: Target + notice_issue_done_ratios_updated: Issue done ratios updated. + error_workflow_copy_source: Please select a source tracker or role + label_update_issue_done_ratios: Update issue done ratios + setting_start_of_week: Start calendars on + permission_view_issues: View Issues + label_display_used_statuses_only: Only display statuses that are used by this tracker + label_revision_id: Revision %{value} + label_api_access_key: API access key + label_api_access_key_created_on: API access key created %{value} ago + label_feeds_access_key: RSS access key + notice_api_access_key_reseted: Your API access key was reset. + setting_rest_api_enabled: Enable REST web service + label_missing_api_access_key: Missing an API access key + label_missing_feeds_access_key: Missing a RSS access key + button_show: Show + text_line_separated: Multiple values allowed (one line for each value). + setting_mail_handler_body_delimiters: Truncate emails after one of these lines + permission_add_subprojects: Create subprojects + label_subproject_new: New subproject + text_own_membership_delete_confirmation: |- + You are about to remove some or all of your permissions and may no longer be able to edit this project after that. + Are you sure you want to continue? + label_close_versions: Close completed versions + label_board_sticky: Sticky + label_board_locked: Locked + permission_export_wiki_pages: Export wiki pages + setting_cache_formatted_text: Cache formatted text + permission_manage_project_activities: Manage project activities + error_unable_delete_issue_status: Unable to delete issue status + label_profile: Profile + permission_manage_subtasks: Manage subtasks + field_parent_issue: Parent task + label_subtask_plural: Subtasks + label_project_copy_notifications: Send email notifications during the project copy + error_can_not_delete_custom_field: Unable to delete custom field + error_unable_to_connect: Unable to connect (%{value}) + error_can_not_remove_role: This role is in use and can not be deleted. + error_can_not_delete_tracker: This tracker contains issues and can't be deleted. + field_principal: Principal + label_my_page_block: My page block + notice_failed_to_save_members: "Failed to save member(s): %{errors}." + text_zoom_out: Zoom out + text_zoom_in: Zoom in + notice_unable_delete_time_entry: Unable to delete time log entry. + label_overall_spent_time: Overall spent time + field_time_entries: Log time + project_module_gantt: Gantt + project_module_calendar: Calendar + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Text field + label_user_mail_option_only_owner: Only for things I am the owner of + setting_default_notification_option: Default notification option + label_user_mail_option_only_my_events: Only for things I watch or I'm involved in + label_user_mail_option_only_assigned: Only for things I am assigned to + label_user_mail_option_none: No events + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + notice_not_authorized_archived_project: The project you're trying to access has been archived. + label_principal_search: "Search for user or group:" + label_user_search: "Search for user:" + field_visible: Visible + setting_emails_header: Emails header + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Enable time logging + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maximum number of items displayed on the gantt chart + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: Priority updated + label_issues_visibility_own: Issues created by or assigned to the user + field_issues_visibility: Issues visibility + label_issues_visibility_all: All issues + permission_set_own_issues_private: Set own issues public or private + field_is_private: Private + permission_set_issues_private: Set issues public or private + label_issues_visibility_public: All non private issues + text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). + field_commit_logs_encoding: Commit messages encoding + field_scm_path_encoding: Path encoding + text_scm_path_encoding_note: "Default: UTF-8" + field_path_to_repository: Path to repository + field_root_directory: Root directory + field_cvs_module: Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Command + text_scm_command_version: Version + label_git_report_last_commit: Report last commit for files and directories + text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 ปัà¸à¸«à¸² + one: 1 ปัà¸à¸«à¸² + other: "%{count} ปัà¸à¸«à¸²" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: ทั้งหมด + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: With subprojects + label_cross_project_tree: With project tree + label_cross_project_hierarchy: With project hierarchy + label_cross_project_system: With all projects + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1b/1bfd626854b349f73d46fb411da252c045cca8a7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1b/1bfd626854b349f73d46fb411da252c045cca8a7.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,18 @@ +sqlite3: + adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3 + database: awesome_nested_set.sqlite3.db +sqlite3mem: + adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3 + database: ":memory:" +postgresql: + adapter: postgresql + username: postgres + password: postgres + database: awesome_nested_set_plugin_test + min_messages: ERROR +mysql: + adapter: mysql2 + host: localhost + username: root + password: + database: awesome_nested_set_plugin_test \ No newline at end of file diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1c/1c38bd1806aa4d1bf50821953dfb345b483fc9ed.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1c/1c38bd1806aa4d1bf50821953dfb345b483fc9ed.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,40 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class PatchesTest < ActiveSupport::TestCase + include Redmine::I18n + + context "ActiveRecord::Base.human_attribute_name" do + setup do + Setting.default_language = 'en' + end + + should "transform name to field_name" do + assert_equal l('field_last_login_on'), ActiveRecord::Base.human_attribute_name('last_login_on') + end + + should "cut extra _id suffix for better validation" do + assert_equal l('field_last_login_on'), ActiveRecord::Base.human_attribute_name('last_login_on_id') + end + + should "default to humanized value if no translation has been found (useful for custom fields)" do + assert_equal 'Patch name', ActiveRecord::Base.human_attribute_name('Patch name') + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1c/1ca8f284dbc1327ae70fe2ea5473249d28c671c0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1c/1ca8f284dbc1327ae70fe2ea5473249d28c671c0.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,90 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class NewsTest < ActiveSupport::TestCase + fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news + + def valid_news + { :title => 'Test news', :description => 'Lorem ipsum etc', :author => User.find(:first) } + end + + def setup + end + + def test_create_should_send_email_notification + ActionMailer::Base.deliveries.clear + news = Project.find(1).news.new(valid_news) + + with_settings :notified_events => %w(news_added) do + assert news.save + end + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_should_include_news_for_projects_with_news_enabled + project = projects(:projects_001) + assert project.enabled_modules.any?{ |em| em.name == 'news' } + + # News.latest should return news from projects_001 + assert News.latest.any? { |news| news.project == project } + end + + def test_should_not_include_news_for_projects_with_news_disabled + EnabledModule.delete_all(["project_id = ? AND name = ?", 2, 'news']) + project = Project.find(2) + + # Add a piece of news to the project + news = project.news.create(valid_news) + + # News.latest should not return that new piece of news + assert News.latest.include?(news) == false + end + + def test_should_only_include_news_from_projects_visibly_to_the_user + assert News.latest(User.anonymous).all? { |news| news.project.is_public? } + end + + def test_should_limit_the_amount_of_returned_news + # Make sure we have a bunch of news stories + 10.times { projects(:projects_001).news.create(valid_news) } + assert_equal 2, News.latest(users(:users_002), 2).size + assert_equal 6, News.latest(users(:users_002), 6).size + end + + def test_should_return_5_news_stories_by_default + # Make sure we have a bunch of news stories + 10.times { projects(:projects_001).news.create(valid_news) } + assert_equal 5, News.latest(users(:users_004)).size + end + + def test_attachments_should_be_visible + assert News.find(1).attachments_visible?(User.anonymous) + end + + def test_attachments_should_be_deletable_with_manage_news_permission + manager = User.find(2) + assert News.find(1).attachments_deletable?(manager) + end + + def test_attachments_should_not_be_deletable_without_manage_news_permission + manager = User.find(2) + Role.find_by_name('Manager').remove_permission!(:manage_news) + assert !News.find(1).attachments_deletable?(manager) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1c/1cbba0550c238f5d4d1ebf5a5bba5abec6d40d7d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1c/1cbba0550c238f5d4d1ebf5a5bba5abec6d40d7d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,5 @@ +From: =?utf-8?b?w4TDpCDDlsO2?= +Subject: foo +Content-Type: text/plain; charset=utf-8 + +testing user creation with quoted From-header diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1c/1ce7c095bdee57845b24106ac69ceda35b831a10.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1c/1ce7c095bdee57845b24106ac69ceda35b831a10.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,12 @@ +

    <%= link_to l(:label_user_plural), users_path %> » <%=l(:label_user_new)%>

    + +<%= labelled_form_for @user do |f| %> + <%= render :partial => 'form', :locals => { :f => f } %> + <% if email_delivery_enabled? %> +

    + <% end %> +

    + <%= submit_tag l(:button_create) %> + <%= submit_tag l(:button_create_and_continue), :name => 'continue' %> +

    +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1d/1d23ef14041c621865426d5030ff2b1bdf3b4a55.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1d/1d23ef14041c621865426d5030ff2b1bdf3b4a55.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,291 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/abstract_adapter' +require 'uri' + +module Redmine + module Scm + module Adapters + class SubversionAdapter < AbstractAdapter + + # SVN executable name + SVN_BIN = Redmine::Configuration['scm_subversion_command'] || "svn" + + class << self + def client_command + @@bin ||= SVN_BIN + end + + def sq_bin + @@sq_bin ||= shell_quote_command + end + + def client_version + @@client_version ||= (svn_binary_version || []) + end + + def client_available + # --xml options are introduced in 1.3. + # http://subversion.apache.org/docs/release-notes/1.3.html + client_version_above?([1, 3]) + end + + def svn_binary_version + scm_version = scm_version_from_command_line.dup + if scm_version.respond_to?(:force_encoding) + scm_version.force_encoding('ASCII-8BIT') + end + if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}) + m[2].scan(%r{\d+}).collect(&:to_i) + end + end + + def scm_version_from_command_line + shellout("#{sq_bin} --version") { |io| io.read }.to_s + end + end + + # Get info about the svn repository + def info + cmd = "#{self.class.sq_bin} info --xml #{target}" + cmd << credentials_string + info = nil + shellout(cmd) do |io| + output = io.read + if output.respond_to?(:force_encoding) + output.force_encoding('UTF-8') + end + begin + doc = parse_xml(output) + # root_url = doc.elements["info/entry/repository/root"].text + info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'], + :lastrev => Revision.new({ + :identifier => doc['info']['entry']['commit']['revision'], + :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime, + :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "") + }) + }) + rescue + end + end + return nil if $? && $?.exitstatus != 0 + info + rescue CommandFailed + return nil + end + + # Returns an Entries collection + # or nil if the given path doesn't exist in the repository + def entries(path=nil, identifier=nil, options={}) + path ||= '' + identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" + entries = Entries.new + cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}" + cmd << credentials_string + shellout(cmd) do |io| + output = io.read + if output.respond_to?(:force_encoding) + output.force_encoding('UTF-8') + end + begin + doc = parse_xml(output) + each_xml_element(doc['lists']['list'], 'entry') do |entry| + commit = entry['commit'] + commit_date = commit['date'] + # Skip directory if there is no commit date (usually that + # means that we don't have read access to it) + next if entry['kind'] == 'dir' && commit_date.nil? + name = entry['name']['__content__'] + entries << Entry.new({:name => URI.unescape(name), + :path => ((path.empty? ? "" : "#{path}/") + name), + :kind => entry['kind'], + :size => ((s = entry['size']) ? s['__content__'].to_i : nil), + :lastrev => Revision.new({ + :identifier => commit['revision'], + :time => Time.parse(commit_date['__content__'].to_s).localtime, + :author => ((a = commit['author']) ? a['__content__'] : nil) + }) + }) + end + rescue Exception => e + logger.error("Error parsing svn output: #{e.message}") + logger.error("Output was:\n #{output}") + end + end + return nil if $? && $?.exitstatus != 0 + logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug? + entries.sort_by_name + end + + def properties(path, identifier=nil) + # proplist xml output supported in svn 1.5.0 and higher + return nil unless self.class.client_version_above?([1, 5, 0]) + + identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" + cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}" + cmd << credentials_string + properties = {} + shellout(cmd) do |io| + output = io.read + if output.respond_to?(:force_encoding) + output.force_encoding('UTF-8') + end + begin + doc = parse_xml(output) + each_xml_element(doc['properties']['target'], 'property') do |property| + properties[ property['name'] ] = property['__content__'].to_s + end + rescue + end + end + return nil if $? && $?.exitstatus != 0 + properties + end + + def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) + path ||= '' + identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD" + identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1 + revisions = Revisions.new + cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}" + cmd << credentials_string + cmd << " --verbose " if options[:with_paths] + cmd << " --limit #{options[:limit].to_i}" if options[:limit] + cmd << ' ' + target(path) + shellout(cmd) do |io| + output = io.read + if output.respond_to?(:force_encoding) + output.force_encoding('UTF-8') + end + begin + doc = parse_xml(output) + each_xml_element(doc['log'], 'logentry') do |logentry| + paths = [] + each_xml_element(logentry['paths'], 'path') do |path| + paths << {:action => path['action'], + :path => path['__content__'], + :from_path => path['copyfrom-path'], + :from_revision => path['copyfrom-rev'] + } + end if logentry['paths'] && logentry['paths']['path'] + paths.sort! { |x,y| x[:path] <=> y[:path] } + + revisions << Revision.new({:identifier => logentry['revision'], + :author => (logentry['author'] ? logentry['author']['__content__'] : ""), + :time => Time.parse(logentry['date']['__content__'].to_s).localtime, + :message => logentry['msg']['__content__'], + :paths => paths + }) + end + rescue + end + end + return nil if $? && $?.exitstatus != 0 + revisions + end + + def diff(path, identifier_from, identifier_to=nil) + path ||= '' + identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : '' + + identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1) + + cmd = "#{self.class.sq_bin} diff -r " + cmd << "#{identifier_to}:" + cmd << "#{identifier_from}" + cmd << " #{target(path)}@#{identifier_from}" + cmd << credentials_string + diff = [] + shellout(cmd) do |io| + io.each_line do |line| + diff << line + end + end + return nil if $? && $?.exitstatus != 0 + diff + end + + def cat(path, identifier=nil) + identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" + cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}" + cmd << credentials_string + cat = nil + shellout(cmd) do |io| + io.binmode + cat = io.read + end + return nil if $? && $?.exitstatus != 0 + cat + end + + def annotate(path, identifier=nil) + identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" + cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}" + cmd << credentials_string + blame = Annotate.new + shellout(cmd) do |io| + io.each_line do |line| + next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$} + rev = $1 + blame.add_line($3.rstrip, + Revision.new( + :identifier => rev, + :revision => rev, + :author => $2.strip + )) + end + end + return nil if $? && $?.exitstatus != 0 + blame + end + + private + + def credentials_string + str = '' + str << " --username #{shell_quote(@login)}" unless @login.blank? + str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank? + str << " --no-auth-cache --non-interactive" + str + end + + # Helper that iterates over the child elements of a xml node + # MiniXml returns a hash when a single child is found + # or an array of hashes for multiple children + def each_xml_element(node, name) + if node && node[name] + if node[name].is_a?(Hash) + yield node[name] + else + node[name].each do |element| + yield element + end + end + end + end + + def target(path = '') + base = path.match(/^\//) ? root_url : url + uri = "#{base}/#{path}" + uri = URI.escape(URI.escape(uri), '[]') + shell_quote(uri.gsub(/[?<>\*]/, '')) + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1d/1d4c356684e7bc2f59df7e93707ab136f908ac26.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1d/1d4c356684e7bc2f59df7e93707ab136f908ac26.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,27 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingAutoCompletesTest < ActionController::IntegrationTest + def test_auto_completes + assert_routing( + { :method => 'get', :path => "/issues/auto_complete" }, + { :controller => 'auto_completes', :action => 'issues' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1e/1e36d3b0b9a7b1dcdb385f344cf25506fbe7bce9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1e/1e36d3b0b9a7b1dcdb385f344cf25506fbe7bce9.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,6 @@ +

    <%=l(:label_auth_source)%> (<%= h(@auth_source.auth_method_name) %>)

    + +<%= form_tag({:action => 'update', :id => @auth_source}, :method => :put, :class => "tabular") do %> + <%= render :partial => auth_source_partial_name(@auth_source) %> + <%= submit_tag l(:button_save) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1e/1e8f4706c992f648980e1bbef5b267351fd0e7ee.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1e/1e8f4706c992f648980e1bbef5b267351fd0e7ee.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,32 @@ + + + + + + <% if tab[:name] == 'IssueCustomField' %> + + + <% end %> + + + + + <% (@custom_fields_by_type[tab[:name]] || []).sort.each do |custom_field| -%> + "> + + + + <% if tab[:name] == 'IssueCustomField' %> + + + <% end %> + + + + <% end; reset_cycle %> + +
    <%=l(:field_name)%><%=l(:field_field_format)%><%=l(:field_is_required)%><%=l(:field_is_for_all)%><%=l(:label_used_by)%><%=l(:button_sort)%>
    <%= link_to h(custom_field.name), edit_custom_field_path(custom_field) %><%= l(Redmine::CustomFieldFormat.label_for(custom_field.field_format)) %><%= checked_image custom_field.is_required? %><%= checked_image custom_field.is_for_all? %><%= l(:label_x_projects, :count => custom_field.projects.count) if custom_field.is_a? IssueCustomField and !custom_field.is_for_all? %><%= reorder_links('custom_field', {:action => 'update', :id => custom_field}, :put) %> + <%= delete_link custom_field_path(custom_field) %> +
    + +

    <%= link_to l(:label_custom_field_new), new_custom_field_path(:type => tab[:name]), :class => 'icon icon-add' %>

    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1e/1ec171c5d371f7815766568fcf1c2d5feb1a3958.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1e/1ec171c5d371f7815766568fcf1c2d5feb1a3958.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,13 @@ +<%= "#{issue.tracker.name} ##{issue.id}: #{issue.subject}" %> +<%= issue_url %> + +* <%=l(:field_author)%>: <%= issue.author %> +* <%=l(:field_status)%>: <%= issue.status %> +* <%=l(:field_priority)%>: <%= issue.priority %> +* <%=l(:field_assigned_to)%>: <%= issue.assigned_to %> +* <%=l(:field_category)%>: <%= issue.category %> +* <%=l(:field_fixed_version)%>: <%= issue.fixed_version %> +<% issue.custom_field_values.each do |c| %>* <%= c.custom_field.name %>: <%= show_value(c) %> +<% end -%> +---------------------------------------- +<%= issue.description %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1f/1f1d77f06e8e92bd47f7239a03b9fe5b586376af.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1f/1f1d77f06e8e92bd47f7239a03b9fe5b586376af.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,30 @@ +api.group do + api.id @group.id + api.name @group.lastname + + render_api_custom_values @group.visible_custom_field_values, api + + api.array :users do + @group.users.each do |user| + api.user :id => user.id, :name => user.name + end + end if include_in_api_response?('users') + + api.array :memberships do + @group.memberships.each do |membership| + api.membership do + api.id membership.id + api.project :id => membership.project.id, :name => membership.project.name + api.array :roles do + membership.member_roles.each do |member_role| + if member_role.role + attrs = {:id => member_role.role.id, :name => member_role.role.name} + attrs.merge!(:inherited => true) if member_role.inherited_from.present? + api.role attrs + end + end + end + end if membership.project + end + end if include_in_api_response?('memberships') +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1f/1f2100d78a901e27c2b48a6bdb60bbd56653d6f4.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1f/1f2100d78a901e27c2b48a6bdb60bbd56653d6f4.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,76 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class AuthSourcesController < ApplicationController + layout 'admin' + menu_item :ldap_authentication + + before_filter :require_admin + + def index + @auth_source_pages, @auth_sources = paginate AuthSource, :per_page => 10 + end + + def new + klass_name = params[:type] || 'AuthSourceLdap' + @auth_source = AuthSource.new_subclass_instance(klass_name, params[:auth_source]) + end + + def create + @auth_source = AuthSource.new_subclass_instance(params[:type], params[:auth_source]) + if @auth_source.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'index' + else + render :action => 'new' + end + end + + def edit + @auth_source = AuthSource.find(params[:id]) + end + + def update + @auth_source = AuthSource.find(params[:id]) + if @auth_source.update_attributes(params[:auth_source]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'index' + else + render :action => 'edit' + end + end + + def test_connection + @auth_source = AuthSource.find(params[:id]) + begin + @auth_source.test_connection + flash[:notice] = l(:notice_successful_connection) + rescue Exception => e + flash[:error] = l(:error_unable_to_connect, e.message) + end + redirect_to :action => 'index' + end + + def destroy + @auth_source = AuthSource.find(params[:id]) + unless @auth_source.users.find(:first) + @auth_source.destroy + flash[:notice] = l(:notice_successful_delete) + end + redirect_to :action => 'index' + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1f/1f612d7afe539ef65bf896738c3265c445e475f7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1f/1f612d7afe539ef65bf896738c3265c445e475f7.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,103 @@ +--- +enumerations_001: + name: Uncategorized + id: 1 + type: DocumentCategory + active: true + position: 1 +enumerations_002: + name: User documentation + id: 2 + type: DocumentCategory + active: true + position: 2 +enumerations_003: + name: Technical documentation + id: 3 + type: DocumentCategory + active: true + position: 3 +enumerations_004: + name: Low + id: 4 + type: IssuePriority + active: true + position: 1 + position_name: lowest +enumerations_005: + name: Normal + id: 5 + type: IssuePriority + is_default: true + active: true + position: 2 + position_name: default +enumerations_006: + name: High + id: 6 + type: IssuePriority + active: true + position: 3 + position_name: high3 +enumerations_007: + name: Urgent + id: 7 + type: IssuePriority + active: true + position: 4 + position_name: high2 +enumerations_008: + name: Immediate + id: 8 + type: IssuePriority + active: true + position: 5 + position_name: highest +enumerations_009: + name: Design + id: 9 + type: TimeEntryActivity + position: 1 + active: true +enumerations_010: + name: Development + id: 10 + type: TimeEntryActivity + position: 2 + is_default: true + active: true +enumerations_011: + name: QA + id: 11 + type: TimeEntryActivity + position: 3 + active: true +enumerations_012: + name: Default Enumeration + id: 12 + type: Enumeration + is_default: true + active: true +enumerations_013: + name: Another Enumeration + id: 13 + type: Enumeration + active: true +enumerations_014: + name: Inactive Activity + id: 14 + type: TimeEntryActivity + position: 4 + active: false +enumerations_015: + name: Inactive Priority + id: 15 + type: IssuePriority + position: 6 + active: false +enumerations_016: + name: Inactive Document Category + id: 16 + type: DocumentCategory + active: false + position: 4 diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1f/1f801059ed33453ec7884c4bce2b26f547910719.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1f/1f801059ed33453ec7884c4bce2b26f547910719.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,16 @@ +

    <%=l(:label_attachment_new)%>

    + +<%= error_messages_for 'attachment' %> +<%= form_tag(project_files_path(@project), :multipart => true, :class => "tabular") do %> +
    + +<% if @versions.any? %> +

    +<%= select_tag "version_id", content_tag('option', '') + + options_from_collection_for_select(@versions, "id", "name") %>

    +<% end %> + +

    <%= render :partial => 'attachments/form' %>

    +
    +<%= submit_tag l(:button_add) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1f/1fbae96ef6943c3bcd6afe5e00988dd05b87165c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1f/1fbae96ef6943c3bcd6afe5e00988dd05b87165c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,81 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class IssueStatusesController < ApplicationController + layout 'admin' + + before_filter :require_admin, :except => :index + before_filter :require_admin_or_api_request, :only => :index + accept_api_auth :index + + def index + respond_to do |format| + format.html { + @issue_status_pages, @issue_statuses = paginate :issue_statuses, :per_page => 25, :order => "position" + render :action => "index", :layout => false if request.xhr? + } + format.api { + @issue_statuses = IssueStatus.all(:order => 'position') + } + end + end + + def new + @issue_status = IssueStatus.new + end + + def create + @issue_status = IssueStatus.new(params[:issue_status]) + if request.post? && @issue_status.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'index' + else + render :action => 'new' + end + end + + def edit + @issue_status = IssueStatus.find(params[:id]) + end + + def update + @issue_status = IssueStatus.find(params[:id]) + if request.put? && @issue_status.update_attributes(params[:issue_status]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'index' + else + render :action => 'edit' + end + end + + def destroy + IssueStatus.find(params[:id]).destroy + redirect_to :action => 'index' + rescue + flash[:error] = l(:error_unable_delete_issue_status) + redirect_to :action => 'index' + end + + def update_issue_done_ratio + if request.post? && IssueStatus.update_issue_done_ratios + flash[:notice] = l(:notice_issue_done_ratios_updated) + else + flash[:error] = l(:error_issue_done_ratios_not_updated) + end + redirect_to :action => 'index' + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1f/1fd17c4ff264e9a3622193c42c35440f9c0f7904.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1f/1fd17c4ff264e9a3622193c42c35440f9c0f7904.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1 @@ +$('#tab-content-users').html('<%= escape_javascript(render :partial => 'groups/users') %>'); diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1f/1fe3164d40b2f175bf0aa2fdbd0804dbea4969a1.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1f/1fe3164d40b2f175bf0aa2fdbd0804dbea4969a1.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,578 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) +require 'projects_controller' + +# Re-raise errors caught by the controller. +class ProjectsController; def rescue_action(e) raise e end; end + +class ProjectsControllerTest < ActionController::TestCase + fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details, + :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages, + :attachments, :custom_fields, :custom_values, :time_entries + + def setup + @controller = ProjectsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @request.session[:user_id] = nil + Setting.default_language = 'en' + end + + def test_index + get :index + assert_response :success + assert_template 'index' + assert_not_nil assigns(:projects) + + assert_tag :ul, :child => {:tag => 'li', + :descendant => {:tag => 'a', :content => 'eCookbook'}, + :child => { :tag => 'ul', + :descendant => { :tag => 'a', + :content => 'Child of private child' + } + } + } + + assert_no_tag :a, :content => /Private child of eCookbook/ + end + + def test_index_atom + get :index, :format => 'atom' + assert_response :success + assert_template 'common/feed' + assert_select 'feed>title', :text => 'Redmine: Latest projects' + assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_condition(User.current)) + end + + context "#index" do + context "by non-admin user with view_time_entries permission" do + setup do + @request.session[:user_id] = 3 + end + should "show overall spent time link" do + get :index + assert_template 'index' + assert_tag :a, :attributes => {:href => '/time_entries'} + end + end + + context "by non-admin user without view_time_entries permission" do + setup do + Role.find(2).remove_permission! :view_time_entries + Role.non_member.remove_permission! :view_time_entries + Role.anonymous.remove_permission! :view_time_entries + @request.session[:user_id] = 3 + end + should "not show overall spent time link" do + get :index + assert_template 'index' + assert_no_tag :a, :attributes => {:href => '/time_entries'} + end + end + end + + context "#new" do + context "by admin user" do + setup do + @request.session[:user_id] = 1 + end + + should "accept get" do + get :new + assert_response :success + assert_template 'new' + end + + end + + context "by non-admin user with add_project permission" do + setup do + Role.non_member.add_permission! :add_project + @request.session[:user_id] = 9 + end + + should "accept get" do + get :new + assert_response :success + assert_template 'new' + assert_no_tag :select, :attributes => {:name => 'project[parent_id]'} + end + end + + context "by non-admin user with add_subprojects permission" do + setup do + Role.find(1).remove_permission! :add_project + Role.find(1).add_permission! :add_subprojects + @request.session[:user_id] = 2 + end + + should "accept get" do + get :new, :parent_id => 'ecookbook' + assert_response :success + assert_template 'new' + # parent project selected + assert_tag :select, :attributes => {:name => 'project[parent_id]'}, + :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}} + # no empty value + assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}, + :child => {:tag => 'option', :attributes => {:value => ''}} + end + end + + end + + context "POST :create" do + context "by admin user" do + setup do + @request.session[:user_id] = 1 + end + + should "create a new project" do + post :create, + :project => { + :name => "blog", + :description => "weblog", + :homepage => 'http://weblog', + :identifier => "blog", + :is_public => 1, + :custom_field_values => { '3' => 'Beta' }, + :tracker_ids => ['1', '3'], + # an issue custom field that is not for all project + :issue_custom_field_ids => ['9'], + :enabled_module_names => ['issue_tracking', 'news', 'repository'] + } + assert_redirected_to '/projects/blog/settings' + + project = Project.find_by_name('blog') + assert_kind_of Project, project + assert project.active? + assert_equal 'weblog', project.description + assert_equal 'http://weblog', project.homepage + assert_equal true, project.is_public? + assert_nil project.parent + assert_equal 'Beta', project.custom_value_for(3).value + assert_equal [1, 3], project.trackers.map(&:id).sort + assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort + assert project.issue_custom_fields.include?(IssueCustomField.find(9)) + end + + should "create a new subproject" do + post :create, :project => { :name => "blog", + :description => "weblog", + :identifier => "blog", + :is_public => 1, + :custom_field_values => { '3' => 'Beta' }, + :parent_id => 1 + } + assert_redirected_to '/projects/blog/settings' + + project = Project.find_by_name('blog') + assert_kind_of Project, project + assert_equal Project.find(1), project.parent + end + + should "continue" do + assert_difference 'Project.count' do + post :create, :project => {:name => "blog", :identifier => "blog"}, :continue => 'Create and continue' + end + assert_redirected_to '/projects/new?' + end + end + + context "by non-admin user with add_project permission" do + setup do + Role.non_member.add_permission! :add_project + @request.session[:user_id] = 9 + end + + should "accept create a Project" do + post :create, :project => { :name => "blog", + :description => "weblog", + :identifier => "blog", + :is_public => 1, + :custom_field_values => { '3' => 'Beta' }, + :tracker_ids => ['1', '3'], + :enabled_module_names => ['issue_tracking', 'news', 'repository'] + } + + assert_redirected_to '/projects/blog/settings' + + project = Project.find_by_name('blog') + assert_kind_of Project, project + assert_equal 'weblog', project.description + assert_equal true, project.is_public? + assert_equal [1, 3], project.trackers.map(&:id).sort + assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort + + # User should be added as a project member + assert User.find(9).member_of?(project) + assert_equal 1, project.members.size + end + + should "fail with parent_id" do + assert_no_difference 'Project.count' do + post :create, :project => { :name => "blog", + :description => "weblog", + :identifier => "blog", + :is_public => 1, + :custom_field_values => { '3' => 'Beta' }, + :parent_id => 1 + } + end + assert_response :success + project = assigns(:project) + assert_kind_of Project, project + assert_not_nil project.errors[:parent_id] + end + end + + context "by non-admin user with add_subprojects permission" do + setup do + Role.find(1).remove_permission! :add_project + Role.find(1).add_permission! :add_subprojects + @request.session[:user_id] = 2 + end + + should "create a project with a parent_id" do + post :create, :project => { :name => "blog", + :description => "weblog", + :identifier => "blog", + :is_public => 1, + :custom_field_values => { '3' => 'Beta' }, + :parent_id => 1 + } + assert_redirected_to '/projects/blog/settings' + project = Project.find_by_name('blog') + end + + should "fail without parent_id" do + assert_no_difference 'Project.count' do + post :create, :project => { :name => "blog", + :description => "weblog", + :identifier => "blog", + :is_public => 1, + :custom_field_values => { '3' => 'Beta' } + } + end + assert_response :success + project = assigns(:project) + assert_kind_of Project, project + assert_not_nil project.errors[:parent_id] + end + + should "fail with unauthorized parent_id" do + assert !User.find(2).member_of?(Project.find(6)) + assert_no_difference 'Project.count' do + post :create, :project => { :name => "blog", + :description => "weblog", + :identifier => "blog", + :is_public => 1, + :custom_field_values => { '3' => 'Beta' }, + :parent_id => 6 + } + end + assert_response :success + project = assigns(:project) + assert_kind_of Project, project + assert_not_nil project.errors[:parent_id] + end + end + end + + def test_create_should_preserve_modules_on_validation_failure + with_settings :default_projects_modules => ['issue_tracking', 'repository'] do + @request.session[:user_id] = 1 + assert_no_difference 'Project.count' do + post :create, :project => { + :name => "blog", + :identifier => "", + :enabled_module_names => %w(issue_tracking news) + } + end + assert_response :success + project = assigns(:project) + assert_equal %w(issue_tracking news), project.enabled_module_names.sort + end + end + + def test_show_by_id + get :show, :id => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:project) + end + + def test_show_by_identifier + get :show, :id => 'ecookbook' + assert_response :success + assert_template 'show' + assert_not_nil assigns(:project) + assert_equal Project.find_by_identifier('ecookbook'), assigns(:project) + + assert_tag 'li', :content => /Development status/ + end + + def test_show_should_not_display_hidden_custom_fields + ProjectCustomField.find_by_name('Development status').update_attribute :visible, false + get :show, :id => 'ecookbook' + assert_response :success + assert_template 'show' + assert_not_nil assigns(:project) + + assert_no_tag 'li', :content => /Development status/ + end + + def test_show_should_not_fail_when_custom_values_are_nil + project = Project.find_by_identifier('ecookbook') + project.custom_values.first.update_attribute(:value, nil) + get :show, :id => 'ecookbook' + assert_response :success + assert_template 'show' + assert_not_nil assigns(:project) + assert_equal Project.find_by_identifier('ecookbook'), assigns(:project) + end + + def show_archived_project_should_be_denied + project = Project.find_by_identifier('ecookbook') + project.archive! + + get :show, :id => 'ecookbook' + assert_response 403 + assert_nil assigns(:project) + assert_tag :tag => 'p', :content => /archived/ + end + + def test_private_subprojects_hidden + get :show, :id => 'ecookbook' + assert_response :success + assert_template 'show' + assert_no_tag :tag => 'a', :content => /Private child/ + end + + def test_private_subprojects_visible + @request.session[:user_id] = 2 # manager who is a member of the private subproject + get :show, :id => 'ecookbook' + assert_response :success + assert_template 'show' + assert_tag :tag => 'a', :content => /Private child/ + end + + def test_settings + @request.session[:user_id] = 2 # manager + get :settings, :id => 1 + assert_response :success + assert_template 'settings' + end + + def test_settings_should_be_denied_for_member_on_closed_project + Project.find(1).close + @request.session[:user_id] = 2 # manager + + get :settings, :id => 1 + assert_response 403 + end + + def test_settings_should_be_denied_for_anonymous_on_closed_project + Project.find(1).close + + get :settings, :id => 1 + assert_response 302 + end + + def test_update + @request.session[:user_id] = 2 # manager + post :update, :id => 1, :project => {:name => 'Test changed name', + :issue_custom_field_ids => ['']} + assert_redirected_to '/projects/ecookbook/settings' + project = Project.find(1) + assert_equal 'Test changed name', project.name + end + + def test_update_with_failure + @request.session[:user_id] = 2 # manager + post :update, :id => 1, :project => {:name => ''} + assert_response :success + assert_template 'settings' + assert_error_tag :content => /name can't be blank/i + end + + def test_update_should_be_denied_for_member_on_closed_project + Project.find(1).close + @request.session[:user_id] = 2 # manager + + post :update, :id => 1, :project => {:name => 'Closed'} + assert_response 403 + assert_equal 'eCookbook', Project.find(1).name + end + + def test_update_should_be_denied_for_anonymous_on_closed_project + Project.find(1).close + + post :update, :id => 1, :project => {:name => 'Closed'} + assert_response 302 + assert_equal 'eCookbook', Project.find(1).name + end + + def test_modules + @request.session[:user_id] = 2 + Project.find(1).enabled_module_names = ['issue_tracking', 'news'] + + post :modules, :id => 1, :enabled_module_names => ['issue_tracking', 'repository', 'documents'] + assert_redirected_to '/projects/ecookbook/settings/modules' + assert_equal ['documents', 'issue_tracking', 'repository'], Project.find(1).enabled_module_names.sort + end + + def test_destroy_without_confirmation + @request.session[:user_id] = 1 # admin + delete :destroy, :id => 1 + assert_response :success + assert_template 'destroy' + assert_not_nil Project.find_by_id(1) + assert_tag :tag => 'strong', + :content => ['Private child of eCookbook', + 'Child of private child, eCookbook Subproject 1', + 'eCookbook Subproject 2'].join(', ') + end + + def test_destroy + @request.session[:user_id] = 1 # admin + delete :destroy, :id => 1, :confirm => 1 + assert_redirected_to '/admin/projects' + assert_nil Project.find_by_id(1) + end + + def test_archive + @request.session[:user_id] = 1 # admin + post :archive, :id => 1 + assert_redirected_to '/admin/projects' + assert !Project.find(1).active? + end + + def test_archive_with_failure + @request.session[:user_id] = 1 + Project.any_instance.stubs(:archive).returns(false) + post :archive, :id => 1 + assert_redirected_to '/admin/projects' + assert_match /project cannot be archived/i, flash[:error] + end + + def test_unarchive + @request.session[:user_id] = 1 # admin + Project.find(1).archive + post :unarchive, :id => 1 + assert_redirected_to '/admin/projects' + assert Project.find(1).active? + end + + def test_close + @request.session[:user_id] = 2 + post :close, :id => 1 + assert_redirected_to '/projects/ecookbook' + assert_equal Project::STATUS_CLOSED, Project.find(1).status + end + + def test_reopen + Project.find(1).close + @request.session[:user_id] = 2 + post :reopen, :id => 1 + assert_redirected_to '/projects/ecookbook' + assert Project.find(1).active? + end + + def test_project_breadcrumbs_should_be_limited_to_3_ancestors + CustomField.delete_all + parent = nil + 6.times do |i| + p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}") + p.set_parent!(parent) + get :show, :id => p + assert_tag :h1, :parent => { :attributes => {:id => 'header'}}, + :children => { :count => [i, 3].min, + :only => { :tag => 'a' } } + + parent = p + end + end + + def test_get_copy + @request.session[:user_id] = 1 # admin + get :copy, :id => 1 + assert_response :success + assert_template 'copy' + assert assigns(:project) + assert_equal Project.find(1).description, assigns(:project).description + assert_nil assigns(:project).id + + assert_tag :tag => 'input', + :attributes => {:name => 'project[enabled_module_names][]', :value => 'issue_tracking'} + end + + def test_get_copy_with_invalid_source_should_respond_with_404 + @request.session[:user_id] = 1 + get :copy, :id => 99 + assert_response 404 + end + + def test_post_copy_should_copy_requested_items + @request.session[:user_id] = 1 # admin + CustomField.delete_all + + assert_difference 'Project.count' do + post :copy, :id => 1, + :project => { + :name => 'Copy', + :identifier => 'unique-copy', + :tracker_ids => ['1', '2', '3', ''], + :enabled_module_names => %w(issue_tracking time_tracking) + }, + :only => %w(issues versions) + end + project = Project.find('unique-copy') + source = Project.find(1) + assert_equal %w(issue_tracking time_tracking), project.enabled_module_names.sort + + assert_equal source.versions.count, project.versions.count, "All versions were not copied" + assert_equal source.issues.count, project.issues.count, "All issues were not copied" + assert_equal 0, project.members.count + end + + def test_post_copy_should_redirect_to_settings_when_successful + @request.session[:user_id] = 1 # admin + post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'} + assert_response :redirect + assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy' + end + + def test_jump_should_redirect_to_active_tab + get :show, :id => 1, :jump => 'issues' + assert_redirected_to '/projects/ecookbook/issues' + end + + def test_jump_should_not_redirect_to_inactive_tab + get :show, :id => 3, :jump => 'documents' + assert_response :success + assert_template 'show' + end + + def test_jump_should_not_redirect_to_unknown_tab + get :show, :id => 3, :jump => 'foobar' + assert_response :success + assert_template 'show' + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/1f/1ff98a740ea3c4a225cc16263b1ad4e019e7465d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/1f/1ff98a740ea3c4a225cc16263b1ad4e019e7465d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,23 @@ +/* Brazilian initialisation for the jQuery UI date picker plugin. */ +/* Written by Leonildo Costa Silva (leocsilva@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['pt-BR'] = { + closeText: 'Fechar', + prevText: '<Anterior', + nextText: 'Próximo>', + currentText: 'Hoje', + monthNames: ['Janeiro','Fevereiro','Março','Abril','Maio','Junho', + 'Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'], + monthNamesShort: ['Jan','Fev','Mar','Abr','Mai','Jun', + 'Jul','Ago','Set','Out','Nov','Dez'], + dayNames: ['Domingo','Segunda-feira','Terça-feira','Quarta-feira','Quinta-feira','Sexta-feira','Sábado'], + dayNamesShort: ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'], + dayNamesMin: ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'], + weekHeader: 'Sm', + dateFormat: 'dd/mm/yy', + firstDay: 0, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['pt-BR']); +}); \ No newline at end of file diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/20/2006d03f57217f185b674373ebf261dbb0c3c839.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/20/2006d03f57217f185b674373ebf261dbb0c3c839.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,200 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class ApiTest::MembershipsTest < ActionController::IntegrationTest + fixtures :projects, :users, :roles, :members, :member_roles + + def setup + Setting.rest_api_enabled = '1' + end + + context "/projects/:project_id/memberships" do + context "GET" do + context "xml" do + should "return memberships" do + get '/projects/1/memberships.xml', {}, credentials('jsmith') + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'memberships', + :attributes => {:type => 'array'}, + :child => { + :tag => 'membership', + :child => { + :tag => 'id', + :content => '2', + :sibling => { + :tag => 'user', + :attributes => {:id => '3', :name => 'Dave Lopper'}, + :sibling => { + :tag => 'roles', + :child => { + :tag => 'role', + :attributes => {:id => '2', :name => 'Developer'} + } + } + } + } + } + end + end + + context "json" do + should "return memberships" do + get '/projects/1/memberships.json', {}, credentials('jsmith') + + assert_response :success + assert_equal 'application/json', @response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_equal({ + "memberships" => + [{"id"=>1, + "project" => {"name"=>"eCookbook", "id"=>1}, + "roles" => [{"name"=>"Manager", "id"=>1}], + "user" => {"name"=>"John Smith", "id"=>2}}, + {"id"=>2, + "project" => {"name"=>"eCookbook", "id"=>1}, + "roles" => [{"name"=>"Developer", "id"=>2}], + "user" => {"name"=>"Dave Lopper", "id"=>3}}], + "limit" => 25, + "total_count" => 2, + "offset" => 0}, + json) + end + end + end + + context "POST" do + context "xml" do + should "create membership" do + assert_difference 'Member.count' do + post '/projects/1/memberships.xml', {:membership => {:user_id => 7, :role_ids => [2,3]}}, credentials('jsmith') + + assert_response :created + end + end + + should "return errors on failure" do + assert_no_difference 'Member.count' do + post '/projects/1/memberships.xml', {:membership => {:role_ids => [2,3]}}, credentials('jsmith') + + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag 'errors', :child => {:tag => 'error', :content => "Principal can't be blank"} + end + end + end + end + end + + context "/memberships/:id" do + context "GET" do + context "xml" do + should "return the membership" do + get '/memberships/2.xml', {}, credentials('jsmith') + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'membership', + :child => { + :tag => 'id', + :content => '2', + :sibling => { + :tag => 'user', + :attributes => {:id => '3', :name => 'Dave Lopper'}, + :sibling => { + :tag => 'roles', + :child => { + :tag => 'role', + :attributes => {:id => '2', :name => 'Developer'} + } + } + } + } + end + end + + context "json" do + should "return the membership" do + get '/memberships/2.json', {}, credentials('jsmith') + + assert_response :success + assert_equal 'application/json', @response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_equal( + {"membership" => { + "id" => 2, + "project" => {"name"=>"eCookbook", "id"=>1}, + "roles" => [{"name"=>"Developer", "id"=>2}], + "user" => {"name"=>"Dave Lopper", "id"=>3}} + }, + json) + end + end + end + + context "PUT" do + context "xml" do + should "update membership" do + assert_not_equal [1,2], Member.find(2).role_ids.sort + assert_no_difference 'Member.count' do + put '/memberships/2.xml', {:membership => {:user_id => 3, :role_ids => [1,2]}}, credentials('jsmith') + + assert_response :ok + assert_equal '', @response.body + end + member = Member.find(2) + assert_equal [1,2], member.role_ids.sort + end + + should "return errors on failure" do + put '/memberships/2.xml', {:membership => {:user_id => 3, :role_ids => [99]}}, credentials('jsmith') + + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag 'errors', :child => {:tag => 'error', :content => /member_roles is invalid/} + end + end + end + + context "DELETE" do + context "xml" do + should "destroy membership" do + assert_difference 'Member.count', -1 do + delete '/memberships/2.xml', {}, credentials('jsmith') + + assert_response :ok + assert_equal '', @response.body + end + assert_nil Member.find_by_id(2) + end + + should "respond with 422 on failure" do + assert_no_difference 'Member.count' do + # A membership with an inherited role can't be deleted + Member.find(2).member_roles.first.update_attribute :inherited_from, 99 + delete '/memberships/2.xml', {}, credentials('jsmith') + + assert_response :unprocessable_entity + end + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/20/205eb718e22b02ea59cca1358f48bb87e5bd7f8d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/20/205eb718e22b02ea59cca1358f48bb87e5bd7f8d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,280 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'iconv' + +class Changeset < ActiveRecord::Base + belongs_to :repository + belongs_to :user + has_many :filechanges, :class_name => 'Change', :dependent => :delete_all + has_and_belongs_to_many :issues + has_and_belongs_to_many :parents, + :class_name => "Changeset", + :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}", + :association_foreign_key => 'parent_id', :foreign_key => 'changeset_id' + has_and_belongs_to_many :children, + :class_name => "Changeset", + :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}", + :association_foreign_key => 'changeset_id', :foreign_key => 'parent_id' + + acts_as_event :title => Proc.new {|o| o.title}, + :description => :long_comments, + :datetime => :committed_on, + :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :repository_id => o.repository.identifier_param, :rev => o.identifier}} + + acts_as_searchable :columns => 'comments', + :include => {:repository => :project}, + :project_key => "#{Repository.table_name}.project_id", + :date_column => 'committed_on' + + acts_as_activity_provider :timestamp => "#{table_name}.committed_on", + :author_key => :user_id, + :find_options => {:include => [:user, {:repository => :project}]} + + validates_presence_of :repository_id, :revision, :committed_on, :commit_date + validates_uniqueness_of :revision, :scope => :repository_id + validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true + + scope :visible, + lambda {|*args| { :include => {:repository => :project}, + :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } } + + after_create :scan_for_issues + before_create :before_create_cs + + def revision=(r) + write_attribute :revision, (r.nil? ? nil : r.to_s) + end + + # Returns the identifier of this changeset; depending on repository backends + def identifier + if repository.class.respond_to? :changeset_identifier + repository.class.changeset_identifier self + else + revision.to_s + end + end + + def committed_on=(date) + self.commit_date = date + super + end + + # Returns the readable identifier + def format_identifier + if repository.class.respond_to? :format_changeset_identifier + repository.class.format_changeset_identifier self + else + identifier + end + end + + def project + repository.project + end + + def author + user || committer.to_s.split('<').first + end + + def before_create_cs + self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding) + self.comments = self.class.normalize_comments( + self.comments, repository.repo_log_encoding) + self.user = repository.find_committer_user(self.committer) + end + + def scan_for_issues + scan_comment_for_issue_ids + end + + TIMELOG_RE = / + ( + ((\d+)(h|hours?))((\d+)(m|min)?)? + | + ((\d+)(h|hours?|m|min)) + | + (\d+):(\d+) + | + (\d+([\.,]\d+)?)h? + ) + /x + + def scan_comment_for_issue_ids + return if comments.blank? + # keywords used to reference issues + ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip) + ref_keywords_any = ref_keywords.delete('*') + # keywords used to fix issues + fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip) + + kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|") + + referenced_issues = [] + + comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match| + action, refs = match[2], match[3] + next unless action.present? || ref_keywords_any + + refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m| + issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2] + if issue + referenced_issues << issue + fix_issue(issue) if fix_keywords.include?(action.to_s.downcase) + log_time(issue, hours) if hours && Setting.commit_logtime_enabled? + end + end + end + + referenced_issues.uniq! + self.issues = referenced_issues unless referenced_issues.empty? + end + + def short_comments + @short_comments || split_comments.first + end + + def long_comments + @long_comments || split_comments.last + end + + def text_tag(ref_project=nil) + tag = if scmid? + "commit:#{scmid}" + else + "r#{revision}" + end + if repository && repository.identifier.present? + tag = "#{repository.identifier}|#{tag}" + end + if ref_project && project && ref_project != project + tag = "#{project.identifier}:#{tag}" + end + tag + end + + # Returns the title used for the changeset in the activity/search results + def title + repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : '' + comm = short_comments.blank? ? '' : (': ' + short_comments) + "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}" + end + + # Returns the previous changeset + def previous + @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first + end + + # Returns the next changeset + def next + @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first + end + + # Creates a new Change from it's common parameters + def create_change(change) + Change.create(:changeset => self, + :action => change[:action], + :path => change[:path], + :from_path => change[:from_path], + :from_revision => change[:from_revision]) + end + + # Finds an issue that can be referenced by the commit message + def find_referenced_issue_by_id(id) + return nil if id.blank? + issue = Issue.find_by_id(id.to_i, :include => :project) + if Setting.commit_cross_project_ref? + # all issues can be referenced/fixed + elsif issue + # issue that belong to the repository project, a subproject or a parent project only + unless issue.project && + (project == issue.project || project.is_ancestor_of?(issue.project) || + project.is_descendant_of?(issue.project)) + issue = nil + end + end + issue + end + + private + + def fix_issue(issue) + status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i) + if status.nil? + logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger + return issue + end + + # the issue may have been updated by the closure of another one (eg. duplicate) + issue.reload + # don't change the status is the issue is closed + return if issue.status && issue.status.is_closed? + + journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project))) + issue.status = status + unless Setting.commit_fix_done_ratio.blank? + issue.done_ratio = Setting.commit_fix_done_ratio.to_i + end + Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update, + { :changeset => self, :issue => issue }) + unless issue.save + logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger + end + issue + end + + def log_time(issue, hours) + time_entry = TimeEntry.new( + :user => user, + :hours => hours, + :issue => issue, + :spent_on => commit_date, + :comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project), + :locale => Setting.default_language) + ) + time_entry.activity = log_time_activity unless log_time_activity.nil? + + unless time_entry.save + logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger + end + time_entry + end + + def log_time_activity + if Setting.commit_logtime_activity_id.to_i > 0 + TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i) + end + end + + def split_comments + comments =~ /\A(.+?)\r?\n(.*)$/m + @short_comments = $1 || comments + @long_comments = $2.to_s.strip + return @short_comments, @long_comments + end + + public + + # Strips and reencodes a commit log before insertion into the database + def self.normalize_comments(str, encoding) + Changeset.to_utf8(str.to_s.strip, encoding) + end + + def self.to_utf8(str, encoding) + Redmine::CodesetUtil.to_utf8(str, encoding) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/20/2069c7a6894bd57d0791bf6d175776952425d0ca.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/20/2069c7a6894bd57d0791bf6d175776952425d0ca.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,354 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'diff' + +# The WikiController follows the Rails REST controller pattern but with +# a few differences +# +# * index - shows a list of WikiPages grouped by page or date +# * new - not used +# * create - not used +# * show - will also show the form for creating a new wiki page +# * edit - used to edit an existing or new page +# * update - used to save a wiki page update to the database, including new pages +# * destroy - normal +# +# Other member and collection methods are also used +# +# TODO: still being worked on +class WikiController < ApplicationController + default_search_scope :wiki_pages + before_filter :find_wiki, :authorize + before_filter :find_existing_or_new_page, :only => [:show, :edit, :update] + before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version] + accept_api_auth :index, :show, :update, :destroy + + helper :attachments + include AttachmentsHelper + helper :watchers + include Redmine::Export::PDF + + # List of pages, sorted alphabetically and by parent (hierarchy) + def index + load_pages_for_index + + respond_to do |format| + format.html { + @pages_by_parent_id = @pages.group_by(&:parent_id) + } + format.api + end + end + + # List of page, by last update + def date_index + load_pages_for_index + @pages_by_date = @pages.group_by {|p| p.updated_on.to_date} + end + + # display a page (in editing mode if it doesn't exist) + def show + if @page.new_record? + if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request? + edit + render :action => 'edit' + else + render_404 + end + return + end + if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project) + deny_access + return + end + @content = @page.content_for_version(params[:version]) + if User.current.allowed_to?(:export_wiki_pages, @project) + if params[:format] == 'pdf' + send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf") + return + elsif params[:format] == 'html' + export = render_to_string :action => 'export', :layout => false + send_data(export, :type => 'text/html', :filename => "#{@page.title}.html") + return + elsif params[:format] == 'txt' + send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt") + return + end + end + @editable = editable? + @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) && + @content.current_version? && + Redmine::WikiFormatting.supports_section_edit? + + respond_to do |format| + format.html + format.api + end + end + + # edit an existing page or a new one + def edit + return render_403 unless editable? + if @page.new_record? + @page.content = WikiContent.new(:page => @page) + if params[:parent].present? + @page.parent = @page.wiki.find_page(params[:parent].to_s) + end + end + + @content = @page.content_for_version(params[:version]) + @content.text = initial_page_content(@page) if @content.text.blank? + # don't keep previous comment + @content.comments = nil + + # To prevent StaleObjectError exception when reverting to a previous version + @content.version = @page.content.version + + @text = @content.text + if params[:section].present? && Redmine::WikiFormatting.supports_section_edit? + @section = params[:section].to_i + @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section) + render_404 if @text.blank? + end + end + + # Creates a new page or updates an existing one + def update + return render_403 unless editable? + was_new_page = @page.new_record? + @page.content = WikiContent.new(:page => @page) if @page.new_record? + @page.safe_attributes = params[:wiki_page] + + @content = @page.content + content_params = params[:content] + if content_params.nil? && params[:wiki_page].is_a?(Hash) + content_params = params[:wiki_page].slice(:text, :comments, :version) + end + content_params ||= {} + + @content.comments = content_params[:comments] + @text = content_params[:text] + if params[:section].present? && Redmine::WikiFormatting.supports_section_edit? + @section = params[:section].to_i + @section_hash = params[:section_hash] + @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash) + else + @content.version = content_params[:version] if content_params[:version] + @content.text = @text + end + @content.author = User.current + + if @page.save_with_content + attachments = Attachment.attach_files(@page, params[:attachments]) + render_attachment_warning_if_needed(@page) + call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page}) + + respond_to do |format| + format.html { redirect_to :action => 'show', :project_id => @project, :id => @page.title } + format.api { + if was_new_page + render :action => 'show', :status => :created, :location => url_for(:controller => 'wiki', :action => 'show', :project_id => @project, :id => @page.title) + else + render_api_ok + end + } + end + else + respond_to do |format| + format.html { render :action => 'edit' } + format.api { render_validation_errors(@content) } + end + end + + rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError + # Optimistic locking exception + respond_to do |format| + format.html { + flash.now[:error] = l(:notice_locking_conflict) + render :action => 'edit' + } + format.api { render_api_head :conflict } + end + rescue ActiveRecord::RecordNotSaved + respond_to do |format| + format.html { render :action => 'edit' } + format.api { render_validation_errors(@content) } + end + end + + # rename a page + def rename + return render_403 unless editable? + @page.redirect_existing_links = true + # used to display the *original* title if some AR validation errors occur + @original_title = @page.pretty_title + if request.post? && @page.update_attributes(params[:wiki_page]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'show', :project_id => @project, :id => @page.title + end + end + + def protect + @page.update_attribute :protected, params[:protected] + redirect_to :action => 'show', :project_id => @project, :id => @page.title + end + + # show page history + def history + @version_count = @page.content.versions.count + @version_pages = Paginator.new self, @version_count, per_page_option, params['page'] + # don't load text + @versions = @page.content.versions.find :all, + :select => "id, author_id, comments, updated_on, version", + :order => 'version DESC', + :limit => @version_pages.items_per_page + 1, + :offset => @version_pages.current.offset + + render :layout => false if request.xhr? + end + + def diff + @diff = @page.diff(params[:version], params[:version_from]) + render_404 unless @diff + end + + def annotate + @annotate = @page.annotate(params[:version]) + render_404 unless @annotate + end + + # Removes a wiki page and its history + # Children can be either set as root pages, removed or reassigned to another parent page + def destroy + return render_403 unless editable? + + @descendants_count = @page.descendants.size + if @descendants_count > 0 + case params[:todo] + when 'nullify' + # Nothing to do + when 'destroy' + # Removes all its descendants + @page.descendants.each(&:destroy) + when 'reassign' + # Reassign children to another parent page + reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i) + return unless reassign_to + @page.children.each do |child| + child.update_attribute(:parent, reassign_to) + end + else + @reassignable_to = @wiki.pages - @page.self_and_descendants + # display the destroy form if it's a user request + return unless api_request? + end + end + @page.destroy + respond_to do |format| + format.html { redirect_to :action => 'index', :project_id => @project } + format.api { render_api_ok } + end + end + + def destroy_version + return render_403 unless editable? + + @content = @page.content_for_version(params[:version]) + @content.destroy + redirect_to_referer_or :action => 'history', :id => @page.title, :project_id => @project + end + + # Export wiki to a single pdf or html file + def export + @pages = @wiki.pages.all(:order => 'title', :include => [:content, {:attachments => :author}]) + respond_to do |format| + format.html { + export = render_to_string :action => 'export_multiple', :layout => false + send_data(export, :type => 'text/html', :filename => "wiki.html") + } + format.pdf { + send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf") + } + end + end + + def preview + page = @wiki.find_page(params[:id]) + # page is nil when previewing a new page + return render_403 unless page.nil? || editable?(page) + if page + @attachements = page.attachments + @previewed = page.content + end + @text = params[:content][:text] + render :partial => 'common/preview' + end + + def add_attachment + return render_403 unless editable? + attachments = Attachment.attach_files(@page, params[:attachments]) + render_attachment_warning_if_needed(@page) + redirect_to :action => 'show', :id => @page.title, :project_id => @project + end + +private + + def find_wiki + @project = Project.find(params[:project_id]) + @wiki = @project.wiki + render_404 unless @wiki + rescue ActiveRecord::RecordNotFound + render_404 + end + + # Finds the requested page or a new page if it doesn't exist + def find_existing_or_new_page + @page = @wiki.find_or_new_page(params[:id]) + if @wiki.page_found_with_redirect? + redirect_to params.update(:id => @page.title) + end + end + + # Finds the requested page and returns a 404 error if it doesn't exist + def find_existing_page + @page = @wiki.find_page(params[:id]) + if @page.nil? + render_404 + return + end + if @wiki.page_found_with_redirect? + redirect_to params.update(:id => @page.title) + end + end + + # Returns true if the current user is allowed to edit the page, otherwise false + def editable?(page = @page) + page.editable_by?(User.current) + end + + # Returns the default content of a new wiki page + def initial_page_content(page) + helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) + extend helper unless self.instance_of?(helper) + helper.instance_method(:initial_page_content).bind(self).call(page) + end + + def load_pages_for_index + @pages = @wiki.pages.with_updated_on.order("#{WikiPage.table_name}.title").includes(:wiki => :project).includes(:parent).all + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/20/208e5f65518788d2e8e359431914f6d85fea6a8a.svn-base --- a/.svn/pristine/20/208e5f65518788d2e8e359431914f6d85fea6a8a.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +0,0 @@ -class Change < ActiveRecord::Base - generator_for :action => 'A' - generator_for :path, :start => 'test/dir/aaa0001' - generator_for :changeset, :method => :generate_changeset - - def self.generate_changeset - Changeset.generate! - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/20/209f9ebc3aa58812be26dbeb5ba5ab0a2d6a75b3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/20/209f9ebc3aa58812be26dbeb5ba5ab0a2d6a75b3.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,242 @@ +# encoding: utf-8 +# +# Helpers to sort tables using clickable column headers. +# +# Author: Stuart Rackham , March 2005. +# Jean-Philippe Lang, 2009 +# License: This source code is released under the MIT license. +# +# - Consecutive clicks toggle the column's sort order. +# - Sort state is maintained by a session hash entry. +# - CSS classes identify sort column and state. +# - Typically used in conjunction with the Pagination module. +# +# Example code snippets: +# +# Controller: +# +# helper :sort +# include SortHelper +# +# def list +# sort_init 'last_name' +# sort_update %w(first_name last_name) +# @items = Contact.find_all nil, sort_clause +# end +# +# Controller (using Pagination module): +# +# helper :sort +# include SortHelper +# +# def list +# sort_init 'last_name' +# sort_update %w(first_name last_name) +# @contact_pages, @items = paginate :contacts, +# :order_by => sort_clause, +# :per_page => 10 +# end +# +# View (table header in list.rhtml): +# +# +# +# <%= sort_header_tag('id', :title => 'Sort by contact ID') %> +# <%= sort_header_tag('last_name', :caption => 'Name') %> +# <%= sort_header_tag('phone') %> +# <%= sort_header_tag('address', :width => 200) %> +# +# +# +# - Introduces instance variables: @sort_default, @sort_criteria +# - Introduces param :sort +# + +module SortHelper + class SortCriteria + + def initialize + @criteria = [] + end + + def available_criteria=(criteria) + unless criteria.is_a?(Hash) + criteria = criteria.inject({}) {|h,k| h[k] = k; h} + end + @available_criteria = criteria + end + + def from_param(param) + @criteria = param.to_s.split(',').collect {|s| s.split(':')[0..1]} + normalize! + end + + def criteria=(arg) + @criteria = arg + normalize! + end + + def to_param + @criteria.collect {|k,o| k + (o ? '' : ':desc')}.join(',') + end + + def to_sql + sql = @criteria.collect do |k,o| + if s = @available_criteria[k] + (o ? s.to_a : s.to_a.collect {|c| append_desc(c)}).join(', ') + end + end.compact.join(', ') + sql.blank? ? nil : sql + end + + def to_a + @criteria.dup + end + + def add!(key, asc) + @criteria.delete_if {|k,o| k == key} + @criteria = [[key, asc]] + @criteria + normalize! + end + + def add(*args) + r = self.class.new.from_param(to_param) + r.add!(*args) + r + end + + def first_key + @criteria.first && @criteria.first.first + end + + def first_asc? + @criteria.first && @criteria.first.last + end + + def empty? + @criteria.empty? + end + + private + + def normalize! + @criteria ||= [] + @criteria = @criteria.collect {|s| s = s.to_a; [s.first, (s.last == false || s.last == 'desc') ? false : true]} + @criteria = @criteria.select {|k,o| @available_criteria.has_key?(k)} if @available_criteria + @criteria.slice!(3) + self + end + + # Appends DESC to the sort criterion unless it has a fixed order + def append_desc(criterion) + if criterion =~ / (asc|desc)$/i + criterion + else + "#{criterion} DESC" + end + end + end + + def sort_name + controller_name + '_' + action_name + '_sort' + end + + # Initializes the default sort. + # Examples: + # + # sort_init 'name' + # sort_init 'id', 'desc' + # sort_init ['name', ['id', 'desc']] + # sort_init [['name', 'desc'], ['id', 'desc']] + # + def sort_init(*args) + case args.size + when 1 + @sort_default = args.first.is_a?(Array) ? args.first : [[args.first]] + when 2 + @sort_default = [[args.first, args.last]] + else + raise ArgumentError + end + end + + # Updates the sort state. Call this in the controller prior to calling + # sort_clause. + # - criteria can be either an array or a hash of allowed keys + # + def sort_update(criteria, sort_name=nil) + sort_name ||= self.sort_name + @sort_criteria = SortCriteria.new + @sort_criteria.available_criteria = criteria + @sort_criteria.from_param(params[:sort] || session[sort_name]) + @sort_criteria.criteria = @sort_default if @sort_criteria.empty? + session[sort_name] = @sort_criteria.to_param + end + + # Clears the sort criteria session data + # + def sort_clear + session[sort_name] = nil + end + + # Returns an SQL sort clause corresponding to the current sort state. + # Use this to sort the controller's table items collection. + # + def sort_clause() + @sort_criteria.to_sql + end + + def sort_criteria + @sort_criteria + end + + # Returns a link which sorts by the named column. + # + # - column is the name of an attribute in the sorted record collection. + # - the optional caption explicitly specifies the displayed link text. + # - 2 CSS classes reflect the state of the link: sort and asc or desc + # + def sort_link(column, caption, default_order) + css, order = nil, default_order + + if column.to_s == @sort_criteria.first_key + if @sort_criteria.first_asc? + css = 'sort asc' + order = 'desc' + else + css = 'sort desc' + order = 'asc' + end + end + caption = column.to_s.humanize unless caption + + sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param } + url_options = params.merge(sort_options) + + # Add project_id to url_options + url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id) + + link_to_content_update(h(caption), url_options, :class => css) + end + + # Returns a table header tag with a sort link for the named column + # attribute. + # + # Options: + # :caption The displayed link name (defaults to titleized column name). + # :title The tag's 'title' attribute (defaults to 'Sort by :caption'). + # + # Other options hash entries generate additional table header tag attributes. + # + # Example: + # + # <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %> + # + def sort_header_tag(column, options = {}) + caption = options.delete(:caption) || column.to_s.humanize + default_order = options.delete(:default_order) || 'asc' + options[:title] = l(:label_sort_by, "\"#{caption}\"") unless options[:title] + content_tag('th', sort_link(column, caption, default_order), options) + end +end + diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/20/20b8ba7bb15b0efb2e3e09bae49971ba5e4c3a84.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/20/20b8ba7bb15b0efb2e3e09bae49971ba5e4c3a84.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,20 @@ +

    <%=l(:label_password_lost)%>

    + +<%= error_messages_for 'user' %> + +<%= form_tag(lost_password_path) do %> + <%= hidden_field_tag 'token', @token.value %> +
    +

    + + <%= password_field_tag 'new_password', nil, :size => 25 %> + <%= l(:text_caracters_minimum, :count => Setting.password_min_length) %> +

    + +

    + + <%= password_field_tag 'new_password_confirmation', nil, :size => 25 %> +

    +
    +

    <%= submit_tag l(:button_save) %>

    +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/20/20e5c0c48e6b23cc5129695034ba2f44417b7624.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/20/20e5c0c48e6b23cc5129695034ba2f44417b7624.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,8 @@ +

    <%= l(:label_spent_time) %>

    + +<%= labelled_form_for @time_entry, :url => time_entries_path do |f| %> + <%= hidden_field_tag 'project_id', params[:project_id] if params[:project_id] %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_create) %> + <%= submit_tag l(:button_create_and_continue), :name => 'continue' %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/20/20f8dff24ed27f7604eb718009f2a8bf899ab23c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/20/20f8dff24ed27f7604eb718009f2a8bf899ab23c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,98 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) +require 'pp' +class ApiTest::NewsTest < ActionController::IntegrationTest + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :workflows, + :news + + def setup + Setting.rest_api_enabled = '1' + end + + context "GET /news" do + context ".xml" do + should "return news" do + get '/news.xml' + + assert_tag :tag => 'news', + :attributes => {:type => 'array'}, + :child => { + :tag => 'news', + :child => { + :tag => 'id', + :content => '2' + } + } + end + end + + context ".json" do + should "return news" do + get '/news.json' + + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_kind_of Array, json['news'] + assert_kind_of Hash, json['news'].first + assert_equal 2, json['news'].first['id'] + end + end + end + + context "GET /projects/:project_id/news" do + context ".xml" do + should_allow_api_authentication(:get, "/projects/onlinestore/news.xml") + + should "return news" do + get '/projects/ecookbook/news.xml' + + assert_tag :tag => 'news', + :attributes => {:type => 'array'}, + :child => { + :tag => 'news', + :child => { + :tag => 'id', + :content => '2' + } + } + end + end + + context ".json" do + should_allow_api_authentication(:get, "/projects/onlinestore/news.json") + + should "return news" do + get '/projects/ecookbook/news.json' + + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_kind_of Array, json['news'] + assert_kind_of Hash, json['news'].first + assert_equal 2, json['news'].first['id'] + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/21/212b78099cd591bc9596c563f09ce2ddb8eb84ad.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/21/212b78099cd591bc9596c563f09ce2ddb8eb84ad.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,59 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingMembersTest < ActionController::IntegrationTest + def test_members + assert_routing( + { :method => 'get', :path => "/projects/5234/memberships.xml" }, + { :controller => 'members', :action => 'index', :project_id => '5234', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/memberships/5234.xml" }, + { :controller => 'members', :action => 'show', :id => '5234', :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/projects/5234/memberships" }, + { :controller => 'members', :action => 'create', :project_id => '5234' } + ) + assert_routing( + { :method => 'post', :path => "/projects/5234/memberships.xml" }, + { :controller => 'members', :action => 'create', :project_id => '5234', :format => 'xml' } + ) + assert_routing( + { :method => 'put', :path => "/memberships/5234" }, + { :controller => 'members', :action => 'update', :id => '5234' } + ) + assert_routing( + { :method => 'put', :path => "/memberships/5234.xml" }, + { :controller => 'members', :action => 'update', :id => '5234', :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/memberships/5234" }, + { :controller => 'members', :action => 'destroy', :id => '5234' } + ) + assert_routing( + { :method => 'delete', :path => "/memberships/5234.xml" }, + { :controller => 'members', :action => 'destroy', :id => '5234', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/projects/5234/memberships/autocomplete" }, + { :controller => 'members', :action => 'autocomplete', :project_id => '5234' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/21/214cb9ff3f6c15c4f88f3e115e3debbc096f870a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/21/214cb9ff3f6c15c4f88f3e115e3debbc096f870a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= "production" +require File.expand_path(File.dirname(__FILE__) + "/../config/environment") +puts +puts Redmine::Info.environment diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/21/21c6ca4067389a6f9db56bd1c0d0ecf126188c0b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/21/21c6ca4067389a6f9db56bd1c0d0ecf126188c0b.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,15 @@ +api.version do + api.id @version.id + api.project(:id => @version.project_id, :name => @version.project.name) unless @version.project.nil? + + api.name @version.name + api.description @version.description + api.status @version.status + api.due_date @version.effective_date + api.sharing @version.sharing + + render_api_custom_values @version.custom_field_values, api + + api.created_on @version.created_on + api.updated_on @version.updated_on +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/22/2215da5ea32efbcca552e4718d4f9896046cbfa7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/22/2215da5ea32efbcca552e4718d4f9896046cbfa7.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,21 @@ +<%= wiki_page_breadcrumb(@page) %> + +

    <%= h @original_title %>

    + +<%= error_messages_for 'page' %> + +<%= labelled_form_for :wiki_page, @page, + :url => { :action => 'rename' }, + :html => { :method => :post } do |f| %> +
    +

    <%= f.text_field :title, :required => true, :size => 100 %>

    +

    <%= f.check_box :redirect_existing_links %>

    +

    <%= f.select :parent_id, + content_tag('option', '', :value => '') + + wiki_page_options_for_select( + @wiki.pages.all(:include => :parent) - @page.self_and_descendants, + @page.parent), + :label => :field_parent_title %>

    +
    +<%= submit_tag l(:button_rename) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/22/2239f10ea5e4bc85a8ddbe53928f7d28bb9ad174.svn-base --- a/.svn/pristine/22/2239f10ea5e4bc85a8ddbe53928f7d28bb9ad174.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -class Tracker < ActiveRecord::Base - generator_for :name, :start => 'Tracker 0' - -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/22/22425409df08842dce9d0920b3c239700ff36d87.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/22/22425409df08842dce9d0920b3c239700ff36d87.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,20 @@ +Date: Mon, 19 Nov 2012 10:17:45 +0900 +Message-ID: +Subject: test +From: John Smith +To: redmine@somenet.foo +Content-Type: multipart/mixed; boundary=bcaec54ee4ea84f77904cecee22e + +--bcaec54ee4ea84f77904cecee22e +Content-Type: text/plain; charset=ISO-8859-1 + +test + +--bcaec54ee4ea84f77904cecee22e +Content-Type: text/plain; charset=US-ASCII; name="=?ISO-2022-JP?B?GyRCJUYlOSVIGyhCLnR4dA==?=" +Content-Disposition: attachment; filename="=?ISO-2022-JP?B?GyRCJUYlOSVIGyhCLnR4dA==?=" +Content-Transfer-Encoding: base64 +X-Attachment-Id: f_h9owndpv0 + +dGVzdAo= +--bcaec54ee4ea84f77904cecee22e-- diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/22/226ca6df54d2f722db0a366c7bc4bc8b7ec109c6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/22/226ca6df54d2f722db0a366c7bc4bc8b7ec109c6.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,47 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module SubclassFactory + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + def get_subclass(class_name) + klass = nil + begin + klass = class_name.to_s.classify.constantize + rescue + # invalid class name + end + unless subclasses.include? klass + klass = nil + end + klass + end + + # Returns an instance of the given subclass name + def new_subclass_instance(class_name, *args) + klass = get_subclass(class_name) + if klass + klass.new(*args) + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/22/228763af0fab9d92d601d760e23db085d02a829d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/22/228763af0fab9d92d601d760e23db085d02a829d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,97 @@ +
    + « + <% unless @changeset.previous.nil? -%> + <%= link_to_revision(@changeset.previous, @repository, :text => l(:label_previous)) %> + <% else -%> + <%= l(:label_previous) %> + <% end -%> +| + <% unless @changeset.next.nil? -%> + <%= link_to_revision(@changeset.next, @repository, :text => l(:label_next)) %> + <% else -%> + <%= l(:label_next) %> + <% end -%> + »  + + <%= form_tag({:controller => 'repositories', + :action => 'revision', + :id => @project, + :repository_id => @repository.identifier_param, + :rev => nil}, + :method => :get) do %> + <%= text_field_tag 'rev', @rev, :size => 8 %> + <%= submit_tag 'OK', :name => nil %> + <% end %> +
    + +

    <%= avatar(@changeset.user, :size => "24") %><%= l(:label_revision) %> <%= format_revision(@changeset) %>

    + +<% if @changeset.scmid.present? || @changeset.parents.present? || @changeset.children.present? %> + + <% if @changeset.scmid.present? %> + + + + <% end %> + <% if @changeset.parents.present? %> + + + + + <% end %> + <% if @changeset.children.present? %> + + + + + <% end %> +
    ID<%= h(@changeset.scmid) %>
    <%= l(:label_parent_revision) %> + <%= @changeset.parents.collect{ + |p| link_to_revision(p, @repository, :text => format_revision(p)) + }.join(", ").html_safe %> +
    <%= l(:label_child_revision) %> + <%= @changeset.children.collect{ + |p| link_to_revision(p, @repository, :text => format_revision(p)) + }.join(", ").html_safe %> +
    +<% end %> + +

    + +<%= authoring(@changeset.committed_on, @changeset.author) %> + +

    + +<%= textilizable @changeset.comments %> + +<% if @changeset.issues.visible.any? || User.current.allowed_to?(:manage_related_issues, @repository.project) %> + <%= render :partial => 'related_issues' %> +<% end %> + +<% if User.current.allowed_to?(:browse_repository, @project) %> +

    <%= l(:label_attachment_plural) %>

    +
      +
    • <%= l(:label_added) %>
    • +
    • <%= l(:label_modified) %>
    • +
    • <%= l(:label_copied) %>
    • +
    • <%= l(:label_renamed) %>
    • +
    • <%= l(:label_deleted) %>
    • +
    + +

    <%= link_to(l(:label_view_diff), + :action => 'diff', + :id => @project, + :repository_id => @repository.identifier_param, + :path => "", + :rev => @changeset.identifier) if @changeset.filechanges.any? %>

    + +
    +<%= render_changeset_changes %> +
    +<% end %> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> + +<% html_title("#{l(:label_revision)} #{format_revision(@changeset)}") -%> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/22/228ab5b14477141cc691a1ebb6e20efbefbf1ba7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/22/228ab5b14477141cc691a1ebb6e20efbefbf1ba7.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,41 @@ + + + + + + + + + <% for new_status in @statuses %> + + <% end %> + + + + <% for old_status in @statuses %> + "> + + <% for new_status in @statuses -%> + <% checked = workflows.detect {|w| w.old_status_id == old_status.id && w.new_status_id == new_status.id} %> + + <% end -%> + + <% end %> + +
    + <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input')", + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %> + <%=l(:label_current_status)%> + <%=l(:label_new_statuses_allowed)%>
    + <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.new-status-#{new_status.id}')", + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %> + <%=h new_status.name %> +
    + <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.old-status-#{old_status.id}')", + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %> + + <%=h old_status.name %> + + <%= check_box_tag "issue_status[#{ old_status.id }][#{new_status.id}][]", name, checked, + :class => "old-status-#{old_status.id} new-status-#{new_status.id}" %> +
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/22/22d0c7f9d45361c16b2b7eccd0c488f009ccf152.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/22/22d0c7f9d45361c16b2b7eccd0c488f009ccf152.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,88 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class TimeEntryActivityTest < ActiveSupport::TestCase + fixtures :enumerations, :time_entries, :custom_fields + + include Redmine::I18n + + def test_should_be_an_enumeration + assert TimeEntryActivity.ancestors.include?(Enumeration) + end + + def test_objects_count + assert_equal 3, TimeEntryActivity.find_by_name("Design").objects_count + assert_equal 2, TimeEntryActivity.find_by_name("Development").objects_count + end + + def test_option_name + assert_equal :enumeration_activities, TimeEntryActivity.new.option_name + end + + def test_create_with_custom_field + field = TimeEntryActivityCustomField.find_by_name('Billable') + e = TimeEntryActivity.new(:name => 'Custom Data') + e.custom_field_values = {field.id => "1"} + assert e.save + + e.reload + assert_equal "1", e.custom_value_for(field).value + end + + def test_create_without_required_custom_field_should_fail + set_language_if_valid 'en' + field = TimeEntryActivityCustomField.find_by_name('Billable') + field.update_attribute(:is_required, true) + + e = TimeEntryActivity.new(:name => 'Custom Data') + assert !e.save + assert_equal ["Billable can't be blank"], e.errors.full_messages + end + + def test_create_with_required_custom_field_should_succeed + field = TimeEntryActivityCustomField.find_by_name('Billable') + field.update_attribute(:is_required, true) + + e = TimeEntryActivity.new(:name => 'Custom Data') + e.custom_field_values = {field.id => "1"} + assert e.save + end + + def test_update_with_required_custom_field_change + set_language_if_valid 'en' + field = TimeEntryActivityCustomField.find_by_name('Billable') + field.update_attribute(:is_required, true) + + e = TimeEntryActivity.find(10) + assert e.available_custom_fields.include?(field) + # No change to custom field, record can be saved + assert e.save + # Blanking custom field, save should fail + e.custom_field_values = {field.id => ""} + assert !e.save + assert_equal ["Billable can't be blank"], e.errors.full_messages + + # Update custom field to valid value, save should succeed + e.custom_field_values = {field.id => "0"} + assert e.save + e.reload + assert_equal "0", e.custom_value_for(field).value + end +end + diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/22/22ddddc8c878fc3e93d3136a28528f9e4f69f7e0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/22/22ddddc8c878fc3e93d3136a28528f9e4f69f7e0.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,102 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../test_helper', __FILE__) + +class Redmine::SafeAttributesTest < ActiveSupport::TestCase + fixtures :users + + class Base + def attributes=(attrs) + attrs.each do |key, value| + send("#{key}=", value) + end + end + end + + class Person < Base + attr_accessor :firstname, :lastname, :login + include Redmine::SafeAttributes + safe_attributes :firstname, :lastname + safe_attributes :login, :if => lambda {|person, user| user.admin?} + end + + class Book < Base + attr_accessor :title + include Redmine::SafeAttributes + safe_attributes :title + end + + def test_safe_attribute_names + p = Person.new + user = User.anonymous + assert_equal ['firstname', 'lastname'], p.safe_attribute_names(user) + assert p.safe_attribute?('firstname', user) + assert !p.safe_attribute?('login', user) + + p = Person.new + user = User.find(1) + assert_equal ['firstname', 'lastname', 'login'], p.safe_attribute_names(user) + assert p.safe_attribute?('firstname', user) + assert p.safe_attribute?('login', user) + end + + def test_safe_attribute_names_without_user + p = Person.new + User.current = nil + assert_equal ['firstname', 'lastname'], p.safe_attribute_names + assert p.safe_attribute?('firstname') + assert !p.safe_attribute?('login') + + p = Person.new + User.current = User.find(1) + assert_equal ['firstname', 'lastname', 'login'], p.safe_attribute_names + assert p.safe_attribute?('firstname') + assert p.safe_attribute?('login') + end + + def test_set_safe_attributes + p = Person.new + p.send('safe_attributes=', {'firstname' => 'John', 'lastname' => 'Smith', 'login' => 'jsmith'}, User.anonymous) + assert_equal 'John', p.firstname + assert_equal 'Smith', p.lastname + assert_nil p.login + + p = Person.new + User.current = User.find(1) + p.send('safe_attributes=', {'firstname' => 'John', 'lastname' => 'Smith', 'login' => 'jsmith'}, User.find(1)) + assert_equal 'John', p.firstname + assert_equal 'Smith', p.lastname + assert_equal 'jsmith', p.login + end + + def test_set_safe_attributes_without_user + p = Person.new + User.current = nil + p.safe_attributes = {'firstname' => 'John', 'lastname' => 'Smith', 'login' => 'jsmith'} + assert_equal 'John', p.firstname + assert_equal 'Smith', p.lastname + assert_nil p.login + + p = Person.new + User.current = User.find(1) + p.safe_attributes = {'firstname' => 'John', 'lastname' => 'Smith', 'login' => 'jsmith'} + assert_equal 'John', p.firstname + assert_equal 'Smith', p.lastname + assert_equal 'jsmith', p.login + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/22/22e0815b30d651aac0c03e283ee06938ada8af0c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/22/22e0815b30d651aac0c03e283ee06938ada8af0c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,60 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'net/imap' + +module Redmine + module IMAP + class << self + def check(imap_options={}, options={}) + host = imap_options[:host] || '127.0.0.1' + port = imap_options[:port] || '143' + ssl = !imap_options[:ssl].nil? + folder = imap_options[:folder] || 'INBOX' + + imap = Net::IMAP.new(host, port, ssl) + imap.login(imap_options[:username], imap_options[:password]) unless imap_options[:username].nil? + imap.select(folder) + imap.search(['NOT', 'SEEN']).each do |message_id| + msg = imap.fetch(message_id,'RFC822')[0].attr['RFC822'] + logger.debug "Receiving message #{message_id}" if logger && logger.debug? + if MailHandler.receive(msg, options) + logger.debug "Message #{message_id} successfully received" if logger && logger.debug? + if imap_options[:move_on_success] + imap.copy(message_id, imap_options[:move_on_success]) + end + imap.store(message_id, "+FLAGS", [:Seen, :Deleted]) + else + logger.debug "Message #{message_id} can not be processed" if logger && logger.debug? + imap.store(message_id, "+FLAGS", [:Seen]) + if imap_options[:move_on_failure] + imap.copy(message_id, imap_options[:move_on_failure]) + imap.store(message_id, "+FLAGS", [:Deleted]) + end + end + end + imap.expunge + end + + private + + def logger + ::Rails.logger + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/22/22fbfd1ac0d86469e5085705e69350a177979509.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/22/22fbfd1ac0d86469e5085705e69350a177979509.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,14 @@ +class AddWorkflowsAssigneeAndAuthor < ActiveRecord::Migration + def self.up + add_column :workflows, :assignee, :boolean, :null => false, :default => false + add_column :workflows, :author, :boolean, :null => false, :default => false + + WorkflowRule.update_all(:assignee => false) + WorkflowRule.update_all(:author => false) + end + + def self.down + remove_column :workflows, :assignee + remove_column :workflows, :author + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/23/23161378fd0429fed8a9ad8cd7fdacb71cc2db68.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/23/23161378fd0429fed8a9ad8cd7fdacb71cc2db68.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,8 @@ +<%= error_messages_for @group %> + +
    +

    <%= f.text_field :name %>

    + <% @group.custom_field_values.each do |value| %> +

    <%= custom_field_tag_with_label :group, value %>

    + <% end %> +
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/23/231b70e58d27161a5f2deb16ab2e057f571aa2ab.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/23/231b70e58d27161a5f2deb16ab2e057f571aa2ab.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,42 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'blankslate' + +module Redmine + module Views + module Builders + class Json < Structure + attr_accessor :jsonp + + def initialize(request, response) + super + self.jsonp = (request.params[:callback] || request.params[:jsonp]).to_s.gsub(/[^a-zA-Z0-9_]/, '') + end + + def output + json = @struct.first.to_json + if jsonp.present? + json = "#{jsonp}(#{json})" + response.content_type = 'application/javascript' + end + json + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/23/2334e365cbae3c0f02226259643ce7e569ce00f6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/23/2334e365cbae3c0f02226259643ce7e569ce00f6.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,64 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class CommentsControllerTest < ActionController::TestCase + fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news, :comments + + def setup + User.current = nil + end + + def test_add_comment + @request.session[:user_id] = 2 + post :create, :id => 1, :comment => { :comments => 'This is a test comment' } + assert_redirected_to '/news/1' + + comment = News.find(1).comments.last + assert_not_nil comment + assert_equal 'This is a test comment', comment.comments + assert_equal User.find(2), comment.author + end + + def test_empty_comment_should_not_be_added + @request.session[:user_id] = 2 + assert_no_difference 'Comment.count' do + post :create, :id => 1, :comment => { :comments => '' } + assert_response :redirect + assert_redirected_to '/news/1' + end + end + + def test_create_should_be_denied_if_news_is_not_commentable + News.any_instance.stubs(:commentable?).returns(false) + @request.session[:user_id] = 2 + assert_no_difference 'Comment.count' do + post :create, :id => 1, :comment => { :comments => 'This is a test comment' } + assert_response 403 + end + end + + def test_destroy_comment + comments_count = News.find(1).comments.size + @request.session[:user_id] = 2 + delete :destroy, :id => 1, :comment_id => 2 + assert_redirected_to '/news/1' + assert_nil Comment.find_by_id(2) + assert_equal comments_count - 1, News.find(1).comments.size + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/23/23389be83ae1e1f9aef946f48e6c0e1c8c6bbfb1.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/23/23389be83ae1e1f9aef946f48e6c0e1c8c6bbfb1.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,263 @@ +<% @gantt.view = self %> +

    <%= @query.new_record? ? l(:label_gantt) : h(@query.name) %>

    + +<%= form_tag({:controller => 'gantts', :action => 'show', + :project_id => @project, :month => params[:month], + :year => params[:year], :months => params[:months]}, + :method => :get, :id => 'query_form') do %> +<%= hidden_field_tag 'set_filter', '1' %> +
    "> + <%= l(:label_filter_plural) %> +
    "> + <%= render :partial => 'queries/filters', :locals => {:query => @query} %> +
    +
    + +

    + <%= gantt_zoom_link(@gantt, :in) %> + <%= gantt_zoom_link(@gantt, :out) %> +

    + +

    +<%= text_field_tag 'months', @gantt.months, :size => 2 %> +<%= l(:label_months_from) %> +<%= select_month(@gantt.month_from, :prefix => "month", :discard_type => true) %> +<%= select_year(@gantt.year_from, :prefix => "year", :discard_type => true) %> +<%= hidden_field_tag 'zoom', @gantt.zoom %> + +<%= link_to_function l(:button_apply), '$("#query_form").submit()', + :class => 'icon icon-checked' %> +<%= link_to l(:button_clear), { :project_id => @project, :set_filter => 1 }, + :class => 'icon icon-reload' %> +

    +<% end %> + +<%= error_messages_for 'query' %> +<% if @query.valid? %> +<% + zoom = 1 + @gantt.zoom.times { zoom = zoom * 2 } + + subject_width = 330 + header_heigth = 18 + + headers_height = header_heigth + show_weeks = false + show_days = false + + if @gantt.zoom > 1 + show_weeks = true + headers_height = 2 * header_heigth + if @gantt.zoom > 2 + show_days = true + headers_height = 3 * header_heigth + end + end + + # Width of the entire chart + g_width = ((@gantt.date_to - @gantt.date_from + 1) * zoom).to_i + @gantt.render(:top => headers_height + 8, + :zoom => zoom, + :g_width => g_width, + :subject_width => subject_width) + g_height = [(20 * (@gantt.number_of_rows + 6)) + 150, 206].max + t_height = g_height + headers_height +%> + +<% if @gantt.truncated %> +

    <%= l(:notice_gantt_chart_truncated, :max => @gantt.max_rows) %>

    +<% end %> + + + + + + + +
    + <% + style = "" + style += "position:relative;" + style += "height: #{t_height + 24}px;" + style += "width: #{subject_width + 1}px;" + %> + <%= content_tag(:div, :style => style) do %> + <% + style = "" + style += "right:-2px;" + style += "width: #{subject_width}px;" + style += "height: #{headers_height}px;" + style += 'background: #eee;' + %> + <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %> + <% + style = "" + style += "right:-2px;" + style += "width: #{subject_width}px;" + style += "height: #{t_height}px;" + style += 'border-left: 1px solid #c0c0c0;' + style += 'overflow: hidden;' + %> + <%= content_tag(:div, "", :style => style, :class => "gantt_hdr") %> + <%= content_tag(:div, :class => "gantt_subjects") do %> + <%= @gantt.subjects.html_safe %> + <% end %> + <% end %> + +
    +<% + style = "" + style += "width: #{g_width - 1}px;" + style += "height: #{headers_height}px;" + style += 'background: #eee;' +%> +<%= content_tag(:div, ' '.html_safe, :style => style, :class => "gantt_hdr") %> + +<% ###### Months headers ###### %> +<% + month_f = @gantt.date_from + left = 0 + height = (show_weeks ? header_heigth : header_heigth + g_height) +%> +<% @gantt.months.times do %> + <% + width = (((month_f >> 1) - month_f) * zoom - 1).to_i + style = "" + style += "left: #{left}px;" + style += "width: #{width}px;" + style += "height: #{height}px;" + %> + <%= content_tag(:div, :style => style, :class => "gantt_hdr") do %> + <%= link_to h("#{month_f.year}-#{month_f.month}"), + @gantt.params.merge(:year => month_f.year, :month => month_f.month), + :title => "#{month_name(month_f.month)} #{month_f.year}" %> + <% end %> + <% + left = left + width + 1 + month_f = month_f >> 1 + %> +<% end %> + +<% ###### Weeks headers ###### %> +<% if show_weeks %> + <% + left = 0 + height = (show_days ? header_heigth - 1 : header_heigth - 1 + g_height) + %> + <% if @gantt.date_from.cwday == 1 %> + <% + # @date_from is monday + week_f = @gantt.date_from + %> + <% else %> + <% + # find next monday after @date_from + week_f = @gantt.date_from + (7 - @gantt.date_from.cwday + 1) + width = (7 - @gantt.date_from.cwday + 1) * zoom - 1 + style = "" + style += "left: #{left}px;" + style += "top: 19px;" + style += "width: #{width}px;" + style += "height: #{height}px;" + %> + <%= content_tag(:div, ' '.html_safe, + :style => style, :class => "gantt_hdr") %> + <% left = left + width + 1 %> + <% end %> + <% while week_f <= @gantt.date_to %> + <% + width = ((week_f + 6 <= @gantt.date_to) ? + 7 * zoom - 1 : + (@gantt.date_to - week_f + 1) * zoom - 1).to_i + style = "" + style += "left: #{left}px;" + style += "top: 19px;" + style += "width: #{width}px;" + style += "height: #{height}px;" + %> + <%= content_tag(:div, :style => style, :class => "gantt_hdr") do %> + <%= content_tag(:small) do %> + <%= week_f.cweek if width >= 16 %> + <% end %> + <% end %> + <% + left = left + width + 1 + week_f = week_f + 7 + %> + <% end %> +<% end %> + +<% ###### Days headers ####### %> +<% if show_days %> + <% + left = 0 + height = g_height + header_heigth - 1 + wday = @gantt.date_from.cwday + %> + <% (@gantt.date_to - @gantt.date_from + 1).to_i.times do %> + <% + width = zoom - 1 + style = "" + style += "left: #{left}px;" + style += "top:37px;" + style += "width: #{width}px;" + style += "height: #{height}px;" + style += "font-size:0.7em;" + clss = "gantt_hdr" + clss << " nwday" if @gantt.non_working_week_days.include?(wday) + %> + <%= content_tag(:div, :style => style, :class => clss) do %> + <%= day_letter(wday) %> + <% end %> + <% + left = left + width + 1 + wday = wday + 1 + wday = 1 if wday > 7 + %> + <% end %> +<% end %> + +<%= @gantt.lines.html_safe %> + +<% ###### Today red line (excluded from cache) ###### %> +<% if Date.today >= @gantt.date_from and Date.today <= @gantt.date_to %> + <% + today_left = (((Date.today - @gantt.date_from + 1) * zoom).floor() - 1).to_i + style = "" + style += "position: absolute;" + style += "height: #{g_height}px;" + style += "top: #{headers_height + 1}px;" + style += "left: #{today_left}px;" + style += "width:10px;" + style += "border-left: 1px dashed red;" + %> + <%= content_tag(:div, ' '.html_safe, :style => style) %> +<% end %> + +
    +
    + + + + + + +
    + <%= link_to_content_update("\xc2\xab " + l(:label_previous), + params.merge(@gantt.params_previous)) %> + + <%= link_to_content_update(l(:label_next) + " \xc2\xbb", + params.merge(@gantt.params_next)) %> +
    + +<% other_formats_links do |f| %> + <%= f.link_to 'PDF', :url => params.merge(@gantt.params) %> + <%= f.link_to('PNG', :url => params.merge(@gantt.params)) if @gantt.respond_to?('to_image') %> +<% end %> +<% end # query.valid? %> + +<% content_for :sidebar do %> + <%= render :partial => 'issues/sidebar' %> +<% end %> + +<% html_title(l(:label_gantt)) -%> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/23/234b04ce45c5af02990560943c7a01a17b8c1584.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/23/234b04ce45c5af02990560943c7a01a17b8c1584.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,18 @@ +<%= javascript_tag do %> +function revisionGraphHandler(){ + drawRevisionGraph( + document.getElementById('holder'), + <%= commits.to_json.html_safe %>, + <%= space %> + ); +} +$(document).ready(revisionGraphHandler); +$(window).resize(revisionGraphHandler); +<% end %> + +
    + +<% content_for :header_tags do %> + <%= javascript_include_tag 'raphael' %> + <%= javascript_include_tag 'revision_graph' %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/23/23d77068631229263f6485f39ba05bd00df8afdf.svn-base Binary file .svn/pristine/23/23d77068631229263f6485f39ba05bd00df8afdf.svn-base has changed diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/24/243b7e66f51039da5e8cb3fc678989779b4b8dd0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/24/243b7e66f51039da5e8cb3fc678989779b4b8dd0.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,15 @@ +

    <%= l(:label_confirmation) %>

    + +<%= form_tag({}, :method => :delete) do %> +<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join("\n").html_safe %> +
    +

    <%= l(:text_destroy_time_entries_question, :hours => number_with_precision(@hours, :precision => 2)) %>

    +

    +
    +
    + +<%= text_field_tag 'reassign_to_id', params[:reassign_to_id], :size => 6, :onfocus => '$("#todo_reassign").attr("checked", true);' %> +

    +
    +<%= submit_tag l(:button_apply) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/24/243de4e756633dfdaa9bf4c362c62553c376fead.svn-base --- a/.svn/pristine/24/243de4e756633dfdaa9bf4c362c62553c376fead.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,43 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module GanttHelper - - def gantt_zoom_link(gantt, in_or_out) - case in_or_out - when :in - if gantt.zoom < 4 - link_to_content_update l(:text_zoom_in), - params.merge(gantt.params.merge(:zoom => (gantt.zoom+1))), - :class => 'icon icon-zoom-in' - else - content_tag('span', l(:text_zoom_in), :class => 'icon icon-zoom-in').html_safe - end - - when :out - if gantt.zoom > 1 - link_to_content_update l(:text_zoom_out), - params.merge(gantt.params.merge(:zoom => (gantt.zoom-1))), - :class => 'icon icon-zoom-out' - else - content_tag('span', l(:text_zoom_out), :class => 'icon icon-zoom-out').html_safe - end - end - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/24/24ef36c7147955d3fac8e868fb99fab632f24089.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/24/24ef36c7147955d3fac8e868fb99fab632f24089.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,100 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class LayoutTest < ActionController::IntegrationTest + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :workflows + + test "browsing to a missing page should render the base layout" do + get "/users/100000000" + + assert_response :not_found + + # UsersController uses the admin layout by default + assert_select "#admin-menu", :count => 0 + end + + test "browsing to an unauthorized page should render the base layout" do + change_user_password('miscuser9', 'test1234') + + log_user('miscuser9','test1234') + + get "/admin" + assert_response :forbidden + assert_select "#admin-menu", :count => 0 + end + + def test_top_menu_and_search_not_visible_when_login_required + with_settings :login_required => '1' do + get '/' + assert_select "#top-menu > ul", 0 + assert_select "#quick-search", 0 + end + end + + def test_top_menu_and_search_visible_when_login_not_required + with_settings :login_required => '0' do + get '/' + assert_select "#top-menu > ul" + assert_select "#quick-search" + end + end + + def test_wiki_formatter_header_tags + Role.anonymous.add_permission! :add_issues + + get '/projects/ecookbook/issues/new' + assert_tag :script, + :attributes => {:src => %r{^/javascripts/jstoolbar/jstoolbar-textile.min.js}}, + :parent => {:tag => 'head'} + end + + def test_calendar_header_tags + with_settings :default_language => 'fr' do + get '/issues' + assert_include "/javascripts/i18n/jquery.ui.datepicker-fr.js", response.body + end + + with_settings :default_language => 'en-GB' do + get '/issues' + assert_include "/javascripts/i18n/jquery.ui.datepicker-en-GB.js", response.body + end + + with_settings :default_language => 'en' do + get '/issues' + assert_not_include "/javascripts/i18n/jquery.ui.datepicker", response.body + end + end + + def test_search_field_outside_project_should_link_to_global_search + get '/' + assert_select 'div#quick-search form[action=/search]' + end + + def test_search_field_inside_project_should_link_to_project_search + get '/projects/ecookbook' + assert_select 'div#quick-search form[action=/projects/ecookbook/search]' + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/25/252eb4f2f106201746352d88a816534e53e8ac4b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/25/252eb4f2f106201746352d88a816534e53e8ac4b.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,49 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine #:nodoc: + module CoreExtensions #:nodoc: + module String #:nodoc: + # Custom string conversions + module Conversions + # Parses hours format and returns a float + def to_hours + s = self.dup + s.strip! + if s =~ %r{^(\d+([.,]\d+)?)h?$} + s = $1 + else + # 2:30 => 2.5 + s.gsub!(%r{^(\d+):(\d+)$}) { $1.to_i + $2.to_i / 60.0 } + # 2h30, 2h, 30m => 2.5, 2, 0.5 + s.gsub!(%r{^((\d+)\s*(h|hours?))?\s*((\d+)\s*(m|min)?)?$}i) { |m| ($1 || $4) ? ($2.to_i + $5.to_i / 60.0) : m[0] } + end + # 2,5 => 2.5 + s.gsub!(',', '.') + begin; Kernel.Float(s); rescue; nil; end + end + + # Object#to_a removed in ruby1.9 + if RUBY_VERSION > '1.9' + def to_a + [self.dup] + end + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/25/25447f18997fb5819d5b27f060defe1630ee82df.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/25/25447f18997fb5819d5b27f060defe1630ee82df.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,17 @@ +<%= board_breadcrumb(@message) %> + +

    <%= avatar(@topic.author, :size => "24") %><%=h @topic.subject %>

    + +<%= form_for @message, { + :as => :message, + :url => {:action => 'edit'}, + :html => {:multipart => true, + :id => 'message-form', + :method => :post} + } do |f| %> + <%= render :partial => 'form', + :locals => {:f => f, :replying => !@message.parent.nil?} %> + <%= submit_tag l(:button_save) %> + <%= preview_link({:controller => 'messages', :action => 'preview', :board_id => @board, :id => @message}, 'message-form') %> +<% end %> +
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/25/259870e377324ce9639c6de14a859fead9c01fbd.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/25/259870e377324ce9639c6de14a859fead9c01fbd.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,18 @@ +<%= form_tag({:controller => 'journals', :action => 'edit', :id => @journal}, + :remote => true, + :id => "journal-#{@journal.id}-form") do %> + <%= label_tag "notes", l(:description_notes), :class => "hidden-for-sighted" %> + <%= text_area_tag :notes, @journal.notes, + :id => "journal_#{@journal.id}_notes", + :class => 'wiki-edit', + :rows => (@journal.notes.blank? ? 10 : [[10, @journal.notes.length / 50].max, 100].min) %> + <%= call_hook(:view_journals_notes_form_after_notes, { :journal => @journal}) %> +

    <%= submit_tag l(:button_save) %> + <%= preview_link preview_edit_issue_path(:project_id => @project, :id => @journal.issue), + "journal-#{@journal.id}-form", + "journal_#{@journal.id}_preview" %> | + <%= link_to l(:button_cancel), '#', :onclick => "$('#journal-#{@journal.id}-form').remove(); $('#journal-#{@journal.id}-notes').show(); return false;" %>

    + +
    +<% end %> +<%= wikitoolbar_for "journal_#{@journal.id}_notes" %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/25/25a248b972bed2adf3bfae26d2e777b6d1e986f8.svn-base --- a/.svn/pristine/25/25a248b972bed2adf3bfae26d2e777b6d1e986f8.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module TrackersHelper -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/25/25a7a4fdea85fbb1b14478fb690111974ca9c5c6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/25/25a7a4fdea85fbb1b14478fb690111974ca9c5c6.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,40 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class DocumentCategory < Enumeration + has_many :documents, :foreign_key => 'category_id' + + OptionName = :enumeration_doc_categories + + def option_name + OptionName + end + + def objects_count + documents.count + end + + def transfer_relations(to) + documents.update_all("category_id = #{to.id}") + end + + def self.default + d = super + d = first if d.nil? + d + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/25/25cdcad944ee39e6a9c2a56e5a525014ecebc7bc.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/25/25cdcad944ee39e6a9c2a56e5a525014ecebc7bc.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,525 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class RepositoriesMercurialControllerTest < ActionController::TestCase + tests RepositoriesController + + fixtures :projects, :users, :roles, :members, :member_roles, + :repositories, :enabled_modules + + REPOSITORY_PATH = Rails.root.join('tmp/test/mercurial_repository').to_s + CHAR_1_HEX = "\xc3\x9c" + PRJ_ID = 3 + NUM_REV = 32 + + ruby19_non_utf8_pass = + (RUBY_VERSION >= '1.9' && Encoding.default_external.to_s != 'UTF-8') + + def setup + User.current = nil + @project = Project.find(PRJ_ID) + @repository = Repository::Mercurial.create( + :project => @project, + :url => REPOSITORY_PATH, + :path_encoding => 'ISO-8859-1' + ) + assert @repository + @diff_c_support = true + @char_1 = CHAR_1_HEX.dup + @tag_char_1 = "tag-#{CHAR_1_HEX}-00" + @branch_char_0 = "branch-#{CHAR_1_HEX}-00" + @branch_char_1 = "branch-#{CHAR_1_HEX}-01" + if @char_1.respond_to?(:force_encoding) + @char_1.force_encoding('UTF-8') + @tag_char_1.force_encoding('UTF-8') + @branch_char_0.force_encoding('UTF-8') + @branch_char_1.force_encoding('UTF-8') + end + end + + if ruby19_non_utf8_pass + puts "TODO: Mercurial functional test fails in Ruby 1.9 " + + "and Encoding.default_external is not UTF-8. " + + "Current value is '#{Encoding.default_external.to_s}'" + def test_fake; assert true end + elsif File.directory?(REPOSITORY_PATH) + + def test_get_new + @request.session[:user_id] = 1 + @project.repository.destroy + get :new, :project_id => 'subproject1', :repository_scm => 'Mercurial' + assert_response :success + assert_template 'new' + assert_kind_of Repository::Mercurial, assigns(:repository) + assert assigns(:repository).new_record? + end + + def test_show_root + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal 4, assigns(:entries).size + assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'} + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + + def test_show_directory + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param] + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name) + entry = assigns(:entries).detect {|e| e.name == 'edit.png'} + assert_not_nil entry + assert_equal 'file', entry.kind + assert_equal 'images/edit.png', entry.path + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + + def test_show_at_given_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [0, '0', '0885933ad4f6'].each do |r1| + get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param], + :rev => r1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['delete.png'], assigns(:entries).collect(&:name) + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + end + + def test_show_directory_sql_escape_percent + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [13, '13', '3a330eb32958'].each do |r1| + get :show, :id => PRJ_ID, + :path => repository_path_hash(['sql_escape', 'percent%dir'])[:param], + :rev => r1 + assert_response :success + assert_template 'show' + + assert_not_nil assigns(:entries) + assert_equal ['percent%file1.txt', 'percentfile1.txt'], + assigns(:entries).collect(&:name) + changesets = assigns(:changesets) + assert_not_nil changesets + assert assigns(:changesets).size > 0 + assert_equal %w(13 11 10 9), changesets.collect(&:revision) + end + end + + def test_show_directory_latin_1_path + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [21, '21', 'adf805632193'].each do |r1| + get :show, :id => PRJ_ID, + :path => repository_path_hash(['latin-1-dir'])[:param], + :rev => r1 + assert_response :success + assert_template 'show' + + assert_not_nil assigns(:entries) + assert_equal ["make-latin-1-file.rb", + "test-#{@char_1}-1.txt", + "test-#{@char_1}-2.txt", + "test-#{@char_1}.txt"], assigns(:entries).collect(&:name) + changesets = assigns(:changesets) + assert_not_nil changesets + assert_equal %w(21 20 19 18 17), changesets.collect(&:revision) + end + end + + def show_should_show_branch_selection_form + @repository.fetch_changesets + @project.reload + get :show, :id => PRJ_ID + assert_tag 'form', :attributes => {:id => 'revision_selector', :action => '/projects/subproject1/repository/show'} + assert_tag 'select', :attributes => {:name => 'branch'}, + :child => {:tag => 'option', :attributes => {:value => 'test-branch-01'}}, + :parent => {:tag => 'form', :attributes => {:id => 'revision_selector'}} + end + + def test_show_branch + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [ + 'default', + @branch_char_1, + 'branch (1)[2]&,%.-3_4', + @branch_char_0, + 'test_branch.latin-1', + 'test-branch-00', + ].each do |bra| + get :show, :id => PRJ_ID, :rev => bra + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert assigns(:entries).size > 0 + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + end + + def test_show_tag + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [ + @tag_char_1, + 'tag_test.00', + 'tag-init-revision' + ].each do |tag| + get :show, :id => PRJ_ID, :rev => tag + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert assigns(:entries).size > 0 + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + end + + def test_changes + get :changes, :id => PRJ_ID, + :path => repository_path_hash(['images', 'edit.png'])[:param] + assert_response :success + assert_template 'changes' + assert_tag :tag => 'h2', :content => 'edit.png' + end + + def test_entry_show + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param] + assert_response :success + assert_template 'entry' + # Line 10 + assert_tag :tag => 'th', + :content => '10', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ } + end + + def test_entry_show_latin_1_path + [21, '21', 'adf805632193'].each do |r1| + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}-2.txt"])[:param], + :rev => r1 + assert_response :success + assert_template 'entry' + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', + :content => /Mercurial is a distributed version control system/ } + end + end + + def test_entry_show_latin_1_contents + with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do + [27, '27', '7bbf4c738e71'].each do |r1| + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}.txt"])[:param], + :rev => r1 + assert_response :success + assert_template 'entry' + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', + :content => /test-#{@char_1}.txt/ } + end + end + end + + def test_entry_download + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param], + :format => 'raw' + assert_response :success + # File content + assert @response.body.include?('WITHOUT ANY WARRANTY') + end + + def test_entry_binary_force_download + get :entry, :id => PRJ_ID, :rev => 1, + :path => repository_path_hash(['images', 'edit.png'])[:param] + assert_response :success + assert_equal 'image/png', @response.content_type + end + + def test_directory_entry + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources'])[:param] + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entry) + assert_equal 'sources', assigns(:entry).name + end + + def test_diff + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [4, '4', 'def6d2f1254a'].each do |r1| + # Full diff of changeset 4 + ['inline', 'sbs'].each do |dt| + get :diff, :id => PRJ_ID, :rev => r1, :type => dt + assert_response :success + assert_template 'diff' + if @diff_c_support + # Line 22 removed + assert_tag :tag => 'th', + :content => '22', + :sibling => { :tag => 'td', + :attributes => { :class => /diff_out/ }, + :content => /def remove/ } + assert_tag :tag => 'h2', :content => /4:def6d2f1254a/ + end + end + end + end + + def test_diff_two_revs + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [2, '400bb8672109', '400', 400].each do |r1| + [4, 'def6d2f1254a'].each do |r2| + ['inline', 'sbs'].each do |dt| + get :diff, + :id => PRJ_ID, + :rev => r1, + :rev_to => r2, + :type => dt + assert_response :success + assert_template 'diff' + diff = assigns(:diff) + assert_not_nil diff + assert_tag :tag => 'h2', + :content => /4:def6d2f1254a 2:400bb8672109/ + end + end + end + end + + def test_diff_latin_1_path + with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do + [21, 'adf805632193'].each do |r1| + ['inline', 'sbs'].each do |dt| + get :diff, :id => PRJ_ID, :rev => r1, :type => dt + assert_response :success + assert_template 'diff' + assert_tag :tag => 'thead', + :descendant => { + :tag => 'th', + :attributes => { :class => 'filename' } , + :content => /latin-1-dir\/test-#{@char_1}-2.txt/ , + }, + :sibling => { + :tag => 'tbody', + :descendant => { + :tag => 'td', + :attributes => { :class => /diff_in/ }, + :content => /It is written in Python/ + } + } + end + end + end + end + + def test_diff_should_show_modified_filenames + get :diff, :id => PRJ_ID, :rev => '400bb8672109', :type => 'inline' + assert_response :success + assert_template 'diff' + assert_select 'th.filename', :text => 'sources/watchers_controller.rb' + end + + def test_diff_should_show_deleted_filenames + get :diff, :id => PRJ_ID, :rev => 'b3a615152df8', :type => 'inline' + assert_response :success + assert_template 'diff' + assert_select 'th.filename', :text => 'sources/welcome_controller.rb' + end + + def test_annotate + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param] + assert_response :success + assert_template 'annotate' + + # Line 22, revision 4:def6d2f1254a + assert_select 'tr' do + assert_select 'th.line-num', :text => '22' + assert_select 'td.revision', :text => '4:def6d2f1254a' + assert_select 'td.author', :text => 'jsmith' + assert_select 'td', :text => /remove_watcher/ + end + end + + def test_annotate_not_in_tip + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'welcome_controller.rb'])[:param] + assert_response 404 + assert_error_tag :content => /was not found/ + end + + def test_annotate_at_given_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + [2, '400bb8672109', '400', 400].each do |r1| + get :annotate, :id => PRJ_ID, :rev => r1, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param] + assert_response :success + assert_template 'annotate' + assert_tag :tag => 'h2', :content => /@ 2:400bb8672109/ + end + end + + def test_annotate_latin_1_path + [21, '21', 'adf805632193'].each do |r1| + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}-2.txt"])[:param], + :rev => r1 + assert_response :success + assert_template 'annotate' + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => + { + :tag => 'td', + :attributes => { :class => 'revision' }, + :child => { :tag => 'a', :content => '20:709858aafd1b' } + } + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => + { + :tag => 'td' , + :content => 'jsmith' , + :attributes => { :class => 'author' }, + } + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', + :content => /Mercurial is a distributed version control system/ } + + end + end + + def test_annotate_latin_1_contents + with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do + [27, '7bbf4c738e71'].each do |r1| + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['latin-1-dir', "test-#{@char_1}.txt"])[:param], + :rev => r1 + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', + :content => /test-#{@char_1}.txt/ } + end + end + end + + def test_empty_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + ['', ' ', nil].each do |r| + get :revision, :id => PRJ_ID, :rev => r + assert_response 404 + assert_error_tag :content => /was not found/ + end + end + + def test_destroy_valid_repository + @request.session[:user_id] = 1 # admin + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + assert_equal NUM_REV, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + + def test_destroy_invalid_repository + @request.session[:user_id] = 1 # admin + @project.repository.destroy + @repository = Repository::Mercurial.create!( + :project => Project.find(PRJ_ID), + :url => "/invalid", + :path_encoding => 'ISO-8859-1' + ) + @repository.fetch_changesets + assert_equal 0, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + else + puts "Mercurial test repository NOT FOUND. Skipping functional tests !!!" + def test_fake; assert true end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/25/25dce4703e6f8e8002b5671dfd69151214860a9a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/25/25dce4703e6f8e8002b5671dfd69151214860a9a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,32 @@ + + +<% 7.times do |i| %><% end %> + + + +<% day = calendar.startdt +while day <= calendar.enddt %> +<%= ("".html_safe) if day.cwday == calendar.first_wday %> + +<%= ''.html_safe if day.cwday==calendar.last_wday and day!=calendar.enddt %> +<% day = day + 1 +end %> + + +
    <%= day_name( (calendar.first_wday+i)%7 ) %>
    #{(day+(11-day.cwday)%7).cweek} +

    <%= day.day %>

    +<% calendar.events_on(day).each do |i| %> + <% if i.is_a? Issue %> +
    + <%= h("#{i.project} -") unless @project && @project == i.project %> + <%= link_to_issue i, :truncate => 30 %> + <%= render_issue_tooltip i %> +
    + <% else %> + + <%= h("#{i.project} -") unless @project && @project == i.project %> + <%= link_to_version i%> + + <% end %> +<% end %> +
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/25/25ed36e9222f92dc1d4148eef3c37d581a09ee23.svn-base Binary file .svn/pristine/25/25ed36e9222f92dc1d4148eef3c37d581a09ee23.svn-base has changed diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/26/2610f5493d7250662d4298685db33ab62ac8d21f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/26/2610f5493d7250662d4298685db33ab62ac8d21f.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,44 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class ApiTest::EnumerationsTest < ActionController::IntegrationTest + fixtures :enumerations + + def setup + Setting.rest_api_enabled = '1' + end + + context "/enumerations/issue_priorities" do + context "GET" do + + should "return priorities" do + get '/enumerations/issue_priorities.xml' + + assert_response :success + assert_equal 'application/xml', response.content_type + assert_select 'issue_priorities[type=array]' do + assert_select 'issue_priority' do + assert_select 'id', :text => '6' + assert_select 'name', :text => 'High' + end + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/26/2647128cbfe83c34c563d9a0e3659f5a24700cec.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/26/2647128cbfe83c34c563d9a0e3659f5a24700cec.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,21 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module IssueStatusesHelper +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/26/26d1f0e306c560c0d8ab8e97cc50b4f5ea6a381f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/26/26d1f0e306c560c0d8ab8e97cc50b4f5ea6a381f.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,23 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class UserCustomField < CustomField + def type_name + :label_user_plural + end +end + diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/26/26d549e31ef6d77ef22f8be4d975f3ff8dfc16bc.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/26/26d549e31ef6d77ef22f8be4d975f3ff8dfc16bc.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1085 @@ +# Redmine catalan translation: +# by Joan Duran + +ca: + # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) + direction: ltr + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%d-%m-%Y" + short: "%e de %b" + long: "%a, %e de %b de %Y" + + day_names: [Diumenge, Dilluns, Dimarts, Dimecres, Dijous, Divendres, Dissabte] + abbr_day_names: [dg, dl, dt, dc, dj, dv, ds] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, Gener, Febrer, Març, Abril, Maig, Juny, Juliol, Agost, Setembre, Octubre, Novembre, Desembre] + abbr_month_names: [~, Gen, Feb, Mar, Abr, Mai, Jun, Jul, Ago, Set, Oct, Nov, Des] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%d-%m-%Y %H:%M" + time: "%H:%M" + short: "%e de %b, %H:%M" + long: "%a, %e de %b de %Y, %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "mig minut" + less_than_x_seconds: + one: "menys d'un segon" + other: "menys de %{count} segons" + x_seconds: + one: "1 segons" + other: "%{count} segons" + less_than_x_minutes: + one: "menys d'un minut" + other: "menys de %{count} minuts" + x_minutes: + one: "1 minut" + other: "%{count} minuts" + about_x_hours: + one: "aproximadament 1 hora" + other: "aproximadament %{count} hores" + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "1 dia" + other: "%{count} dies" + about_x_months: + one: "aproximadament 1 mes" + other: "aproximadament %{count} mesos" + x_months: + one: "1 mes" + other: "%{count} mesos" + about_x_years: + one: "aproximadament 1 any" + other: "aproximadament %{count} anys" + over_x_years: + one: "més d'un any" + other: "més de %{count} anys" + almost_x_years: + one: "almost 1 year" + other: "almost %{count} years" + + number: + # Default format for numbers + format: + separator: "." + delimiter: "" + precision: 3 + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + +# Used in array.to_sentence. + support: + array: + sentence_connector: "i" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + messages: + inclusion: "no està inclòs a la llista" + exclusion: "està reservat" + invalid: "no és vàlid" + confirmation: "la confirmació no coincideix" + accepted: "s'ha d'acceptar" + empty: "no pot estar buit" + blank: "no pot estar en blanc" + too_long: "és massa llarg" + too_short: "és massa curt" + wrong_length: "la longitud és incorrecta" + taken: "ja s'està utilitzant" + not_a_number: "no és un número" + not_a_date: "no és una data vàlida" + greater_than: "ha de ser més gran que %{count}" + greater_than_or_equal_to: "ha de ser més gran o igual a %{count}" + equal_to: "ha de ser igual a %{count}" + less_than: "ha de ser menys que %{count}" + less_than_or_equal_to: "ha de ser menys o igual a %{count}" + odd: "ha de ser senar" + even: "ha de ser parell" + greater_than_start_date: "ha de ser superior que la data inicial" + not_same_project: "no pertany al mateix projecte" + circular_dependency: "Aquesta relació crearia una dependència circular" + cant_link_an_issue_with_a_descendant: "Un assumpte no es pot enllaçar a una de les seves subtasques" + + actionview_instancetag_blank_option: Seleccioneu + + general_text_No: 'No' + general_text_Yes: 'Si' + general_text_no: 'no' + general_text_yes: 'si' + general_lang_name: 'Català' + general_csv_separator: ';' + general_csv_decimal_separator: ',' + general_csv_encoding: ISO-8859-15 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: "El compte s'ha actualitzat correctament." + notice_account_invalid_creditentials: Usuari o contrasenya invàlid + notice_account_password_updated: "La contrasenya s'ha modificat correctament." + notice_account_wrong_password: Contrasenya incorrecta + notice_account_register_done: "El compte s'ha creat correctament. Per a activar el compte, feu clic en l'enllaç que us han enviat per correu electrònic." + notice_account_unknown_email: Usuari desconegut. + notice_can_t_change_password: "Aquest compte utilitza una font d'autenticació externa. No és possible canviar la contrasenya." + notice_account_lost_email_sent: "S'ha enviat un correu electrònic amb instruccions per a seleccionar una contrasenya nova." + notice_account_activated: "El compte s'ha activat. Ara podeu entrar." + notice_successful_create: "S'ha creat correctament." + notice_successful_update: "S'ha modificat correctament." + notice_successful_delete: "S'ha suprimit correctament." + notice_successful_connection: "S'ha connectat correctament." + notice_file_not_found: "La pàgina a la que intenteu accedir no existeix o s'ha suprimit." + notice_locking_conflict: Un altre usuari ha actualitzat les dades. + notice_not_authorized: No teniu permís per a accedir a aquesta pàgina. + notice_email_sent: "S'ha enviat un correu electrònic a %{value}" + notice_email_error: "S'ha produït un error en enviar el correu (%{value})" + notice_feeds_access_key_reseted: "S'ha reiniciat la clau d'accés del RSS." + notice_api_access_key_reseted: "S'ha reiniciat la clau d'accés a l'API." + notice_failed_to_save_issues: "No s'han pogut desar %{count} assumptes de %{total} seleccionats: %{ids}." + notice_failed_to_save_members: "No s'han pogut desar els membres: %{errors}." + notice_no_issue_selected: "No s'ha seleccionat cap assumpte. Activeu els assumptes que voleu editar." + notice_account_pending: "S'ha creat el compte i ara està pendent de l'aprovació de l'administrador." + notice_default_data_loaded: "S'ha carregat correctament la configuració predeterminada." + notice_unable_delete_version: "No s'ha pogut suprimir la versió." + notice_unable_delete_time_entry: "No s'ha pogut suprimir l'entrada del registre de temps." + notice_issue_done_ratios_updated: "S'ha actualitzat el tant per cent dels assumptes." + + error_can_t_load_default_data: "No s'ha pogut carregar la configuració predeterminada: %{value} " + error_scm_not_found: "No s'ha trobat l'entrada o la revisió en el dipòsit." + error_scm_command_failed: "S'ha produït un error en intentar accedir al dipòsit: %{value}" + error_scm_annotate: "L'entrada no existeix o no s'ha pogut anotar." + error_issue_not_found_in_project: "No s'ha trobat l'assumpte o no pertany a aquest projecte" + error_no_tracker_in_project: "Aquest projecte no té seguidor associat. Comproveu els paràmetres del projecte." + error_no_default_issue_status: "No s'ha definit cap estat d'assumpte predeterminat. Comproveu la configuració (aneu a «Administració -> Estats de l'assumpte»)." + error_can_not_delete_custom_field: "No s'ha pogut suprimir el camp personalitat" + error_can_not_delete_tracker: "Aquest seguidor conté assumptes i no es pot suprimir." + error_can_not_remove_role: "Aquest rol s'està utilitzant i no es pot suprimir." + error_can_not_reopen_issue_on_closed_version: "Un assumpte assignat a una versió tancada no es pot tornar a obrir" + error_can_not_archive_project: "Aquest projecte no es pot arxivar" + error_issue_done_ratios_not_updated: "No s'ha actualitza el tant per cent dels assumptes." + error_workflow_copy_source: "Seleccioneu un seguidor o rol font" + error_workflow_copy_target: "Seleccioneu seguidors i rols objectiu" + error_unable_delete_issue_status: "No s'ha pogut suprimir l'estat de l'assumpte" + error_unable_to_connect: "No s'ha pogut connectar (%{value})" + warning_attachments_not_saved: "No s'han pogut desar %{count} fitxers." + + mail_subject_lost_password: "Contrasenya de %{value}" + mail_body_lost_password: "Per a canviar la contrasenya, feu clic en l'enllaç següent:" + mail_subject_register: "Activació del compte de %{value}" + mail_body_register: "Per a activar el compte, feu clic en l'enllaç següent:" + mail_body_account_information_external: "Podeu utilitzar el compte «%{value}» per a entrar." + mail_body_account_information: Informació del compte + mail_subject_account_activation_request: "Sol·licitud d'activació del compte de %{value}" + mail_body_account_activation_request: "S'ha registrat un usuari nou (%{value}). El seu compte està pendent d'aprovació:" + mail_subject_reminder: "%{count} assumptes venceran els següents %{days} dies" + mail_body_reminder: "%{count} assumptes que teniu assignades venceran els següents %{days} dies:" + mail_subject_wiki_content_added: "S'ha afegit la pàgina wiki «%{id}»" + mail_body_wiki_content_added: "En %{author} ha afegit la pàgina wiki «%{id}»." + mail_subject_wiki_content_updated: "S'ha actualitzat la pàgina wiki «%{id}»" + mail_body_wiki_content_updated: "En %{author} ha actualitzat la pàgina wiki «%{id}»." + + gui_validation_error: 1 error + gui_validation_error_plural: "%{count} errors" + + field_name: Nom + field_description: Descripció + field_summary: Resum + field_is_required: Necessari + field_firstname: Nom + field_lastname: Cognom + field_mail: Correu electrònic + field_filename: Fitxer + field_filesize: Mida + field_downloads: Baixades + field_author: Autor + field_created_on: Creat + field_updated_on: Actualitzat + field_field_format: Format + field_is_for_all: Per a tots els projectes + field_possible_values: Valores possibles + field_regexp: Expressió regular + field_min_length: Longitud mínima + field_max_length: Longitud màxima + field_value: Valor + field_category: Categoria + field_title: Títol + field_project: Projecte + field_issue: Assumpte + field_status: Estat + field_notes: Notes + field_is_closed: Assumpte tancat + field_is_default: Estat predeterminat + field_tracker: Seguidor + field_subject: Tema + field_due_date: Data de venciment + field_assigned_to: Assignat a + field_priority: Prioritat + field_fixed_version: Versió objectiu + field_user: Usuari + field_principal: Principal + field_role: Rol + field_homepage: Pàgina web + field_is_public: Públic + field_parent: Subprojecte de + field_is_in_roadmap: Assumptes mostrats en la planificació + field_login: Entrada + field_mail_notification: Notificacions per correu electrònic + field_admin: Administrador + field_last_login_on: Última connexió + field_language: Idioma + field_effective_date: Data + field_password: Contrasenya + field_new_password: Contrasenya nova + field_password_confirmation: Confirmació + field_version: Versió + field_type: Tipus + field_host: Ordinador + field_port: Port + field_account: Compte + field_base_dn: Base DN + field_attr_login: "Atribut d'entrada" + field_attr_firstname: Atribut del nom + field_attr_lastname: Atribut del cognom + field_attr_mail: Atribut del correu electrònic + field_onthefly: "Creació de l'usuari «al vol»" + field_start_date: Inici + field_done_ratio: "% realitzat" + field_auth_source: "Mode d'autenticació" + field_hide_mail: "Oculta l'adreça de correu electrònic" + field_comments: Comentari + field_url: URL + field_start_page: Pàgina inicial + field_subproject: Subprojecte + field_hours: Hores + field_activity: Activitat + field_spent_on: Data + field_identifier: Identificador + field_is_filter: "S'ha utilitzat com a filtre" + field_issue_to: Assumpte relacionat + field_delay: Retard + field_assignable: Es poden assignar assumptes a aquest rol + field_redirect_existing_links: Redirigeix els enllaços existents + field_estimated_hours: Temps previst + field_column_names: Columnes + field_time_entries: "Registre de temps" + field_time_zone: Zona horària + field_searchable: Es pot cercar + field_default_value: Valor predeterminat + field_comments_sorting: Mostra els comentaris + field_parent_title: Pàgina pare + field_editable: Es pot editar + field_watcher: Vigilància + field_identity_url: URL OpenID + field_content: Contingut + field_group_by: "Agrupa els resultats per" + field_sharing: Compartició + field_parent_issue: "Tasca pare" + + setting_app_title: "Títol de l'aplicació" + setting_app_subtitle: "Subtítol de l'aplicació" + setting_welcome_text: Text de benvinguda + setting_default_language: Idioma predeterminat + setting_login_required: Es necessita autenticació + setting_self_registration: Registre automàtic + setting_attachment_max_size: Mida màxima dels adjunts + setting_issues_export_limit: "Límit d'exportació d'assumptes" + setting_mail_from: "Adreça de correu electrònic d'emissió" + setting_bcc_recipients: Vincula els destinataris de les còpies amb carbó (bcc) + setting_plain_text_mail: només text pla (no HTML) + setting_host_name: "Nom de l'ordinador" + setting_text_formatting: Format del text + setting_wiki_compression: "Comprimeix l'historial del wiki" + setting_feeds_limit: Límit de contingut del canal + setting_default_projects_public: Els projectes nous són públics per defecte + setting_autofetch_changesets: Omple automàticament les publicacions + setting_sys_api_enabled: Habilita el WS per a la gestió del dipòsit + setting_commit_ref_keywords: Paraules claus per a la referència + setting_commit_fix_keywords: Paraules claus per a la correcció + setting_autologin: Entrada automàtica + setting_date_format: Format de la data + setting_time_format: Format de hora + setting_cross_project_issue_relations: "Permet les relacions d'assumptes entre projectes" + setting_issue_list_default_columns: "Columnes mostrades per defecte en la llista d'assumptes" + setting_emails_footer: Peu dels correus electrònics + setting_protocol: Protocol + setting_per_page_options: Opcions dels objectes per pàgina + setting_user_format: "Format de com mostrar l'usuari" + setting_activity_days_default: "Dies a mostrar l'activitat del projecte" + setting_display_subprojects_issues: "Mostra els assumptes d'un subprojecte en el projecte pare per defecte" + setting_enabled_scm: "Habilita l'SCM" + setting_mail_handler_body_delimiters: "Trunca els correus electrònics després d'una d'aquestes línies" + setting_mail_handler_api_enabled: "Habilita el WS per correus electrònics d'entrada" + setting_mail_handler_api_key: Clau API + setting_sequential_project_identifiers: Genera identificadors de projecte seqüencials + setting_gravatar_enabled: "Utilitza les icones d'usuari Gravatar" + setting_gravatar_default: "Imatge Gravatar predeterminada" + setting_diff_max_lines_displayed: Número màxim de línies amb diferències mostrades + setting_file_max_size_displayed: Mida màxima dels fitxers de text mostrats en línia + setting_repository_log_display_limit: Número màxim de revisions que es mostren al registre de fitxers + setting_openid: "Permet entrar i registrar-se amb l'OpenID" + setting_password_min_length: "Longitud mínima de la contrasenya" + setting_new_project_user_role_id: "Aquest rol es dóna a un usuari no administrador per a crear projectes" + setting_default_projects_modules: "Mòduls activats per defecte en els projectes nous" + setting_issue_done_ratio: "Calcula tant per cent realitzat de l'assumpte amb" + setting_issue_done_ratio_issue_status: "Utilitza l'estat de l'assumpte" + setting_issue_done_ratio_issue_field: "Utilitza el camp de l'assumpte" + setting_start_of_week: "Inicia les setmanes en" + setting_rest_api_enabled: "Habilita el servei web REST" + setting_cache_formatted_text: Cache formatted text + + permission_add_project: "Crea projectes" + permission_add_subprojects: "Crea subprojectes" + permission_edit_project: Edita el projecte + permission_select_project_modules: Selecciona els mòduls del projecte + permission_manage_members: Gestiona els membres + permission_manage_project_activities: "Gestiona les activitats del projecte" + permission_manage_versions: Gestiona les versions + permission_manage_categories: Gestiona les categories dels assumptes + permission_view_issues: "Visualitza els assumptes" + permission_add_issues: Afegeix assumptes + permission_edit_issues: Edita els assumptes + permission_manage_issue_relations: Gestiona les relacions dels assumptes + permission_add_issue_notes: Afegeix notes + permission_edit_issue_notes: Edita les notes + permission_edit_own_issue_notes: Edita les notes pròpies + permission_move_issues: Mou els assumptes + permission_delete_issues: Suprimeix els assumptes + permission_manage_public_queries: Gestiona les consultes públiques + permission_save_queries: Desa les consultes + permission_view_gantt: Visualitza la gràfica de Gantt + permission_view_calendar: Visualitza el calendari + permission_view_issue_watchers: Visualitza la llista de vigilàncies + permission_add_issue_watchers: Afegeix vigilàncies + permission_delete_issue_watchers: Suprimeix els vigilants + permission_log_time: Registra el temps invertit + permission_view_time_entries: Visualitza el temps invertit + permission_edit_time_entries: Edita els registres de temps + permission_edit_own_time_entries: Edita els registres de temps propis + permission_manage_news: Gestiona les noticies + permission_comment_news: Comenta les noticies + permission_manage_documents: Gestiona els documents + permission_view_documents: Visualitza els documents + permission_manage_files: Gestiona els fitxers + permission_view_files: Visualitza els fitxers + permission_manage_wiki: Gestiona el wiki + permission_rename_wiki_pages: Canvia el nom de les pàgines wiki + permission_delete_wiki_pages: Suprimeix les pàgines wiki + permission_view_wiki_pages: Visualitza el wiki + permission_view_wiki_edits: "Visualitza l'historial del wiki" + permission_edit_wiki_pages: Edita les pàgines wiki + permission_delete_wiki_pages_attachments: Suprimeix adjunts + permission_protect_wiki_pages: Protegeix les pàgines wiki + permission_manage_repository: Gestiona el dipòsit + permission_browse_repository: Navega pel dipòsit + permission_view_changesets: Visualitza els canvis realitzats + permission_commit_access: Accés a les publicacions + permission_manage_boards: Gestiona els taulers + permission_view_messages: Visualitza els missatges + permission_add_messages: Envia missatges + permission_edit_messages: Edita els missatges + permission_edit_own_messages: Edita els missatges propis + permission_delete_messages: Suprimeix els missatges + permission_delete_own_messages: Suprimeix els missatges propis + permission_export_wiki_pages: "Exporta les pàgines wiki" + permission_manage_subtasks: "Gestiona subtasques" + + project_module_issue_tracking: "Seguidor d'assumptes" + project_module_time_tracking: Seguidor de temps + project_module_news: Noticies + project_module_documents: Documents + project_module_files: Fitxers + project_module_wiki: Wiki + project_module_repository: Dipòsit + project_module_boards: Taulers + project_module_calendar: Calendari + project_module_gantt: Gantt + + label_user: Usuari + label_user_plural: Usuaris + label_user_new: Usuari nou + label_user_anonymous: Anònim + label_project: Projecte + label_project_new: Projecte nou + label_project_plural: Projectes + label_x_projects: + zero: cap projecte + one: 1 projecte + other: "%{count} projectes" + label_project_all: Tots els projectes + label_project_latest: Els últims projectes + label_issue: Assumpte + label_issue_new: Assumpte nou + label_issue_plural: Assumptes + label_issue_view_all: Visualitza tots els assumptes + label_issues_by: "Assumptes per %{value}" + label_issue_added: Assumpte afegit + label_issue_updated: Assumpte actualitzat + label_document: Document + label_document_new: Document nou + label_document_plural: Documents + label_document_added: Document afegit + label_role: Rol + label_role_plural: Rols + label_role_new: Rol nou + label_role_and_permissions: Rols i permisos + label_member: Membre + label_member_new: Membre nou + label_member_plural: Membres + label_tracker: Seguidor + label_tracker_plural: Seguidors + label_tracker_new: Seguidor nou + label_workflow: Flux de treball + label_issue_status: "Estat de l'assumpte" + label_issue_status_plural: "Estats de l'assumpte" + label_issue_status_new: Estat nou + label_issue_category: "Categoria de l'assumpte" + label_issue_category_plural: "Categories de l'assumpte" + label_issue_category_new: Categoria nova + label_custom_field: Camp personalitzat + label_custom_field_plural: Camps personalitzats + label_custom_field_new: Camp personalitzat nou + label_enumerations: Enumeracions + label_enumeration_new: Valor nou + label_information: Informació + label_information_plural: Informació + label_please_login: Entreu + label_register: Registre + label_login_with_open_id_option: "o entra amb l'OpenID" + label_password_lost: Contrasenya perduda + label_home: Inici + label_my_page: La meva pàgina + label_my_account: El meu compte + label_my_projects: Els meus projectes + label_my_page_block: "Els meus blocs de pàgina" + label_administration: Administració + label_login: Entra + label_logout: Surt + label_help: Ajuda + label_reported_issues: Assumptes informats + label_assigned_to_me_issues: Assumptes assignats a mi + label_last_login: Última connexió + label_registered_on: Informat el + label_activity: Activitat + label_overall_activity: Activitat global + label_user_activity: "Activitat de %{value}" + label_new: Nou + label_logged_as: Heu entrat com a + label_environment: Entorn + label_authentication: Autenticació + label_auth_source: "Mode d'autenticació" + label_auth_source_new: "Mode d'autenticació nou" + label_auth_source_plural: "Modes d'autenticació" + label_subproject_plural: Subprojectes + label_subproject_new: "Subprojecte nou" + label_and_its_subprojects: "%{value} i els seus subprojectes" + label_min_max_length: Longitud mín - max + label_list: Llist + label_date: Data + label_integer: Enter + label_float: Flotant + label_boolean: Booleà + label_string: Text + label_text: Text llarg + label_attribute: Atribut + label_attribute_plural: Atributs + label_download: "%{count} baixada" + label_download_plural: "%{count} baixades" + label_no_data: Sense dades a mostrar + label_change_status: "Canvia l'estat" + label_history: Historial + label_attachment: Fitxer + label_attachment_new: Fitxer nou + label_attachment_delete: Suprimeix el fitxer + label_attachment_plural: Fitxers + label_file_added: Fitxer afegit + label_report: Informe + label_report_plural: Informes + label_news: Noticies + label_news_new: Afegeix noticies + label_news_plural: Noticies + label_news_latest: Últimes noticies + label_news_view_all: Visualitza totes les noticies + label_news_added: Noticies afegides + label_settings: Paràmetres + label_overview: Resum + label_version: Versió + label_version_new: Versió nova + label_version_plural: Versions + label_close_versions: "Tanca les versions completades" + label_confirmation: Confirmació + label_export_to: "També disponible a:" + label_read: Llegeix... + label_public_projects: Projectes públics + label_open_issues: obert + label_open_issues_plural: oberts + label_closed_issues: tancat + label_closed_issues_plural: tancats + label_x_open_issues_abbr_on_total: + zero: 0 oberts / %{total} + one: 1 obert / %{total} + other: "%{count} oberts / %{total}" + label_x_open_issues_abbr: + zero: 0 oberts + one: 1 obert + other: "%{count} oberts" + label_x_closed_issues_abbr: + zero: 0 tancats + one: 1 tancat + other: "%{count} tancats" + label_total: Total + label_permissions: Permisos + label_current_status: Estat actual + label_new_statuses_allowed: Nous estats autoritzats + label_all: tots + label_none: cap + label_nobody: ningú + label_next: Següent + label_previous: Anterior + label_used_by: Utilitzat per + label_details: Detalls + label_add_note: Afegeix una nota + label_per_page: Per pàgina + label_calendar: Calendari + label_months_from: mesos des de + label_gantt: Gantt + label_internal: Intern + label_last_changes: "últims %{count} canvis" + label_change_view_all: Visualitza tots els canvis + label_personalize_page: Personalitza aquesta pàgina + label_comment: Comentari + label_comment_plural: Comentaris + label_x_comments: + zero: sense comentaris + one: 1 comentari + other: "%{count} comentaris" + label_comment_add: Afegeix un comentari + label_comment_added: Comentari afegit + label_comment_delete: Suprimeix comentaris + label_query: Consulta personalitzada + label_query_plural: Consultes personalitzades + label_query_new: Consulta nova + label_filter_add: Afegeix un filtre + label_filter_plural: Filtres + label_equals: és + label_not_equals: no és + label_in_less_than: en menys de + label_in_more_than: en més de + label_greater_or_equal: ">=" + label_less_or_equal: <= + label_in: en + label_today: avui + label_all_time: tot el temps + label_yesterday: ahir + label_this_week: aquesta setmana + label_last_week: "l'última setmana" + label_last_n_days: "els últims %{count} dies" + label_this_month: aquest més + label_last_month: "l'últim més" + label_this_year: aquest any + label_date_range: Abast de les dates + label_less_than_ago: fa menys de + label_more_than_ago: fa més de + label_ago: fa + label_contains: conté + label_not_contains: no conté + label_day_plural: dies + label_repository: Dipòsit + label_repository_plural: Dipòsits + label_browse: Navega + label_modification: "%{count} canvi" + label_modification_plural: "%{count} canvis" + label_branch: Branca + label_tag: Etiqueta + label_revision: Revisió + label_revision_plural: Revisions + label_revision_id: "Revisió %{value}" + label_associated_revisions: Revisions associades + label_added: afegit + label_modified: modificat + label_copied: copiat + label_renamed: reanomenat + label_deleted: suprimit + label_latest_revision: Última revisió + label_latest_revision_plural: Últimes revisions + label_view_revisions: Visualitza les revisions + label_view_all_revisions: "Visualitza totes les revisions" + label_max_size: Mida màxima + label_sort_highest: Mou a la part superior + label_sort_higher: Mou cap amunt + label_sort_lower: Mou cap avall + label_sort_lowest: Mou a la part inferior + label_roadmap: Planificació + label_roadmap_due_in: "Venç en %{value}" + label_roadmap_overdue: "%{value} tard" + label_roadmap_no_issues: No hi ha assumptes per a aquesta versió + label_search: Cerca + label_result_plural: Resultats + label_all_words: Totes les paraules + label_wiki: Wiki + label_wiki_edit: Edició wiki + label_wiki_edit_plural: Edicions wiki + label_wiki_page: Pàgina wiki + label_wiki_page_plural: Pàgines wiki + label_index_by_title: Ãndex per títol + label_index_by_date: Ãndex per data + label_current_version: Versió actual + label_preview: Previsualització + label_feed_plural: Canals + label_changes_details: Detalls de tots els canvis + label_issue_tracking: "Seguiment d'assumptes" + label_spent_time: Temps invertit + label_overall_spent_time: "Temps total invertit" + label_f_hour: "%{value} hora" + label_f_hour_plural: "%{value} hores" + label_time_tracking: Temps de seguiment + label_change_plural: Canvis + label_statistics: Estadístiques + label_commits_per_month: Publicacions per mes + label_commits_per_author: Publicacions per autor + label_view_diff: Visualitza les diferències + label_diff_inline: en línia + label_diff_side_by_side: costat per costat + label_options: Opcions + label_copy_workflow_from: Copia el flux de treball des de + label_permissions_report: Informe de permisos + label_watched_issues: Assumptes vigilats + label_related_issues: Assumptes relacionats + label_applied_status: Estat aplicat + label_loading: "S'està carregant..." + label_relation_new: Relació nova + label_relation_delete: Suprimeix la relació + label_relates_to: relacionat amb + label_duplicates: duplicats + label_duplicated_by: duplicat per + label_blocks: bloqueja + label_blocked_by: bloquejats per + label_precedes: anterior a + label_follows: posterior a + label_end_to_start: final al començament + label_end_to_end: final al final + label_start_to_start: començament al començament + label_start_to_end: començament al final + label_stay_logged_in: "Manté l'entrada" + label_disabled: inhabilitat + label_show_completed_versions: Mostra les versions completes + label_me: jo mateix + label_board: Fòrum + label_board_new: Fòrum nou + label_board_plural: Fòrums + label_board_locked: Bloquejat + label_board_sticky: Sticky + label_topic_plural: Temes + label_message_plural: Missatges + label_message_last: Últim missatge + label_message_new: Missatge nou + label_message_posted: Missatge afegit + label_reply_plural: Respostes + label_send_information: "Envia la informació del compte a l'usuari" + label_year: Any + label_month: Mes + label_week: Setmana + label_date_from: Des de + label_date_to: A + label_language_based: "Basat en l'idioma de l'usuari" + label_sort_by: "Ordena per %{value}" + label_send_test_email: Envia un correu electrònic de prova + label_feeds_access_key: "Clau d'accés del RSS" + label_missing_feeds_access_key: "Falta una clau d'accés del RSS" + label_feeds_access_key_created_on: "Clau d'accés del RSS creada fa %{value}" + label_module_plural: Mòduls + label_added_time_by: "Afegit per %{author} fa %{age}" + label_updated_time_by: "Actualitzat per %{author} fa %{age}" + label_updated_time: "Actualitzat fa %{value}" + label_jump_to_a_project: Salta al projecte... + label_file_plural: Fitxers + label_changeset_plural: Conjunt de canvis + label_default_columns: Columnes predeterminades + label_no_change_option: (sense canvis) + label_bulk_edit_selected_issues: Edita en bloc els assumptes seleccionats + label_theme: Tema + label_default: Predeterminat + label_search_titles_only: Cerca només en els títols + label_user_mail_option_all: "Per qualsevol esdeveniment en tots els meus projectes" + label_user_mail_option_selected: "Per qualsevol esdeveniment en els projectes seleccionats..." + label_user_mail_no_self_notified: "No vull ser notificat pels canvis que faig jo mateix" + label_registration_activation_by_email: activació del compte per correu electrònic + label_registration_manual_activation: activació del compte manual + label_registration_automatic_activation: activació del compte automàtica + label_display_per_page: "Per pàgina: %{value}" + label_age: Edat + label_change_properties: Canvia les propietats + label_general: General + label_more: Més + label_scm: SCM + label_plugins: Connectors + label_ldap_authentication: Autenticació LDAP + label_downloads_abbr: Baixades + label_optional_description: Descripció opcional + label_add_another_file: Afegeix un altre fitxer + label_preferences: Preferències + label_chronological_order: En ordre cronològic + label_reverse_chronological_order: En ordre cronològic invers + label_planning: Planificació + label_incoming_emails: "Correu electrònics d'entrada" + label_generate_key: Genera una clau + label_issue_watchers: Vigilàncies + label_example: Exemple + label_display: Mostra + label_sort: Ordena + label_ascending: Ascendent + label_descending: Descendent + label_date_from_to: Des de %{start} a %{end} + label_wiki_content_added: "S'ha afegit la pàgina wiki" + label_wiki_content_updated: "S'ha actualitzat la pàgina wiki" + label_group: Grup + label_group_plural: Grups + label_group_new: Grup nou + label_time_entry_plural: Temps invertit + label_version_sharing_hierarchy: "Amb la jerarquia del projecte" + label_version_sharing_system: "Amb tots els projectes" + label_version_sharing_descendants: "Amb tots els subprojectes" + label_version_sharing_tree: "Amb l'arbre del projecte" + label_version_sharing_none: "Sense compartir" + label_update_issue_done_ratios: "Actualitza el tant per cent dels assumptes realitzats" + label_copy_source: Font + label_copy_target: Objectiu + label_copy_same_as_target: "El mateix que l'objectiu" + label_display_used_statuses_only: "Mostra només els estats que utilitza aquest seguidor" + label_api_access_key: "Clau d'accés a l'API" + label_missing_api_access_key: "Falta una clau d'accés de l'API" + label_api_access_key_created_on: "Clau d'accés de l'API creada fa %{value}" + label_profile: Perfil + label_subtask_plural: Subtasques + label_project_copy_notifications: "Envia notificacions de correu electrònic durant la còpia del projecte" + + button_login: Entra + button_submit: Tramet + button_save: Desa + button_check_all: Activa-ho tot + button_uncheck_all: Desactiva-ho tot + button_delete: Suprimeix + button_create: Crea + button_create_and_continue: Crea i continua + button_test: Test + button_edit: Edit + button_add: Afegeix + button_change: Canvia + button_apply: Aplica + button_clear: Neteja + button_lock: Bloca + button_unlock: Desbloca + button_download: Baixa + button_list: Llista + button_view: Visualitza + button_move: Mou + button_move_and_follow: "Mou i segueix" + button_back: Enrere + button_cancel: Cancel·la + button_activate: Activa + button_sort: Ordena + button_log_time: "Registre de temps" + button_rollback: Torna a aquesta versió + button_watch: Vigila + button_unwatch: No vigilis + button_reply: Resposta + button_archive: Arxiva + button_unarchive: Desarxiva + button_reset: Reinicia + button_rename: Reanomena + button_change_password: Canvia la contrasenya + button_copy: Copia + button_copy_and_follow: "Copia i segueix" + button_annotate: Anota + button_update: Actualitza + button_configure: Configura + button_quote: Cita + button_duplicate: Duplica + button_show: Mostra + + status_active: actiu + status_registered: informat + status_locked: bloquejat + + version_status_open: oberta + version_status_locked: bloquejada + version_status_closed: tancada + + field_active: Actiu + + text_select_mail_notifications: "Seleccioneu les accions per les quals s'hauria d'enviar una notificació per correu electrònic." + text_regexp_info: ex. ^[A-Z0-9]+$ + text_min_max_length_info: 0 significa sense restricció + text_project_destroy_confirmation: Segur que voleu suprimir aquest projecte i les dades relacionades? + text_subprojects_destroy_warning: "També seran suprimits els seus subprojectes: %{value}." + text_workflow_edit: Seleccioneu un rol i un seguidor per a editar el flux de treball + text_are_you_sure: Segur? + text_journal_changed: "%{label} ha canviat de %{old} a %{new}" + text_journal_set_to: "%{label} s'ha establert a %{value}" + text_journal_deleted: "%{label} s'ha suprimit (%{old})" + text_journal_added: "S'ha afegit %{label} %{value}" + text_tip_issue_begin_day: "tasca que s'inicia aquest dia" + text_tip_issue_end_day: tasca que finalitza aquest dia + text_tip_issue_begin_end_day: "tasca que s'inicia i finalitza aquest dia" + text_caracters_maximum: "%{count} caràcters com a màxim." + text_caracters_minimum: "Com a mínim ha de tenir %{count} caràcters." + text_length_between: "Longitud entre %{min} i %{max} caràcters." + text_tracker_no_workflow: "No s'ha definit cap flux de treball per a aquest seguidor" + text_unallowed_characters: Caràcters no permesos + text_comma_separated: Es permeten valors múltiples (separats per una coma). + text_line_separated: "Es permeten diversos valors (una línia per cada valor)." + text_issues_ref_in_commit_messages: Referència i soluciona els assumptes en els missatges publicats + text_issue_added: "L'assumpte %{id} ha sigut informat per %{author}." + text_issue_updated: "L'assumpte %{id} ha sigut actualitzat per %{author}." + text_wiki_destroy_confirmation: Segur que voleu suprimir aquest wiki i tots els seus continguts? + text_issue_category_destroy_question: "Alguns assumptes (%{count}) estan assignats a aquesta categoria. Què voleu fer?" + text_issue_category_destroy_assignments: Suprimeix les assignacions de la categoria + text_issue_category_reassign_to: Torna a assignar els assumptes a aquesta categoria + text_user_mail_option: "Per als projectes no seleccionats, només rebreu notificacions sobre les coses que vigileu o que hi esteu implicat (ex. assumptes que en sou l'autor o hi esteu assignat)." + text_no_configuration_data: "Encara no s'han configurat els rols, seguidors, estats de l'assumpte i flux de treball.\nÉs altament recomanable que carregueu la configuració predeterminada. Podreu modificar-la un cop carregada." + text_load_default_configuration: Carrega la configuració predeterminada + text_status_changed_by_changeset: "Aplicat en el conjunt de canvis %{value}." + text_issues_destroy_confirmation: "Segur que voleu suprimir els assumptes seleccionats?" + text_select_project_modules: "Seleccioneu els mòduls a habilitar per a aquest projecte:" + text_default_administrator_account_changed: "S'ha canviat el compte d'administrador predeterminat" + text_file_repository_writable: Es pot escriure en el dipòsit de fitxers + text_plugin_assets_writable: Es pot escriure als connectors actius + text_rmagick_available: RMagick disponible (opcional) + text_destroy_time_entries_question: "S'han informat %{hours} hores en els assumptes que aneu a suprimir. Què voleu fer?" + text_destroy_time_entries: Suprimeix les hores informades + text_assign_time_entries_to_project: Assigna les hores informades al projecte + text_reassign_time_entries: "Torna a assignar les hores informades a aquest assumpte:" + text_user_wrote: "%{value} va escriure:" + text_enumeration_destroy_question: "%{count} objectes estan assignats a aquest valor." + text_enumeration_category_reassign_to: "Torna a assignar-los a aquest valor:" + text_email_delivery_not_configured: "El lliurament per correu electrònic no està configurat i les notificacions estan inhabilitades.\nConfigureu el servidor SMTP a config/configuration.yml i reinicieu l'aplicació per habilitar-lo." + text_repository_usernames_mapping: "Seleccioneu l'assignació entre els usuaris del Redmine i cada nom d'usuari trobat al dipòsit.\nEls usuaris amb el mateix nom d'usuari o correu del Redmine i del dipòsit s'assignaran automàticament." + text_diff_truncated: "... Aquestes diferències s'han trucat perquè excedeixen la mida màxima que es pot mostrar." + text_custom_field_possible_values_info: "Una línia per a cada valor" + text_wiki_page_destroy_question: "Aquesta pàgina té %{descendants} pàgines fill i descendents. Què voleu fer?" + text_wiki_page_nullify_children: "Deixa les pàgines fill com a pàgines arrel" + text_wiki_page_destroy_children: "Suprimeix les pàgines fill i tots els seus descendents" + text_wiki_page_reassign_children: "Reasigna les pàgines fill a aquesta pàgina pare" + text_own_membership_delete_confirmation: "Esteu a punt de suprimir algun o tots els vostres permisos i potser no podreu editar més aquest projecte.\nSegur que voleu continuar?" + text_zoom_in: Redueix + text_zoom_out: Amplia + + default_role_manager: Gestor + default_role_developer: Desenvolupador + default_role_reporter: Informador + default_tracker_bug: Error + default_tracker_feature: Característica + default_tracker_support: Suport + default_issue_status_new: Nou + default_issue_status_in_progress: In Progress + default_issue_status_resolved: Resolt + default_issue_status_feedback: Comentaris + default_issue_status_closed: Tancat + default_issue_status_rejected: Rebutjat + default_doc_category_user: "Documentació d'usuari" + default_doc_category_tech: Documentació tècnica + default_priority_low: Baixa + default_priority_normal: Normal + default_priority_high: Alta + default_priority_urgent: Urgent + default_priority_immediate: Immediata + default_activity_design: Disseny + default_activity_development: Desenvolupament + + enumeration_issue_priorities: Prioritat dels assumptes + enumeration_doc_categories: Categories del document + enumeration_activities: Activitats (seguidor de temps) + enumeration_system_activity: Activitat del sistema + + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Text field + label_user_mail_option_only_owner: Only for things I am the owner of + setting_default_notification_option: Default notification option + label_user_mail_option_only_my_events: Only for things I watch or I'm involved in + label_user_mail_option_only_assigned: Only for things I am assigned to + label_user_mail_option_none: No events + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + notice_not_authorized_archived_project: The project you're trying to access has been archived. + label_principal_search: "Search for user or group:" + label_user_search: "Search for user:" + field_visible: Visible + setting_emails_header: Emails header + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Enable time logging + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maximum number of items displayed on the gantt chart + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: Priority updated + label_issues_visibility_own: Issues created by or assigned to the user + field_issues_visibility: Issues visibility + label_issues_visibility_all: All issues + permission_set_own_issues_private: Set own issues public or private + field_is_private: Private + permission_set_issues_private: Set issues public or private + label_issues_visibility_public: All non private issues + text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). + field_commit_logs_encoding: Codificació dels missatges publicats + field_scm_path_encoding: Path encoding + text_scm_path_encoding_note: "Default: UTF-8" + field_path_to_repository: Path to repository + field_root_directory: Root directory + field_cvs_module: Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Command + text_scm_command_version: Version + label_git_report_last_commit: Report last commit for files and directories + text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 assumpte + one: 1 assumpte + other: "%{count} assumptes" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: tots + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: "Amb tots els subprojectes" + label_cross_project_tree: "Amb l'arbre del projecte" + label_cross_project_hierarchy: "Amb la jerarquia del projecte" + label_cross_project_system: "Amb tots els projectes" + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/27/2708d68d35dcff231feda420c97046b8532681e3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/27/2708d68d35dcff231feda420c97046b8532681e3.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Получер'; +jsToolBar.strings['Italic'] = 'КурÑив'; +jsToolBar.strings['Underline'] = 'Подчертан'; +jsToolBar.strings['Deleted'] = 'Изтрит'; +jsToolBar.strings['Code'] = 'Вграден код'; +jsToolBar.strings['Heading 1'] = 'Заглавие 1'; +jsToolBar.strings['Heading 2'] = 'Заглавие 2'; +jsToolBar.strings['Heading 3'] = 'Заглавие 3'; +jsToolBar.strings['Unordered list'] = 'Ðеподреден ÑпиÑък'; +jsToolBar.strings['Ordered list'] = 'Подреден ÑпиÑък'; +jsToolBar.strings['Quote'] = 'Цитат'; +jsToolBar.strings['Unquote'] = 'Премахване на цитат'; +jsToolBar.strings['Preformatted text'] = 'Preformatted text'; +jsToolBar.strings['Wiki link'] = 'Връзка до Wiki Ñтраница'; +jsToolBar.strings['Image'] = 'Изображение'; diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/28/28353268d3c195811c8a35a1db4bfdbe62848deb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/28/28353268d3c195811c8a35a1db4bfdbe62848deb.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,6 @@ +

    <%= link_to h(document.title), document_path(document) %>

    +

    <%= format_time(document.updated_on) %>

    + +
    + <%= textilizable(truncate_lines(document.description), :object => document) %> +
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/28/28460f7863cf0e308a71ddfcfde8f112f06716f0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/28/28460f7863cf0e308a71ddfcfde8f112f06716f0.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,115 @@ +module ActiveRecord + module Acts + module Tree + def self.included(base) + base.extend(ClassMethods) + end + + # Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children + # association. This requires that you have a foreign key column, which by default is called +parent_id+. + # + # class Category < ActiveRecord::Base + # acts_as_tree :order => "name" + # end + # + # Example: + # root + # \_ child1 + # \_ subchild1 + # \_ subchild2 + # + # root = Category.create("name" => "root") + # child1 = root.children.create("name" => "child1") + # subchild1 = child1.children.create("name" => "subchild1") + # + # root.parent # => nil + # child1.parent # => root + # root.children # => [child1] + # root.children.first.children.first # => subchild1 + # + # In addition to the parent and children associations, the following instance methods are added to the class + # after calling acts_as_tree: + # * siblings - Returns all the children of the parent, excluding the current node ([subchild2] when called on subchild1) + # * self_and_siblings - Returns all the children of the parent, including the current node ([subchild1, subchild2] when called on subchild1) + # * ancestors - Returns all the ancestors of the current node ([child1, root] when called on subchild2) + # * root - Returns the root of the current node (root when called on subchild2) + module ClassMethods + # Configuration options are: + # + # * foreign_key - specifies the column name to use for tracking of the tree (default: +parent_id+) + # * order - makes it possible to sort the children according to this SQL snippet. + # * counter_cache - keeps a count in a +children_count+ column if set to +true+ (default: +false+). + def acts_as_tree(options = {}) + configuration = { :foreign_key => "parent_id", :dependent => :destroy, :order => nil, :counter_cache => nil } + configuration.update(options) if options.is_a?(Hash) + + belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache] + has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => configuration[:dependent] + + class_eval <<-EOV + include ActiveRecord::Acts::Tree::InstanceMethods + + def self.roots + find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}}) + end + + def self.root + find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}}) + end + EOV + end + end + + module InstanceMethods + # Returns list of ancestors, starting from parent until root. + # + # subchild1.ancestors # => [child1, root] + def ancestors + node, nodes = self, [] + nodes << node = node.parent while node.parent + nodes + end + + # Returns list of descendants. + # + # root.descendants # => [child1, subchild1, subchild2] + def descendants(depth=nil) + depth ||= 0 + result = children.dup + unless depth == 1 + result += children.collect {|child| child.descendants(depth-1)}.flatten + end + result + end + + # Returns list of descendants and a reference to the current node. + # + # root.self_and_descendants # => [root, child1, subchild1, subchild2] + def self_and_descendants(depth=nil) + [self] + descendants(depth) + end + + # Returns the root node of the tree. + def root + node = self + node = node.parent while node.parent + node + end + + # Returns all siblings of the current node. + # + # subchild1.siblings # => [subchild2] + def siblings + self_and_siblings - [self] + end + + # Returns all siblings and a reference to the current node. + # + # subchild1.self_and_siblings # => [subchild1, subchild2] + def self_and_siblings + parent ? parent.children : self.class.roots + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/28/284f5f782295ac4c8fe3dbacfe87f5903171e538.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/28/284f5f782295ac4c8fe3dbacfe87f5903171e538.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,126 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class ApiTest::IssueCategoriesTest < ActionController::IntegrationTest + fixtures :projects, :users, :issue_categories, :issues, + :roles, + :member_roles, + :members, + :enabled_modules + + def setup + Setting.rest_api_enabled = '1' + end + + context "GET /projects/:project_id/issue_categories.xml" do + should "return issue categories" do + get '/projects/1/issue_categories.xml', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'issue_categories', + :child => {:tag => 'issue_category', :child => {:tag => 'id', :content => '2'}} + end + end + + context "GET /issue_categories/2.xml" do + should "return requested issue category" do + get '/issue_categories/2.xml', {}, credentials('jsmith') + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'issue_category', + :child => {:tag => 'id', :content => '2'} + end + end + + context "POST /projects/:project_id/issue_categories.xml" do + should "return create issue category" do + assert_difference 'IssueCategory.count' do + post '/projects/1/issue_categories.xml', {:issue_category => {:name => 'API'}}, credentials('jsmith') + end + assert_response :created + assert_equal 'application/xml', @response.content_type + + category = IssueCategory.first(:order => 'id DESC') + assert_equal 'API', category.name + assert_equal 1, category.project_id + end + + context "with invalid parameters" do + should "return errors" do + assert_no_difference 'IssueCategory.count' do + post '/projects/1/issue_categories.xml', {:issue_category => {:name => ''}}, credentials('jsmith') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + + assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"} + end + end + end + + context "PUT /issue_categories/2.xml" do + context "with valid parameters" do + should "update issue category" do + assert_no_difference 'IssueCategory.count' do + put '/issue_categories/2.xml', {:issue_category => {:name => 'API Update'}}, credentials('jsmith') + end + assert_response :ok + assert_equal '', @response.body + assert_equal 'API Update', IssueCategory.find(2).name + end + end + + context "with invalid parameters" do + should "return errors" do + assert_no_difference 'IssueCategory.count' do + put '/issue_categories/2.xml', {:issue_category => {:name => ''}}, credentials('jsmith') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + + assert_tag 'errors', :child => {:tag => 'error', :content => "Name can't be blank"} + end + end + end + + context "DELETE /issue_categories/1.xml" do + should "destroy issue categories" do + assert_difference 'IssueCategory.count', -1 do + delete '/issue_categories/1.xml', {}, credentials('jsmith') + end + assert_response :ok + assert_equal '', @response.body + assert_nil IssueCategory.find_by_id(1) + end + + should "reassign issues with :reassign_to_id param" do + issue_count = Issue.count(:conditions => {:category_id => 1}) + assert issue_count > 0 + + assert_difference 'IssueCategory.count', -1 do + assert_difference 'Issue.count(:conditions => {:category_id => 2})', 3 do + delete '/issue_categories/1.xml', {:reassign_to_id => 2}, credentials('jsmith') + end + end + assert_response :ok + assert_equal '', @response.body + assert_nil IssueCategory.find_by_id(1) + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/28/287ad425b706a1e849a16749d3a013cf82f1d342.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/28/287ad425b706a1e849a16749d3a013cf82f1d342.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,76 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../../test_helper', __FILE__) + +class Redmine::Utils::DateCalculationTest < ActiveSupport::TestCase + include Redmine::Utils::DateCalculation + + def test_working_days_without_non_working_week_days + with_settings :non_working_week_days => [] do + assert_working_days 18, '2012-10-09', '2012-10-27' + assert_working_days 6, '2012-10-09', '2012-10-15' + assert_working_days 5, '2012-10-09', '2012-10-14' + assert_working_days 3, '2012-10-09', '2012-10-12' + assert_working_days 3, '2012-10-14', '2012-10-17' + assert_working_days 16, '2012-10-14', '2012-10-30' + end + end + + def test_working_days_with_non_working_week_days + with_settings :non_working_week_days => %w(6 7) do + assert_working_days 14, '2012-10-09', '2012-10-27' + assert_working_days 4, '2012-10-09', '2012-10-15' + assert_working_days 4, '2012-10-09', '2012-10-14' + assert_working_days 3, '2012-10-09', '2012-10-12' + assert_working_days 8, '2012-10-09', '2012-10-19' + assert_working_days 8, '2012-10-11', '2012-10-23' + assert_working_days 2, '2012-10-14', '2012-10-17' + assert_working_days 11, '2012-10-14', '2012-10-30' + end + end + + def test_add_working_days_without_non_working_week_days + with_settings :non_working_week_days => [] do + assert_add_working_days '2012-10-10', '2012-10-10', 0 + assert_add_working_days '2012-10-11', '2012-10-10', 1 + assert_add_working_days '2012-10-12', '2012-10-10', 2 + assert_add_working_days '2012-10-13', '2012-10-10', 3 + assert_add_working_days '2012-10-25', '2012-10-10', 15 + end + end + + def test_add_working_days_with_non_working_week_days + with_settings :non_working_week_days => %w(6 7) do + assert_add_working_days '2012-10-10', '2012-10-10', 0 + assert_add_working_days '2012-10-11', '2012-10-10', 1 + assert_add_working_days '2012-10-12', '2012-10-10', 2 + assert_add_working_days '2012-10-15', '2012-10-10', 3 + assert_add_working_days '2012-10-31', '2012-10-10', 15 + assert_add_working_days '2012-10-19', '2012-10-09', 8 + assert_add_working_days '2012-10-23', '2012-10-11', 8 + end + end + + def assert_working_days(expected_days, from, to) + assert_equal expected_days, working_days(from.to_date, to.to_date) + end + + def assert_add_working_days(expected_date, from, working_days) + assert_equal expected_date.to_date, add_working_days(from.to_date, working_days) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/28/287b739e36a269ed72c54dcd10ebdefa5f23d6ee.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/28/287b739e36a269ed72c54dcd10ebdefa5f23d6ee.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,173 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'iconv' +require 'net/ldap' +require 'net/ldap/dn' +require 'timeout' + +class AuthSourceLdap < AuthSource + validates_presence_of :host, :port, :attr_login + validates_length_of :name, :host, :maximum => 60, :allow_nil => true + validates_length_of :account, :account_password, :base_dn, :filter, :maximum => 255, :allow_blank => true + validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true + validates_numericality_of :port, :only_integer => true + validates_numericality_of :timeout, :only_integer => true, :allow_blank => true + validate :validate_filter + + before_validation :strip_ldap_attributes + + def initialize(attributes=nil, *args) + super + self.port = 389 if self.port == 0 + end + + def authenticate(login, password) + return nil if login.blank? || password.blank? + + with_timeout do + attrs = get_user_dn(login, password) + if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password) + logger.debug "Authentication successful for '#{login}'" if logger && logger.debug? + return attrs.except(:dn) + end + end + rescue Net::LDAP::LdapError => e + raise AuthSourceException.new(e.message) + end + + # test the connection to the LDAP + def test_connection + with_timeout do + ldap_con = initialize_ldap_con(self.account, self.account_password) + ldap_con.open { } + end + rescue Net::LDAP::LdapError => e + raise AuthSourceException.new(e.message) + end + + def auth_method_name + "LDAP" + end + + private + + def with_timeout(&block) + timeout = self.timeout + timeout = 20 unless timeout && timeout > 0 + Timeout.timeout(timeout) do + return yield + end + rescue Timeout::Error => e + raise AuthSourceTimeoutException.new(e.message) + end + + def ldap_filter + if filter.present? + Net::LDAP::Filter.construct(filter) + end + rescue Net::LDAP::LdapError + nil + end + + def validate_filter + if filter.present? && ldap_filter.nil? + errors.add(:filter, :invalid) + end + end + + def strip_ldap_attributes + [:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr| + write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil? + end + end + + def initialize_ldap_con(ldap_user, ldap_password) + options = { :host => self.host, + :port => self.port, + :encryption => (self.tls ? :simple_tls : nil) + } + options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank? + Net::LDAP.new options + end + + def get_user_attributes_from_ldap_entry(entry) + { + :dn => entry.dn, + :firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname), + :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname), + :mail => AuthSourceLdap.get_attr(entry, self.attr_mail), + :auth_source_id => self.id + } + end + + # Return the attributes needed for the LDAP search. It will only + # include the user attributes if on-the-fly registration is enabled + def search_attributes + if onthefly_register? + ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail] + else + ['dn'] + end + end + + # Check if a DN (user record) authenticates with the password + def authenticate_dn(dn, password) + if dn.present? && password.present? + initialize_ldap_con(dn, password).bind + end + end + + # Get the user's dn and any attributes for them, given their login + def get_user_dn(login, password) + ldap_con = nil + if self.account && self.account.include?("$login") + ldap_con = initialize_ldap_con(self.account.sub("$login", Net::LDAP::DN.escape(login)), password) + else + ldap_con = initialize_ldap_con(self.account, self.account_password) + end + login_filter = Net::LDAP::Filter.eq( self.attr_login, login ) + object_filter = Net::LDAP::Filter.eq( "objectClass", "*" ) + attrs = {} + + search_filter = object_filter & login_filter + if f = ldap_filter + search_filter = search_filter & f + end + + ldap_con.search( :base => self.base_dn, + :filter => search_filter, + :attributes=> search_attributes) do |entry| + + if onthefly_register? + attrs = get_user_attributes_from_ldap_entry(entry) + else + attrs = {:dn => entry.dn} + end + + logger.debug "DN found for #{login}: #{attrs[:dn]}" if logger && logger.debug? + end + + attrs + end + + def self.get_attr(entry, attr_name) + if !attr_name.blank? + entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name] + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/28/28ad874b5f46ac1b00ddf0031a0a20aed5e4e9c0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/28/28ad874b5f46ac1b00ddf0031a0a20aed5e4e9c0.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,5 @@ +Description: + Generates a plugin model. + +Examples: + ./script/rails generate redmine_plugin_model meetings pool diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/29/295c065c2e14259a8750855cd61d0d4651444042.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/29/295c065c2e14259a8750855cd61d0d4651444042.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,5 @@ +require File.dirname(__FILE__) + '/date/calculations' + +class Date #:nodoc: + include Redmine::CoreExtensions::Date::Calculations +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/29/296d1bfd06d81d82f9fb7843d172271a5dcd1eec.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/29/296d1bfd06d81d82f9fb7843d172271a5dcd1eec.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,95 @@ +source 'http://rubygems.org' + +gem 'rails', '3.2.10' +gem "jquery-rails", "~> 2.0.2" +gem "i18n", "~> 0.6.0" +gem "coderay", "~> 1.0.6" +gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby] +gem "builder", "3.0.0" + +# Optional gem for LDAP authentication +group :ldap do + gem "net-ldap", "~> 0.3.1" +end + +# Optional gem for OpenID authentication +group :openid do + gem "ruby-openid", "~> 2.1.4", :require => "openid" + gem "rack-openid" +end + +# Optional gem for exporting the gantt to a PNG file, not supported with jruby +platforms :mri, :mingw do + group :rmagick do + # RMagick 2 supports ruby 1.9 + # RMagick 1 would be fine for ruby 1.8 but Bundler does not support + # different requirements for the same gem on different platforms + gem "rmagick", ">= 2.0.0" + end +end + +# Database gems +platforms :mri, :mingw do + group :postgresql do + gem "pg", ">= 0.11.0" + end + + group :sqlite do + gem "sqlite3" + end +end + +platforms :mri_18, :mingw_18 do + group :mysql do + gem "mysql", "~> 2.8.1" + end +end + +platforms :mri_19, :mingw_19 do + group :mysql do + gem "mysql2", "~> 0.3.11" + end +end + +platforms :jruby do + gem "jruby-openssl" + + group :mysql do + gem "activerecord-jdbcmysql-adapter" + end + + group :postgresql do + gem "activerecord-jdbcpostgresql-adapter" + end + + group :sqlite do + gem "activerecord-jdbcsqlite3-adapter" + end +end + +group :development do + gem "rdoc", ">= 2.4.2" + gem "yard" +end + +group :test do + gem "shoulda", "~> 2.11" + # Shoulda does not work nice on Ruby 1.9.3 and JRuby 1.7. + # It seems to need test-unit explicitely. + platforms = [:mri_19] + platforms << :jruby if defined?(JRUBY_VERSION) && JRUBY_VERSION >= "1.7" + gem "test-unit", :platforms => platforms + gem "mocha", "0.12.3" +end + +local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local") +if File.exists?(local_gemfile) + puts "Loading Gemfile.local ..." if $DEBUG # `ruby -d` or `bundle -v` + instance_eval File.read(local_gemfile) +end + +# Load plugins' Gemfiles +Dir.glob File.expand_path("../plugins/*/Gemfile", __FILE__) do |file| + puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v` + instance_eval File.read(file) +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/29/298be127abf8c78d6ccda6e13729844112a36c53.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/29/298be127abf8c78d6ccda6e13729844112a36c53.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,131 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Journal < ActiveRecord::Base + belongs_to :journalized, :polymorphic => true + # added as a quick fix to allow eager loading of the polymorphic association + # since always associated to an issue, for now + belongs_to :issue, :foreign_key => :journalized_id + + belongs_to :user + has_many :details, :class_name => "JournalDetail", :dependent => :delete_all + attr_accessor :indice + + acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" }, + :description => :notes, + :author => :user, + :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' }, + :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}} + + acts_as_activity_provider :type => 'issues', + :author_key => :user_id, + :find_options => {:include => [{:issue => :project}, :details, :user], + :conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" + + " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"} + + before_create :split_private_notes + + scope :visible, lambda {|*args| + user = args.shift || User.current + + includes(:issue => :project). + where(Issue.visible_condition(user, *args)). + where("(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes, *args)}))", false) + } + + def save(*args) + # Do not save an empty journal + (details.empty? && notes.blank?) ? false : super + end + + # Returns the new status if the journal contains a status change, otherwise nil + def new_status + c = details.detect {|detail| detail.prop_key == 'status_id'} + (c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil + end + + def new_value_for(prop) + c = details.detect {|detail| detail.prop_key == prop} + c ? c.value : nil + end + + def editable_by?(usr) + usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project))) + end + + def project + journalized.respond_to?(:project) ? journalized.project : nil + end + + def attachments + journalized.respond_to?(:attachments) ? journalized.attachments : nil + end + + # Returns a string of css classes + def css_classes + s = 'journal' + s << ' has-notes' unless notes.blank? + s << ' has-details' unless details.blank? + s << ' private-notes' if private_notes? + s + end + + def notify? + @notify != false + end + + def notify=(arg) + @notify = arg + end + + def recipients + notified = journalized.notified_users + if private_notes? + notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)} + end + notified.map(&:mail) + end + + def watcher_recipients + notified = journalized.notified_watchers + if private_notes? + notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)} + end + notified.map(&:mail) + end + + private + + def split_private_notes + if private_notes? + if notes.present? + if details.any? + # Split the journal (notes/changes) so we don't have half-private journals + journal = Journal.new(:journalized => journalized, :user => user, :notes => nil, :private_notes => false) + journal.details = details + journal.save + self.details = [] + self.created_on = journal.created_on + end + else + # Blank notes should not be private + self.private_notes = false + end + end + true + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2a/2a2a8eb00afb134f0884b2c4a0fe9a62cc43712a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2a/2a2a8eb00afb134f0884b2c4a0fe9a62cc43712a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,4 @@ +# This file is used by Rack-based servers to start the application. + +require ::File.expand_path('../config/environment', __FILE__) +run RedmineApp::Application diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2a/2ad19a5d216db9900f8c479e9b99d1592793dd37.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2a/2ad19a5d216db9900f8c479e9b99d1592793dd37.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,91 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Acts + module Event + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + def acts_as_event(options = {}) + return if self.included_modules.include?(Redmine::Acts::Event::InstanceMethods) + default_options = { :datetime => :created_on, + :title => :title, + :description => :description, + :author => :author, + :url => {:controller => 'welcome'}, + :type => self.name.underscore.dasherize } + + cattr_accessor :event_options + self.event_options = default_options.merge(options) + send :include, Redmine::Acts::Event::InstanceMethods + end + end + + module InstanceMethods + def self.included(base) + base.extend ClassMethods + end + + %w(datetime title description author type).each do |attr| + src = <<-END_SRC + def event_#{attr} + option = event_options[:#{attr}] + if option.is_a?(Proc) + option.call(self) + elsif option.is_a?(Symbol) + send(option) + else + option + end + end + END_SRC + class_eval src, __FILE__, __LINE__ + end + + def event_date + event_datetime.to_date + end + + def event_url(options = {}) + option = event_options[:url] + if option.is_a?(Proc) + option.call(self).merge(options) + elsif option.is_a?(Hash) + option.merge(options) + elsif option.is_a?(Symbol) + send(option).merge(options) + else + option + end + end + + # Returns the mail adresses of users that should be notified + def recipients + notified = project.notified_users + notified.reject! {|user| !visible?(user)} + notified.collect(&:mail) + end + + module ClassMethods + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2a/2adb9560bc07d7847c894a43ae505527519cc349.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2a/2adb9560bc07d7847c894a43ae505527519cc349.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,11 @@ +class AddWorkflowsRuleFields < ActiveRecord::Migration + def up + add_column :workflows, :field_name, :string, :limit => 30 + add_column :workflows, :rule, :string, :limit => 30 + end + + def down + remove_column :workflows, :field_name + remove_column :workflows, :rule + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2b/2b0ee259d41451978f73ac2e9b9372c5b4686f27.svn-base --- a/.svn/pristine/2b/2b0ee259d41451978f73ac2e9b9372c5b4686f27.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ -class Version < ActiveRecord::Base - generator_for :name, :start => 'Version 1.0.0' - generator_for :status => 'open' - -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2b/2b217a0880dd8ada2c9c72a208a8a9c562cb5d2c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2b/2b217a0880dd8ada2c9c72a208a8a9c562cb5d2c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,69 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingAttachmentsTest < ActionController::IntegrationTest + def test_attachments + assert_routing( + { :method => 'get', :path => "/attachments/1" }, + { :controller => 'attachments', :action => 'show', :id => '1' } + ) + assert_routing( + { :method => 'get', :path => "/attachments/1.xml" }, + { :controller => 'attachments', :action => 'show', :id => '1', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/attachments/1.json" }, + { :controller => 'attachments', :action => 'show', :id => '1', :format => 'json' } + ) + assert_routing( + { :method => 'get', :path => "/attachments/1/filename.ext" }, + { :controller => 'attachments', :action => 'show', :id => '1', + :filename => 'filename.ext' } + ) + assert_routing( + { :method => 'get', :path => "/attachments/download/1" }, + { :controller => 'attachments', :action => 'download', :id => '1' } + ) + assert_routing( + { :method => 'get', :path => "/attachments/download/1/filename.ext" }, + { :controller => 'attachments', :action => 'download', :id => '1', + :filename => 'filename.ext' } + ) + assert_routing( + { :method => 'get', :path => "/attachments/thumbnail/1" }, + { :controller => 'attachments', :action => 'thumbnail', :id => '1' } + ) + assert_routing( + { :method => 'get', :path => "/attachments/thumbnail/1/200" }, + { :controller => 'attachments', :action => 'thumbnail', :id => '1', :size => '200' } + ) + assert_routing( + { :method => 'delete', :path => "/attachments/1" }, + { :controller => 'attachments', :action => 'destroy', :id => '1' } + ) + assert_routing( + { :method => 'post', :path => '/uploads.xml' }, + { :controller => 'attachments', :action => 'upload', :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => '/uploads.json' }, + { :controller => 'attachments', :action => 'upload', :format => 'json' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2b/2b8a016bf78b5250d2e474ce2a1c8d8864231ad4.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2b/2b8a016bf78b5250d2e474ce2a1c8d8864231ad4.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,47 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingCustomFieldsTest < ActionController::IntegrationTest + def test_custom_fields + assert_routing( + { :method => 'get', :path => "/custom_fields" }, + { :controller => 'custom_fields', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/custom_fields/new" }, + { :controller => 'custom_fields', :action => 'new' } + ) + assert_routing( + { :method => 'post', :path => "/custom_fields" }, + { :controller => 'custom_fields', :action => 'create' } + ) + assert_routing( + { :method => 'get', :path => "/custom_fields/2/edit" }, + { :controller => 'custom_fields', :action => 'edit', :id => '2' } + ) + assert_routing( + { :method => 'put', :path => "/custom_fields/2" }, + { :controller => 'custom_fields', :action => 'update', :id => '2' } + ) + assert_routing( + { :method => 'delete', :path => "/custom_fields/2" }, + { :controller => 'custom_fields', :action => 'destroy', :id => '2' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2b/2bdd14c51d14eeccec1ac400b671772bf40793e4.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2b/2bdd14c51d14eeccec1ac400b671772bf40793e4.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1084 @@ +hr: + direction: ltr + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%m/%d/%Y" + short: "%b %d" + long: "%B %d, %Y" + + day_names: [Ponedjeljak, Utorak, Srijeda, ÄŒetvrtak, Petak, Subota, Nedjelja] + abbr_day_names: [Ned, Pon, Uto, Sri, ÄŒet, Pet, Sub] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, Sijecanj, Veljaca, Ožujak, Travanj, Svibanj, Lipanj, Srpanj, Kolovoz, Rujan, Listopad, Studeni, Prosinac] + abbr_month_names: [~, Sij, Velj, Ožu, Tra, Svi, Lip, Srp, Kol, Ruj, List, Stu, Pro] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%m/%d/%Y %I:%M %p" + time: "%I:%M %p" + short: "%d %b %H:%M" + long: "%B %d, %Y %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "pola minute" + less_than_x_seconds: + one: "manje od sekunde" + other: "manje od %{count} sekundi" + x_seconds: + one: "1 sekunda" + other: "%{count} sekundi" + less_than_x_minutes: + one: "manje od minute" + other: "manje od %{count} minuta" + x_minutes: + one: "1 minuta" + other: "%{count} minuta" + about_x_hours: + one: "oko sat vremena" + other: "oko %{count} sati" + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "1 dan" + other: "%{count} dana" + about_x_months: + one: "oko 1 mjesec" + other: "oko %{count} mjeseci" + x_months: + one: "mjesec" + other: "%{count} mjeseci" + about_x_years: + one: "1 godina" + other: "%{count} godina" + over_x_years: + one: "preko 1 godine" + other: "preko %{count} godina" + + number: + format: + separator: "." + delimiter: "" + precision: 3 + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + +# Used in array.to_sentence. + support: + array: + sentence_connector: "i" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + messages: + inclusion: "nije ukljuceno u listu" + exclusion: "je rezervirano" + invalid: "nije ispravno" + confirmation: "ne odgovara za potvrdu" + accepted: "mora biti prihvaćen" + empty: "ne može biti prazno" + blank: "ne može biti razmaka" + too_long: "je predug (maximum is %{count} characters)" + too_short: "je prekratak (minimum is %{count} characters)" + wrong_length: "je pogreÅ¡ne dužine (should be %{count} characters)" + taken: "već je zauzeto" + not_a_number: "nije broj" + not_a_date: "nije ispravan datum" + greater_than: "mora biti veći od %{count}" + greater_than_or_equal_to: "mora biti veći ili jednak %{count}" + equal_to: "mora biti jednak %{count}" + less_than: "mora biti manji od %{count}" + less_than_or_equal_to: "mora bit manji ili jednak%{count}" + odd: "mora biti neparan" + even: "mora biti paran" + greater_than_start_date: "mora biti veci nego pocetni datum" + not_same_project: "ne pripada istom projektu" + circular_dependency: "Ovaj relacija stvara kružnu ovisnost" + cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks" + + actionview_instancetag_blank_option: Molimo odaberite + + general_text_No: 'Ne' + general_text_Yes: 'Da' + general_text_no: 'ne' + general_text_yes: 'da' + general_lang_name: 'Hrvatski' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '7' + + notice_account_updated: VaÅ¡ profil je uspjeÅ¡no promijenjen. + notice_account_invalid_creditentials: Neispravno korisniÄko ime ili zaporka. + notice_account_password_updated: Zaporka je uspjeÅ¡no promijenjena. + notice_account_wrong_password: PogreÅ¡na zaporka + notice_account_register_done: Racun je uspjeÅ¡no napravljen. Da biste aktivirali svoj raÄun, kliknite na link koji vam je poslan na e-mail. + notice_account_unknown_email: Nepoznati korisnik. + notice_can_t_change_password: Ovaj raÄun koristi eksterni izvor prijavljivanja. Nemoguće je promijeniti zaporku. + notice_account_lost_email_sent: E-mail s uputama kako bi odabrali novu zaporku je poslan na na vaÅ¡u e-mail adresu. + notice_account_activated: VaÅ¡ racun je aktiviran. Možete se prijaviti. + notice_successful_create: UspjeÅ¡no napravljeno. + notice_successful_update: UspjeÅ¡na promjena. + notice_successful_delete: UspjeÅ¡no brisanje. + notice_successful_connection: UspjeÅ¡na veza. + notice_file_not_found: Stranica kojoj ste pokuÅ¡ali pristupiti ne postoji ili je uklonjena. + notice_locking_conflict: Podataci su ažurirani od strane drugog korisnika. + notice_not_authorized: Niste ovlaÅ¡teni za pristup ovoj stranici. + notice_email_sent: E-mail je poslan %{value}" + notice_email_error: Dogodila se pogreÅ¡ka tijekom slanja E-maila (%{value})" + notice_feeds_access_key_reseted: VaÅ¡ RSS pristup je resetovan. + notice_api_access_key_reseted: VaÅ¡ API pristup je resetovan. + notice_failed_to_save_issues: "Neuspjelo spremanje %{count} predmeta na %{total} odabrane: %{ids}." + notice_no_issue_selected: "Niti jedan predmet nije odabran! Molim, odaberite predmete koje želite urediti." + notice_account_pending: "VaÅ¡ korisnicki raÄun je otvoren, Äeka odobrenje administratora." + notice_default_data_loaded: Konfiguracija je uspjeÅ¡no uÄitana. + notice_unable_delete_version: Nije moguće izbrisati verziju. + notice_issue_done_ratios_updated: Issue done ratios updated. + + error_can_t_load_default_data: "Zadanu konfiguracija nije uÄitana: %{value}" + error_scm_not_found: "Unos i/ili revizija nije pronaÄ‘en." + error_scm_command_failed: "Dogodila se pogreÅ¡ka prilikom pokuÅ¡aja pristupa: %{value}" + error_scm_annotate: "Ne postoji ili ne može biti obilježen." + error_issue_not_found_in_project: 'Nije pronaÄ‘en ili ne pripada u ovaj projekt' + error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.' + error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").' + error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version can not be reopened' + error_can_not_archive_project: This project can not be archived + error_issue_done_ratios_not_updated: "Issue done ratios not updated." + error_workflow_copy_source: 'Please select a source tracker or role' + error_workflow_copy_target: 'Please select target tracker(s) and role(s)' + + warning_attachments_not_saved: "%{count} Datoteka/e nije mogla biti spremljena." + + mail_subject_lost_password: "VaÅ¡a %{value} zaporka" + mail_body_lost_password: 'Kako biste promijenili VaÅ¡u zaporku slijedite poveznicu:' + mail_subject_register: "Aktivacija korisniÄog raÄuna %{value}" + mail_body_register: 'Da biste aktivirali svoj raÄun, kliknite na sljedeci link:' + mail_body_account_information_external: "Možete koristiti vaÅ¡ raÄun %{value} za prijavu." + mail_body_account_information: VaÅ¡i korisniÄki podaci + mail_subject_account_activation_request: "%{value} predmet za aktivaciju korisniÄkog raÄuna" + mail_body_account_activation_request: "Novi korisnik (%{value}) je registriran. Njegov korisniÄki raÄun Äeka vaÅ¡e odobrenje:" + mail_subject_reminder: "%{count} predmet(a) dospijeva sljedećih %{days} dana" + mail_body_reminder: "%{count} vama dodijeljen(ih) predmet(a) dospijeva u sljedećih %{days} dana:" + mail_subject_wiki_content_added: "'%{id}' wiki page has been added" + mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}." + mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated" + mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}." + + gui_validation_error: 1 pogreÅ¡ka + gui_validation_error_plural: "%{count} pogreÅ¡aka" + + field_name: Ime + field_description: Opis + field_summary: Sažetak + field_is_required: Obavezno + field_firstname: Ime + field_lastname: Prezime + field_mail: E-poÅ¡ta + field_filename: Datoteka + field_filesize: VeliÄina + field_downloads: Preuzimanja + field_author: Autor + field_created_on: Napravljen + field_updated_on: Promijenjen + field_field_format: Format + field_is_for_all: Za sve projekte + field_possible_values: Moguće vrijednosti + field_regexp: Regularni izraz + field_min_length: Minimalna dužina + field_max_length: Maksimalna dužina + field_value: Vrijednost + field_category: Kategorija + field_title: Naslov + field_project: Projekt + field_issue: Predmet + field_status: Status + field_notes: Napomene + field_is_closed: Predmet je zatvoren + field_is_default: Zadana vrijednost + field_tracker: Tracker + field_subject: Predmet + field_due_date: Do datuma + field_assigned_to: Dodijeljeno + field_priority: Prioritet + field_fixed_version: Verzija + field_user: Korisnik + field_role: Uloga + field_homepage: Naslovnica + field_is_public: Javni projekt + field_parent: Potprojekt od + field_is_in_roadmap: Predmeti se prikazuju u Putokazu + field_login: KorisniÄko ime + field_mail_notification: Obavijest putem e-poÅ¡te + field_admin: Administrator + field_last_login_on: Zadnja prijava + field_language: Primarni jezik + field_effective_date: Datum + field_password: Zaporka + field_new_password: Nova zaporka + field_password_confirmation: Potvrda zaporke + field_version: Verzija + field_type: Tip + field_host: Host + field_port: Port + field_account: Racun + field_base_dn: Osnovni DN + field_attr_login: Login atribut + field_attr_firstname: Atribut imena + field_attr_lastname: Atribut prezimena + field_attr_mail: Atribut e-poÅ¡te + field_onthefly: "Izrada korisnika \"u hodu\"" + field_start_date: Pocetak + field_done_ratio: "% UÄinjeno" + field_auth_source: Vrsta prijavljivanja + field_hide_mail: Sakrij moju adresu e-poÅ¡te + field_comments: Komentar + field_url: URL + field_start_page: PoÄetna stranica + field_subproject: Potprojekt + field_hours: Sati + field_activity: Aktivnost + field_spent_on: Datum + field_identifier: Identifikator + field_is_filter: KoriÅ¡teno kao filtar + field_issue_to_id: Povezano s predmetom + field_delay: Odgodeno + field_assignable: Predmeti mogu biti dodijeljeni ovoj ulozi + field_redirect_existing_links: Preusmjeravanje postojećih linkova + field_estimated_hours: Procijenjeno vrijeme + field_column_names: Stupci + field_time_zone: Vremenska zona + field_searchable: Pretraživo + field_default_value: Zadana vrijednost + field_comments_sorting: Prikaz komentara + field_parent_title: Parent page + field_editable: Editable + field_watcher: Watcher + field_identity_url: OpenID URL + field_content: Content + field_group_by: Group results by + + setting_app_title: Naziv aplikacije + setting_app_subtitle: Podnaslov aplikacije + setting_welcome_text: Tekst dobrodoÅ¡lice + setting_default_language: Zadani jezik + setting_login_required: Potrebna je prijava + setting_self_registration: Samoregistracija je dozvoljena + setting_attachment_max_size: Maksimalna veliÄina privitka + setting_issues_export_limit: OgraniÄenje izvoza predmeta + setting_mail_from: Izvorna adresa e-poÅ¡te + setting_bcc_recipients: Blind carbon copy primatelja (bcc) + setting_plain_text_mail: obiÄni tekst poÅ¡te (bez HTML-a) + setting_host_name: Naziv domaćina (host) + setting_text_formatting: Oblikovanje teksta + setting_wiki_compression: Sažimanje + setting_feeds_limit: Ogranicenje unosa sadržaja + setting_default_projects_public: Novi projekti su javni po defaultu + setting_autofetch_changesets: Autofetch commits + setting_sys_api_enabled: Omogući WS za upravljanje skladiÅ¡tem + setting_commit_ref_keywords: Referentne kljuÄne rijeÄi + setting_commit_fix_keywords: Fiksne kljuÄne rijeÄi + setting_autologin: Automatska prijava + setting_date_format: Format datuma + setting_time_format: Format vremena + setting_cross_project_issue_relations: Dozvoli povezivanje predmeta izmedu razliÄitih projekata + setting_issue_list_default_columns: Stupci prikazani na listi predmeta + setting_emails_footer: Zaglavlje e-poÅ¡te + setting_protocol: Protokol + setting_per_page_options: Objekata po stranici opcija + setting_user_format: Oblik prikaza korisnika + setting_activity_days_default: Dani prikazane aktivnosti na projektu + setting_display_subprojects_issues: Prikaz predmeta potprojekta na glavnom projektu po defaultu + setting_enabled_scm: Omogućen SCM + setting_mail_handler_body_delimiters: "Truncate emails after one of these lines" + setting_mail_handler_api_enabled: Omoguci WS za dolaznu e-poÅ¡tu + setting_mail_handler_api_key: API kljuÄ + setting_sequential_project_identifiers: Generiraj slijedne identifikatore projekta + setting_gravatar_enabled: Koristi Gravatar korisniÄke ikone + setting_gravatar_default: Default Gravatar image + setting_diff_max_lines_displayed: Maksimalni broj diff linija za prikazati + setting_file_max_size_displayed: Max size of text files displayed inline + setting_repository_log_display_limit: Maximum number of revisions displayed on file log + setting_openid: Allow OpenID login and registration + setting_password_min_length: Minimum password length + setting_new_project_user_role_id: Role given to a non-admin user who creates a project + setting_default_projects_modules: Default enabled modules for new projects + setting_issue_done_ratio: Calculate the issue done ratio with + setting_issue_done_ratio_issue_field: Use the issue field + setting_issue_done_ratio_issue_status: Use the issue status + setting_start_of_week: Start calendars on + setting_rest_api_enabled: Enable REST web service + + permission_add_project: Dodaj projekt + permission_add_subprojects: Dodaj potprojekt + permission_edit_project: Uredi projekt + permission_select_project_modules: Odaberi projektne module + permission_manage_members: Upravljaj Älanovima + permission_manage_versions: Upravljaj verzijama + permission_manage_categories: Upravljaj kategorijama predmeta + permission_view_issues: Pregledaj zahtjeve + permission_add_issues: Dodaj predmete + permission_edit_issues: Uredi predmete + permission_manage_issue_relations: Upravljaj relacijama predmeta + permission_add_issue_notes: Dodaj biljeÅ¡ke + permission_edit_issue_notes: Uredi biljeÅ¡ke + permission_edit_own_issue_notes: Uredi vlastite biljeÅ¡ke + permission_move_issues: Premjesti predmete + permission_delete_issues: Brisanje predmeta + permission_manage_public_queries: Upravljaj javnim upitima + permission_save_queries: Spremi upite + permission_view_gantt: Pregledaj gantt grafikon + permission_view_calendar: Pregledaj kalendar + permission_view_issue_watchers: Pregledaj listu promatraca + permission_add_issue_watchers: Dodaj promatraÄa + permission_delete_issue_watchers: Delete watchers + permission_log_time: Dnevnik utroÅ¡enog vremena + permission_view_time_entries: Pregledaj utroÅ¡eno vrijeme + permission_edit_time_entries: Uredi vremenske dnevnike + permission_edit_own_time_entries: Edit own time logs + permission_manage_news: Upravljaj novostima + permission_comment_news: Komentiraj novosti + permission_manage_documents: Upravljaj dokumentima + permission_view_documents: Pregledaj dokumente + permission_manage_files: Upravljaj datotekama + permission_view_files: Pregledaj datoteke + permission_manage_wiki: Upravljaj wikijem + permission_rename_wiki_pages: Promijeni ime wiki stranicama + permission_delete_wiki_pages: ObriÅ¡i wiki stranice + permission_view_wiki_pages: Pregledaj wiki + permission_view_wiki_edits: Pregledaj povijest wikija + permission_edit_wiki_pages: Uredi wiki stranice + permission_delete_wiki_pages_attachments: ObriÅ¡i privitke + permission_protect_wiki_pages: ZaÅ¡titi wiki stranice + permission_manage_repository: Upravljaj skladiÅ¡tem + permission_browse_repository: Browse repository + permission_view_changesets: View changesets + permission_commit_access: Mogućnost pohranjivanja + permission_manage_boards: Manage boards + permission_view_messages: Pregledaj poruke + permission_add_messages: Objavi poruke + permission_edit_messages: Uredi poruke + permission_edit_own_messages: Uredi vlastite poruke + permission_delete_messages: ObriÅ¡i poruke + permission_delete_own_messages: ObriÅ¡i vlastite poruke + + project_module_issue_tracking: Praćenje predmeta + project_module_time_tracking: Praćenje vremena + project_module_news: Novosti + project_module_documents: Dokumenti + project_module_files: Datoteke + project_module_wiki: Wiki + project_module_repository: SkladiÅ¡te + project_module_boards: Boards + + label_user: Korisnik + label_user_plural: Korisnici + label_user_new: Novi korisnik + label_user_anonymous: Anonymous + label_project: Projekt + label_project_new: Novi projekt + label_project_plural: Projekti + label_x_projects: + zero: no projects + one: 1 project + other: "%{count} projects" + label_project_all: Svi Projekti + label_project_latest: Najnoviji projekt + label_issue: Predmet + label_issue_new: Novi predmet + label_issue_plural: Predmeti + label_issue_view_all: Pregled svih predmeta + label_issues_by: "Predmeti od %{value}" + label_issue_added: Predmet dodan + label_issue_updated: Predmet promijenjen + label_document: Dokument + label_document_new: Novi dokument + label_document_plural: Dokumenti + label_document_added: Dokument dodan + label_role: Uloga + label_role_plural: Uloge + label_role_new: Nova uloga + label_role_and_permissions: Uloge i ovlasti + label_member: ÄŒlan + label_member_new: Novi Älan + label_member_plural: ÄŒlanovi + label_tracker: Vrsta + label_tracker_plural: Vrste predmeta + label_tracker_new: Nova vrsta + label_workflow: Tijek rada + label_issue_status: Status predmeta + label_issue_status_plural: Status predmeta + label_issue_status_new: Novi status + label_issue_category: Kategorija predmeta + label_issue_category_plural: Kategorije predmeta + label_issue_category_new: Nova kategorija + label_custom_field: KorisniÄki definirano polje + label_custom_field_plural: KorisniÄki definirana polja + label_custom_field_new: Novo korisniÄki definirano polje + label_enumerations: Pobrojenice + label_enumeration_new: Nova vrijednost + label_information: Informacija + label_information_plural: Informacije + label_please_login: Molim prijavite se + label_register: Registracija + label_login_with_open_id_option: or login with OpenID + label_password_lost: Izgubljena zaporka + label_home: PoÄetna stranica + label_my_page: Moja stranica + label_my_account: Moj profil + label_my_projects: Moji projekti + label_administration: Administracija + label_login: Korisnik + label_logout: Odjava + label_help: Pomoć + label_reported_issues: Prijavljeni predmeti + label_assigned_to_me_issues: Moji predmeti + label_last_login: Last connection + label_registered_on: Registrirano + label_activity: Aktivnosti + label_overall_activity: Aktivnosti + label_user_activity: "%{value} ova/ina aktivnost" + label_new: Novi + label_logged_as: Prijavljeni ste kao + label_environment: Okolina + label_authentication: Autentikacija + label_auth_source: NaÄin prijavljivanja + label_auth_source_new: Novi naÄin prijavljivanja + label_auth_source_plural: NaÄini prijavljivanja + label_subproject_plural: Potprojekti + label_subproject_new: Novi potprojekt + label_and_its_subprojects: "%{value} i njegovi potprojekti" + label_min_max_length: Min - Maks veliÄina + label_list: Liste + label_date: Datum + label_integer: Integer + label_float: Float + label_boolean: Boolean + label_string: Text + label_text: Long text + label_attribute: Atribut + label_attribute_plural: Atributi + label_download: "%{count} Download" + label_download_plural: "%{count} Downloads" + label_no_data: Nema podataka za prikaz + label_change_status: Promjena statusa + label_history: Povijest + label_attachment: Datoteka + label_attachment_new: Nova datoteka + label_attachment_delete: Brisanje datoteke + label_attachment_plural: Datoteke + label_file_added: Datoteka dodana + label_report: Izvješće + label_report_plural: Izvješća + label_news: Novosti + label_news_new: Dodaj novost + label_news_plural: Novosti + label_news_latest: Novosti + label_news_view_all: Pregled svih novosti + label_news_added: Novosti dodane + label_settings: Postavke + label_overview: Pregled + label_version: Verzija + label_version_new: Nova verzija + label_version_plural: Verzije + label_confirmation: Potvrda + label_export_to: 'Izvoz u:' + label_read: ÄŒitaj... + label_public_projects: Javni projekti + label_open_issues: Otvoren + label_open_issues_plural: Otvoreno + label_closed_issues: Zatvoren + label_closed_issues_plural: Zatvoreno + label_x_open_issues_abbr_on_total: + zero: 0 open / %{total} + one: 1 open / %{total} + other: "%{count} open / %{total}" + label_x_open_issues_abbr: + zero: 0 open + one: 1 open + other: "%{count} open" + label_x_closed_issues_abbr: + zero: 0 closed + one: 1 closed + other: "%{count} closed" + label_total: Ukupno + label_permissions: Dozvole + label_current_status: Trenutni status + label_new_statuses_allowed: Novi status je dozvoljen + label_all: Svi + label_none: nema + label_nobody: nitko + label_next: Naredni + label_previous: Prethodni + label_used_by: KoriÅ¡ten od + label_details: Detalji + label_add_note: Dodaj napomenu + label_per_page: Po stranici + label_calendar: Kalendar + label_months_from: Mjeseci od + label_gantt: Gantt + label_internal: Interno + label_last_changes: "Posljednjih %{count} promjena" + label_change_view_all: Prikaz svih promjena + label_personalize_page: Prilagodite ovu stranicu + label_comment: Komentar + label_comment_plural: Komentari + label_x_comments: + zero: no comments + one: 1 comment + other: "%{count} comments" + label_comment_add: Dodaj komentar + label_comment_added: Komentar dodan + label_comment_delete: Brisanje komentara + label_query: KorisniÄki upit + label_query_plural: KorisniÄki upiti + label_query_new: Novi upit + label_filter_add: Dodaj filtar + label_filter_plural: Filtri + label_equals: je + label_not_equals: nije + label_in_less_than: za manje od + label_in_more_than: za viÅ¡e od + label_greater_or_equal: '>=' + label_less_or_equal: '<=' + label_in: za toÄno + label_today: danas + label_all_time: sva vremena + label_yesterday: juÄer + label_this_week: ovog tjedna + label_last_week: proÅ¡log tjedna + label_last_n_days: "zadnjih %{count} dana" + label_this_month: ovog mjeseca + label_last_month: proÅ¡log mjeseca + label_this_year: ove godine + label_date_range: vremenski raspon + label_less_than_ago: manje od + label_more_than_ago: viÅ¡e od + label_ago: prije + label_contains: Sadrži + label_not_contains: ne sadrži + label_day_plural: dana + label_repository: SkladiÅ¡te + label_repository_plural: SkladiÅ¡ta + label_browse: Pregled + label_modification: "%{count} promjena" + label_modification_plural: "%{count} promjena" + label_branch: Branch + label_tag: Tag + label_revision: Revizija + label_revision_plural: Revizije + label_revision_id: "Revision %{value}" + label_associated_revisions: Dodijeljene revizije + label_added: dodano + label_modified: promijenjen + label_copied: kopirano + label_renamed: preimenovano + label_deleted: obrisano + label_latest_revision: Posljednja revizija + label_latest_revision_plural: Posljednje revizije + label_view_revisions: Pregled revizija + label_view_all_revisions: View all revisions + label_max_size: Maksimalna veliÄina + label_sort_highest: Premjesti na vrh + label_sort_higher: Premjesti prema gore + label_sort_lower: Premjesti prema dolje + label_sort_lowest: Premjesti na dno + label_roadmap: Putokaz + label_roadmap_due_in: "ZavrÅ¡ava se za %{value}" + label_roadmap_overdue: "%{value} kasni" + label_roadmap_no_issues: Nema predmeta za ovu verziju + label_search: Traži + label_result_plural: Rezultati + label_all_words: Sve rijeÄi + label_wiki: Wiki + label_wiki_edit: Wiki promjena + label_wiki_edit_plural: Wiki promjene + label_wiki_page: Wiki stranica + label_wiki_page_plural: Wiki stranice + label_index_by_title: Indeks po naslovima + label_index_by_date: Indeks po datumu + label_current_version: Trenutna verzija + label_preview: Brzi pregled + label_feed_plural: Feeds + label_changes_details: Detalji svih promjena + label_issue_tracking: Praćenje predmeta + label_spent_time: UtroÅ¡eno vrijeme + label_f_hour: "%{value} sata" + label_f_hour_plural: "%{value} sati" + label_time_tracking: Praćenje vremena + label_change_plural: Promjene + label_statistics: Statistika + label_commits_per_month: Pohrana po mjesecu + label_commits_per_author: Pohrana po autoru + label_view_diff: Pregled razlika + label_diff_inline: uvuÄeno + label_diff_side_by_side: paralelno + label_options: Opcije + label_copy_workflow_from: Kopiraj tijek rada od + label_permissions_report: Izvješće o dozvolama + label_watched_issues: Praćeni predmeti + label_related_issues: Povezani predmeti + label_applied_status: Primijenjen status + label_loading: UÄitavam... + label_relation_new: Nova relacija + label_relation_delete: Brisanje relacije + label_relates_to: u relaciji sa + label_duplicates: Duplira + label_duplicated_by: ponovljen kao + label_blocks: blokira + label_blocked_by: blokiran od strane + label_precedes: prethodi + label_follows: slijedi + label_end_to_start: od kraja do poÄetka + label_end_to_end: od kraja do kraja + label_end_to_start: od kraja do poÄetka + label_end_to_end: od kraja do kraja + label_stay_logged_in: Ostanite prijavljeni + label_disabled: IskljuÄen + label_show_completed_versions: Prikaži zavrÅ¡ene verzije + label_me: ja + label_board: Forum + label_board_new: Novi forum + label_board_plural: Forumi + label_topic_plural: Teme + label_message_plural: Poruke + label_message_last: Posljednja poruka + label_message_new: Nova poruka + label_message_posted: Poruka dodana + label_reply_plural: Odgovori + label_send_information: PoÅ¡alji korisniku informaciju o profilu + label_year: Godina + label_month: Mjesec + label_week: Tjedan + label_date_from: Od + label_date_to: Do + label_language_based: Zasnovano na jeziku + label_sort_by: "Uredi po %{value}" + label_send_test_email: PoÅ¡alji testno E-pismo + label_feeds_access_key: RSS access key + label_missing_feeds_access_key: Missing a RSS access key + label_feeds_access_key_created_on: "RSS kljuc za pristup je napravljen prije %{value}" + label_module_plural: Moduli + label_added_time_by: "Promijenio %{author} prije %{age}" + label_updated_time_by: "Dodao/la %{author} prije %{age}" + label_updated_time: "Promijenjeno prije %{value}" + label_jump_to_a_project: Prebaci se na projekt... + label_file_plural: Datoteke + label_changeset_plural: Promjene + label_default_columns: Zadani stupci + label_no_change_option: (Bez promjene) + label_bulk_edit_selected_issues: ZajedniÄka promjena izabranih predmeta + label_theme: Tema + label_default: Zadana + label_search_titles_only: Pretraživanje samo naslova + label_user_mail_option_all: "Za bilo koji dogaÄ‘aj na svim mojim projektima" + label_user_mail_option_selected: "Za bilo koji dogaÄ‘aj samo za izabrane projekte..." + label_user_mail_no_self_notified: "Ne želim primati obavijesti o promjenama koje sam napravim" + label_registration_activation_by_email: aktivacija putem e-poÅ¡te + label_registration_manual_activation: ruÄna aktivacija + label_registration_automatic_activation: automatska aktivacija + label_display_per_page: "Po stranici: %{value}" + label_age: Starost + label_change_properties: Promijeni svojstva + label_general: Općenito + label_more: JoÅ¡ + label_scm: SCM + label_plugins: Plugins + label_ldap_authentication: LDAP autentikacija + label_downloads_abbr: D/L + label_optional_description: Opcije + label_add_another_file: Dodaj joÅ¡ jednu datoteku + label_preferences: Preferences + label_chronological_order: U kronoloÅ¡kom redoslijedu + label_reverse_chronological_order: U obrnutom kronoloÅ¡kom redoslijedu + label_planning: Planiranje + label_incoming_emails: Dolazne poruke e-poÅ¡te + label_generate_key: Generiraj kljuÄ + label_issue_watchers: PromatraÄi + label_example: Primjer + label_display: Display + label_sort: Sort + label_ascending: Ascending + label_descending: Descending + label_date_from_to: From %{start} to %{end} + label_wiki_content_added: Wiki page added + label_wiki_content_updated: Wiki page updated + label_group: Group + label_group_plural: Grupe + label_group_new: Nova grupa + label_time_entry_plural: Spent time + label_version_sharing_none: Not shared + label_version_sharing_descendants: With subprojects + label_version_sharing_hierarchy: With project hierarchy + label_version_sharing_tree: With project tree + label_version_sharing_system: With all projects + label_update_issue_done_ratios: Update issue done ratios + label_copy_source: Source + label_copy_target: Target + label_copy_same_as_target: Same as target + label_display_used_statuses_only: Only display statuses that are used by this tracker + label_api_access_key: API access key + label_missing_api_access_key: Missing an API access key + label_api_access_key_created_on: "API access key created %{value} ago" + + button_login: Prijavi + button_submit: PoÅ¡alji + button_save: Spremi + button_check_all: OznaÄi sve + button_uncheck_all: IskljuÄi sve + button_delete: ObriÅ¡i + button_create: Napravi + button_create_and_continue: Napravi i nastavi + button_test: Test + button_edit: Uredi + button_add: Dodaj + button_change: Promijeni + button_apply: Primijeni + button_clear: Ukloni + button_lock: ZakljuÄaj + button_unlock: OtkljuÄaj + button_download: Preuzmi + button_list: Spisak + button_view: Pregled + button_move: Premjesti + button_move_and_follow: Move and follow + button_back: Nazad + button_cancel: Odustani + button_activate: Aktiviraj + button_sort: Redoslijed + button_log_time: ZapiÅ¡i vrijeme + button_rollback: IzvrÅ¡i rollback na ovu verziju + button_watch: Prati + button_unwatch: Prekini pracenje + button_reply: Odgovori + button_archive: Arhiviraj + button_rollback: Dearhiviraj + button_reset: PoniÅ¡ti + button_rename: Promijeni ime + button_change_password: Promjena zaporke + button_copy: Kopiraj + button_copy_and_follow: Copy and follow + button_annotate: Annotate + button_update: Promijeni + button_configure: Konfiguracija + button_quote: Navod + button_duplicate: Duplicate + button_show: Show + + status_active: aktivan + status_registered: Registriran + status_locked: zakljuÄan + + version_status_open: open + version_status_locked: locked + version_status_closed: closed + + field_active: Active + + text_select_mail_notifications: Izbor akcija za koje će biti poslana obavijest e-poÅ¡tom. + text_regexp_info: eg. ^[A-Z0-9]+$ + text_min_max_length_info: 0 znaÄi bez ograniÄenja + text_project_destroy_confirmation: Da li ste sigurni da želite izbrisati ovaj projekt i sve njegove podatke? + text_subprojects_destroy_warning: "Njegov(i) potprojekt(i): %{value} će takoÄ‘er biti obrisan." + text_workflow_edit: Select a role and a tracker to edit the workflow + text_are_you_sure: Da li ste sigurni? + text_journal_changed: "%{label} promijenjen iz %{old} u %{new}" + text_journal_set_to: "%{label} postavi na %{value}" + text_journal_deleted: "%{label} izbrisano (%{old})" + text_journal_added: "%{label} %{value} added" + text_tip_issue_begin_day: Zadaci koji poÄinju ovog dana + text_tip_issue_end_day: zadaci koji se zavrÅ¡avaju ovog dana + text_tip_issue_begin_end_day: Zadaci koji poÄinju i zavrÅ¡avaju se ovog dana + text_caracters_maximum: "NajviÅ¡e %{count} znakova." + text_caracters_minimum: "Mora biti dugaÄko najmanje %{count} znakova." + text_length_between: "Dužina izmedu %{min} i %{max} znakova." + text_tracker_no_workflow: Tijek rada nije definiran za ovaj tracker + text_unallowed_characters: Nedozvoljeni znakovi + text_comma_separated: ViÅ¡estruke vrijednosti su dozvoljene (razdvojene zarezom). + text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages + text_tracker_no_workflow: No workflow defined for this tracker + text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages + text_issue_added: "Predmet %{id} je prijavljen (prijavio %{author})." + text_issue_updated: "Predmet %{id} je promijenjen %{author})." + text_wiki_destroy_confirmation: Da li ste sigurni da želite izbrisati ovaj wiki i njegov sadržaj? + text_issue_category_destroy_question: "Neke predmeti (%{count}) su dodijeljeni ovoj kategoriji. Å to želite uraditi?" + text_issue_category_destroy_assignments: Ukloni dodjeljivanje kategorija + text_issue_category_reassign_to: Ponovo dodijeli predmete ovoj kategoriji + text_user_mail_option: "Za neizabrane projekte, primit ćete obavjesti samo o stvarima koje pratite ili u kojima sudjelujete (npr. predmete koje ste vi napravili ili koje su vama dodjeljeni)." + text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded." + text_load_default_configuration: UÄitaj poÄetnu konfiguraciju + text_status_changed_by_changeset: "Applied in changeset %{value}." + text_issues_destroy_confirmation: 'Jeste li sigurni da želite obrisati izabrani/e predmet(e)?' + text_select_project_modules: 'Odaberite module koji će biti omogućeni za ovaj projekt:' + text_default_administrator_account_changed: Default administrator account changed + text_file_repository_writable: Dozvoljeno pisanje u direktorij za privitke + text_plugin_assets_writable: Plugin assets directory writable + text_rmagick_available: RMagick dostupan (nije obavezno) + text_destroy_time_entries_question: "%{hours} sati je prijavljeno za predmete koje želite obrisati. Å to ćete uÄiniti?" + text_destroy_time_entries: ObriÅ¡i prijavljene sate + text_assign_time_entries_to_project: Pridruži prijavljene sate projektu + text_reassign_time_entries: 'Premjesti prijavljene sate ovom predmetu:' + text_user_wrote: "%{value} je napisao/la:" + text_enumeration_destroy_question: "%{count} objekata je pridruženo toj vrijednosti." + text_enumeration_category_reassign_to: 'Premjesti ih ovoj vrijednosti:' + text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/configuration.yml and restart the application to enable them." + text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped." + text_diff_truncated: '... Ovaj diff je odrezan zato Å¡to prelazi maksimalnu veliÄinu koja može biti prikazana.' + text_custom_field_possible_values_info: 'One line for each value' + text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?" + text_wiki_page_nullify_children: "Keep child pages as root pages" + text_wiki_page_destroy_children: "Delete child pages and all their descendants" + text_wiki_page_reassign_children: "Reassign child pages to this parent page" + default_role_manager: Upravitelj + default_role_developer: Razvojni inženjer + default_role_reporter: Korisnik + default_tracker_bug: PogreÅ¡ka + default_tracker_feature: Funkcionalnost + default_tracker_support: PodrÅ¡ka + default_issue_status_new: Novo + default_issue_status_assigned: Dodijeljeno + default_issue_status_resolved: RijeÅ¡eno + default_issue_status_feedback: Povratna informacija + default_issue_status_closed: Zatvoreno + default_issue_status_rejected: Odbaceno + default_doc_category_user: KorisniÄka dokumentacija + default_doc_category_tech: TehniÄka dokumentacija + default_priority_low: Nizak + default_priority_normal: Redovan + default_priority_high: Visok + default_priority_urgent: Hitan + default_priority_immediate: Odmah + default_activity_design: Dizajn + default_activity_development: Razvoj + enumeration_issue_priorities: Prioriteti predmeta + enumeration_doc_categories: Kategorija dokumenata + enumeration_activities: Aktivnosti (po vremenu) + enumeration_system_activity: System Activity + field_sharing: Sharing + text_line_separated: Multiple values allowed (one line for each value). + label_close_versions: Close completed versions + button_unarchive: Unarchive + label_start_to_end: start to end + label_start_to_start: start to start + field_issue_to: Related issue + default_issue_status_in_progress: In Progress + text_own_membership_delete_confirmation: |- + You are about to remove some or all of your permissions and may no longer be able to edit this project after that. + Are you sure you want to continue? + label_board_sticky: Sticky + label_board_locked: Locked + permission_export_wiki_pages: Export wiki pages + setting_cache_formatted_text: Cache formatted text + permission_manage_project_activities: Manage project activities + error_unable_delete_issue_status: Unable to delete issue status + label_profile: Profile + permission_manage_subtasks: Manage subtasks + field_parent_issue: Parent task + label_subtask_plural: Subtasks + label_project_copy_notifications: Send email notifications during the project copy + error_can_not_delete_custom_field: Unable to delete custom field + error_unable_to_connect: Unable to connect (%{value}) + error_can_not_remove_role: This role is in use and can not be deleted. + error_can_not_delete_tracker: This tracker contains issues and can't be deleted. + field_principal: Principal + label_my_page_block: My page block + notice_failed_to_save_members: "Failed to save member(s): %{errors}." + text_zoom_out: Zoom out + text_zoom_in: Zoom in + notice_unable_delete_time_entry: Unable to delete time log entry. + label_overall_spent_time: Overall spent time + field_time_entries: Log time + project_module_gantt: Gantt + project_module_calendar: Calendar + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Text field + label_user_mail_option_only_owner: Only for things I am the owner of + setting_default_notification_option: Default notification option + label_user_mail_option_only_my_events: Only for things I watch or I'm involved in + label_user_mail_option_only_assigned: Only for things I am assigned to + label_user_mail_option_none: No events + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + notice_not_authorized_archived_project: The project you're trying to access has been archived. + label_principal_search: "Search for user or group:" + label_user_search: "Search for user:" + field_visible: Visible + setting_emails_header: Emails header + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Enable time logging + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maximum number of items displayed on the gantt chart + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: Priority updated + label_issues_visibility_own: Issues created by or assigned to the user + field_issues_visibility: Issues visibility + label_issues_visibility_all: All issues + permission_set_own_issues_private: Set own issues public or private + field_is_private: Private + permission_set_issues_private: Set issues public or private + label_issues_visibility_public: All non private issues + text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). + field_commit_logs_encoding: Commit messages encoding + field_scm_path_encoding: Path encoding + text_scm_path_encoding_note: "Default: UTF-8" + field_path_to_repository: Path to repository + field_root_directory: Root directory + field_cvs_module: Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Command + text_scm_command_version: Version + label_git_report_last_commit: Report last commit for files and directories + text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 predmet + one: 1 predmet + other: "%{count} predmeti" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: Svi + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: With subprojects + label_cross_project_tree: With project tree + label_cross_project_hierarchy: With project hierarchy + label_cross_project_system: With all projects + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2b/2bf169c59e60650b88bcb73540687a2bc02b9736.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2b/2bf169c59e60650b88bcb73540687a2bc02b9736.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,63 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../../test_helper', __FILE__) + +class CalendarTest < ActiveSupport::TestCase + + def test_monthly + c = Redmine::Helpers::Calendar.new(Date.today, :fr, :month) + assert_equal [1, 7], [c.startdt.cwday, c.enddt.cwday] + + c = Redmine::Helpers::Calendar.new('2007-07-14'.to_date, :fr, :month) + assert_equal ['2007-06-25'.to_date, '2007-08-05'.to_date], [c.startdt, c.enddt] + + c = Redmine::Helpers::Calendar.new(Date.today, :en, :month) + assert_equal [7, 6], [c.startdt.cwday, c.enddt.cwday] + end + + def test_weekly + c = Redmine::Helpers::Calendar.new(Date.today, :fr, :week) + assert_equal [1, 7], [c.startdt.cwday, c.enddt.cwday] + + c = Redmine::Helpers::Calendar.new('2007-07-14'.to_date, :fr, :week) + assert_equal ['2007-07-09'.to_date, '2007-07-15'.to_date], [c.startdt, c.enddt] + + c = Redmine::Helpers::Calendar.new(Date.today, :en, :week) + assert_equal [7, 6], [c.startdt.cwday, c.enddt.cwday] + end + + def test_monthly_start_day + [1, 6, 7].each do |day| + with_settings :start_of_week => day do + c = Redmine::Helpers::Calendar.new(Date.today, :en, :month) + assert_equal day , c.startdt.cwday + assert_equal (day + 5) % 7 + 1, c.enddt.cwday + end + end + end + + def test_weekly_start_day + [1, 6, 7].each do |day| + with_settings :start_of_week => day do + c = Redmine::Helpers::Calendar.new(Date.today, :en, :week) + assert_equal day, c.startdt.cwday + assert_equal (day + 5) % 7 + 1, c.enddt.cwday + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2b/2bffeae9e506603f05893c49874a197ccc797b57.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2b/2bffeae9e506603f05893c49874a197ccc797b57.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,38 @@ +

    <%= l(:label_board_plural) %>

    + + + + + + + + + +<% Board.board_tree(@boards) do |board, level| %> + + + + + + +<% end %> + +
    <%= l(:label_board) %><%= l(:label_topic_plural) %><%= l(:label_message_plural) %><%= l(:label_message_last) %>
    + <%= link_to h(board.name), {:action => 'show', :id => board}, :class => "board" %>
    + <%=h board.description %> +
    <%= board.topics_count %><%= board.messages_count %> + <% if board.last_message %> + <%= authoring board.last_message.created_on, board.last_message.author %>
    + <%= link_to_message board.last_message %> + <% end %> +
    + +<% other_formats_links do |f| %> + <%= f.link_to 'Atom', :url => {:controller => 'activities', :action => 'index', :id => @project, :show_messages => 1, :key => User.current.rss_key} %> +<% end %> + +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, {:controller => 'activities', :action => 'index', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}) %> +<% end %> + +<% html_title l(:label_board_plural) %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2c/2c03b2451a9c38796d0008f31034571e1a5677c0.svn-base --- a/.svn/pristine/2c/2c03b2451a9c38796d0008f31034571e1a5677c0.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,84 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class AdminController < ApplicationController - layout 'admin' - before_filter :require_admin - helper :sort - include SortHelper - - def index - @no_configuration_data = Redmine::DefaultData::Loader::no_data? - end - - def projects - @status = params[:status] ? params[:status].to_i : 1 - c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status]) - unless params[:name].blank? - name = "%#{params[:name].strip.downcase}%" - c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name] - end - @projects = Project.find :all, :order => 'lft', - :conditions => c.conditions - - render :action => "projects", :layout => false if request.xhr? - end - - def plugins - @plugins = Redmine::Plugin.all - end - - # Loads the default configuration - # (roles, trackers, statuses, workflow, enumerations) - def default_configuration - if request.post? - begin - Redmine::DefaultData::Loader::load(params[:lang]) - flash[:notice] = l(:notice_default_data_loaded) - rescue Exception => e - flash[:error] = l(:error_can_t_load_default_data, e.message) - end - end - redirect_to :action => 'index' - end - - def test_email - raise_delivery_errors = ActionMailer::Base.raise_delivery_errors - # Force ActionMailer to raise delivery errors so we can catch it - ActionMailer::Base.raise_delivery_errors = true - begin - @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 - ActionMailer::Base.raise_delivery_errors = raise_delivery_errors - redirect_to :controller => 'settings', :action => 'edit', :tab => 'notifications' - end - - def info - @db_adapter_name = ActiveRecord::Base.connection.adapter_name - @checklist = [ - [:text_default_administrator_account_changed, - User.find(:first, - :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?], - [:text_file_repository_writable, File.writable?(Attachment.storage_path)], - [:text_plugin_assets_writable, File.writable?(Engines.public_directory)], - [:text_rmagick_available, Object.const_defined?(:Magick)] - ] - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2c/2c2690417bf095320f6312e550c41841ed5d033a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2c/2c2690417bf095320f6312e550c41841ed5d033a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,165 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class AccountControllerOpenidTest < ActionController::TestCase + tests AccountController + fixtures :users, :roles + + def setup + User.current = nil + Setting.openid = '1' + end + + def teardown + Setting.openid = '0' + end + + if Object.const_defined?(:OpenID) + + def test_login_with_openid_for_existing_user + Setting.self_registration = '3' + existing_user = User.new(:firstname => 'Cool', + :lastname => 'User', + :mail => 'user@somedomain.com', + :identity_url => 'http://openid.example.com/good_user') + existing_user.login = 'cool_user' + assert existing_user.save! + + post :login, :openid_url => existing_user.identity_url + assert_redirected_to '/my/page' + end + + def test_login_with_invalid_openid_provider + Setting.self_registration = '0' + post :login, :openid_url => 'http;//openid.example.com/good_user' + assert_redirected_to home_url + end + + def test_login_with_openid_for_existing_non_active_user + Setting.self_registration = '2' + existing_user = User.new(:firstname => 'Cool', + :lastname => 'User', + :mail => 'user@somedomain.com', + :identity_url => 'http://openid.example.com/good_user', + :status => User::STATUS_REGISTERED) + existing_user.login = 'cool_user' + assert existing_user.save! + + post :login, :openid_url => existing_user.identity_url + assert_redirected_to '/login' + end + + def test_login_with_openid_with_new_user_created + Setting.self_registration = '3' + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_redirected_to '/my/account' + user = User.find_by_login('cool_user') + assert user + assert_equal 'Cool', user.firstname + assert_equal 'User', user.lastname + end + + def test_login_with_openid_with_new_user_and_self_registration_off + Setting.self_registration = '0' + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_redirected_to home_url + user = User.find_by_login('cool_user') + assert_nil user + end + + def test_login_with_openid_with_new_user_created_with_email_activation_should_have_a_token + Setting.self_registration = '1' + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_redirected_to '/login' + user = User.find_by_login('cool_user') + assert user + + token = Token.find_by_user_id_and_action(user.id, 'register') + assert token + end + + def test_login_with_openid_with_new_user_created_with_manual_activation + Setting.self_registration = '2' + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_redirected_to '/login' + user = User.find_by_login('cool_user') + assert user + assert_equal User::STATUS_REGISTERED, user.status + end + + def test_login_with_openid_with_new_user_with_conflict_should_register + Setting.self_registration = '3' + existing_user = User.new(:firstname => 'Cool', :lastname => 'User', :mail => 'user@somedomain.com') + existing_user.login = 'cool_user' + assert existing_user.save! + + post :login, :openid_url => 'http://openid.example.com/good_user' + assert_response :success + assert_template 'register' + assert assigns(:user) + assert_equal 'http://openid.example.com/good_user', assigns(:user)[:identity_url] + end + + def test_login_with_openid_with_new_user_with_missing_information_should_register + Setting.self_registration = '3' + + post :login, :openid_url => 'http://openid.example.com/good_blank_user' + assert_response :success + assert_template 'register' + assert assigns(:user) + assert_equal 'http://openid.example.com/good_blank_user', assigns(:user)[:identity_url] + + assert_select 'input[name=?]', 'user[login]' + assert_select 'input[name=?]', 'user[password]' + assert_select 'input[name=?]', 'user[password_confirmation]' + assert_select 'input[name=?][value=?]', 'user[identity_url]', 'http://openid.example.com/good_blank_user' + end + + def test_register_after_login_failure_should_not_require_user_to_enter_a_password + Setting.self_registration = '3' + + assert_difference 'User.count' do + post :register, :user => { + :login => 'good_blank_user', + :password => '', + :password_confirmation => '', + :firstname => 'Cool', + :lastname => 'User', + :mail => 'user@somedomain.com', + :identity_url => 'http://openid.example.com/good_blank_user' + } + assert_response 302 + end + + user = User.first(:order => 'id DESC') + assert_equal 'http://openid.example.com/good_blank_user', user.identity_url + assert user.hashed_password.blank?, "Hashed password was #{user.hashed_password}" + end + + def test_setting_openid_should_return_true_when_set_to_true + assert_equal true, Setting.openid? + end + + else + puts "Skipping openid tests." + + def test_dummy + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2c/2c3a4de5492d19130df8d9d9f0bc6d32734619b7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2c/2c3a4de5492d19130df8d9d9f0bc6d32734619b7.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,341 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/abstract_adapter' +require 'cgi' + +module Redmine + module Scm + module Adapters + class MercurialAdapter < AbstractAdapter + + # Mercurial executable name + HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg" + HELPERS_DIR = File.dirname(__FILE__) + "/mercurial" + HG_HELPER_EXT = "#{HELPERS_DIR}/redminehelper.py" + TEMPLATE_NAME = "hg-template" + TEMPLATE_EXTENSION = "tmpl" + + # raised if hg command exited with error, e.g. unknown revision. + class HgCommandAborted < CommandFailed; end + + class << self + def client_command + @@bin ||= HG_BIN + end + + def sq_bin + @@sq_bin ||= shell_quote_command + end + + def client_version + @@client_version ||= (hgversion || []) + end + + def client_available + client_version_above?([1, 2]) + end + + def hgversion + # The hg version is expressed either as a + # release number (eg 0.9.5 or 1.0) or as a revision + # id composed of 12 hexa characters. + theversion = hgversion_from_command_line.dup + if theversion.respond_to?(:force_encoding) + theversion.force_encoding('ASCII-8BIT') + end + if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)}) + m[2].scan(%r{\d+}).collect(&:to_i) + end + end + + def hgversion_from_command_line + shellout("#{sq_bin} --version") { |io| io.read }.to_s + end + + def template_path + @@template_path ||= template_path_for(client_version) + end + + def template_path_for(version) + "#{HELPERS_DIR}/#{TEMPLATE_NAME}-1.0.#{TEMPLATE_EXTENSION}" + end + end + + def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil) + super + @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding + end + + def path_encoding + @path_encoding + end + + def info + tip = summary['repository']['tip'] + Info.new(:root_url => CGI.unescape(summary['repository']['root']), + :lastrev => Revision.new(:revision => tip['revision'], + :scmid => tip['node'])) + # rescue HgCommandAborted + rescue Exception => e + logger.error "hg: error during getting info: #{e.message}" + nil + end + + def tags + as_ary(summary['repository']['tag']).map { |e| e['name'] } + end + + # Returns map of {'tag' => 'nodeid', ...} + def tagmap + alist = as_ary(summary['repository']['tag']).map do |e| + e.values_at('name', 'node') + end + Hash[*alist.flatten] + end + + def branches + brs = [] + as_ary(summary['repository']['branch']).each do |e| + br = Branch.new(e['name']) + br.revision = e['revision'] + br.scmid = e['node'] + brs << br + end + brs + end + + # Returns map of {'branch' => 'nodeid', ...} + def branchmap + alist = as_ary(summary['repository']['branch']).map do |e| + e.values_at('name', 'node') + end + Hash[*alist.flatten] + end + + def summary + return @summary if @summary + hg 'rhsummary' do |io| + output = io.read + if output.respond_to?(:force_encoding) + output.force_encoding('UTF-8') + end + begin + @summary = parse_xml(output)['rhsummary'] + rescue + end + end + end + private :summary + + def entries(path=nil, identifier=nil, options={}) + p1 = scm_iconv(@path_encoding, 'UTF-8', path) + manifest = hg('rhmanifest', '-r', CGI.escape(hgrev(identifier)), + CGI.escape(without_leading_slash(p1.to_s))) do |io| + output = io.read + if output.respond_to?(:force_encoding) + output.force_encoding('UTF-8') + end + begin + parse_xml(output)['rhmanifest']['repository']['manifest'] + rescue + end + end + path_prefix = path.blank? ? '' : with_trailling_slash(path) + + entries = Entries.new + as_ary(manifest['dir']).each do |e| + n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name'])) + p = "#{path_prefix}#{n}" + entries << Entry.new(:name => n, :path => p, :kind => 'dir') + end + + as_ary(manifest['file']).each do |e| + n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name'])) + p = "#{path_prefix}#{n}" + lr = Revision.new(:revision => e['revision'], :scmid => e['node'], + :identifier => e['node'], + :time => Time.at(e['time'].to_i)) + entries << Entry.new(:name => n, :path => p, :kind => 'file', + :size => e['size'].to_i, :lastrev => lr) + end + + entries + rescue HgCommandAborted + nil # means not found + end + + def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) + revs = Revisions.new + each_revision(path, identifier_from, identifier_to, options) { |e| revs << e } + revs + end + + # Iterates the revisions by using a template file that + # makes Mercurial produce a xml output. + def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={}) + hg_args = ['log', '--debug', '-C', '--style', self.class.template_path] + hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}" + hg_args << '--limit' << options[:limit] if options[:limit] + hg_args << hgtarget(path) unless path.blank? + log = hg(*hg_args) do |io| + output = io.read + if output.respond_to?(:force_encoding) + output.force_encoding('UTF-8') + end + begin + # Mercurial < 1.5 does not support footer template for '' + parse_xml("#{output}")['log'] + rescue + end + end + as_ary(log['logentry']).each do |le| + cpalist = as_ary(le['paths']['path-copied']).map do |e| + [e['__content__'], e['copyfrom-path']].map do |s| + scm_iconv('UTF-8', @path_encoding, CGI.unescape(s)) + end + end + cpmap = Hash[*cpalist.flatten] + paths = as_ary(le['paths']['path']).map do |e| + p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']) ) + {:action => e['action'], + :path => with_leading_slash(p), + :from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil), + :from_revision => (cpmap.member?(p) ? le['node'] : nil)} + end.sort { |a, b| a[:path] <=> b[:path] } + parents_ary = [] + as_ary(le['parents']['parent']).map do |par| + parents_ary << par['__content__'] if par['__content__'] != "000000000000" + end + yield Revision.new(:revision => le['revision'], + :scmid => le['node'], + :author => (le['author']['__content__'] rescue ''), + :time => Time.parse(le['date']['__content__']), + :message => le['msg']['__content__'], + :paths => paths, + :parents => parents_ary) + end + self + end + + # Returns list of nodes in the specified branch + def nodes_in_branch(branch, options={}) + hg_args = ['rhlog', '--template', '{node|short}\n', '--rhbranch', CGI.escape(branch)] + hg_args << '--from' << CGI.escape(branch) + hg_args << '--to' << '0' + hg_args << '--limit' << options[:limit] if options[:limit] + hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } } + end + + def diff(path, identifier_from, identifier_to=nil) + hg_args = %w|rhdiff| + if identifier_to + hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from) + else + hg_args << '-c' << hgrev(identifier_from) + end + unless path.blank? + p = scm_iconv(@path_encoding, 'UTF-8', path) + hg_args << CGI.escape(hgtarget(p)) + end + diff = [] + hg *hg_args do |io| + io.each_line do |line| + diff << line + end + end + diff + rescue HgCommandAborted + nil # means not found + end + + def cat(path, identifier=nil) + p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path)) + hg 'rhcat', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io| + io.binmode + io.read + end + rescue HgCommandAborted + nil # means not found + end + + def annotate(path, identifier=nil) + p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path)) + blame = Annotate.new + hg 'rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io| + io.each_line do |line| + line.force_encoding('ASCII-8BIT') if line.respond_to?(:force_encoding) + next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$} + r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3, + :identifier => $3) + blame.add_line($4.rstrip, r) + end + end + blame + rescue HgCommandAborted + # means not found or cannot be annotated + Annotate.new + end + + class Revision < Redmine::Scm::Adapters::Revision + # Returns the readable identifier + def format_identifier + "#{revision}:#{scmid}" + end + end + + # Runs 'hg' command with the given args + def hg(*args, &block) + repo_path = root_url || url + full_args = ['-R', repo_path, '--encoding', 'utf-8'] + full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}" + full_args << '--config' << 'diff.git=false' + full_args += args + ret = shellout( + self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '), + &block + ) + if $? && $?.exitstatus != 0 + raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}" + end + ret + end + private :hg + + # Returns correct revision identifier + def hgrev(identifier, sq=false) + rev = identifier.blank? ? 'tip' : identifier.to_s + rev = shell_quote(rev) if sq + rev + end + private :hgrev + + def hgtarget(path) + path ||= '' + root_url + '/' + without_leading_slash(path) + end + private :hgtarget + + def as_ary(o) + return [] unless o + o.is_a?(Array) ? o : Array[o] + end + private :as_ary + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2d/2d40a9e2c317c7ef78f729e12dcba72428f142c7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2d/2d40a9e2c317c7ef78f729e12dcba72428f142c7.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1083 @@ +sl: + # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) + direction: ltr + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%Y-%m-%d" + short: "%b %d" + long: "%B %d, %Y" + + day_names: [Nedelja, Ponedeljek, Torek, Sreda, ÄŒetrtek, Petek, Sobota] + abbr_day_names: [Ned, Pon, To, Sr, ÄŒet, Pet, Sob] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, Januar, Februar, Marec, April, Maj, Junij, Julij, Avgust, September, Oktober, November, December] + abbr_month_names: [~, Jan, Feb, Mar, Apr, Maj, Jun, Jul, Aug, Sep, Okt, Nov, Dec] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%a, %d %b %Y %H:%M:%S %z" + time: "%H:%M" + short: "%d %b %H:%M" + long: "%B %d, %Y %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "pol minute" + less_than_x_seconds: + one: "manj kot 1. sekundo" + other: "manj kot %{count} sekund" + x_seconds: + one: "1. sekunda" + other: "%{count} sekund" + less_than_x_minutes: + one: "manj kot minuto" + other: "manj kot %{count} minut" + x_minutes: + one: "1 minuta" + other: "%{count} minut" + about_x_hours: + one: "okrog 1. ure" + other: "okrog %{count} ur" + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "1 dan" + other: "%{count} dni" + about_x_months: + one: "okrog 1. mesec" + other: "okrog %{count} mesecev" + x_months: + one: "1 mesec" + other: "%{count} mesecev" + about_x_years: + one: "okrog 1. leto" + other: "okrog %{count} let" + over_x_years: + one: "veÄ kot 1. leto" + other: "veÄ kot %{count} let" + almost_x_years: + one: "skoraj 1. leto" + other: "skoraj %{count} let" + + number: + format: + separator: "," + delimiter: "." + precision: 3 + human: + format: + precision: 3 + delimiter: "" + storage_units: + format: "%n %u" + units: + kb: KB + tb: TB + gb: GB + byte: + one: Byte + other: Bytes + mb: MB + +# Used in array.to_sentence. + support: + array: + sentence_connector: "in" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1. napaka je prepreÄila temu %{model} da bi se shranil" + other: "%{count} napak je prepreÄilo temu %{model} da bi se shranil" + messages: + inclusion: "ni vkljuÄen na seznamu" + exclusion: "je rezerviran" + invalid: "je napaÄen" + confirmation: "ne ustreza potrdilu" + accepted: "mora biti sprejet" + empty: "ne sme biti prazen" + blank: "ne sme biti neizpolnjen" + too_long: "je predolg" + too_short: "je prekratek" + wrong_length: "je napaÄne dolžine" + taken: "je že zaseden" + not_a_number: "ni Å¡tevilo" + not_a_date: "ni veljaven datum" + greater_than: "mora biti veÄji kot %{count}" + greater_than_or_equal_to: "mora biti veÄji ali enak kot %{count}" + equal_to: "mora biti enak kot %{count}" + less_than: "mora biti manjÅ¡i kot %{count}" + less_than_or_equal_to: "mora biti manjÅ¡i ali enak kot %{count}" + odd: "mora biti sodo" + even: "mora biti liho" + greater_than_start_date: "mora biti kasnejÅ¡i kot zaÄetni datum" + not_same_project: "ne pripada istemu projektu" + circular_dependency: "Ta odnos bi povzroÄil krožno odvisnost" + cant_link_an_issue_with_a_descendant: "Zahtevek ne more biti povezan s svojo podnalogo" + + actionview_instancetag_blank_option: Prosimo izberite + + general_text_No: 'Ne' + general_text_Yes: 'Da' + general_text_no: 'ne' + general_text_yes: 'da' + general_lang_name: 'SlovenÅ¡Äina' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: RaÄun je bil uspeÅ¡no posodobljen. + notice_account_invalid_creditentials: NapaÄno uporabniÅ¡ko ime ali geslo + notice_account_password_updated: Geslo je bilo uspeÅ¡no posodobljeno. + notice_account_wrong_password: NapaÄno geslo + notice_account_register_done: RaÄun je bil uspeÅ¡no ustvarjen. Za aktivacijo potrdite povezavo, ki vam je bila poslana v e-nabiralnik. + notice_account_unknown_email: Neznan uporabnik. + notice_can_t_change_password: Ta raÄun za overovljanje uporablja zunanji. Gesla ni mogoÄe spremeniti. + notice_account_lost_email_sent: Poslano vam je bilo e-pismo z navodili za izbiro novega gesla. + notice_account_activated: VaÅ¡ raÄun je bil aktiviran. Sedaj se lahko prijavite. + notice_successful_create: Ustvarjanje uspelo. + notice_successful_update: Posodobitev uspela. + notice_successful_delete: Izbris uspel. + notice_successful_connection: Povezava uspela. + notice_file_not_found: Stran na katero se želite povezati ne obstaja ali pa je bila umaknjena. + notice_locking_conflict: Drug uporabnik je posodobil podatke. + notice_not_authorized: Nimate privilegijev za dostop do te strani. + notice_email_sent: "E-poÅ¡tno sporoÄilo je bilo poslano %{value}" + notice_email_error: "Ob poÅ¡iljanju e-sporoÄila je priÅ¡lo do napake (%{value})" + notice_feeds_access_key_reseted: VaÅ¡ RSS dostopni kljuÄ je bil ponastavljen. + notice_failed_to_save_issues: "Neuspelo shranjevanje %{count} zahtevka na %{total} izbranem: %{ids}." + notice_no_issue_selected: "Izbran ni noben zahtevek! Prosimo preverite zahtevke, ki jih želite urediti." + notice_account_pending: "VaÅ¡ raÄun je bil ustvarjen in Äaka na potrditev s strani administratorja." + notice_default_data_loaded: Privzete nastavitve so bile uspeÅ¡no naložene. + notice_unable_delete_version: Verzije ni bilo mogoÄe izbrisati. + + error_can_t_load_default_data: "Privzetih nastavitev ni bilo mogoÄe naložiti: %{value}" + error_scm_not_found: "Vnos ali revizija v shrambi ni bila najdena ." + error_scm_command_failed: "Med vzpostavljem povezave s shrambo je priÅ¡lo do napake: %{value}" + error_scm_annotate: "Vnos ne obstaja ali pa ga ni mogoÄe komentirati." + error_issue_not_found_in_project: 'Zahtevek ni bil najden ali pa ne pripada temu projektu' + + mail_subject_lost_password: "VaÅ¡e %{value} geslo" + mail_body_lost_password: 'Za spremembo glesla kliknite na naslednjo povezavo:' + mail_subject_register: "Aktivacija %{value} vaÅ¡ega raÄuna" + mail_body_register: 'Za aktivacijo vaÅ¡ega raÄuna kliknite na naslednjo povezavo:' + mail_body_account_information_external: "Za prijavo lahko uporabite vaÅ¡ %{value} raÄun." + mail_body_account_information: Informacije o vaÅ¡em raÄunu + mail_subject_account_activation_request: "%{value} zahtevek za aktivacijo raÄuna" + mail_body_account_activation_request: "Registriral se je nov uporabnik (%{value}). RaÄun Äaka na vaÅ¡o odobritev:" + mail_subject_reminder: "%{count} zahtevek(zahtevki) zapadejo v naslednjih %{days} dneh" + mail_body_reminder: "%{count} zahtevek(zahtevki), ki so vam dodeljeni bodo zapadli v naslednjih %{days} dneh:" + + gui_validation_error: 1 napaka + gui_validation_error_plural: "%{count} napak" + + field_name: Ime + field_description: Opis + field_summary: Povzetek + field_is_required: Zahtevano + field_firstname: Ime + field_lastname: Priimek + field_mail: E-naslov + field_filename: Datoteka + field_filesize: Velikost + field_downloads: Prenosi + field_author: Avtor + field_created_on: Ustvarjen + field_updated_on: Posodobljeno + field_field_format: Format + field_is_for_all: Za vse projekte + field_possible_values: Možne vrednosti + field_regexp: Regularni izraz + field_min_length: Minimalna dolžina + field_max_length: Maksimalna dolžina + field_value: Vrednost + field_category: Kategorija + field_title: Naslov + field_project: Projekt + field_issue: Zahtevek + field_status: Status + field_notes: Zabeležka + field_is_closed: Zahtevek zaprt + field_is_default: Privzeta vrednost + field_tracker: Vrsta zahtevka + field_subject: Tema + field_due_date: Do datuma + field_assigned_to: Dodeljen + field_priority: Prioriteta + field_fixed_version: Ciljna verzija + field_user: Uporabnik + field_role: Vloga + field_homepage: DomaÄa stran + field_is_public: Javno + field_parent: Podprojekt projekta + field_is_in_roadmap: Zahtevki prikazani na zemljevidu + field_login: Prijava + field_mail_notification: E-poÅ¡tna oznanila + field_admin: Administrator + field_last_login_on: ZadnjiÄ povezan(a) + field_language: Jezik + field_effective_date: Datum + field_password: Geslo + field_new_password: Novo geslo + field_password_confirmation: Potrditev + field_version: Verzija + field_type: Tip + field_host: Gostitelj + field_port: Vrata + field_account: RaÄun + field_base_dn: Bazni DN + field_attr_login: Oznaka za prijavo + field_attr_firstname: Oznaka za ime + field_attr_lastname: Oznaka za priimek + field_attr_mail: Oznaka za e-naslov + field_onthefly: Sprotna izdelava uporabnikov + field_start_date: ZaÄetek + field_done_ratio: "% Narejeno" + field_auth_source: NaÄin overovljanja + field_hide_mail: Skrij moj e-naslov + field_comments: Komentar + field_url: URL + field_start_page: ZaÄetna stran + field_subproject: Podprojekt + field_hours: Ur + field_activity: Aktivnost + field_spent_on: Datum + field_identifier: Identifikator + field_is_filter: Uporabljen kot filter + field_issue_to: Povezan zahtevek + field_delay: Zamik + field_assignable: Zahtevki so lahko dodeljeni tej vlogi + field_redirect_existing_links: Preusmeri obstojeÄe povezave + field_estimated_hours: Ocenjen Äas + field_column_names: Stolpci + field_time_zone: ÄŒasovni pas + field_searchable: Zmožen iskanja + field_default_value: Privzeta vrednost + field_comments_sorting: Prikaži komentarje + field_parent_title: MatiÄna stran + + setting_app_title: Naslov aplikacije + setting_app_subtitle: Podnaslov aplikacije + setting_welcome_text: Pozdravno besedilo + setting_default_language: Privzeti jezik + setting_login_required: Zahtevano overovljanje + setting_self_registration: Samostojna registracija + setting_attachment_max_size: Maksimalna velikost priponk + setting_issues_export_limit: Skrajna meja za izvoz zahtevkov + setting_mail_from: E-naslov za emisijo + setting_bcc_recipients: Prejemniki slepih kopij (bcc) + setting_plain_text_mail: navadno e-sporoÄilo (ne HTML) + setting_host_name: Ime gostitelja in pot + setting_text_formatting: Oblikovanje besedila + setting_wiki_compression: Stiskanje Wiki zgodovine + setting_feeds_limit: Meja obsega RSS virov + setting_default_projects_public: Novi projekti so privzeto javni + setting_autofetch_changesets: Samodejni izvleÄek zapisa sprememb + setting_sys_api_enabled: OmogoÄi WS za upravljanje shrambe + setting_commit_ref_keywords: Sklicne kljuÄne besede + setting_commit_fix_keywords: Urejanje kljuÄne besede + setting_autologin: Avtomatska prijava + setting_date_format: Oblika datuma + setting_time_format: Oblika Äasa + setting_cross_project_issue_relations: Dovoli povezave zahtevkov med razliÄnimi projekti + setting_issue_list_default_columns: Privzeti stolpci prikazani na seznamu zahtevkov + setting_emails_footer: Noga e-sporoÄil + setting_protocol: Protokol + setting_per_page_options: Å tevilo elementov na stran + setting_user_format: Oblika prikaza uporabnikov + setting_activity_days_default: Prikaz dni na aktivnost projekta + setting_display_subprojects_issues: Privzeti prikaz zahtevkov podprojektov v glavnem projektu + setting_enabled_scm: OmogoÄen SCM + setting_mail_handler_api_enabled: OmogoÄi WS za prihajajoÄo e-poÅ¡to + setting_mail_handler_api_key: API kljuÄ + setting_sequential_project_identifiers: Generiraj projektne identifikatorje sekvenÄno + setting_gravatar_enabled: Uporabljaj Gravatar ikone + setting_diff_max_lines_displayed: Maksimalno Å¡tevilo prikazanih vrstic razliÄnosti + + permission_edit_project: Uredi projekt + permission_select_project_modules: Izberi module projekta + permission_manage_members: Uredi Älane + permission_manage_versions: Uredi verzije + permission_manage_categories: Urejanje kategorij zahtevkov + permission_add_issues: Dodaj zahtevke + permission_edit_issues: Uredi zahtevke + permission_manage_issue_relations: Uredi odnose med zahtevki + permission_add_issue_notes: Dodaj zabeležke + permission_edit_issue_notes: Uredi zabeležke + permission_edit_own_issue_notes: Uredi lastne zabeležke + permission_move_issues: Premakni zahtevke + permission_delete_issues: IzbriÅ¡i zahtevke + permission_manage_public_queries: Uredi javna povpraÅ¡evanja + permission_save_queries: Shrani povpraÅ¡evanje + permission_view_gantt: Poglej gantogram + permission_view_calendar: Poglej koledar + permission_view_issue_watchers: Oglej si listo spremeljevalcev + permission_add_issue_watchers: Dodaj spremljevalce + permission_log_time: Beleži porabljen Äas + permission_view_time_entries: Poglej porabljen Äas + permission_edit_time_entries: Uredi beležko Äasa + permission_edit_own_time_entries: Uredi beležko lastnega Äasa + permission_manage_news: Uredi novice + permission_comment_news: Komentiraj novice + permission_manage_documents: Uredi dokumente + permission_view_documents: Poglej dokumente + permission_manage_files: Uredi datoteke + permission_view_files: Poglej datoteke + permission_manage_wiki: Uredi wiki + permission_rename_wiki_pages: Preimenuj wiki strani + permission_delete_wiki_pages: IzbriÅ¡i wiki strani + permission_view_wiki_pages: Poglej wiki + permission_view_wiki_edits: Poglej wiki zgodovino + permission_edit_wiki_pages: Uredi wiki strani + permission_delete_wiki_pages_attachments: IzbriÅ¡i priponke + permission_protect_wiki_pages: ZaÅ¡Äiti wiki strani + permission_manage_repository: Uredi shrambo + permission_browse_repository: Prebrskaj shrambo + permission_view_changesets: Poglej zapis sprememb + permission_commit_access: Dostop za predajo + permission_manage_boards: Uredi table + permission_view_messages: Poglej sporoÄila + permission_add_messages: Objavi sporoÄila + permission_edit_messages: Uredi sporoÄila + permission_edit_own_messages: Uredi lastna sporoÄila + permission_delete_messages: IzbriÅ¡i sporoÄila + permission_delete_own_messages: IzbriÅ¡i lastna sporoÄila + + project_module_issue_tracking: Sledenje zahtevkom + project_module_time_tracking: Sledenje Äasa + project_module_news: Novice + project_module_documents: Dokumenti + project_module_files: Datoteke + project_module_wiki: Wiki + project_module_repository: Shramba + project_module_boards: Table + + label_user: Uporabnik + label_user_plural: Uporabniki + label_user_new: Nov uporabnik + label_project: Projekt + label_project_new: Nov projekt + label_project_plural: Projekti + label_x_projects: + zero: ni projektov + one: 1 projekt + other: "%{count} projektov" + label_project_all: Vsi projekti + label_project_latest: Zadnji projekti + label_issue: Zahtevek + label_issue_new: Nov zahtevek + label_issue_plural: Zahtevki + label_issue_view_all: Poglej vse zahtevke + label_issues_by: "Zahtevki od %{value}" + label_issue_added: Zahtevek dodan + label_issue_updated: Zahtevek posodobljen + label_document: Dokument + label_document_new: Nov dokument + label_document_plural: Dokumenti + label_document_added: Dokument dodan + label_role: Vloga + label_role_plural: Vloge + label_role_new: Nova vloga + label_role_and_permissions: Vloge in dovoljenja + label_member: ÄŒlan + label_member_new: Nov Älan + label_member_plural: ÄŒlani + label_tracker: Vrsta zahtevka + label_tracker_plural: Vrste zahtevkov + label_tracker_new: Nova vrsta zahtevka + label_workflow: Potek dela + label_issue_status: Stanje zahtevka + label_issue_status_plural: Stanje zahtevkov + label_issue_status_new: Novo stanje + label_issue_category: Kategorija zahtevka + label_issue_category_plural: Kategorije zahtevkov + label_issue_category_new: Nova kategorija + label_custom_field: Polje po meri + label_custom_field_plural: Polja po meri + label_custom_field_new: Novo polje po meri + label_enumerations: Seznami + label_enumeration_new: Nova vrednost + label_information: Informacija + label_information_plural: Informacije + label_please_login: Prosimo prijavite se + label_register: Registracija + label_password_lost: Izgubljeno geslo + label_home: Domov + label_my_page: Moja stran + label_my_account: Moj raÄun + label_my_projects: Moji projekti + label_administration: Upravljanje + label_login: Prijavi se + label_logout: Odjavi se + label_help: PomoÄ + label_reported_issues: Prijavljeni zahtevki + label_assigned_to_me_issues: Zahtevki dodeljeni meni + label_last_login: Zadnja povezava + label_registered_on: Registriran + label_activity: Aktivnost + label_overall_activity: Celotna aktivnost + label_user_activity: "Aktivnost %{value}" + label_new: Nov + label_logged_as: Prijavljen(a) kot + label_environment: Okolje + label_authentication: Overovitev + label_auth_source: NaÄin overovitve + label_auth_source_new: Nov naÄin overovitve + label_auth_source_plural: NaÄini overovitve + label_subproject_plural: Podprojekti + label_and_its_subprojects: "%{value} in njegovi podprojekti" + label_min_max_length: Min - Max dolžina + label_list: Seznam + label_date: Datum + label_integer: Celo Å¡tevilo + label_float: Decimalno Å¡tevilo + label_boolean: Boolean + label_string: Besedilo + label_text: Dolgo besedilo + label_attribute: Lastnost + label_attribute_plural: Lastnosti + label_download: "%{count} Prenos" + label_download_plural: "%{count} Prenosi" + label_no_data: Ni podatkov za prikaz + label_change_status: Spremeni stanje + label_history: Zgodovina + label_attachment: Datoteka + label_attachment_new: Nova datoteka + label_attachment_delete: IzbriÅ¡i datoteko + label_attachment_plural: Datoteke + label_file_added: Datoteka dodana + label_report: PoroÄilo + label_report_plural: PoroÄila + label_news: Novica + label_news_new: Dodaj novico + label_news_plural: Novice + label_news_latest: Zadnje novice + label_news_view_all: Poglej vse novice + label_news_added: Dodane novice + label_settings: Nastavitve + label_overview: Pregled + label_version: Verzija + label_version_new: Nova verzija + label_version_plural: Verzije + label_confirmation: Potrditev + label_export_to: 'Na razpolago tudi v:' + label_read: Preberi... + label_public_projects: Javni projekti + label_open_issues: odprt zahtevek + label_open_issues_plural: odprti zahtevki + label_closed_issues: zaprt zahtevek + label_closed_issues_plural: zaprti zahtevki + label_x_open_issues_abbr_on_total: + zero: 0 odprtih / %{total} + one: 1 odprt / %{total} + other: "%{count} odprtih / %{total}" + label_x_open_issues_abbr: + zero: 0 odprtih + one: 1 odprt + other: "%{count} odprtih" + label_x_closed_issues_abbr: + zero: 0 zaprtih + one: 1 zaprt + other: "%{count} zaprtih" + label_total: Skupaj + label_permissions: Dovoljenja + label_current_status: Trenutno stanje + label_new_statuses_allowed: Novi zahtevki dovoljeni + label_all: vsi + label_none: noben + label_nobody: nihÄe + label_next: Naslednji + label_previous: PrejÅ¡nji + label_used_by: V uporabi od + label_details: Podrobnosti + label_add_note: Dodaj zabeležko + label_per_page: Na stran + label_calendar: Koledar + label_months_from: mesecev od + label_gantt: Gantogram + label_internal: Notranji + label_last_changes: "zadnjih %{count} sprememb" + label_change_view_all: Poglej vse spremembe + label_personalize_page: Individualiziraj to stran + label_comment: Komentar + label_comment_plural: Komentarji + label_x_comments: + zero: ni komentarjev + one: 1 komentar + other: "%{count} komentarjev" + label_comment_add: Dodaj komentar + label_comment_added: Komentar dodan + label_comment_delete: IzbriÅ¡i komentarje + label_query: Iskanje po meri + label_query_plural: Iskanja po meri + label_query_new: Novo iskanje + label_filter_add: Dodaj filter + label_filter_plural: Filtri + label_equals: je enako + label_not_equals: ni enako + label_in_less_than: v manj kot + label_in_more_than: v veÄ kot + label_in: v + label_today: danes + label_all_time: v vsem Äasu + label_yesterday: vÄeraj + label_this_week: ta teden + label_last_week: pretekli teden + label_last_n_days: "zadnjih %{count} dni" + label_this_month: ta mesec + label_last_month: zadnji mesec + label_this_year: to leto + label_date_range: Razpon datumov + label_less_than_ago: manj kot dni nazaj + label_more_than_ago: veÄ kot dni nazaj + label_ago: dni nazaj + label_contains: vsebuje + label_not_contains: ne vsebuje + label_day_plural: dni + label_repository: Shramba + label_repository_plural: Shrambe + label_browse: Prebrskaj + label_modification: "%{count} sprememba" + label_modification_plural: "%{count} spremembe" + label_revision: Revizija + label_revision_plural: Revizije + label_associated_revisions: Povezane revizije + label_added: dodano + label_modified: spremenjeno + label_copied: kopirano + label_renamed: preimenovano + label_deleted: izbrisano + label_latest_revision: Zadnja revizija + label_latest_revision_plural: Zadnje revizije + label_view_revisions: Poglej revizije + label_max_size: NajveÄja velikost + label_sort_highest: Premakni na vrh + label_sort_higher: Premakni gor + label_sort_lower: Premakni dol + label_sort_lowest: Premakni na dno + label_roadmap: NaÄrt + label_roadmap_due_in: "Do %{value}" + label_roadmap_overdue: "%{value} zakasnel" + label_roadmap_no_issues: Ni zahtevkov za to verzijo + label_search: IÅ¡Äi + label_result_plural: Rezultati + label_all_words: Vse besede + label_wiki: Wiki + label_wiki_edit: Wiki urejanje + label_wiki_edit_plural: Wiki urejanja + label_wiki_page: Wiki stran + label_wiki_page_plural: Wiki strani + label_index_by_title: Razvrsti po naslovu + label_index_by_date: Razvrsti po datumu + label_current_version: Trenutna verzija + label_preview: Predogled + label_feed_plural: RSS viri + label_changes_details: Podrobnosti o vseh spremembah + label_issue_tracking: Sledenje zahtevkom + label_spent_time: Porabljen Äas + label_f_hour: "%{value} ura" + label_f_hour_plural: "%{value} ur" + label_time_tracking: Sledenje Äasu + label_change_plural: Spremembe + label_statistics: Statistika + label_commits_per_month: Predaj na mesec + label_commits_per_author: Predaj na avtorja + label_view_diff: Preglej razlike + label_diff_inline: znotraj + label_diff_side_by_side: vzporedno + label_options: Možnosti + label_copy_workflow_from: Kopiraj potek dela od + label_permissions_report: PoroÄilo o dovoljenjih + label_watched_issues: Spremljani zahtevki + label_related_issues: Povezani zahtevki + label_applied_status: Uveljavljeno stanje + label_loading: Nalaganje... + label_relation_new: Nova povezava + label_relation_delete: IzbriÅ¡i povezavo + label_relates_to: povezan z + label_duplicates: duplikati + label_duplicated_by: dupliciral + label_blocks: blok + label_blocked_by: blokiral + label_precedes: ima prednost pred + label_follows: sledi + label_end_to_start: konec na zaÄetek + label_end_to_end: konec na konec + label_start_to_start: zaÄetek na zaÄetek + label_start_to_end: zaÄetek na konec + label_stay_logged_in: Ostani prijavljen(a) + label_disabled: onemogoÄi + label_show_completed_versions: Prikaži zakljuÄene verzije + label_me: jaz + label_board: Forum + label_board_new: Nov forum + label_board_plural: Forumi + label_topic_plural: Teme + label_message_plural: SporoÄila + label_message_last: Zadnje sporoÄilo + label_message_new: Novo sporoÄilo + label_message_posted: SporoÄilo dodano + label_reply_plural: Odgovori + label_send_information: PoÅ¡lji informacijo o raÄunu uporabniku + label_year: Leto + label_month: Mesec + label_week: Teden + label_date_from: Do + label_date_to: Do + label_language_based: Glede na uporabnikov jezik + label_sort_by: "Razporedi po %{value}" + label_send_test_email: PoÅ¡lji testno e-pismo + label_feeds_access_key_created_on: "RSS dostopni kljuÄ narejen %{value} nazaj" + label_module_plural: Moduli + label_added_time_by: "Dodan %{author} %{age} nazaj" + label_updated_time_by: "Posodobljen od %{author} %{age} nazaj" + label_updated_time: "Posodobljen %{value} nazaj" + label_jump_to_a_project: SkoÄi na projekt... + label_file_plural: Datoteke + label_changeset_plural: Zapisi sprememb + label_default_columns: Privzeti stolpci + label_no_change_option: (Ni spremembe) + label_bulk_edit_selected_issues: Uredi izbrane zahtevke skupaj + label_theme: Tema + label_default: Privzeto + label_search_titles_only: PreiÅ¡Äi samo naslove + label_user_mail_option_all: "Za vsak dogodek v vseh mojih projektih" + label_user_mail_option_selected: "Za vsak dogodek samo na izbranih projektih..." + label_user_mail_no_self_notified: "Ne želim biti opozorjen(a) na spremembe, ki jih naredim sam(a)" + label_registration_activation_by_email: aktivacija raÄuna po e-poÅ¡ti + label_registration_manual_activation: roÄna aktivacija raÄuna + label_registration_automatic_activation: samodejna aktivacija raÄuna + label_display_per_page: "Na stran: %{value}" + label_age: Starost + label_change_properties: Sprememba lastnosti + label_general: SploÅ¡no + label_more: VeÄ + label_scm: SCM + label_plugins: VtiÄniki + label_ldap_authentication: LDAP overovljanje + label_downloads_abbr: D/L + label_optional_description: Neobvezen opis + label_add_another_file: Dodaj Å¡e eno datoteko + label_preferences: Preference + label_chronological_order: KronoloÅ¡ko + label_reverse_chronological_order: Obrnjeno kronoloÅ¡ko + label_planning: NaÄrtovanje + label_incoming_emails: PrihajajoÄa e-poÅ¡ta + label_generate_key: Ustvari kljuÄ + label_issue_watchers: Spremljevalci + label_example: Vzorec + + button_login: Prijavi se + button_submit: PoÅ¡lji + button_save: Shrani + button_check_all: OznaÄi vse + button_uncheck_all: OdznaÄi vse + button_delete: IzbriÅ¡i + button_create: Ustvari + button_test: Testiraj + button_edit: Uredi + button_add: Dodaj + button_change: Spremeni + button_apply: Uporabi + button_clear: PoÄisti + button_lock: Zakleni + button_unlock: Odkleni + button_download: Prenesi + button_list: Seznam + button_view: Pogled + button_move: Premakni + button_back: Nazaj + button_cancel: PrekliÄi + button_activate: Aktiviraj + button_sort: Razvrsti + button_log_time: Beleži Äas + button_rollback: Povrni na to verzijo + button_watch: Spremljaj + button_unwatch: Ne spremljaj + button_reply: Odgovori + button_archive: Arhiviraj + button_unarchive: Odarhiviraj + button_reset: Ponastavi + button_rename: Preimenuj + button_change_password: Spremeni geslo + button_copy: Kopiraj + button_annotate: ZapiÅ¡i pripombo + button_update: Posodobi + button_configure: Konfiguriraj + button_quote: Citiraj + + status_active: aktivni + status_registered: registriran + status_locked: zaklenjen + + text_select_mail_notifications: Izberi dejanja za katera naj bodo poslana oznanila preko e-poÅ¡to. + text_regexp_info: npr. ^[A-Z0-9]+$ + text_min_max_length_info: 0 pomeni brez omejitev + text_project_destroy_confirmation: Ali ste prepriÄani da želite izbrisati izbrani projekt in vse z njim povezane podatke? + text_subprojects_destroy_warning: "Njegov(i) podprojekt(i): %{value} bodo prav tako izbrisani." + text_workflow_edit: Izberite vlogo in zahtevek za urejanje poteka dela + text_are_you_sure: Ali ste prepriÄani? + text_tip_issue_begin_day: naloga z zaÄetkom na ta dan + text_tip_issue_end_day: naloga z zakljuÄkom na ta dan + text_tip_issue_begin_end_day: naloga ki se zaÄne in konÄa ta dan + text_caracters_maximum: "najveÄ %{count} znakov." + text_caracters_minimum: "Mora biti vsaj dolg vsaj %{count} znakov." + text_length_between: "Dolžina med %{min} in %{max} znakov." + text_tracker_no_workflow: Potek dela za to vrsto zahtevka ni doloÄen + text_unallowed_characters: Nedovoljeni znaki + text_comma_separated: Dovoljenih je veÄ vrednosti (loÄenih z vejico). + text_issues_ref_in_commit_messages: Zahtevki sklicev in popravkov v sporoÄilu predaje + text_issue_added: "Zahtevek %{id} je sporoÄil(a) %{author}." + text_issue_updated: "Zahtevek %{id} je posodobil(a) %{author}." + text_wiki_destroy_confirmation: Ali ste prepriÄani da želite izbrisati ta wiki in vso njegovo vsebino? + text_issue_category_destroy_question: "Nekateri zahtevki (%{count}) so dodeljeni tej kategoriji. Kaj želite storiti?" + text_issue_category_destroy_assignments: Odstrani naloge v kategoriji + text_issue_category_reassign_to: Ponovno dodeli zahtevke tej kategoriji + text_user_mail_option: "Na neizbrane projekte boste prejemali le obvestila o zadevah ki jih spremljate ali v katere ste vkljuÄeni (npr. zahtevki katerih avtor(ica) ste)" + text_no_configuration_data: "Vloge, vrste zahtevkov, statusi zahtevkov in potek dela Å¡e niso bili doloÄeni. \nZelo priporoÄljivo je, da naložite privzeto konfiguracijo, ki jo lahko kasneje tudi prilagodite." + text_load_default_configuration: Naloži privzeto konfiguracijo + text_status_changed_by_changeset: "Dodano v zapis sprememb %{value}." + text_issues_destroy_confirmation: 'Ali ste prepriÄani, da želite izbrisati izbrani(e) zahtevek(ke)?' + text_select_project_modules: 'Izberite module, ki jih želite omogoÄiti za ta projekt:' + text_default_administrator_account_changed: Spremenjen privzeti administratorski raÄun + text_file_repository_writable: OmogoÄeno pisanje v shrambo datotek + text_rmagick_available: RMagick je na voljo(neobvezno) + text_destroy_time_entries_question: "%{hours} ur je bilo opravljenih na zahtevku, ki ga želite izbrisati. Kaj želite storiti?" + text_destroy_time_entries: IzbriÅ¡i opravljene ure + text_assign_time_entries_to_project: Predaj opravljene ure projektu + text_reassign_time_entries: 'Prenesi opravljene ure na ta zahtevek:' + text_user_wrote: "%{value} je napisal(a):" + text_enumeration_destroy_question: "%{count} objektov je doloÄenih tej vrednosti." + text_enumeration_category_reassign_to: 'Ponastavi jih na to vrednost:' + text_email_delivery_not_configured: "E-poÅ¡tna dostava ni nastavljena in oznanila so onemogoÄena.\nNastavite vaÅ¡ SMTP strežnik v config/configuration.yml in ponovno zaženite aplikacijo da ga omogoÄite.\n" + text_repository_usernames_mapping: "Izberite ali posodobite Redmine uporabnika dodeljenega vsakemu uporabniÅ¡kemu imenu najdenemu v zapisniku shrambe.\n Uporabniki z enakim Redmine ali shrambinem uporabniÅ¡kem imenu ali e-poÅ¡tnem naslovu so samodejno dodeljeni." + text_diff_truncated: '... Ta sprememba je bila odsekana ker presega najveÄjo velikost ki je lahko prikazana.' + + default_role_manager: Upravnik + default_role_developer: Razvijalec + default_role_reporter: PoroÄevalec + default_tracker_bug: HroÅ¡Ä + default_tracker_feature: Funkcija + default_tracker_support: Podpora + default_issue_status_new: Nov + default_issue_status_in_progress: V teku + default_issue_status_resolved: ReÅ¡en + default_issue_status_feedback: Povratna informacija + default_issue_status_closed: ZakljuÄen + default_issue_status_rejected: Zavrnjen + default_doc_category_user: UporabniÅ¡ka dokumentacija + default_doc_category_tech: TehniÄna dokumentacija + default_priority_low: Nizka + default_priority_normal: ObiÄajna + default_priority_high: Visoka + default_priority_urgent: Urgentna + default_priority_immediate: TakojÅ¡nje ukrepanje + default_activity_design: Oblikovanje + default_activity_development: Razvoj + + enumeration_issue_priorities: Prioritete zahtevkov + enumeration_doc_categories: Kategorije dokumentov + enumeration_activities: Aktivnosti (sledenje Äasa) + warning_attachments_not_saved: "%{count} datotek(e) ni bilo mogoÄe shraniti." + field_editable: Uredljivo + text_plugin_assets_writable: Zapisljiva mapa za vtiÄnike + label_display: Prikaz + button_create_and_continue: Ustvari in nadaljuj + text_custom_field_possible_values_info: 'Ena vrstica za vsako vrednost' + setting_repository_log_display_limit: NajveÄje Å¡tevilo prikazanih revizij v log datoteki + setting_file_max_size_displayed: NajveÄja velikost besedilnih datotek v vkljuÄenem prikazu + field_watcher: Opazovalec + setting_openid: Dovoli OpenID prijavo in registracijo + field_identity_url: OpenID URL + label_login_with_open_id_option: ali se prijavi z OpenID + field_content: Vsebina + label_descending: PadajoÄe + label_sort: Razvrsti + label_ascending: NaraÅ¡ÄajoÄe + label_date_from_to: Od %{start} do %{end} + label_greater_or_equal: ">=" + label_less_or_equal: <= + text_wiki_page_destroy_question: Ta stran ima %{descendants} podstran(i) in naslednik(ov). Kaj želite storiti? + text_wiki_page_reassign_children: Znova dodeli podstrani tej glavni strani + text_wiki_page_nullify_children: Obdrži podstrani kot glavne strani + text_wiki_page_destroy_children: IzbriÅ¡i podstrani in vse njihove naslednike + setting_password_min_length: Minimalna dolžina gesla + field_group_by: Združi rezultate po + mail_subject_wiki_content_updated: "'%{id}' wiki stran je bila posodobljena" + label_wiki_content_added: Wiki stran dodana + mail_subject_wiki_content_added: "'%{id}' wiki stran je bila dodana" + mail_body_wiki_content_added: "%{author} je dodal '%{id}' wiki stran" + label_wiki_content_updated: Wiki stran posodobljena + mail_body_wiki_content_updated: "%{author} je posodobil '%{id}' wiki stran." + permission_add_project: Ustvari projekt + setting_new_project_user_role_id: Vloga, dodeljena neadministratorskemu uporabniku, ki je ustvaril projekt + label_view_all_revisions: Poglej vse revizije + label_tag: Oznaka + label_branch: Veja + error_no_tracker_in_project: Noben sledilnik ni povezan s tem projektom. Prosimo preverite nastavitve projekta. + error_no_default_issue_status: Privzeti zahtevek ni definiran. Prosimo preverite svoje nastavitve (Pojdite na "Administracija -> Stanje zahtevkov"). + text_journal_changed: "%{label} se je spremenilo iz %{old} v %{new}" + text_journal_set_to: "%{label} nastavljeno na %{value}" + text_journal_deleted: "%{label} izbrisan (%{old})" + label_group_plural: Skupine + label_group: Skupina + label_group_new: Nova skupina + label_time_entry_plural: Porabljen Äas + text_journal_added: "%{label} %{value} dodan" + field_active: Aktiven + enumeration_system_activity: Sistemska aktivnost + permission_delete_issue_watchers: IzbriÅ¡i opazovalce + version_status_closed: zaprt + version_status_locked: zaklenjen + version_status_open: odprt + error_can_not_reopen_issue_on_closed_version: Zahtevek dodeljen zaprti verziji ne more biti ponovno odprt + label_user_anonymous: Anonimni + button_move_and_follow: Premakni in sledi + setting_default_projects_modules: Privzeti moduli za nove projekte + setting_gravatar_default: Privzeta Gravatar slika + field_sharing: Deljenje + label_version_sharing_hierarchy: S projektno hierarhijo + label_version_sharing_system: Z vsemi projekti + label_version_sharing_descendants: S podprojekti + label_version_sharing_tree: Z drevesom projekta + label_version_sharing_none: Ni deljeno + error_can_not_archive_project: Ta projekt ne more biti arhiviran + button_duplicate: Podvoji + button_copy_and_follow: Kopiraj in sledi + label_copy_source: Vir + setting_issue_done_ratio: IzraÄunaj razmerje opravljenega zahtevka z + setting_issue_done_ratio_issue_status: Uporabi stanje zahtevka + error_issue_done_ratios_not_updated: Razmerje opravljenega zahtevka ni bilo posodobljeno. + error_workflow_copy_target: Prosimo izberite ciljni(e) sledilnik(e) in vlogo(e) + setting_issue_done_ratio_issue_field: Uporabi polje zahtevka + label_copy_same_as_target: Enako kot cilj + label_copy_target: Cilj + notice_issue_done_ratios_updated: Razmerje opravljenega zahtevka posodobljeno. + error_workflow_copy_source: Prosimo izberite vir zahtevka ali vlogo + label_update_issue_done_ratios: Posodobi razmerje opravljenega zahtevka + setting_start_of_week: ZaÄni koledarje z + permission_view_issues: Poglej zahtevke + label_display_used_statuses_only: Prikaži samo stanja ki uporabljajo ta sledilnik + label_revision_id: Revizija %{value} + label_api_access_key: API dostopni kljuÄ + label_api_access_key_created_on: API dostopni kljuÄ ustvarjen pred %{value} + label_feeds_access_key: RSS dostopni kljuÄ + notice_api_access_key_reseted: VaÅ¡ API dostopni kljuÄ je bil ponastavljen. + setting_rest_api_enabled: OmogoÄi REST spletni servis + label_missing_api_access_key: ManjkajoÄ API dostopni kljuÄ + label_missing_feeds_access_key: ManjkajoÄ RSS dostopni kljuÄ + button_show: Prikaži + text_line_separated: Dovoljenih veÄ vrednosti (ena vrstica za vsako vrednost). + setting_mail_handler_body_delimiters: Odreži e-poÅ¡to po eni od teh vrstic + permission_add_subprojects: Ustvari podprojekte + label_subproject_new: Nov podprojekt + text_own_membership_delete_confirmation: |- + Odstranili boste nekatere ali vse od dovoljenj zaradi Äesar morda ne boste mogli veÄ urejati tega projekta. + Ali ste prepriÄani, da želite nadaljevati? + label_close_versions: Zapri dokonÄane verzije + label_board_sticky: Lepljivo + label_board_locked: Zaklenjeno + permission_export_wiki_pages: Izvozi wiki strani + setting_cache_formatted_text: Predpomni oblikovano besedilo + permission_manage_project_activities: Uredi aktivnosti projekta + error_unable_delete_issue_status: Stanja zahtevka ni bilo možno spremeniti + label_profile: Profil + permission_manage_subtasks: Uredi podnaloge + field_parent_issue: Nadrejena naloga + label_subtask_plural: Podnaloge + label_project_copy_notifications: Med kopiranjem projekta poÅ¡lji e-poÅ¡tno sporoÄilo + error_can_not_delete_custom_field: Polja po meri ni mogoÄe izbrisati + error_unable_to_connect: Povezava ni mogoÄa (%{value}) + error_can_not_remove_role: Ta vloga je v uporabi in je ni mogoÄe izbrisati. + error_can_not_delete_tracker: Ta sledilnik vsebuje zahtevke in se ga ne more izbrisati. + field_principal: Upravnik varnosti + label_my_page_block: Moj gradnik strani + notice_failed_to_save_members: "Shranjevanje uporabnika(ov) ni uspelo: %{errors}." + text_zoom_out: Približaj + text_zoom_in: Oddalji + notice_unable_delete_time_entry: Brisanje dnevnika porabljenaga Äasa ni mogoÄe. + label_overall_spent_time: Skupni porabljeni Äas + field_time_entries: Beleži porabljeni Äas + project_module_gantt: Gantogram + project_module_calendar: Koledear + button_edit_associated_wikipage: "Uredi povezano Wiki stran: %{page_title}" + field_text: Besedilno polje + label_user_mail_option_only_owner: Samo za stvari katerih lastnik sem + setting_default_notification_option: Privzeta možnost obveÅ¡Äanja + label_user_mail_option_only_my_events: Samo za stvari, ki jih opazujem ali sem v njih vpleten + label_user_mail_option_only_assigned: Samo za stvari, ki smo mi dodeljene + label_user_mail_option_none: Noben dogodek + field_member_of_group: PooblaÅ¡ÄenÄeva skupina + field_assigned_to_role: PooblaÅ¡ÄenÄeva vloga + notice_not_authorized_archived_project: Projekt, do katerega poskuÅ¡ate dostopati, je bil arhiviran. + label_principal_search: "PoiÅ¡Äi uporabnika ali skupino:" + label_user_search: "PoiÅ¡Äi uporabnikia:" + field_visible: Viden + setting_emails_header: Glava e-poÅ¡te + setting_commit_logtime_activity_id: Aktivnost zabeleženega Äasa + text_time_logged_by_changeset: Uporabljeno v spremembi %{value}. + setting_commit_logtime_enabled: OmogoÄi beleženje Äasa + notice_gantt_chart_truncated: Graf je bil odrezan, ker je prekoraÄil najveÄje dovoljeno Å¡tevilo elementov, ki se jih lahko prikaže (%{max}) + setting_gantt_items_limit: NajveÄje Å¡tevilo elementov prikazano na gantogramu + field_warn_on_leaving_unsaved: Opozori me, kadar zapuÅ¡Äam stran z neshranjenim besedilom + text_warn_on_leaving_unsaved: Trenutna stran vsebuje neshranjeno besedilo ki bo izgubljeno, Äe zapustite to stran. + label_my_queries: Moje poizvedbe po meri + text_journal_changed_no_detail: "%{label} posodobljen" + label_news_comment_added: Komentar dodan novici + button_expand_all: RazÅ¡iri vse + button_collapse_all: SkrÄi vse + label_additional_workflow_transitions_for_assignee: Dovoljeni dodatni prehodi kadar je uporabnik pooblaÅ¡Äenec + label_additional_workflow_transitions_for_author: Dovoljeni dodatni prehodi kadar je uporabnik avtor + label_bulk_edit_selected_time_entries: Skupinsko urejanje izbranih Äasovnih zapisov + text_time_entries_destroy_confirmation: Ali ste prepriÄani, da želite izbristai izbran(e) Äasovn(i/e) zapis(e)? + label_role_anonymous: Anonimni + label_role_non_member: NeÄlan + label_issue_note_added: Dodan zaznamek + label_issue_status_updated: Status posodobljen + label_issue_priority_updated: Prioriteta posodobljena + label_issues_visibility_own: Zahtevek ustvarjen s strani uporabnika ali dodeljen uporabniku + field_issues_visibility: Vidljivost zahtevkov + label_issues_visibility_all: Vsi zahtevki + permission_set_own_issues_private: Nastavi lastne zahtevke kot javne ali zasebne + field_is_private: Zaseben + permission_set_issues_private: Nastavi zahtevke kot javne ali zasebne + label_issues_visibility_public: Vsi nezasebni zahtevki + text_issues_destroy_descendants_confirmation: To bo izbrisalo tudi %{count} podnalog(o). + field_commit_logs_encoding: Kodiranje sporoÄil ob predaji + field_scm_path_encoding: Pot do kodiranja + text_scm_path_encoding_note: "Privzeto: UTF-8" + field_path_to_repository: Pot do shrambe + field_root_directory: Korenska mapa + field_cvs_module: Modul + field_cvsroot: CVSROOT + text_mercurial_repository_note: Lokalna shramba (npr. /hgrepo, c:\hgrepo) + text_scm_command: Ukaz + text_scm_command_version: Verzija + label_git_report_last_commit: SporoÄi zadnje uveljavljanje datotek in map + text_scm_config: Svoje SCM ukaze lahko nastavite v datoteki config/configuration.yml. Po urejanju prosimo ponovno zaženite aplikacijo. + text_scm_command_not_available: SCM ukaz ni na voljo. Prosimo preverite nastavitve v upravljalskem podoknu. + + text_git_repository_note: Shramba je prazna in lokalna (npr. /gitrepo, c:\gitrepo) + + notice_issue_successful_create: Ustvarjen zahtevek %{id}. + label_between: med + setting_issue_group_assignment: Dovoli dodeljevanje zahtevka skupinam + label_diff: diff + + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 zahtevek + one: 1 zahtevek + other: "%{count} zahtevki" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: vsi + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: S podprojekti + label_cross_project_tree: Z drevesom projekta + label_cross_project_hierarchy: S projektno hierarhijo + label_cross_project_system: Z vsemi projekti + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2d/2d5c5458ec41dd3315a619640122843b9e0e9952.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2d/2d5c5458ec41dd3315a619640122843b9e0e9952.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,51 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'action_view/helpers/form_helper' + +class Redmine::Views::LabelledFormBuilder < ActionView::Helpers::FormBuilder + include Redmine::I18n + + (field_helpers.map(&:to_s) - %w(radio_button hidden_field fields_for) + + %w(date_select)).each do |selector| + src = <<-END_SRC + def #{selector}(field, options = {}) + label_for_field(field, options) + super(field, options.except(:label)).html_safe + end + END_SRC + class_eval src, __FILE__, __LINE__ + end + + def select(field, choices, options = {}, html_options = {}) + label_for_field(field, options) + super(field, choices, options, html_options.except(:label)).html_safe + end + + def time_zone_select(field, priority_zones = nil, options = {}, html_options = {}) + label_for_field(field, options) + super(field, priority_zones, options, html_options.except(:label)).html_safe + end + + # Returns a label tag for the given field + def label_for_field(field, options = {}) + return ''.html_safe if options.delete(:no_label) + text = options[:label].is_a?(Symbol) ? l(options[:label]) : options[:label] + text ||= l(("field_" + field.to_s.gsub(/\_id$/, "")).to_sym) + text += @template.content_tag("span", " *", :class => "required") if options.delete(:required) + @template.content_tag("label", text.html_safe, + :class => (@object && @object.errors[field].present? ? "error" : nil), + :for => (@object_name.to_s + "_" + field.to_s)) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2d/2d6ad8e6f87f6d379c227b9a81e69b5a26399856.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2d/2d6ad8e6f87f6d379c227b9a81e69b5a26399856.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,254 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) +require 'account_controller' + +# Re-raise errors caught by the controller. +class AccountController; def rescue_action(e) raise e end; end + +class AccountControllerTest < ActionController::TestCase + fixtures :users, :roles + + def setup + @controller = AccountController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_get_login + get :login + assert_response :success + assert_template 'login' + + assert_select 'input[name=username]' + assert_select 'input[name=password]' + end + + def test_login_should_redirect_to_back_url_param + # request.uri is "test.host" in test environment + post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http://test.host/issues/show/1' + assert_redirected_to '/issues/show/1' + end + + def test_login_should_not_redirect_to_another_host + post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http://test.foo/fake' + assert_redirected_to '/my/page' + end + + def test_login_with_wrong_password + post :login, :username => 'admin', :password => 'bad' + assert_response :success + assert_template 'login' + + assert_select 'div.flash.error', :text => /Invalid user or password/ + assert_select 'input[name=username][value=admin]' + assert_select 'input[name=password]' + assert_select 'input[name=password][value]', 0 + end + + def test_login_should_rescue_auth_source_exception + source = AuthSource.create!(:name => 'Test') + User.find(2).update_attribute :auth_source_id, source.id + AuthSource.any_instance.stubs(:authenticate).raises(AuthSourceException.new("Something wrong")) + + post :login, :username => 'jsmith', :password => 'jsmith' + assert_response 500 + assert_error_tag :content => /Something wrong/ + end + + def test_login_should_reset_session + @controller.expects(:reset_session).once + + post :login, :username => 'jsmith', :password => 'jsmith' + assert_response 302 + end + + def test_logout + @request.session[:user_id] = 2 + get :logout + assert_redirected_to '/' + assert_nil @request.session[:user_id] + end + + def test_logout_should_reset_session + @controller.expects(:reset_session).once + + @request.session[:user_id] = 2 + get :logout + assert_response 302 + end + + def test_get_register_with_registration_on + with_settings :self_registration => '3' do + get :register + assert_response :success + assert_template 'register' + assert_not_nil assigns(:user) + + assert_tag 'input', :attributes => {:name => 'user[password]'} + assert_tag 'input', :attributes => {:name => 'user[password_confirmation]'} + end + end + + def test_get_register_with_registration_off_should_redirect + with_settings :self_registration => '0' do + get :register + assert_redirected_to '/' + end + end + + # See integration/account_test.rb for the full test + def test_post_register_with_registration_on + with_settings :self_registration => '3' do + assert_difference 'User.count' do + post :register, :user => { + :login => 'register', + :password => 'secret123', + :password_confirmation => 'secret123', + :firstname => 'John', + :lastname => 'Doe', + :mail => 'register@example.com' + } + assert_redirected_to '/my/account' + end + user = User.first(:order => 'id DESC') + assert_equal 'register', user.login + assert_equal 'John', user.firstname + assert_equal 'Doe', user.lastname + assert_equal 'register@example.com', user.mail + assert user.check_password?('secret123') + assert user.active? + end + end + + def test_post_register_with_registration_off_should_redirect + with_settings :self_registration => '0' do + assert_no_difference 'User.count' do + post :register, :user => { + :login => 'register', + :password => 'test', + :password_confirmation => 'test', + :firstname => 'John', + :lastname => 'Doe', + :mail => 'register@example.com' + } + assert_redirected_to '/' + end + end + end + + def test_get_lost_password_should_display_lost_password_form + get :lost_password + assert_response :success + assert_select 'input[name=mail]' + end + + def test_lost_password_for_active_user_should_create_a_token + Token.delete_all + ActionMailer::Base.deliveries.clear + assert_difference 'ActionMailer::Base.deliveries.size' do + assert_difference 'Token.count' do + with_settings :host_name => 'mydomain.foo', :protocol => 'http' do + post :lost_password, :mail => 'JSmith@somenet.foo' + assert_redirected_to '/login' + end + end + end + + token = Token.order('id DESC').first + assert_equal User.find(2), token.user + assert_equal 'recovery', token.action + + assert_select_email do + assert_select "a[href=?]", "http://mydomain.foo/account/lost_password?token=#{token.value}" + end + end + + def test_lost_password_for_unknown_user_should_fail + Token.delete_all + assert_no_difference 'Token.count' do + post :lost_password, :mail => 'invalid@somenet.foo' + assert_response :success + end + end + + def test_lost_password_for_non_active_user_should_fail + Token.delete_all + assert User.find(2).lock! + + assert_no_difference 'Token.count' do + post :lost_password, :mail => 'JSmith@somenet.foo' + assert_response :success + end + end + + def test_get_lost_password_with_token_should_display_the_password_recovery_form + user = User.find(2) + token = Token.create!(:action => 'recovery', :user => user) + + get :lost_password, :token => token.value + assert_response :success + assert_template 'password_recovery' + + assert_select 'input[type=hidden][name=token][value=?]', token.value + end + + def test_get_lost_password_with_invalid_token_should_redirect + get :lost_password, :token => "abcdef" + assert_redirected_to '/' + end + + def test_post_lost_password_with_token_should_change_the_user_password + user = User.find(2) + token = Token.create!(:action => 'recovery', :user => user) + + post :lost_password, :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123' + assert_redirected_to '/login' + user.reload + assert user.check_password?('newpass123') + assert_nil Token.find_by_id(token.id), "Token was not deleted" + end + + def test_post_lost_password_with_token_for_non_active_user_should_fail + user = User.find(2) + token = Token.create!(:action => 'recovery', :user => user) + user.lock! + + post :lost_password, :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123' + assert_redirected_to '/' + assert ! user.check_password?('newpass123') + end + + def test_post_lost_password_with_token_and_password_confirmation_failure_should_redisplay_the_form + user = User.find(2) + token = Token.create!(:action => 'recovery', :user => user) + + post :lost_password, :token => token.value, :new_password => 'newpass', :new_password_confirmation => 'wrongpass' + assert_response :success + assert_template 'password_recovery' + assert_not_nil Token.find_by_id(token.id), "Token was deleted" + + assert_select 'input[type=hidden][name=token][value=?]', token.value + end + + def test_post_lost_password_with_invalid_token_should_redirect + post :lost_password, :token => "abcdef", :new_password => 'newpass', :new_password_confirmation => 'newpass' + assert_redirected_to '/' + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2d/2d704054566a75db6513ae575339e7bf14942054.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2d/2d704054566a75db6513ae575339e7bf14942054.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,60 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../test_helper', __FILE__) + +class Redmine::WikiFormattingTest < ActiveSupport::TestCase + fixtures :issues + + def test_textile_formatter + assert_equal Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting.formatter_for('textile') + assert_equal Redmine::WikiFormatting::Textile::Helper, Redmine::WikiFormatting.helper_for('textile') + end + + def test_null_formatter + assert_equal Redmine::WikiFormatting::NullFormatter::Formatter, Redmine::WikiFormatting.formatter_for('') + assert_equal Redmine::WikiFormatting::NullFormatter::Helper, Redmine::WikiFormatting.helper_for('') + end + + def test_should_link_urls_and_email_addresses + raw = <<-DIFF +This is a sample *text* with a link: http://www.redmine.org +and an email address foo@example.net +DIFF + + expected = <<-EXPECTED +

    This is a sample *text* with a link: http://www.redmine.org
    +and an email address

    +EXPECTED + + assert_equal expected.gsub(%r{[\r\n\t]}, ''), Redmine::WikiFormatting::NullFormatter::Formatter.new(raw).to_html.gsub(%r{[\r\n\t]}, '') + end + + def test_supports_section_edit + with_settings :text_formatting => 'textile' do + assert_equal true, Redmine::WikiFormatting.supports_section_edit? + end + + with_settings :text_formatting => '' do + assert_equal false, Redmine::WikiFormatting.supports_section_edit? + end + end + + def test_cache_key_for_saved_object_should_no_be_nil + assert_not_nil Redmine::WikiFormatting.cache_key_for('textile', 'Text', Issue.find(1), :description) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2d/2d79f51bb3aabe1350946a7c8d7303968d415bb8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2d/2d79f51bb3aabe1350946a7c8d7303968d415bb8.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,229 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class IssuesTest < ActionController::IntegrationTest + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :trackers, + :projects_trackers, + :enabled_modules, + :issue_statuses, + :issues, + :enumerations, + :custom_fields, + :custom_values, + :custom_fields_trackers + + # create an issue + def test_add_issue + log_user('jsmith', 'jsmith') + get 'projects/1/issues/new', :tracker_id => '1' + assert_response :success + assert_template 'issues/new' + + post 'projects/1/issues', :tracker_id => "1", + :issue => { :start_date => "2006-12-26", + :priority_id => "4", + :subject => "new test issue", + :category_id => "", + :description => "new issue", + :done_ratio => "0", + :due_date => "", + :assigned_to_id => "" }, + :custom_fields => {'2' => 'Value for field 2'} + # find created issue + issue = Issue.find_by_subject("new test issue") + assert_kind_of Issue, issue + + # check redirection + assert_redirected_to :controller => 'issues', :action => 'show', :id => issue + follow_redirect! + assert_equal issue, assigns(:issue) + + # check issue attributes + assert_equal 'jsmith', issue.author.login + assert_equal 1, issue.project.id + assert_equal 1, issue.status.id + end + + def test_update_issue_form + log_user('jsmith', 'jsmith') + post 'projects/ecookbook/issues/new', :issue => { :tracker_id => "2"} + assert_response :success + assert_tag 'select', + :attributes => {:name => 'issue[tracker_id]'}, + :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}} + end + + # add then remove 2 attachments to an issue + def test_issue_attachments + log_user('jsmith', 'jsmith') + set_tmp_attachments_directory + + put 'issues/1', + :notes => 'Some notes', + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'This is an attachment'}} + assert_redirected_to "/issues/1" + + # make sure attachment was saved + attachment = Issue.find(1).attachments.find_by_filename("testfile.txt") + assert_kind_of Attachment, attachment + assert_equal Issue.find(1), attachment.container + assert_equal 'This is an attachment', attachment.description + # verify the size of the attachment stored in db + #assert_equal file_data_1.length, attachment.filesize + # verify that the attachment was written to disk + assert File.exist?(attachment.diskfile) + + # remove the attachments + Issue.find(1).attachments.each(&:destroy) + assert_equal 0, Issue.find(1).attachments.length + end + + def test_other_formats_links_on_index + get '/projects/ecookbook/issues' + + %w(Atom PDF CSV).each do |format| + assert_tag :a, :content => format, + :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}", + :rel => 'nofollow' } + end + end + + def test_other_formats_links_on_index_without_project_id_in_url + get '/issues', :project_id => 'ecookbook' + + %w(Atom PDF CSV).each do |format| + assert_tag :a, :content => format, + :attributes => { :href => "/projects/ecookbook/issues.#{format.downcase}", + :rel => 'nofollow' } + end + end + + def test_pagination_links_on_index + Setting.per_page_options = '2' + get '/projects/ecookbook/issues' + + assert_tag :a, :content => '2', + :attributes => { :href => '/projects/ecookbook/issues?page=2' } + + end + + def test_pagination_links_on_index_without_project_id_in_url + Setting.per_page_options = '2' + get '/issues', :project_id => 'ecookbook' + + assert_tag :a, :content => '2', + :attributes => { :href => '/projects/ecookbook/issues?page=2' } + + end + + def test_issue_with_user_custom_field + @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true, :trackers => Tracker.all) + Role.anonymous.add_permission! :add_issues, :edit_issues + users = Project.find(1).users + tester = users.first + + # Issue form + get '/projects/ecookbook/issues/new' + assert_response :success + assert_tag :select, + :attributes => {:name => "issue[custom_field_values][#{@field.id}]"}, + :children => {:count => (users.size + 1)}, # +1 for blank value + :child => { + :tag => 'option', + :attributes => {:value => tester.id.to_s}, + :content => tester.name + } + + # Create issue + assert_difference 'Issue.count' do + post '/projects/ecookbook/issues', + :issue => { + :tracker_id => '1', + :priority_id => '4', + :subject => 'Issue with user custom field', + :custom_field_values => {@field.id.to_s => users.first.id.to_s} + } + end + issue = Issue.first(:order => 'id DESC') + assert_response 302 + + # Issue view + follow_redirect! + assert_tag :th, + :content => /Tester/, + :sibling => { + :tag => 'td', + :content => tester.name + } + assert_tag :select, + :attributes => {:name => "issue[custom_field_values][#{@field.id}]"}, + :children => {:count => (users.size + 1)}, # +1 for blank value + :child => { + :tag => 'option', + :attributes => {:value => tester.id.to_s, :selected => 'selected'}, + :content => tester.name + } + + # Update issue + new_tester = users[1] + assert_difference 'Journal.count' do + put "/issues/#{issue.id}", + :notes => 'Updating custom field', + :issue => { + :custom_field_values => {@field.id.to_s => new_tester.id.to_s} + } + end + assert_response 302 + + # Issue view + follow_redirect! + assert_tag :content => 'Tester', + :ancestor => {:tag => 'ul', :attributes => {:class => /details/}}, + :sibling => { + :content => tester.name, + :sibling => { + :content => new_tester.name + } + } + end + + def test_update_using_invalid_http_verbs + subject = 'Updated by an invalid http verb' + + get '/issues/update/1', {:issue => {:subject => subject}}, credentials('jsmith') + assert_response 404 + assert_not_equal subject, Issue.find(1).subject + + post '/issues/1', {:issue => {:subject => subject}}, credentials('jsmith') + assert_response 404 + assert_not_equal subject, Issue.find(1).subject + end + + def test_get_watch_should_be_invalid + assert_no_difference 'Watcher.count' do + get '/watchers/watch?object_type=issue&object_id=1', {}, credentials('jsmith') + assert_response 404 + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2d/2d9594f5015d38ea372a90ebea36c8f5d88663f3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2d/2d9594f5015d38ea372a90ebea36c8f5d88663f3.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,11 @@ +Content-Type: application/ms-tnef; name="winmail.dat" +Content-Transfer-Encoding: binary +From: John Smith +To: "redmine@somenet.foo" +Subject: =?iso-8859-1?Q?Testmail_from_Webmail:_=E4_=F6_=FC...?= +Date: Fri, 1 Jun 2012 14:39:38 +0200 +Message-ID: <87C31D42249DD0489D1A1444E3232DD7019D6183@foo.bar> +Accept-Language: de-CH, en-US +Content-Language: de-CH + +Fixture diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2d/2da963c2125a0fb3eab8836a66637a6767b326a5.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2d/2da963c2125a0fb3eab8836a66637a6767b326a5.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1884 @@ +--- +WorkflowTransitions_189: + new_status_id: 5 + role_id: 1 + old_status_id: 2 + id: 189 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_001: + new_status_id: 2 + role_id: 1 + old_status_id: 1 + id: 1 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_002: + new_status_id: 3 + role_id: 1 + old_status_id: 1 + id: 2 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_003: + new_status_id: 4 + role_id: 1 + old_status_id: 1 + id: 3 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_110: + new_status_id: 6 + role_id: 1 + old_status_id: 4 + id: 110 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_004: + new_status_id: 5 + role_id: 1 + old_status_id: 1 + id: 4 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_030: + new_status_id: 5 + role_id: 1 + old_status_id: 6 + id: 30 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_111: + new_status_id: 1 + role_id: 1 + old_status_id: 5 + id: 111 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_005: + new_status_id: 6 + role_id: 1 + old_status_id: 1 + id: 5 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_031: + new_status_id: 2 + role_id: 2 + old_status_id: 1 + id: 31 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_112: + new_status_id: 2 + role_id: 1 + old_status_id: 5 + id: 112 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_006: + new_status_id: 1 + role_id: 1 + old_status_id: 2 + id: 6 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_032: + new_status_id: 3 + role_id: 2 + old_status_id: 1 + id: 32 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_113: + new_status_id: 3 + role_id: 1 + old_status_id: 5 + id: 113 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_220: + new_status_id: 6 + role_id: 2 + old_status_id: 2 + id: 220 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_007: + new_status_id: 3 + role_id: 1 + old_status_id: 2 + id: 7 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_033: + new_status_id: 4 + role_id: 2 + old_status_id: 1 + id: 33 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_060: + new_status_id: 5 + role_id: 2 + old_status_id: 6 + id: 60 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_114: + new_status_id: 4 + role_id: 1 + old_status_id: 5 + id: 114 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_140: + new_status_id: 6 + role_id: 2 + old_status_id: 4 + id: 140 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_221: + new_status_id: 1 + role_id: 2 + old_status_id: 3 + id: 221 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_008: + new_status_id: 4 + role_id: 1 + old_status_id: 2 + id: 8 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_034: + new_status_id: 5 + role_id: 2 + old_status_id: 1 + id: 34 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_115: + new_status_id: 6 + role_id: 1 + old_status_id: 5 + id: 115 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_141: + new_status_id: 1 + role_id: 2 + old_status_id: 5 + id: 141 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_222: + new_status_id: 2 + role_id: 2 + old_status_id: 3 + id: 222 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_223: + new_status_id: 4 + role_id: 2 + old_status_id: 3 + id: 223 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_009: + new_status_id: 5 + role_id: 1 + old_status_id: 2 + id: 9 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_035: + new_status_id: 6 + role_id: 2 + old_status_id: 1 + id: 35 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_061: + new_status_id: 2 + role_id: 3 + old_status_id: 1 + id: 61 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_116: + new_status_id: 1 + role_id: 1 + old_status_id: 6 + id: 116 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_142: + new_status_id: 2 + role_id: 2 + old_status_id: 5 + id: 142 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_250: + new_status_id: 6 + role_id: 3 + old_status_id: 2 + id: 250 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_224: + new_status_id: 5 + role_id: 2 + old_status_id: 3 + id: 224 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_036: + new_status_id: 1 + role_id: 2 + old_status_id: 2 + id: 36 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_062: + new_status_id: 3 + role_id: 3 + old_status_id: 1 + id: 62 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_117: + new_status_id: 2 + role_id: 1 + old_status_id: 6 + id: 117 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_143: + new_status_id: 3 + role_id: 2 + old_status_id: 5 + id: 143 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_170: + new_status_id: 6 + role_id: 3 + old_status_id: 4 + id: 170 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_251: + new_status_id: 1 + role_id: 3 + old_status_id: 3 + id: 251 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_225: + new_status_id: 6 + role_id: 2 + old_status_id: 3 + id: 225 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_063: + new_status_id: 4 + role_id: 3 + old_status_id: 1 + id: 63 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_090: + new_status_id: 5 + role_id: 3 + old_status_id: 6 + id: 90 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_118: + new_status_id: 3 + role_id: 1 + old_status_id: 6 + id: 118 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_144: + new_status_id: 4 + role_id: 2 + old_status_id: 5 + id: 144 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_252: + new_status_id: 2 + role_id: 3 + old_status_id: 3 + id: 252 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_226: + new_status_id: 1 + role_id: 2 + old_status_id: 4 + id: 226 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_038: + new_status_id: 4 + role_id: 2 + old_status_id: 2 + id: 38 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_064: + new_status_id: 5 + role_id: 3 + old_status_id: 1 + id: 64 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_091: + new_status_id: 2 + role_id: 1 + old_status_id: 1 + id: 91 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_119: + new_status_id: 4 + role_id: 1 + old_status_id: 6 + id: 119 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_145: + new_status_id: 6 + role_id: 2 + old_status_id: 5 + id: 145 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_171: + new_status_id: 1 + role_id: 3 + old_status_id: 5 + id: 171 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_253: + new_status_id: 4 + role_id: 3 + old_status_id: 3 + id: 253 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_227: + new_status_id: 2 + role_id: 2 + old_status_id: 4 + id: 227 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_039: + new_status_id: 5 + role_id: 2 + old_status_id: 2 + id: 39 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_065: + new_status_id: 6 + role_id: 3 + old_status_id: 1 + id: 65 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_092: + new_status_id: 3 + role_id: 1 + old_status_id: 1 + id: 92 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_146: + new_status_id: 1 + role_id: 2 + old_status_id: 6 + id: 146 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_172: + new_status_id: 2 + role_id: 3 + old_status_id: 5 + id: 172 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_254: + new_status_id: 5 + role_id: 3 + old_status_id: 3 + id: 254 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_228: + new_status_id: 3 + role_id: 2 + old_status_id: 4 + id: 228 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_066: + new_status_id: 1 + role_id: 3 + old_status_id: 2 + id: 66 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_093: + new_status_id: 4 + role_id: 1 + old_status_id: 1 + id: 93 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_147: + new_status_id: 2 + role_id: 2 + old_status_id: 6 + id: 147 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_173: + new_status_id: 3 + role_id: 3 + old_status_id: 5 + id: 173 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_255: + new_status_id: 6 + role_id: 3 + old_status_id: 3 + id: 255 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_229: + new_status_id: 5 + role_id: 2 + old_status_id: 4 + id: 229 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_067: + new_status_id: 3 + role_id: 3 + old_status_id: 2 + id: 67 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_148: + new_status_id: 3 + role_id: 2 + old_status_id: 6 + id: 148 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_174: + new_status_id: 4 + role_id: 3 + old_status_id: 5 + id: 174 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_256: + new_status_id: 1 + role_id: 3 + old_status_id: 4 + id: 256 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_068: + new_status_id: 4 + role_id: 3 + old_status_id: 2 + id: 68 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_094: + new_status_id: 5 + role_id: 1 + old_status_id: 1 + id: 94 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_149: + new_status_id: 4 + role_id: 2 + old_status_id: 6 + id: 149 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_175: + new_status_id: 6 + role_id: 3 + old_status_id: 5 + id: 175 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_257: + new_status_id: 2 + role_id: 3 + old_status_id: 4 + id: 257 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_069: + new_status_id: 5 + role_id: 3 + old_status_id: 2 + id: 69 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_095: + new_status_id: 6 + role_id: 1 + old_status_id: 1 + id: 95 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_176: + new_status_id: 1 + role_id: 3 + old_status_id: 6 + id: 176 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_258: + new_status_id: 3 + role_id: 3 + old_status_id: 4 + id: 258 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_096: + new_status_id: 1 + role_id: 1 + old_status_id: 2 + id: 96 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_177: + new_status_id: 2 + role_id: 3 + old_status_id: 6 + id: 177 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_259: + new_status_id: 5 + role_id: 3 + old_status_id: 4 + id: 259 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_097: + new_status_id: 3 + role_id: 1 + old_status_id: 2 + id: 97 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_178: + new_status_id: 3 + role_id: 3 + old_status_id: 6 + id: 178 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_098: + new_status_id: 4 + role_id: 1 + old_status_id: 2 + id: 98 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_179: + new_status_id: 4 + role_id: 3 + old_status_id: 6 + id: 179 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_099: + new_status_id: 5 + role_id: 1 + old_status_id: 2 + id: 99 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_100: + new_status_id: 6 + role_id: 1 + old_status_id: 2 + id: 100 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_020: + new_status_id: 6 + role_id: 1 + old_status_id: 4 + id: 20 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_101: + new_status_id: 1 + role_id: 1 + old_status_id: 3 + id: 101 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_021: + new_status_id: 1 + role_id: 1 + old_status_id: 5 + id: 21 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_102: + new_status_id: 2 + role_id: 1 + old_status_id: 3 + id: 102 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_210: + new_status_id: 5 + role_id: 1 + old_status_id: 6 + id: 210 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_022: + new_status_id: 2 + role_id: 1 + old_status_id: 5 + id: 22 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_103: + new_status_id: 4 + role_id: 1 + old_status_id: 3 + id: 103 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_023: + new_status_id: 3 + role_id: 1 + old_status_id: 5 + id: 23 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_104: + new_status_id: 5 + role_id: 1 + old_status_id: 3 + id: 104 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_130: + new_status_id: 6 + role_id: 2 + old_status_id: 2 + id: 130 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_211: + new_status_id: 2 + role_id: 2 + old_status_id: 1 + id: 211 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_024: + new_status_id: 4 + role_id: 1 + old_status_id: 5 + id: 24 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_050: + new_status_id: 6 + role_id: 2 + old_status_id: 4 + id: 50 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_105: + new_status_id: 6 + role_id: 1 + old_status_id: 3 + id: 105 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_131: + new_status_id: 1 + role_id: 2 + old_status_id: 3 + id: 131 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_212: + new_status_id: 3 + role_id: 2 + old_status_id: 1 + id: 212 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_025: + new_status_id: 6 + role_id: 1 + old_status_id: 5 + id: 25 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_051: + new_status_id: 1 + role_id: 2 + old_status_id: 5 + id: 51 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_106: + new_status_id: 1 + role_id: 1 + old_status_id: 4 + id: 106 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_132: + new_status_id: 2 + role_id: 2 + old_status_id: 3 + id: 132 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_213: + new_status_id: 4 + role_id: 2 + old_status_id: 1 + id: 213 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_240: + new_status_id: 5 + role_id: 2 + old_status_id: 6 + id: 240 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_026: + new_status_id: 1 + role_id: 1 + old_status_id: 6 + id: 26 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_052: + new_status_id: 2 + role_id: 2 + old_status_id: 5 + id: 52 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_107: + new_status_id: 2 + role_id: 1 + old_status_id: 4 + id: 107 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_133: + new_status_id: 4 + role_id: 2 + old_status_id: 3 + id: 133 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_214: + new_status_id: 5 + role_id: 2 + old_status_id: 1 + id: 214 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_241: + new_status_id: 2 + role_id: 3 + old_status_id: 1 + id: 241 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_027: + new_status_id: 2 + role_id: 1 + old_status_id: 6 + id: 27 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_053: + new_status_id: 3 + role_id: 2 + old_status_id: 5 + id: 53 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_080: + new_status_id: 6 + role_id: 3 + old_status_id: 4 + id: 80 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_108: + new_status_id: 3 + role_id: 1 + old_status_id: 4 + id: 108 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_134: + new_status_id: 5 + role_id: 2 + old_status_id: 3 + id: 134 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_160: + new_status_id: 6 + role_id: 3 + old_status_id: 2 + id: 160 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_215: + new_status_id: 6 + role_id: 2 + old_status_id: 1 + id: 215 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_242: + new_status_id: 3 + role_id: 3 + old_status_id: 1 + id: 242 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_028: + new_status_id: 3 + role_id: 1 + old_status_id: 6 + id: 28 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_054: + new_status_id: 4 + role_id: 2 + old_status_id: 5 + id: 54 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_081: + new_status_id: 1 + role_id: 3 + old_status_id: 5 + id: 81 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_109: + new_status_id: 5 + role_id: 1 + old_status_id: 4 + id: 109 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_135: + new_status_id: 6 + role_id: 2 + old_status_id: 3 + id: 135 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_161: + new_status_id: 1 + role_id: 3 + old_status_id: 3 + id: 161 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_216: + new_status_id: 1 + role_id: 2 + old_status_id: 2 + id: 216 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_243: + new_status_id: 4 + role_id: 3 + old_status_id: 1 + id: 243 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_029: + new_status_id: 4 + role_id: 1 + old_status_id: 6 + id: 29 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_055: + new_status_id: 6 + role_id: 2 + old_status_id: 5 + id: 55 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_082: + new_status_id: 2 + role_id: 3 + old_status_id: 5 + id: 82 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_136: + new_status_id: 1 + role_id: 2 + old_status_id: 4 + id: 136 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_162: + new_status_id: 2 + role_id: 3 + old_status_id: 3 + id: 162 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_217: + new_status_id: 3 + role_id: 2 + old_status_id: 2 + id: 217 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_270: + new_status_id: 5 + role_id: 3 + old_status_id: 6 + id: 270 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_244: + new_status_id: 5 + role_id: 3 + old_status_id: 1 + id: 244 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_056: + new_status_id: 1 + role_id: 2 + old_status_id: 6 + id: 56 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_137: + new_status_id: 2 + role_id: 2 + old_status_id: 4 + id: 137 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_163: + new_status_id: 4 + role_id: 3 + old_status_id: 3 + id: 163 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_190: + new_status_id: 6 + role_id: 1 + old_status_id: 2 + id: 190 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_218: + new_status_id: 4 + role_id: 2 + old_status_id: 2 + id: 218 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_245: + new_status_id: 6 + role_id: 3 + old_status_id: 1 + id: 245 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_057: + new_status_id: 2 + role_id: 2 + old_status_id: 6 + id: 57 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_083: + new_status_id: 3 + role_id: 3 + old_status_id: 5 + id: 83 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_138: + new_status_id: 3 + role_id: 2 + old_status_id: 4 + id: 138 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_164: + new_status_id: 5 + role_id: 3 + old_status_id: 3 + id: 164 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_191: + new_status_id: 1 + role_id: 1 + old_status_id: 3 + id: 191 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_219: + new_status_id: 5 + role_id: 2 + old_status_id: 2 + id: 219 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_246: + new_status_id: 1 + role_id: 3 + old_status_id: 2 + id: 246 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_058: + new_status_id: 3 + role_id: 2 + old_status_id: 6 + id: 58 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_084: + new_status_id: 4 + role_id: 3 + old_status_id: 5 + id: 84 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_139: + new_status_id: 5 + role_id: 2 + old_status_id: 4 + id: 139 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_165: + new_status_id: 6 + role_id: 3 + old_status_id: 3 + id: 165 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_192: + new_status_id: 2 + role_id: 1 + old_status_id: 3 + id: 192 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_247: + new_status_id: 3 + role_id: 3 + old_status_id: 2 + id: 247 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_059: + new_status_id: 4 + role_id: 2 + old_status_id: 6 + id: 59 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_085: + new_status_id: 6 + role_id: 3 + old_status_id: 5 + id: 85 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_166: + new_status_id: 1 + role_id: 3 + old_status_id: 4 + id: 166 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_248: + new_status_id: 4 + role_id: 3 + old_status_id: 2 + id: 248 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_086: + new_status_id: 1 + role_id: 3 + old_status_id: 6 + id: 86 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_167: + new_status_id: 2 + role_id: 3 + old_status_id: 4 + id: 167 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_193: + new_status_id: 4 + role_id: 1 + old_status_id: 3 + id: 193 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_249: + new_status_id: 5 + role_id: 3 + old_status_id: 2 + id: 249 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_087: + new_status_id: 2 + role_id: 3 + old_status_id: 6 + id: 87 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_168: + new_status_id: 3 + role_id: 3 + old_status_id: 4 + id: 168 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_194: + new_status_id: 5 + role_id: 1 + old_status_id: 3 + id: 194 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_088: + new_status_id: 3 + role_id: 3 + old_status_id: 6 + id: 88 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_169: + new_status_id: 5 + role_id: 3 + old_status_id: 4 + id: 169 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_195: + new_status_id: 6 + role_id: 1 + old_status_id: 3 + id: 195 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_089: + new_status_id: 4 + role_id: 3 + old_status_id: 6 + id: 89 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_196: + new_status_id: 1 + role_id: 1 + old_status_id: 4 + id: 196 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_197: + new_status_id: 2 + role_id: 1 + old_status_id: 4 + id: 197 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_198: + new_status_id: 3 + role_id: 1 + old_status_id: 4 + id: 198 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_199: + new_status_id: 5 + role_id: 1 + old_status_id: 4 + id: 199 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_010: + new_status_id: 6 + role_id: 1 + old_status_id: 2 + id: 10 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_011: + new_status_id: 1 + role_id: 1 + old_status_id: 3 + id: 11 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_012: + new_status_id: 2 + role_id: 1 + old_status_id: 3 + id: 12 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_200: + new_status_id: 6 + role_id: 1 + old_status_id: 4 + id: 200 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_013: + new_status_id: 4 + role_id: 1 + old_status_id: 3 + id: 13 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_120: + new_status_id: 5 + role_id: 1 + old_status_id: 6 + id: 120 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_201: + new_status_id: 1 + role_id: 1 + old_status_id: 5 + id: 201 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_040: + new_status_id: 6 + role_id: 2 + old_status_id: 2 + id: 40 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_121: + new_status_id: 2 + role_id: 2 + old_status_id: 1 + id: 121 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_202: + new_status_id: 2 + role_id: 1 + old_status_id: 5 + id: 202 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_014: + new_status_id: 5 + role_id: 1 + old_status_id: 3 + id: 14 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_041: + new_status_id: 1 + role_id: 2 + old_status_id: 3 + id: 41 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_122: + new_status_id: 3 + role_id: 2 + old_status_id: 1 + id: 122 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_203: + new_status_id: 3 + role_id: 1 + old_status_id: 5 + id: 203 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_015: + new_status_id: 6 + role_id: 1 + old_status_id: 3 + id: 15 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_230: + new_status_id: 6 + role_id: 2 + old_status_id: 4 + id: 230 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_123: + new_status_id: 4 + role_id: 2 + old_status_id: 1 + id: 123 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_204: + new_status_id: 4 + role_id: 1 + old_status_id: 5 + id: 204 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_016: + new_status_id: 1 + role_id: 1 + old_status_id: 4 + id: 16 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_042: + new_status_id: 2 + role_id: 2 + old_status_id: 3 + id: 42 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_231: + new_status_id: 1 + role_id: 2 + old_status_id: 5 + id: 231 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_070: + new_status_id: 6 + role_id: 3 + old_status_id: 2 + id: 70 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_124: + new_status_id: 5 + role_id: 2 + old_status_id: 1 + id: 124 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_150: + new_status_id: 5 + role_id: 2 + old_status_id: 6 + id: 150 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_205: + new_status_id: 6 + role_id: 1 + old_status_id: 5 + id: 205 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_017: + new_status_id: 2 + role_id: 1 + old_status_id: 4 + id: 17 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_043: + new_status_id: 4 + role_id: 2 + old_status_id: 3 + id: 43 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_232: + new_status_id: 2 + role_id: 2 + old_status_id: 5 + id: 232 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_125: + new_status_id: 6 + role_id: 2 + old_status_id: 1 + id: 125 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_151: + new_status_id: 2 + role_id: 3 + old_status_id: 1 + id: 151 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_206: + new_status_id: 1 + role_id: 1 + old_status_id: 6 + id: 206 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_018: + new_status_id: 3 + role_id: 1 + old_status_id: 4 + id: 18 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_044: + new_status_id: 5 + role_id: 2 + old_status_id: 3 + id: 44 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_071: + new_status_id: 1 + role_id: 3 + old_status_id: 3 + id: 71 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_233: + new_status_id: 3 + role_id: 2 + old_status_id: 5 + id: 233 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_126: + new_status_id: 1 + role_id: 2 + old_status_id: 2 + id: 126 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_152: + new_status_id: 3 + role_id: 3 + old_status_id: 1 + id: 152 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_207: + new_status_id: 2 + role_id: 1 + old_status_id: 6 + id: 207 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_019: + new_status_id: 5 + role_id: 1 + old_status_id: 4 + id: 19 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_045: + new_status_id: 6 + role_id: 2 + old_status_id: 3 + id: 45 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_260: + new_status_id: 6 + role_id: 3 + old_status_id: 4 + id: 260 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_234: + new_status_id: 4 + role_id: 2 + old_status_id: 5 + id: 234 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_127: + new_status_id: 3 + role_id: 2 + old_status_id: 2 + id: 127 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_153: + new_status_id: 4 + role_id: 3 + old_status_id: 1 + id: 153 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_180: + new_status_id: 5 + role_id: 3 + old_status_id: 6 + id: 180 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_208: + new_status_id: 3 + role_id: 1 + old_status_id: 6 + id: 208 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_046: + new_status_id: 1 + role_id: 2 + old_status_id: 4 + id: 46 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_072: + new_status_id: 2 + role_id: 3 + old_status_id: 3 + id: 72 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_261: + new_status_id: 1 + role_id: 3 + old_status_id: 5 + id: 261 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_235: + new_status_id: 6 + role_id: 2 + old_status_id: 5 + id: 235 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_154: + new_status_id: 5 + role_id: 3 + old_status_id: 1 + id: 154 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_181: + new_status_id: 2 + role_id: 1 + old_status_id: 1 + id: 181 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_209: + new_status_id: 4 + role_id: 1 + old_status_id: 6 + id: 209 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_047: + new_status_id: 2 + role_id: 2 + old_status_id: 4 + id: 47 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_073: + new_status_id: 4 + role_id: 3 + old_status_id: 3 + id: 73 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_128: + new_status_id: 4 + role_id: 2 + old_status_id: 2 + id: 128 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_262: + new_status_id: 2 + role_id: 3 + old_status_id: 5 + id: 262 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_236: + new_status_id: 1 + role_id: 2 + old_status_id: 6 + id: 236 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_155: + new_status_id: 6 + role_id: 3 + old_status_id: 1 + id: 155 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_048: + new_status_id: 3 + role_id: 2 + old_status_id: 4 + id: 48 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_074: + new_status_id: 5 + role_id: 3 + old_status_id: 3 + id: 74 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_129: + new_status_id: 5 + role_id: 2 + old_status_id: 2 + id: 129 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_263: + new_status_id: 3 + role_id: 3 + old_status_id: 5 + id: 263 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_237: + new_status_id: 2 + role_id: 2 + old_status_id: 6 + id: 237 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_182: + new_status_id: 3 + role_id: 1 + old_status_id: 1 + id: 182 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_049: + new_status_id: 5 + role_id: 2 + old_status_id: 4 + id: 49 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_075: + new_status_id: 6 + role_id: 3 + old_status_id: 3 + id: 75 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_156: + new_status_id: 1 + role_id: 3 + old_status_id: 2 + id: 156 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_264: + new_status_id: 4 + role_id: 3 + old_status_id: 5 + id: 264 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_238: + new_status_id: 3 + role_id: 2 + old_status_id: 6 + id: 238 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_183: + new_status_id: 4 + role_id: 1 + old_status_id: 1 + id: 183 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_076: + new_status_id: 1 + role_id: 3 + old_status_id: 4 + id: 76 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_157: + new_status_id: 3 + role_id: 3 + old_status_id: 2 + id: 157 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_265: + new_status_id: 6 + role_id: 3 + old_status_id: 5 + id: 265 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_239: + new_status_id: 4 + role_id: 2 + old_status_id: 6 + id: 239 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_077: + new_status_id: 2 + role_id: 3 + old_status_id: 4 + id: 77 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_158: + new_status_id: 4 + role_id: 3 + old_status_id: 2 + id: 158 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_184: + new_status_id: 5 + role_id: 1 + old_status_id: 1 + id: 184 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_266: + new_status_id: 1 + role_id: 3 + old_status_id: 6 + id: 266 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_078: + new_status_id: 3 + role_id: 3 + old_status_id: 4 + id: 78 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_159: + new_status_id: 5 + role_id: 3 + old_status_id: 2 + id: 159 + tracker_id: 2 + type: WorkflowTransition +WorkflowTransitions_185: + new_status_id: 6 + role_id: 1 + old_status_id: 1 + id: 185 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_267: + new_status_id: 2 + role_id: 3 + old_status_id: 6 + id: 267 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_079: + new_status_id: 5 + role_id: 3 + old_status_id: 4 + id: 79 + tracker_id: 1 + type: WorkflowTransition +WorkflowTransitions_186: + new_status_id: 1 + role_id: 1 + old_status_id: 2 + id: 186 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_268: + new_status_id: 3 + role_id: 3 + old_status_id: 6 + id: 268 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_187: + new_status_id: 3 + role_id: 1 + old_status_id: 2 + id: 187 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_269: + new_status_id: 4 + role_id: 3 + old_status_id: 6 + id: 269 + tracker_id: 3 + type: WorkflowTransition +WorkflowTransitions_188: + new_status_id: 4 + role_id: 1 + old_status_id: 2 + id: 188 + tracker_id: 3 + type: WorkflowTransition diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2d/2daba565342ce382c16cf232af8159b068679217.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2d/2daba565342ce382c16cf232af8159b068679217.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,23 @@ +/* Polish initialisation for the jQuery UI date picker plugin. */ +/* Written by Jacek Wysocki (jacek.wysocki@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['pl'] = { + closeText: 'Zamknij', + prevText: '<Poprzedni', + nextText: 'Następny>', + currentText: 'Dziś', + monthNames: ['Styczeń','Luty','Marzec','Kwiecień','Maj','Czerwiec', + 'Lipiec','Sierpień','Wrzesień','Październik','Listopad','Grudzień'], + monthNamesShort: ['Sty','Lu','Mar','Kw','Maj','Cze', + 'Lip','Sie','Wrz','Pa','Lis','Gru'], + dayNames: ['Niedziela','Poniedziałek','Wtorek','Środa','Czwartek','Piątek','Sobota'], + dayNamesShort: ['Nie','Pn','Wt','Śr','Czw','Pt','So'], + dayNamesMin: ['N','Pn','Wt','Śr','Cz','Pt','So'], + weekHeader: 'Tydz', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['pl']); +}); diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2d/2df27d340a83d7deb5fbd1be49feac615fba34af.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2d/2df27d340a83d7deb5fbd1be49feac615fba34af.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,45 @@ +<%= labelled_fields_for :issue, @issue do |f| %> +<%= call_hook(:view_issues_form_details_top, { :issue => @issue, :form => f }) %> + +<% if @issue.safe_attribute? 'is_private' %> +

    + <%= f.check_box :is_private, :no_label => true %> +

    +<% end %> + +<% if @issue.safe_attribute? 'project_id' %> +

    <%= f.select :project_id, project_tree_options_for_select(@issue.allowed_target_projects, :selected => @issue.project), {:required => true}, + :onchange => "updateIssueFrom('#{escape_javascript project_issue_form_path(@project, :id => @issue, :format => 'js')}')" %>

    +<% end %> + +<% if @issue.safe_attribute? 'tracker_id' %> +

    <%= f.select :tracker_id, @issue.project.trackers.collect {|t| [t.name, t.id]}, {:required => true}, + :onchange => "updateIssueFrom('#{escape_javascript project_issue_form_path(@project, :id => @issue, :format => 'js')}')" %>

    +<% end %> + +<% if @issue.safe_attribute? 'subject' %> +

    <%= f.text_field :subject, :size => 80, :required => true %>

    +<% end %> + +<% if @issue.safe_attribute? 'description' %> +

    + <%= f.label_for_field :description, :required => @issue.required_attribute?('description') %> + <%= link_to_function image_tag('edit.png'), '$(this).hide(); $("#issue_description_and_toolbar").show()' unless @issue.new_record? %> + <%= content_tag 'span', :id => "issue_description_and_toolbar", :style => (@issue.new_record? ? nil : 'display:none') do %> + <%= f.text_area :description, + :cols => 60, + :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min), + :accesskey => accesskey(:edit), + :class => 'wiki-edit', + :no_label => true %> + <% end %> +

    +<%= wikitoolbar_for 'issue_description' %> +<% end %> + +
    + <%= render :partial => 'issues/attributes' %> +
    + +<%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2e/2e6c87fbf008e52f19138f6c4bd2e546325437b5.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2e/2e6c87fbf008e52f19138f6c4bd2e546325437b5.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,29 @@ +<% if issues && issues.any? %> +<%= form_tag({}) do %> + + + + + + + + + <% for issue in issues %> + + + + + + + <% end %> + +
    #<%=l(:field_project)%><%=l(:field_tracker)%><%=l(:field_subject)%>
    + <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;', :id => nil) %> + <%= link_to(h(issue.id), :controller => 'issues', :action => 'show', :id => issue) %> + <%= link_to_project(issue.project) %><%=h issue.tracker %> + <%= link_to h(truncate(issue.subject, :length => 60)), :controller => 'issues', :action => 'show', :id => issue %> (<%=h issue.status %>) +
    +<% end %> +<% else %> +

    <%= l(:label_no_data) %>

    +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2e/2e761f17a6eb6248759d869c76abad698e943338.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2e/2e761f17a6eb6248759d869c76abad698e943338.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,107 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class GanttsControllerTest < ActionController::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :workflows, + :versions + + def test_gantt_should_work + i2 = Issue.find(2) + i2.update_attribute(:due_date, 1.month.from_now) + get :show, :project_id => 1 + assert_response :success + assert_template 'gantts/show' + assert_not_nil assigns(:gantt) + # Issue with start and due dates + i = Issue.find(1) + assert_not_nil i.due_date + assert_select "div a.issue", /##{i.id}/ + # Issue with on a targeted version should not be in the events but loaded in the html + i = Issue.find(2) + assert_select "div a.issue", /##{i.id}/ + end + + def test_gantt_should_work_without_issue_due_dates + Issue.update_all("due_date = NULL") + get :show, :project_id => 1 + assert_response :success + assert_template 'gantts/show' + assert_not_nil assigns(:gantt) + end + + def test_gantt_should_work_without_issue_and_version_due_dates + Issue.update_all("due_date = NULL") + Version.update_all("effective_date = NULL") + get :show, :project_id => 1 + assert_response :success + assert_template 'gantts/show' + assert_not_nil assigns(:gantt) + end + + def test_gantt_should_work_cross_project + get :show + assert_response :success + assert_template 'gantts/show' + assert_not_nil assigns(:gantt) + assert_not_nil assigns(:gantt).query + assert_nil assigns(:gantt).project + end + + def test_gantt_should_not_disclose_private_projects + get :show + assert_response :success + assert_template 'gantts/show' + assert_tag 'a', :content => /eCookbook/ + # Root private project + assert_no_tag 'a', {:content => /OnlineStore/} + # Private children of a public project + assert_no_tag 'a', :content => /Private child of eCookbook/ + end + + def test_gantt_should_export_to_pdf + get :show, :project_id => 1, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + assert_not_nil assigns(:gantt) + end + + def test_gantt_should_export_to_pdf_cross_project + get :show, :format => 'pdf' + assert_response :success + assert_equal 'application/pdf', @response.content_type + assert @response.body.starts_with?('%PDF') + assert_not_nil assigns(:gantt) + end + + if Object.const_defined?(:Magick) + def test_gantt_should_export_to_png + get :show, :project_id => 1, :format => 'png' + assert_response :success + assert_equal 'image/png', @response.content_type + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2e/2e7cd667f7708d524c177cdc1b5c822705ec53e1.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2e/2e7cd667f7708d524c177cdc1b5c822705ec53e1.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,7 @@ +require 'awesome_nested_set/awesome_nested_set' +ActiveRecord::Base.send :extend, CollectiveIdea::Acts::NestedSet + +if defined?(ActionView) + require 'awesome_nested_set/helper' + ActionView::Base.send :include, CollectiveIdea::Acts::NestedSet::Helper +end \ No newline at end of file diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2f/2fb6efca6ea9d063a58d2593af2f43422f15665e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2f/2fb6efca6ea9d063a58d2593af2f43422f15665e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,279 @@ +module ActiveRecord + module Acts #:nodoc: + module List #:nodoc: + def self.included(base) + base.extend(ClassMethods) + end + + # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list. + # The class that has this specified needs to have a +position+ column defined as an integer on + # the mapped database table. + # + # Todo list example: + # + # class TodoList < ActiveRecord::Base + # has_many :todo_items, :order => "position" + # end + # + # class TodoItem < ActiveRecord::Base + # belongs_to :todo_list + # acts_as_list :scope => :todo_list + # end + # + # todo_list.first.move_to_bottom + # todo_list.last.move_higher + module ClassMethods + # Configuration options are: + # + # * +column+ - specifies the column name to use for keeping the position integer (default: +position+) + # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach _id + # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible + # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key. + # Example: acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0' + def acts_as_list(options = {}) + configuration = { :column => "position", :scope => "1 = 1" } + configuration.update(options) if options.is_a?(Hash) + + configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/ + + if configuration[:scope].is_a?(Symbol) + scope_condition_method = %( + def scope_condition + if #{configuration[:scope].to_s}.nil? + "#{configuration[:scope].to_s} IS NULL" + else + "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}" + end + end + ) + else + scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end" + end + + class_eval <<-EOV + include ActiveRecord::Acts::List::InstanceMethods + + def acts_as_list_class + ::#{self.name} + end + + def position_column + '#{configuration[:column]}' + end + + #{scope_condition_method} + + before_destroy :remove_from_list + before_create :add_to_list_bottom + EOV + end + end + + # All the methods available to a record that has had acts_as_list specified. Each method works + # by assuming the object to be the item in the list, so chapter.move_lower would move that chapter + # lower in the list of all chapters. Likewise, chapter.first? would return +true+ if that chapter is + # the first in the list of all chapters. + module InstanceMethods + # Insert the item at the given position (defaults to the top position of 1). + def insert_at(position = 1) + insert_at_position(position) + end + + # Swap positions with the next lower item, if one exists. + def move_lower + return unless lower_item + + acts_as_list_class.transaction do + lower_item.decrement_position + increment_position + end + end + + # Swap positions with the next higher item, if one exists. + def move_higher + return unless higher_item + + acts_as_list_class.transaction do + higher_item.increment_position + decrement_position + end + end + + # Move to the bottom of the list. If the item is already in the list, the items below it have their + # position adjusted accordingly. + def move_to_bottom + return unless in_list? + acts_as_list_class.transaction do + decrement_positions_on_lower_items + assume_bottom_position + end + end + + # Move to the top of the list. If the item is already in the list, the items above it have their + # position adjusted accordingly. + def move_to_top + return unless in_list? + acts_as_list_class.transaction do + increment_positions_on_higher_items + assume_top_position + end + end + + # Move to the given position + def move_to=(pos) + case pos.to_s + when 'highest' + move_to_top + when 'higher' + move_higher + when 'lower' + move_lower + when 'lowest' + move_to_bottom + end + reset_positions_in_list + end + + def reset_positions_in_list + acts_as_list_class.where(scope_condition).reorder("#{position_column} ASC, id ASC").each_with_index do |item, i| + unless item.send(position_column) == (i + 1) + acts_as_list_class.update_all({position_column => (i + 1)}, {:id => item.id}) + end + end + end + + # Removes the item from the list. + def remove_from_list + if in_list? + decrement_positions_on_lower_items + update_attribute position_column, nil + end + end + + # Increase the position of this item without adjusting the rest of the list. + def increment_position + return unless in_list? + update_attribute position_column, self.send(position_column).to_i + 1 + end + + # Decrease the position of this item without adjusting the rest of the list. + def decrement_position + return unless in_list? + update_attribute position_column, self.send(position_column).to_i - 1 + end + + # Return +true+ if this object is the first in the list. + def first? + return false unless in_list? + self.send(position_column) == 1 + end + + # Return +true+ if this object is the last in the list. + def last? + return false unless in_list? + self.send(position_column) == bottom_position_in_list + end + + # Return the next higher item in the list. + def higher_item + return nil unless in_list? + acts_as_list_class.find(:first, :conditions => + "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}" + ) + end + + # Return the next lower item in the list. + def lower_item + return nil unless in_list? + acts_as_list_class.find(:first, :conditions => + "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}" + ) + end + + # Test if this record is in a list + def in_list? + !send(position_column).nil? + end + + private + def add_to_list_top + increment_positions_on_all_items + end + + def add_to_list_bottom + self[position_column] = bottom_position_in_list.to_i + 1 + end + + # Overwrite this method to define the scope of the list changes + def scope_condition() "1" end + + # Returns the bottom position number in the list. + # bottom_position_in_list # => 2 + def bottom_position_in_list(except = nil) + item = bottom_item(except) + item ? item.send(position_column) : 0 + end + + # Returns the bottom item + def bottom_item(except = nil) + conditions = scope_condition + conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except + acts_as_list_class.where(conditions).reorder("#{position_column} DESC").first + end + + # Forces item to assume the bottom position in the list. + def assume_bottom_position + update_attribute(position_column, bottom_position_in_list(self).to_i + 1) + end + + # Forces item to assume the top position in the list. + def assume_top_position + update_attribute(position_column, 1) + end + + # This has the effect of moving all the higher items up one. + def decrement_positions_on_higher_items(position) + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}" + ) + end + + # This has the effect of moving all the lower items up one. + def decrement_positions_on_lower_items + return unless in_list? + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}" + ) + end + + # This has the effect of moving all the higher items down one. + def increment_positions_on_higher_items + return unless in_list? + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}" + ) + end + + # This has the effect of moving all the lower items down one. + def increment_positions_on_lower_items(position) + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}" + ) + end + + # Increments position (position_column) of all items in the list. + def increment_positions_on_all_items + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} + 1)", "#{scope_condition}" + ) + end + + def insert_at_position(position) + remove_from_list + increment_positions_on_lower_items(position) + self.update_attribute(position_column, position) + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/2f/2fe801d5801d8321bfdbd57f3425598592dd401b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/2f/2fe801d5801d8321bfdbd57f3425598592dd401b.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,9 @@ +<% if @journal.frozen? %> + $("#change-<%= @journal.id %>").remove(); +<% else %> + $("#journal-<%= @journal.id %>-notes").replaceWith('<%= escape_javascript(render_notes(@journal.issue, @journal, :reply_links => authorize_for('issues', 'edit'))) %>'); + $("#journal-<%= @journal.id %>-notes").show(); + $("#journal-<%= @journal.id %>-form").remove(); +<% end %> + +<%= call_hook(:view_journals_update_js_bottom, { :journal => @journal }) %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/30/3023c43eca89098e7ad3d81f2db133b0643baf81.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/30/3023c43eca89098e7ad3d81f2db133b0643baf81.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,134 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redcloth3' +require 'digest/md5' + +module Redmine + module WikiFormatting + module Textile + class Formatter < RedCloth3 + include ActionView::Helpers::TagHelper + include Redmine::WikiFormatting::LinksHelper + + alias :inline_auto_link :auto_link! + alias :inline_auto_mailto :auto_mailto! + + # auto_link rule after textile rules so that it doesn't break !image_url! tags + RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto] + + def initialize(*args) + super + self.hard_breaks=true + self.no_span_caps=true + self.filter_styles=false + end + + def to_html(*rules) + @toc = [] + super(*RULES).to_s + end + + def get_section(index) + section = extract_sections(index)[1] + hash = Digest::MD5.hexdigest(section) + return section, hash + end + + def update_section(index, update, hash=nil) + t = extract_sections(index) + if hash.present? && hash != Digest::MD5.hexdigest(t[1]) + raise Redmine::WikiFormatting::StaleSectionError + end + t[1] = update unless t[1].blank? + t.reject(&:blank?).join "\n\n" + end + + def extract_sections(index) + @pre_list = [] + text = self.dup + rip_offtags text, false, false + before = '' + s = '' + after = '' + i = 0 + l = 1 + started = false + ended = false + text.scan(/(((?:.*?)(\A|\r?\n\s*\r?\n))(h(\d+)(#{A}#{C})\.(?::(\S+))?[ \t](.*?)$)|.*)/m).each do |all, content, lf, heading, level| + if heading.nil? + if ended + after << all + elsif started + s << all + else + before << all + end + break + end + i += 1 + if ended + after << all + elsif i == index + l = level.to_i + before << content + s << heading + started = true + elsif i > index + s << content + if level.to_i > l + s << heading + else + after << heading + ended = true + end + else + before << all + end + end + sections = [before.strip, s.strip, after.strip] + sections.each {|section| smooth_offtags_without_code_highlighting section} + sections + end + + private + + # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet. + # http://code.whytheluckystiff.net/redcloth/changeset/128 + def hard_break( text ) + text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1
    " ) if hard_breaks + end + + alias :smooth_offtags_without_code_highlighting :smooth_offtags + # Patch to add code highlighting support to RedCloth + def smooth_offtags( text ) + unless @pre_list.empty? + ## replace
     content
    +            text.gsub!(//) do
    +              content = @pre_list[$1.to_i]
    +              if content.match(/\s?(.+)/m)
    +                content = "" +
    +                  Redmine::SyntaxHighlighting.highlight_by_language($2, $1)
    +              end
    +              content
    +            end
    +          end
    +        end
    +      end
    +    end
    +  end
    +end
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/30/303368c5998413441d0a22f71bb064b12cf186a2.svn-base
    --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    +++ b/.svn/pristine/30/303368c5998413441d0a22f71bb064b12cf186a2.svn-base	Thu Jun 20 08:47:50 2013 +0100
    @@ -0,0 +1,16 @@
    +jsToolBar.strings = {};
    +jsToolBar.strings['Strong'] = 'Получер';
    +jsToolBar.strings['Italic'] = 'КурÑив';
    +jsToolBar.strings['Underline'] = 'Подчертан';
    +jsToolBar.strings['Deleted'] = 'Изтрит';
    +jsToolBar.strings['Code'] = 'Вграден код';
    +jsToolBar.strings['Heading 1'] = 'Заглавие 1';
    +jsToolBar.strings['Heading 2'] = 'Заглавие 2';
    +jsToolBar.strings['Heading 3'] = 'Заглавие 3';
    +jsToolBar.strings['Unordered list'] = 'Ðеподреден ÑпиÑък';
    +jsToolBar.strings['Ordered list'] = 'Подреден ÑпиÑък';
    +jsToolBar.strings['Quote'] = 'Цитат';
    +jsToolBar.strings['Unquote'] = 'Премахване на цитат';
    +jsToolBar.strings['Preformatted text'] = 'Форматиран текÑÑ‚';
    +jsToolBar.strings['Wiki link'] = 'Връзка до Wiki Ñтраница';
    +jsToolBar.strings['Image'] = 'Изображение';
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/30/30714e8131663bc1da511ec9946626d040077630.svn-base
    --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    +++ b/.svn/pristine/30/30714e8131663bc1da511ec9946626d040077630.svn-base	Thu Jun 20 08:47:50 2013 +0100
    @@ -0,0 +1,7 @@
    +From: John Smith 
    +To: "redmine@somenet.foo" 
    +Subject: Re: =?iso-2022-jp?b?GyRCJUYlOSVIGyhCCg=?=
    +Date: Fri, 1 Jun 2012 14:39:38 +0200
    +Message-ID: <87C31D42249DD0489D1A1444E3232DD7019D6183@foo.bar>
    +
    +Fixture
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/30/30accb20c8e9d14a6bba9ac1e15c6b0323bdc2b1.svn-base
    --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    +++ b/.svn/pristine/30/30accb20c8e9d14a6bba9ac1e15c6b0323bdc2b1.svn-base	Thu Jun 20 08:47:50 2013 +0100
    @@ -0,0 +1,35 @@
    +# Redmine - project management software
    +# Copyright (C) 2006-2012  Jean-Philippe Lang
    +#
    +# This program is free software; you can redistribute it and/or
    +# modify it under the terms of the GNU General Public License
    +# as published by the Free Software Foundation; either version 2
    +# of the License, or (at your option) any later version.
    +#
    +# This program is distributed in the hope that it will be useful,
    +# but WITHOUT ANY WARRANTY; without even the implied warranty of
    +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    +# GNU General Public License for more details.
    +#
    +# You should have received a copy of the GNU General Public License
    +# along with this program; if not, write to the Free Software
    +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
    +
    +
    +namespace :db do
    +  desc 'Encrypts SCM and LDAP passwords in the database.'
    +  task :encrypt => :environment do
    +    unless (Repository.encrypt_all(:password) &&
    +      AuthSource.encrypt_all(:account_password))
    +      raise "Some objects could not be saved after encryption, update was rollback'ed."
    +    end
    +  end
    +
    +  desc 'Decrypts SCM and LDAP passwords in the database.'
    +  task :decrypt => :environment do
    +    unless (Repository.decrypt_all(:password) &&
    +      AuthSource.decrypt_all(:account_password))
    +      raise "Some objects could not be saved after decryption, update was rollback'ed."
    +    end
    +  end
    +end
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/30/30da10719771ca2dd5b41906f732295b752ed80d.svn-base
    --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    +++ b/.svn/pristine/30/30da10719771ca2dd5b41906f732295b752ed80d.svn-base	Thu Jun 20 08:47:50 2013 +0100
    @@ -0,0 +1,23 @@
    +/* Korean initialisation for the jQuery calendar extension. */
    +/* Written by DaeKwon Kang (ncrash.dk@gmail.com), Edited by Genie. */
    +jQuery(function($){
    +	$.datepicker.regional['ko'] = {
    +		closeText: '닫기',
    +		prevText: 'ì´ì „달',
    +		nextText: '다ìŒë‹¬',
    +		currentText: '오늘',
    +		monthNames: ['1ì›”','2ì›”','3ì›”','4ì›”','5ì›”','6ì›”',
    +		'7ì›”','8ì›”','9ì›”','10ì›”','11ì›”','12ì›”'],
    +		monthNamesShort: ['1ì›”','2ì›”','3ì›”','4ì›”','5ì›”','6ì›”',
    +		'7ì›”','8ì›”','9ì›”','10ì›”','11ì›”','12ì›”'],
    +		dayNames: ['ì¼ìš”ì¼','월요ì¼','화요ì¼','수요ì¼','목요ì¼','금요ì¼','토요ì¼'],
    +		dayNamesShort: ['ì¼','ì›”','í™”','수','목','금','토'],
    +		dayNamesMin: ['ì¼','ì›”','í™”','수','목','금','토'],
    +		weekHeader: 'Wk',
    +		dateFormat: 'yy-mm-dd',
    +		firstDay: 0,
    +		isRTL: false,
    +		showMonthAfterYear: true,
    +		yearSuffix: 'ë…„'};
    +	$.datepicker.setDefaults($.datepicker.regional['ko']);
    +});
    \ No newline at end of file
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/30/30e01132e9ab5b3c9ae62f165db3ff3e34e482a8.svn-base
    --- a/.svn/pristine/30/30e01132e9ab5b3c9ae62f165db3ff3e34e482a8.svn-base	Thu Jun 20 08:46:39 2013 +0100
    +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    @@ -1,86 +0,0 @@
    -# Redmine - project management software
    -# Copyright (C) 2006-2011  Jean-Philippe Lang
    -#
    -# This program is free software; you can redistribute it and/or
    -# modify it under the terms of the GNU General Public License
    -# as published by the Free Software Foundation; either version 2
    -# of the License, or (at your option) any later version.
    -#
    -# This program is distributed in the hope that it will be useful,
    -# but WITHOUT ANY WARRANTY; without even the implied warranty of
    -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    -# GNU General Public License for more details.
    -#
    -# You should have received a copy of the GNU General Public License
    -# along with this program; if not, write to the Free Software
    -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
    -
    -class DocumentsController < ApplicationController
    -  default_search_scope :documents
    -  model_object Document
    -  before_filter :find_project, :only => [:index, :new]
    -  before_filter :find_model_object, :except => [:index, :new]
    -  before_filter :find_project_from_association, :except => [:index, :new]
    -  before_filter :authorize
    -
    -  helper :attachments
    -
    -  def index
    -    @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
    -    documents = @project.documents.find :all, :include => [:attachments, :category]
    -    case @sort_by
    -    when 'date'
    -      @grouped = documents.group_by {|d| d.updated_on.to_date }
    -    when 'title'
    -      @grouped = documents.group_by {|d| d.title.first.upcase}
    -    when 'author'
    -      @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
    -    else
    -      @grouped = documents.group_by(&:category)
    -    end
    -    @document = @project.documents.build
    -    render :layout => false if request.xhr?
    -  end
    -
    -  def show
    -    @attachments = @document.attachments.find(:all, :order => "created_on DESC")
    -  end
    -
    -  def new
    -    @document = @project.documents.build(params[:document])
    -    if request.post? and @document.save	
    -      attachments = Attachment.attach_files(@document, params[:attachments])
    -      render_attachment_warning_if_needed(@document)
    -      flash[:notice] = l(:notice_successful_create)
    -      redirect_to :action => 'index', :project_id => @project
    -    end
    -  end
    -
    -  def edit
    -    @categories = DocumentCategory.active #TODO: use it in the views
    -    if request.post? and @document.update_attributes(params[:document])
    -      flash[:notice] = l(:notice_successful_update)
    -      redirect_to :action => 'show', :id => @document
    -    end
    -  end
    -
    -  def destroy
    -    @document.destroy
    -    redirect_to :controller => 'documents', :action => 'index', :project_id => @project
    -  end
    -
    -  def add_attachment
    -    attachments = Attachment.attach_files(@document, params[:attachments])
    -    render_attachment_warning_if_needed(@document)
    -
    -    Mailer.deliver_attachments_added(attachments[:files]) if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
    -    redirect_to :action => 'show', :id => @document
    -  end
    -
    -private
    -  def find_project
    -    @project = Project.find(params[:project_id])
    -  rescue ActiveRecord::RecordNotFound
    -    render_404
    -  end
    -end
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/31/319ace742da33939c6f0a3745726645fe20c4534.svn-base
    --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    +++ b/.svn/pristine/31/319ace742da33939c6f0a3745726645fe20c4534.svn-base	Thu Jun 20 08:47:50 2013 +0100
    @@ -0,0 +1,26 @@
    +
    + <%= l(:notice_issue_update_conflict) %> + <% if @conflict_journals.present? %> +
    + <% @conflict_journals.sort_by(&:id).each do |journal| %> +

    <%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %>

    + <% if journal.details.any? %> +
      + <% details_to_strings(journal.details).each do |string| %> +
    • <%= string %>
    • + <% end %> +
    + <% end %> + <%= textilizable(journal, :notes) unless journal.notes.blank? %> + <% end %> +
    + <% end %> +
    +

    +
    + <% if @issue.notes.present? %> +
    + <% end %> + +

    +

    <%= submit_tag l(:button_submit) %>

    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/31/31bb85f22f4d21a57af6d104f857fd83f936b8f3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/31/31bb85f22f4d21a57af6d104f857fd83f936b8f3.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,65 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class WorkflowTest < ActiveSupport::TestCase + fixtures :roles, :trackers, :issue_statuses + + def test_copy + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 2) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 3, :assignee => true) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :new_status_id => 4, :author => true) + + assert_difference 'WorkflowTransition.count', 3 do + WorkflowTransition.copy(Tracker.find(2), Role.find(1), Tracker.find(3), Role.find(2)) + end + + assert WorkflowTransition.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false}) + assert WorkflowTransition.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 3, :author => false, :assignee => true}) + assert WorkflowTransition.first(:conditions => {:role_id => 2, :tracker_id => 3, :old_status_id => 1, :new_status_id => 4, :author => true, :assignee => false}) + end + + def test_workflow_permission_should_validate_rule + wp = WorkflowPermission.new(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :field_name => 'due_date') + assert !wp.save + + wp.rule = 'foo' + assert !wp.save + + wp.rule = 'required' + assert wp.save + + wp.rule = 'readonly' + assert wp.save + end + + def test_workflow_permission_should_validate_field_name + wp = WorkflowPermission.new(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :rule => 'required') + assert !wp.save + + wp.field_name = 'foo' + assert !wp.save + + wp.field_name = 'due_date' + assert wp.save + + wp.field_name = '1' + assert wp.save + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/31/31dde3fcb99bae1f1bf825ebba05c5ceb4525424.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/31/31dde3fcb99bae1f1bf825ebba05c5ceb4525424.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,27 @@ +
    +<%= link_to_if_authorized l(:label_query_new), new_project_query_path(:project_id => @project), :class => 'icon icon-add' %> +
    + +

    <%= l(:label_query_plural) %>

    + +<% if @queries.empty? %> +

    <%=l(:label_no_data)%>

    +<% else %> + + <% @queries.each do |query| %> + + + + + <% end %> +
    + <%= link_to h(query.name), :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %> + + + <% if query.editable_by?(User.current) %> + <%= link_to l(:button_edit), edit_query_path(query), :class => 'icon icon-edit' %> + <%= delete_link query_path(query) %> + + <% end %> +
    +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/31/31fa3914a91d4720d86c081f871b079fae9c31a5.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/31/31fa3914a91d4720d86c081f871b079fae9c31a5.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,15 @@ +class <%= @migration_class_name %> < ActiveRecord::Migration + def change + create_table :<%= @table_name %> do |t| +<% attributes.each do |attribute| -%> + t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %> +<% end -%> +<% if options[:timestamps] %> + t.timestamps +<% end -%> + end +<% attributes_with_index.each do |attribute| -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> +<% end -%> + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/32/325cbd5f2592da1a1293c4a50be14ad1338348eb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/32/325cbd5f2592da1a1293c4a50be14ad1338348eb.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,212 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class ApiTest::GroupsTest < ActionController::IntegrationTest + fixtures :users, :groups_users + + def setup + Setting.rest_api_enabled = '1' + end + + context "GET /groups" do + context ".xml" do + should "require authentication" do + get '/groups.xml' + assert_response 401 + end + + should "return groups" do + get '/groups.xml', {}, credentials('admin') + assert_response :success + assert_equal 'application/xml', response.content_type + + assert_select 'groups' do + assert_select 'group' do + assert_select 'name', :text => 'A Team' + assert_select 'id', :text => '10' + end + end + end + end + + context ".json" do + should "require authentication" do + get '/groups.json' + assert_response 401 + end + + should "return groups" do + get '/groups.json', {}, credentials('admin') + assert_response :success + assert_equal 'application/json', response.content_type + + json = MultiJson.load(response.body) + groups = json['groups'] + assert_kind_of Array, groups + group = groups.detect {|g| g['name'] == 'A Team'} + assert_not_nil group + assert_equal({'id' => 10, 'name' => 'A Team'}, group) + end + end + end + + context "GET /groups/:id" do + context ".xml" do + should "return the group with its users" do + get '/groups/10.xml', {}, credentials('admin') + assert_response :success + assert_equal 'application/xml', response.content_type + + assert_select 'group' do + assert_select 'name', :text => 'A Team' + assert_select 'id', :text => '10' + end + end + + should "include users if requested" do + get '/groups/10.xml?include=users', {}, credentials('admin') + assert_response :success + assert_equal 'application/xml', response.content_type + + assert_select 'group' do + assert_select 'users' do + assert_select 'user', Group.find(10).users.count + assert_select 'user[id=8]' + end + end + end + + should "include memberships if requested" do + get '/groups/10.xml?include=memberships', {}, credentials('admin') + assert_response :success + assert_equal 'application/xml', response.content_type + + assert_select 'group' do + assert_select 'memberships' + end + end + end + end + + context "POST /groups" do + context "with valid parameters" do + context ".xml" do + should "create groups" do + assert_difference('Group.count') do + post '/groups.xml', {:group => {:name => 'Test', :user_ids => [2, 3]}}, credentials('admin') + assert_response :created + assert_equal 'application/xml', response.content_type + end + + group = Group.order('id DESC').first + assert_equal 'Test', group.name + assert_equal [2, 3], group.users.map(&:id).sort + + assert_select 'group' do + assert_select 'name', :text => 'Test' + end + end + end + end + + context "with invalid parameters" do + context ".xml" do + should "return errors" do + assert_no_difference('Group.count') do + post '/groups.xml', {:group => {:name => ''}}, credentials('admin') + end + assert_response :unprocessable_entity + assert_equal 'application/xml', response.content_type + + assert_select 'errors' do + assert_select 'error', :text => /Name can't be blank/ + end + end + end + end + end + + context "PUT /groups/:id" do + context "with valid parameters" do + context ".xml" do + should "update the group" do + put '/groups/10.xml', {:group => {:name => 'New name', :user_ids => [2, 3]}}, credentials('admin') + assert_response :ok + assert_equal '', @response.body + + group = Group.find(10) + assert_equal 'New name', group.name + assert_equal [2, 3], group.users.map(&:id).sort + end + end + end + + context "with invalid parameters" do + context ".xml" do + should "return errors" do + put '/groups/10.xml', {:group => {:name => ''}}, credentials('admin') + assert_response :unprocessable_entity + assert_equal 'application/xml', response.content_type + + assert_select 'errors' do + assert_select 'error', :text => /Name can't be blank/ + end + end + end + end + end + + context "DELETE /groups/:id" do + context ".xml" do + should "delete the group" do + assert_difference 'Group.count', -1 do + delete '/groups/10.xml', {}, credentials('admin') + assert_response :ok + assert_equal '', @response.body + end + end + end + end + + context "POST /groups/:id/users" do + context ".xml" do + should "add user to the group" do + assert_difference 'Group.find(10).users.count' do + post '/groups/10/users.xml', {:user_id => 5}, credentials('admin') + assert_response :ok + assert_equal '', @response.body + end + assert_include User.find(5), Group.find(10).users + end + end + end + + context "DELETE /groups/:id/users/:user_id" do + context ".xml" do + should "remove user from the group" do + assert_difference 'Group.find(10).users.count', -1 do + delete '/groups/10/users/8.xml', {}, credentials('admin') + assert_response :ok + assert_equal '', @response.body + end + assert_not_include User.find(8), Group.find(10).users + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/32/32621c66778dffd99e5e7238a5aa46a6518b1b3c.svn-base --- a/.svn/pristine/32/32621c66778dffd99e5e7238a5aa46a6518b1b3c.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,304 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module IssuesHelper - include ApplicationHelper - - def issue_list(issues, &block) - ancestors = [] - issues.each do |issue| - while (ancestors.any? && !issue.is_descendant_of?(ancestors.last)) - ancestors.pop - end - yield issue, ancestors.size - ancestors << issue unless issue.leaf? - end - end - - # Renders a HTML/CSS tooltip - # - # To use, a trigger div is needed. This is a div with the class of "tooltip" - # that contains this method wrapped in a span with the class of "tip" - # - #
    <%= link_to_issue(issue) %> - # <%= render_issue_tooltip(issue) %> - #
    - # - def render_issue_tooltip(issue) - @cached_label_status ||= l(:field_status) - @cached_label_start_date ||= l(:field_start_date) - @cached_label_due_date ||= l(:field_due_date) - @cached_label_assigned_to ||= l(:field_assigned_to) - @cached_label_priority ||= l(:field_priority) - @cached_label_project ||= l(:field_project) - - (link_to_issue(issue) + "

    " + - "#{@cached_label_project}: #{link_to_project(issue.project)}
    " + - "#{@cached_label_status}: #{h(issue.status.name)}
    " + - "#{@cached_label_start_date}: #{format_date(issue.start_date)}
    " + - "#{@cached_label_due_date}: #{format_date(issue.due_date)}
    " + - "#{@cached_label_assigned_to}: #{h(issue.assigned_to)}
    " + - "#{@cached_label_priority}: #{h(issue.priority.name)}").html_safe - end - - def issue_heading(issue) - h("#{issue.tracker} ##{issue.id}") - end - - def render_issue_subject_with_tree(issue) - s = '' - ancestors = issue.root? ? [] : issue.ancestors.visible.all - ancestors.each do |ancestor| - s << '
    ' + content_tag('p', link_to_issue(ancestor)) - end - s << '
    ' - subject = h(issue.subject) - if issue.is_private? - subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject - end - s << content_tag('h3', subject) - s << '
    ' * (ancestors.size + 1) - s.html_safe - end - - def render_descendants_tree(issue) - s = '
    ' - issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level| - s << content_tag('tr', - content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') + - content_tag('td', link_to_issue(child, :truncate => 60), :class => 'subject') + - content_tag('td', h(child.status)) + - content_tag('td', link_to_user(child.assigned_to)) + - content_tag('td', progress_bar(child.done_ratio, :width => '80px')), - :class => "issue issue-#{child.id} hascontextmenu #{level > 0 ? "idnt idnt-#{level}" : nil}") - end - s << '
    ' - s.html_safe - end - - def render_custom_fields_rows(issue) - return if issue.custom_field_values.empty? - ordered_values = [] - half = (issue.custom_field_values.size / 2.0).ceil - half.times do |i| - ordered_values << issue.custom_field_values[i] - ordered_values << issue.custom_field_values[i + half] - end - s = "\n" - n = 0 - ordered_values.compact.each do |value| - s << "\n\n" if n > 0 && (n % 2) == 0 - s << "\t#{ h(value.custom_field.name) }:#{ simple_format_without_paragraph(h(show_value(value))) }\n" - n += 1 - end - s << "\n" - s.html_safe - end - - def issues_destroy_confirmation_message(issues) - issues = [issues] unless issues.is_a?(Array) - message = l(:text_issues_destroy_confirmation) - descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2} - if descendant_count > 0 - issues.each do |issue| - next if issue.root? - issues.each do |other_issue| - descendant_count -= 1 if issue.is_descendant_of?(other_issue) - end - end - if descendant_count > 0 - message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count) - end - end - message - end - - def sidebar_queries - unless @sidebar_queries - # User can see public queries and his own queries - visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)]) - # Project specific queries and global queries - visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]) - @sidebar_queries = Query.find(:all, - :select => 'id, name, is_public', - :order => "name ASC", - :conditions => visible.conditions) - end - @sidebar_queries - end - - def query_links(title, queries) - # links to #index on issues/show - url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params - - content_tag('h3', h(title)) + - queries.collect {|query| - link_to(h(query.name), url_params.merge(:query_id => query)) - }.join('
    ') - end - - def render_sidebar_queries - out = '' - queries = sidebar_queries.select {|q| !q.is_public?} - out << query_links(l(:label_my_queries), queries) if queries.any? - queries = sidebar_queries.select {|q| q.is_public?} - out << query_links(l(:label_query_plural), queries) if queries.any? - out - end - - def show_detail(detail, no_html=false) - case detail.property - when 'attr' - field = detail.prop_key.to_s.gsub(/\_id$/, "") - label = l(("field_" + field).to_sym) - case - when ['due_date', 'start_date'].include?(detail.prop_key) - value = format_date(detail.value.to_date) if detail.value - old_value = format_date(detail.old_value.to_date) if detail.old_value - - when ['project_id', 'status_id', 'tracker_id', 'assigned_to_id', 'priority_id', 'category_id', 'fixed_version_id'].include?(detail.prop_key) - value = find_name_by_reflection(field, detail.value) - old_value = find_name_by_reflection(field, detail.old_value) - - when detail.prop_key == 'estimated_hours' - value = "%0.02f" % detail.value.to_f unless detail.value.blank? - old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank? - - when detail.prop_key == 'parent_id' - label = l(:field_parent_issue) - value = "##{detail.value}" unless detail.value.blank? - old_value = "##{detail.old_value}" unless detail.old_value.blank? - - when detail.prop_key == 'is_private' - value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank? - old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank? - end - when 'cf' - custom_field = CustomField.find_by_id(detail.prop_key) - if custom_field - label = custom_field.name - value = format_value(detail.value, custom_field.field_format) if detail.value - old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value - end - when 'attachment' - label = l(:label_attachment) - end - call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value }) - - label ||= detail.prop_key - value ||= detail.value - old_value ||= detail.old_value - - unless no_html - label = content_tag('strong', label) - old_value = content_tag("i", h(old_value)) if detail.old_value - old_value = content_tag("strike", old_value) if detail.old_value and detail.value.blank? - if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key) - # Link to the attachment if it has not been removed - value = link_to_attachment(a) - else - value = content_tag("i", h(value)) if value - end - end - - if detail.property == 'attr' && detail.prop_key == 'description' - s = l(:text_journal_changed_no_detail, :label => label) - unless no_html - diff_link = link_to 'diff', - {:controller => 'journals', :action => 'diff', :id => detail.journal_id, :detail_id => detail.id}, - :title => l(:label_view_diff) - s << " (#{ diff_link })" - end - s - elsif !detail.value.blank? - case detail.property - when 'attr', 'cf' - if !detail.old_value.blank? - l(:text_journal_changed, :label => label, :old => old_value, :new => value) - else - l(:text_journal_set_to, :label => label, :value => value) - end - when 'attachment' - l(:text_journal_added, :label => label, :value => value) - end - else - l(:text_journal_deleted, :label => label, :old => old_value) - end - end - - # Find the name of an associated record stored in the field attribute - def find_name_by_reflection(field, id) - association = Issue.reflect_on_association(field.to_sym) - if association - record = association.class_name.constantize.find_by_id(id) - return record.name if record - end - end - - # Renders issue children recursively - def render_api_issue_children(issue, api) - return if issue.leaf? - api.array :children do - issue.children.each do |child| - api.issue(:id => child.id) do - api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil? - api.subject child.subject - render_api_issue_children(child, api) - end - end - end - end - - def issues_to_csv(issues, project, query, options={}) - decimal_separator = l(:general_csv_decimal_separator) - encoding = l(:general_csv_encoding) - columns = (options[:columns] == 'all' ? query.available_columns : query.columns) - - export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv| - # csv header fields - csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) } + - (options[:description] ? [Redmine::CodesetUtil.from_utf8(l(:field_description), encoding)] : []) - - # csv lines - issues.each do |issue| - col_values = columns.collect do |column| - s = if column.is_a?(QueryCustomFieldColumn) - cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id} - show_value(cv) - else - value = issue.send(column.name) - if value.is_a?(Date) - format_date(value) - elsif value.is_a?(Time) - format_time(value) - elsif value.is_a?(Float) - value.to_s.gsub('.', decimal_separator) - else - value - end - end - s.to_s - end - csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } + - (options[:description] ? [Redmine::CodesetUtil.from_utf8(issue.description, encoding)] : []) - end - end - export - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/32/3263175b802b3311f08d06f669648333f55d611c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/32/3263175b802b3311f08d06f669648333f55d611c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,51 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingWatchersTest < ActionController::IntegrationTest + def test_watchers + assert_routing( + { :method => 'get', :path => "/watchers/new" }, + { :controller => 'watchers', :action => 'new' } + ) + assert_routing( + { :method => 'post', :path => "/watchers/append" }, + { :controller => 'watchers', :action => 'append' } + ) + assert_routing( + { :method => 'post', :path => "/watchers" }, + { :controller => 'watchers', :action => 'create' } + ) + assert_routing( + { :method => 'post', :path => "/watchers/destroy" }, + { :controller => 'watchers', :action => 'destroy' } + ) + assert_routing( + { :method => 'get', :path => "/watchers/autocomplete_for_user" }, + { :controller => 'watchers', :action => 'autocomplete_for_user' } + ) + assert_routing( + { :method => 'post', :path => "/watchers/watch" }, + { :controller => 'watchers', :action => 'watch' } + ) + assert_routing( + { :method => 'post', :path => "/watchers/unwatch" }, + { :controller => 'watchers', :action => 'unwatch' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/32/32bb68107ce90e09885cb5aa647d1b1b670aefe9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/32/32bb68107ce90e09885cb5aa647d1b1b670aefe9.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,86 @@ +<%= board_breadcrumb(@message) %> + +
    + <%= watcher_tag(@topic, User.current) %> + <%= link_to( + l(:button_quote), + {:action => 'quote', :id => @topic}, + :remote => true, + :method => 'get', + :class => 'icon icon-comment', + :remote => true) if !@topic.locked? && authorize_for('messages', 'reply') %> + <%= link_to( + l(:button_edit), + {:action => 'edit', :id => @topic}, + :class => 'icon icon-edit' + ) if @message.editable_by?(User.current) %> + <%= link_to( + l(:button_delete), + {:action => 'destroy', :id => @topic}, + :method => :post, + :data => {:confirm => l(:text_are_you_sure)}, + :class => 'icon icon-del' + ) if @message.destroyable_by?(User.current) %> +
    + +

    <%= avatar(@topic.author, :size => "24") %><%=h @topic.subject %>

    + +
    +

    <%= authoring @topic.created_on, @topic.author %>

    +
    +<%= textilizable(@topic, :content) %> +
    +<%= link_to_attachments @topic, :author => false %> +
    +
    + +<% unless @replies.empty? %> +

    <%= l(:label_reply_plural) %> (<%= @reply_count %>)

    +<% @replies.each do |message| %> +
    "> +
    + <%= link_to( + image_tag('comment.png'), + {:action => 'quote', :id => message}, + :remote => true, + :method => 'get', + :title => l(:button_quote)) if !@topic.locked? && authorize_for('messages', 'reply') %> + <%= link_to( + image_tag('edit.png'), + {:action => 'edit', :id => message}, + :title => l(:button_edit) + ) if message.editable_by?(User.current) %> + <%= link_to( + image_tag('delete.png'), + {:action => 'destroy', :id => message}, + :method => :post, + :data => {:confirm => l(:text_are_you_sure)}, + :title => l(:button_delete) + ) if message.destroyable_by?(User.current) %> +
    +

    + <%= avatar(message.author, :size => "24") %> + <%= link_to h(message.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => @topic, :r => message, :anchor => "message-#{message.id}" } %> + - + <%= authoring message.created_on, message.author %> +

    +
    <%= textilizable message, :content, :attachments => message.attachments %>
    + <%= link_to_attachments message, :author => false %> +
    +<% end %> +

    <%= pagination_links_full @reply_pages, @reply_count, :per_page_links => false %>

    +<% end %> + +<% if !@topic.locked? && authorize_for('messages', 'reply') %> +

    <%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %>

    + +<% end %> + +<% html_title @topic.subject %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/32/32c52319e33e839952a511178f3d013ae8279f9b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/32/32c52319e33e839952a511178f3d013ae8279f9b.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,85 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Helpers + + # Simple class to compute the start and end dates of a calendar + class Calendar + include Redmine::I18n + attr_reader :startdt, :enddt + + def initialize(date, lang = current_language, period = :month) + @date = date + @events = [] + @ending_events_by_days = {} + @starting_events_by_days = {} + set_language_if_valid lang + case period + when :month + @startdt = Date.civil(date.year, date.month, 1) + @enddt = (@startdt >> 1)-1 + # starts from the first day of the week + @startdt = @startdt - (@startdt.cwday - first_wday)%7 + # ends on the last day of the week + @enddt = @enddt + (last_wday - @enddt.cwday)%7 + when :week + @startdt = date - (date.cwday - first_wday)%7 + @enddt = date + (last_wday - date.cwday)%7 + else + raise 'Invalid period' + end + end + + # Sets calendar events + def events=(events) + @events = events + @ending_events_by_days = @events.group_by {|event| event.due_date} + @starting_events_by_days = @events.group_by {|event| event.start_date} + end + + # Returns events for the given day + def events_on(day) + ((@ending_events_by_days[day] || []) + (@starting_events_by_days[day] || [])).uniq + end + + # Calendar current month + def month + @date.month + end + + # Return the first day of week + # 1 = Monday ... 7 = Sunday + def first_wday + case Setting.start_of_week.to_i + when 1 + @first_dow ||= (1 - 1)%7 + 1 + when 6 + @first_dow ||= (6 - 1)%7 + 1 + when 7 + @first_dow ||= (7 - 1)%7 + 1 + else + @first_dow ||= (l(:general_first_day_of_week).to_i - 1)%7 + 1 + end + end + + def last_wday + @last_dow ||= (first_wday + 5)%7 + 1 + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/32/32d36ad9d8096df93d00980c0d2786c39a657ed2.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/32/32d36ad9d8096df93d00980c0d2786c39a657ed2.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,247 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module WikiFormatting + module Macros + module Definitions + # Returns true if +name+ is the name of an existing macro + def macro_exists?(name) + Redmine::WikiFormatting::Macros.available_macros.key?(name.to_sym) + end + + def exec_macro(name, obj, args, text) + macro_options = Redmine::WikiFormatting::Macros.available_macros[name.to_sym] + return unless macro_options + + method_name = "macro_#{name}" + unless macro_options[:parse_args] == false + args = args.split(',').map(&:strip) + end + + begin + if self.class.instance_method(method_name).arity == 3 + send(method_name, obj, args, text) + elsif text + raise "This macro does not accept a block of text" + else + send(method_name, obj, args) + end + rescue => e + "
    Error executing the #{h name} macro (#{h e.to_s})
    ".html_safe + end + end + + def extract_macro_options(args, *keys) + options = {} + while args.last.to_s.strip =~ %r{^(.+?)\=(.+)$} && keys.include?($1.downcase.to_sym) + options[$1.downcase.to_sym] = $2 + args.pop + end + return [args, options] + end + end + + @@available_macros = {} + mattr_accessor :available_macros + + class << self + # Plugins can use this method to define new macros: + # + # Redmine::WikiFormatting::Macros.register do + # desc "This is my macro" + # macro :my_macro do |obj, args| + # "My macro output" + # end + # + # desc "This is my macro that accepts a block of text" + # macro :my_macro do |obj, args, text| + # "My macro output" + # end + # end + def register(&block) + class_eval(&block) if block_given? + end + + # Defines a new macro with the given name, options and block. + # + # Options: + # * :desc - A description of the macro + # * :parse_args => false - Disables arguments parsing (the whole arguments + # string is passed to the macro) + # + # Macro blocks accept 2 or 3 arguments: + # * obj: the object that is rendered (eg. an Issue, a WikiContent...) + # * args: macro arguments + # * text: the block of text given to the macro (should be present only if the + # macro accepts a block of text). text is a String or nil if the macro is + # invoked without a block of text. + # + # Examples: + # By default, when the macro is invoked, the coma separated list of arguments + # is split and passed to the macro block as an array. If no argument is given + # the macro will be invoked with an empty array: + # + # macro :my_macro do |obj, args| + # # args is an array + # # and this macro do not accept a block of text + # end + # + # You can disable arguments spliting with the :parse_args => false option. In + # this case, the full string of arguments is passed to the macro: + # + # macro :my_macro, :parse_args => false do |obj, args| + # # args is a string + # end + # + # Macro can optionally accept a block of text: + # + # macro :my_macro do |obj, args, text| + # # this macro accepts a block of text + # end + # + # Macros are invoked in formatted text using double curly brackets. Arguments + # must be enclosed in parenthesis if any. A new line after the macro name or the + # arguments starts the block of text that will be passe to the macro (invoking + # a macro that do not accept a block of text with some text will fail). + # Examples: + # + # No arguments: + # {{my_macro}} + # + # With arguments: + # {{my_macro(arg1, arg2)}} + # + # With a block of text: + # {{my_macro + # multiple lines + # of text + # }} + # + # With arguments and a block of text + # {{my_macro(arg1, arg2) + # multiple lines + # of text + # }} + # + # If a block of text is given, the closing tag }} must be at the start of a new line. + def macro(name, options={}, &block) + options.assert_valid_keys(:desc, :parse_args) + unless name.to_s.match(/\A\w+\z/) + raise "Invalid macro name: #{name} (only 0-9, A-Z, a-z and _ characters are accepted)" + end + unless block_given? + raise "Can not create a macro without a block!" + end + name = name.to_s.downcase.to_sym + available_macros[name] = {:desc => @@desc || ''}.merge(options) + @@desc = nil + Definitions.send :define_method, "macro_#{name}", &block + end + + # Sets description for the next macro to be defined + def desc(txt) + @@desc = txt + end + end + + # Builtin macros + desc "Sample macro." + macro :hello_world do |obj, args, text| + h("Hello world! Object: #{obj.class.name}, " + + (args.empty? ? "Called with no argument" : "Arguments: #{args.join(', ')}") + + " and " + (text.present? ? "a #{text.size} bytes long block of text." : "no block of text.") + ) + end + + desc "Displays a list of all available macros, including description if available." + macro :macro_list do |obj, args| + out = ''.html_safe + @@available_macros.each do |macro, options| + out << content_tag('dt', content_tag('code', macro.to_s)) + out << content_tag('dd', textilizable(options[:desc])) + end + content_tag('dl', out) + end + + desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" + + " !{{child_pages}} -- can be used from a wiki page only\n" + + " !{{child_pages(depth=2)}} -- display 2 levels nesting only\n" + " !{{child_pages(Foo)}} -- lists all children of page Foo\n" + + " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo" + macro :child_pages do |obj, args| + args, options = extract_macro_options(args, :parent, :depth) + options[:depth] = options[:depth].to_i if options[:depth].present? + + page = nil + if args.size > 0 + page = Wiki.find_page(args.first.to_s, :project => @project) + elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version) + page = obj.page + else + raise 'With no argument, this macro can be called from wiki pages only.' + end + raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project) + pages = page.self_and_descendants(options[:depth]).group_by(&:parent_id) + render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id) + end + + desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}" + macro :include do |obj, args| + page = Wiki.find_page(args.first.to_s, :project => @project) + raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project) + @included_wiki_pages ||= [] + raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title) + @included_wiki_pages << page.title + out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false) + @included_wiki_pages.pop + out + end + + desc "Inserts of collapsed block of text. Example:\n\n {{collapse(View details...)\nThis is a block of text that is collapsed by default.\nIt can be expanded by clicking a link.\n}}" + macro :collapse do |obj, args, text| + html_id = "collapse-#{Redmine::Utils.random_hex(4)}" + show_label = args[0] || l(:button_show) + hide_label = args[1] || args[0] || l(:button_hide) + js = "$('##{html_id}-show, ##{html_id}-hide').toggle(); $('##{html_id}').fadeToggle(150);" + out = ''.html_safe + out << link_to_function(show_label, js, :id => "#{html_id}-show", :class => 'collapsible collapsed') + out << link_to_function(hide_label, js, :id => "#{html_id}-hide", :class => 'collapsible', :style => 'display:none;') + out << content_tag('div', textilizable(text, :object => obj), :id => html_id, :class => 'collapsed-text', :style => 'display:none;') + out + end + + desc "Displays a clickable thumbnail of an attached image. Examples:\n\n
    {{thumbnail(image.png)}}\n{{thumbnail(image.png, size=300, title=Thumbnail)}}
    " + macro :thumbnail do |obj, args| + args, options = extract_macro_options(args, :size, :title) + filename = args.first + raise 'Filename required' unless filename.present? + size = options[:size] + raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/) + size = size.to_i + size = nil unless size > 0 + if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename) + title = options[:title] || attachment.title + img = image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size), :alt => attachment.filename) + link_to(img, url_for(:controller => 'attachments', :action => 'show', :id => attachment), :class => 'thumbnail', :title => title) + else + raise "Attachment #{filename} not found" + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/33/330c3139ce88daeba1ec5a385dfead443884905c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/33/330c3139ce88daeba1ec5a385dfead443884905c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,81 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) +require 'mail_handler_controller' + +# Re-raise errors caught by the controller. +class MailHandlerController; def rescue_action(e) raise e end; end + +class MailHandlerControllerTest < ActionController::TestCase + fixtures :users, :projects, :enabled_modules, :roles, :members, :member_roles, :issues, :issue_statuses, + :trackers, :projects_trackers, :enumerations + + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' + + def setup + @controller = MailHandlerController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_should_create_issue + # Enable API and set a key + Setting.mail_handler_api_enabled = 1 + Setting.mail_handler_api_key = 'secret' + + assert_difference 'Issue.count' do + post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) + end + assert_response 201 + end + + def test_should_respond_with_422_if_not_created + Project.find('onlinestore').destroy + + Setting.mail_handler_api_enabled = 1 + Setting.mail_handler_api_key = 'secret' + + assert_no_difference 'Issue.count' do + post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) + end + assert_response 422 + end + + def test_should_not_allow_with_api_disabled + # Disable API + Setting.mail_handler_api_enabled = 0 + Setting.mail_handler_api_key = 'secret' + + assert_no_difference 'Issue.count' do + post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) + end + assert_response 403 + end + + def test_should_not_allow_with_wrong_key + # Disable API + Setting.mail_handler_api_enabled = 1 + Setting.mail_handler_api_key = 'secret' + + assert_no_difference 'Issue.count' do + post :index, :key => 'wrong', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) + end + assert_response 403 + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/33/332d3d52b592a1156755d111d7a6b0f8eac58495.svn-base --- a/.svn/pristine/33/332d3d52b592a1156755d111d7a6b0f8eac58495.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,83 +0,0 @@ -<%= error_messages_for 'member' %> -<% roles = Role.find_all_givable - members = @project.member_principals.find(:all, :include => [:roles, :principal]).sort %> - -
    -<% if members.any? %> - - - - - - <%= call_hook(:view_projects_settings_members_table_header, :project => @project) %> - - - <% members.each do |member| %> - <% next if member.new_record? %> - - - - - <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %> - -<% end; reset_cycle %> - -
    <%= l(:label_user) %> / <%= l(:label_group) %><%= l(:label_role_plural) %>
    <%= link_to_user member.principal %> - <%=h member.roles.sort.collect(&:to_s).join(', ') %> - <% if authorize_for('members', 'edit') %> - <% remote_form_for(:member, member, :url => {:controller => 'members', :action => 'edit', :id => member}, - :method => :post, - :html => { :id => "member-#{member.id}-roles-form", :class => 'hol' }) do |f| %> -

    <% roles.each do |role| %> -
    - <% end %>

    - <%= hidden_field_tag 'member[role_ids][]', '' %> -

    <%= submit_tag l(:button_change), :class => "small" %> - <%= link_to_function l(:button_cancel), "$('member-#{member.id}-roles').show(); $('member-#{member.id}-roles-form').hide(); return false;" %>

    - <% end %> - <% end %> -
    - <%= link_to_function l(:button_edit), "$('member-#{member.id}-roles').hide(); $('member-#{member.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %> - <%= link_to_remote(l(:button_delete), { :url => {:controller => 'members', :action => 'destroy', :id => member}, - :method => :post, - :confirm => (!User.current.admin? && member.include?(User.current) ? l(:text_own_membership_delete_confirmation) : nil) - }, :title => l(:button_delete), - :class => 'icon icon-del') if member.deletable? %> -
    -<% else %> -

    <%= l(:label_no_data) %>

    -<% end %> -
    - -<% principals = Principal.active.find(:all, :limit => 100, :order => 'type, login, lastname ASC') - @project.principals %> - -
    -<% if roles.any? && principals.any? %> - <% remote_form_for(:member, @member, :url => {:controller => 'members', :action => 'new', :id => @project}, :method => :post, - :loading => '$(\'member-add-submit\').disable();', - :complete => 'if($(\'member-add-submit\')) $(\'member-add-submit\').enable();') do |f| %> -
    <%=l(:label_member_new)%> - -

    <%= label_tag "principal_search", l(:label_principal_search) %><%= text_field_tag 'principal_search', nil %>

    - <%= observe_field(:principal_search, - :frequency => 0.5, - :update => :principals, - :url => { :controller => 'members', :action => 'autocomplete_for_member', :id => @project }, - :with => 'q') - %> - -
    - <%= principals_check_box_tags 'member[user_ids][]', principals %> -
    - -

    <%= l(:label_role_plural) %>: - <% roles.each do |role| %> - - <% end %>

    - -

    <%= submit_tag l(:button_add), :id => 'member-add-submit' %>

    -
    - <% end %> -<% end %> -
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/33/33518d1c584baba3aa48b3bfa72c87ee64c3e83d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/33/33518d1c584baba3aa48b3bfa72c87ee64c3e83d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,20 @@ +<%= call_hook(:view_repositories_show_contextual, { :repository => @repository, :project => @project }) %> + +
    + <%= render :partial => 'navigation' %> +
    + +

    <%= render :partial => 'breadcrumbs', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %>

    + +<%= render :partial => 'link_to_functions' %> + +<%= render_properties(@properties) %> + +<%= render(:partial => 'revisions', + :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }) unless @changesets.empty? %> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> + +<% html_title(l(:label_change_plural)) -%> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/33/3364851cb2b94990315933534000d4b88ef31e3b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/33/3364851cb2b94990315933534000d4b88ef31e3b.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,34 @@ +top_level: + id: 1 + name: Top Level + lft: 1 + rgt: 10 +child_1: + id: 2 + name: Child 1 + parent_id: 1 + lft: 2 + rgt: 3 +child_2: + id: 3 + name: Child 2 + parent_id: 1 + lft: 4 + rgt: 7 +child_2_1: + id: 4 + name: Child 2.1 + parent_id: 3 + lft: 5 + rgt: 6 +child_3: + id: 5 + name: Child 3 + parent_id: 1 + lft: 8 + rgt: 9 +top_level_2: + id: 6 + name: Top Level 2 + lft: 11 + rgt: 12 diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/33/3364d2e2a94809de51cfbc8bdc61808015e64cb9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/33/3364d2e2a94809de51cfbc8bdc61808015e64cb9.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1104 @@ +pt-BR: + direction: ltr + date: + formats: + default: "%d/%m/%Y" + short: "%d de %B" + long: "%d de %B de %Y" + only_day: "%d" + + day_names: [Domingo, Segunda, Terça, Quarta, Quinta, Sexta, Sábado] + abbr_day_names: [Dom, Seg, Ter, Qua, Qui, Sex, Sáb] + month_names: [~, Janeiro, Fevereiro, Março, Abril, Maio, Junho, Julho, Agosto, Setembro, Outubro, Novembro, Dezembro] + abbr_month_names: [~, Jan, Fev, Mar, Abr, Mai, Jun, Jul, Ago, Set, Out, Nov, Dez] + order: + - :day + - :month + - :year + + time: + formats: + default: "%A, %d de %B de %Y, %H:%M h" + time: "%H:%M h" + short: "%d/%m, %H:%M h" + long: "%A, %d de %B de %Y, %H:%M h" + only_second: "%S" + datetime: + formats: + default: "%Y-%m-%dT%H:%M:%S%Z" + am: '' + pm: '' + + # date helper distancia em palavras + datetime: + distance_in_words: + half_a_minute: 'meio minuto' + less_than_x_seconds: + one: 'menos de 1 segundo' + other: 'menos de %{count} segundos' + + x_seconds: + one: '1 segundo' + other: '%{count} segundos' + + less_than_x_minutes: + one: 'menos de um minuto' + other: 'menos de %{count} minutos' + + x_minutes: + one: '1 minuto' + other: '%{count} minutos' + + about_x_hours: + one: 'aproximadamente 1 hora' + other: 'aproximadamente %{count} horas' + x_hours: + one: "1 hour" + other: "%{count} hours" + + x_days: + one: '1 dia' + other: '%{count} dias' + + about_x_months: + one: 'aproximadamente 1 mês' + other: 'aproximadamente %{count} meses' + + x_months: + one: '1 mês' + other: '%{count} meses' + + about_x_years: + one: 'aproximadamente 1 ano' + other: 'aproximadamente %{count} anos' + + over_x_years: + one: 'mais de 1 ano' + other: 'mais de %{count} anos' + almost_x_years: + one: "almost 1 year" + other: "almost %{count} years" + + # numeros + number: + format: + precision: 3 + separator: ',' + delimiter: '.' + currency: + format: + unit: 'R$' + precision: 2 + format: '%u %n' + separator: ',' + delimiter: '.' + percentage: + format: + delimiter: '.' + precision: + format: + delimiter: '.' + human: + format: + precision: 3 + delimiter: '.' + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + support: + array: + sentence_connector: "e" + skip_last_comma: true + + # Active Record + activerecord: + errors: + template: + header: + one: "model não pode ser salvo: 1 erro" + other: "model não pode ser salvo: %{count} erros." + body: "Por favor, verifique os seguintes campos:" + messages: + inclusion: "não está incluso na lista" + exclusion: "não está disponível" + invalid: "não é válido" + confirmation: "não está de acordo com a confirmação" + accepted: "precisa ser aceito" + empty: "não pode ficar vazio" + blank: "não pode ficar vazio" + too_long: "é muito longo (máximo: %{count} caracteres)" + too_short: "é muito curto (mínimon: %{count} caracteres)" + wrong_length: "deve ter %{count} caracteres" + taken: "não está disponível" + not_a_number: "não é um número" + greater_than: "precisa ser maior do que %{count}" + greater_than_or_equal_to: "precisa ser maior ou igual a %{count}" + equal_to: "precisa ser igual a %{count}" + less_than: "precisa ser menor do que %{count}" + less_than_or_equal_to: "precisa ser menor ou igual a %{count}" + odd: "precisa ser ímpar" + even: "precisa ser par" + greater_than_start_date: "deve ser maior que a data inicial" + not_same_project: "não pertence ao mesmo projeto" + circular_dependency: "Esta relação geraria uma dependência circular" + cant_link_an_issue_with_a_descendant: "Uma tarefa não pode ser relaciona a uma de suas subtarefas" + + actionview_instancetag_blank_option: Selecione + + general_text_No: 'Não' + general_text_Yes: 'Sim' + general_text_no: 'não' + general_text_yes: 'sim' + general_lang_name: 'Português(Brasil)' + general_csv_separator: ';' + general_csv_decimal_separator: ',' + general_csv_encoding: ISO-8859-1 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: Conta atualizada com sucesso. + notice_account_invalid_creditentials: Usuário ou senha inválido. + notice_account_password_updated: Senha alterada com sucesso. + notice_account_wrong_password: Senha inválida. + notice_account_register_done: Conta criada com sucesso. Para ativar sua conta, clique no link que lhe foi enviado por e-mail. + notice_account_unknown_email: Usuário desconhecido. + notice_can_t_change_password: Esta conta utiliza autenticação externa. Não é possível alterar a senha. + notice_account_lost_email_sent: Um e-mail com instruções para escolher uma nova senha foi enviado para você. + notice_account_activated: Sua conta foi ativada. Você pode acessá-la agora. + notice_successful_create: Criado com sucesso. + notice_successful_update: Alterado com sucesso. + notice_successful_delete: Excluído com sucesso. + notice_successful_connection: Conectado com sucesso. + notice_file_not_found: A página que você está tentando acessar não existe ou foi excluída. + notice_locking_conflict: Os dados foram atualizados por outro usuário. + notice_not_authorized: Você não está autorizado a acessar esta página. + notice_email_sent: "Um e-mail foi enviado para %{value}" + notice_email_error: "Ocorreu um erro ao enviar o e-mail (%{value})" + notice_feeds_access_key_reseted: Sua chave RSS foi reconfigurada. + notice_failed_to_save_issues: "Problema ao salvar %{count} tarefa(s) de %{total} selecionadas: %{ids}." + notice_no_issue_selected: "Nenhuma tarefa selecionada! Por favor, marque as tarefas que você deseja editar." + notice_account_pending: "Sua conta foi criada e está aguardando aprovação do administrador." + notice_default_data_loaded: Configuração padrão carregada com sucesso. + + error_can_t_load_default_data: "A configuração padrão não pode ser carregada: %{value}" + error_scm_not_found: "A entrada e/ou a revisão não existe no repositório." + error_scm_command_failed: "Ocorreu um erro ao tentar acessar o repositório: %{value}" + error_scm_annotate: "Esta entrada não existe ou não pode ser anotada." + error_issue_not_found_in_project: 'A tarefa não foi encontrada ou não pertence a este projeto' + error_no_tracker_in_project: 'Não há um tipo de tarefa associado a este projeto. Favor verificar as configurações do projeto.' + error_no_default_issue_status: 'A situação padrão para tarefa não está definida. Favor verificar sua configuração (Vá em "Administração -> Situação da tarefa").' + + mail_subject_lost_password: "Sua senha do %{value}." + mail_body_lost_password: 'Para mudar sua senha, clique no link abaixo:' + mail_subject_register: "Ativação de conta do %{value}." + mail_body_register: 'Para ativar sua conta, clique no link abaixo:' + mail_body_account_information_external: "Você pode usar sua conta do %{value} para entrar." + mail_body_account_information: Informações sobre sua conta + mail_subject_account_activation_request: "%{value} - Requisição de ativação de conta" + mail_body_account_activation_request: "Um novo usuário (%{value}) se registrou. A conta está aguardando sua aprovação:" + mail_subject_reminder: "%{count} tarefa(s) com data prevista para os próximos %{days} dias" + mail_body_reminder: "%{count} tarefa(s) para você com data prevista para os próximos %{days} dias:" + + gui_validation_error: 1 erro + gui_validation_error_plural: "%{count} erros" + + field_name: Nome + field_description: Descrição + field_summary: Resumo + field_is_required: Obrigatório + field_firstname: Nome + field_lastname: Sobrenome + field_mail: E-mail + field_filename: Arquivo + field_filesize: Tamanho + field_downloads: Downloads + field_author: Autor + field_created_on: Criado em + field_updated_on: Alterado em + field_field_format: Formato + field_is_for_all: Para todos os projetos + field_possible_values: Possíveis valores + field_regexp: Expressão regular + field_min_length: Tamanho mínimo + field_max_length: Tamanho máximo + field_value: Valor + field_category: Categoria + field_title: Título + field_project: Projeto + field_issue: Tarefa + field_status: Situação + field_notes: Notas + field_is_closed: Tarefa fechada + field_is_default: Situação padrão + field_tracker: Tipo + field_subject: Título + field_due_date: Data prevista + field_assigned_to: Atribuído para + field_priority: Prioridade + field_fixed_version: Versão + field_user: Usuário + field_role: Cargo + field_homepage: Página do projeto + field_is_public: Público + field_parent: Sub-projeto de + field_is_in_roadmap: Exibir no planejamento + field_login: Usuário + field_mail_notification: Notificações por e-mail + field_admin: Administrador + field_last_login_on: Última conexão + field_language: Idioma + field_effective_date: Data + field_password: Senha + field_new_password: Nova senha + field_password_confirmation: Confirmação + field_version: Versão + field_type: Tipo + field_host: Servidor + field_port: Porta + field_account: Conta + field_base_dn: DN Base + field_attr_login: Atributo para nome de usuário + field_attr_firstname: Atributo para nome + field_attr_lastname: Atributo para sobrenome + field_attr_mail: Atributo para e-mail + field_onthefly: Criar usuários dinamicamente ("on-the-fly") + field_start_date: Início + field_done_ratio: "% Terminado" + field_auth_source: Modo de autenticação + field_hide_mail: Ocultar meu e-mail + field_comments: Comentário + field_url: URL + field_start_page: Página inicial + field_subproject: Sub-projeto + field_hours: Horas + field_activity: Atividade + field_spent_on: Data + field_identifier: Identificador + field_is_filter: É um filtro + field_issue_to: Tarefa relacionada + field_delay: Atraso + field_assignable: Tarefas podem ser atribuídas a este papel + field_redirect_existing_links: Redirecionar links existentes + field_estimated_hours: Tempo estimado + field_column_names: Colunas + field_time_zone: Fuso-horário + field_searchable: Pesquisável + field_default_value: Padrão + field_comments_sorting: Visualizar comentários + field_parent_title: Página pai + + setting_app_title: Título da aplicação + setting_app_subtitle: Sub-título da aplicação + setting_welcome_text: Texto de boas-vindas + setting_default_language: Idioma padrão + setting_login_required: Exigir autenticação + setting_self_registration: Permitido Auto-registro + setting_attachment_max_size: Tamanho máximo do anexo + setting_issues_export_limit: Limite de exportação das tarefas + setting_mail_from: E-mail enviado de + setting_bcc_recipients: Enviar com cópia oculta (cco) + setting_host_name: Nome do Servidor e subdomínio + setting_text_formatting: Formatação do texto + setting_wiki_compression: Compactação de histórico do Wiki + setting_feeds_limit: Número de registros por Feed + setting_default_projects_public: Novos projetos são públicos por padrão + setting_autofetch_changesets: Obter commits automaticamente + setting_sys_api_enabled: Ativa WS para gerenciamento do repositório (SVN) + setting_commit_ref_keywords: Palavras de referência + setting_commit_fix_keywords: Palavras de fechamento + setting_autologin: Auto-login + setting_date_format: Formato da data + setting_time_format: Formato de hora + setting_cross_project_issue_relations: Permitir relacionar tarefas entre projetos + setting_issue_list_default_columns: Colunas padrão visíveis na lista de tarefas + setting_emails_footer: Rodapé do e-mail + setting_protocol: Protocolo + setting_per_page_options: Número de itens exibidos por página + setting_user_format: Formato de exibição de nome de usuário + setting_activity_days_default: Dias visualizados na atividade do projeto + setting_display_subprojects_issues: Visualizar tarefas dos subprojetos nos projetos principais por padrão + setting_enabled_scm: SCM habilitados + setting_mail_handler_api_enabled: Habilitar WS para e-mails de entrada + setting_mail_handler_api_key: Chave de API + setting_sequential_project_identifiers: Gerar identificadores sequenciais de projeto + + project_module_issue_tracking: Gerenciamento de Tarefas + project_module_time_tracking: Gerenciamento de tempo + project_module_news: Notícias + project_module_documents: Documentos + project_module_files: Arquivos + project_module_wiki: Wiki + project_module_repository: Repositório + project_module_boards: Fóruns + + label_user: Usuário + label_user_plural: Usuários + label_user_new: Novo usuário + label_project: Projeto + label_project_new: Novo projeto + label_project_plural: Projetos + label_x_projects: + zero: nenhum projeto + one: 1 projeto + other: "%{count} projetos" + label_project_all: Todos os projetos + label_project_latest: Últimos projetos + label_issue: Tarefa + label_issue_new: Nova tarefa + label_issue_plural: Tarefas + label_issue_view_all: Ver todas as tarefas + label_issues_by: "Tarefas por %{value}" + label_issue_added: Tarefa adicionada + label_issue_updated: Tarefa atualizada + label_issue_note_added: Nota adicionada + label_issue_status_updated: Situação atualizada + label_issue_priority_updated: Prioridade atualizada + label_document: Documento + label_document_new: Novo documento + label_document_plural: Documentos + label_document_added: Documento adicionado + label_role: Papel + label_role_plural: Papéis + label_role_new: Novo papel + label_role_and_permissions: Papéis e permissões + label_member: Membro + label_member_new: Novo membro + label_member_plural: Membros + label_tracker: Tipo de tarefa + label_tracker_plural: Tipos de tarefas + label_tracker_new: Novo tipo + label_workflow: Fluxo de trabalho + label_issue_status: Situação da tarefa + label_issue_status_plural: Situação das tarefas + label_issue_status_new: Nova situação + label_issue_category: Categoria da tarefa + label_issue_category_plural: Categorias das tarefas + label_issue_category_new: Nova categoria + label_custom_field: Campo personalizado + label_custom_field_plural: Campos personalizados + label_custom_field_new: Novo campo personalizado + label_enumerations: 'Tipos & Categorias' + label_enumeration_new: Novo + label_information: Informação + label_information_plural: Informações + label_please_login: Efetue o login + label_register: Cadastre-se + label_password_lost: Perdi minha senha + label_home: Página inicial + label_my_page: Minha página + label_my_account: Minha conta + label_my_projects: Meus projetos + label_administration: Administração + label_login: Entrar + label_logout: Sair + label_help: Ajuda + label_reported_issues: Tarefas reportadas + label_assigned_to_me_issues: Minhas tarefas + label_last_login: Última conexão + label_registered_on: Registrado em + label_activity: Atividade + label_overall_activity: Atividades gerais + label_new: Novo + label_logged_as: "Acessando como:" + label_environment: Ambiente + label_authentication: Autenticação + label_auth_source: Modo de autenticação + label_auth_source_new: Novo modo de autenticação + label_auth_source_plural: Modos de autenticação + label_subproject_plural: Sub-projetos + label_and_its_subprojects: "%{value} e seus sub-projetos" + label_min_max_length: Tamanho mín-máx + label_list: Lista + label_date: Data + label_integer: Inteiro + label_float: Decimal + label_boolean: Boleano + label_string: Texto + label_text: Texto longo + label_attribute: Atributo + label_attribute_plural: Atributos + label_download: "%{count} Download" + label_download_plural: "%{count} Downloads" + label_no_data: Nenhuma informação disponível + label_change_status: Alterar situação + label_history: Histórico + label_attachment: Arquivo + label_attachment_new: Novo arquivo + label_attachment_delete: Excluir arquivo + label_attachment_plural: Arquivos + label_file_added: Arquivo adicionado + label_report: Relatório + label_report_plural: Relatório + label_news: Notícia + label_news_new: Adicionar notícia + label_news_plural: Notícias + label_news_latest: Últimas notícias + label_news_view_all: Ver todas as notícias + label_news_added: Notícia adicionada + label_settings: Configurações + label_overview: Visão geral + label_version: Versão + label_version_new: Nova versão + label_version_plural: Versões + label_confirmation: Confirmação + label_export_to: Exportar para + label_read: Ler... + label_public_projects: Projetos públicos + label_open_issues: Aberta + label_open_issues_plural: Abertas + label_closed_issues: Fechada + label_closed_issues_plural: Fechadas + label_x_open_issues_abbr_on_total: + zero: 0 aberta / %{total} + one: 1 aberta / %{total} + other: "%{count} abertas / %{total}" + label_x_open_issues_abbr: + zero: 0 aberta + one: 1 aberta + other: "%{count} abertas" + label_x_closed_issues_abbr: + zero: 0 fechada + one: 1 fechada + other: "%{count} fechadas" + label_total: Total + label_permissions: Permissões + label_current_status: Situação atual + label_new_statuses_allowed: Nova situação permitida + label_all: todos + label_none: nenhum + label_nobody: ninguém + label_next: Próximo + label_previous: Anterior + label_used_by: Usado por + label_details: Detalhes + label_add_note: Adicionar nota + label_per_page: Por página + label_calendar: Calendário + label_months_from: meses a partir de + label_gantt: Gantt + label_internal: Interno + label_last_changes: "últimas %{count} alterações" + label_change_view_all: Mostrar todas as alterações + label_personalize_page: Personalizar esta página + label_comment: Comentário + label_comment_plural: Comentários + label_x_comments: + zero: nenhum comentário + one: 1 comentário + other: "%{count} comentários" + label_comment_add: Adicionar comentário + label_comment_added: Comentário adicionado + label_comment_delete: Excluir comentário + label_query: Consulta personalizada + label_query_plural: Consultas personalizadas + label_query_new: Nova consulta + label_filter_add: Adicionar filtro + label_filter_plural: Filtros + label_equals: igual a + label_not_equals: diferente de + label_in_less_than: maior que + label_in_more_than: menor que + label_in: em + label_today: hoje + label_all_time: tudo + label_yesterday: ontem + label_this_week: esta semana + label_last_week: última semana + label_last_n_days: "últimos %{count} dias" + label_this_month: este mês + label_last_month: último mês + label_this_year: este ano + label_date_range: Período + label_less_than_ago: menos de + label_more_than_ago: mais de + label_ago: dias atrás + label_contains: contém + label_not_contains: não contém + label_day_plural: dias + label_repository: Repositório + label_repository_plural: Repositórios + label_browse: Procurar + label_modification: "%{count} alteração" + label_modification_plural: "%{count} alterações" + label_revision: Revisão + label_revision_plural: Revisões + label_associated_revisions: Revisões associadas + label_added: adicionada + label_modified: alterada + label_deleted: excluída + label_latest_revision: Última revisão + label_latest_revision_plural: Últimas revisões + label_view_revisions: Ver revisões + label_max_size: Tamanho máximo + label_sort_highest: Mover para o início + label_sort_higher: Mover para cima + label_sort_lower: Mover para baixo + label_sort_lowest: Mover para o fim + label_roadmap: Planejamento + label_roadmap_due_in: "Previsto para %{value}" + label_roadmap_overdue: "%{value} atrasado" + label_roadmap_no_issues: Sem tarefas para esta versão + label_search: Busca + label_result_plural: Resultados + label_all_words: Todas as palavras + label_wiki: Wiki + label_wiki_edit: Editar Wiki + label_wiki_edit_plural: Edições Wiki + label_wiki_page: Página Wiki + label_wiki_page_plural: páginas Wiki + label_index_by_title: Ãndice por título + label_index_by_date: Ãndice por data + label_current_version: Versão atual + label_preview: Pré-visualizar + label_feed_plural: Feeds + label_changes_details: Detalhes de todas as alterações + label_issue_tracking: Tarefas + label_spent_time: Tempo gasto + label_f_hour: "%{value} hora" + label_f_hour_plural: "%{value} horas" + label_time_tracking: Registro de horas + label_change_plural: Alterações + label_statistics: Estatísticas + label_commits_per_month: Commits por mês + label_commits_per_author: Commits por autor + label_view_diff: Ver diferenças + label_diff_inline: inline + label_diff_side_by_side: lado a lado + label_options: Opções + label_copy_workflow_from: Copiar fluxo de trabalho de + label_permissions_report: Relatório de permissões + label_watched_issues: Tarefas observadas + label_related_issues: Tarefas relacionadas + label_applied_status: Situação alterada + label_loading: Carregando... + label_relation_new: Nova relação + label_relation_delete: Excluir relação + label_relates_to: relacionado a + label_duplicates: duplica + label_duplicated_by: duplicado por + label_blocks: bloqueia + label_blocked_by: bloqueado por + label_precedes: precede + label_follows: segue + label_end_to_start: fim para o início + label_end_to_end: fim para fim + label_start_to_start: início para início + label_start_to_end: início para fim + label_stay_logged_in: Permanecer logado + label_disabled: desabilitado + label_show_completed_versions: Exibir versões completas + label_me: mim + label_board: Fórum + label_board_new: Novo fórum + label_board_plural: Fóruns + label_topic_plural: Tópicos + label_message_plural: Mensagens + label_message_last: Última mensagem + label_message_new: Nova mensagem + label_message_posted: Mensagem enviada + label_reply_plural: Respostas + label_send_information: Enviar informação da nova conta para o usuário + label_year: Ano + label_month: Mês + label_week: Semana + label_date_from: De + label_date_to: Para + label_language_based: Com base no idioma do usuário + label_sort_by: "Ordenar por %{value}" + label_send_test_email: Enviar um e-mail de teste + label_feeds_access_key_created_on: "chave de acesso RSS criada %{value} atrás" + label_module_plural: Módulos + label_added_time_by: "Adicionado por %{author} %{age} atrás" + label_updated_time: "Atualizado %{value} atrás" + label_jump_to_a_project: Ir para o projeto... + label_file_plural: Arquivos + label_changeset_plural: Changesets + label_default_columns: Colunas padrão + label_no_change_option: (Sem alteração) + label_bulk_edit_selected_issues: Edição em massa das tarefas selecionados. + label_theme: Tema + label_default: Padrão + label_search_titles_only: Pesquisar somente títulos + label_user_mail_option_all: "Para qualquer evento em todos os meus projetos" + label_user_mail_option_selected: "Para qualquer evento somente no(s) projeto(s) selecionado(s)..." + label_user_mail_no_self_notified: "Eu não quero ser notificado de minhas próprias modificações" + label_registration_activation_by_email: ativação de conta por e-mail + label_registration_manual_activation: ativação manual de conta + label_registration_automatic_activation: ativação automática de conta + label_display_per_page: "Por página: %{value}" + label_age: Idade + label_change_properties: Alterar propriedades + label_general: Geral + label_more: Mais + label_scm: 'Controle de versão:' + label_plugins: Plugins + label_ldap_authentication: Autenticação LDAP + label_downloads_abbr: D/L + label_optional_description: Descrição opcional + label_add_another_file: Adicionar outro arquivo + label_preferences: Preferências + label_chronological_order: Em ordem cronológica + label_reverse_chronological_order: Em ordem cronológica inversa + label_planning: Planejamento + label_incoming_emails: E-mails recebidos + label_generate_key: Gerar uma chave + label_issue_watchers: Observadores + + button_login: Entrar + button_submit: Enviar + button_save: Salvar + button_check_all: Marcar todos + button_uncheck_all: Desmarcar todos + button_delete: Excluir + button_create: Criar + button_test: Testar + button_edit: Editar + button_add: Adicionar + button_change: Alterar + button_apply: Aplicar + button_clear: Limpar + button_lock: Bloquear + button_unlock: Desbloquear + button_download: Baixar + button_list: Listar + button_view: Ver + button_move: Mover + button_back: Voltar + button_cancel: Cancelar + button_activate: Ativar + button_sort: Ordenar + button_log_time: Tempo de trabalho + button_rollback: Voltar para esta versão + button_watch: Observar + button_unwatch: Parar de observar + button_reply: Responder + button_archive: Arquivar + button_unarchive: Desarquivar + button_reset: Redefinir + button_rename: Renomear + button_change_password: Alterar senha + button_copy: Copiar + button_annotate: Anotar + button_update: Atualizar + button_configure: Configurar + button_quote: Responder + + status_active: ativo + status_registered: registrado + status_locked: bloqueado + + text_select_mail_notifications: Ações a serem notificadas por e-mail + text_regexp_info: ex. ^[A-Z0-9]+$ + text_min_max_length_info: 0 = sem restrição + text_project_destroy_confirmation: Você tem certeza que deseja excluir este projeto e todos os dados relacionados? + text_subprojects_destroy_warning: "Seu(s) subprojeto(s): %{value} também serão excluídos." + text_workflow_edit: Selecione um papel e um tipo de tarefa para editar o fluxo de trabalho + text_are_you_sure: Você tem certeza? + text_tip_issue_begin_day: tarefa inicia neste dia + text_tip_issue_end_day: tarefa termina neste dia + text_tip_issue_begin_end_day: tarefa inicia e termina neste dia + text_caracters_maximum: "máximo %{count} caracteres" + text_caracters_minimum: "deve ter ao menos %{count} caracteres." + text_length_between: "deve ter entre %{min} e %{max} caracteres." + text_tracker_no_workflow: Sem fluxo de trabalho definido para este tipo. + text_unallowed_characters: Caracteres não permitidos + text_comma_separated: Múltiplos valores são permitidos (separados por vírgula). + text_issues_ref_in_commit_messages: Referenciando tarefas nas mensagens de commit + text_issue_added: "Tarefa %{id} incluída (por %{author})." + text_issue_updated: "Tarefa %{id} alterada (por %{author})." + text_wiki_destroy_confirmation: Você tem certeza que deseja excluir este wiki e TODO o seu conteúdo? + text_issue_category_destroy_question: "Algumas tarefas (%{count}) estão atribuídas a esta categoria. O que você deseja fazer?" + text_issue_category_destroy_assignments: Remover atribuições da categoria + text_issue_category_reassign_to: Redefinir tarefas para esta categoria + text_user_mail_option: "Para projetos (não selecionados), você somente receberá notificações sobre o que você está observando ou está envolvido (ex. tarefas das quais você é o autor ou que estão atribuídas a você)" + text_no_configuration_data: "Os Papéis, tipos de tarefas, situação de tarefas e fluxos de trabalho não foram configurados ainda.\nÉ altamente recomendado carregar as configurações padrão. Você poderá modificar estas configurações assim que carregadas." + text_load_default_configuration: Carregar a configuração padrão + text_status_changed_by_changeset: "Aplicado no changeset %{value}." + text_issues_destroy_confirmation: 'Você tem certeza que deseja excluir a(s) tarefa(s) selecionada(s)?' + text_select_project_modules: 'Selecione módulos para habilitar para este projeto:' + text_default_administrator_account_changed: Conta padrão do administrador alterada + text_file_repository_writable: Repositório com permissão de escrita + text_rmagick_available: RMagick disponível (opcional) + text_destroy_time_entries_question: "%{hours} horas de trabalho foram registradas nas tarefas que você está excluindo. O que você deseja fazer?" + text_destroy_time_entries: Excluir horas de trabalho + text_assign_time_entries_to_project: Atribuir estas horas de trabalho para outro projeto + text_reassign_time_entries: 'Atribuir horas reportadas para esta tarefa:' + text_user_wrote: "%{value} escreveu:" + text_enumeration_destroy_question: "%{count} objetos estão atribuídos a este valor." + text_enumeration_category_reassign_to: 'Reatribuí-los ao valor:' + text_email_delivery_not_configured: "O envio de e-mail não está configurado, e as notificações estão inativas.\nConfigure seu servidor SMTP no arquivo config/configuration.yml e reinicie a aplicação para ativá-las." + + default_role_manager: Gerente + default_role_developer: Desenvolvedor + default_role_reporter: Informante + default_tracker_bug: Defeito + default_tracker_feature: Funcionalidade + default_tracker_support: Suporte + default_issue_status_new: Nova + default_issue_status_in_progress: Em andamento + default_issue_status_resolved: Resolvida + default_issue_status_feedback: Feedback + default_issue_status_closed: Fechada + default_issue_status_rejected: Rejeitada + default_doc_category_user: Documentação do usuário + default_doc_category_tech: Documentação técnica + default_priority_low: Baixa + default_priority_normal: Normal + default_priority_high: Alta + default_priority_urgent: Urgente + default_priority_immediate: Imediata + default_activity_design: Design + default_activity_development: Desenvolvimento + + enumeration_issue_priorities: Prioridade das tarefas + enumeration_doc_categories: Categorias de documento + enumeration_activities: Atividades (registro de horas) + notice_unable_delete_version: Não foi possível excluir a versão + label_renamed: renomeado + label_copied: copiado + setting_plain_text_mail: Usar mensagem sem formatação HTML + permission_view_files: Ver arquivos + permission_edit_issues: Editar tarefas + permission_edit_own_time_entries: Editar o próprio tempo de trabalho + permission_manage_public_queries: Gerenciar consultas publicas + permission_add_issues: Adicionar tarefas + permission_log_time: Adicionar tempo gasto + permission_view_changesets: Ver changesets + permission_view_time_entries: Ver tempo gasto + permission_manage_versions: Gerenciar versões + permission_manage_wiki: Gerenciar wiki + permission_manage_categories: Gerenciar categorias de tarefas + permission_protect_wiki_pages: Proteger páginas wiki + permission_comment_news: Comentar notícias + permission_delete_messages: Excluir mensagens + permission_select_project_modules: Selecionar módulos de projeto + permission_manage_documents: Gerenciar documentos + permission_edit_wiki_pages: Editar páginas wiki + permission_add_issue_watchers: Adicionar observadores + permission_view_gantt: Ver gráfico gantt + permission_move_issues: Mover tarefas + permission_manage_issue_relations: Gerenciar relacionamentos de tarefas + permission_delete_wiki_pages: Excluir páginas wiki + permission_manage_boards: Gerenciar fóruns + permission_delete_wiki_pages_attachments: Excluir anexos + permission_view_wiki_edits: Ver histórico do wiki + permission_add_messages: Postar mensagens + permission_view_messages: Ver mensagens + permission_manage_files: Gerenciar arquivos + permission_edit_issue_notes: Editar notas + permission_manage_news: Gerenciar notícias + permission_view_calendar: Ver calendário + permission_manage_members: Gerenciar membros + permission_edit_messages: Editar mensagens + permission_delete_issues: Excluir tarefas + permission_view_issue_watchers: Ver lista de observadores + permission_manage_repository: Gerenciar repositório + permission_commit_access: Acesso de commit + permission_browse_repository: Pesquisar repositório + permission_view_documents: Ver documentos + permission_edit_project: Editar projeto + permission_add_issue_notes: Adicionar notas + permission_save_queries: Salvar consultas + permission_view_wiki_pages: Ver wiki + permission_rename_wiki_pages: Renomear páginas wiki + permission_edit_time_entries: Editar tempo gasto + permission_edit_own_issue_notes: Editar suas próprias notas + setting_gravatar_enabled: Usar ícones do Gravatar + label_example: Exemplo + text_repository_usernames_mapping: "Seleciona ou atualiza os usuários do Redmine mapeando para cada usuário encontrado no log do repositório.\nUsuários com o mesmo login ou e-mail no Redmine e no repositório serão mapeados automaticamente." + permission_edit_own_messages: Editar próprias mensagens + permission_delete_own_messages: Excluir próprias mensagens + label_user_activity: "Atividade de %{value}" + label_updated_time_by: "Atualizado por %{author} há %{age}" + text_diff_truncated: '... Este diff foi truncado porque excede o tamanho máximo que pode ser exibido.' + setting_diff_max_lines_displayed: Número máximo de linhas exibidas no diff + text_plugin_assets_writable: Diretório de plugins gravável + warning_attachments_not_saved: "%{count} arquivo(s) não puderam ser salvo(s)." + button_create_and_continue: Criar e continuar + text_custom_field_possible_values_info: 'Uma linha para cada valor' + label_display: Exibição + field_editable: Editável + setting_repository_log_display_limit: Número máximo de revisões exibidas no arquivo de log + setting_file_max_size_displayed: Tamanho máximo dos arquivos textos exibidos inline + field_identity_urler: Observador + setting_openid: Permitir Login e Registro via OpenID + field_identity_url: OpenID URL + label_login_with_open_id_option: ou use o OpenID + field_content: Conteúdo + label_descending: Descendente + label_sort: Ordenar + label_ascending: Ascendente + label_date_from_to: De %{start} até %{end} + label_greater_or_equal: ">=" + label_less_or_equal: <= + text_wiki_page_destroy_question: Esta página tem %{descendants} página(s) filha(s) e descendente(s). O que você quer fazer? + text_wiki_page_reassign_children: Reatribuir páginas filhas para esta página pai + text_wiki_page_nullify_children: Manter as páginas filhas como páginas raízes + text_wiki_page_destroy_children: Excluir páginas filhas e todas suas descendentes + setting_password_min_length: Comprimento mínimo para senhas + field_group_by: Agrupar por + mail_subject_wiki_content_updated: "A página wiki '%{id}' foi atualizada" + label_wiki_content_added: Página wiki adicionada + mail_subject_wiki_content_added: "A página wiki '%{id}' foi adicionada" + mail_body_wiki_content_added: A página wiki '%{id}' foi adicionada por %{author}. + label_wiki_content_updated: Página wiki atualizada + mail_body_wiki_content_updated: A página wiki '%{id}' foi atualizada por %{author}. + permission_add_project: Criar projeto + setting_new_project_user_role_id: Papel atribuído a um usuário não-administrador que cria um projeto + label_view_all_revisions: Ver todas as revisões + label_tag: Etiqueta + label_branch: Ramo + text_journal_changed: "%{label} alterado de %{old} para %{new}" + text_journal_set_to: "%{label} ajustado para %{value}" + text_journal_deleted: "%{label} excluído (%{old})" + label_group_plural: Grupos + label_group: Grupo + label_group_new: Novo grupo + label_time_entry_plural: Tempos gastos + text_journal_added: "%{label} %{value} adicionado" + field_active: Ativo + enumeration_system_activity: Atividade do sistema + permission_delete_issue_watchers: Excluir observadores + version_status_closed: fechado + version_status_locked: travado + version_status_open: aberto + error_can_not_reopen_issue_on_closed_version: Uma tarefa atribuída a uma versão fechada não pode ser reaberta + label_user_anonymous: Anônimo + button_move_and_follow: Mover e seguir + setting_default_projects_modules: Módulos habilitados por padrão para novos projetos + setting_gravatar_default: Imagem-padrão de Gravatar + field_sharing: Compartilhamento + label_version_sharing_hierarchy: Com a hierarquia do projeto + label_version_sharing_system: Com todos os projetos + label_version_sharing_descendants: Com sub-projetos + label_version_sharing_tree: Com a árvore do projeto + label_version_sharing_none: Sem compartilhamento + error_can_not_archive_project: Este projeto não pode ser arquivado + button_duplicate: Duplicar + button_copy_and_follow: Copiar e seguir + label_copy_source: Origem + setting_issue_done_ratio: Calcular o percentual de conclusão da tarefa + setting_issue_done_ratio_issue_status: Usar a situação da tarefa + error_issue_done_ratios_not_updated: O pecentual de conclusão das tarefas não foi atualizado. + error_workflow_copy_target: Por favor, selecione os tipos de tarefa e os papéis alvo + setting_issue_done_ratio_issue_field: Use the issue field + label_copy_same_as_target: Mesmo alvo + label_copy_target: Alvo + notice_issue_done_ratios_updated: Percentual de conslusão atualizados. + error_workflow_copy_source: Por favor, selecione um tipo de tarefa e papel de origem + label_update_issue_done_ratios: Atualizar percentual de conclusão das tarefas + setting_start_of_week: Início da semana + field_watcher: Observador + permission_view_issues: Ver tarefas + label_display_used_statuses_only: Somente exibir situações que são usadas por este tipo de tarefa + label_revision_id: Revisão %{value} + label_api_access_key: Chave de acesso a API + button_show: Exibir + label_api_access_key_created_on: Chave de acesso a API criado a %{value} atrás + label_feeds_access_key: Chave de acesso ao RSS + notice_api_access_key_reseted: Sua chave de acesso a API foi redefinida. + setting_rest_api_enabled: Habilitar REST web service + label_missing_api_access_key: Chave de acesso a API faltando + label_missing_feeds_access_key: Chave de acesso ao RSS faltando + text_line_separated: Múltiplos valores permitidos (uma linha para cada valor). + setting_mail_handler_body_delimiters: Truncar e-mails após uma destas linhas + permission_add_subprojects: Criar subprojetos + label_subproject_new: Novo subprojeto + text_own_membership_delete_confirmation: |- + Você está para excluir algumas de suas próprias permissões e pode não mais estar apto a editar este projeto após esta operação. + Você tem certeza que deseja continuar? + label_close_versions: Fechar versões concluídas + label_board_sticky: Marcado + label_board_locked: Travado + permission_export_wiki_pages: Exportar páginas wiki + setting_cache_formatted_text: Realizar cache de texto formatado + permission_manage_project_activities: Gerenciar atividades do projeto + error_unable_delete_issue_status: Não foi possível excluir situação da tarefa + label_profile: Perfil + permission_manage_subtasks: Gerenciar subtarefas + field_parent_issue: Tarefa pai + label_subtask_plural: Subtarefas + label_project_copy_notifications: Enviar notificações por e-mail ao copiar projeto + error_can_not_delete_custom_field: Não foi possível excluir o campo personalizado + error_unable_to_connect: Não foi possível conectar (%{value}) + error_can_not_remove_role: Este papel está em uso e não pode ser excluído. + error_can_not_delete_tracker: Este tipo de tarefa está atribuído a alguma(s) tarefa(s) e não pode ser excluído. + field_principal: Principal + label_my_page_block: Meu bloco de página + notice_failed_to_save_members: "Falha ao gravar membro(s): %{errors}." + text_zoom_out: Afastar zoom + text_zoom_in: Aproximar zoom + notice_unable_delete_time_entry: Não foi possível excluir a entrada no registro de horas trabalhadas. + label_overall_spent_time: Tempo gasto geral + field_time_entries: Registro de horas + project_module_gantt: Gantt + project_module_calendar: Calendário + button_edit_associated_wikipage: "Editar página wiki relacionada: %{page_title}" + field_text: Campo de texto + label_user_mail_option_only_owner: Somente para as coisas que eu criei + setting_default_notification_option: Opção padrão de notificação + label_user_mail_option_only_my_events: Somente para as coisas que eu esteja observando ou esteja envolvido + label_user_mail_option_only_assigned: Somente para as coisas que estejam atribuídas a mim + label_user_mail_option_none: Sem eventos + field_member_of_group: Grupo do responsável + field_assigned_to_role: Papel do responsável + notice_not_authorized_archived_project: O projeto que você está tentando acessar foi arquivado. + label_principal_search: "Pesquisar por usuários ou grupos:" + label_user_search: "Pesquisar por usuário:" + field_visible: Visível + setting_emails_header: Cabeçalho do e-mail + setting_commit_logtime_activity_id: Atividade para registrar horas + text_time_logged_by_changeset: Aplicado no changeset %{value}. + setting_commit_logtime_enabled: Habilitar registro de horas + notice_gantt_chart_truncated: O gráfico foi cortado por exceder o tamanho máximo de linhas que podem ser exibidas (%{max}) + setting_gantt_items_limit: Número máximo de itens exibidos no gráfico gatt + field_warn_on_leaving_unsaved: Alertar-me ao sair de uma página sem salvar o texto + text_warn_on_leaving_unsaved: A página atual contem texto que não foi salvo e será perdido se você sair desta página. + label_my_queries: Minhas consultas personalizadas + text_journal_changed_no_detail: "%{label} atualizado(a)" + label_news_comment_added: Notícia recebeu um comentário + button_expand_all: Expandir tudo + button_collapse_all: Recolher tudo + label_additional_workflow_transitions_for_assignee: Transições adicionais permitidas quando o usuário é o responsável pela tarefa + label_additional_workflow_transitions_for_author: Transições adicionais permitidas quando o usuário é o autor + + label_bulk_edit_selected_time_entries: Alteração em massa do registro de horas + text_time_entries_destroy_confirmation: Tem certeza que quer excluir o(s) registro(s) de horas selecionado(s)? + label_role_anonymous: Anônimo + label_role_non_member: Não Membro + label_issues_visibility_own: Tarefas criadas ou atribuídas ao usuário + field_issues_visibility: Visibilidade das tarefas + label_issues_visibility_all: Todas as tarefas + permission_set_own_issues_private: Alterar as próprias tarefas para públicas ou privadas + field_is_private: Privado + permission_set_issues_private: Alterar tarefas para públicas ou privadas + label_issues_visibility_public: Todas as tarefas não privadas + text_issues_destroy_descendants_confirmation: Isto também irá excluir %{count} subtarefa(s). + field_commit_logs_encoding: Codificação das mensagens de commit + field_scm_path_encoding: Codificação do caminho + text_scm_path_encoding_note: "Padrão: UTF-8" + field_path_to_repository: Caminho para o repositório + field_root_directory: Diretório raiz + field_cvs_module: Módulo + field_cvsroot: CVSROOT + text_mercurial_repository_note: "Repositório local (ex.: /hgrepo, c:\\hgrepo)" + text_scm_command: Comando + text_scm_command_version: Versão + label_git_report_last_commit: Relatar última alteração para arquivos e diretórios + text_scm_config: Você pode configurar seus comandos de versionamento em config/configurations.yml. Por favor reinicie a aplicação após alterá-lo. + text_scm_command_not_available: Comando de versionamento não disponível. Por favor verifique as configurações no painel de administração. + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + + description_query_sort_criteria_direction: Sort direction + description_project_scope: Escopo da pesquisa + description_filter: Filtro + description_user_mail_notification: Configuração de notificações por e-mail + description_date_from: Digita a data inicial + description_message_content: Conteúdo da mensagem + description_available_columns: Colunas disponíveis + description_date_range_interval: Escolha um período selecionando a data de início e fim + description_issue_category_reassign: Escolha uma categoria de tarefas + description_search: Searchfield + description_notes: Notas + description_date_range_list: Escolha um período a partira da lista + description_choose_project: Projetos + description_date_to: Digite a data final + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Escolha uma nova página pai + description_selected_columns: Colunas selecionadas + + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Usar data corrente como data inicial para novas tarefas + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 tarefa + one: 1 tarefa + other: "%{count} tarefas" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: todos + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Com sub-projetos + label_cross_project_tree: Com a árvore do projeto + label_cross_project_hierarchy: Com a hierarquia do projeto + label_cross_project_system: Com todos os projetos + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/33/3370cd22f75e851f0796f6234d205c9ce519841c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/33/3370cd22f75e851f0796f6234d205c9ce519841c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,77 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class ProjectsHelperTest < ActionView::TestCase + include ApplicationHelper + include ProjectsHelper + include ERB::Util + + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :versions, + :projects_trackers, + :member_roles, + :members, + :groups_users, + :enabled_modules, + :workflows + + def setup + super + set_language_if_valid('en') + User.current = nil + end + + def test_link_to_version_within_project + @project = Project.find(2) + User.current = User.find(1) + assert_equal 'Alpha', link_to_version(Version.find(5)) + end + + def test_link_to_version + User.current = User.find(1) + assert_equal 'OnlineStore - Alpha', link_to_version(Version.find(5)) + end + + def test_link_to_private_version + assert_equal 'OnlineStore - Alpha', link_to_version(Version.find(5)) + end + + def test_link_to_version_invalid_version + assert_equal '', link_to_version(Object) + end + + def test_format_version_name_within_project + @project = Project.find(1) + assert_equal "0.1", format_version_name(Version.find(1)) + end + + def test_format_version_name + assert_equal "eCookbook - 0.1", format_version_name(Version.find(1)) + end + + def test_format_version_name_for_system_version + assert_equal "OnlineStore - Systemwide visible version", format_version_name(Version.find(7)) + end + + def test_version_options_for_select_with_no_versions + assert_equal '', version_options_for_select([]) + assert_equal '', version_options_for_select([], Version.find(1)) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/33/3383cdc013135ce68a6882494e36114039a2db31.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/33/3383cdc013135ce68a6882494e36114039a2db31.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1 @@ +$('#watchers').html('<%= escape_javascript(render(:partial => 'watchers/watchers', :locals => {:watched => @watched})) %>'); diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/33/33925e326046a9d648a4cd718d11bd1b53eeb061.svn-base Binary file .svn/pristine/33/33925e326046a9d648a4cd718d11bd1b53eeb061.svn-base has changed diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/33/33b064777c7814b0bfea3fc391793dc9acb2090a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/33/33b064777c7814b0bfea3fc391793dc9acb2090a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,13 @@ +--- +auth_sources_001: + id: 1 + type: AuthSourceLdap + name: 'LDAP test server' + host: '127.0.0.1' + port: 389 + base_dn: 'OU=Person,DC=redmine,DC=org' + attr_login: uid + attr_firstname: givenName + attr_lastname: sn + attr_mail: mail + onthefly_register: false diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/33/33f1e1dee1ea9d39975afb9ebca4632d0c5e2445.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/33/33f1e1dee1ea9d39975afb9ebca4632d0c5e2445.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,72 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class UserPreferenceTest < ActiveSupport::TestCase + fixtures :users, :user_preferences + + def test_create + user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo") + user.login = "newuser" + user.password, user.password_confirmation = "password", "password" + assert user.save + + assert_kind_of UserPreference, user.pref + assert_kind_of Hash, user.pref.others + assert user.pref.save + end + + def test_update + user = User.find(1) + assert_equal true, user.pref.hide_mail + user.pref['preftest'] = 'value' + assert user.pref.save + + user.reload + assert_equal 'value', user.pref['preftest'] + end + + def test_others_hash + user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo") + user.login = "newuser" + user.password, user.password_confirmation = "password", "password" + assert user.save + assert_nil user.preference + up = UserPreference.new(:user => user) + assert_kind_of Hash, up.others + up.others = nil + assert_nil up.others + assert up.save + assert_kind_of Hash, up.others + end + + def test_reading_value_from_nil_others_hash + up = UserPreference.new(:user => User.new) + up.others = nil + assert_nil up.others + assert_nil up[:foo] + end + + def test_writing_value_to_nil_others_hash + up = UserPreference.new(:user => User.new) + up.others = nil + assert_nil up.others + up[:foo] = 'bar' + assert_equal 'bar', up[:foo] + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/34/342285b027af8a7f1e30f8ee50c822bf5f5c7ecb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/34/342285b027af8a7f1e30f8ee50c822bf5f5c7ecb.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,71 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../../../test_helper', __FILE__) + +begin + require 'mocha' + + class SubversionAdapterTest < ActiveSupport::TestCase + + if repository_configured?('subversion') + def setup + @adapter = Redmine::Scm::Adapters::SubversionAdapter.new(self.class.subversion_repository_url) + end + + def test_client_version + v = Redmine::Scm::Adapters::SubversionAdapter.client_version + assert v.is_a?(Array) + end + + def test_scm_version + to_test = { "svn, version 1.6.13 (r1002816)\n" => [1,6,13], + "svn, versione 1.6.13 (r1002816)\n" => [1,6,13], + "1.6.1\n1.7\n1.8" => [1,6,1], + "1.6.2\r\n1.8.1\r\n1.9.1" => [1,6,2]} + to_test.each do |s, v| + test_scm_version_for(s, v) + end + end + + def test_info_not_nil + assert_not_nil @adapter.info + end + + def test_info_nil + adpt = Redmine::Scm::Adapters::SubversionAdapter.new( + "file:///invalid/invalid/" + ) + assert_nil adpt.info + end + + private + + def test_scm_version_for(scm_version, version) + @adapter.class.expects(:scm_version_from_command_line).returns(scm_version) + assert_equal version, @adapter.class.svn_binary_version + end + else + puts "Subversion test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end + end +rescue LoadError + class SubversionMochaFake < ActiveSupport::TestCase + def test_fake; assert(false, "Requires mocha to run those tests") end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/34/34322da11fdda11e13ebe7d947206d86863f3c74.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/34/34322da11fdda11e13ebe7d947206d86863f3c74.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,4 @@ +<%= labelled_form_for @project do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/34/345f7acae2c1ee68bbdbe5e119f5efcf6fb9fb67.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/34/345f7acae2c1ee68bbdbe5e119f5efcf6fb9fb67.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,54 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class IssueTransactionTest < ActiveSupport::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, + :trackers, :projects_trackers, + :versions, + :issue_statuses, :issue_categories, :issue_relations, :workflows, + :enumerations, + :issues, + :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values, + :time_entries + + self.use_transactional_fixtures = false + + def test_invalid_move_to_another_project + parent1 = Issue.generate! + child = Issue.generate!(:parent_issue_id => parent1.id) + grandchild = Issue.generate!(:parent_issue_id => child.id, :tracker_id => 2) + Project.find(2).tracker_ids = [1] + + parent1.reload + assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt] + + # child can not be moved to Project 2 because its child is on a disabled tracker + child = Issue.find(child.id) + child.project = Project.find(2) + assert !child.save + child.reload + grandchild.reload + parent1.reload + + # no change + assert_equal [1, parent1.id, 1, 6], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt] + assert_equal [1, parent1.id, 2, 5], [child.project_id, child.root_id, child.lft, child.rgt] + assert_equal [1, parent1.id, 3, 4], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt] + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/34/345fb72e40574b54a9481a84a098650cf151872f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/34/345fb72e40574b54a9481a84a098650cf151872f.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,107 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class QueriesController < ApplicationController + menu_item :issues + before_filter :find_query, :except => [:new, :create, :index] + before_filter :find_optional_project, :only => [:new, :create] + + accept_api_auth :index + + include QueriesHelper + + def index + case params[:format] + when 'xml', 'json' + @offset, @limit = api_offset_and_limit + else + @limit = per_page_option + end + + @query_count = Query.visible.count + @query_pages = Paginator.new self, @query_count, @limit, params['page'] + @queries = Query.visible.all(:limit => @limit, :offset => @offset, :order => "#{Query.table_name}.name") + + respond_to do |format| + format.html { render :nothing => true } + format.api + end + end + + def new + @query = Query.new + @query.user = User.current + @query.project = @project + @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin? + build_query_from_params + end + + def create + @query = Query.new(params[:query]) + @query.user = User.current + @query.project = params[:query_is_for_all] ? nil : @project + @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin? + build_query_from_params + @query.column_names = nil if params[:default_columns] + + if @query.save + flash[:notice] = l(:notice_successful_create) + redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query + else + render :action => 'new', :layout => !request.xhr? + end + end + + def edit + end + + def update + @query.attributes = params[:query] + @query.project = nil if params[:query_is_for_all] + @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin? + build_query_from_params + @query.column_names = nil if params[:default_columns] + + if @query.save + flash[:notice] = l(:notice_successful_update) + redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query + else + render :action => 'edit' + end + end + + def destroy + @query.destroy + redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 + end + +private + def find_query + @query = Query.find(params[:id]) + @project = @query.project + render_403 unless @query.editable_by?(User.current) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_optional_project + @project = Project.find(params[:project_id]) if params[:project_id] + render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true) + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/34/34944e69635f53bef7eb7777ca3abc691bf31c39.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/34/34944e69635f53bef7eb7777ca3abc691bf31c39.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,94 @@ +var revisionGraph = null; + +function drawRevisionGraph(holder, commits_hash, graph_space) { + var XSTEP = 20, + CIRCLE_INROW_OFFSET = 10; + var commits_by_scmid = commits_hash, + commits = $.map(commits_by_scmid, function(val,i){return val;}); + var max_rdmid = commits.length - 1; + var commit_table_rows = $('table.changesets tr.changeset'); + + // create graph + if(revisionGraph != null) + revisionGraph.clear(); + else + revisionGraph = Raphael(holder); + + var top = revisionGraph.set(); + // init dimensions + var graph_x_offset = commit_table_rows.first().find('td').first().position().left - $(holder).position().left, + graph_y_offset = $(holder).position().top, + graph_right_side = graph_x_offset + (graph_space + 1) * XSTEP, + graph_bottom = commit_table_rows.last().position().top + commit_table_rows.last().height() - graph_y_offset; + + revisionGraph.setSize(graph_right_side, graph_bottom); + + // init colors + var colors = []; + Raphael.getColor.reset(); + for (var k = 0; k <= graph_space; k++) { + colors.push(Raphael.getColor()); + } + + var parent_commit; + var x, y, parent_x, parent_y; + var path, title; + var revision_dot_overlay; + $.each(commits, function(index, commit) { + if (!commit.hasOwnProperty("space")) + commit.space = 0; + + y = commit_table_rows.eq(max_rdmid - commit.rdmid).position().top - graph_y_offset + CIRCLE_INROW_OFFSET; + x = graph_x_offset + XSTEP / 2 + XSTEP * commit.space; + revisionGraph.circle(x, y, 3) + .attr({ + fill: colors[commit.space], + stroke: 'none', + }).toFront(); + // paths to parents + $.each(commit.parent_scmids, function(index, parent_scmid) { + parent_commit = commits_by_scmid[parent_scmid]; + if (parent_commit) { + if (!parent_commit.hasOwnProperty("space")) + parent_commit.space = 0; + + parent_y = commit_table_rows.eq(max_rdmid - parent_commit.rdmid).position().top - graph_y_offset + CIRCLE_INROW_OFFSET; + parent_x = graph_x_offset + XSTEP / 2 + XSTEP * parent_commit.space; + if (parent_commit.space == commit.space) { + // vertical path + path = revisionGraph.path([ + 'M', x, y, + 'V', parent_y]); + } else { + // path to a commit in a different branch (Bezier curve) + path = revisionGraph.path([ + 'M', x, y, + 'C', x, y, x, y + (parent_y - y) / 2, x + (parent_x - x) / 2, y + (parent_y - y) / 2, + 'C', x + (parent_x - x) / 2, y + (parent_y - y) / 2, parent_x, parent_y-(parent_y-y)/2, parent_x, parent_y]); + } + } else { + // vertical path ending at the bottom of the revisionGraph + path = revisionGraph.path([ + 'M', x, y, + 'V', graph_bottom]); + } + path.attr({stroke: colors[commit.space], "stroke-width": 1.5}).toBack(); + }); + revision_dot_overlay = revisionGraph.circle(x, y, 10); + revision_dot_overlay + .attr({ + fill: '#000', + opacity: 0, + cursor: 'pointer', + href: commit.href + }); + + if(commit.refs != null && commit.refs.length > 0) { + title = document.createElementNS(revisionGraph.canvas.namespaceURI, 'title'); + title.appendChild(document.createTextNode(commit.refs)); + revision_dot_overlay.node.appendChild(title); + } + top.push(revision_dot_overlay); + }); + top.toFront(); +}; diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/34/349bb4552fd5e3ee5085280e36098a4598dd3ace.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/34/349bb4552fd5e3ee5085280e36098a4598dd3ace.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,130 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class AuthSourceLdapTest < ActiveSupport::TestCase + include Redmine::I18n + fixtures :auth_sources + + def setup + end + + def test_create + a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName') + assert a.save + end + + def test_should_strip_ldap_attributes + a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName', + :attr_firstname => 'givenName ') + assert a.save + assert_equal 'givenName', a.reload.attr_firstname + end + + def test_replace_port_zero_to_389 + a = AuthSourceLdap.new( + :name => 'My LDAP', :host => 'ldap.example.net', :port => 0, + :base_dn => 'dc=example,dc=net', :attr_login => 'sAMAccountName', + :attr_firstname => 'givenName ') + assert a.save + assert_equal 389, a.port + end + + def test_filter_should_be_validated + set_language_if_valid 'en' + + a = AuthSourceLdap.new(:name => 'My LDAP', :host => 'ldap.example.net', :port => 389, :attr_login => 'sn') + a.filter = "(mail=*@redmine.org" + assert !a.valid? + assert_include "LDAP filter is invalid", a.errors.full_messages + + a.filter = "(mail=*@redmine.org)" + assert a.valid? + end + + if ldap_configured? + context '#authenticate' do + setup do + @auth = AuthSourceLdap.find(1) + @auth.update_attribute :onthefly_register, true + end + + context 'with a valid LDAP user' do + should 'return the user attributes' do + attributes = @auth.authenticate('example1','123456') + assert attributes.is_a?(Hash), "An hash was not returned" + assert_equal 'Example', attributes[:firstname] + assert_equal 'One', attributes[:lastname] + assert_equal 'example1@redmine.org', attributes[:mail] + assert_equal @auth.id, attributes[:auth_source_id] + attributes.keys.each do |attribute| + assert User.new.respond_to?("#{attribute}="), "Unexpected :#{attribute} attribute returned" + end + end + end + + context 'with an invalid LDAP user' do + should 'return nil' do + assert_equal nil, @auth.authenticate('nouser','123456') + end + end + + context 'without a login' do + should 'return nil' do + assert_equal nil, @auth.authenticate('','123456') + end + end + + context 'without a password' do + should 'return nil' do + assert_equal nil, @auth.authenticate('edavis','') + end + end + + context 'without filter' do + should 'return any user' do + assert @auth.authenticate('example1','123456') + assert @auth.authenticate('edavis', '123456') + end + end + + context 'with filter' do + setup do + @auth.filter = "(mail=*@redmine.org)" + end + + should 'return user who matches the filter only' do + assert @auth.authenticate('example1','123456') + assert_nil @auth.authenticate('edavis', '123456') + end + end + end + + def test_authenticate_should_timeout + auth_source = AuthSourceLdap.find(1) + auth_source.timeout = 1 + def auth_source.initialize_ldap_con(*args); sleep(5); end + + assert_raise AuthSourceTimeoutException do + auth_source.authenticate 'example1', '123456' + end + end + else + puts '(Test LDAP server not configured)' + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/34/34b50e0d1c5a0c7f91a728223c1c4ed77c186cd6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/34/34b50e0d1c5a0c7f91a728223c1c4ed77c186cd6.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,11 @@ +--- +wikis_001: + status: 1 + start_page: CookBook documentation + project_id: 1 + id: 1 +wikis_002: + status: 1 + start_page: Start page + project_id: 2 + id: 2 diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/34/34bbd22558c309af2608a5aaf343302b2d47bfb5.svn-base --- a/.svn/pristine/34/34bbd22558c309af2608a5aaf343302b2d47bfb5.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module WorkflowsHelper -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/34/34e8f3b289ef523b2e5c39a872d6efc48111b2f3.svn-base --- a/.svn/pristine/34/34e8f3b289ef523b2e5c39a872d6efc48111b2f3.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -class Query < ActiveRecord::Base - generator_for :name, :start => 'Query 0' - -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/35/35e7528b96f6bca4ea96cc9fa52b687ed6909cb9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/35/35e7528b96f6bca4ea96cc9fa52b687ed6909cb9.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,219 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class RolesControllerTest < ActionController::TestCase + fixtures :roles, :users, :members, :member_roles, :workflows, :trackers + + def setup + @controller = RolesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + @request.session[:user_id] = 1 # admin + end + + def test_index + get :index + assert_response :success + assert_template 'index' + + assert_not_nil assigns(:roles) + assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles) + + assert_tag :tag => 'a', :attributes => { :href => '/roles/1/edit' }, + :content => 'Manager' + end + + def test_new + get :new + assert_response :success + assert_template 'new' + end + + def test_new_with_copy + copy_from = Role.find(2) + + get :new, :copy => copy_from.id.to_s + assert_response :success + assert_template 'new' + + role = assigns(:role) + assert_equal copy_from.permissions, role.permissions + + assert_select 'form' do + # blank name + assert_select 'input[name=?][value=]', 'role[name]' + # edit_project permission checked + assert_select 'input[type=checkbox][name=?][value=edit_project][checked=checked]', 'role[permissions][]' + # add_project permission not checked + assert_select 'input[type=checkbox][name=?][value=add_project]', 'role[permissions][]' + assert_select 'input[type=checkbox][name=?][value=add_project][checked=checked]', 'role[permissions][]', 0 + # workflow copy selected + assert_select 'select[name=?]', 'copy_workflow_from' do + assert_select 'option[value=2][selected=selected]' + end + end + end + + def test_create_with_validaton_failure + post :create, :role => {:name => '', + :permissions => ['add_issues', 'edit_issues', 'log_time', ''], + :assignable => '0'} + + assert_response :success + assert_template 'new' + assert_tag :tag => 'div', :attributes => { :id => 'errorExplanation' } + end + + def test_create_without_workflow_copy + post :create, :role => {:name => 'RoleWithoutWorkflowCopy', + :permissions => ['add_issues', 'edit_issues', 'log_time', ''], + :assignable => '0'} + + assert_redirected_to '/roles' + role = Role.find_by_name('RoleWithoutWorkflowCopy') + assert_not_nil role + assert_equal [:add_issues, :edit_issues, :log_time], role.permissions + assert !role.assignable? + end + + def test_create_with_workflow_copy + post :create, :role => {:name => 'RoleWithWorkflowCopy', + :permissions => ['add_issues', 'edit_issues', 'log_time', ''], + :assignable => '0'}, + :copy_workflow_from => '1' + + assert_redirected_to '/roles' + role = Role.find_by_name('RoleWithWorkflowCopy') + assert_not_nil role + assert_equal Role.find(1).workflow_rules.size, role.workflow_rules.size + end + + def test_edit + get :edit, :id => 1 + assert_response :success + assert_template 'edit' + assert_equal Role.find(1), assigns(:role) + assert_select 'select[name=?]', 'role[issues_visibility]' + end + + def test_edit_anonymous + get :edit, :id => Role.anonymous.id + assert_response :success + assert_template 'edit' + assert_select 'select[name=?]', 'role[issues_visibility]', 0 + end + + def test_edit_invalid_should_respond_with_404 + get :edit, :id => 999 + assert_response 404 + end + + def test_update + put :update, :id => 1, + :role => {:name => 'Manager', + :permissions => ['edit_project', ''], + :assignable => '0'} + + assert_redirected_to '/roles' + role = Role.find(1) + assert_equal [:edit_project], role.permissions + end + + def test_update_with_failure + put :update, :id => 1, :role => {:name => ''} + assert_response :success + assert_template 'edit' + end + + def test_destroy + r = Role.create!(:name => 'ToBeDestroyed', :permissions => [:view_wiki_pages]) + + delete :destroy, :id => r + assert_redirected_to '/roles' + assert_nil Role.find_by_id(r.id) + end + + def test_destroy_role_in_use + delete :destroy, :id => 1 + assert_redirected_to '/roles' + assert_equal 'This role is in use and cannot be deleted.', flash[:error] + assert_not_nil Role.find_by_id(1) + end + + def test_get_permissions + get :permissions + assert_response :success + assert_template 'permissions' + + assert_not_nil assigns(:roles) + assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles) + + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'permissions[3][]', + :value => 'add_issues', + :checked => 'checked' } + + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'permissions[3][]', + :value => 'delete_issues', + :checked => nil } + end + + def test_post_permissions + post :permissions, :permissions => { '0' => '', '1' => ['edit_issues'], '3' => ['add_issues', 'delete_issues']} + assert_redirected_to '/roles' + + assert_equal [:edit_issues], Role.find(1).permissions + assert_equal [:add_issues, :delete_issues], Role.find(3).permissions + assert Role.find(2).permissions.empty? + end + + def test_clear_all_permissions + post :permissions, :permissions => { '0' => '' } + assert_redirected_to '/roles' + assert Role.find(1).permissions.empty? + end + + def test_move_highest + put :update, :id => 3, :role => {:move_to => 'highest'} + assert_redirected_to '/roles' + assert_equal 1, Role.find(3).position + end + + def test_move_higher + position = Role.find(3).position + put :update, :id => 3, :role => {:move_to => 'higher'} + assert_redirected_to '/roles' + assert_equal position - 1, Role.find(3).position + end + + def test_move_lower + position = Role.find(2).position + put :update, :id => 2, :role => {:move_to => 'lower'} + assert_redirected_to '/roles' + assert_equal position + 1, Role.find(2).position + end + + def test_move_lowest + put :update, :id => 2, :role => {:move_to => 'lowest'} + assert_redirected_to '/roles' + assert_equal Role.count, Role.find(2).position + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/36/3666692670ecc84af41e4a546b7591a94d203311.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/36/3666692670ecc84af41e4a546b7591a94d203311.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,14 @@ +api.time_entry do + api.id @time_entry.id + api.project(:id => @time_entry.project_id, :name => @time_entry.project.name) unless @time_entry.project.nil? + api.issue(:id => @time_entry.issue_id) unless @time_entry.issue.nil? + api.user(:id => @time_entry.user_id, :name => @time_entry.user.name) unless @time_entry.user.nil? + api.activity(:id => @time_entry.activity_id, :name => @time_entry.activity.name) unless @time_entry.activity.nil? + api.hours @time_entry.hours + api.comments @time_entry.comments + api.spent_on @time_entry.spent_on + api.created_on @time_entry.created_on + api.updated_on @time_entry.updated_on + + render_api_custom_values @time_entry.custom_field_values, api +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/36/367408f1f75021a71b939b14007872b6740672f3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/36/367408f1f75021a71b939b14007872b6740672f3.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,136 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class ApiTest::VersionsTest < ActionController::IntegrationTest + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :workflows, + :versions + + def setup + Setting.rest_api_enabled = '1' + end + + context "/projects/:project_id/versions" do + context "GET" do + should "return project versions" do + get '/projects/1/versions.xml' + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag :tag => 'versions', + :attributes => {:type => 'array'}, + :child => { + :tag => 'version', + :child => { + :tag => 'id', + :content => '2', + :sibling => { + :tag => 'name', + :content => '1.0' + } + } + } + end + end + + context "POST" do + should "create the version" do + assert_difference 'Version.count' do + post '/projects/1/versions.xml', {:version => {:name => 'API test'}}, credentials('jsmith') + end + + version = Version.first(:order => 'id DESC') + assert_equal 'API test', version.name + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s} + end + + should "create the version with due date" do + assert_difference 'Version.count' do + post '/projects/1/versions.xml', {:version => {:name => 'API test', :due_date => '2012-01-24'}}, credentials('jsmith') + end + + version = Version.first(:order => 'id DESC') + assert_equal 'API test', version.name + assert_equal Date.parse('2012-01-24'), version.due_date + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_tag 'version', :child => {:tag => 'id', :content => version.id.to_s} + end + + context "with failure" do + should "return the errors" do + assert_no_difference('Version.count') do + post '/projects/1/versions.xml', {:version => {:name => ''}}, credentials('jsmith') + end + + assert_response :unprocessable_entity + assert_tag :errors, :child => {:tag => 'error', :content => "Name can't be blank"} + end + end + end + end + + context "/versions/:id" do + context "GET" do + should "return the version" do + get '/versions/2.xml' + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_select 'version' do + assert_select 'id', :text => '2' + assert_select 'name', :text => '1.0' + assert_select 'sharing', :text => 'none' + end + end + end + + context "PUT" do + should "update the version" do + put '/versions/2.xml', {:version => {:name => 'API update'}}, credentials('jsmith') + + assert_response :ok + assert_equal '', @response.body + assert_equal 'API update', Version.find(2).name + end + end + + context "DELETE" do + should "destroy the version" do + assert_difference 'Version.count', -1 do + delete '/versions/3.xml', {}, credentials('jsmith') + end + + assert_response :ok + assert_equal '', @response.body + assert_nil Version.find_by_id(3) + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/36/36a71efadc331c3001a59098a85ebc847b7a529a.svn-base --- a/.svn/pristine/36/36a71efadc331c3001a59098a85ebc847b7a529a.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,2021 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -require File.expand_path('../../test_helper', __FILE__) -require 'issues_controller' - -class IssuesControllerTest < ActionController::TestCase - fixtures :projects, - :users, - :roles, - :members, - :member_roles, - :issues, - :issue_statuses, - :versions, - :trackers, - :projects_trackers, - :issue_categories, - :enabled_modules, - :enumerations, - :attachments, - :workflows, - :custom_fields, - :custom_values, - :custom_fields_projects, - :custom_fields_trackers, - :time_entries, - :journals, - :journal_details, - :queries - - include Redmine::I18n - - def setup - @controller = IssuesController.new - @request = ActionController::TestRequest.new - @response = ActionController::TestResponse.new - User.current = nil - end - - def test_index - Setting.default_language = 'en' - - get :index - assert_response :success - assert_template 'index' - assert_not_nil assigns(:issues) - assert_nil assigns(:project) - assert_tag :tag => 'a', :content => /Can't print recipes/ - assert_tag :tag => 'a', :content => /Subproject issue/ - # private projects hidden - assert_no_tag :tag => 'a', :content => /Issue of a private subproject/ - assert_no_tag :tag => 'a', :content => /Issue on project 2/ - # project column - assert_tag :tag => 'th', :content => /Project/ - end - - def test_index_should_not_list_issues_when_module_disabled - EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1") - get :index - assert_response :success - assert_template 'index' - assert_not_nil assigns(:issues) - assert_nil assigns(:project) - assert_no_tag :tag => 'a', :content => /Can't print recipes/ - assert_tag :tag => 'a', :content => /Subproject issue/ - end - - def test_index_should_list_visible_issues_only - get :index, :per_page => 100 - assert_response :success - assert_not_nil assigns(:issues) - assert_nil assigns(:issues).detect {|issue| !issue.visible?} - end - - def test_index_with_project - Setting.display_subprojects_issues = 0 - get :index, :project_id => 1 - assert_response :success - assert_template 'index' - assert_not_nil assigns(:issues) - assert_tag :tag => 'a', :content => /Can't print recipes/ - assert_no_tag :tag => 'a', :content => /Subproject issue/ - end - - def test_index_with_project_and_subprojects - Setting.display_subprojects_issues = 1 - get :index, :project_id => 1 - assert_response :success - assert_template 'index' - assert_not_nil assigns(:issues) - assert_tag :tag => 'a', :content => /Can't print recipes/ - assert_tag :tag => 'a', :content => /Subproject issue/ - assert_no_tag :tag => 'a', :content => /Issue of a private subproject/ - end - - def test_index_with_project_and_subprojects_should_show_private_subprojects - @request.session[:user_id] = 2 - Setting.display_subprojects_issues = 1 - get :index, :project_id => 1 - assert_response :success - assert_template 'index' - assert_not_nil assigns(:issues) - assert_tag :tag => 'a', :content => /Can't print recipes/ - assert_tag :tag => 'a', :content => /Subproject issue/ - assert_tag :tag => 'a', :content => /Issue of a private subproject/ - end - - def test_index_with_project_and_default_filter - get :index, :project_id => 1, :set_filter => 1 - assert_response :success - assert_template 'index' - assert_not_nil assigns(:issues) - - query = assigns(:query) - assert_not_nil query - # default filter - assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters) - end - - def test_index_with_project_and_filter - get :index, :project_id => 1, :set_filter => 1, - :f => ['tracker_id'], - :op => {'tracker_id' => '='}, - :v => {'tracker_id' => ['1']} - assert_response :success - assert_template 'index' - assert_not_nil assigns(:issues) - - query = assigns(:query) - assert_not_nil query - assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters) - end - - def test_index_with_short_filters - - to_test = { - 'status_id' => { - 'o' => { :op => 'o', :values => [''] }, - 'c' => { :op => 'c', :values => [''] }, - '7' => { :op => '=', :values => ['7'] }, - '7|3|4' => { :op => '=', :values => ['7', '3', '4'] }, - '=7' => { :op => '=', :values => ['7'] }, - '!3' => { :op => '!', :values => ['3'] }, - '!7|3|4' => { :op => '!', :values => ['7', '3', '4'] }}, - 'subject' => { - 'This is a subject' => { :op => '=', :values => ['This is a subject'] }, - 'o' => { :op => '=', :values => ['o'] }, - '~This is part of a subject' => { :op => '~', :values => ['This is part of a subject'] }, - '!~This is part of a subject' => { :op => '!~', :values => ['This is part of a subject'] }}, - 'tracker_id' => { - '3' => { :op => '=', :values => ['3'] }, - '=3' => { :op => '=', :values => ['3'] }}, - 'start_date' => { - '2011-10-12' => { :op => '=', :values => ['2011-10-12'] }, - '=2011-10-12' => { :op => '=', :values => ['2011-10-12'] }, - '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] }, - '<=2011-10-12' => { :op => '<=', :values => ['2011-10-12'] }, - '><2011-10-01|2011-10-30' => { :op => '><', :values => ['2011-10-01', '2011-10-30'] }, - ' { :op => ' ['2'] }, - '>t+2' => { :op => '>t+', :values => ['2'] }, - 't+2' => { :op => 't+', :values => ['2'] }, - 't' => { :op => 't', :values => [''] }, - 'w' => { :op => 'w', :values => [''] }, - '>t-2' => { :op => '>t-', :values => ['2'] }, - ' { :op => ' ['2'] }, - 't-2' => { :op => 't-', :values => ['2'] }}, - 'created_on' => { - '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] }, - ' { :op => '=', :values => ['t+2' => { :op => '=', :values => ['>t+2'] }, - 't+2' => { :op => 't', :values => ['+2'] }}, - 'cf_1' => { - 'c' => { :op => '=', :values => ['c'] }, - '!c' => { :op => '!', :values => ['c'] }, - '!*' => { :op => '!*', :values => [''] }, - '*' => { :op => '*', :values => [''] }}, - 'estimated_hours' => { - '=13.4' => { :op => '=', :values => ['13.4'] }, - '>=45' => { :op => '>=', :values => ['45'] }, - '<=125' => { :op => '<=', :values => ['125'] }, - '><10.5|20.5' => { :op => '><', :values => ['10.5', '20.5'] }, - '!*' => { :op => '!*', :values => [''] }, - '*' => { :op => '*', :values => [''] }} - } - - default_filter = { 'status_id' => {:operator => 'o', :values => [''] }} - - to_test.each do |field, expression_and_expected| - expression_and_expected.each do |filter_expression, expected| - - get :index, :set_filter => 1, field => filter_expression - - assert_response :success - assert_template 'index' - assert_not_nil assigns(:issues) - - query = assigns(:query) - assert_not_nil query - assert query.has_filter?(field) - assert_equal(default_filter.merge({field => {:operator => expected[:op], :values => expected[:values]}}), query.filters) - end - end - - end - - def test_index_with_project_and_empty_filters - get :index, :project_id => 1, :set_filter => 1, :fields => [''] - assert_response :success - assert_template 'index' - assert_not_nil assigns(:issues) - - query = assigns(:query) - assert_not_nil query - # no filter - assert_equal({}, query.filters) - end - - def test_index_with_query - get :index, :project_id => 1, :query_id => 5 - assert_response :success - assert_template 'index' - assert_not_nil assigns(:issues) - assert_nil assigns(:issue_count_by_group) - end - - def test_index_with_query_grouped_by_tracker - get :index, :project_id => 1, :query_id => 6 - assert_response :success - assert_template 'index' - assert_not_nil assigns(:issues) - assert_not_nil assigns(:issue_count_by_group) - end - - def test_index_with_query_grouped_by_list_custom_field - get :index, :project_id => 1, :query_id => 9 - assert_response :success - assert_template 'index' - assert_not_nil assigns(:issues) - assert_not_nil assigns(:issue_count_by_group) - end - - def test_index_with_query_id_and_project_id_should_set_session_query - get :index, :project_id => 1, :query_id => 4 - assert_response :success - assert_kind_of Hash, session[:query] - assert_equal 4, session[:query][:id] - assert_equal 1, session[:query][:project_id] - end - - def test_index_with_cross_project_query_in_session_should_show_project_issues - q = Query.create!(:name => "test", :user_id => 2, :is_public => false, :project => nil) - @request.session[:query] = {:id => q.id, :project_id => 1} - - with_settings :display_subprojects_issues => '0' do - get :index, :project_id => 1 - end - assert_response :success - assert_not_nil assigns(:query) - assert_equal q.id, assigns(:query).id - assert_equal 1, assigns(:query).project_id - assert_equal [1], assigns(:issues).map(&:project_id).uniq - end - - def test_private_query_should_not_be_available_to_other_users - q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil) - @request.session[:user_id] = 3 - - get :index, :query_id => q.id - assert_response 403 - end - - def test_private_query_should_be_available_to_its_user - q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil) - @request.session[:user_id] = 2 - - get :index, :query_id => q.id - assert_response :success - end - - def test_public_query_should_be_available_to_other_users - q = Query.create!(:name => "private", :user => User.find(2), :is_public => true, :project => nil) - @request.session[:user_id] = 3 - - get :index, :query_id => q.id - assert_response :success - end - - def test_index_csv - get :index, :format => 'csv' - assert_response :success - assert_not_nil assigns(:issues) - assert_equal 'text/csv', @response.content_type - assert @response.body.starts_with?("#,") - lines = @response.body.chomp.split("\n") - assert_equal assigns(:query).columns.size + 1, lines[0].split(',').size - end - - def test_index_csv_with_project - get :index, :project_id => 1, :format => 'csv' - assert_response :success - assert_not_nil assigns(:issues) - assert_equal 'text/csv', @response.content_type - end - - def test_index_csv_with_description - get :index, :format => 'csv', :description => '1' - assert_response :success - assert_not_nil assigns(:issues) - assert_equal 'text/csv', @response.content_type - assert @response.body.starts_with?("#,") - lines = @response.body.chomp.split("\n") - assert_equal assigns(:query).columns.size + 2, lines[0].split(',').size - end - - def test_index_csv_with_all_columns - get :index, :format => 'csv', :columns => 'all' - assert_response :success - assert_not_nil assigns(:issues) - assert_equal 'text/csv', @response.content_type - assert @response.body.starts_with?("#,") - lines = @response.body.chomp.split("\n") - assert_equal assigns(:query).available_columns.size + 1, lines[0].split(',').size - end - - def test_index_csv_big_5 - with_settings :default_language => "zh-TW" do - str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88" - str_big5 = "\xa4@\xa4\xeb" - if str_utf8.respond_to?(:force_encoding) - str_utf8.force_encoding('UTF-8') - str_big5.force_encoding('Big5') - end - issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, - :status_id => 1, :priority => IssuePriority.all.first, - :subject => str_utf8) - assert issue.save - - get :index, :project_id => 1, - :f => ['subject'], - :op => '=', :values => [str_utf8], - :format => 'csv' - assert_equal 'text/csv', @response.content_type - lines = @response.body.chomp.split("\n") - s1 = "\xaa\xac\xbaA" - if str_utf8.respond_to?(:force_encoding) - s1.force_encoding('Big5') - end - assert lines[0].include?(s1) - assert lines[1].include?(str_big5) - end - end - - def test_index_csv_cannot_convert_should_be_replaced_big_5 - with_settings :default_language => "zh-TW" do - str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85" - if str_utf8.respond_to?(:force_encoding) - str_utf8.force_encoding('UTF-8') - end - issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, - :status_id => 1, :priority => IssuePriority.all.first, - :subject => str_utf8) - assert issue.save - - get :index, :project_id => 1, - :f => ['subject'], - :op => '=', :values => [str_utf8], - :c => ['status', 'subject'], - :format => 'csv', - :set_filter => 1 - assert_equal 'text/csv', @response.content_type - lines = @response.body.chomp.split("\n") - s1 = "\xaa\xac\xbaA" # status - if str_utf8.respond_to?(:force_encoding) - s1.force_encoding('Big5') - end - assert lines[0].include?(s1) - s2 = lines[1].split(",")[2] - if s1.respond_to?(:force_encoding) - s3 = "\xa5H?" # subject - s3.force_encoding('Big5') - assert_equal s3, s2 - elsif RUBY_PLATFORM == 'java' - assert_equal "??", s2 - else - assert_equal "\xa5H???", s2 - end - end - end - - def test_index_csv_tw - with_settings :default_language => "zh-TW" do - str1 = "test_index_csv_tw" - issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, - :status_id => 1, :priority => IssuePriority.all.first, - :subject => str1, :estimated_hours => '1234.5') - assert issue.save - assert_equal 1234.5, issue.estimated_hours - - get :index, :project_id => 1, - :f => ['subject'], - :op => '=', :values => [str1], - :c => ['estimated_hours', 'subject'], - :format => 'csv', - :set_filter => 1 - assert_equal 'text/csv', @response.content_type - lines = @response.body.chomp.split("\n") - assert_equal "#{issue.id},1234.5,#{str1}", lines[1] - - str_tw = "Traditional Chinese (\xe7\xb9\x81\xe9\xab\x94\xe4\xb8\xad\xe6\x96\x87)" - if str_tw.respond_to?(:force_encoding) - str_tw.force_encoding('UTF-8') - end - assert_equal str_tw, l(:general_lang_name) - assert_equal ',', l(:general_csv_separator) - assert_equal '.', l(:general_csv_decimal_separator) - end - end - - def test_index_csv_fr - with_settings :default_language => "fr" do - str1 = "test_index_csv_fr" - issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, - :status_id => 1, :priority => IssuePriority.all.first, - :subject => str1, :estimated_hours => '1234.5') - assert issue.save - assert_equal 1234.5, issue.estimated_hours - - get :index, :project_id => 1, - :f => ['subject'], - :op => '=', :values => [str1], - :c => ['estimated_hours', 'subject'], - :format => 'csv', - :set_filter => 1 - assert_equal 'text/csv', @response.content_type - lines = @response.body.chomp.split("\n") - assert_equal "#{issue.id};1234,5;#{str1}", lines[1] - - str_fr = "Fran\xc3\xa7ais" - if str_fr.respond_to?(:force_encoding) - str_fr.force_encoding('UTF-8') - end - assert_equal str_fr, l(:general_lang_name) - assert_equal ';', l(:general_csv_separator) - assert_equal ',', l(:general_csv_decimal_separator) - end - end - - def test_index_pdf - ["en", "zh", "zh-TW", "ja", "ko"].each do |lang| - with_settings :default_language => lang do - - get :index - assert_response :success - assert_template 'index' - - if lang == "ja" - if RUBY_PLATFORM != 'java' - assert_equal "CP932", l(:general_pdf_encoding) - end - if RUBY_PLATFORM == 'java' && l(:general_pdf_encoding) == "CP932" - next - end - end - - get :index, :format => 'pdf' - assert_response :success - assert_not_nil assigns(:issues) - assert_equal 'application/pdf', @response.content_type - - get :index, :project_id => 1, :format => 'pdf' - assert_response :success - assert_not_nil assigns(:issues) - assert_equal 'application/pdf', @response.content_type - - get :index, :project_id => 1, :query_id => 6, :format => 'pdf' - assert_response :success - assert_not_nil assigns(:issues) - assert_equal 'application/pdf', @response.content_type - end - end - end - - def test_index_pdf_with_query_grouped_by_list_custom_field - get :index, :project_id => 1, :query_id => 9, :format => 'pdf' - assert_response :success - assert_not_nil assigns(:issues) - assert_not_nil assigns(:issue_count_by_group) - assert_equal 'application/pdf', @response.content_type - end - - def test_index_sort - get :index, :sort => 'tracker,id:desc' - assert_response :success - - sort_params = @request.session['issues_index_sort'] - assert sort_params.is_a?(String) - assert_equal 'tracker,id:desc', sort_params - - issues = assigns(:issues) - assert_not_nil issues - assert !issues.empty? - assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id) - end - - def test_index_sort_by_field_not_included_in_columns - Setting.issue_list_default_columns = %w(subject author) - get :index, :sort => 'tracker' - end - - def test_index_sort_by_assigned_to - get :index, :sort => 'assigned_to' - assert_response :success - assignees = assigns(:issues).collect(&:assigned_to).compact - assert_equal assignees.sort, assignees - end - - def test_index_sort_by_assigned_to_desc - get :index, :sort => 'assigned_to:desc' - assert_response :success - assignees = assigns(:issues).collect(&:assigned_to).compact - assert_equal assignees.sort.reverse, assignees - end - - def test_index_group_by_assigned_to - get :index, :group_by => 'assigned_to', :sort => 'priority' - assert_response :success - end - - def test_index_sort_by_author - get :index, :sort => 'author' - assert_response :success - authors = assigns(:issues).collect(&:author) - assert_equal authors.sort, authors - end - - def test_index_sort_by_author_desc - get :index, :sort => 'author:desc' - assert_response :success - authors = assigns(:issues).collect(&:author) - assert_equal authors.sort.reverse, authors - end - - def test_index_group_by_author - get :index, :group_by => 'author', :sort => 'priority' - assert_response :success - end - - def test_index_with_columns - columns = ['tracker', 'subject', 'assigned_to'] - get :index, :set_filter => 1, :c => columns - assert_response :success - - # query should use specified columns - query = assigns(:query) - assert_kind_of Query, query - assert_equal columns, query.column_names.map(&:to_s) - - # columns should be stored in session - assert_kind_of Hash, session[:query] - assert_kind_of Array, session[:query][:column_names] - assert_equal columns, session[:query][:column_names].map(&:to_s) - - # ensure only these columns are kept in the selected columns list - assert_tag :tag => 'select', :attributes => { :id => 'selected_columns' }, - :children => { :count => 3 } - assert_no_tag :tag => 'option', :attributes => { :value => 'project' }, - :parent => { :tag => 'select', :attributes => { :id => "selected_columns" } } - end - - def test_index_without_project_should_implicitly_add_project_column_to_default_columns - Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to'] - get :index, :set_filter => 1 - - # query should use specified columns - query = assigns(:query) - assert_kind_of Query, query - assert_equal [:project, :tracker, :subject, :assigned_to], query.columns.map(&:name) - end - - def test_index_without_project_and_explicit_default_columns_should_not_add_project_column - Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to'] - columns = ['tracker', 'subject', 'assigned_to'] - get :index, :set_filter => 1, :c => columns - - # query should use specified columns - query = assigns(:query) - assert_kind_of Query, query - assert_equal columns.map(&:to_sym), query.columns.map(&:name) - end - - def test_index_with_custom_field_column - columns = %w(tracker subject cf_2) - get :index, :set_filter => 1, :c => columns - assert_response :success - - # query should use specified columns - query = assigns(:query) - assert_kind_of Query, query - assert_equal columns, query.column_names.map(&:to_s) - - assert_tag :td, - :attributes => {:class => 'cf_2 string'}, - :ancestor => {:tag => 'table', :attributes => {:class => /issues/}} - end - - def test_index_with_date_column - Issue.find(1).update_attribute :start_date, '1987-08-24' - - with_settings :date_format => '%d/%m/%Y' do - get :index, :set_filter => 1, :c => %w(start_date) - assert_tag 'td', :attributes => {:class => /start_date/}, :content => '24/08/1987' - end - end - - def test_index_with_done_ratio - Issue.find(1).update_attribute :done_ratio, 40 - - get :index, :set_filter => 1, :c => %w(done_ratio) - assert_tag 'td', :attributes => {:class => /done_ratio/}, - :child => {:tag => 'table', :attributes => {:class => 'progress'}, - :descendant => {:tag => 'td', :attributes => {:class => 'closed', :style => 'width: 40%;'}} - } - end - - def test_index_with_fixed_version - get :index, :set_filter => 1, :c => %w(fixed_version) - assert_tag 'td', :attributes => {:class => /fixed_version/}, - :child => {:tag => 'a', :content => '1.0', :attributes => {:href => '/versions/2'}} - end - - def test_index_send_html_if_query_is_invalid - get :index, :f => ['start_date'], :op => {:start_date => '='} - assert_equal 'text/html', @response.content_type - assert_template 'index' - end - - def test_index_send_nothing_if_query_is_invalid - get :index, :f => ['start_date'], :op => {:start_date => '='}, :format => 'csv' - assert_equal 'text/csv', @response.content_type - assert @response.body.blank? - end - - def test_show_by_anonymous - get :show, :id => 1 - assert_response :success - assert_template 'show' - assert_not_nil assigns(:issue) - assert_equal Issue.find(1), assigns(:issue) - - # anonymous role is allowed to add a note - assert_tag :tag => 'form', - :descendant => { :tag => 'fieldset', - :child => { :tag => 'legend', - :content => /Notes/ } } - assert_tag :tag => 'title', - :content => "Bug #1: Can't print recipes - eCookbook - Redmine" - end - - def test_show_by_manager - @request.session[:user_id] = 2 - get :show, :id => 1 - assert_response :success - - assert_tag :tag => 'a', - :content => /Quote/ - - assert_tag :tag => 'form', - :descendant => { :tag => 'fieldset', - :child => { :tag => 'legend', - :content => /Change properties/ } }, - :descendant => { :tag => 'fieldset', - :child => { :tag => 'legend', - :content => /Log time/ } }, - :descendant => { :tag => 'fieldset', - :child => { :tag => 'legend', - :content => /Notes/ } } - end - - def test_update_form_should_not_display_inactive_enumerations - @request.session[:user_id] = 2 - get :show, :id => 1 - assert_response :success - - assert ! IssuePriority.find(15).active? - assert_no_tag :option, :attributes => {:value => '15'}, - :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} } - end - - def test_update_form_should_allow_attachment_upload - @request.session[:user_id] = 2 - get :show, :id => 1 - - assert_tag :tag => 'form', - :attributes => {:id => 'issue-form', :method => 'post', :enctype => 'multipart/form-data'}, - :descendant => { - :tag => 'input', - :attributes => {:type => 'file', :name => 'attachments[1][file]'} - } - end - - def test_show_should_deny_anonymous_access_without_permission - Role.anonymous.remove_permission!(:view_issues) - get :show, :id => 1 - assert_response :redirect - end - - def test_show_should_deny_anonymous_access_to_private_issue - Issue.update_all(["is_private = ?", true], "id = 1") - get :show, :id => 1 - assert_response :redirect - end - - def test_show_should_deny_non_member_access_without_permission - Role.non_member.remove_permission!(:view_issues) - @request.session[:user_id] = 9 - get :show, :id => 1 - assert_response 403 - end - - def test_show_should_deny_non_member_access_to_private_issue - Issue.update_all(["is_private = ?", true], "id = 1") - @request.session[:user_id] = 9 - get :show, :id => 1 - assert_response 403 - end - - def test_show_should_deny_member_access_without_permission - Role.find(1).remove_permission!(:view_issues) - @request.session[:user_id] = 2 - get :show, :id => 1 - assert_response 403 - end - - def test_show_should_deny_member_access_to_private_issue_without_permission - Issue.update_all(["is_private = ?", true], "id = 1") - @request.session[:user_id] = 3 - get :show, :id => 1 - assert_response 403 - end - - def test_show_should_allow_author_access_to_private_issue - Issue.update_all(["is_private = ?, author_id = 3", true], "id = 1") - @request.session[:user_id] = 3 - get :show, :id => 1 - assert_response :success - end - - def test_show_should_allow_assignee_access_to_private_issue - Issue.update_all(["is_private = ?, assigned_to_id = 3", true], "id = 1") - @request.session[:user_id] = 3 - get :show, :id => 1 - assert_response :success - end - - def test_show_should_allow_member_access_to_private_issue_with_permission - Issue.update_all(["is_private = ?", true], "id = 1") - User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all' - @request.session[:user_id] = 3 - get :show, :id => 1 - assert_response :success - end - - def test_show_should_not_disclose_relations_to_invisible_issues - Setting.cross_project_issue_relations = '1' - IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates') - # Relation to a private project issue - IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates') - - get :show, :id => 1 - assert_response :success - - assert_tag :div, :attributes => { :id => 'relations' }, - :descendant => { :tag => 'a', :content => /#2$/ } - assert_no_tag :div, :attributes => { :id => 'relations' }, - :descendant => { :tag => 'a', :content => /#4$/ } - end - - def test_show_atom - get :show, :id => 2, :format => 'atom' - assert_response :success - assert_template 'journals/index' - # Inline image - assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10')) - end - - def test_show_export_to_pdf - get :show, :id => 3, :format => 'pdf' - assert_response :success - assert_equal 'application/pdf', @response.content_type - assert @response.body.starts_with?('%PDF') - assert_not_nil assigns(:issue) - end - - def test_get_new - @request.session[:user_id] = 2 - get :new, :project_id => 1, :tracker_id => 1 - assert_response :success - assert_template 'new' - - assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]', - :value => 'Default string' } - - # Be sure we don't display inactive IssuePriorities - assert ! IssuePriority.find(15).active? - assert_no_tag :option, :attributes => {:value => '15'}, - :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} } - end - - def test_get_new_without_default_start_date_is_creation_date - Setting.default_issue_start_date_to_creation_date = 0 - - @request.session[:user_id] = 2 - get :new, :project_id => 1, :tracker_id => 1 - assert_response :success - assert_template 'new' - - assert_tag :tag => 'input', :attributes => { :name => 'issue[start_date]', - :value => nil } - end - - def test_get_new_with_default_start_date_is_creation_date - Setting.default_issue_start_date_to_creation_date = 1 - - @request.session[:user_id] = 2 - get :new, :project_id => 1, :tracker_id => 1 - assert_response :success - assert_template 'new' - - assert_tag :tag => 'input', :attributes => { :name => 'issue[start_date]', - :value => Date.today.to_s } - end - - def test_get_new_form_should_allow_attachment_upload - @request.session[:user_id] = 2 - get :new, :project_id => 1, :tracker_id => 1 - - assert_tag :tag => 'form', - :attributes => {:id => 'issue-form', :method => 'post', :enctype => 'multipart/form-data'}, - :descendant => { - :tag => 'input', - :attributes => {:type => 'file', :name => 'attachments[1][file]'} - } - end - - def test_get_new_without_tracker_id - @request.session[:user_id] = 2 - get :new, :project_id => 1 - assert_response :success - assert_template 'new' - - issue = assigns(:issue) - assert_not_nil issue - assert_equal Project.find(1).trackers.first, issue.tracker - end - - def test_get_new_with_no_default_status_should_display_an_error - @request.session[:user_id] = 2 - IssueStatus.delete_all - - get :new, :project_id => 1 - assert_response 500 - assert_error_tag :content => /No default issue/ - end - - def test_get_new_with_no_tracker_should_display_an_error - @request.session[:user_id] = 2 - Tracker.delete_all - - get :new, :project_id => 1 - assert_response 500 - assert_error_tag :content => /No tracker/ - end - - def test_update_new_form - @request.session[:user_id] = 2 - xhr :post, :new, :project_id => 1, - :issue => {:tracker_id => 2, - :subject => 'This is the test_new issue', - :description => 'This is the description', - :priority_id => 5} - assert_response :success - assert_template 'attributes' - - issue = assigns(:issue) - assert_kind_of Issue, issue - assert_equal 1, issue.project_id - assert_equal 2, issue.tracker_id - assert_equal 'This is the test_new issue', issue.subject - end - - def test_post_create - @request.session[:user_id] = 2 - assert_difference 'Issue.count' do - post :create, :project_id => 1, - :issue => {:tracker_id => 3, - :status_id => 2, - :subject => 'This is the test_new issue', - :description => 'This is the description', - :priority_id => 5, - :start_date => '2010-11-07', - :estimated_hours => '', - :custom_field_values => {'2' => 'Value for field 2'}} - end - assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id - - issue = Issue.find_by_subject('This is the test_new issue') - assert_not_nil issue - assert_equal 2, issue.author_id - assert_equal 3, issue.tracker_id - assert_equal 2, issue.status_id - assert_equal Date.parse('2010-11-07'), issue.start_date - assert_nil issue.estimated_hours - v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2}) - assert_not_nil v - assert_equal 'Value for field 2', v.value - end - - def test_post_new_with_group_assignment - group = Group.find(11) - project = Project.find(1) - project.members << Member.new(:principal => group, :roles => [Role.first]) - - with_settings :issue_group_assignment => '1' do - @request.session[:user_id] = 2 - assert_difference 'Issue.count' do - post :create, :project_id => project.id, - :issue => {:tracker_id => 3, - :status_id => 1, - :subject => 'This is the test_new_with_group_assignment issue', - :assigned_to_id => group.id} - end - end - assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id - - issue = Issue.find_by_subject('This is the test_new_with_group_assignment issue') - assert_not_nil issue - assert_equal group, issue.assigned_to - end - - def test_post_create_without_start_date_and_default_start_date_is_not_creation_date - Setting.default_issue_start_date_to_creation_date = 0 - - @request.session[:user_id] = 2 - assert_difference 'Issue.count' do - post :create, :project_id => 1, - :issue => {:tracker_id => 3, - :status_id => 2, - :subject => 'This is the test_new issue', - :description => 'This is the description', - :priority_id => 5, - :estimated_hours => '', - :custom_field_values => {'2' => 'Value for field 2'}} - end - assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id - - issue = Issue.find_by_subject('This is the test_new issue') - assert_not_nil issue - assert_nil issue.start_date - end - - def test_post_create_without_start_date_and_default_start_date_is_creation_date - Setting.default_issue_start_date_to_creation_date = 1 - - @request.session[:user_id] = 2 - assert_difference 'Issue.count' do - post :create, :project_id => 1, - :issue => {:tracker_id => 3, - :status_id => 2, - :subject => 'This is the test_new issue', - :description => 'This is the description', - :priority_id => 5, - :estimated_hours => '', - :custom_field_values => {'2' => 'Value for field 2'}} - end - assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id - - issue = Issue.find_by_subject('This is the test_new issue') - assert_not_nil issue - assert_equal Date.today, issue.start_date - end - - def test_post_create_and_continue - @request.session[:user_id] = 2 - assert_difference 'Issue.count' do - post :create, :project_id => 1, - :issue => {:tracker_id => 3, :subject => 'This is first issue', :priority_id => 5}, - :continue => '' - end - - issue = Issue.first(:order => 'id DESC') - assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook', :issue => {:tracker_id => 3} - assert_not_nil flash[:notice], "flash was not set" - assert flash[:notice].include?("##{issue.id}"), "issue link not found in flash: #{flash[:notice]}" - end - - def test_post_create_without_custom_fields_param - @request.session[:user_id] = 2 - assert_difference 'Issue.count' do - post :create, :project_id => 1, - :issue => {:tracker_id => 1, - :subject => 'This is the test_new issue', - :description => 'This is the description', - :priority_id => 5} - end - assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id - end - - def test_post_create_with_required_custom_field_and_without_custom_fields_param - field = IssueCustomField.find_by_name('Database') - field.update_attribute(:is_required, true) - - @request.session[:user_id] = 2 - post :create, :project_id => 1, - :issue => {:tracker_id => 1, - :subject => 'This is the test_new issue', - :description => 'This is the description', - :priority_id => 5} - assert_response :success - assert_template 'new' - issue = assigns(:issue) - assert_not_nil issue - assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values) - end - - def test_post_create_with_watchers - @request.session[:user_id] = 2 - ActionMailer::Base.deliveries.clear - - assert_difference 'Watcher.count', 2 do - post :create, :project_id => 1, - :issue => {:tracker_id => 1, - :subject => 'This is a new issue with watchers', - :description => 'This is the description', - :priority_id => 5, - :watcher_user_ids => ['2', '3']} - end - issue = Issue.find_by_subject('This is a new issue with watchers') - assert_not_nil issue - assert_redirected_to :controller => 'issues', :action => 'show', :id => issue - - # Watchers added - assert_equal [2, 3], issue.watcher_user_ids.sort - assert issue.watched_by?(User.find(3)) - # Watchers notified - mail = ActionMailer::Base.deliveries.last - assert_kind_of TMail::Mail, mail - assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail) - end - - def test_post_create_subissue - @request.session[:user_id] = 2 - - assert_difference 'Issue.count' do - post :create, :project_id => 1, - :issue => {:tracker_id => 1, - :subject => 'This is a child issue', - :parent_issue_id => 2} - end - issue = Issue.find_by_subject('This is a child issue') - assert_not_nil issue - assert_equal Issue.find(2), issue.parent - end - - def test_post_create_subissue_with_non_numeric_parent_id - @request.session[:user_id] = 2 - - assert_difference 'Issue.count' do - post :create, :project_id => 1, - :issue => {:tracker_id => 1, - :subject => 'This is a child issue', - :parent_issue_id => 'ABC'} - end - issue = Issue.find_by_subject('This is a child issue') - assert_not_nil issue - assert_nil issue.parent - end - - def test_post_create_private - @request.session[:user_id] = 2 - - assert_difference 'Issue.count' do - post :create, :project_id => 1, - :issue => {:tracker_id => 1, - :subject => 'This is a private issue', - :is_private => '1'} - end - issue = Issue.first(:order => 'id DESC') - assert issue.is_private? - end - - def test_post_create_private_with_set_own_issues_private_permission - role = Role.find(1) - role.remove_permission! :set_issues_private - role.add_permission! :set_own_issues_private - - @request.session[:user_id] = 2 - - assert_difference 'Issue.count' do - post :create, :project_id => 1, - :issue => {:tracker_id => 1, - :subject => 'This is a private issue', - :is_private => '1'} - end - issue = Issue.first(:order => 'id DESC') - assert issue.is_private? - end - - def test_post_create_should_send_a_notification - ActionMailer::Base.deliveries.clear - @request.session[:user_id] = 2 - assert_difference 'Issue.count' do - post :create, :project_id => 1, - :issue => {:tracker_id => 3, - :subject => 'This is the test_new issue', - :description => 'This is the description', - :priority_id => 5, - :estimated_hours => '', - :custom_field_values => {'2' => 'Value for field 2'}} - end - assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id - - assert_equal 1, ActionMailer::Base.deliveries.size - end - - def test_post_create_should_preserve_fields_values_on_validation_failure - @request.session[:user_id] = 2 - post :create, :project_id => 1, - :issue => {:tracker_id => 1, - # empty subject - :subject => '', - :description => 'This is a description', - :priority_id => 6, - :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}} - assert_response :success - assert_template 'new' - - assert_tag :textarea, :attributes => { :name => 'issue[description]' }, - :content => 'This is a description' - assert_tag :select, :attributes => { :name => 'issue[priority_id]' }, - :child => { :tag => 'option', :attributes => { :selected => 'selected', - :value => '6' }, - :content => 'High' } - # Custom fields - assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' }, - :child => { :tag => 'option', :attributes => { :selected => 'selected', - :value => 'Oracle' }, - :content => 'Oracle' } - assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]', - :value => 'Value for field 2'} - end - - def test_post_create_should_ignore_non_safe_attributes - @request.session[:user_id] = 2 - assert_nothing_raised do - post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" } - end - end - - def test_post_create_with_attachment - set_tmp_attachments_directory - @request.session[:user_id] = 2 - - assert_difference 'Issue.count' do - assert_difference 'Attachment.count' do - post :create, :project_id => 1, - :issue => { :tracker_id => '1', :subject => 'With attachment' }, - :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}} - end - end - - issue = Issue.first(:order => 'id DESC') - attachment = Attachment.first(:order => 'id DESC') - - assert_equal issue, attachment.container - assert_equal 2, attachment.author_id - assert_equal 'testfile.txt', attachment.filename - assert_equal 'text/plain', attachment.content_type - assert_equal 'test file', attachment.description - assert_equal 59, attachment.filesize - assert File.exists?(attachment.diskfile) - assert_equal 59, File.size(attachment.diskfile) - end - - context "without workflow privilege" do - setup do - Workflow.delete_all(["role_id = ?", Role.anonymous.id]) - Role.anonymous.add_permission! :add_issues, :add_issue_notes - end - - context "#new" do - should "propose default status only" do - get :new, :project_id => 1 - assert_response :success - assert_template 'new' - assert_tag :tag => 'select', - :attributes => {:name => 'issue[status_id]'}, - :children => {:count => 1}, - :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}} - end - - should "accept default status" do - assert_difference 'Issue.count' do - post :create, :project_id => 1, - :issue => {:tracker_id => 1, - :subject => 'This is an issue', - :status_id => 1} - end - issue = Issue.last(:order => 'id') - assert_equal IssueStatus.default, issue.status - end - - should "ignore unauthorized status" do - assert_difference 'Issue.count' do - post :create, :project_id => 1, - :issue => {:tracker_id => 1, - :subject => 'This is an issue', - :status_id => 3} - end - issue = Issue.last(:order => 'id') - assert_equal IssueStatus.default, issue.status - end - end - - context "#update" do - should "ignore status change" do - assert_difference 'Journal.count' do - put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3} - end - assert_equal 1, Issue.find(1).status_id - end - - should "ignore attributes changes" do - assert_difference 'Journal.count' do - put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2} - end - issue = Issue.find(1) - assert_equal "Can't print recipes", issue.subject - assert_nil issue.assigned_to - end - end - end - - context "with workflow privilege" do - setup do - Workflow.delete_all(["role_id = ?", Role.anonymous.id]) - Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3) - Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4) - Role.anonymous.add_permission! :add_issues, :add_issue_notes - end - - context "#update" do - should "accept authorized status" do - assert_difference 'Journal.count' do - put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3} - end - assert_equal 3, Issue.find(1).status_id - end - - should "ignore unauthorized status" do - assert_difference 'Journal.count' do - put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2} - end - assert_equal 1, Issue.find(1).status_id - end - - should "accept authorized attributes changes" do - assert_difference 'Journal.count' do - put :update, :id => 1, :notes => 'just trying', :issue => {:assigned_to_id => 2} - end - issue = Issue.find(1) - assert_equal 2, issue.assigned_to_id - end - - should "ignore unauthorized attributes changes" do - assert_difference 'Journal.count' do - put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed'} - end - issue = Issue.find(1) - assert_equal "Can't print recipes", issue.subject - end - end - - context "and :edit_issues permission" do - setup do - Role.anonymous.add_permission! :add_issues, :edit_issues - end - - should "accept authorized status" do - assert_difference 'Journal.count' do - put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3} - end - assert_equal 3, Issue.find(1).status_id - end - - should "ignore unauthorized status" do - assert_difference 'Journal.count' do - put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2} - end - assert_equal 1, Issue.find(1).status_id - end - - should "accept authorized attributes changes" do - assert_difference 'Journal.count' do - put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2} - end - issue = Issue.find(1) - assert_equal "changed", issue.subject - assert_equal 2, issue.assigned_to_id - end - end - end - - def test_copy_issue - @request.session[:user_id] = 2 - get :new, :project_id => 1, :copy_from => 1 - assert_template 'new' - assert_not_nil assigns(:issue) - orig = Issue.find(1) - assert_equal orig.subject, assigns(:issue).subject - end - - def test_get_edit - @request.session[:user_id] = 2 - get :edit, :id => 1 - assert_response :success - assert_template 'edit' - assert_not_nil assigns(:issue) - assert_equal Issue.find(1), assigns(:issue) - - # Be sure we don't display inactive IssuePriorities - assert ! IssuePriority.find(15).active? - assert_no_tag :option, :attributes => {:value => '15'}, - :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} } - end - - def test_get_edit_should_display_the_time_entry_form_with_log_time_permission - @request.session[:user_id] = 2 - Role.find_by_name('Manager').update_attribute :permissions, [:view_issues, :edit_issues, :log_time] - - get :edit, :id => 1 - assert_tag 'input', :attributes => {:name => 'time_entry[hours]'} - end - - def test_get_edit_should_not_display_the_time_entry_form_without_log_time_permission - @request.session[:user_id] = 2 - Role.find_by_name('Manager').remove_permission! :log_time - - get :edit, :id => 1 - assert_no_tag 'input', :attributes => {:name => 'time_entry[hours]'} - end - - def test_get_edit_with_params - @request.session[:user_id] = 2 - get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }, - :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => TimeEntryActivity.first.id } - assert_response :success - assert_template 'edit' - - issue = assigns(:issue) - assert_not_nil issue - - assert_equal 5, issue.status_id - assert_tag :select, :attributes => { :name => 'issue[status_id]' }, - :child => { :tag => 'option', - :content => 'Closed', - :attributes => { :selected => 'selected' } } - - assert_equal 7, issue.priority_id - assert_tag :select, :attributes => { :name => 'issue[priority_id]' }, - :child => { :tag => 'option', - :content => 'Urgent', - :attributes => { :selected => 'selected' } } - - assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => '2.5' } - assert_tag :select, :attributes => { :name => 'time_entry[activity_id]' }, - :child => { :tag => 'option', - :attributes => { :selected => 'selected', :value => TimeEntryActivity.first.id } } - assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => 'test_get_edit_with_params' } - end - - def test_update_edit_form - @request.session[:user_id] = 2 - xhr :post, :new, :project_id => 1, - :id => 1, - :issue => {:tracker_id => 2, - :subject => 'This is the test_new issue', - :description => 'This is the description', - :priority_id => 5} - assert_response :success - assert_template 'attributes' - - issue = assigns(:issue) - assert_kind_of Issue, issue - assert_equal 1, issue.id - assert_equal 1, issue.project_id - assert_equal 2, issue.tracker_id - assert_equal 'This is the test_new issue', issue.subject - end - - def test_update_using_invalid_http_verbs - @request.session[:user_id] = 2 - subject = 'Updated by an invalid http verb' - - get :update, :id => 1, :issue => {:subject => subject} - assert_not_equal subject, Issue.find(1).subject - - post :update, :id => 1, :issue => {:subject => subject} - assert_not_equal subject, Issue.find(1).subject - - delete :update, :id => 1, :issue => {:subject => subject} - assert_not_equal subject, Issue.find(1).subject - end - - def test_put_update_without_custom_fields_param - @request.session[:user_id] = 2 - ActionMailer::Base.deliveries.clear - - issue = Issue.find(1) - assert_equal '125', issue.custom_value_for(2).value - old_subject = issue.subject - new_subject = 'Subject modified by IssuesControllerTest#test_post_edit' - - assert_difference('Journal.count') do - assert_difference('JournalDetail.count', 2) do - put :update, :id => 1, :issue => {:subject => new_subject, - :priority_id => '6', - :category_id => '1' # no change - } - end - end - assert_redirected_to :action => 'show', :id => '1' - issue.reload - assert_equal new_subject, issue.subject - # Make sure custom fields were not cleared - assert_equal '125', issue.custom_value_for(2).value - - mail = ActionMailer::Base.deliveries.last - assert_kind_of TMail::Mail, mail - assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]") - assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}") - end - - def test_put_update_with_custom_field_change - @request.session[:user_id] = 2 - issue = Issue.find(1) - assert_equal '125', issue.custom_value_for(2).value - - assert_difference('Journal.count') do - assert_difference('JournalDetail.count', 3) do - put :update, :id => 1, :issue => {:subject => 'Custom field change', - :priority_id => '6', - :category_id => '1', # no change - :custom_field_values => { '2' => 'New custom value' } - } - end - end - assert_redirected_to :action => 'show', :id => '1' - issue.reload - assert_equal 'New custom value', issue.custom_value_for(2).value - - mail = ActionMailer::Base.deliveries.last - assert_kind_of TMail::Mail, mail - assert mail.body.include?("Searchable field changed from 125 to New custom value") - end - - def test_put_update_with_status_and_assignee_change - issue = Issue.find(1) - assert_equal 1, issue.status_id - @request.session[:user_id] = 2 - assert_difference('TimeEntry.count', 0) do - put :update, - :id => 1, - :issue => { :status_id => 2, :assigned_to_id => 3 }, - :notes => 'Assigned to dlopper', - :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first } - end - assert_redirected_to :action => 'show', :id => '1' - issue.reload - assert_equal 2, issue.status_id - j = Journal.find(:first, :order => 'id DESC') - assert_equal 'Assigned to dlopper', j.notes - assert_equal 2, j.details.size - - mail = ActionMailer::Base.deliveries.last - assert mail.body.include?("Status changed from New to Assigned") - # subject should contain the new status - assert mail.subject.include?("(#{ IssueStatus.find(2).name })") - end - - def test_put_update_with_note_only - notes = 'Note added by IssuesControllerTest#test_update_with_note_only' - # anonymous user - put :update, - :id => 1, - :notes => notes - assert_redirected_to :action => 'show', :id => '1' - j = Journal.find(:first, :order => 'id DESC') - assert_equal notes, j.notes - assert_equal 0, j.details.size - assert_equal User.anonymous, j.user - - mail = ActionMailer::Base.deliveries.last - assert mail.body.include?(notes) - end - - def test_put_update_with_note_and_spent_time - @request.session[:user_id] = 2 - spent_hours_before = Issue.find(1).spent_hours - assert_difference('TimeEntry.count') do - put :update, - :id => 1, - :notes => '2.5 hours added', - :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id } - end - assert_redirected_to :action => 'show', :id => '1' - - issue = Issue.find(1) - - j = Journal.find(:first, :order => 'id DESC') - assert_equal '2.5 hours added', j.notes - assert_equal 0, j.details.size - - t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time') - assert_not_nil t - assert_equal 2.5, t.hours - assert_equal spent_hours_before + 2.5, issue.spent_hours - end - - def test_put_update_with_attachment_only - set_tmp_attachments_directory - - # Delete all fixtured journals, a race condition can occur causing the wrong - # journal to get fetched in the next find. - Journal.delete_all - - # anonymous user - assert_difference 'Attachment.count' do - put :update, :id => 1, - :notes => '', - :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}} - end - - assert_redirected_to :action => 'show', :id => '1' - j = Issue.find(1).journals.find(:first, :order => 'id DESC') - assert j.notes.blank? - assert_equal 1, j.details.size - assert_equal 'testfile.txt', j.details.first.value - assert_equal User.anonymous, j.user - - attachment = Attachment.first(:order => 'id DESC') - assert_equal Issue.find(1), attachment.container - assert_equal User.anonymous, attachment.author - assert_equal 'testfile.txt', attachment.filename - assert_equal 'text/plain', attachment.content_type - assert_equal 'test file', attachment.description - assert_equal 59, attachment.filesize - assert File.exists?(attachment.diskfile) - assert_equal 59, File.size(attachment.diskfile) - - mail = ActionMailer::Base.deliveries.last - assert mail.body.include?('testfile.txt') - end - - def test_put_update_with_attachment_that_fails_to_save - set_tmp_attachments_directory - - # Delete all fixtured journals, a race condition can occur causing the wrong - # journal to get fetched in the next find. - Journal.delete_all - - # Mock out the unsaved attachment - Attachment.any_instance.stubs(:create).returns(Attachment.new) - - # anonymous user - put :update, - :id => 1, - :notes => '', - :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}} - assert_redirected_to :action => 'show', :id => '1' - assert_equal '1 file(s) could not be saved.', flash[:warning] - - end if Object.const_defined?(:Mocha) - - def test_put_update_with_no_change - issue = Issue.find(1) - issue.journals.clear - ActionMailer::Base.deliveries.clear - - put :update, - :id => 1, - :notes => '' - assert_redirected_to :action => 'show', :id => '1' - - issue.reload - assert issue.journals.empty? - # No email should be sent - assert ActionMailer::Base.deliveries.empty? - end - - def test_put_update_should_send_a_notification - @request.session[:user_id] = 2 - ActionMailer::Base.deliveries.clear - issue = Issue.find(1) - old_subject = issue.subject - new_subject = 'Subject modified by IssuesControllerTest#test_post_edit' - - put :update, :id => 1, :issue => {:subject => new_subject, - :priority_id => '6', - :category_id => '1' # no change - } - assert_equal 1, ActionMailer::Base.deliveries.size - end - - def test_put_update_with_invalid_spent_time_hours_only - @request.session[:user_id] = 2 - notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time' - - assert_no_difference('Journal.count') do - put :update, - :id => 1, - :notes => notes, - :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"} - end - assert_response :success - assert_template 'edit' - - assert_error_tag :descendant => {:content => /Activity can't be blank/} - assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes - assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" } - end - - def test_put_update_with_invalid_spent_time_comments_only - @request.session[:user_id] = 2 - notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time' - - assert_no_difference('Journal.count') do - put :update, - :id => 1, - :notes => notes, - :time_entry => {"comments"=>"this is my comment", "activity_id"=>"", "hours"=>""} - end - assert_response :success - assert_template 'edit' - - assert_error_tag :descendant => {:content => /Activity can't be blank/} - assert_error_tag :descendant => {:content => /Hours can't be blank/} - assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes - assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => "this is my comment" } - end - - def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject - issue = Issue.find(2) - @request.session[:user_id] = 2 - - put :update, - :id => issue.id, - :issue => { - :fixed_version_id => 4 - } - - assert_response :redirect - issue.reload - assert_equal 4, issue.fixed_version_id - assert_not_equal issue.project_id, issue.fixed_version.project_id - end - - def test_put_update_should_redirect_back_using_the_back_url_parameter - issue = Issue.find(2) - @request.session[:user_id] = 2 - - put :update, - :id => issue.id, - :issue => { - :fixed_version_id => 4 - }, - :back_url => '/issues' - - assert_response :redirect - assert_redirected_to '/issues' - end - - def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host - issue = Issue.find(2) - @request.session[:user_id] = 2 - - put :update, - :id => issue.id, - :issue => { - :fixed_version_id => 4 - }, - :back_url => 'http://google.com' - - assert_response :redirect - assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id - end - - def test_get_bulk_edit - @request.session[:user_id] = 2 - get :bulk_edit, :ids => [1, 2] - assert_response :success - assert_template 'bulk_edit' - - assert_tag :input, :attributes => {:name => 'issue[parent_issue_id]'} - - # Project specific custom field, date type - field = CustomField.find(9) - assert !field.is_for_all? - assert_equal 'date', field.field_format - assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'} - - # System wide custom field - assert CustomField.find(1).is_for_all? - assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'} - - # Be sure we don't display inactive IssuePriorities - assert ! IssuePriority.find(15).active? - assert_no_tag :option, :attributes => {:value => '15'}, - :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} } - end - - def test_get_bulk_edit_on_different_projects - @request.session[:user_id] = 2 - get :bulk_edit, :ids => [1, 2, 6] - assert_response :success - assert_template 'bulk_edit' - - # Can not set issues from different projects as children of an issue - assert_no_tag :input, :attributes => {:name => 'issue[parent_issue_id]'} - - # Project specific custom field, date type - field = CustomField.find(9) - assert !field.is_for_all? - assert !field.project_ids.include?(Issue.find(6).project_id) - assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'} - end - - def test_get_bulk_edit_with_user_custom_field - field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true) - - @request.session[:user_id] = 2 - get :bulk_edit, :ids => [1, 2] - assert_response :success - assert_template 'bulk_edit' - - assert_tag :select, - :attributes => {:name => "issue[custom_field_values][#{field.id}]"}, - :children => { - :only => {:tag => 'option'}, - :count => Project.find(1).users.count + 1 - } - end - - def test_get_bulk_edit_with_version_custom_field - field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true) - - @request.session[:user_id] = 2 - get :bulk_edit, :ids => [1, 2] - assert_response :success - assert_template 'bulk_edit' - - assert_tag :select, - :attributes => {:name => "issue[custom_field_values][#{field.id}]"}, - :children => { - :only => {:tag => 'option'}, - :count => Project.find(1).shared_versions.count + 1 - } - end - - def test_bulk_update - @request.session[:user_id] = 2 - # update issues priority - post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing', - :issue => {:priority_id => 7, - :assigned_to_id => '', - :custom_field_values => {'2' => ''}} - - assert_response 302 - # check that the issues were updated - assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id} - - issue = Issue.find(1) - journal = issue.journals.find(:first, :order => 'created_on DESC') - assert_equal '125', issue.custom_value_for(2).value - assert_equal 'Bulk editing', journal.notes - assert_equal 1, journal.details.size - end - - def test_bulk_update_with_group_assignee - group = Group.find(11) - project = Project.find(1) - project.members << Member.new(:principal => group, :roles => [Role.first]) - - @request.session[:user_id] = 2 - # update issues assignee - post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing', - :issue => {:priority_id => '', - :assigned_to_id => group.id, - :custom_field_values => {'2' => ''}} - - assert_response 302 - assert_equal [group, group], Issue.find_all_by_id([1, 2]).collect {|i| i.assigned_to} - end - - def test_bulk_update_on_different_projects - @request.session[:user_id] = 2 - # update issues priority - post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing', - :issue => {:priority_id => 7, - :assigned_to_id => '', - :custom_field_values => {'2' => ''}} - - assert_response 302 - # check that the issues were updated - assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id) - - issue = Issue.find(1) - journal = issue.journals.find(:first, :order => 'created_on DESC') - assert_equal '125', issue.custom_value_for(2).value - assert_equal 'Bulk editing', journal.notes - assert_equal 1, journal.details.size - end - - def test_bulk_update_on_different_projects_without_rights - @request.session[:user_id] = 3 - user = User.find(3) - action = { :controller => "issues", :action => "bulk_update" } - assert user.allowed_to?(action, Issue.find(1).project) - assert ! user.allowed_to?(action, Issue.find(6).project) - post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail', - :issue => {:priority_id => 7, - :assigned_to_id => '', - :custom_field_values => {'2' => ''}} - assert_response 403 - assert_not_equal "Bulk should fail", Journal.last.notes - end - - def test_bullk_update_should_send_a_notification - @request.session[:user_id] = 2 - ActionMailer::Base.deliveries.clear - post(:bulk_update, - { - :ids => [1, 2], - :notes => 'Bulk editing', - :issue => { - :priority_id => 7, - :assigned_to_id => '', - :custom_field_values => {'2' => ''} - } - }) - - assert_response 302 - assert_equal 2, ActionMailer::Base.deliveries.size - end - - def test_bulk_update_status - @request.session[:user_id] = 2 - # update issues priority - post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status', - :issue => {:priority_id => '', - :assigned_to_id => '', - :status_id => '5'} - - assert_response 302 - issue = Issue.find(1) - assert issue.closed? - end - - def test_bulk_update_parent_id - @request.session[:user_id] = 2 - post :bulk_update, :ids => [1, 3], - :notes => 'Bulk editing parent', - :issue => {:priority_id => '', :assigned_to_id => '', :status_id => '', :parent_issue_id => '2'} - - assert_response 302 - parent = Issue.find(2) - assert_equal parent.id, Issue.find(1).parent_id - assert_equal parent.id, Issue.find(3).parent_id - assert_equal [1, 3], parent.children.collect(&:id).sort - end - - def test_bulk_update_custom_field - @request.session[:user_id] = 2 - # update issues priority - post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field', - :issue => {:priority_id => '', - :assigned_to_id => '', - :custom_field_values => {'2' => '777'}} - - assert_response 302 - - issue = Issue.find(1) - journal = issue.journals.find(:first, :order => 'created_on DESC') - assert_equal '777', issue.custom_value_for(2).value - assert_equal 1, journal.details.size - assert_equal '125', journal.details.first.old_value - assert_equal '777', journal.details.first.value - end - - def test_bulk_update_unassign - assert_not_nil Issue.find(2).assigned_to - @request.session[:user_id] = 2 - # unassign issues - post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'} - assert_response 302 - # check that the issues were updated - assert_nil Issue.find(2).assigned_to - end - - def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject - @request.session[:user_id] = 2 - - post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4} - - assert_response :redirect - issues = Issue.find([1,2]) - issues.each do |issue| - assert_equal 4, issue.fixed_version_id - assert_not_equal issue.project_id, issue.fixed_version.project_id - end - end - - def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter - @request.session[:user_id] = 2 - post :bulk_update, :ids => [1,2], :back_url => '/issues' - - assert_response :redirect - assert_redirected_to '/issues' - end - - def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host - @request.session[:user_id] = 2 - post :bulk_update, :ids => [1,2], :back_url => 'http://google.com' - - assert_response :redirect - assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier - end - - def test_destroy_issue_with_no_time_entries - assert_nil TimeEntry.find_by_issue_id(2) - @request.session[:user_id] = 2 - post :destroy, :id => 2 - assert_redirected_to :action => 'index', :project_id => 'ecookbook' - assert_nil Issue.find_by_id(2) - end - - def test_destroy_issues_with_time_entries - @request.session[:user_id] = 2 - post :destroy, :ids => [1, 3] - assert_response :success - assert_template 'destroy' - assert_not_nil assigns(:hours) - assert Issue.find_by_id(1) && Issue.find_by_id(3) - end - - def test_destroy_issues_and_destroy_time_entries - @request.session[:user_id] = 2 - post :destroy, :ids => [1, 3], :todo => 'destroy' - assert_redirected_to :action => 'index', :project_id => 'ecookbook' - assert !(Issue.find_by_id(1) || Issue.find_by_id(3)) - assert_nil TimeEntry.find_by_id([1, 2]) - end - - def test_destroy_issues_and_assign_time_entries_to_project - @request.session[:user_id] = 2 - post :destroy, :ids => [1, 3], :todo => 'nullify' - assert_redirected_to :action => 'index', :project_id => 'ecookbook' - assert !(Issue.find_by_id(1) || Issue.find_by_id(3)) - assert_nil TimeEntry.find(1).issue_id - assert_nil TimeEntry.find(2).issue_id - end - - def test_destroy_issues_and_reassign_time_entries_to_another_issue - @request.session[:user_id] = 2 - post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2 - assert_redirected_to :action => 'index', :project_id => 'ecookbook' - assert !(Issue.find_by_id(1) || Issue.find_by_id(3)) - assert_equal 2, TimeEntry.find(1).issue_id - assert_equal 2, TimeEntry.find(2).issue_id - end - - def test_destroy_issues_from_different_projects - @request.session[:user_id] = 2 - post :destroy, :ids => [1, 2, 6], :todo => 'destroy' - assert_redirected_to :controller => 'issues', :action => 'index' - assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6)) - end - - def test_destroy_parent_and_child_issues - parent = Issue.generate!(:project_id => 1, :tracker_id => 1) - child = Issue.generate!(:project_id => 1, :tracker_id => 1, :parent_issue_id => parent.id) - assert child.is_descendant_of?(parent.reload) - - @request.session[:user_id] = 2 - assert_difference 'Issue.count', -2 do - post :destroy, :ids => [parent.id, child.id], :todo => 'destroy' - end - assert_response 302 - end - - def test_default_search_scope - get :index - assert_tag :div, :attributes => {:id => 'quick-search'}, - :child => {:tag => 'form', - :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}} - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/36/36da07b1ee2e0ce6db594d8c9206610d637ebf42.svn-base --- a/.svn/pristine/36/36da07b1ee2e0ce6db594d8c9206610d637ebf42.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ -class Repository::Subversion < Repository - generator_for :type, :method => 'Subversion' - generator_for :url, :start => 'file:///test/svn' - -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/36/36ee24f61bb993329a397c77fd4b05efc6e92acd.svn-base --- a/.svn/pristine/36/36ee24f61bb993329a397c77fd4b05efc6e92acd.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ -class Issue < ActiveRecord::Base - generator_for :subject, :start => 'Subject 0' - generator_for :author, :method => :next_author - generator_for :priority, :method => :fetch_priority - - def self.next_author - User.generate_with_protected! - end - - def self.fetch_priority - IssuePriority.first || IssuePriority.generate! - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/36/36f96aa6819066dbc18f6f0375ee24e3c2d52b96.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/36/36f96aa6819066dbc18f6f0375ee24e3c2d52b96.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,296 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class AccountController < ApplicationController + helper :custom_fields + include CustomFieldsHelper + + # prevents login action to be filtered by check_if_login_required application scope filter + skip_before_filter :check_if_login_required + + # Login request and validation + def login + if request.get? + logout_user + else + authenticate_user + end + rescue AuthSourceException => e + logger.error "An error occured when authenticating #{params[:username]}: #{e.message}" + render_error :message => e.message + end + + # Log out current user and redirect to welcome page + def logout + logout_user + redirect_to home_url + end + + # Lets user choose a new password + def lost_password + redirect_to(home_url) && return unless Setting.lost_password? + if params[:token] + @token = Token.find_by_action_and_value("recovery", params[:token].to_s) + if @token.nil? || @token.expired? + redirect_to home_url + return + end + @user = @token.user + unless @user && @user.active? + redirect_to home_url + return + end + if request.post? + @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation] + if @user.save + @token.destroy + flash[:notice] = l(:notice_account_password_updated) + redirect_to signin_path + return + end + end + render :template => "account/password_recovery" + return + else + if request.post? + user = User.find_by_mail(params[:mail].to_s) + # user not found or not active + unless user && user.active? + flash.now[:error] = l(:notice_account_unknown_email) + return + end + # user cannot change its password + unless user.change_password_allowed? + flash.now[:error] = l(:notice_can_t_change_password) + return + end + # create a new token for password recovery + token = Token.new(:user => user, :action => "recovery") + if token.save + Mailer.lost_password(token).deliver + flash[:notice] = l(:notice_account_lost_email_sent) + redirect_to signin_path + return + end + end + end + end + + # User self-registration + def register + redirect_to(home_url) && return unless Setting.self_registration? || session[:auth_source_registration] + if request.get? + session[:auth_source_registration] = nil + @user = User.new(:language => Setting.default_language) + else + user_params = params[:user] || {} + @user = User.new + @user.safe_attributes = user_params + @user.admin = false + @user.register + if session[:auth_source_registration] + @user.activate + @user.login = session[:auth_source_registration][:login] + @user.auth_source_id = session[:auth_source_registration][:auth_source_id] + if @user.save + session[:auth_source_registration] = nil + self.logged_user = @user + flash[:notice] = l(:notice_account_activated) + redirect_to :controller => 'my', :action => 'account' + end + else + @user.login = params[:user][:login] + unless user_params[:identity_url].present? && user_params[:password].blank? && user_params[:password_confirmation].blank? + @user.password, @user.password_confirmation = user_params[:password], user_params[:password_confirmation] + end + + case Setting.self_registration + when '1' + register_by_email_activation(@user) + when '3' + register_automatically(@user) + else + register_manually_by_administrator(@user) + end + end + end + end + + # Token based account activation + def activate + redirect_to(home_url) && return unless Setting.self_registration? && params[:token] + token = Token.find_by_action_and_value('register', params[:token]) + redirect_to(home_url) && return unless token and !token.expired? + user = token.user + redirect_to(home_url) && return unless user.registered? + user.activate + if user.save + token.destroy + flash[:notice] = l(:notice_account_activated) + end + redirect_to signin_path + end + + private + + def authenticate_user + if Setting.openid? && using_open_id? + open_id_authenticate(params[:openid_url]) + else + password_authentication + end + end + + def password_authentication + user = User.try_to_login(params[:username], params[:password]) + + if user.nil? + invalid_credentials + elsif user.new_record? + onthefly_creation_failed(user, {:login => user.login, :auth_source_id => user.auth_source_id }) + else + # Valid user + successful_authentication(user) + end + end + + def open_id_authenticate(openid_url) + authenticate_with_open_id(openid_url, :required => [:nickname, :fullname, :email], :return_to => signin_url, :method => :post) do |result, identity_url, registration| + if result.successful? + user = User.find_or_initialize_by_identity_url(identity_url) + if user.new_record? + # Self-registration off + redirect_to(home_url) && return unless Setting.self_registration? + + # Create on the fly + user.login = registration['nickname'] unless registration['nickname'].nil? + user.mail = registration['email'] unless registration['email'].nil? + user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil? + user.random_password + user.register + + case Setting.self_registration + when '1' + register_by_email_activation(user) do + onthefly_creation_failed(user) + end + when '3' + register_automatically(user) do + onthefly_creation_failed(user) + end + else + register_manually_by_administrator(user) do + onthefly_creation_failed(user) + end + end + else + # Existing record + if user.active? + successful_authentication(user) + else + account_pending + end + end + end + end + end + + def successful_authentication(user) + logger.info "Successful authentication for '#{user.login}' from #{request.remote_ip} at #{Time.now.utc}" + # Valid user + self.logged_user = user + # generate a key and set cookie if autologin + if params[:autologin] && Setting.autologin? + set_autologin_cookie(user) + end + call_hook(:controller_account_success_authentication_after, {:user => user }) + redirect_back_or_default :controller => 'my', :action => 'page' + end + + def set_autologin_cookie(user) + token = Token.create(:user => user, :action => 'autologin') + cookie_name = Redmine::Configuration['autologin_cookie_name'] || 'autologin' + cookie_options = { + :value => token.value, + :expires => 1.year.from_now, + :path => (Redmine::Configuration['autologin_cookie_path'] || '/'), + :secure => (Redmine::Configuration['autologin_cookie_secure'] ? true : false), + :httponly => true + } + cookies[cookie_name] = cookie_options + end + + # Onthefly creation failed, display the registration form to fill/fix attributes + def onthefly_creation_failed(user, auth_source_options = { }) + @user = user + session[:auth_source_registration] = auth_source_options unless auth_source_options.empty? + render :action => 'register' + end + + def invalid_credentials + logger.warn "Failed login for '#{params[:username]}' from #{request.remote_ip} at #{Time.now.utc}" + flash.now[:error] = l(:notice_account_invalid_creditentials) + end + + # Register a user for email activation. + # + # Pass a block for behavior when a user fails to save + def register_by_email_activation(user, &block) + token = Token.new(:user => user, :action => "register") + if user.save and token.save + Mailer.register(token).deliver + flash[:notice] = l(:notice_account_register_done) + redirect_to signin_path + else + yield if block_given? + end + end + + # Automatically register a user + # + # Pass a block for behavior when a user fails to save + def register_automatically(user, &block) + # Automatic activation + user.activate + user.last_login_on = Time.now + if user.save + self.logged_user = user + flash[:notice] = l(:notice_account_activated) + redirect_to :controller => 'my', :action => 'account' + else + yield if block_given? + end + end + + # Manual activation by the administrator + # + # Pass a block for behavior when a user fails to save + def register_manually_by_administrator(user, &block) + if user.save + # Sends an email to the administrators + Mailer.account_activation_request(user).deliver + account_pending + else + yield if block_given? + end + end + + def account_pending + flash[:notice] = l(:notice_account_pending) + redirect_to signin_path + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/37/37b8a238cdb19b858fe5e9a42787ac44cba6e2d2.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/37/37b8a238cdb19b858fe5e9a42787ac44cba6e2d2.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,14 @@ +2.0.2 +* Fixed deprecation warning under Rails 3.1 [Philip Arndt] +* Converted Test::Unit matchers to RSpec. [UÄ£is Ozols] +* Added inverse_of to associations to improve performance rendering trees. [Sergio Cambra] +* Added row locking and fixed some race conditions. [Markus J. Q. Roberts] + +2.0.1 +* Fixed a bug with move_to not using nested_set_scope [Andreas Sekine] + +2.0.0.pre +* Expect Rails 3 +* Changed how callbacks work. Returning false in a before_move action does not block save operations. Use a validation or exception in the callback if you need that. +* Switched to RSpec +* Remove use of Comparable diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/38/382e2cf70f6b21745f336fa99cf77293579a69ff.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/38/382e2cf70f6b21745f336fa99cf77293579a69ff.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,9 @@ +module OpenIdAuthentication + class Association < ActiveRecord::Base + self.table_name = :open_id_authentication_associations + + def from_record + OpenID::Association.new(handle, secret, issued, lifetime, assoc_type) + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/38/385c6750511d0a36b5d915c83580ce155323ad24.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/38/385c6750511d0a36b5d915c83580ce155323ad24.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1085 @@ +# Serbian translations for Redmine +# by Vladimir Medarović (vlada@medarovic.com) +sr-YU: + direction: ltr + jquery: + locale: "sr" + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%d.%m.%Y." + short: "%e %b" + long: "%B %e, %Y" + + day_names: [nedelja, ponedeljak, utorak, sreda, Äetvrtak, petak, subota] + abbr_day_names: [ned, pon, uto, sre, Äet, pet, sub] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, januar, februar, mart, april, maj, jun, jul, avgust, septembar, oktobar, novembar, decembar] + abbr_month_names: [~, jan, feb, mar, apr, maj, jun, jul, avg, sep, okt, nov, dec] + # Used in date_select and datime_select. + order: + - :day + - :month + - :year + + time: + formats: + default: "%d.%m.%Y. u %H:%M" + time: "%H:%M" + short: "%d. %b u %H:%M" + long: "%d. %B %Y u %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "pola minuta" + less_than_x_seconds: + one: "manje od jedne sekunde" + other: "manje od %{count} sek." + x_seconds: + one: "jedna sekunda" + other: "%{count} sek." + less_than_x_minutes: + one: "manje od minuta" + other: "manje od %{count} min." + x_minutes: + one: "jedan minut" + other: "%{count} min." + about_x_hours: + one: "približno jedan sat" + other: "približno %{count} sati" + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "jedan dan" + other: "%{count} dana" + about_x_months: + one: "približno jedan mesec" + other: "približno %{count} meseci" + x_months: + one: "jedan mesec" + other: "%{count} meseci" + about_x_years: + one: "približno godinu dana" + other: "približno %{count} god." + over_x_years: + one: "preko godinu dana" + other: "preko %{count} god." + almost_x_years: + one: "skoro godinu dana" + other: "skoro %{count} god." + + number: + format: + separator: "," + delimiter: "" + precision: 3 + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + + +# Used in array.to_sentence. + support: + array: + sentence_connector: "i" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + messages: + inclusion: "nije ukljuÄen u spisak" + exclusion: "je rezervisan" + invalid: "je neispravan" + confirmation: "potvrda ne odgovara" + accepted: "mora biti prihvaćen" + empty: "ne može biti prazno" + blank: "ne može biti prazno" + too_long: "je predugaÄka (maksimum znakova je %{count})" + too_short: "je prekratka (minimum znakova je %{count})" + wrong_length: "je pogreÅ¡ne dužine (broj znakova mora biti %{count})" + taken: "je već u upotrebi" + not_a_number: "nije broj" + not_a_date: "nije ispravan datum" + greater_than: "mora biti veći od %{count}" + greater_than_or_equal_to: "mora biti veći ili jednak %{count}" + equal_to: "mora biti jednak %{count}" + less_than: "mora biti manji od %{count}" + less_than_or_equal_to: "mora biti manji ili jednak %{count}" + odd: "mora biti paran" + even: "mora biti neparan" + greater_than_start_date: "mora biti veći od poÄetnog datuma" + not_same_project: "ne pripada istom projektu" + circular_dependency: "Ova veza će stvoriti kružnu referencu" + cant_link_an_issue_with_a_descendant: "Problem ne može biti povezan sa jednim od svojih podzadataka" + + actionview_instancetag_blank_option: Molim odaberite + + general_text_No: 'Ne' + general_text_Yes: 'Da' + general_text_no: 'ne' + general_text_yes: 'da' + general_lang_name: 'Serbian (Srpski)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: Nalog je uspeÅ¡no ažuriran. + notice_account_invalid_creditentials: Neispravno korisniÄko ime ili lozinka. + notice_account_password_updated: Lozinka je uspeÅ¡no ažurirana. + notice_account_wrong_password: PogreÅ¡na lozinka + notice_account_register_done: KorisniÄki nalog je uspeÅ¡no kreiran. Kliknite na link koji ste dobili u e-poruci za aktivaciju. + notice_account_unknown_email: Nepoznat korisnik. + notice_can_t_change_password: Ovaj korisniÄki nalog za potvrdu identiteta koristi spoljni izvor. Nemoguće je promeniti lozinku. + notice_account_lost_email_sent: Poslata vam je e-poruka sa uputstvom za izbor nove lozinke + notice_account_activated: VaÅ¡ korisniÄki nalog je aktiviran. Sada se možete prijaviti. + notice_successful_create: UspeÅ¡no kreiranje. + notice_successful_update: UspeÅ¡no ažuriranje. + notice_successful_delete: UspeÅ¡no brisanje. + notice_successful_connection: UspeÅ¡no povezivanje. + notice_file_not_found: Strana kojoj želite pristupiti ne postoji ili je uklonjena. + notice_locking_conflict: Podatak je ažuriran od strane drugog korisnika. + notice_not_authorized: Niste ovlašćeni za pristup ovoj strani. + notice_email_sent: "E-poruka je poslata na %{value}" + notice_email_error: "Dogodila se greÅ¡ka prilikom slanja e-poruke (%{value})" + notice_feeds_access_key_reseted: VaÅ¡ RSS pristupni kljuÄ je poniÅ¡ten. + notice_api_access_key_reseted: VaÅ¡ API pristupni kljuÄ je poniÅ¡ten. + notice_failed_to_save_issues: "NeuspeÅ¡no snimanje %{count} problema od %{total} odabranih: %{ids}." + notice_failed_to_save_members: "NeuspeÅ¡no snimanje Älana(ova): %{errors}." + notice_no_issue_selected: "Ni jedan problem nije odabran! Molimo, odaberite problem koji želite da menjate." + notice_account_pending: "VaÅ¡ nalog je kreiran i Äeka na odobrenje administratora." + notice_default_data_loaded: Podrazumevano konfigurisanje je uspeÅ¡no uÄitano. + notice_unable_delete_version: Verziju je nemoguće izbrisati. + notice_unable_delete_time_entry: Stavku evidencije vremena je nemoguće izbrisati. + notice_issue_done_ratios_updated: Odnos reÅ¡enih problema je ažuriran. + + error_can_t_load_default_data: "Podrazumevano konfigurisanje je nemoguće uÄitati: %{value}" + error_scm_not_found: "Stavka ili ispravka nisu pronaÄ‘ene u spremiÅ¡tu." + error_scm_command_failed: "GreÅ¡ka se javila prilikom pokuÅ¡aja pristupa spremiÅ¡tu: %{value}" + error_scm_annotate: "Stavka ne postoji ili ne može biti oznaÄena." + error_issue_not_found_in_project: 'Problem nije pronaÄ‘en ili ne pripada ovom projektu.' + error_no_tracker_in_project: 'Ni jedno praćenje nije povezano sa ovim projektom. Molimo proverite podeÅ¡avanja projekta.' + error_no_default_issue_status: 'Podrazumevani status problema nije definisan. Molimo proverite vaÅ¡e konfigurisanje (idite na "Administracija -> Statusi problema").' + error_can_not_delete_custom_field: Nemoguće je izbrisati prilagoÄ‘eno polje + error_can_not_delete_tracker: "Ovo praćenje sadrži probleme i ne može biti obrisano." + error_can_not_remove_role: "Ova uloga je u upotrebi i ne može biti obrisana." + error_can_not_reopen_issue_on_closed_version: 'Problem dodeljen zatvorenoj verziji ne može biti ponovo otvoren' + error_can_not_archive_project: Ovaj projekat se ne može arhivirati + error_issue_done_ratios_not_updated: "Odnos reÅ¡enih problema nije ažuriran." + error_workflow_copy_source: 'Molimo odaberite izvorno praćenje ili ulogu' + error_workflow_copy_target: 'Molimo odaberite odrediÅ¡no praćenje i ulogu' + error_unable_delete_issue_status: 'Status problema je nemoguće obrisati' + error_unable_to_connect: "Povezivanje sa (%{value}) je nemoguće" + warning_attachments_not_saved: "%{count} datoteka ne može biti snimljena." + + mail_subject_lost_password: "VaÅ¡a %{value} lozinka" + mail_body_lost_password: 'Za promenu vaÅ¡e lozinke, kliknite na sledeći link:' + mail_subject_register: "Aktivacija vaÅ¡eg %{value} naloga" + mail_body_register: 'Za aktivaciju vaÅ¡eg naloga, kliknite na sledeći link:' + mail_body_account_information_external: "VaÅ¡ nalog %{value} možete koristiti za prijavu." + mail_body_account_information: Informacije o vaÅ¡em nalogu + mail_subject_account_activation_request: "Zahtev za aktivaciju naloga %{value}" + mail_body_account_activation_request: "Novi korisnik (%{value}) je registrovan. Nalog Äeka na vaÅ¡e odobrenje:" + mail_subject_reminder: "%{count} problema dospeva narednih %{days} dana" + mail_body_reminder: "%{count} problema dodeljenih vama dospeva u narednih %{days} dana:" + mail_subject_wiki_content_added: "Wiki stranica '%{id}' je dodata" + mail_body_wiki_content_added: "%{author} je dodao wiki stranicu '%{id}'." + mail_subject_wiki_content_updated: "Wiki stranica '%{id}' je ažurirana" + mail_body_wiki_content_updated: "%{author} je ažurirao wiki stranicu '%{id}'." + + gui_validation_error: jedna greÅ¡ka + gui_validation_error_plural: "%{count} greÅ¡aka" + + field_name: Naziv + field_description: Opis + field_summary: Rezime + field_is_required: Obavezno + field_firstname: Ime + field_lastname: Prezime + field_mail: E-adresa + field_filename: Datoteka + field_filesize: VeliÄina + field_downloads: Preuzimanja + field_author: Autor + field_created_on: Kreirano + field_updated_on: Ažurirano + field_field_format: Format + field_is_for_all: Za sve projekte + field_possible_values: Moguće vrednosti + field_regexp: Regularan izraz + field_min_length: Minimalna dužina + field_max_length: Maksimalna dužina + field_value: Vrednost + field_category: Kategorija + field_title: Naslov + field_project: Projekat + field_issue: Problem + field_status: Status + field_notes: BeleÅ¡ke + field_is_closed: Zatvoren problem + field_is_default: Podrazumevana vrednost + field_tracker: Praćenje + field_subject: Predmet + field_due_date: Krajnji rok + field_assigned_to: Dodeljeno + field_priority: Prioritet + field_fixed_version: OdrediÅ¡na verzija + field_user: Korisnik + field_principal: Glavni + field_role: Uloga + field_homepage: PoÄetna stranica + field_is_public: Javno objavljivanje + field_parent: Potprojekat od + field_is_in_roadmap: Problemi prikazani u planu rada + field_login: KorisniÄko ime + field_mail_notification: ObaveÅ¡tenja putem e-poÅ¡te + field_admin: Administrator + field_last_login_on: Poslednje povezivanje + field_language: Jezik + field_effective_date: Datum + field_password: Lozinka + field_new_password: Nova lozinka + field_password_confirmation: Potvrda lozinke + field_version: Verzija + field_type: Tip + field_host: Glavni raÄunar + field_port: Port + field_account: KorisniÄki nalog + field_base_dn: Bazni DN + field_attr_login: Atribut prijavljivanja + field_attr_firstname: Atribut imena + field_attr_lastname: Atribut prezimena + field_attr_mail: Atribut e-adrese + field_onthefly: Kreiranje korisnika u toku rada + field_start_date: PoÄetak + field_done_ratio: "% uraÄ‘eno" + field_auth_source: Režim potvrde identiteta + field_hide_mail: Sakrij moju e-adresu + field_comments: Komentar + field_url: URL + field_start_page: PoÄetna stranica + field_subproject: Potprojekat + field_hours: sati + field_activity: Aktivnost + field_spent_on: Datum + field_identifier: Identifikator + field_is_filter: Upotrebi kao filter + field_issue_to: Srodni problemi + field_delay: KaÅ¡njenje + field_assignable: Problem može biti dodeljen ovoj ulozi + field_redirect_existing_links: Preusmeri postojeće veze + field_estimated_hours: Proteklo vreme + field_column_names: Kolone + field_time_zone: Vremenska zona + field_searchable: Može da se pretražuje + field_default_value: Podrazumevana vrednost + field_comments_sorting: Prikaži komentare + field_parent_title: MatiÄna stranica + field_editable: Izmenljivo + field_watcher: PosmatraÄ + field_identity_url: OpenID URL + field_content: Sadržaj + field_group_by: Grupisanje rezultata po + field_sharing: Deljenje + field_parent_issue: MatiÄni zadatak + + setting_app_title: Naslov aplikacije + setting_app_subtitle: Podnaslov aplikacije + setting_welcome_text: Tekst dobrodoÅ¡lice + setting_default_language: Podrazumevani jezik + setting_login_required: Obavezna potvrda identiteta + setting_self_registration: Samoregistracija + setting_attachment_max_size: Maks. veliÄina priložene datoteke + setting_issues_export_limit: OgraniÄenje izvoza „problema“ + setting_mail_from: E-adresa poÅ¡iljaoca + setting_bcc_recipients: Primaoci „Bcc“ kopije + setting_plain_text_mail: Poruka sa Äistim tekstom (bez HTML-a) + setting_host_name: Putanja i naziv glavnog raÄunara + setting_text_formatting: Oblikovanje teksta + setting_wiki_compression: Kompresija Wiki istorije + setting_feeds_limit: OgraniÄenje sadržaja izvora vesti + setting_default_projects_public: Podrazumeva se javno prikazivanje novih projekata + setting_autofetch_changesets: IzvrÅ¡avanje automatskog preuzimanja + setting_sys_api_enabled: Omogućavanje WS za upravljanje spremiÅ¡tem + setting_commit_ref_keywords: Referenciranje kljuÄnih reÄi + setting_commit_fix_keywords: Popravljanje kljuÄnih reÄi + setting_autologin: Automatska prijava + setting_date_format: Format datuma + setting_time_format: Format vremena + setting_cross_project_issue_relations: Dozvoli povezivanje problema iz unakrsnih projekata + setting_issue_list_default_columns: Podrazumevane kolone prikazane na spisku problema + setting_emails_footer: Podnožje stranice e-poruke + setting_protocol: Protokol + setting_per_page_options: Opcije prikaza objekata po stranici + setting_user_format: Format prikaza korisnika + setting_activity_days_default: Broj dana prikazanih na projektnoj aktivnosti + setting_display_subprojects_issues: Prikazuj probleme iz potprojekata na glavnom projektu, ukoliko nije drugaÄije navedeno + setting_enabled_scm: Omogućavanje SCM + setting_mail_handler_body_delimiters: "Skraćivanje e-poruke nakon jedne od ovih linija" + setting_mail_handler_api_enabled: Omogućavanje WS dolazne e-poruke + setting_mail_handler_api_key: API kljuÄ + setting_sequential_project_identifiers: Generisanje sekvencijalnog imena projekta + setting_gravatar_enabled: Koristi Gravatar korisniÄke ikone + setting_gravatar_default: Podrazumevana Gravatar slika + setting_diff_max_lines_displayed: Maks. broj prikazanih razliÄitih linija + setting_file_max_size_displayed: Maks. veliÄina tekst. datoteka prikazanih umetnuto + setting_repository_log_display_limit: Maks. broj revizija prikazanih u datoteci za evidenciju + setting_openid: Dozvoli OpenID prijavu i registraciju + setting_password_min_length: Minimalna dužina lozinke + setting_new_project_user_role_id: Kreatoru projekta (koji nije administrator) dodeljuje je uloga + setting_default_projects_modules: Podrazumevano omogućeni moduli za nove projekte + setting_issue_done_ratio: IzraÄunaj odnos reÅ¡enih problema + setting_issue_done_ratio_issue_field: koristeći polje problema + setting_issue_done_ratio_issue_status: koristeći status problema + setting_start_of_week: Prvi dan u sedmici + setting_rest_api_enabled: Omogući REST web usluge + setting_cache_formatted_text: KeÅ¡iranje obraÄ‘enog teksta + + permission_add_project: Kreiranje projekta + permission_add_subprojects: Kreiranje potpojekta + permission_edit_project: Izmena projekata + permission_select_project_modules: Odabiranje modula projekta + permission_manage_members: Upravljanje Älanovima + permission_manage_project_activities: Upravljanje projektnim aktivnostima + permission_manage_versions: Upravljanje verzijama + permission_manage_categories: Upravljanje kategorijama problema + permission_view_issues: Pregled problema + permission_add_issues: Dodavanje problema + permission_edit_issues: Izmena problema + permission_manage_issue_relations: Upravljanje vezama izmeÄ‘u problema + permission_add_issue_notes: Dodavanje beleÅ¡ki + permission_edit_issue_notes: Izmena beleÅ¡ki + permission_edit_own_issue_notes: Izmena sopstvenih beleÅ¡ki + permission_move_issues: Pomeranje problema + permission_delete_issues: Brisanje problema + permission_manage_public_queries: Upravljanje javnim upitima + permission_save_queries: Snimanje upita + permission_view_gantt: Pregledanje Gantovog dijagrama + permission_view_calendar: Pregledanje kalendara + permission_view_issue_watchers: Pregledanje spiska posmatraÄa + permission_add_issue_watchers: Dodavanje posmatraÄa + permission_delete_issue_watchers: Brisanje posmatraÄa + permission_log_time: Beleženje utroÅ¡enog vremena + permission_view_time_entries: Pregledanje utroÅ¡enog vremena + permission_edit_time_entries: Izmena utroÅ¡enog vremena + permission_edit_own_time_entries: Izmena sopstvenog utroÅ¡enog vremena + permission_manage_news: Upravljanje vestima + permission_comment_news: Komentarisanje vesti + permission_manage_documents: Upravljanje dokumentima + permission_view_documents: Pregledanje dokumenata + permission_manage_files: Upravljanje datotekama + permission_view_files: Pregledanje datoteka + permission_manage_wiki: Upravljanje wiki stranicama + permission_rename_wiki_pages: Promena imena wiki stranicama + permission_delete_wiki_pages: Brisanje wiki stranica + permission_view_wiki_pages: Pregledanje wiki stranica + permission_view_wiki_edits: Pregledanje wiki istorije + permission_edit_wiki_pages: Izmena wiki stranica + permission_delete_wiki_pages_attachments: Brisanje priloženih datoteka + permission_protect_wiki_pages: ZaÅ¡tita wiki stranica + permission_manage_repository: Upravljanje spremiÅ¡tem + permission_browse_repository: Pregledanje spremiÅ¡ta + permission_view_changesets: Pregledanje skupa promena + permission_commit_access: Potvrda pristupa + permission_manage_boards: Upravljanje forumima + permission_view_messages: Pregledanje poruka + permission_add_messages: Slanje poruka + permission_edit_messages: Izmena poruka + permission_edit_own_messages: Izmena sopstvenih poruka + permission_delete_messages: Brisanje poruka + permission_delete_own_messages: Brisanje sopstvenih poruka + permission_export_wiki_pages: Izvoz wiki stranica + permission_manage_subtasks: Upravljanje podzadacima + + project_module_issue_tracking: Praćenje problema + project_module_time_tracking: Praćenje vremena + project_module_news: Vesti + project_module_documents: Dokumenti + project_module_files: Datoteke + project_module_wiki: Wiki + project_module_repository: SpremiÅ¡te + project_module_boards: Forumi + + label_user: Korisnik + label_user_plural: Korisnici + label_user_new: Novi korisnik + label_user_anonymous: Anoniman + label_project: Projekat + label_project_new: Novi projekat + label_project_plural: Projekti + label_x_projects: + zero: nema projekata + one: jedan projekat + other: "%{count} projekata" + label_project_all: Svi projekti + label_project_latest: Poslednji projekti + label_issue: Problem + label_issue_new: Novi problem + label_issue_plural: Problemi + label_issue_view_all: Prikaz svih problema + label_issues_by: "Problemi (%{value})" + label_issue_added: Problem je dodat + label_issue_updated: Problem je ažuriran + label_document: Dokument + label_document_new: Novi dokument + label_document_plural: Dokumenti + label_document_added: Dokument je dodat + label_role: Uloga + label_role_plural: Uloge + label_role_new: Nova uloga + label_role_and_permissions: Uloge i dozvole + label_member: ÄŒlan + label_member_new: Novi Älan + label_member_plural: ÄŒlanovi + label_tracker: Praćenje + label_tracker_plural: Praćenja + label_tracker_new: Novo praćenje + label_workflow: Tok posla + label_issue_status: Status problema + label_issue_status_plural: Statusi problema + label_issue_status_new: Novi status + label_issue_category: Kategorija problema + label_issue_category_plural: Kategorije problema + label_issue_category_new: Nova kategorija + label_custom_field: PrilagoÄ‘eno polje + label_custom_field_plural: PrilagoÄ‘ena polja + label_custom_field_new: Novo prilagoÄ‘eno polje + label_enumerations: Nabrojiva lista + label_enumeration_new: Nova vrednost + label_information: Informacija + label_information_plural: Informacije + label_please_login: Molimo, prijavite se + label_register: Registracija + label_login_with_open_id_option: ili prijava sa OpenID + label_password_lost: Izgubljena lozinka + label_home: PoÄetak + label_my_page: Moja stranica + label_my_account: Moj nalog + label_my_projects: Moji projekti + label_my_page_block: My page block + label_administration: Administracija + label_login: Prijava + label_logout: Odjava + label_help: Pomoć + label_reported_issues: Prijavljeni problemi + label_assigned_to_me_issues: Problemi dodeljeni meni + label_last_login: Poslednje povezivanje + label_registered_on: Registrovan + label_activity: Aktivnost + label_overall_activity: Celokupna aktivnost + label_user_activity: "Aktivnost korisnika %{value}" + label_new: Novo + label_logged_as: Prijavljeni ste kao + label_environment: Okruženje + label_authentication: Potvrda identiteta + label_auth_source: Režim potvrde identiteta + label_auth_source_new: Novi režim potvrde identiteta + label_auth_source_plural: Režimi potvrde identiteta + label_subproject_plural: Potprojekti + label_subproject_new: Novi potprojekat + label_and_its_subprojects: "%{value} i njegovi potprojekti" + label_min_max_length: Min. - Maks. dužina + label_list: Spisak + label_date: Datum + label_integer: Ceo broj + label_float: Sa pokretnim zarezom + label_boolean: LogiÄki operator + label_string: Tekst + label_text: Dugi tekst + label_attribute: Osobina + label_attribute_plural: Osobine + label_download: "%{count} preuzimanje" + label_download_plural: "%{count} preuzimanja" + label_no_data: Nema podataka za prikazivanje + label_change_status: Promena statusa + label_history: Istorija + label_attachment: Datoteka + label_attachment_new: Nova datoteka + label_attachment_delete: Brisanje datoteke + label_attachment_plural: Datoteke + label_file_added: Datoteka je dodata + label_report: IzveÅ¡taj + label_report_plural: IzveÅ¡taji + label_news: Vesti + label_news_new: Dodavanje vesti + label_news_plural: Vesti + label_news_latest: Poslednje vesti + label_news_view_all: Prikaz svih vesti + label_news_added: Vesti su dodate + label_settings: PodeÅ¡avanja + label_overview: Pregled + label_version: Verzija + label_version_new: Nova verzija + label_version_plural: Verzije + label_close_versions: Zatvori zavrÅ¡ene verzije + label_confirmation: Potvrda + label_export_to: 'TakoÄ‘e dostupno i u varijanti:' + label_read: ÄŒitanje... + label_public_projects: Javni projekti + label_open_issues: otvoren + label_open_issues_plural: otvorenih + label_closed_issues: zatvoren + label_closed_issues_plural: zatvorenih + label_x_open_issues_abbr_on_total: + zero: 0 otvorenih / %{total} + one: 1 otvoren / %{total} + other: "%{count} otvorenih / %{total}" + label_x_open_issues_abbr: + zero: 0 otvorenih + one: 1 otvoren + other: "%{count} otvorenih" + label_x_closed_issues_abbr: + zero: 0 zatvorenih + one: 1 zatvoren + other: "%{count} zatvorenih" + label_total: Ukupno + label_permissions: Dozvole + label_current_status: Trenutni status + label_new_statuses_allowed: Novi statusi dozvoljeni + label_all: svi + label_none: nijedan + label_nobody: nikome + label_next: Sledeće + label_previous: Prethodno + label_used_by: Koristio + label_details: Detalji + label_add_note: Dodaj beleÅ¡ku + label_per_page: Po strani + label_calendar: Kalendar + label_months_from: meseci od + label_gantt: Gantov dijagram + label_internal: UnutraÅ¡nji + label_last_changes: "poslednjih %{count} promena" + label_change_view_all: Prikaži sve promene + label_personalize_page: Personalizuj ovu stranu + label_comment: Komentar + label_comment_plural: Komentari + label_x_comments: + zero: bez komentara + one: jedan komentar + other: "%{count} komentara" + label_comment_add: Dodaj komentar + label_comment_added: Komentar dodat + label_comment_delete: ObriÅ¡i komentare + label_query: PrilagoÄ‘en upit + label_query_plural: PrilagoÄ‘eni upiti + label_query_new: Novi upit + label_filter_add: Dodavanje filtera + label_filter_plural: Filteri + label_equals: je + label_not_equals: nije + label_in_less_than: manje od + label_in_more_than: viÅ¡e od + label_greater_or_equal: '>=' + label_less_or_equal: '<=' + label_in: u + label_today: danas + label_all_time: sve vreme + label_yesterday: juÄe + label_this_week: ove sedmice + label_last_week: poslednje sedmice + label_last_n_days: "poslednjih %{count} dana" + label_this_month: ovog meseca + label_last_month: poslednjeg meseca + label_this_year: ove godine + label_date_range: Vremenski period + label_less_than_ago: pre manje od nekoliko dana + label_more_than_ago: pre viÅ¡e od nekoliko dana + label_ago: pre nekoliko dana + label_contains: sadrži + label_not_contains: ne sadrži + label_day_plural: dana + label_repository: SpremiÅ¡te + label_repository_plural: SpremiÅ¡ta + label_browse: Pregledanje + label_modification: "%{count} promena" + label_modification_plural: "%{count} promena" + label_branch: Grana + label_tag: Oznaka + label_revision: Revizija + label_revision_plural: Revizije + label_revision_id: "Revizija %{value}" + label_associated_revisions: Pridružene revizije + label_added: dodato + label_modified: promenjeno + label_copied: kopirano + label_renamed: preimenovano + label_deleted: izbrisano + label_latest_revision: Poslednja revizija + label_latest_revision_plural: Poslednje revizije + label_view_revisions: Pregled revizija + label_view_all_revisions: Pregled svih revizija + label_max_size: Maksimalna veliÄina + label_sort_highest: PremeÅ¡tanje na vrh + label_sort_higher: PremeÅ¡tanje na gore + label_sort_lower: PremeÅ¡tanje na dole + label_sort_lowest: PremeÅ¡tanje na dno + label_roadmap: Plan rada + label_roadmap_due_in: "Dospeva %{value}" + label_roadmap_overdue: "%{value} najkasnije" + label_roadmap_no_issues: Nema problema za ovu verziju + label_search: Pretraga + label_result_plural: Rezultati + label_all_words: Sve reÄi + label_wiki: Wiki + label_wiki_edit: Wiki izmena + label_wiki_edit_plural: Wiki izmene + label_wiki_page: Wiki stranica + label_wiki_page_plural: Wiki stranice + label_index_by_title: Indeksiranje po naslovu + label_index_by_date: Indeksiranje po datumu + label_current_version: Trenutna verzija + label_preview: Pregled + label_feed_plural: Izvori vesti + label_changes_details: Detalji svih promena + label_issue_tracking: Praćenje problema + label_spent_time: UtroÅ¡eno vreme + label_overall_spent_time: Celokupno utroÅ¡eno vreme + label_f_hour: "%{value} sat" + label_f_hour_plural: "%{value} sati" + label_time_tracking: Praćenje vremena + label_change_plural: Promene + label_statistics: Statistika + label_commits_per_month: IzvrÅ¡enja meseÄno + label_commits_per_author: IzvrÅ¡enja po autoru + label_view_diff: Pogledaj razlike + label_diff_inline: unutra + label_diff_side_by_side: uporedo + label_options: Opcije + label_copy_workflow_from: Kopiranje toka posla od + label_permissions_report: IzveÅ¡taj o dozvolama + label_watched_issues: Posmatrani problemi + label_related_issues: Srodni problemi + label_applied_status: Primenjeni statusi + label_loading: UÄitavanje... + label_relation_new: Nova relacija + label_relation_delete: Brisanje relacije + label_relates_to: srodnih sa + label_duplicates: dupliranih + label_duplicated_by: dupliranih od + label_blocks: odbijenih + label_blocked_by: odbijenih od + label_precedes: prethodi + label_follows: praćenih + label_end_to_start: od kraja do poÄetka + label_end_to_end: od kraja do kraja + label_start_to_start: od poÄetka do poÄetka + label_start_to_end: od poÄetka do kraja + label_stay_logged_in: Ostanite prijavljeni + label_disabled: onemogućeno + label_show_completed_versions: Prikazivanje zavrÅ¡ene verzije + label_me: meni + label_board: Forum + label_board_new: Novi forum + label_board_plural: Forumi + label_board_locked: ZakljuÄana + label_board_sticky: Lepljiva + label_topic_plural: Teme + label_message_plural: Poruke + label_message_last: Poslednja poruka + label_message_new: Nova poruka + label_message_posted: Poruka je dodata + label_reply_plural: Odgovori + label_send_information: PoÅ¡alji korisniku detalje naloga + label_year: Godina + label_month: Mesec + label_week: Sedmica + label_date_from: Å alje + label_date_to: Prima + label_language_based: Bazirano na jeziku korisnika + label_sort_by: "Sortirano po %{value}" + label_send_test_email: Slanje probne e-poruke + label_feeds_access_key: RSS pristupni kljuÄ + label_missing_feeds_access_key: RSS pristupni kljuÄ nedostaje + label_feeds_access_key_created_on: "RSS pristupni kljuÄ je napravljen pre %{value}" + label_module_plural: Moduli + label_added_time_by: "Dodao %{author} pre %{age}" + label_updated_time_by: "Ažurirao %{author} pre %{age}" + label_updated_time: "Ažurirano pre %{value}" + label_jump_to_a_project: Skok na projekat... + label_file_plural: Datoteke + label_changeset_plural: Skupovi promena + label_default_columns: Podrazumevane kolone + label_no_change_option: (Bez promena) + label_bulk_edit_selected_issues: Grupna izmena odabranih problema + label_theme: Tema + label_default: Podrazumevano + label_search_titles_only: Pretražuj samo naslove + label_user_mail_option_all: "Za bilo koji dogaÄ‘aj na svim mojim projektima" + label_user_mail_option_selected: "Za bilo koji dogaÄ‘aj na samo odabranim projektima..." + label_user_mail_no_self_notified: "Ne želim biti obaveÅ¡tavan za promene koje sam pravim" + label_registration_activation_by_email: aktivacija naloga putem e-poruke + label_registration_manual_activation: ruÄna aktivacija naloga + label_registration_automatic_activation: automatska aktivacija naloga + label_display_per_page: "Broj stavki po stranici: %{value}" + label_age: Starost + label_change_properties: Promeni svojstva + label_general: OpÅ¡ti + label_more: ViÅ¡e + label_scm: SCM + label_plugins: Dodatne komponente + label_ldap_authentication: LDAP potvrda identiteta + label_downloads_abbr: D/L + label_optional_description: Opciono opis + label_add_another_file: Dodaj joÅ¡ jednu datoteku + label_preferences: PodeÅ¡avanja + label_chronological_order: po hronoloÅ¡kom redosledu + label_reverse_chronological_order: po obrnutom hronoloÅ¡kom redosledu + label_planning: Planiranje + label_incoming_emails: Dolazne e-poruke + label_generate_key: Generisanje kljuÄa + label_issue_watchers: PosmatraÄi + label_example: Primer + label_display: Prikaz + label_sort: Sortiranje + label_ascending: Rastući niz + label_descending: Opadajući niz + label_date_from_to: Od %{start} do %{end} + label_wiki_content_added: Wiki stranica je dodata + label_wiki_content_updated: Wiki stranica je ažurirana + label_group: Grupa + label_group_plural: Grupe + label_group_new: Nova grupa + label_time_entry_plural: UtroÅ¡eno vreme + label_version_sharing_none: Nije deljeno + label_version_sharing_descendants: Sa potprojektima + label_version_sharing_hierarchy: Sa hijerarhijom projekta + label_version_sharing_tree: Sa stablom projekta + label_version_sharing_system: Sa svim projektima + label_update_issue_done_ratios: Ažuriraj odnos reÅ¡enih problema + label_copy_source: Izvor + label_copy_target: OdrediÅ¡te + label_copy_same_as_target: Isto kao odrediÅ¡te + label_display_used_statuses_only: Prikazuj statuse korišćene samo od strane ovog praćenja + label_api_access_key: API pristupni kljuÄ + label_missing_api_access_key: Nedostaje API pristupni kljuÄ + label_api_access_key_created_on: "API pristupni kljuÄ je kreiran pre %{value}" + label_profile: Profil + label_subtask_plural: Podzadatak + label_project_copy_notifications: PoÅ¡alji e-poruku sa obaveÅ¡tenjem prilikom kopiranja projekta + + button_login: Prijava + button_submit: PoÅ¡alji + button_save: Snimi + button_check_all: UkljuÄi sve + button_uncheck_all: IskljuÄi sve + button_delete: IzbriÅ¡i + button_create: Kreiraj + button_create_and_continue: Kreiraj i nastavi + button_test: Test + button_edit: Izmeni + button_add: Dodaj + button_change: Promeni + button_apply: Primeni + button_clear: ObriÅ¡i + button_lock: ZakljuÄaj + button_unlock: OtkljuÄaj + button_download: Preuzmi + button_list: Spisak + button_view: Prikaži + button_move: Pomeri + button_move_and_follow: Pomeri i prati + button_back: Nazad + button_cancel: PoniÅ¡ti + button_activate: Aktiviraj + button_sort: Sortiraj + button_log_time: Evidentiraj vreme + button_rollback: Povratak na ovu verziju + button_watch: Prati + button_unwatch: Ne prati viÅ¡e + button_reply: Odgovori + button_archive: Arhiviraj + button_unarchive: Vrati iz arhive + button_reset: PoniÅ¡ti + button_rename: Preimenuj + button_change_password: Promeni lozinku + button_copy: Kopiraj + button_copy_and_follow: Kopiraj i prati + button_annotate: Pribeleži + button_update: Ažuriraj + button_configure: Podesi + button_quote: Pod navodnicima + button_duplicate: Dupliraj + button_show: Prikaži + + status_active: aktivni + status_registered: registrovani + status_locked: zakljuÄani + + version_status_open: otvoren + version_status_locked: zakljuÄan + version_status_closed: zatvoren + + field_active: Aktivan + + text_select_mail_notifications: Odaberi akcije za koje će obaveÅ¡tenje biti poslato putem e-poÅ¡te. + text_regexp_info: npr. ^[A-Z0-9]+$ + text_min_max_length_info: 0 znaÄi bez ograniÄenja + text_project_destroy_confirmation: Jeste li sigurni da želite da izbriÅ¡ete ovaj projekat i sve pripadajuće podatke? + text_subprojects_destroy_warning: "Potprojekti: %{value} će takoÄ‘e biti izbrisan." + text_workflow_edit: Odaberite ulogu i praćenje za izmenu toka posla + text_are_you_sure: Jeste li sigurni? + text_journal_changed: "%{label} promenjen od %{old} u %{new}" + text_journal_set_to: "%{label} postavljen u %{value}" + text_journal_deleted: "%{label} izbrisano (%{old})" + text_journal_added: "%{label} %{value} dodato" + text_tip_issue_begin_day: zadatak poÄinje ovog dana + text_tip_issue_end_day: zadatak se zavrÅ¡ava ovog dana + text_tip_issue_begin_end_day: zadatak poÄinje i zavrÅ¡ava ovog dana + text_caracters_maximum: "NajviÅ¡e %{count} znak(ova)." + text_caracters_minimum: "Broj znakova mora biti najmanje %{count}." + text_length_between: "Broj znakova mora biti izmeÄ‘u %{min} i %{max}." + text_tracker_no_workflow: Ovo praćenje nema definisan tok posla + text_unallowed_characters: Nedozvoljeni znakovi + text_comma_separated: Dozvoljene su viÅ¡estruke vrednosti (odvojene zarezom). + text_line_separated: Dozvoljene su viÅ¡estruke vrednosti (jedan red za svaku vrednost). + text_issues_ref_in_commit_messages: Referenciranje i popravljanje problema u izvrÅ¡nim porukama + text_issue_added: "%{author} je prijavio problem %{id}." + text_issue_updated: "%{author} je ažurirao problem %{id}." + text_wiki_destroy_confirmation: Jeste li sigurni da želite da obriÅ¡ete wiki i sav sadržaj? + text_issue_category_destroy_question: "Nekoliko problema (%{count}) je dodeljeno ovoj kategoriji. Å ta želite da uradite?" + text_issue_category_destroy_assignments: Ukloni dodeljene kategorije + text_issue_category_reassign_to: Dodeli ponovo probleme ovoj kategoriji + text_user_mail_option: "Za neizabrane projekte, dobićete samo obaveÅ¡tenje o stvarima koje pratite ili ste ukljuÄeni (npr. problemi Äiji ste vi autor ili zastupnik)." + text_no_configuration_data: "Uloge, praćenja, statusi problema i toka posla joÅ¡ uvek nisu podeÅ¡eni.\nPreporuÄljivo je da uÄitate podrazumevano konfigurisanje. Izmena je moguća nakon prvog uÄitavanja." + text_load_default_configuration: UÄitaj podrazumevano konfigurisanje + text_status_changed_by_changeset: "Primenjeno u skupu sa promenama %{value}." + text_issues_destroy_confirmation: 'Jeste li sigurni da želite da izbriÅ¡ete odabrane probleme?' + text_select_project_modules: 'Odaberite module koje želite omogućiti za ovaj projekat:' + text_default_administrator_account_changed: Podrazumevani administratorski nalog je promenjen + text_file_repository_writable: Fascikla priloženih datoteka je upisiva + text_plugin_assets_writable: Fascikla elemenata dodatnih komponenti je upisiva + text_rmagick_available: RMagick je dostupan (opciono) + text_destroy_time_entries_question: "%{hours} sati je prijavljeno za ovaj problem koji želite izbrisati. Å ta želite da uradite?" + text_destroy_time_entries: IzbriÅ¡i prijavljene sate + text_assign_time_entries_to_project: Dodeli prijavljene sate projektu + text_reassign_time_entries: 'Dodeli ponovo prijavljene sate ovom problemu:' + text_user_wrote: "%{value} je napisao:" + text_enumeration_destroy_question: "%{count} objekat(a) je dodeljeno ovoj vrednosti." + text_enumeration_category_reassign_to: 'Dodeli ih ponovo ovoj vrednosti:' + text_email_delivery_not_configured: "Isporuka e-poruka nije konfigurisana i obaveÅ¡tenja su onemogućena.\nPodesite vaÅ¡ SMTP server u config/configuration.yml i pokrenite ponovo aplikaciju za njihovo omogućavanje." + text_repository_usernames_mapping: "Odaberite ili ažurirajte Redmine korisnike mapiranjem svakog korisniÄkog imena pronaÄ‘enog u evidenciji spremiÅ¡ta.\nKorisnici sa istim Redmine imenom i imenom spremiÅ¡ta ili e-adresom su automatski mapirani." + text_diff_truncated: '... Ova razlika je iseÄena jer je dostignuta maksimalna veliÄina prikaza.' + text_custom_field_possible_values_info: 'Jedan red za svaku vrednost' + text_wiki_page_destroy_question: "Ova stranica ima %{descendants} podreÄ‘enih stranica i podstranica. Å ta želite da uradite?" + text_wiki_page_nullify_children: "Zadrži podreÄ‘ene stranice kao korene stranice" + text_wiki_page_destroy_children: "IzbriÅ¡i podreÄ‘ene stranice i sve njihove podstranice" + text_wiki_page_reassign_children: "Dodeli ponovo podreÄ‘ene stranice ovoj matiÄnoj stranici" + text_own_membership_delete_confirmation: "Nakon uklanjanja pojedinih ili svih vaÅ¡ih dozvola nećete viÅ¡e moći da ureÄ‘ujete ovaj projekat.\nŽelite li da nastavite?" + text_zoom_in: Uvećaj + text_zoom_out: Umanji + + default_role_manager: Menadžer + default_role_developer: Programer + default_role_reporter: IzveÅ¡taÄ + default_tracker_bug: GreÅ¡ka + default_tracker_feature: Funkcionalnost + default_tracker_support: PodrÅ¡ka + default_issue_status_new: Novo + default_issue_status_in_progress: U toku + default_issue_status_resolved: ReÅ¡eno + default_issue_status_feedback: Povratna informacija + default_issue_status_closed: Zatvoreno + default_issue_status_rejected: Odbijeno + default_doc_category_user: KorisniÄka dokumentacija + default_doc_category_tech: TehniÄka dokumentacija + default_priority_low: Nizak + default_priority_normal: Normalan + default_priority_high: Visok + default_priority_urgent: Hitno + default_priority_immediate: Neposredno + default_activity_design: Dizajn + default_activity_development: Razvoj + + enumeration_issue_priorities: Prioriteti problema + enumeration_doc_categories: Kategorije dokumenta + enumeration_activities: Aktivnosti (praćenje vremena) + enumeration_system_activity: Sistemska aktivnost + + field_time_entries: Vreme evidencije + project_module_gantt: Gantov dijagram + project_module_calendar: Kalendar + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Text field + label_user_mail_option_only_owner: Samo za stvari koje posedujem + setting_default_notification_option: Podrazumevana opcija za notifikaciju + label_user_mail_option_only_my_events: Za dogadjaje koje pratim ili sam u njih ukljuÄen + label_user_mail_option_only_assigned: Za dogadjaje koji su mi dodeljeni liÄno + label_user_mail_option_none: Bez obaveÅ¡tenja + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + notice_not_authorized_archived_project: Projekat kome pokuÅ¡avate da pristupite je arhiviran + label_principal_search: "Traži korisnike ili grupe:" + label_user_search: "Traži korisnike:" + field_visible: Vidljivo + setting_emails_header: Email zaglavlje + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Omogući praćenje vremena + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maksimalan broj stavki na gant grafiku + field_warn_on_leaving_unsaved: Upozori me ako napuÅ¡tam stranu sa tekstom koji nije snimljen + text_warn_on_leaving_unsaved: Strana sadrži tekst koji nije snimljen i biće izgubljen ako je napustite. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} ažuriran" + label_news_comment_added: Komentar dodat u novosti + button_expand_all: ProÅ¡iri sve + button_collapse_all: Zatvori sve + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Da li ste sigurni da želite da obriÅ¡ete selektovane stavke ? + label_role_anonymous: Anonimus + label_role_non_member: Nije Älan + label_issue_note_added: Nota dodana + label_issue_status_updated: Status ažuriran + label_issue_priority_updated: Prioritet ažuriran + label_issues_visibility_own: Problem kreiran od strane ili je dodeljen korisniku + field_issues_visibility: Vidljivost problema + label_issues_visibility_all: Svi problemi + permission_set_own_issues_private: Podesi sopstveni problem kao privatan ili javan + field_is_private: Privatno + permission_set_issues_private: Podesi problem kao privatan ili javan + label_issues_visibility_public: Svi javni problemi + text_issues_destroy_descendants_confirmation: Ova operacija će takoÄ‘e obrisati %{count} podzadataka. + field_commit_logs_encoding: Kodiranje izvrÅ¡nih poruka + field_scm_path_encoding: Path encoding + text_scm_path_encoding_note: "Default: UTF-8" + field_path_to_repository: Path to repository + field_root_directory: Root directory + field_cvs_module: Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Command + text_scm_command_version: Version + label_git_report_last_commit: Report last commit for files and directories + text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 problem + one: 1 problem + other: "%{count} problemi" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: svi + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Sa potprojektima + label_cross_project_tree: Sa stablom projekta + label_cross_project_hierarchy: Sa hijerarhijom projekta + label_cross_project_system: Sa svim projektima + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/39/394e5e5e010d8386c450e9f1041325a98522fbd0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/39/394e5e5e010d8386c450e9f1041325a98522fbd0.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,23 @@ +/* Hebrew initialisation for the UI Datepicker extension. */ +/* Written by Amir Hardon (ahardon at gmail dot com). */ +jQuery(function($){ + $.datepicker.regional['he'] = { + closeText: 'סגור', + prevText: '<הקוד×', + nextText: 'הב×>', + currentText: 'היו×', + monthNames: ['ינו×ר','פברו×ר','מרץ','×פריל','מ××™','יוני', + 'יולי','×וגוסט','ספטמבר','×וקטובר','נובמבר','דצמבר'], + monthNamesShort: ['ינו','פבר','מרץ','×פר','מ××™','יוני', + 'יולי','×וג','ספט','×וק','נוב','דצמ'], + dayNames: ['ר×שון','שני','שלישי','רביעי','חמישי','שישי','שבת'], + dayNamesShort: ['×\'','ב\'','×’\'','ד\'','×”\'','ו\'','שבת'], + dayNamesMin: ['×\'','ב\'','×’\'','ד\'','×”\'','ו\'','שבת'], + weekHeader: 'Wk', + dateFormat: 'dd/mm/yy', + firstDay: 0, + isRTL: true, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['he']); +}); diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/39/39c71682ebab394069659920f9082d90bb8389ce.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/39/39c71682ebab394069659920f9082d90bb8389ce.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,49 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class DefaultDataTest < ActiveSupport::TestCase + include Redmine::I18n + fixtures :roles + + def test_no_data + assert !Redmine::DefaultData::Loader::no_data? + Role.delete_all("builtin = 0") + Tracker.delete_all + IssueStatus.delete_all + Enumeration.delete_all + assert Redmine::DefaultData::Loader::no_data? + end + + def test_load + valid_languages.each do |lang| + begin + Role.delete_all("builtin = 0") + Tracker.delete_all + IssueStatus.delete_all + Enumeration.delete_all + assert Redmine::DefaultData::Loader::load(lang) + assert_not_nil DocumentCategory.first + assert_not_nil IssuePriority.first + assert_not_nil TimeEntryActivity.first + rescue ActiveRecord::RecordInvalid => e + assert false, ":#{lang} default data is invalid (#{e.message})." + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/39/39db13c4cad8ea5def9b39b1a667edc1cb253f4e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/39/39db13c4cad8ea5def9b39b1a667edc1cb253f4e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,185 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +begin + require 'mocha' +rescue + # Won't run some tests +end + +class AccountTest < ActionController::IntegrationTest + fixtures :users, :roles + + # Replace this with your real tests. + def test_login + get "my/page" + assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fmy%2Fpage" + log_user('jsmith', 'jsmith') + + get "my/account" + assert_response :success + assert_template "my/account" + end + + def test_autologin + user = User.find(1) + Setting.autologin = "7" + Token.delete_all + + # User logs in with 'autologin' checked + post '/login', :username => user.login, :password => 'admin', :autologin => 1 + assert_redirected_to '/my/page' + token = Token.find :first + assert_not_nil token + assert_equal user, token.user + assert_equal 'autologin', token.action + assert_equal user.id, session[:user_id] + assert_equal token.value, cookies['autologin'] + + # Session is cleared + reset! + User.current = nil + # Clears user's last login timestamp + user.update_attribute :last_login_on, nil + assert_nil user.reload.last_login_on + + # User comes back with his autologin cookie + cookies[:autologin] = token.value + get '/my/page' + assert_response :success + assert_template 'my/page' + assert_equal user.id, session[:user_id] + assert_not_nil user.reload.last_login_on + end + + def test_lost_password + Token.delete_all + + get "account/lost_password" + assert_response :success + assert_template "account/lost_password" + assert_select 'input[name=mail]' + + post "account/lost_password", :mail => 'jSmith@somenet.foo' + assert_redirected_to "/login" + + token = Token.find(:first) + assert_equal 'recovery', token.action + assert_equal 'jsmith@somenet.foo', token.user.mail + assert !token.expired? + + get "account/lost_password", :token => token.value + assert_response :success + assert_template "account/password_recovery" + assert_select 'input[type=hidden][name=token][value=?]', token.value + assert_select 'input[name=new_password]' + assert_select 'input[name=new_password_confirmation]' + + post "account/lost_password", :token => token.value, :new_password => 'newpass123', :new_password_confirmation => 'newpass123' + assert_redirected_to "/login" + assert_equal 'Password was successfully updated.', flash[:notice] + + log_user('jsmith', 'newpass123') + assert_equal 0, Token.count + end + + def test_register_with_automatic_activation + Setting.self_registration = '3' + + get 'account/register' + assert_response :success + assert_template 'account/register' + + post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar", + :password => "newpass123", :password_confirmation => "newpass123"} + assert_redirected_to '/my/account' + follow_redirect! + assert_response :success + assert_template 'my/account' + + user = User.find_by_login('newuser') + assert_not_nil user + assert user.active? + assert_not_nil user.last_login_on + end + + def test_register_with_manual_activation + Setting.self_registration = '2' + + post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar", + :password => "newpass123", :password_confirmation => "newpass123"} + assert_redirected_to '/login' + assert !User.find_by_login('newuser').active? + end + + def test_register_with_email_activation + Setting.self_registration = '1' + Token.delete_all + + post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar", + :password => "newpass123", :password_confirmation => "newpass123"} + assert_redirected_to '/login' + assert !User.find_by_login('newuser').active? + + token = Token.find(:first) + assert_equal 'register', token.action + assert_equal 'newuser@foo.bar', token.user.mail + assert !token.expired? + + get 'account/activate', :token => token.value + assert_redirected_to '/login' + log_user('newuser', 'newpass123') + end + + def test_onthefly_registration + # disable registration + Setting.self_registration = '0' + AuthSource.expects(:authenticate).returns({:login => 'foo', :firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com', :auth_source_id => 66}) + + post '/login', :username => 'foo', :password => 'bar' + assert_redirected_to '/my/page' + + user = User.find_by_login('foo') + assert user.is_a?(User) + assert_equal 66, user.auth_source_id + assert user.hashed_password.blank? + end + + def test_onthefly_registration_with_invalid_attributes + # disable registration + Setting.self_registration = '0' + AuthSource.expects(:authenticate).returns({:login => 'foo', :lastname => 'Smith', :auth_source_id => 66}) + + post '/login', :username => 'foo', :password => 'bar' + assert_response :success + assert_template 'account/register' + assert_tag :input, :attributes => { :name => 'user[firstname]', :value => '' } + assert_tag :input, :attributes => { :name => 'user[lastname]', :value => 'Smith' } + assert_no_tag :input, :attributes => { :name => 'user[login]' } + assert_no_tag :input, :attributes => { :name => 'user[password]' } + + post 'account/register', :user => {:firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com'} + assert_redirected_to '/my/account' + + user = User.find_by_login('foo') + assert user.is_a?(User) + assert_equal 66, user.auth_source_id + assert user.hashed_password.blank? + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/39/39ee4f083009a3f1f617d8499c373a58890ca7f4.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/39/39ee4f083009a3f1f617d8499c373a58890ca7f4.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,2 @@ +# Load the Redmine helper +require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3a/3a0ec6ca576efa35e50d358195ea2367ed65fcba.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3a/3a0ec6ca576efa35e50d358195ea2367ed65fcba.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,42 @@ +<% if @deliveries %> +<%= form_tag({:action => 'edit', :tab => 'notifications'}) do %> + +
    +

    <%= setting_text_field :mail_from, :size => 60 %>

    + +

    <%= setting_check_box :bcc_recipients %>

    + +

    <%= setting_check_box :plain_text_mail %>

    + +

    <%= setting_select(:default_notification_option, User.valid_notification_options.collect {|o| [l(o.last), o.first.to_s]}) %>

    + +
    + +
    <%=l(:text_select_mail_notifications)%> +<%= hidden_field_tag 'settings[notified_events][]', '' %> +<% @notifiables.each do |notifiable| %> +<%= notification_field notifiable %> +
    +<% end %> +

    <%= check_all_links('notified_events') %>

    +
    + +
    <%= l(:setting_emails_header) %> +<%= setting_text_area :emails_header, :label => false, :class => 'wiki-edit', :rows => 5 %> +
    + +
    <%= l(:setting_emails_footer) %> +<%= setting_text_area :emails_footer, :label => false, :class => 'wiki-edit', :rows => 5 %> +
    + +
    +<%= link_to l(:label_send_test_email), :controller => 'admin', :action => 'test_email' %> +
    + +<%= submit_tag l(:button_save) %> +<% end %> +<% else %> +
    +<%= simple_format(l(:text_email_delivery_not_configured)) %> +
    +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3a/3a4efd0e9afcc8108cd60a444d52b8e7ec19f986.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3a/3a4efd0e9afcc8108cd60a444d52b8e7ec19f986.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,23 @@ +/* Lithuanian (UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* @author Arturas Paleicikas */ +jQuery(function($){ + $.datepicker.regional['lt'] = { + closeText: 'Uždaryti', + prevText: '<Atgal', + nextText: 'Pirmyn>', + currentText: 'Å iandien', + monthNames: ['Sausis','Vasaris','Kovas','Balandis','Gegužė','Birželis', + 'Liepa','RugpjÅ«tis','RugsÄ—jis','Spalis','Lapkritis','Gruodis'], + monthNamesShort: ['Sau','Vas','Kov','Bal','Geg','Bir', + 'Lie','Rugp','Rugs','Spa','Lap','Gru'], + dayNames: ['sekmadienis','pirmadienis','antradienis','treÄiadienis','ketvirtadienis','penktadienis','Å¡eÅ¡tadienis'], + dayNamesShort: ['sek','pir','ant','tre','ket','pen','Å¡eÅ¡'], + dayNamesMin: ['Se','Pr','An','Tr','Ke','Pe','Å e'], + weekHeader: 'Wk', + dateFormat: 'yy-mm-dd', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['lt']); +}); \ No newline at end of file diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3a/3a513868a533431476e8232f1b95f5f62246b3de.svn-base --- a/.svn/pristine/3a/3a513868a533431476e8232f1b95f5f62246b3de.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ -class Journal < ActiveRecord::Base - generator_for :journalized, :method => :generate_issue - generator_for :user, :method => :generate_user - - def self.generate_issue - project = Project.generate! - Issue.generate_for_project!(project) - end - - def self.generate_user - User.generate_with_protected! - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3a/3abfa86ad3c57ab24eae53ec4cf3ec382c0f6086.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3a/3abfa86ad3c57ab24eae53ec4cf3ec382c0f6086.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,41 @@ +class RedminePluginModelGenerator < Rails::Generators::NamedBase + + source_root File.expand_path("../templates", __FILE__) + argument :model, :type => :string + argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]" + class_option :migration, :type => :boolean + class_option :timestamps, :type => :boolean + class_option :parent, :type => :string, :desc => "The parent class for the generated model" + class_option :indexes, :type => :boolean, :default => true, :desc => "Add indexes for references and belongs_to columns" + + attr_reader :plugin_path, :plugin_name, :plugin_pretty_name + + def initialize(*args) + super + @plugin_name = file_name.underscore + @plugin_pretty_name = plugin_name.titleize + @plugin_path = "plugins/#{plugin_name}" + @model_class = model.camelize + @table_name = @model_class.tableize + @migration_filename = "create_#{@table_name}" + @migration_class_name = @migration_filename.camelize + end + + def copy_templates + template 'model.rb.erb', "#{plugin_path}/app/models/#{model.underscore}.rb" + template 'unit_test.rb.erb', "#{plugin_path}/test/unit/#{model.underscore}_test.rb" + + migration_filename = "%03i_#{@migration_filename}.rb" % (migration_number + 1) + template "migration.rb", "#{plugin_path}/db/migrate/#{migration_filename}" + end + + def attributes_with_index + attributes.select { |a| a.has_index? || (a.reference? && options[:indexes]) } + end + + def migration_number + current = Dir.glob("#{plugin_path}/db/migrate/*.rb").map do |file| + File.basename(file).split("_").first.to_i + end.max.to_i + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3b/3b05c0abe47ab505483ca11e9db820e225ca0d09.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3b/3b05c0abe47ab505483ca11e9db820e225ca0d09.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,23 @@ +/* Swedish initialisation for the jQuery UI date picker plugin. */ +/* Written by Anders Ekdahl ( anders@nomadiz.se). */ +jQuery(function($){ + $.datepicker.regional['sv'] = { + closeText: 'Stäng', + prevText: '«Förra', + nextText: 'Nästa»', + currentText: 'Idag', + monthNames: ['Januari','Februari','Mars','April','Maj','Juni', + 'Juli','Augusti','September','Oktober','November','December'], + monthNamesShort: ['Jan','Feb','Mar','Apr','Maj','Jun', + 'Jul','Aug','Sep','Okt','Nov','Dec'], + dayNamesShort: ['Sön','MÃ¥n','Tis','Ons','Tor','Fre','Lör'], + dayNames: ['Söndag','MÃ¥ndag','Tisdag','Onsdag','Torsdag','Fredag','Lördag'], + dayNamesMin: ['Sö','MÃ¥','Ti','On','To','Fr','Lö'], + weekHeader: 'Ve', + dateFormat: 'yy-mm-dd', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['sv']); +}); diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3b/3b67d2d05a6b7514939bf8c3230d36fc1aef4515.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3b/3b67d2d05a6b7514939bf8c3230d36fc1aef4515.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,23 @@ +/* Dutch (UTF-8) initialisation for the jQuery UI date picker plugin. */ +/* Written by Mathias Bynens */ +jQuery(function($){ + $.datepicker.regional.nl = { + closeText: 'Sluiten', + prevText: 'â†', + nextText: '→', + currentText: 'Vandaag', + monthNames: ['januari', 'februari', 'maart', 'april', 'mei', 'juni', + 'juli', 'augustus', 'september', 'oktober', 'november', 'december'], + monthNamesShort: ['jan', 'feb', 'mrt', 'apr', 'mei', 'jun', + 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'], + dayNames: ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'], + dayNamesShort: ['zon', 'maa', 'din', 'woe', 'don', 'vri', 'zat'], + dayNamesMin: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'], + weekHeader: 'Wk', + dateFormat: 'dd-mm-yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional.nl); +}); \ No newline at end of file diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3b/3b9b2d2576b7f54c4c40091aa286f522953a9ec0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3b/3b9b2d2576b7f54c4c40091aa286f522953a9ec0.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,43 @@ +--- +journal_details_001: + old_value: "1" + property: attr + id: 1 + value: "2" + prop_key: status_id + journal_id: 1 +journal_details_002: + old_value: "40" + property: attr + id: 2 + value: "30" + prop_key: done_ratio + journal_id: 1 +journal_details_003: + old_value: nil + property: attr + id: 3 + value: "6" + prop_key: fixed_version_id + journal_id: 4 +journal_details_004: + old_value: "This word was removed and an other was" + property: attr + id: 4 + value: "This word was and an other was added" + prop_key: description + journal_id: 3 +journal_details_005: + old_value: Old value + property: cf + id: 5 + value: New value + prop_key: 2 + journal_id: 3 +journal_details_006: + old_value: nil + property: attachment + id: 6 + value: 060719210727_picture.jpg + prop_key: 4 + journal_id: 3 diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3b/3bb5e5f9c78e113c7cca95c62e3d7f0c93e77d6c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3b/3bb5e5f9c78e113c7cca95c62e3d7f0c93e77d6c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1138 @@ +html {overflow-y:scroll;} +body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; } + +h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;} +#content h1, h2, h3, h4 {color: #555;} +h2, .wiki h1 {font-size: 20px;} +h3, .wiki h2 {font-size: 16px;} +h4, .wiki h3 {font-size: 13px;} +h4 {border-bottom: 1px dotted #bbb;} + +/***** Layout *****/ +#wrapper {background: white;} + +#top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;} +#top-menu ul {margin: 0; padding: 0;} +#top-menu li { + float:left; + list-style-type:none; + margin: 0px 0px 0px 0px; + padding: 0px 0px 0px 0px; + white-space:nowrap; +} +#top-menu a {color: #fff; margin-right: 8px; font-weight: bold;} +#top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; } + +#account {float:right;} + +#header {min-height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 20px 6px; position:relative;} +#header a {color:#f8f8f8;} +#header h1 a.ancestor { font-size: 80%; } +#quick-search {float:right;} + +#main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;} +#main-menu ul {margin: 0; padding: 0;} +#main-menu li { + float:left; + list-style-type:none; + margin: 0px 2px 0px 0px; + padding: 0px 0px 0px 0px; + white-space:nowrap; +} +#main-menu li a { + display: block; + color: #fff; + text-decoration: none; + font-weight: bold; + margin: 0; + padding: 4px 10px 4px 10px; +} +#main-menu li a:hover {background:#759FCF; color:#fff;} +#main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;} + +#admin-menu ul {margin: 0; padding: 0;} +#admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;} + +#admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;} +#admin-menu a.projects { background-image: url(../images/projects.png); } +#admin-menu a.users { background-image: url(../images/user.png); } +#admin-menu a.groups { background-image: url(../images/group.png); } +#admin-menu a.roles { background-image: url(../images/database_key.png); } +#admin-menu a.trackers { background-image: url(../images/ticket.png); } +#admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); } +#admin-menu a.workflows { background-image: url(../images/ticket_go.png); } +#admin-menu a.custom_fields { background-image: url(../images/textfield.png); } +#admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); } +#admin-menu a.settings { background-image: url(../images/changeset.png); } +#admin-menu a.plugins { background-image: url(../images/plugin.png); } +#admin-menu a.info { background-image: url(../images/help.png); } +#admin-menu a.server_authentication { background-image: url(../images/server_key.png); } + +#main {background-color:#EEEEEE;} + +#sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;} +* html #sidebar{ width: 22%; } +#sidebar h3{ font-size: 14px; margin-top:14px; color: #666; } +#sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; } +* html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; } +#sidebar .contextual { margin-right: 1em; } + +#content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; } +* html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;} +html>body #content { min-height: 600px; } +* html body #content { height: 600px; } /* IE */ + +#main.nosidebar #sidebar{ display: none; } +#main.nosidebar #content{ width: auto; border-right: 0; } + +#footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;} + +#login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; } +#login-form table td {padding: 6px;} +#login-form label {font-weight: bold;} +#login-form input#username, #login-form input#password { width: 300px; } + +div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;} +div.modal h3.title {display:none;} +div.modal p.buttons {text-align:right; margin-bottom:0;} + +input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; } + +.clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; } + +/***** Links *****/ +a, a:link, a:visited{ color: #169; text-decoration: none; } +a:hover, a:active{ color: #c61a1a; text-decoration: underline;} +a img{ border: 0; } + +a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; } +a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; } +a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;} + +#sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;} +#sidebar a.selected:hover {text-decoration:none;} +#admin-menu a {line-height:1.7em;} +#admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;} + +a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;} +a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;} + +a#toggle-completed-versions {color:#999;} +/***** Tables *****/ +table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; } +table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; } +table.list td { vertical-align: top; padding-right:10px; } +table.list td.id { width: 2%; text-align: center;} +table.list td.checkbox { width: 15px; padding: 2px 0 0 0; } +table.list td.checkbox input {padding:0px;} +table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; } +table.list td.buttons a { padding-right: 0.6em; } +table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; } + +tr.project td.name a { white-space:nowrap; } +tr.project.closed, tr.project.archived { color: #aaa; } +tr.project.closed a, tr.project.archived a { color: #aaa; } + +tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;} +tr.project.idnt-1 td.name {padding-left: 0.5em;} +tr.project.idnt-2 td.name {padding-left: 2em;} +tr.project.idnt-3 td.name {padding-left: 3.5em;} +tr.project.idnt-4 td.name {padding-left: 5em;} +tr.project.idnt-5 td.name {padding-left: 6.5em;} +tr.project.idnt-6 td.name {padding-left: 8em;} +tr.project.idnt-7 td.name {padding-left: 9.5em;} +tr.project.idnt-8 td.name {padding-left: 11em;} +tr.project.idnt-9 td.name {padding-left: 12.5em;} + +tr.issue { text-align: center; white-space: nowrap; } +tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations { white-space: normal; } +tr.issue td.subject, tr.issue td.relations { text-align: left; } +tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;} +tr.issue td.relations span {white-space: nowrap;} +table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;} +table.issues td.description pre {white-space:normal;} + +tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;} +tr.issue.idnt-1 td.subject {padding-left: 0.5em;} +tr.issue.idnt-2 td.subject {padding-left: 2em;} +tr.issue.idnt-3 td.subject {padding-left: 3.5em;} +tr.issue.idnt-4 td.subject {padding-left: 5em;} +tr.issue.idnt-5 td.subject {padding-left: 6.5em;} +tr.issue.idnt-6 td.subject {padding-left: 8em;} +tr.issue.idnt-7 td.subject {padding-left: 9.5em;} +tr.issue.idnt-8 td.subject {padding-left: 11em;} +tr.issue.idnt-9 td.subject {padding-left: 12.5em;} + +tr.entry { border: 1px solid #f8f8f8; } +tr.entry td { white-space: nowrap; } +tr.entry td.filename { width: 30%; } +tr.entry td.filename_no_report { width: 70%; } +tr.entry td.size { text-align: right; font-size: 90%; } +tr.entry td.revision, tr.entry td.author { text-align: center; } +tr.entry td.age { text-align: right; } +tr.entry.file td.filename a { margin-left: 16px; } +tr.entry.file td.filename_no_report a { margin-left: 16px; } + +tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;} +tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);} + +tr.changeset { height: 20px } +tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; } +tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; } +tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;} +tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;} + +table.files tr.file td { text-align: center; } +table.files tr.file td.filename { text-align: left; padding-left: 24px; } +table.files tr.file td.digest { font-size: 80%; } + +table.members td.roles, table.memberships td.roles { width: 45%; } + +tr.message { height: 2.6em; } +tr.message td.subject { padding-left: 20px; } +tr.message td.created_on { white-space: nowrap; } +tr.message td.last_message { font-size: 80%; white-space: nowrap; } +tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; } +tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; } + +tr.version.closed, tr.version.closed a { color: #999; } +tr.version td.name { padding-left: 20px; } +tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; } +tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; } + +tr.user td { width:13%; } +tr.user td.email { width:18%; } +tr.user td { white-space: nowrap; } +tr.user.locked, tr.user.registered { color: #aaa; } +tr.user.locked a, tr.user.registered a { color: #aaa; } + +table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;} + +tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;} + +tr.time-entry { text-align: center; white-space: nowrap; } +tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; } +td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; } +td.hours .hours-dec { font-size: 0.9em; } + +table.plugins td { vertical-align: middle; } +table.plugins td.configure { text-align: right; padding-right: 1em; } +table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; } +table.plugins span.description { display: block; font-size: 0.9em; } +table.plugins span.url { display: block; font-size: 0.9em; } + +table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; } +table.list tbody tr.group span.count {position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;} +tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;} +tr.group:hover a.toggle-all { display:inline;} +a.toggle-all:hover {text-decoration:none;} + +table.list tbody tr:hover { background-color:#ffffdd; } +table.list tbody tr.group:hover { background-color:inherit; } +table td {padding:2px;} +table p {margin:0;} +.odd {background-color:#f6f7f8;} +.even {background-color: #fff;} + +a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; } +a.sort.asc { background-image: url(../images/sort_asc.png); } +a.sort.desc { background-image: url(../images/sort_desc.png); } + +table.attributes { width: 100% } +table.attributes th { vertical-align: top; text-align: left; } +table.attributes td { vertical-align: top; } + +table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; } +table.boards td.topic-count, table.boards td.message-count {text-align:center;} +table.boards td.last-message {font-size:80%;} + +table.messages td.author, table.messages td.created_on, table.messages td.reply-count {text-align:center;} + +table.query-columns { + border-collapse: collapse; + border: 0; +} + +table.query-columns td.buttons { + vertical-align: middle; + text-align: center; +} + +td.center {text-align:center;} + +h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; } + +div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; } +div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; } +div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; } +div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; } + +#watchers ul {margin: 0; padding: 0;} +#watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;} +#watchers select {width: 95%; display: block;} +#watchers a.delete {opacity: 0.4;} +#watchers a.delete:hover {opacity: 1;} +#watchers img.gravatar {margin: 0 4px 2px 0;} + +span#watchers_inputs {overflow:auto; display:block;} +span.search_for_watchers {display:block;} +span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;} +span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; } + + +.highlight { background-color: #FCFD8D;} +.highlight.token-1 { background-color: #faa;} +.highlight.token-2 { background-color: #afa;} +.highlight.token-3 { background-color: #aaf;} + +.box{ + padding:6px; + margin-bottom: 10px; + background-color:#f6f6f6; + color:#505050; + line-height:1.5em; + border: 1px solid #e4e4e4; +} + +div.square { + border: 1px solid #999; + float: left; + margin: .3em .4em 0 .4em; + overflow: hidden; + width: .6em; height: .6em; +} +.contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;} +.contextual input, .contextual select {font-size:0.9em;} +.message .contextual { margin-top: 0; } + +.splitcontent {overflow:auto;} +.splitcontentleft{float:left; width:49%;} +.splitcontentright{float:right; width:49%;} +form {display: inline;} +input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;} +fieldset {border: 1px solid #e4e4e4; margin:0;} +legend {color: #484848;} +hr { width: 100%; height: 1px; background: #ccc; border: 0;} +blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;} +blockquote blockquote { margin-left: 0;} +acronym { border-bottom: 1px dotted; cursor: help; } +textarea.wiki-edit {width:99%; resize:vertical;} +li p {margin-top: 0;} +div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;} +p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;} +p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; } +p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; } + +div.issue div.subject div div { padding-left: 16px; } +div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;} +div.issue div.subject>div>p { margin-top: 0.5em; } +div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;} +div.issue span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;} +div.issue .next-prev-links {color:#999;} +div.issue table.attributes th {width:22%;} +div.issue table.attributes td {width:28%;} + +#issue_tree table.issues, #relations table.issues { border: 0; } +#issue_tree td.checkbox, #relations td.checkbox {display:none;} +#relations td.buttons {padding:0;} + +fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; } +fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; } +fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); } + +fieldset#date-range p { margin: 2px 0 2px 0; } +fieldset#filters table { border-collapse: collapse; } +fieldset#filters table td { padding: 0; vertical-align: middle; } +fieldset#filters tr.filter { height: 2.1em; } +fieldset#filters td.field { width:230px; } +fieldset#filters td.operator { width:180px; } +fieldset#filters td.operator select {max-width:170px;} +fieldset#filters td.values { white-space:nowrap; } +fieldset#filters td.values select {min-width:130px;} +fieldset#filters td.values input {height:1em;} +fieldset#filters td.add-filter { text-align: right; vertical-align: top; } + +.toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;} +.buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; } + +div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;} +div#issue-changesets div.changeset { padding: 4px;} +div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; } +div#issue-changesets p { margin-top: 0; margin-bottom: 1em;} + +.journal ul.details img {margin:0 0 -3px 4px;} +div.journal {overflow:auto;} +div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;} + +div#activity dl, #search-results { margin-left: 2em; } +div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; } +div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } +div#activity dt.me .time { border-bottom: 1px solid #999; } +div#activity dt .time { color: #777; font-size: 80%; } +div#activity dd .description, #search-results dd .description { font-style: italic; } +div#activity span.project:after, #search-results span.project:after { content: " -"; } +div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; } + +#search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; } + +div#search-results-counts {float:right;} +div#search-results-counts ul { margin-top: 0.5em; } +div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; } + +dt.issue { background-image: url(../images/ticket.png); } +dt.issue-edit { background-image: url(../images/ticket_edit.png); } +dt.issue-closed { background-image: url(../images/ticket_checked.png); } +dt.issue-note { background-image: url(../images/ticket_note.png); } +dt.changeset { background-image: url(../images/changeset.png); } +dt.news { background-image: url(../images/news.png); } +dt.message { background-image: url(../images/message.png); } +dt.reply { background-image: url(../images/comments.png); } +dt.wiki-page { background-image: url(../images/wiki_edit.png); } +dt.attachment { background-image: url(../images/attachment.png); } +dt.document { background-image: url(../images/document.png); } +dt.project { background-image: url(../images/projects.png); } +dt.time-entry { background-image: url(../images/time.png); } + +#search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); } + +div#roadmap .related-issues { margin-bottom: 1em; } +div#roadmap .related-issues td.checkbox { display: none; } +div#roadmap .wiki h1:first-child { display: none; } +div#roadmap .wiki h1 { font-size: 120%; } +div#roadmap .wiki h2 { font-size: 110%; } +body.controller-versions.action-show div#roadmap .related-issues {width:70%;} + +div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; } +div#version-summary fieldset { margin-bottom: 1em; } +div#version-summary fieldset.time-tracking table { width:100%; } +div#version-summary th, div#version-summary td.total-hours { text-align: right; } + +table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; } +table#time-report tbody tr.subtotal { font-style: italic; color:#777;} +table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; } +table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;} +table#time-report .hours-dec { font-size: 0.9em; } + +div.wiki-page .contextual a {opacity: 0.4} +div.wiki-page .contextual a:hover {opacity: 1} + +form .attributes select { width: 60%; } +input#issue_subject { width: 99%; } +select#issue_done_ratio { width: 95px; } + +ul.projects {margin:0; padding-left:1em;} +ul.projects ul {padding-left:1.6em;} +ul.projects.root {margin:0; padding:0;} +ul.projects li {list-style-type:none;} + +#projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;} +#projects-index ul.projects li.root {margin-bottom: 1em;} +#projects-index ul.projects li.child {margin-top: 1em;} +#projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; } +.my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; } + +#notified-projects ul, #tracker_project_ids ul {max-height:250px; overflow-y:auto;} + +#related-issues li img {vertical-align:middle;} + +ul.properties {padding:0; font-size: 0.9em; color: #777;} +ul.properties li {list-style-type:none;} +ul.properties li span {font-style:italic;} + +.total-hours { font-size: 110%; font-weight: bold; } +.total-hours span.hours-int { font-size: 120%; } + +.autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;} +#user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; } + +#workflow_copy_form select { width: 200px; } +table.transitions td.enabled {background: #bfb;} +table.fields_permissions select {font-size:90%} +table.fields_permissions td.readonly {background:#ddd;} +table.fields_permissions td.required {background:#d88;} + +textarea#custom_field_possible_values {width: 99%} +input#content_comments {width: 99%} + +.pagination {font-size: 90%} +p.pagination {margin-top:8px;} + +/***** Tabular forms ******/ +.tabular p{ + margin: 0; + padding: 3px 0 3px 0; + padding-left: 180px; /* width of left column containing the label elements */ + min-height: 1.8em; + clear:left; +} + +html>body .tabular p {overflow:hidden;} + +.tabular label{ + font-weight: bold; + float: left; + text-align: right; + /* width of left column */ + margin-left: -180px; + /* width of labels. Should be smaller than left column to create some right margin */ + width: 175px; +} + +.tabular label.floating{ + font-weight: normal; + margin-left: 0px; + text-align: left; + width: 270px; +} + +.tabular label.block{ + font-weight: normal; + margin-left: 0px !important; + text-align: left; + float: none; + display: block; + width: auto; +} + +.tabular label.inline{ + font-weight: normal; + float:none; + margin-left: 5px !important; + width: auto; +} + +label.no-css { + font-weight: inherit; + float:none; + text-align:left; + margin-left:0px; + width:auto; +} +input#time_entry_comments { width: 90%;} + +#preview fieldset {margin-top: 1em; background: url(../images/draft.png)} + +.tabular.settings p{ padding-left: 300px; } +.tabular.settings label{ margin-left: -300px; width: 295px; } +.tabular.settings textarea { width: 99%; } + +.settings.enabled_scm table {width:100%} +.settings.enabled_scm td.scm_name{ font-weight: bold; } + +fieldset.settings label { display: block; } +fieldset#notified_events .parent { padding-left: 20px; } + +span.required {color: #bb0000;} +.summary {font-style: italic;} + +#attachments_fields input.description {margin-left: 8px; width:340px;} +#attachments_fields span {display:block; white-space:nowrap;} +#attachments_fields img {vertical-align: middle;} + +div.attachments { margin-top: 12px; } +div.attachments p { margin:4px 0 2px 0; } +div.attachments img { vertical-align: middle; } +div.attachments span.author { font-size: 0.9em; color: #888; } + +div.thumbnails {margin-top:0.6em;} +div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;} +div.thumbnails img {margin: 3px;} + +p.other-formats { text-align: right; font-size:0.9em; color: #666; } +.other-formats span + span:before { content: "| "; } + +a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; } + +em.info {font-style:normal;font-size:90%;color:#888;display:block;} +em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;} + +textarea.text_cf {width:90%;} + +/* Project members tab */ +div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% } +div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% } +div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; } +div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; } +div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; } +div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; } + +#users_for_watcher {height: 200px; overflow:auto;} +#users_for_watcher label {display: block;} + +table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; } + +input#principal_search, input#user_search {width:100%} +input#principal_search, input#user_search { + background: url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px; + border:1px solid #9EB1C2; border-radius:3px; height:1.5em; width:95%; +} +input#principal_search.ajax-loading, input#user_search.ajax-loading { + background-image: url(../images/loading.gif); +} + +* html div#tab-content-members fieldset div { height: 450px; } + +/***** Flash & error messages ****/ +#errorExplanation, div.flash, .nodata, .warning, .conflict { + padding: 4px 4px 4px 30px; + margin-bottom: 12px; + font-size: 1.1em; + border: 2px solid; +} + +div.flash {margin-top: 8px;} + +div.flash.error, #errorExplanation { + background: url(../images/exclamation.png) 8px 50% no-repeat; + background-color: #ffe3e3; + border-color: #dd0000; + color: #880000; +} + +div.flash.notice { + background: url(../images/true.png) 8px 5px no-repeat; + background-color: #dfffdf; + border-color: #9fcf9f; + color: #005f00; +} + +div.flash.warning, .conflict { + background: url(../images/warning.png) 8px 5px no-repeat; + background-color: #FFEBC1; + border-color: #FDBF3B; + color: #A6750C; + text-align: left; +} + +.nodata, .warning { + text-align: center; + background-color: #FFEBC1; + border-color: #FDBF3B; + color: #A6750C; +} + +#errorExplanation ul { font-size: 0.9em;} +#errorExplanation h2, #errorExplanation p { display: none; } + +.conflict-details {font-size:80%;} + +/***** Ajax indicator ******/ +#ajax-indicator { +position: absolute; /* fixed not supported by IE */ +background-color:#eee; +border: 1px solid #bbb; +top:35%; +left:40%; +width:20%; +font-weight:bold; +text-align:center; +padding:0.6em; +z-index:100; +opacity: 0.5; +} + +html>body #ajax-indicator { position: fixed; } + +#ajax-indicator span { +background-position: 0% 40%; +background-repeat: no-repeat; +background-image: url(../images/loading.gif); +padding-left: 26px; +vertical-align: bottom; +} + +/***** Calendar *****/ +table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;} +table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; } +table.cal thead th.week-number {width: auto;} +table.cal tbody tr {height: 100px;} +table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;} +table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;} +table.cal td p.day-num {font-size: 1.1em; text-align:right;} +table.cal td.odd p.day-num {color: #bbb;} +table.cal td.today {background:#ffffdd;} +table.cal td.today p.day-num {font-weight: bold;} +table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;} +table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;} +table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;} +p.cal.legend span {display:block;} + +/***** Tooltips ******/ +.tooltip{position:relative;z-index:24;} +.tooltip:hover{z-index:25;color:#000;} +.tooltip span.tip{display: none; text-align:left;} + +div.tooltip:hover span.tip{ +display:block; +position:absolute; +top:12px; left:24px; width:270px; +border:1px solid #555; +background-color:#fff; +padding: 4px; +font-size: 0.8em; +color:#505050; +} + +img.ui-datepicker-trigger { + cursor: pointer; + vertical-align: middle; + margin-left: 4px; +} + +/***** Progress bar *****/ +table.progress { + border-collapse: collapse; + border-spacing: 0pt; + empty-cells: show; + text-align: center; + float:left; + margin: 1px 6px 1px 0px; +} + +table.progress td { height: 1em; } +table.progress td.closed { background: #BAE0BA none repeat scroll 0%; } +table.progress td.done { background: #D3EDD3 none repeat scroll 0%; } +table.progress td.todo { background: #eee none repeat scroll 0%; } +p.pourcent {font-size: 80%;} +p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;} + +#roadmap table.progress td { height: 1.2em; } +/***** Tabs *****/ +#content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;} +#content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;} +#content .tabs ul li { + float:left; + list-style-type:none; + white-space:nowrap; + margin-right:4px; + background:#fff; + position:relative; + margin-bottom:-1px; +} +#content .tabs ul li a{ + display:block; + font-size: 0.9em; + text-decoration:none; + line-height:1.3em; + padding:4px 6px 4px 6px; + border: 1px solid #ccc; + border-bottom: 1px solid #bbbbbb; + background-color: #f6f6f6; + color:#999; + font-weight:bold; + border-top-left-radius:3px; + border-top-right-radius:3px; +} + +#content .tabs ul li a:hover { + background-color: #ffffdd; + text-decoration:none; +} + +#content .tabs ul li a.selected { + background-color: #fff; + border: 1px solid #bbbbbb; + border-bottom: 1px solid #fff; + color:#444; +} + +#content .tabs ul li a.selected:hover {background-color: #fff;} + +div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; } + +button.tab-left, button.tab-right { + font-size: 0.9em; + cursor: pointer; + height:24px; + border: 1px solid #ccc; + border-bottom: 1px solid #bbbbbb; + position:absolute; + padding:4px; + width: 20px; + bottom: -1px; +} + +button.tab-left { + right: 20px; + background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%; + border-top-left-radius:3px; +} + +button.tab-right { + right: 0; + background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%; + border-top-right-radius:3px; +} + +/***** Diff *****/ +.diff_out { background: #fcc; } +.diff_out span { background: #faa; } +.diff_in { background: #cfc; } +.diff_in span { background: #afa; } + +.text-diff { + padding: 1em; + background-color:#f6f6f6; + color:#505050; + border: 1px solid #e4e4e4; +} + +/***** Wiki *****/ +div.wiki table { + border-collapse: collapse; + margin-bottom: 1em; +} + +div.wiki table, div.wiki td, div.wiki th { + border: 1px solid #bbb; + padding: 4px; +} + +div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;} + +div.wiki .external { + background-position: 0% 60%; + background-repeat: no-repeat; + padding-left: 12px; + background-image: url(../images/external.png); +} + +div.wiki a.new {color: #b73535;} + +div.wiki ul, div.wiki ol {margin-bottom:1em;} + +div.wiki pre { + margin: 1em 1em 1em 1.6em; + padding: 8px; + background-color: #fafafa; + border: 1px solid #e2e2e2; + width:auto; + overflow-x: auto; + overflow-y: hidden; +} + +div.wiki ul.toc { + background-color: #ffffdd; + border: 1px solid #e4e4e4; + padding: 4px; + line-height: 1.2em; + margin-bottom: 12px; + margin-right: 12px; + margin-left: 0; + display: table +} +* html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */ + +div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; } +div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; } +div.wiki ul.toc ul { margin: 0; padding: 0; } +div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;} +div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;} +div.wiki ul.toc a { + font-size: 0.9em; + font-weight: normal; + text-decoration: none; + color: #606060; +} +div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;} + +a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; } +a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; } +h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; } + +div.wiki img { vertical-align: middle; } + +/***** My page layout *****/ +.block-receiver { + border:1px dashed #c0c0c0; + margin-bottom: 20px; + padding: 15px 0 15px 0; +} + +.mypage-box { + margin:0 0 20px 0; + color:#505050; + line-height:1.5em; +} + +.handle {cursor: move;} + +a.close-icon { + display:block; + margin-top:3px; + overflow:hidden; + width:12px; + height:12px; + background-repeat: no-repeat; + cursor:pointer; + background-image:url('../images/close.png'); +} +a.close-icon:hover {background-image:url('../images/close_hl.png');} + +/***** Gantt chart *****/ +.gantt_hdr { + position:absolute; + top:0; + height:16px; + border-top: 1px solid #c0c0c0; + border-bottom: 1px solid #c0c0c0; + border-right: 1px solid #c0c0c0; + text-align: center; + overflow: hidden; +} + +.gantt_hdr.nwday {background-color:#f1f1f1;} + +.gantt_subjects { font-size: 0.8em; } +.gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; } + +.task { + position: absolute; + height:8px; + font-size:0.8em; + color:#888; + padding:0; + margin:0; + line-height:16px; + white-space:nowrap; +} + +.task.label {width:100%;} +.task.label.project, .task.label.version { font-weight: bold; } + +.task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; } +.task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; } +.task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; } + +.task_todo.parent { background: #888; border: 1px solid #888; height: 3px;} +.task_late.parent, .task_done.parent { height: 3px;} +.task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;} +.task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;} + +.version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} +.version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} +.version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} +.version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; } + +.project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} +.project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} +.project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} +.project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; } + +.version-behind-schedule a, .issue-behind-schedule a {color: #f66914;} +.version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;} + +/***** Icons *****/ +.icon { + background-position: 0% 50%; + background-repeat: no-repeat; + padding-left: 20px; + padding-top: 2px; + padding-bottom: 3px; +} + +.icon-add { background-image: url(../images/add.png); } +.icon-edit { background-image: url(../images/edit.png); } +.icon-copy { background-image: url(../images/copy.png); } +.icon-duplicate { background-image: url(../images/duplicate.png); } +.icon-del { background-image: url(../images/delete.png); } +.icon-move { background-image: url(../images/move.png); } +.icon-save { background-image: url(../images/save.png); } +.icon-cancel { background-image: url(../images/cancel.png); } +.icon-multiple { background-image: url(../images/table_multiple.png); } +.icon-folder { background-image: url(../images/folder.png); } +.open .icon-folder { background-image: url(../images/folder_open.png); } +.icon-package { background-image: url(../images/package.png); } +.icon-user { background-image: url(../images/user.png); } +.icon-projects { background-image: url(../images/projects.png); } +.icon-help { background-image: url(../images/help.png); } +.icon-attachment { background-image: url(../images/attachment.png); } +.icon-history { background-image: url(../images/history.png); } +.icon-time { background-image: url(../images/time.png); } +.icon-time-add { background-image: url(../images/time_add.png); } +.icon-stats { background-image: url(../images/stats.png); } +.icon-warning { background-image: url(../images/warning.png); } +.icon-fav { background-image: url(../images/fav.png); } +.icon-fav-off { background-image: url(../images/fav_off.png); } +.icon-reload { background-image: url(../images/reload.png); } +.icon-lock { background-image: url(../images/locked.png); } +.icon-unlock { background-image: url(../images/unlock.png); } +.icon-checked { background-image: url(../images/true.png); } +.icon-details { background-image: url(../images/zoom_in.png); } +.icon-report { background-image: url(../images/report.png); } +.icon-comment { background-image: url(../images/comment.png); } +.icon-summary { background-image: url(../images/lightning.png); } +.icon-server-authentication { background-image: url(../images/server_key.png); } +.icon-issue { background-image: url(../images/ticket.png); } +.icon-zoom-in { background-image: url(../images/zoom_in.png); } +.icon-zoom-out { background-image: url(../images/zoom_out.png); } +.icon-passwd { background-image: url(../images/textfield_key.png); } +.icon-test { background-image: url(../images/bullet_go.png); } + +.icon-file { background-image: url(../images/files/default.png); } +.icon-file.text-plain { background-image: url(../images/files/text.png); } +.icon-file.text-x-c { background-image: url(../images/files/c.png); } +.icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); } +.icon-file.text-x-java { background-image: url(../images/files/java.png); } +.icon-file.text-x-javascript { background-image: url(../images/files/js.png); } +.icon-file.text-x-php { background-image: url(../images/files/php.png); } +.icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); } +.icon-file.text-xml { background-image: url(../images/files/xml.png); } +.icon-file.text-css { background-image: url(../images/files/css.png); } +.icon-file.text-html { background-image: url(../images/files/html.png); } +.icon-file.image-gif { background-image: url(../images/files/image.png); } +.icon-file.image-jpeg { background-image: url(../images/files/image.png); } +.icon-file.image-png { background-image: url(../images/files/image.png); } +.icon-file.image-tiff { background-image: url(../images/files/image.png); } +.icon-file.application-pdf { background-image: url(../images/files/pdf.png); } +.icon-file.application-zip { background-image: url(../images/files/zip.png); } +.icon-file.application-x-gzip { background-image: url(../images/files/zip.png); } + +img.gravatar { + padding: 2px; + border: solid 1px #d5d5d5; + background: #fff; + vertical-align: middle; +} + +div.issue img.gravatar { + float: left; + margin: 0 6px 0 0; + padding: 5px; +} + +div.issue table img.gravatar { + height: 14px; + width: 14px; + padding: 2px; + float: left; + margin: 0 0.5em 0 0; +} + +h2 img.gravatar {margin: -2px 4px -4px 0;} +h3 img.gravatar {margin: -4px 4px -4px 0;} +h4 img.gravatar {margin: -6px 4px -4px 0;} +td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;} +#activity dt img.gravatar {float: left; margin: 0 1em 1em 0;} +/* Used on 12px Gravatar img tags without the icon background */ +.icon-gravatar {float: left; margin-right: 4px;} + +#activity dt, .journal {clear: left;} + +.journal-link {float: right;} + +h2 img { vertical-align:middle; } + +.hascontextmenu { cursor: context-menu; } + +/************* CodeRay styles *************/ +.syntaxhl div {display: inline;} +.syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;} +.syntaxhl .code pre { overflow: auto } +.syntaxhl .debug { color: white !important; background: blue !important; } + +.syntaxhl .annotation { color:#007 } +.syntaxhl .attribute-name { color:#b48 } +.syntaxhl .attribute-value { color:#700 } +.syntaxhl .binary { color:#509 } +.syntaxhl .char .content { color:#D20 } +.syntaxhl .char .delimiter { color:#710 } +.syntaxhl .char { color:#D20 } +.syntaxhl .class { color:#258; font-weight:bold } +.syntaxhl .class-variable { color:#369 } +.syntaxhl .color { color:#0A0 } +.syntaxhl .comment { color:#385 } +.syntaxhl .comment .char { color:#385 } +.syntaxhl .comment .delimiter { color:#385 } +.syntaxhl .complex { color:#A08 } +.syntaxhl .constant { color:#258; font-weight:bold } +.syntaxhl .decorator { color:#B0B } +.syntaxhl .definition { color:#099; font-weight:bold } +.syntaxhl .delimiter { color:black } +.syntaxhl .directive { color:#088; font-weight:bold } +.syntaxhl .doc { color:#970 } +.syntaxhl .doc-string { color:#D42; font-weight:bold } +.syntaxhl .doctype { color:#34b } +.syntaxhl .entity { color:#800; font-weight:bold } +.syntaxhl .error { color:#F00; background-color:#FAA } +.syntaxhl .escape { color:#666 } +.syntaxhl .exception { color:#C00; font-weight:bold } +.syntaxhl .float { color:#06D } +.syntaxhl .function { color:#06B; font-weight:bold } +.syntaxhl .global-variable { color:#d70 } +.syntaxhl .hex { color:#02b } +.syntaxhl .imaginary { color:#f00 } +.syntaxhl .include { color:#B44; font-weight:bold } +.syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black } +.syntaxhl .inline-delimiter { font-weight: bold; color: #666 } +.syntaxhl .instance-variable { color:#33B } +.syntaxhl .integer { color:#06D } +.syntaxhl .key .char { color: #60f } +.syntaxhl .key .delimiter { color: #404 } +.syntaxhl .key { color: #606 } +.syntaxhl .keyword { color:#939; font-weight:bold } +.syntaxhl .label { color:#970; font-weight:bold } +.syntaxhl .local-variable { color:#963 } +.syntaxhl .namespace { color:#707; font-weight:bold } +.syntaxhl .octal { color:#40E } +.syntaxhl .operator { } +.syntaxhl .predefined { color:#369; font-weight:bold } +.syntaxhl .predefined-constant { color:#069 } +.syntaxhl .predefined-type { color:#0a5; font-weight:bold } +.syntaxhl .preprocessor { color:#579 } +.syntaxhl .pseudo-class { color:#00C; font-weight:bold } +.syntaxhl .regexp .content { color:#808 } +.syntaxhl .regexp .delimiter { color:#404 } +.syntaxhl .regexp .modifier { color:#C2C } +.syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); } +.syntaxhl .reserved { color:#080; font-weight:bold } +.syntaxhl .shell .content { color:#2B2 } +.syntaxhl .shell .delimiter { color:#161 } +.syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); } +.syntaxhl .string .char { color: #46a } +.syntaxhl .string .content { color: #46a } +.syntaxhl .string .delimiter { color: #46a } +.syntaxhl .string .modifier { color: #46a } +.syntaxhl .symbol .content { color:#d33 } +.syntaxhl .symbol .delimiter { color:#d33 } +.syntaxhl .symbol { color:#d33 } +.syntaxhl .tag { color:#070 } +.syntaxhl .type { color:#339; font-weight:bold } +.syntaxhl .value { color: #088; } +.syntaxhl .variable { color:#037 } + +.syntaxhl .insert { background: hsla(120,100%,50%,0.12) } +.syntaxhl .delete { background: hsla(0,100%,50%,0.12) } +.syntaxhl .change { color: #bbf; background: #007; } +.syntaxhl .head { color: #f8f; background: #505 } +.syntaxhl .head .filename { color: white; } + +.syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; } +.syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; } + +.syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold } +.syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold } +.syntaxhl .change .change { color: #88f } +.syntaxhl .head .head { color: #f4f } + +/***** Media print specific styles *****/ +@media print { + #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; } + #main { background: #fff; } + #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;} + #wiki_add_attachment { display:none; } + .hide-when-print { display: none; } + .autoscroll {overflow-x: visible;} + table.list {margin-top:0.5em;} + table.list th, table.list td {border: 1px solid #aaa;} +} + +/* Accessibility specific styles */ +.hidden-for-sighted { + position:absolute; + left:-10000px; + top:auto; + width:1px; + height:1px; + overflow:hidden; +} diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3c/3c182a61315731239b388b13cfd0c9e853524d29.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3c/3c182a61315731239b388b13cfd0c9e853524d29.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,68 @@ +<% diff = Redmine::UnifiedDiff.new( + diff, :type => diff_type, + :max_lines => Setting.diff_max_lines_displayed.to_i, + :style => diff_style) -%> + +<% diff.each do |table_file| -%> +
    +<% if diff.diff_type == 'sbs' -%> + + + + + + + +<% table_file.each_line do |spacing, line| -%> +<% if spacing -%> + + + +<% end -%> + + + + + + +<% end -%> + +
    + <%= h(Redmine::CodesetUtil.to_utf8_by_setting(table_file.file_name)) %> +
    ......
    <%= line.nb_line_left %> +
    <%= Redmine::CodesetUtil.to_utf8_by_setting(line.html_line_left).html_safe %>
    +
    <%= line.nb_line_right %> +
    <%= Redmine::CodesetUtil.to_utf8_by_setting(line.html_line_right).html_safe %>
    +
    + +<% else -%> + + + + + + + +<% table_file.each_line do |spacing, line| %> +<% if spacing -%> + + + +<% end -%> + + + + + +<% end -%> + +
    + <%= h(Redmine::CodesetUtil.to_utf8_by_setting(table_file.file_name)) %> +
    ......
    <%= line.nb_line_left %><%= line.nb_line_right %> +
    <%= Redmine::CodesetUtil.to_utf8_by_setting(line.html_line).html_safe %>
    +
    +<% end -%> +
    +<% end -%> + +<%= l(:text_diff_truncated) if diff.truncated? %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3c/3c9d342ac8ea509336a4fbcb76da57ef9ad85e78.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3c/3c9d342ac8ea509336a4fbcb76da57ef9ad85e78.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,253 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../../test_helper', __FILE__) + +class Redmine::MenuManager::MenuHelperTest < ActionView::TestCase + + include Redmine::MenuManager::MenuHelper + include ERB::Util + fixtures :users, :members, :projects, :enabled_modules, :roles, :member_roles + + def setup + setup_with_controller + # Stub the current menu item in the controller + def current_menu_item + :index + end + end + + context "MenuManager#current_menu_item" do + should "be tested" + end + + context "MenuManager#render_main_menu" do + should "be tested" + end + + context "MenuManager#render_menu" do + should "be tested" + end + + context "MenuManager#menu_item_and_children" do + should "be tested" + end + + context "MenuManager#extract_node_details" do + should "be tested" + end + + def test_render_single_menu_node + node = Redmine::MenuManager::MenuItem.new(:testing, '/test', { }) + @output_buffer = render_single_menu_node(node, 'This is a test', node.url, false) + + assert_select("a.testing", "This is a test") + end + + def test_render_menu_node + single_node = Redmine::MenuManager::MenuItem.new(:single_node, '/test', { }) + @output_buffer = render_menu_node(single_node, nil) + + assert_select("li") do + assert_select("a.single-node", "Single node") + end + end + + def test_render_menu_node_with_nested_items + parent_node = Redmine::MenuManager::MenuItem.new(:parent_node, '/test', { }) + parent_node << Redmine::MenuManager::MenuItem.new(:child_one_node, '/test', { }) + parent_node << Redmine::MenuManager::MenuItem.new(:child_two_node, '/test', { }) + parent_node << + Redmine::MenuManager::MenuItem.new(:child_three_node, '/test', { }) << + Redmine::MenuManager::MenuItem.new(:child_three_inner_node, '/test', { }) + + @output_buffer = render_menu_node(parent_node, nil) + + assert_select("li") do + assert_select("a.parent-node", "Parent node") + assert_select("ul") do + assert_select("li a.child-one-node", "Child one node") + assert_select("li a.child-two-node", "Child two node") + assert_select("li") do + assert_select("a.child-three-node", "Child three node") + assert_select("ul") do + assert_select("li a.child-three-inner-node", "Child three inner node") + end + end + end + end + + end + + def test_render_menu_node_with_children + User.current = User.find(2) + + parent_node = Redmine::MenuManager::MenuItem.new(:parent_node, + '/test', + { + :children => Proc.new {|p| + children = [] + 3.times do |time| + children << Redmine::MenuManager::MenuItem.new("test_child_#{time}", + {:controller => 'issues', :action => 'index'}, + {}) + end + children + } + }) + @output_buffer = render_menu_node(parent_node, Project.find(1)) + + assert_select("li") do + assert_select("a.parent-node", "Parent node") + assert_select("ul") do + assert_select("li a.test-child-0", "Test child 0") + assert_select("li a.test-child-1", "Test child 1") + assert_select("li a.test-child-2", "Test child 2") + end + end + end + + def test_render_menu_node_with_nested_items_and_children + User.current = User.find(2) + + parent_node = Redmine::MenuManager::MenuItem.new(:parent_node, + '/test', + { + :children => Proc.new {|p| + children = [] + 3.times do |time| + children << Redmine::MenuManager::MenuItem.new("test_child_#{time}", {:controller => 'issues', :action => 'index'}, {}) + end + children + } + }) + + parent_node << Redmine::MenuManager::MenuItem.new(:child_node, + '/test', + { + :children => Proc.new {|p| + children = [] + 6.times do |time| + children << Redmine::MenuManager::MenuItem.new("test_dynamic_child_#{time}", {:controller => 'issues', :action => 'index'}, {}) + end + children + } + }) + + @output_buffer = render_menu_node(parent_node, Project.find(1)) + + assert_select("li") do + assert_select("a.parent-node", "Parent node") + assert_select("ul") do + assert_select("li a.child-node", "Child node") + assert_select("ul") do + assert_select("li a.test-dynamic-child-0", "Test dynamic child 0") + assert_select("li a.test-dynamic-child-1", "Test dynamic child 1") + assert_select("li a.test-dynamic-child-2", "Test dynamic child 2") + assert_select("li a.test-dynamic-child-3", "Test dynamic child 3") + assert_select("li a.test-dynamic-child-4", "Test dynamic child 4") + assert_select("li a.test-dynamic-child-5", "Test dynamic child 5") + end + assert_select("li a.test-child-0", "Test child 0") + assert_select("li a.test-child-1", "Test child 1") + assert_select("li a.test-child-2", "Test child 2") + end + end + end + + def test_render_menu_node_with_children_without_an_array + parent_node = Redmine::MenuManager::MenuItem.new(:parent_node, + '/test', + { + :children => Proc.new {|p| Redmine::MenuManager::MenuItem.new("test_child", "/testing", {})} + }) + + assert_raises Redmine::MenuManager::MenuError, ":children must be an array of MenuItems" do + @output_buffer = render_menu_node(parent_node, Project.find(1)) + end + end + + def test_render_menu_node_with_incorrect_children + parent_node = Redmine::MenuManager::MenuItem.new(:parent_node, + '/test', + { + :children => Proc.new {|p| ["a string"] } + }) + + assert_raises Redmine::MenuManager::MenuError, ":children must be an array of MenuItems" do + @output_buffer = render_menu_node(parent_node, Project.find(1)) + end + + end + + def test_menu_items_for_should_yield_all_items_if_passed_a_block + menu_name = :test_menu_items_for_should_yield_all_items_if_passed_a_block + Redmine::MenuManager.map menu_name do |menu| + menu.push(:a_menu, '/', { }) + menu.push(:a_menu_2, '/', { }) + menu.push(:a_menu_3, '/', { }) + end + + items_yielded = [] + menu_items_for(menu_name) do |item| + items_yielded << item + end + + assert_equal 3, items_yielded.size + end + + def test_menu_items_for_should_return_all_items + menu_name = :test_menu_items_for_should_return_all_items + Redmine::MenuManager.map menu_name do |menu| + menu.push(:a_menu, '/', { }) + menu.push(:a_menu_2, '/', { }) + menu.push(:a_menu_3, '/', { }) + end + + items = menu_items_for(menu_name) + assert_equal 3, items.size + end + + def test_menu_items_for_should_skip_unallowed_items_on_a_project + menu_name = :test_menu_items_for_should_skip_unallowed_items_on_a_project + Redmine::MenuManager.map menu_name do |menu| + menu.push(:a_menu, {:controller => 'issues', :action => 'index' }, { }) + menu.push(:a_menu_2, {:controller => 'issues', :action => 'index' }, { }) + menu.push(:unallowed, {:controller => 'issues', :action => 'unallowed' }, { }) + end + + User.current = User.find(2) + + items = menu_items_for(menu_name, Project.find(1)) + assert_equal 2, items.size + end + + def test_menu_items_for_should_skip_items_that_fail_the_conditions + menu_name = :test_menu_items_for_should_skip_items_that_fail_the_conditions + Redmine::MenuManager.map menu_name do |menu| + menu.push(:a_menu, {:controller => 'issues', :action => 'index' }, { }) + menu.push(:unallowed, + {:controller => 'issues', :action => 'index' }, + { :if => Proc.new { false }}) + end + + User.current = User.find(2) + + items = menu_items_for(menu_name, Project.find(1)) + assert_equal 1, items.size + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3c/3cbd1a22df619e52f4440d56f4eefc4b5f35d719.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3c/3cbd1a22df619e52f4440d56f4eefc4b5f35d719.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,129 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class TimeEntryTest < ActiveSupport::TestCase + fixtures :issues, :projects, :users, :time_entries, + :members, :roles, :member_roles, + :trackers, :issue_statuses, + :projects_trackers, + :journals, :journal_details, + :issue_categories, :enumerations, + :groups_users, + :enabled_modules, + :workflows + + def test_hours_format + assertions = { "2" => 2.0, + "21.1" => 21.1, + "2,1" => 2.1, + "1,5h" => 1.5, + "7:12" => 7.2, + "10h" => 10.0, + "10 h" => 10.0, + "45m" => 0.75, + "45 m" => 0.75, + "3h15" => 3.25, + "3h 15" => 3.25, + "3 h 15" => 3.25, + "3 h 15m" => 3.25, + "3 h 15 m" => 3.25, + "3 hours" => 3.0, + "12min" => 0.2, + "12 Min" => 0.2, + } + + assertions.each do |k, v| + t = TimeEntry.new(:hours => k) + assert_equal v, t.hours, "Converting #{k} failed:" + end + end + + def test_hours_should_default_to_nil + assert_nil TimeEntry.new.hours + end + + def test_spent_on_with_blank + c = TimeEntry.new + c.spent_on = '' + assert_nil c.spent_on + end + + def test_spent_on_with_nil + c = TimeEntry.new + c.spent_on = nil + assert_nil c.spent_on + end + + def test_spent_on_with_string + c = TimeEntry.new + c.spent_on = "2011-01-14" + assert_equal Date.parse("2011-01-14"), c.spent_on + end + + def test_spent_on_with_invalid_string + c = TimeEntry.new + c.spent_on = "foo" + assert_nil c.spent_on + end + + def test_spent_on_with_date + c = TimeEntry.new + c.spent_on = Date.today + assert_equal Date.today, c.spent_on + end + + def test_spent_on_with_time + c = TimeEntry.new + c.spent_on = Time.now + assert_equal Date.today, c.spent_on + end + + def test_validate_time_entry + anon = User.anonymous + project = Project.find(1) + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => anon.id, :status_id => 1, + :priority => IssuePriority.all.first, :subject => 'test_create', + :description => 'IssueTest#test_create', :estimated_hours => '1:30') + assert issue.save + activity = TimeEntryActivity.find_by_name('Design') + te = TimeEntry.create(:spent_on => '2010-01-01', + :hours => 100000, + :issue => issue, + :project => project, + :user => anon, + :activity => activity) + assert_equal 1, te.errors.count + end + + def test_set_project_if_nil + anon = User.anonymous + project = Project.find(1) + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => anon.id, :status_id => 1, + :priority => IssuePriority.all.first, :subject => 'test_create', + :description => 'IssueTest#test_create', :estimated_hours => '1:30') + assert issue.save + activity = TimeEntryActivity.find_by_name('Design') + te = TimeEntry.create(:spent_on => '2010-01-01', + :hours => 10, + :issue => issue, + :user => anon, + :activity => activity) + assert_equal project.id, te.project.id + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3d/3d0a99ce133ffd8be7f79466600a3a56ffab0213.svn-base --- a/.svn/pristine/3d/3d0a99ce133ffd8be7f79466600a3a56ffab0213.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module MailHandlerHelper -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3d/3d39eaf1a13ab4e59f2f962003d5f13e35d876e3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3d/3d39eaf1a13ab4e59f2f962003d5f13e35d876e3.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,347 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class TimelogController < ApplicationController + menu_item :issues + + before_filter :find_project_for_new_time_entry, :only => [:create] + before_filter :find_time_entry, :only => [:show, :edit, :update] + before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy] + before_filter :authorize, :except => [:new, :index, :report] + + before_filter :find_optional_project, :only => [:index, :report] + before_filter :find_optional_project_for_new_time_entry, :only => [:new] + before_filter :authorize_global, :only => [:new, :index, :report] + + accept_rss_auth :index + accept_api_auth :index, :show, :create, :update, :destroy + + helper :sort + include SortHelper + helper :issues + include TimelogHelper + helper :custom_fields + include CustomFieldsHelper + + def index + sort_init 'spent_on', 'desc' + sort_update 'spent_on' => ['spent_on', "#{TimeEntry.table_name}.created_on"], + 'user' => 'user_id', + 'activity' => 'activity_id', + 'project' => "#{Project.table_name}.name", + 'issue' => 'issue_id', + 'hours' => 'hours' + + retrieve_date_range + + scope = TimeEntry.visible.spent_between(@from, @to) + if @issue + scope = scope.on_issue(@issue) + elsif @project + scope = scope.on_project(@project, Setting.display_subprojects_issues?) + end + + respond_to do |format| + format.html { + # Paginate results + @entry_count = scope.count + @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page'] + @entries = scope.all( + :include => [:project, :activity, :user, {:issue => :tracker}], + :order => sort_clause, + :limit => @entry_pages.items_per_page, + :offset => @entry_pages.current.offset + ) + @total_hours = scope.sum(:hours).to_f + + render :layout => !request.xhr? + } + format.api { + @entry_count = scope.count + @offset, @limit = api_offset_and_limit + @entries = scope.all( + :include => [:project, :activity, :user, {:issue => :tracker}], + :order => sort_clause, + :limit => @limit, + :offset => @offset + ) + } + format.atom { + entries = scope.all( + :include => [:project, :activity, :user, {:issue => :tracker}], + :order => "#{TimeEntry.table_name}.created_on DESC", + :limit => Setting.feeds_limit.to_i + ) + render_feed(entries, :title => l(:label_spent_time)) + } + format.csv { + # Export all entries + @entries = scope.all( + :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], + :order => sort_clause + ) + send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv') + } + end + end + + def report + retrieve_date_range + @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], @from, @to) + + respond_to do |format| + format.html { render :layout => !request.xhr? } + format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') } + end + end + + def show + respond_to do |format| + # TODO: Implement html response + format.html { render :nothing => true, :status => 406 } + format.api + end + end + + def new + @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today) + @time_entry.safe_attributes = params[:time_entry] + end + + def create + @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today) + @time_entry.safe_attributes = params[:time_entry] + + call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry }) + + if @time_entry.save + respond_to do |format| + format.html { + flash[:notice] = l(:notice_successful_create) + if params[:continue] + if params[:project_id] + redirect_to :action => 'new', :project_id => @time_entry.project, :issue_id => @time_entry.issue, + :time_entry => {:issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id}, + :back_url => params[:back_url] + else + redirect_to :action => 'new', + :time_entry => {:project_id => @time_entry.project_id, :issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id}, + :back_url => params[:back_url] + end + else + redirect_back_or_default :action => 'index', :project_id => @time_entry.project + end + } + format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) } + end + else + respond_to do |format| + format.html { render :action => 'new' } + format.api { render_validation_errors(@time_entry) } + end + end + end + + def edit + @time_entry.safe_attributes = params[:time_entry] + end + + def update + @time_entry.safe_attributes = params[:time_entry] + + call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry }) + + if @time_entry.save + respond_to do |format| + format.html { + flash[:notice] = l(:notice_successful_update) + redirect_back_or_default :action => 'index', :project_id => @time_entry.project + } + format.api { render_api_ok } + end + else + respond_to do |format| + format.html { render :action => 'edit' } + format.api { render_validation_errors(@time_entry) } + end + end + end + + def bulk_edit + @available_activities = TimeEntryActivity.shared.active + @custom_fields = TimeEntry.first.available_custom_fields + end + + def bulk_update + attributes = parse_params_for_bulk_time_entry_attributes(params) + + unsaved_time_entry_ids = [] + @time_entries.each do |time_entry| + time_entry.reload + time_entry.safe_attributes = attributes + call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry }) + unless time_entry.save + # Keep unsaved time_entry ids to display them in flash error + unsaved_time_entry_ids << time_entry.id + end + end + set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids) + redirect_back_or_default({:controller => 'timelog', :action => 'index', :project_id => @projects.first}) + end + + def destroy + destroyed = TimeEntry.transaction do + @time_entries.each do |t| + unless t.destroy && t.destroyed? + raise ActiveRecord::Rollback + end + end + end + + respond_to do |format| + format.html { + if destroyed + flash[:notice] = l(:notice_successful_delete) + else + flash[:error] = l(:notice_unable_delete_time_entry) + end + redirect_back_or_default(:action => 'index', :project_id => @projects.first) + } + format.api { + if destroyed + render_api_ok + else + render_validation_errors(@time_entries) + end + } + end + end + +private + def find_time_entry + @time_entry = TimeEntry.find(params[:id]) + unless @time_entry.editable_by?(User.current) + render_403 + return false + end + @project = @time_entry.project + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_time_entries + @time_entries = TimeEntry.find_all_by_id(params[:id] || params[:ids]) + raise ActiveRecord::RecordNotFound if @time_entries.empty? + @projects = @time_entries.collect(&:project).compact.uniq + @project = @projects.first if @projects.size == 1 + rescue ActiveRecord::RecordNotFound + render_404 + end + + def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids) + if unsaved_time_entry_ids.empty? + flash[:notice] = l(:notice_successful_update) unless time_entries.empty? + else + flash[:error] = l(:notice_failed_to_save_time_entries, + :count => unsaved_time_entry_ids.size, + :total => time_entries.size, + :ids => '#' + unsaved_time_entry_ids.join(', #')) + end + end + + def find_optional_project_for_new_time_entry + if (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present? + @project = Project.find(project_id) + end + if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present? + @issue = Issue.find(issue_id) + @project ||= @issue.project + end + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_project_for_new_time_entry + find_optional_project_for_new_time_entry + if @project.nil? + render_404 + end + end + + def find_optional_project + if !params[:issue_id].blank? + @issue = Issue.find(params[:issue_id]) + @project = @issue.project + elsif !params[:project_id].blank? + @project = Project.find(params[:project_id]) + end + end + + # Retrieves the date range based on predefined ranges or specific from/to param dates + def retrieve_date_range + @free_period = false + @from, @to = nil, nil + + if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?) + case params[:period].to_s + when 'today' + @from = @to = Date.today + when 'yesterday' + @from = @to = Date.today - 1 + when 'current_week' + @from = Date.today - (Date.today.cwday - 1)%7 + @to = @from + 6 + when 'last_week' + @from = Date.today - 7 - (Date.today.cwday - 1)%7 + @to = @from + 6 + when 'last_2_weeks' + @from = Date.today - 14 - (Date.today.cwday - 1)%7 + @to = @from + 13 + when '7_days' + @from = Date.today - 7 + @to = Date.today + when 'current_month' + @from = Date.civil(Date.today.year, Date.today.month, 1) + @to = (@from >> 1) - 1 + when 'last_month' + @from = Date.civil(Date.today.year, Date.today.month, 1) << 1 + @to = (@from >> 1) - 1 + when '30_days' + @from = Date.today - 30 + @to = Date.today + when 'current_year' + @from = Date.civil(Date.today.year, 1, 1) + @to = Date.civil(Date.today.year, 12, 31) + end + elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?)) + begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end + begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end + @free_period = true + else + # default + end + + @from, @to = @to, @from if @from && @to && @from > @to + end + + def parse_params_for_bulk_time_entry_attributes(params) + attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?} + attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'} + attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values] + attributes + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3d/3d5312afd2b7a1501b0ec17ef249ac149a4a5a3f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3d/3d5312afd2b7a1501b0ec17ef249ac149a4a5a3f.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,383 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) +require 'attachments_controller' + +# Re-raise errors caught by the controller. +class AttachmentsController; def rescue_action(e) raise e end; end + +class AttachmentsControllerTest < ActionController::TestCase + fixtures :users, :projects, :roles, :members, :member_roles, + :enabled_modules, :issues, :trackers, :attachments, + :versions, :wiki_pages, :wikis, :documents + + def setup + @controller = AttachmentsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + set_fixtures_attachments_directory + end + + def teardown + set_tmp_attachments_directory + end + + def test_show_diff + ['inline', 'sbs'].each do |dt| + # 060719210727_changeset_utf8.diff + get :show, :id => 14, :type => dt + assert_response :success + assert_template 'diff' + assert_equal 'text/html', @response.content_type + assert_tag 'th', + :attributes => {:class => /filename/}, + :content => /issues_controller.rb\t\(révision 1484\)/ + assert_tag 'td', + :attributes => {:class => /line-code/}, + :content => /Demande créée avec succès/ + end + set_tmp_attachments_directory + end + + def test_show_diff_replcace_cannot_convert_content + with_settings :repositories_encodings => 'UTF-8' do + ['inline', 'sbs'].each do |dt| + # 060719210727_changeset_iso8859-1.diff + get :show, :id => 5, :type => dt + assert_response :success + assert_template 'diff' + assert_equal 'text/html', @response.content_type + assert_tag 'th', + :attributes => {:class => "filename"}, + :content => /issues_controller.rb\t\(r\?vision 1484\)/ + assert_tag 'td', + :attributes => {:class => /line-code/}, + :content => /Demande cr\?\?e avec succ\?s/ + end + end + set_tmp_attachments_directory + end + + def test_show_diff_latin_1 + with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do + ['inline', 'sbs'].each do |dt| + # 060719210727_changeset_iso8859-1.diff + get :show, :id => 5, :type => dt + assert_response :success + assert_template 'diff' + assert_equal 'text/html', @response.content_type + assert_tag 'th', + :attributes => {:class => "filename"}, + :content => /issues_controller.rb\t\(révision 1484\)/ + assert_tag 'td', + :attributes => {:class => /line-code/}, + :content => /Demande créée avec succès/ + end + end + set_tmp_attachments_directory + end + + def test_save_diff_type + user1 = User.find(1) + user1.pref[:diff_type] = nil + user1.preference.save + user = User.find(1) + assert_nil user.pref[:diff_type] + + @request.session[:user_id] = 1 # admin + get :show, :id => 5 + assert_response :success + assert_template 'diff' + user.reload + assert_equal "inline", user.pref[:diff_type] + get :show, :id => 5, :type => 'sbs' + assert_response :success + assert_template 'diff' + user.reload + assert_equal "sbs", user.pref[:diff_type] + end + + def test_diff_show_filename_in_mercurial_export + set_tmp_attachments_directory + a = Attachment.new(:container => Issue.find(1), + :file => uploaded_test_file("hg-export.diff", "text/plain"), + :author => User.find(1)) + assert a.save + assert_equal 'hg-export.diff', a.filename + + get :show, :id => a.id, :type => 'inline' + assert_response :success + assert_template 'diff' + assert_equal 'text/html', @response.content_type + assert_select 'th.filename', :text => 'test1.txt' + end + + def test_show_text_file + get :show, :id => 4 + assert_response :success + assert_template 'file' + assert_equal 'text/html', @response.content_type + set_tmp_attachments_directory + end + + def test_show_text_file_utf_8 + set_tmp_attachments_directory + a = Attachment.new(:container => Issue.find(1), + :file => uploaded_test_file("japanese-utf-8.txt", "text/plain"), + :author => User.find(1)) + assert a.save + assert_equal 'japanese-utf-8.txt', a.filename + + str_japanese = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e" + str_japanese.force_encoding('UTF-8') if str_japanese.respond_to?(:force_encoding) + + get :show, :id => a.id + assert_response :success + assert_template 'file' + assert_equal 'text/html', @response.content_type + assert_tag :tag => 'th', + :content => '1', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', :content => /#{str_japanese}/ } + end + + def test_show_text_file_replcace_cannot_convert_content + set_tmp_attachments_directory + with_settings :repositories_encodings => 'UTF-8' do + a = Attachment.new(:container => Issue.find(1), + :file => uploaded_test_file("iso8859-1.txt", "text/plain"), + :author => User.find(1)) + assert a.save + assert_equal 'iso8859-1.txt', a.filename + + get :show, :id => a.id + assert_response :success + assert_template 'file' + assert_equal 'text/html', @response.content_type + assert_tag :tag => 'th', + :content => '7', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', :content => /Demande cr\?\?e avec succ\?s/ } + end + end + + def test_show_text_file_latin_1 + set_tmp_attachments_directory + with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do + a = Attachment.new(:container => Issue.find(1), + :file => uploaded_test_file("iso8859-1.txt", "text/plain"), + :author => User.find(1)) + assert a.save + assert_equal 'iso8859-1.txt', a.filename + + get :show, :id => a.id + assert_response :success + assert_template 'file' + assert_equal 'text/html', @response.content_type + assert_tag :tag => 'th', + :content => '7', + :attributes => { :class => 'line-num' }, + :sibling => { :tag => 'td', :content => /Demande créée avec succès/ } + end + end + + def test_show_text_file_should_send_if_too_big + Setting.file_max_size_displayed = 512 + Attachment.find(4).update_attribute :filesize, 754.kilobyte + + get :show, :id => 4 + assert_response :success + assert_equal 'application/x-ruby', @response.content_type + set_tmp_attachments_directory + end + + def test_show_other + get :show, :id => 6 + assert_response :success + assert_equal 'application/octet-stream', @response.content_type + set_tmp_attachments_directory + end + + def test_show_file_from_private_issue_without_permission + get :show, :id => 15 + assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2F15' + set_tmp_attachments_directory + end + + def test_show_file_from_private_issue_with_permission + @request.session[:user_id] = 2 + get :show, :id => 15 + assert_response :success + assert_tag 'h2', :content => /private.diff/ + set_tmp_attachments_directory + end + + def test_show_file_without_container_should_be_denied + set_tmp_attachments_directory + attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2) + + @request.session[:user_id] = 2 + get :show, :id => attachment.id + assert_response 403 + end + + def test_show_invalid_should_respond_with_404 + get :show, :id => 999 + assert_response 404 + end + + def test_download_text_file + get :download, :id => 4 + assert_response :success + assert_equal 'application/x-ruby', @response.content_type + set_tmp_attachments_directory + end + + def test_download_version_file_with_issue_tracking_disabled + Project.find(1).disable_module! :issue_tracking + get :download, :id => 9 + assert_response :success + end + + def test_download_should_assign_content_type_if_blank + Attachment.find(4).update_attribute(:content_type, '') + + get :download, :id => 4 + assert_response :success + assert_equal 'text/x-ruby', @response.content_type + set_tmp_attachments_directory + end + + def test_download_missing_file + get :download, :id => 2 + assert_response 404 + set_tmp_attachments_directory + end + + def test_download_should_be_denied_without_permission + get :download, :id => 7 + assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdownload%2F7' + set_tmp_attachments_directory + end + + if convert_installed? + def test_thumbnail + Attachment.clear_thumbnails + @request.session[:user_id] = 2 + + get :thumbnail, :id => 16 + assert_response :success + assert_equal 'image/png', response.content_type + end + + def test_thumbnail_should_not_exceed_maximum_size + Redmine::Thumbnail.expects(:generate).with {|source, target, size| size == 800} + + @request.session[:user_id] = 2 + get :thumbnail, :id => 16, :size => 2000 + end + + def test_thumbnail_should_round_size + Redmine::Thumbnail.expects(:generate).with {|source, target, size| size == 250} + + @request.session[:user_id] = 2 + get :thumbnail, :id => 16, :size => 260 + end + + def test_thumbnail_should_return_404_for_non_image_attachment + @request.session[:user_id] = 2 + + get :thumbnail, :id => 15 + assert_response 404 + end + + def test_thumbnail_should_return_404_if_thumbnail_generation_failed + Attachment.any_instance.stubs(:thumbnail).returns(nil) + @request.session[:user_id] = 2 + + get :thumbnail, :id => 16 + assert_response 404 + end + + def test_thumbnail_should_be_denied_without_permission + get :thumbnail, :id => 16 + assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fthumbnail%2F16' + end + else + puts '(ImageMagick convert not available)' + end + + def test_destroy_issue_attachment + set_tmp_attachments_directory + issue = Issue.find(3) + @request.session[:user_id] = 2 + + assert_difference 'issue.attachments.count', -1 do + assert_difference 'Journal.count' do + delete :destroy, :id => 1 + assert_redirected_to '/projects/ecookbook' + end + end + assert_nil Attachment.find_by_id(1) + j = Journal.first(:order => 'id DESC') + assert_equal issue, j.journalized + assert_equal 'attachment', j.details.first.property + assert_equal '1', j.details.first.prop_key + assert_equal 'error281.txt', j.details.first.old_value + assert_equal User.find(2), j.user + end + + def test_destroy_wiki_page_attachment + set_tmp_attachments_directory + @request.session[:user_id] = 2 + assert_difference 'Attachment.count', -1 do + delete :destroy, :id => 3 + assert_response 302 + end + end + + def test_destroy_project_attachment + set_tmp_attachments_directory + @request.session[:user_id] = 2 + assert_difference 'Attachment.count', -1 do + delete :destroy, :id => 8 + assert_response 302 + end + end + + def test_destroy_version_attachment + set_tmp_attachments_directory + @request.session[:user_id] = 2 + assert_difference 'Attachment.count', -1 do + delete :destroy, :id => 9 + assert_response 302 + end + end + + def test_destroy_without_permission + set_tmp_attachments_directory + assert_no_difference 'Attachment.count' do + delete :destroy, :id => 3 + end + assert_response 302 + assert Attachment.find_by_id(3) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3d/3d70721df6b551b90581f71def1a31b05dec4f44.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3d/3d70721df6b551b90581f71def1a31b05dec4f44.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,80 @@ + + + + +<%=h html_title %> + + +<%= csrf_meta_tag %> +<%= favicon %> +<%= stylesheet_link_tag 'jquery/jquery-ui-1.8.21', 'application', :media => 'all' %> +<%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %> +<%= javascript_heads %> +<%= heads_for_theme %> +<%= call_hook :view_layouts_base_html_head %> + +<%= yield :header_tags -%> + + +
    +
    +
    +
    +
    + <%= render_menu :account_menu -%> +
    + <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe, :id => 'loggedas') if User.current.logged? %> + <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%> +
    + + + + +
    + + + + + +
    +
    +<%= call_hook :view_layouts_base_body_bottom %> + + diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3d/3d708c19507cb5d1ea8104b5420cd367defc0853.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3d/3d708c19507cb5d1ea8104b5420cd367defc0853.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,23 @@ +/* Italian initialisation for the jQuery UI date picker plugin. */ +/* Written by Antonello Pasella (antonello.pasella@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['it'] = { + closeText: 'Chiudi', + prevText: '<Prec', + nextText: 'Succ>', + currentText: 'Oggi', + monthNames: ['Gennaio','Febbraio','Marzo','Aprile','Maggio','Giugno', + 'Luglio','Agosto','Settembre','Ottobre','Novembre','Dicembre'], + monthNamesShort: ['Gen','Feb','Mar','Apr','Mag','Giu', + 'Lug','Ago','Set','Ott','Nov','Dic'], + dayNames: ['Domenica','Lunedì','Martedì','Mercoledì','Giovedì','Venerdì','Sabato'], + dayNamesShort: ['Dom','Lun','Mar','Mer','Gio','Ven','Sab'], + dayNamesMin: ['Do','Lu','Ma','Me','Gi','Ve','Sa'], + weekHeader: 'Sm', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['it']); +}); diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3e/3e094de32f8f71e746d96f45ad6b2f217c6be36e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3e/3e094de32f8f71e746d96f45ad6b2f217c6be36e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,48 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +In-Reply-To: +From: "John Smith" +To: +Subject: Re: update to issue 2 +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +An update to the issue by the sender. + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris --- Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +>> > --- Reply above. Do not remove this line. --- +>> > +>> > Issue #6779 has been updated by Eric Davis. +>> > +>> > Subject changed from Projects with JSON to Project JSON API +>> > Status changed from New to Assigned +>> > Assignee set to Eric Davis +>> > Priority changed from Low to Normal +>> > Estimated time deleted (1.00) +>> > +>> > Looks like the JSON api for projects was missed. I'm going to be +>> > reviewing the existing APIs and trying to clean them up over the next +>> > few weeks. diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3e/3e339092f343524c6085cf1a81f2c664a0ae64ee.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3e/3e339092f343524c6085cf1a81f2c664a0ae64ee.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,27 @@ +class RedminePluginControllerGenerator < Rails::Generators::NamedBase + source_root File.expand_path("../templates", __FILE__) + argument :controller, :type => :string + argument :actions, :type => :array, :default => [], :banner => "ACTION ACTION ..." + + attr_reader :plugin_path, :plugin_name, :plugin_pretty_name + + def initialize(*args) + super + @plugin_name = file_name.underscore + @plugin_pretty_name = plugin_name.titleize + @plugin_path = "plugins/#{plugin_name}" + @controller_class = controller.camelize + end + + def copy_templates + template 'controller.rb.erb', "#{plugin_path}/app/controllers/#{controller}_controller.rb" + template 'helper.rb.erb', "#{plugin_path}/app/helpers/#{controller}_helper.rb" + template 'functional_test.rb.erb', "#{plugin_path}/test/functional/#{controller}_controller_test.rb" + # View template for each action. + actions.each do |action| + path = "#{plugin_path}/app/views/#{controller}/#{action}.html.erb" + @action_name = action + template 'view.html.erb', path + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3e/3e3965205c1d0192fd841d76b3912a082dfcf332.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3e/3e3965205c1d0192fd841d76b3912a082dfcf332.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,43 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class EnabledModuleTest < ActiveSupport::TestCase + fixtures :projects, :wikis + + def test_enabling_wiki_should_create_a_wiki + CustomField.delete_all + project = Project.create!(:name => 'Project with wiki', :identifier => 'wikiproject') + assert_nil project.wiki + project.enabled_module_names = ['wiki'] + project.reload + assert_not_nil project.wiki + assert_equal 'Wiki', project.wiki.start_page + end + + def test_reenabling_wiki_should_not_create_another_wiki + project = Project.find(1) + assert_not_nil project.wiki + project.enabled_module_names = [] + project.reload + assert_no_difference 'Wiki.count' do + project.enabled_module_names = ['wiki'] + end + assert_not_nil project.wiki + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3e/3e6b94e5f06c2c16daf8b3fdee45e4f66096d317.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3e/3e6b94e5f06c2c16daf8b3fdee45e4f66096d317.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,11 @@ +

    <%=l(:label_watched_issues)%> (<%= Issue.visible.watched_by(user.id).count %>)

    +<% watched_issues = Issue.visible.on_active_project.watched_by(user.id).recently_updated.limit(10) %> + +<%= render :partial => 'issues/list_simple', :locals => { :issues => watched_issues } %> +<% if watched_issues.length > 0 %> +

    <%= link_to l(:label_issue_view_all), :controller => 'issues', + :action => 'index', + :set_filter => 1, + :watcher_id => 'me', + :sort => 'updated_on:desc' %>

    +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3e/3e7da359af4ad8a51a0841b79dbc50ddb307b033.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3e/3e7da359af4ad8a51a0841b79dbc50ddb307b033.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1102 @@ +# Hungarian translations for Ruby on Rails +# by Richard Abonyi (richard.abonyi@gmail.com) +# thanks to KKata, replaced and #hup.hu +# Cleaned up by László Bácsi (http://lackac.hu) +# updated by kfl62 kfl62g@gmail.com +# updated by Gábor Takács (taky77@gmail.com) + +"hu": + direction: ltr + date: + formats: + default: "%Y.%m.%d." + short: "%b %e." + long: "%Y. %B %e." + day_names: [vasárnap, hétfÅ‘, kedd, szerda, csütörtök, péntek, szombat] + abbr_day_names: [v., h., k., sze., cs., p., szo.] + month_names: [~, január, február, március, április, május, június, július, augusztus, szeptember, október, november, december] + abbr_month_names: [~, jan., febr., márc., ápr., máj., jún., júl., aug., szept., okt., nov., dec.] + order: + - :year + - :month + - :day + + time: + formats: + default: "%Y. %b %d., %H:%M" + time: "%H:%M" + short: "%b %e., %H:%M" + long: "%Y. %B %e., %A, %H:%M" + am: "de." + pm: "du." + + datetime: + distance_in_words: + half_a_minute: 'fél perc' + less_than_x_seconds: +# zero: 'kevesebb, mint 1 másodperce' + one: 'kevesebb, mint 1 másodperce' + other: 'kevesebb, mint %{count} másodperce' + x_seconds: + one: '1 másodperce' + other: '%{count} másodperce' + less_than_x_minutes: +# zero: 'kevesebb, mint 1 perce' + one: 'kevesebb, mint 1 perce' + other: 'kevesebb, mint %{count} perce' + x_minutes: + one: '1 perce' + other: '%{count} perce' + about_x_hours: + one: 'csaknem 1 órája' + other: 'csaknem %{count} órája' + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: '1 napja' + other: '%{count} napja' + about_x_months: + one: 'csaknem 1 hónapja' + other: 'csaknem %{count} hónapja' + x_months: + one: '1 hónapja' + other: '%{count} hónapja' + about_x_years: + one: 'csaknem 1 éve' + other: 'csaknem %{count} éve' + over_x_years: + one: 'több, mint 1 éve' + other: 'több, mint %{count} éve' + almost_x_years: + one: "csaknem 1 éve" + other: "csaknem %{count} éve" + prompts: + year: "Év" + month: "Hónap" + day: "Nap" + hour: "Óra" + minute: "Perc" + second: "Másodperc" + + number: + format: + precision: 2 + separator: ',' + delimiter: ' ' + currency: + format: + unit: 'Ft' + precision: 0 + format: '%n %u' + separator: "," + delimiter: "" + percentage: + format: + delimiter: "" + precision: + format: + delimiter: "" + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "bájt" + other: "bájt" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + + support: + array: +# sentence_connector: "és" +# skip_last_comma: true + words_connector: ", " + two_words_connector: " és " + last_word_connector: " és " + activerecord: + errors: + template: + header: + one: "1 hiba miatt nem menthetÅ‘ a következÅ‘: %{model}" + other: "%{count} hiba miatt nem menthetÅ‘ a következÅ‘: %{model}" + body: "Problémás mezÅ‘k:" + messages: + inclusion: "nincs a listában" + exclusion: "nem elérhetÅ‘" + invalid: "nem megfelelÅ‘" + confirmation: "nem egyezik" + accepted: "nincs elfogadva" + empty: "nincs megadva" + blank: "nincs megadva" + too_long: "túl hosszú (nem lehet több %{count} karakternél)" + too_short: "túl rövid (legalább %{count} karakter kell legyen)" + wrong_length: "nem megfelelÅ‘ hosszúságú (%{count} karakter szükséges)" + taken: "már foglalt" + not_a_number: "nem szám" + greater_than: "nagyobb kell legyen, mint %{count}" + greater_than_or_equal_to: "legalább %{count} kell legyen" + equal_to: "pontosan %{count} kell legyen" + less_than: "kevesebb, mint %{count} kell legyen" + less_than_or_equal_to: "legfeljebb %{count} lehet" + odd: "páratlan kell legyen" + even: "páros kell legyen" + greater_than_start_date: "nagyobbnak kell lennie, mint az indítás dátuma" + not_same_project: "nem azonos projekthez tartozik" + circular_dependency: "Ez a kapcsolat egy körkörös függÅ‘séget eredményez" + cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks" + + actionview_instancetag_blank_option: Kérem válasszon + + general_text_No: 'Nem' + general_text_Yes: 'Igen' + general_text_no: 'nem' + general_text_yes: 'igen' + general_lang_name: 'Magyar' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: ISO-8859-2 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: A fiók adatai sikeresen frissítve. + notice_account_invalid_creditentials: Hibás felhasználói név, vagy jelszó + notice_account_password_updated: A jelszó módosítása megtörtént. + notice_account_wrong_password: Hibás jelszó + notice_account_register_done: A fiók sikeresen létrehozva. Aktiválásához kattints az e-mailben kapott linkre + notice_account_unknown_email: Ismeretlen felhasználó. + notice_can_t_change_password: A fiók külsÅ‘ azonosítási forrást használ. A jelszó megváltoztatása nem lehetséges. + notice_account_lost_email_sent: Egy e-mail üzenetben postáztunk Önnek egy leírást az új jelszó beállításáról. + notice_account_activated: Fiókját aktiváltuk. Most már be tud jelentkezni a rendszerbe. + notice_successful_create: Sikeres létrehozás. + notice_successful_update: Sikeres módosítás. + notice_successful_delete: Sikeres törlés. + notice_successful_connection: Sikeres bejelentkezés. + notice_file_not_found: Az oldal, amit meg szeretne nézni nem található, vagy átkerült egy másik helyre. + notice_locking_conflict: Az adatot egy másik felhasználó idÅ‘ közben módosította. + notice_not_authorized: Nincs hozzáférési engedélye ehhez az oldalhoz. + notice_email_sent: "Egy e-mail üzenetet küldtünk a következÅ‘ címre %{value}" + notice_email_error: "Hiba történt a levél küldése közben (%{value})" + notice_feeds_access_key_reseted: Az RSS hozzáférési kulcsát újra generáltuk. + notice_failed_to_save_issues: "Nem sikerült a %{count} feladat(ok) mentése a %{total} -ban kiválasztva: %{ids}." + notice_no_issue_selected: "Nincs feladat kiválasztva! Kérem jelölje meg melyik feladatot szeretné szerkeszteni!" + notice_account_pending: "A fiókja létrejött, és adminisztrátori jóváhagyásra vár." + notice_default_data_loaded: Az alapértelmezett konfiguráció betöltése sikeresen megtörtént. + + error_can_t_load_default_data: "Az alapértelmezett konfiguráció betöltése nem lehetséges: %{value}" + error_scm_not_found: "A bejegyzés, vagy revízió nem található a tárolóban." + error_scm_command_failed: "A tároló elérése közben hiba lépett fel: %{value}" + error_scm_annotate: "A bejegyzés nem létezik, vagy nics jegyzetekkel ellátva." + error_issue_not_found_in_project: 'A feladat nem található, vagy nem ehhez a projekthez tartozik' + + mail_subject_lost_password: Az Ön Redmine jelszava + mail_body_lost_password: 'A Redmine jelszó megváltoztatásához, kattintson a következÅ‘ linkre:' + mail_subject_register: Redmine azonosító aktiválása + mail_body_register: 'A Redmine azonosítója aktiválásához, kattintson a következÅ‘ linkre:' + mail_body_account_information_external: "A %{value} azonosító használatával bejelentkezhet a Redmine-ba." + mail_body_account_information: Az Ön Redmine azonosítójának információi + mail_subject_account_activation_request: Redmine azonosító aktiválási kérelem + mail_body_account_activation_request: "Egy új felhasználó (%{value}) regisztrált, azonosítója jóváhasgyásra várakozik:" + + gui_validation_error: 1 hiba + gui_validation_error_plural: "%{count} hiba" + + field_name: Név + field_description: Leírás + field_summary: Összegzés + field_is_required: KötelezÅ‘ + field_firstname: Keresztnév + field_lastname: Vezetéknév + field_mail: E-mail + field_filename: Fájl + field_filesize: Méret + field_downloads: Letöltések + field_author: SzerzÅ‘ + field_created_on: Létrehozva + field_updated_on: Módosítva + field_field_format: Formátum + field_is_for_all: Minden projekthez + field_possible_values: Lehetséges értékek + field_regexp: Reguláris kifejezés + field_min_length: Minimum hossz + field_max_length: Maximum hossz + field_value: Érték + field_category: Kategória + field_title: Cím + field_project: Projekt + field_issue: Feladat + field_status: Státusz + field_notes: Feljegyzések + field_is_closed: Feladat lezárva + field_is_default: Alapértelmezett érték + field_tracker: Típus + field_subject: Tárgy + field_due_date: Befejezés dátuma + field_assigned_to: FelelÅ‘s + field_priority: Prioritás + field_fixed_version: Cél verzió + field_user: Felhasználó + field_role: Szerepkör + field_homepage: Weboldal + field_is_public: Nyilvános + field_parent: SzülÅ‘ projekt + field_is_in_roadmap: Feladatok látszanak az életútban + field_login: Azonosító + field_mail_notification: E-mail értesítések + field_admin: Adminisztrátor + field_last_login_on: Utolsó bejelentkezés + field_language: Nyelv + field_effective_date: Dátum + field_password: Jelszó + field_new_password: Új jelszó + field_password_confirmation: MegerÅ‘sítés + field_version: Verzió + field_type: Típus + field_host: Kiszolgáló + field_port: Port + field_account: Felhasználói fiók + field_base_dn: Base DN + field_attr_login: Bejelentkezési tulajdonság + field_attr_firstname: Keresztnév + field_attr_lastname: Vezetéknév + field_attr_mail: E-mail + field_onthefly: On-the-fly felhasználó létrehozás + field_start_date: Kezdés dátuma + field_done_ratio: Készültség (%) + field_auth_source: Azonosítási mód + field_hide_mail: Rejtse el az e-mail címem + field_comments: Megjegyzés + field_url: URL + field_start_page: KezdÅ‘lap + field_subproject: Alprojekt + field_hours: Óra + field_activity: Aktivitás + field_spent_on: Dátum + field_identifier: Azonosító + field_is_filter: SzűrÅ‘ként használható + field_issue_to: Kapcsolódó feladat + field_delay: Késés + field_assignable: Feladat rendelhetÅ‘ ehhez a szerepkörhöz + field_redirect_existing_links: LétezÅ‘ linkek átirányítása + field_estimated_hours: Becsült idÅ‘igény + field_column_names: Oszlopok + field_time_zone: IdÅ‘zóna + field_searchable: KereshetÅ‘ + field_default_value: Alapértelmezett érték + field_comments_sorting: Feljegyzések megjelenítése + + setting_app_title: Alkalmazás címe + setting_app_subtitle: Alkalmazás alcíme + setting_welcome_text: ÜdvözlÅ‘ üzenet + setting_default_language: Alapértelmezett nyelv + setting_login_required: Azonosítás szükséges + setting_self_registration: Regisztráció + setting_attachment_max_size: Melléklet max. mérete + setting_issues_export_limit: Feladatok exportálásának korlátja + setting_mail_from: Kibocsátó e-mail címe + setting_bcc_recipients: Titkos másolat címzet (bcc) + setting_host_name: Kiszolgáló neve + setting_text_formatting: Szöveg formázás + setting_wiki_compression: Wiki történet tömörítés + setting_feeds_limit: RSS tartalom korlát + setting_default_projects_public: Az új projektek alapértelmezés szerint nyilvánosak + setting_autofetch_changesets: Commitok automatikus lehúzása + setting_sys_api_enabled: WS engedélyezése a tárolók kezeléséhez + setting_commit_ref_keywords: Hivatkozó kulcsszavak + setting_commit_fix_keywords: Javítások kulcsszavai + setting_autologin: Automatikus bejelentkezés + setting_date_format: Dátum formátum + setting_time_format: IdÅ‘ formátum + setting_cross_project_issue_relations: Kereszt-projekt feladat hivatkozások engedélyezése + setting_issue_list_default_columns: Az alapértelmezésként megjelenített oszlopok a feladat listában + setting_emails_footer: E-mail lábléc + setting_protocol: Protokol + setting_per_page_options: Objektum / oldal opciók + setting_user_format: Felhasználók megjelenítésének formája + setting_activity_days_default: Napok megjelenítése a project aktivitásnál + setting_display_subprojects_issues: Alapértelmezettként mutassa az alprojektek feladatait is a projekteken + setting_start_of_week: A hét elsÅ‘ napja + + project_module_issue_tracking: Feladat követés + project_module_time_tracking: IdÅ‘ rögzítés + project_module_news: Hírek + project_module_documents: Dokumentumok + project_module_files: Fájlok + project_module_wiki: Wiki + project_module_repository: Forráskód + project_module_boards: Fórumok + + label_user: Felhasználó + label_user_plural: Felhasználók + label_user_new: Új felhasználó + label_project: Projekt + label_project_new: Új projekt + label_project_plural: Projektek + label_x_projects: + zero: nincsenek projektek + one: 1 projekt + other: "%{count} projekt" + label_project_all: Az összes projekt + label_project_latest: Legutóbbi projektek + label_issue: Feladat + label_issue_new: Új feladat + label_issue_plural: Feladatok + label_issue_view_all: Minden feladat + label_issues_by: "%{value} feladatai" + label_issue_added: Feladat hozzáadva + label_issue_updated: Feladat frissítve + label_document: Dokumentum + label_document_new: Új dokumentum + label_document_plural: Dokumentumok + label_document_added: Dokumentum hozzáadva + label_role: Szerepkör + label_role_plural: Szerepkörök + label_role_new: Új szerepkör + label_role_and_permissions: Szerepkörök, és jogosultságok + label_member: RésztvevÅ‘ + label_member_new: Új résztvevÅ‘ + label_member_plural: RésztvevÅ‘k + label_tracker: Feladat típus + label_tracker_plural: Feladat típusok + label_tracker_new: Új feladat típus + label_workflow: Workflow + label_issue_status: Feladat státusz + label_issue_status_plural: Feladat státuszok + label_issue_status_new: Új státusz + label_issue_category: Feladat kategória + label_issue_category_plural: Feladat kategóriák + label_issue_category_new: Új kategória + label_custom_field: Egyéni mezÅ‘ + label_custom_field_plural: Egyéni mezÅ‘k + label_custom_field_new: Új egyéni mezÅ‘ + label_enumerations: Felsorolások + label_enumeration_new: Új érték + label_information: Információ + label_information_plural: Információk + label_please_login: Jelentkezzen be + label_register: Regisztráljon + label_password_lost: Elfelejtett jelszó + label_home: KezdÅ‘lap + label_my_page: Saját kezdÅ‘lapom + label_my_account: Fiókom adatai + label_my_projects: Saját projektem + label_administration: Adminisztráció + label_login: Bejelentkezés + label_logout: Kijelentkezés + label_help: Súgó + label_reported_issues: Bejelentett feladatok + label_assigned_to_me_issues: A nekem kiosztott feladatok + label_last_login: Utolsó bejelentkezés + label_registered_on: Regisztrált + label_activity: Történések + label_overall_activity: Teljes aktivitás + label_new: Új + label_logged_as: Bejelentkezve, mint + label_environment: Környezet + label_authentication: Azonosítás + label_auth_source: Azonosítás módja + label_auth_source_new: Új azonosítási mód + label_auth_source_plural: Azonosítási módok + label_subproject_plural: Alprojektek + label_and_its_subprojects: "%{value} és alprojektjei" + label_min_max_length: Min - Max hossz + label_list: Lista + label_date: Dátum + label_integer: Egész + label_float: LebegÅ‘pontos + label_boolean: Logikai + label_string: Szöveg + label_text: Hosszú szöveg + label_attribute: Tulajdonság + label_attribute_plural: Tulajdonságok + label_download: "%{count} Letöltés" + label_download_plural: "%{count} Letöltés" + label_no_data: Nincs megjeleníthetÅ‘ adat + label_change_status: Státusz módosítása + label_history: Történet + label_attachment: Fájl + label_attachment_new: Új fájl + label_attachment_delete: Fájl törlése + label_attachment_plural: Fájlok + label_file_added: Fájl hozzáadva + label_report: Jelentés + label_report_plural: Jelentések + label_news: Hírek + label_news_new: Hír hozzáadása + label_news_plural: Hírek + label_news_latest: Legutóbbi hírek + label_news_view_all: Minden hír megtekintése + label_news_added: Hír hozzáadva + label_settings: Beállítások + label_overview: Ãttekintés + label_version: Verzió + label_version_new: Új verzió + label_version_plural: Verziók + label_confirmation: Jóváhagyás + label_export_to: Exportálás + label_read: Olvas... + label_public_projects: Nyilvános projektek + label_open_issues: nyitott + label_open_issues_plural: nyitott + label_closed_issues: lezárt + label_closed_issues_plural: lezárt + label_x_open_issues_abbr_on_total: + zero: nyitott 0 / %{total} + one: nyitott 1 / %{total} + other: "nyitott %{count} / %{total}" + label_x_open_issues_abbr: + zero: 0 nyitott + one: 1 nyitott + other: "%{count} nyitott" + label_x_closed_issues_abbr: + zero: 0 lezárt + one: 1 lezárt + other: "%{count} lezárt" + label_total: Összesen + label_permissions: Jogosultságok + label_current_status: Jelenlegi státusz + label_new_statuses_allowed: Státusz változtatások engedélyei + label_all: mind + label_none: nincs + label_nobody: senki + label_next: KövetkezÅ‘ + label_previous: ElÅ‘zÅ‘ + label_used_by: Használja + label_details: Részletek + label_add_note: Jegyzet hozzáadása + label_per_page: Oldalanként + label_calendar: Naptár + label_months_from: hónap, kezdve + label_gantt: Gantt + label_internal: BelsÅ‘ + label_last_changes: "utolsó %{count} változás" + label_change_view_all: Minden változás megtekintése + label_personalize_page: Az oldal testreszabása + label_comment: Megjegyzés + label_comment_plural: Megjegyzés + label_x_comments: + zero: nincs megjegyzés + one: 1 megjegyzés + other: "%{count} megjegyzés" + label_comment_add: Megjegyzés hozzáadása + label_comment_added: Megjegyzés hozzáadva + label_comment_delete: Megjegyzések törlése + label_query: Egyéni lekérdezés + label_query_plural: Egyéni lekérdezések + label_query_new: Új lekérdezés + label_filter_add: SzűrÅ‘ hozzáadása + label_filter_plural: SzűrÅ‘k + label_equals: egyenlÅ‘ + label_not_equals: nem egyenlÅ‘ + label_in_less_than: kevesebb, mint + label_in_more_than: több, mint + label_in: in + label_today: ma + label_all_time: mindenkor + label_yesterday: tegnap + label_this_week: aktuális hét + label_last_week: múlt hét + label_last_n_days: "az elmúlt %{count} nap" + label_this_month: aktuális hónap + label_last_month: múlt hónap + label_this_year: aktuális év + label_date_range: Dátum intervallum + label_less_than_ago: kevesebb, mint nappal ezelÅ‘tt + label_more_than_ago: több, mint nappal ezelÅ‘tt + label_ago: nappal ezelÅ‘tt + label_contains: tartalmazza + label_not_contains: nem tartalmazza + label_day_plural: nap + label_repository: Forráskód + label_repository_plural: Forráskódok + label_browse: Tallóz + label_modification: "%{count} változás" + label_modification_plural: "%{count} változás" + label_revision: Revízió + label_revision_plural: Revíziók + label_associated_revisions: Kapcsolt revíziók + label_added: hozzáadva + label_modified: módosítva + label_deleted: törölve + label_latest_revision: Legutolsó revízió + label_latest_revision_plural: Legutolsó revíziók + label_view_revisions: Revíziók megtekintése + label_max_size: Maximális méret + label_sort_highest: Az elejére + label_sort_higher: Eggyel feljebb + label_sort_lower: Eggyel lejjebb + label_sort_lowest: Az aljára + label_roadmap: Életút + label_roadmap_due_in: "Elkészültéig várhatóan még %{value}" + label_roadmap_overdue: "%{value} késésben" + label_roadmap_no_issues: Nincsenek feladatok ehhez a verzióhoz + label_search: Keresés + label_result_plural: Találatok + label_all_words: Minden szó + label_wiki: Wiki + label_wiki_edit: Wiki szerkesztés + label_wiki_edit_plural: Wiki szerkesztések + label_wiki_page: Wiki oldal + label_wiki_page_plural: Wiki oldalak + label_index_by_title: Cím szerint indexelve + label_index_by_date: Dátum szerint indexelve + label_current_version: Jelenlegi verzió + label_preview: ElÅ‘nézet + label_feed_plural: Visszajelzések + label_changes_details: Változások részletei + label_issue_tracking: Feladat követés + label_spent_time: Ráfordított idÅ‘ + label_f_hour: "%{value} óra" + label_f_hour_plural: "%{value} óra" + label_time_tracking: IdÅ‘ rögzítés + label_change_plural: Változások + label_statistics: Statisztikák + label_commits_per_month: Commitok havonta + label_commits_per_author: Commitok szerzÅ‘nként + label_view_diff: Különbségek megtekintése + label_diff_inline: soronként + label_diff_side_by_side: egymás mellett + label_options: Opciók + label_copy_workflow_from: Workflow másolása innen + label_permissions_report: Jogosultsági riport + label_watched_issues: Megfigyelt feladatok + label_related_issues: Kapcsolódó feladatok + label_applied_status: Alkalmazandó státusz + label_loading: Betöltés... + label_relation_new: Új kapcsolat + label_relation_delete: Kapcsolat törlése + label_relates_to: kapcsolódik + label_duplicates: duplikálja + label_blocks: zárolja + label_blocked_by: zárolta + label_precedes: megelÅ‘zi + label_follows: követi + label_end_to_start: végétÅ‘l indulásig + label_end_to_end: végétÅ‘l végéig + label_start_to_start: indulástól indulásig + label_start_to_end: indulástól végéig + label_stay_logged_in: Emlékezzen rám + label_disabled: kikapcsolva + label_show_completed_versions: A kész verziók mutatása + label_me: én + label_board: Fórum + label_board_new: Új fórum + label_board_plural: Fórumok + label_topic_plural: Témák + label_message_plural: Üzenetek + label_message_last: Utolsó üzenet + label_message_new: Új üzenet + label_message_posted: Üzenet hozzáadva + label_reply_plural: Válaszok + label_send_information: Fiók infomációk küldése a felhasználónak + label_year: Év + label_month: Hónap + label_week: Hét + label_date_from: 'Kezdet:' + label_date_to: 'Vége:' + label_language_based: A felhasználó nyelve alapján + label_sort_by: "%{value} szerint rendezve" + label_send_test_email: Teszt e-mail küldése + label_feeds_access_key_created_on: "RSS hozzáférési kulcs létrehozva %{value}" + label_module_plural: Modulok + label_added_time_by: "%{author} adta hozzá %{age}" + label_updated_time: "Utolsó módosítás %{value}" + label_jump_to_a_project: Ugrás projekthez... + label_file_plural: Fájlok + label_changeset_plural: Changesets + label_default_columns: Alapértelmezett oszlopok + label_no_change_option: (Nincs változás) + label_bulk_edit_selected_issues: A kiválasztott feladatok kötegelt szerkesztése + label_theme: Téma + label_default: Alapértelmezett + label_search_titles_only: Keresés csak a címekben + label_user_mail_option_all: "Minden eseményrÅ‘l minden saját projektemben" + label_user_mail_option_selected: "Minden eseményrÅ‘l a kiválasztott projektekben..." + label_user_mail_no_self_notified: "Nem kérek értesítést az általam végzett módosításokról" + label_registration_activation_by_email: Fiók aktiválása e-mailben + label_registration_manual_activation: Manuális fiók aktiválás + label_registration_automatic_activation: Automatikus fiók aktiválás + label_display_per_page: "Oldalanként: %{value}" + label_age: Kor + label_change_properties: Tulajdonságok változtatása + label_general: Ãltalános + label_more: továbbiak + label_scm: SCM + label_plugins: Pluginek + label_ldap_authentication: LDAP azonosítás + label_downloads_abbr: D/L + label_optional_description: Opcionális leírás + label_add_another_file: Újabb fájl hozzáadása + label_preferences: Tulajdonságok + label_chronological_order: IdÅ‘rendben + label_reverse_chronological_order: Fordított idÅ‘rendben + label_planning: Tervezés + + button_login: Bejelentkezés + button_submit: Elfogad + button_save: Mentés + button_check_all: Mindent kijelöl + button_uncheck_all: Kijelölés törlése + button_delete: Töröl + button_create: Létrehoz + button_test: Teszt + button_edit: Szerkeszt + button_add: Hozzáad + button_change: Változtat + button_apply: Alkalmaz + button_clear: Töröl + button_lock: Zárol + button_unlock: Felold + button_download: Letöltés + button_list: Lista + button_view: Megnéz + button_move: Mozgat + button_back: Vissza + button_cancel: Mégse + button_activate: Aktivál + button_sort: Rendezés + button_log_time: IdÅ‘ rögzítés + button_rollback: Visszaáll erre a verzióra + button_watch: Megfigyel + button_unwatch: Megfigyelés törlése + button_reply: Válasz + button_archive: Archivál + button_unarchive: Dearchivál + button_reset: Reset + button_rename: Ãtnevez + button_change_password: Jelszó megváltoztatása + button_copy: Másol + button_annotate: Jegyzetel + button_update: Módosít + button_configure: Konfigurál + + status_active: aktív + status_registered: regisztrált + status_locked: zárolt + + text_select_mail_notifications: Válasszon eseményeket, amelyekrÅ‘l e-mail értesítést kell küldeni. + text_regexp_info: pl. ^[A-Z0-9]+$ + text_min_max_length_info: 0 = nincs korlátozás + text_project_destroy_confirmation: Biztosan törölni szeretné a projektet és vele együtt minden kapcsolódó adatot ? + text_subprojects_destroy_warning: "Az alprojekt(ek): %{value} szintén törlésre kerülnek." + text_workflow_edit: Válasszon egy szerepkört, és egy feladat típust a workflow szerkesztéséhez + text_are_you_sure: Biztos benne ? + text_tip_issue_begin_day: a feladat ezen a napon kezdÅ‘dik + text_tip_issue_end_day: a feladat ezen a napon ér véget + text_tip_issue_begin_end_day: a feladat ezen a napon kezdÅ‘dik és ér véget + text_caracters_maximum: "maximum %{count} karakter." + text_caracters_minimum: "Legkevesebb %{count} karakter hosszúnek kell lennie." + text_length_between: "Legalább %{min} és legfeljebb %{max} hosszú karakter." + text_tracker_no_workflow: Nincs workflow definiálva ehhez a feladat típushoz + text_unallowed_characters: Tiltott karakterek + text_comma_separated: Több érték megengedett (vesszÅ‘vel elválasztva) + text_issues_ref_in_commit_messages: Hivatkozás feladatokra, feladatok javítása a commit üzenetekben + text_issue_added: "%{author} új feladatot hozott létre %{id} sorszámmal." + text_issue_updated: "%{author} módosította a %{id} sorszámú feladatot." + text_wiki_destroy_confirmation: Biztosan törölni szeretné ezt a wiki-t minden tartalmával együtt ? + text_issue_category_destroy_question: "Néhány feladat (%{count}) hozzá van rendelve ehhez a kategóriához. Mit szeretne tenni?" + text_issue_category_destroy_assignments: Kategória hozzárendelés megszüntetése + text_issue_category_reassign_to: Feladatok újra hozzárendelése másik kategóriához + text_user_mail_option: "A nem kiválasztott projektekrÅ‘l csak akkor kap értesítést, ha figyelést kér rá, vagy részt vesz benne (pl. Ön a létrehozó, vagy a hozzárendelÅ‘)" + text_no_configuration_data: "Szerepkörök, feladat típusok, feladat státuszok, és workflow adatok még nincsenek konfigurálva.\nErÅ‘sen ajánlott, az alapértelmezett konfiguráció betöltése, és utána módosíthatja azt." + text_load_default_configuration: Alapértelmezett konfiguráció betöltése + text_status_changed_by_changeset: "Applied in changeset %{value}." + text_issues_destroy_confirmation: 'Biztos benne, hogy törölni szeretné a kijelölt feladato(ka)t ?' + text_select_project_modules: 'Válassza ki az engedélyezett modulokat ehhez a projekthez:' + text_default_administrator_account_changed: Alapértelmezett adminisztrátor fiók megváltoztatva + text_file_repository_writable: Fájl tároló írható + text_rmagick_available: RMagick elérhetÅ‘ (nem kötelezÅ‘) + text_destroy_time_entries_question: "%{hours} órányi munka van rögzítve a feladatokon, amiket törölni szeretne. Mit szeretne tenni?" + text_destroy_time_entries: A rögzített órák törlése + text_assign_time_entries_to_project: A rögzített órák hozzárendelése a projekthez + text_reassign_time_entries: 'A rögzített órák újra hozzárendelése másik feladathoz:' + + default_role_manager: VezetÅ‘ + default_role_developer: FejlesztÅ‘ + default_role_reporter: BejelentÅ‘ + default_tracker_bug: Hiba + default_tracker_feature: Fejlesztés + default_tracker_support: Támogatás + default_issue_status_new: Új + default_issue_status_in_progress: Folyamatban + default_issue_status_resolved: Megoldva + default_issue_status_feedback: Visszajelzés + default_issue_status_closed: Lezárt + default_issue_status_rejected: Elutasított + default_doc_category_user: Felhasználói dokumentáció + default_doc_category_tech: Technikai dokumentáció + default_priority_low: Alacsony + default_priority_normal: Normál + default_priority_high: Magas + default_priority_urgent: SürgÅ‘s + default_priority_immediate: Azonnal + default_activity_design: Tervezés + default_activity_development: Fejlesztés + + enumeration_issue_priorities: Feladat prioritások + enumeration_doc_categories: Dokumentum kategóriák + enumeration_activities: Tevékenységek (idÅ‘ rögzítés) + mail_body_reminder: "%{count} neked kiosztott feladat határidÅ‘s az elkövetkezÅ‘ %{days} napban:" + mail_subject_reminder: "%{count} feladat határidÅ‘s az elkövetkezÅ‘ %{days} napban" + text_user_wrote: "%{value} írta:" + label_duplicated_by: duplikálta + setting_enabled_scm: ForráskódkezelÅ‘ (SCM) engedélyezése + text_enumeration_category_reassign_to: 'Újra hozzárendelés ehhez:' + text_enumeration_destroy_question: "%{count} objektum van hozzárendelve ehhez az értékhez." + label_incoming_emails: Beérkezett levelek + label_generate_key: Kulcs generálása + setting_mail_handler_api_enabled: Web Service engedélyezése a beérkezett levelekhez + setting_mail_handler_api_key: API kulcs + text_email_delivery_not_configured: "Az E-mail küldés nincs konfigurálva, és az értesítések ki vannak kapcsolva.\nÃllítsd be az SMTP szervert a config/configuration.yml fájlban és indítsd újra az alkalmazást, hogy érvénybe lépjen." + field_parent_title: SzülÅ‘ oldal + label_issue_watchers: MegfigyelÅ‘k + button_quote: Hozzászólás / Idézet + setting_sequential_project_identifiers: Szekvenciális projekt azonosítók generálása + notice_unable_delete_version: A verziót nem lehet törölni + label_renamed: átnevezve + label_copied: lemásolva + setting_plain_text_mail: csak szöveg (nem HTML) + permission_view_files: Fájlok megtekintése + permission_edit_issues: Feladatok szerkesztése + permission_edit_own_time_entries: Saját idÅ‘napló szerkesztése + permission_manage_public_queries: Nyilvános kérések kezelése + permission_add_issues: Feladat felvétele + permission_log_time: IdÅ‘ rögzítése + permission_view_changesets: Változáskötegek megtekintése + permission_view_time_entries: IdÅ‘rögzítések megtekintése + permission_manage_versions: Verziók kezelése + permission_manage_wiki: Wiki kezelése + permission_manage_categories: Feladat kategóriák kezelése + permission_protect_wiki_pages: Wiki oldalak védelme + permission_comment_news: Hírek kommentelése + permission_delete_messages: Üzenetek törlése + permission_select_project_modules: Projekt modulok kezelése + permission_manage_documents: Dokumentumok kezelése + permission_edit_wiki_pages: Wiki oldalak szerkesztése + permission_add_issue_watchers: MegfigyelÅ‘k felvétele + permission_view_gantt: Gannt diagramm megtekintése + permission_move_issues: Feladatok mozgatása + permission_manage_issue_relations: Feladat kapcsolatok kezelése + permission_delete_wiki_pages: Wiki oldalak törlése + permission_manage_boards: Fórumok kezelése + permission_delete_wiki_pages_attachments: Csatolmányok törlése + permission_view_wiki_edits: Wiki történet megtekintése + permission_add_messages: Üzenet beküldése + permission_view_messages: Üzenetek megtekintése + permission_manage_files: Fájlok kezelése + permission_edit_issue_notes: Jegyzetek szerkesztése + permission_manage_news: Hírek kezelése + permission_view_calendar: Naptár megtekintése + permission_manage_members: Tagok kezelése + permission_edit_messages: Üzenetek szerkesztése + permission_delete_issues: Feladatok törlése + permission_view_issue_watchers: MegfigyelÅ‘k listázása + permission_manage_repository: Tárolók kezelése + permission_commit_access: Commit hozzáférés + permission_browse_repository: Tároló böngészése + permission_view_documents: Dokumetumok megtekintése + permission_edit_project: Projekt szerkesztése + permission_add_issue_notes: Jegyzet rögzítése + permission_save_queries: Kérések mentése + permission_view_wiki_pages: Wiki megtekintése + permission_rename_wiki_pages: Wiki oldalak átnevezése + permission_edit_time_entries: IdÅ‘naplók szerkesztése + permission_edit_own_issue_notes: Saját jegyzetek szerkesztése + setting_gravatar_enabled: Felhasználói fényképek engedélyezése + label_example: Példa + text_repository_usernames_mapping: "Ãllítsd be a felhasználó összerendeléseket a Redmine, és a tároló logban található felhasználók között.\nAz azonos felhasználó nevek összerendelése automatikusan megtörténik." + permission_edit_own_messages: Saját üzenetek szerkesztése + permission_delete_own_messages: Saját üzenetek törlése + label_user_activity: "%{value} tevékenységei" + label_updated_time_by: "Módosította %{author} %{age}" + text_diff_truncated: '... A diff fájl vége nem jelenik meg, mert hosszab, mint a megjeleníthetÅ‘ sorok száma.' + setting_diff_max_lines_displayed: A megjelenítendÅ‘ sorok száma (maximum) a diff fájloknál + text_plugin_assets_writable: Plugin eszközök könyvtár írható + warning_attachments_not_saved: "%{count} fájl mentése nem sikerült." + button_create_and_continue: Létrehozás és folytatás + text_custom_field_possible_values_info: 'Értékenként egy sor' + label_display: Megmutat + field_editable: SzerkeszthetÅ‘ + setting_repository_log_display_limit: Maximum hány revíziót mutasson meg a log megjelenítésekor + setting_file_max_size_displayed: Maximum mekkora szövegfájlokat jelenítsen meg soronkénti összehasonlításnál + field_watcher: MegfigyelÅ‘ + setting_openid: OpenID regisztráció és bejelentkezés engedélyezése + field_identity_url: OpenID URL + label_login_with_open_id_option: bejelentkezés OpenID használatával + field_content: Tartalom + label_descending: CsökkenÅ‘ + label_sort: Rendezés + label_ascending: NövekvÅ‘ + label_date_from_to: "%{start} -tól %{end} -ig" + label_greater_or_equal: ">=" + label_less_or_equal: "<=" + text_wiki_page_destroy_question: Ennek az oldalnak %{descendants} gyermek-, és leszármazott oldala van. Mit szeretne tenni? + text_wiki_page_reassign_children: Aloldalak hozzárendelése ehhez a szülÅ‘ oldalhoz + text_wiki_page_nullify_children: Aloldalak átalakítása fÅ‘oldallá + text_wiki_page_destroy_children: Minden aloldal és leszármazottjának törlése + setting_password_min_length: Minimum jelszó hosszúság + field_group_by: Szerint csoportosítva + mail_subject_wiki_content_updated: "'%{id}' wiki oldal frissítve" + label_wiki_content_added: Wiki oldal hozzáadva + mail_subject_wiki_content_added: "Új wiki oldal: '%{id}'" + mail_body_wiki_content_added: "%{author} létrehozta a '%{id}' wiki oldalt." + label_wiki_content_updated: Wiki oldal frissítve + mail_body_wiki_content_updated: "%{author} frissítette a '%{id}' wiki oldalt." + permission_add_project: Projekt létrehozása + setting_new_project_user_role_id: Projekt létrehozási jog nem adminisztrátor felhasználóknak + label_view_all_revisions: Összes verzió + label_tag: Tag + label_branch: Branch + error_no_tracker_in_project: Nincs feladat típus hozzárendelve ehhez a projekthez. Kérem ellenÅ‘rizze a projekt beállításait. + error_no_default_issue_status: Nincs alapértelmezett feladat státusz beállítva. Kérem ellenÅ‘rizze a beállításokat (Itt találja "Adminisztráció -> Feladat státuszok"). + text_journal_changed: "%{label} megváltozott, %{old} helyett %{new} lett" + text_journal_set_to: "%{label} új értéke: %{value}" + text_journal_deleted: "%{label} törölve lett (%{old})" + label_group_plural: Csoportok + label_group: Csoport + label_group_new: Új csoport + label_time_entry_plural: IdÅ‘ráfordítás + text_journal_added: "%{label} %{value} hozzáadva" + field_active: Aktív + enumeration_system_activity: Rendszertevékenység + permission_delete_issue_watchers: MegfigyelÅ‘k törlése + version_status_closed: lezárt + version_status_locked: zárolt + version_status_open: nyitott + error_can_not_reopen_issue_on_closed_version: Lezárt verzióhoz rendelt feladatot nem lehet újranyitni + label_user_anonymous: Névtelen + button_move_and_follow: Mozgatás és követés + setting_default_projects_modules: Alapértelmezett modulok az új projektekhez + setting_gravatar_default: Alapértelmezett Gravatar kép + field_sharing: Megosztás + label_version_sharing_hierarchy: Projekt hierarchiával + label_version_sharing_system: Minden projekttel + label_version_sharing_descendants: Alprojektekkel + label_version_sharing_tree: Projekt fával + label_version_sharing_none: Nincs megosztva + error_can_not_archive_project: A projektet nem lehet archiválni + button_duplicate: Duplikálás + button_copy_and_follow: Másolás és követés + label_copy_source: Forrás + setting_issue_done_ratio: Feladat készültségi szint számolása a következÅ‘ alapján + setting_issue_done_ratio_issue_status: Feladat státusz alapján + error_issue_done_ratios_not_updated: A feladat készültségi szintek nem lettek frissítve. + error_workflow_copy_target: Kérem válasszon cél feladat típus(oka)t és szerepkör(öke)t. + setting_issue_done_ratio_issue_field: A feladat mezÅ‘ alapján + label_copy_same_as_target: A céllal egyezÅ‘ + label_copy_target: Cél + notice_issue_done_ratios_updated: Feladat készültségi szintek frissítve. + error_workflow_copy_source: Kérem válasszon forrás feladat típust vagy szerepkört + label_update_issue_done_ratios: Feladat készültségi szintek frissítése + setting_start_of_week: A hét elsÅ‘ napja + permission_view_issues: Feladatok megtekintése + label_display_used_statuses_only: Csak olyan feladat státuszok megjelenítése, amit ez a feladat típus használ + label_revision_id: Revízió %{value} + label_api_access_key: API hozzáférési kulcs + label_api_access_key_created_on: API hozzáférési kulcs létrehozva %{value} ezelÅ‘tt + label_feeds_access_key: RSS hozzáférési kulcs + notice_api_access_key_reseted: Az API hozzáférési kulcsa újragenerálva. + setting_rest_api_enabled: REST web service engedélyezése + label_missing_api_access_key: Egy API hozzáférési kulcs hiányzik + label_missing_feeds_access_key: RSS hozzáférési kulcs hiányzik + button_show: Megmutat + text_line_separated: Több érték megadása lehetséges (soronként 1 érték). + setting_mail_handler_body_delimiters: E-mailek levágása a következÅ‘ sorok valamelyike esetén + permission_add_subprojects: Alprojektek létrehozása + label_subproject_new: Új alprojekt + text_own_membership_delete_confirmation: |- + Arra készül, hogy eltávolítja egyes vagy minden jogosultságát! Ezt követÅ‘en lehetséges, hogy nem fogja tudni szerkeszteni ezt a projektet! + Biztosan folyatni szeretné? + label_close_versions: Kész verziók lezárása + label_board_sticky: Sticky + setting_cache_formatted_text: Formázott szöveg gyorsítótárazása (Cache) + permission_export_wiki_pages: Wiki oldalak exportálása + permission_manage_project_activities: Projekt tevékenységek kezelése + label_board_locked: Zárolt + error_can_not_delete_custom_field: Nem lehet törölni az egyéni mezÅ‘t + permission_manage_subtasks: Alfeladatok kezelése + label_profile: Profil + error_unable_to_connect: Nem lehet csatlakozni (%{value}) + error_can_not_remove_role: Ez a szerepkör használatban van és ezért nem törölhetÅ‘- + field_parent_issue: SzülÅ‘ feladat + error_unable_delete_issue_status: Nem lehet törölni a feladat állapotát + label_subtask_plural: Alfeladatok + error_can_not_delete_tracker: Ebbe a kategóriába feladatok tartoznak és ezért nem törölhetÅ‘. + label_project_copy_notifications: Küldjön e-mail értesítéseket projektmásolás közben. + field_principal: FelelÅ‘s + label_my_page_block: Saját kezdÅ‘lap-blokk + notice_failed_to_save_members: "Nem sikerült menteni a tago(ka)t: %{errors}." + text_zoom_out: Kicsinyít + text_zoom_in: Nagyít + notice_unable_delete_time_entry: Az idÅ‘rögzítés nem törölhetÅ‘ + label_overall_spent_time: Összes ráfordított idÅ‘ + field_time_entries: IdÅ‘ rögzítés + project_module_gantt: Gantt + project_module_calendar: Naptár + button_edit_associated_wikipage: "Hozzárendelt Wiki oldal szerkesztése: %{page_title}" + field_text: Szöveg mezÅ‘ + label_user_mail_option_only_owner: Csak arról, aminek én vagyok a tulajdonosa + setting_default_notification_option: Alapértelmezett értesítési beállítások + label_user_mail_option_only_my_events: Csak az általam megfigyelt dolgokról vagy amiben részt veszek + label_user_mail_option_only_assigned: Csak a hozzámrendelt dolgokról + label_user_mail_option_none: Semilyen eseményrÅ‘l + field_member_of_group: Hozzárendelt csoport + field_assigned_to_role: Hozzárendelt szerepkör + notice_not_authorized_archived_project: A projekt, amihez hozzá szeretnél férni archiválva lett. + label_principal_search: "Felhasználó vagy csoport keresése:" + label_user_search: "Felhasználó keresése:" + field_visible: Látható + setting_emails_header: Emailek fejléce + setting_commit_logtime_activity_id: A rögzített idÅ‘höz tartozó tevékenység + text_time_logged_by_changeset: Alkalmazva a %{value} changeset-ben. + setting_commit_logtime_enabled: IdÅ‘rögzítés engedélyezése + notice_gantt_chart_truncated: A diagram le lett vágva, mert elérte a maximálisan megjeleníthetÅ‘ elemek számát (%{max}) + setting_gantt_items_limit: A gantt diagrammon megjeleníthetÅ‘ maximális elemek száma + field_warn_on_leaving_unsaved: Figyelmeztessen, nem mentett módosításokat tartalmazó oldal elhagyásakor + text_warn_on_leaving_unsaved: A jelenlegi oldal nem mentett módosításokat tartalmaz, ami elvész, ha elhagyja az oldalt. + label_my_queries: Egyéni lekérdezéseim + text_journal_changed_no_detail: "%{label} módosítva" + label_news_comment_added: Megjegyzés hozzáadva a hírhez + button_expand_all: Mindet kibont + button_collapse_all: Mindet összecsuk + label_additional_workflow_transitions_for_assignee: További átmenetek engedélyezettek, ha a felhasználó a hozzárendelt + label_additional_workflow_transitions_for_author: További átmenetek engedélyezettek, ha a felhasználó a szerzÅ‘ + label_bulk_edit_selected_time_entries: A kiválasztott idÅ‘ bejegyzések csoportos szerkesztése + text_time_entries_destroy_confirmation: Biztos benne, hogy törölni szeretné a kiválasztott idÅ‘ bejegyzés(eke)t? + label_role_anonymous: Anonymous + label_role_non_member: Nem tag + label_issue_note_added: Jegyzet hozzáadva + label_issue_status_updated: Ãllapot módosítva + label_issue_priority_updated: Prioritás módosítva + label_issues_visibility_own: A felhasználó által létrehozott vagy hozzárendelt feladatok + field_issues_visibility: Feladatok láthatósága + label_issues_visibility_all: Minden feladat + permission_set_own_issues_private: Saját feladatok beállítása nyilvánosra vagy privátra + field_is_private: Privát + permission_set_issues_private: Feladatok beállítása nyilvánosra vagy privátra + label_issues_visibility_public: Minden nem privát feladat + text_issues_destroy_descendants_confirmation: Ezzel törölni fog %{count} alfeladatot is. + field_commit_logs_encoding: Commit üzenetek kódlapja + field_scm_path_encoding: Elérési útvonal kódlapja + text_scm_path_encoding_note: "Alapértelmezett: UTF-8" + field_path_to_repository: A repository elérési útja + field_root_directory: Gyökér könyvtár + field_cvs_module: Modul + field_cvsroot: CVSROOT + text_mercurial_repository_note: Helyi repository (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Parancs + text_scm_command_version: Verzió + label_git_report_last_commit: Report last commit for files and directories + text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 feladat + one: 1 feladat + other: "%{count} feladatok" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: mind + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Alprojektekkel + label_cross_project_tree: Projekt fával + label_cross_project_hierarchy: Projekt hierarchiával + label_cross_project_system: Minden projekttel + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3e/3ed8c1b8169d56ca657416dab8458700070dce49.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3e/3ed8c1b8169d56ca657416dab8458700070dce49.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,605 @@ +/* Redmine - project management software + Copyright (C) 2006-2012 Jean-Philippe Lang */ + +function checkAll(id, checked) { + if (checked) { + $('#'+id).find('input[type=checkbox]').attr('checked', true); + } else { + $('#'+id).find('input[type=checkbox]').removeAttr('checked'); + } +} + +function toggleCheckboxesBySelector(selector) { + var all_checked = true; + $(selector).each(function(index) { + if (!$(this).is(':checked')) { all_checked = false; } + }); + $(selector).attr('checked', !all_checked) +} + +function showAndScrollTo(id, focus) { + $('#'+id).show(); + if (focus!=null) { + $('#'+focus).focus(); + } + $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100); +} + +function toggleRowGroup(el) { + var tr = $(el).parents('tr').first(); + var n = tr.next(); + tr.toggleClass('open'); + while (n.length && !n.hasClass('group')) { + n.toggle(); + n = n.next('tr'); + } +} + +function collapseAllRowGroups(el) { + var tbody = $(el).parents('tbody').first(); + tbody.children('tr').each(function(index) { + if ($(this).hasClass('group')) { + $(this).removeClass('open'); + } else { + $(this).hide(); + } + }); +} + +function expandAllRowGroups(el) { + var tbody = $(el).parents('tbody').first(); + tbody.children('tr').each(function(index) { + if ($(this).hasClass('group')) { + $(this).addClass('open'); + } else { + $(this).show(); + } + }); +} + +function toggleAllRowGroups(el) { + var tr = $(el).parents('tr').first(); + if (tr.hasClass('open')) { + collapseAllRowGroups(el); + } else { + expandAllRowGroups(el); + } +} + +function toggleFieldset(el) { + var fieldset = $(el).parents('fieldset').first(); + fieldset.toggleClass('collapsed'); + fieldset.children('div').toggle(); +} + +function hideFieldset(el) { + var fieldset = $(el).parents('fieldset').first(); + fieldset.toggleClass('collapsed'); + fieldset.children('div').hide(); +} + +function initFilters(){ + $('#add_filter_select').change(function(){ + addFilter($(this).val(), '', []); + }); + $('#filters-table td.field input[type=checkbox]').each(function(){ + toggleFilter($(this).val()); + }); + $('#filters-table td.field input[type=checkbox]').live('click',function(){ + toggleFilter($(this).val()); + }); + $('#filters-table .toggle-multiselect').live('click',function(){ + toggleMultiSelect($(this).siblings('select')); + }); + $('#filters-table input[type=text]').live('keypress', function(e){ + if (e.keyCode == 13) submit_query_form("query_form"); + }); +} + +function addFilter(field, operator, values) { + var fieldId = field.replace('.', '_'); + var tr = $('#tr_'+fieldId); + if (tr.length > 0) { + tr.show(); + } else { + buildFilterRow(field, operator, values); + } + $('#cb_'+fieldId).attr('checked', true); + toggleFilter(field); + $('#add_filter_select').val('').children('option').each(function(){ + if ($(this).attr('value') == field) { + $(this).attr('disabled', true); + } + }); +} + +function buildFilterRow(field, operator, values) { + var fieldId = field.replace('.', '_'); + var filterTable = $("#filters-table"); + var filterOptions = availableFilters[field]; + var operators = operatorByType[filterOptions['type']]; + var filterValues = filterOptions['values']; + var i, select; + + var tr = $('').attr('id', 'tr_'+fieldId).html( + '' + + '' + + '  ' + ); + select = tr.find('td.values select'); + if (values.length > 1) {select.attr('multiple', true)}; + for (i=0;i'); + if ($.isArray(filterValue)) { + option.val(filterValue[1]).text(filterValue[0]); + if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);} + } else { + option.val(filterValue).text(filterValue); + if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);} + } + select.append(option); + } + break; + case "date": + case "date_past": + tr.find('td.values').append( + '' + + ' ' + + ' '+labelDayPlural+'' + ); + $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions); + $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions); + $('#values_'+fieldId).val(values[0]); + break; + case "string": + case "text": + tr.find('td.values').append( + '' + ); + $('#values_'+fieldId).val(values[0]); + break; + case "relation": + tr.find('td.values').append( + '' + + '' + ); + $('#values_'+fieldId).val(values[0]); + select = tr.find('td.values select'); + for (i=0;i'); + option.val(filterValue[1]).text(filterValue[0]); + if (values[0] == filterValue[1]) {option.attr('selected', true)}; + select.append(option); + } + case "integer": + case "float": + tr.find('td.values').append( + '' + + ' ' + ); + $('#values_'+fieldId+'_1').val(values[0]); + $('#values_'+fieldId+'_2').val(values[1]); + break; + } +} + +function toggleFilter(field) { + var fieldId = field.replace('.', '_'); + if ($('#cb_' + fieldId).is(':checked')) { + $("#operators_" + fieldId).show().removeAttr('disabled'); + toggleOperator(field); + } else { + $("#operators_" + fieldId).hide().attr('disabled', true); + enableValues(field, []); + } +} + +function enableValues(field, indexes) { + var fieldId = field.replace('.', '_'); + $('#tr_'+fieldId+' td.values .value').each(function(index) { + if ($.inArray(index, indexes) >= 0) { + $(this).removeAttr('disabled'); + $(this).parents('span').first().show(); + } else { + $(this).val(''); + $(this).attr('disabled', true); + $(this).parents('span').first().hide(); + } + + if ($(this).hasClass('group')) { + $(this).addClass('open'); + } else { + $(this).show(); + } + }); +} + +function toggleOperator(field) { + var fieldId = field.replace('.', '_'); + var operator = $("#operators_" + fieldId); + switch (operator.val()) { + case "!*": + case "*": + case "t": + case "w": + case "o": + case "c": + enableValues(field, []); + break; + case "><": + enableValues(field, [0,1]); + break; + case "t+": + case ">t-": + case "= 10) return false; + fileFieldCount++; + var s = fields.children('span').first().clone(); + s.children('input.file').attr('name', "attachments[" + fileFieldCount + "][file]").val(''); + s.children('input.description').attr('name', "attachments[" + fileFieldCount + "][description]").val(''); + fields.append(s); +} + +function removeFileField(el) { + var fields = $('#attachments_fields'); + var s = $(el).parents('span').first(); + if (fields.children().length > 1) { + s.remove(); + } else { + s.children('input.file').val(''); + s.children('input.description').val(''); + } +} + +function checkFileSize(el, maxSize, message) { + var files = el.files; + if (files) { + for (var i=0; i maxSize) { + alert(message); + el.value = ""; + } + } + } +} + +function showTab(name) { + $('div#content .tab-content').hide(); + $('div.tabs a').removeClass('selected'); + $('#tab-content-' + name).show(); + $('#tab-' + name).addClass('selected'); + return false; +} + +function moveTabRight(el) { + var lis = $(el).parents('div.tabs').first().find('ul').children(); + var tabsWidth = 0; + var i = 0; + lis.each(function(){ + if ($(this).is(':visible')) { + tabsWidth += $(this).width() + 6; + } + }); + if (tabsWidth < $(el).parents('div.tabs').first().width() - 60) { return; } + while (i0) { + lis.eq(i-1).show(); + } +} + +function displayTabsButtons() { + var lis; + var tabsWidth = 0; + var el; + $('div.tabs').each(function() { + el = $(this); + lis = el.find('ul').children(); + lis.each(function(){ + if ($(this).is(':visible')) { + tabsWidth += $(this).width() + 6; + } + }); + if ((tabsWidth < el.width() - 60) && (lis.first().is(':visible'))) { + el.find('div.tabs-buttons').hide(); + } else { + el.find('div.tabs-buttons').show(); + } + }); +} + +function setPredecessorFieldsVisibility() { + var relationType = $('#relation_relation_type'); + if (relationType.val() == "precedes" || relationType.val() == "follows") { + $('#predecessor_fields').show(); + } else { + $('#predecessor_fields').hide(); + } +} + +function showModal(id, width) { + var el = $('#'+id).first(); + if (el.length == 0 || el.is(':visible')) {return;} + var title = el.find('h3.title').text(); + el.dialog({ + width: width, + modal: true, + resizable: false, + dialogClass: 'modal', + title: title + }); + el.find("input[type=text], input[type=submit]").first().focus(); +} + +function hideModal(el) { + var modal; + if (el) { + modal = $(el).parents('.ui-dialog-content'); + } else { + modal = $('#ajax-modal'); + } + modal.dialog("close"); +} + +function submitPreview(url, form, target) { + $.ajax({ + url: url, + type: 'post', + data: $('#'+form).serialize(), + success: function(data){ + $('#'+target).html(data); + } + }); +} + +function collapseScmEntry(id) { + $('.'+id).each(function() { + if ($(this).hasClass('open')) { + collapseScmEntry($(this).attr('id')); + } + $(this).hide(); + }); + $('#'+id).removeClass('open'); +} + +function expandScmEntry(id) { + $('.'+id).each(function() { + $(this).show(); + if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) { + expandScmEntry($(this).attr('id')); + } + }); + $('#'+id).addClass('open'); +} + +function scmEntryClick(id, url) { + el = $('#'+id); + if (el.hasClass('open')) { + collapseScmEntry(id); + el.addClass('collapsed'); + return false; + } else if (el.hasClass('loaded')) { + expandScmEntry(id); + el.removeClass('collapsed'); + return false; + } + if (el.hasClass('loading')) { + return false; + } + el.addClass('loading'); + $.ajax({ + url: url, + success: function(data){ + el.after(data); + el.addClass('open').addClass('loaded').removeClass('loading'); + } + }); + return true; +} + +function randomKey(size) { + var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'); + var key = ''; + for (i = 0; i < size; i++) { + key += chars[Math.floor(Math.random() * chars.length)]; + } + return key; +} + +// Can't use Rails' remote select because we need the form data +function updateIssueFrom(url) { + $.ajax({ + url: url, + type: 'post', + data: $('#issue-form').serialize() + }); +} + +function updateBulkEditFrom(url) { + $.ajax({ + url: url, + type: 'post', + data: $('#bulk_edit_form').serialize() + }); +} + +function observeAutocompleteField(fieldId, url) { + $(document).ready(function() { + $('#'+fieldId).autocomplete({ + source: url, + minLength: 2 + }); + }); +} + +function observeSearchfield(fieldId, targetId, url) { + $('#'+fieldId).each(function() { + var $this = $(this); + $this.attr('data-value-was', $this.val()); + var check = function() { + var val = $this.val(); + if ($this.attr('data-value-was') != val){ + $this.attr('data-value-was', val); + $.ajax({ + url: url, + type: 'get', + data: {q: $this.val()}, + success: function(data){ $('#'+targetId).html(data); }, + beforeSend: function(){ $this.addClass('ajax-loading'); }, + complete: function(){ $this.removeClass('ajax-loading'); } + }); + } + }; + var reset = function() { + if (timer) { + clearInterval(timer); + timer = setInterval(check, 300); + } + }; + var timer = setInterval(check, 300); + $this.bind('keyup click mousemove', reset); + }); +} + +function observeProjectModules() { + var f = function() { + /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */ + if ($('#project_enabled_module_names_issue_tracking').attr('checked')) { + $('#project_trackers').show(); + }else{ + $('#project_trackers').hide(); + } + }; + + $(window).load(f); + $('#project_enabled_module_names_issue_tracking').change(f); +} + +function initMyPageSortable(list, url) { + $('#list-'+list).sortable({ + connectWith: '.block-receiver', + tolerance: 'pointer', + update: function(){ + $.ajax({ + url: url, + type: 'post', + data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})} + }); + } + }); + $("#list-top, #list-left, #list-right").disableSelection(); +} + +var warnLeavingUnsavedMessage; +function warnLeavingUnsaved(message) { + warnLeavingUnsavedMessage = message; + + $('form').submit(function(){ + $('textarea').removeData('changed'); + }); + $('textarea').change(function(){ + $(this).data('changed', 'changed'); + }); + window.onbeforeunload = function(){ + var warn = false; + $('textarea').blur().each(function(){ + if ($(this).data('changed')) { + warn = true; + } + }); + if (warn) {return warnLeavingUnsavedMessage;} + }; +}; + +$(document).ready(function(){ + $('#ajax-indicator').bind('ajaxSend', function(){ + if ($('.ajax-loading').length == 0) { + $('#ajax-indicator').show(); + } + }); + $('#ajax-indicator').bind('ajaxStop', function(){ + $('#ajax-indicator').hide(); + }); +}); + +function hideOnLoad() { + $('.hol').hide(); +} + +function addFormObserversForDoubleSubmit() { + $('form[method=post]').each(function() { + if (!$(this).hasClass('multiple-submit')) { + $(this).submit(function(form_submission) { + if ($(form_submission.target).attr('data-submitted')) { + form_submission.preventDefault(); + } else { + $(form_submission.target).attr('data-submitted', true); + } + }); + } + }); +} + +$(document).ready(hideOnLoad); +$(document).ready(addFormObserversForDoubleSubmit); diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3f/3f96fdb167ef6475d848f64ee14b65e8b759b8e8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3f/3f96fdb167ef6475d848f64ee14b65e8b759b8e8.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,66 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Watcher < ActiveRecord::Base + belongs_to :watchable, :polymorphic => true + belongs_to :user + + validates_presence_of :user + validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id] + validate :validate_user + + # Unwatch things that users are no longer allowed to view + def self.prune(options={}) + if options.has_key?(:user) + prune_single_user(options[:user], options) + else + pruned = 0 + User.find(:all, :conditions => "id IN (SELECT DISTINCT user_id FROM #{table_name})").each do |user| + pruned += prune_single_user(user, options) + end + pruned + end + end + + protected + + def validate_user + errors.add :user_id, :invalid unless user.nil? || user.active? + end + + private + + def self.prune_single_user(user, options={}) + return unless user.is_a?(User) + pruned = 0 + find(:all, :conditions => {:user_id => user.id}).each do |watcher| + next if watcher.watchable.nil? + + if options.has_key?(:project) + next unless watcher.watchable.respond_to?(:project) && watcher.watchable.project == options[:project] + end + + if watcher.watchable.respond_to?(:visible?) + unless watcher.watchable.visible?(user) + watcher.destroy + pruned += 1 + end + end + end + pruned + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3f/3fb6e89a6e5f14a7dfc6f31c98abfe75827ecbea.svn-base --- a/.svn/pristine/3f/3fb6e89a6e5f14a7dfc6f31c98abfe75827ecbea.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,66 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -require File.expand_path('../../test_helper', __FILE__) - -class PrincipalTest < ActiveSupport::TestCase - - context "#like" do - setup do - Principal.generate!(:login => 'login') - Principal.generate!(:login => 'login2') - - Principal.generate!(:firstname => 'firstname') - Principal.generate!(:firstname => 'firstname2') - - Principal.generate!(:lastname => 'lastname') - Principal.generate!(:lastname => 'lastname2') - - Principal.generate!(:mail => 'mail@example.com') - Principal.generate!(:mail => 'mail2@example.com') - end - - should "search login" do - results = Principal.like('login') - - assert_equal 2, results.count - assert results.all? {|u| u.login.match(/login/) } - end - - should "search firstname" do - results = Principal.like('firstname') - - assert_equal 2, results.count - assert results.all? {|u| u.firstname.match(/firstname/) } - end - - should "search lastname" do - results = Principal.like('lastname') - - assert_equal 2, results.count - assert results.all? {|u| u.lastname.match(/lastname/) } - end - - should "search mail" do - results = Principal.like('mail') - - assert_equal 2, results.count - assert results.all? {|u| u.mail.match(/mail/) } - end - end - -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3f/3fc095ae15466e551cacbea7f0776a64d74a2acd.svn-base --- a/.svn/pristine/3f/3fc095ae15466e551cacbea7f0776a64d74a2acd.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,63 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module UsersHelper - def users_status_options_for_select(selected) - user_count_by_status = User.count(:group => 'status').to_hash - options_for_select([[l(:label_all), ''], - ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", 1], - ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", 2], - ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", 3]], selected) - end - - # Options for the new membership projects combo-box - def options_for_membership_project_select(user, projects) - options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") - options << project_tree_options_for_select(projects) do |p| - {:disabled => (user.projects.include?(p))} - end - options - end - - def user_mail_notification_options(user) - user.valid_notification_options.collect {|o| [l(o.last), o.first]} - end - - def change_status_link(user) - url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil} - - if user.locked? - link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock' - elsif user.registered? - link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock' - elsif user != User.current - link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :put, :class => 'icon icon-lock' - end - end - - def user_settings_tabs - tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general}, - {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural} - ] - if Group.all.any? - tabs.insert 1, {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural} - end - tabs - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3f/3fc44a4cc04ed1435106069e4fc48b4490b1b6a0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3f/3fc44a4cc04ed1435106069e4fc48b4490b1b6a0.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,94 @@ +<%= form_tag({:action => 'edit', :tab => 'repositories'}) do %> + +
    +<%= l(:setting_enabled_scm) %> +<%= hidden_field_tag 'settings[enabled_scm][]', '' %> + + + + + + + <% Redmine::Scm::Base.all.collect do |choice| %> + <% scm_class = "Repository::#{choice}".constantize %> + <% text, value = (choice.is_a?(Array) ? choice : [choice, choice]) %> + <% setting = :enabled_scm %> + <% enabled = Setting.send(setting).include?(value) %> + + + + + + <% end %> +
    <%= l(:text_scm_command) %><%= l(:text_scm_command_version) %>
    + + + <% if enabled %> + <%= + image_tag( + (scm_class.scm_available ? 'true.png' : 'exclamation.png'), + :style => "vertical-align:bottom;" + ) + %> + <%= scm_class.scm_command %> + <% end %> + + <%= scm_class.scm_version_string if enabled %> +
    +

    <%= l(:text_scm_config) %>

    +
    + +
    +

    <%= setting_check_box :autofetch_changesets %>

    + +

    <%= setting_check_box :sys_api_enabled, + :onclick => + "if (this.checked) { $('#settings_sys_api_key').removeAttr('disabled'); } else { $('#settings_sys_api_key').attr('disabled', true); }" %>

    + +

    <%= setting_text_field :sys_api_key, + :size => 30, + :id => 'settings_sys_api_key', + :disabled => !Setting.sys_api_enabled?, + :label => :setting_mail_handler_api_key %> + <%= link_to_function l(:label_generate_key), + "if (!$('#settings_sys_api_key').attr('disabled')) { $('#settings_sys_api_key').val(randomKey(20)) }" %> +

    + +

    <%= setting_text_field :repository_log_display_limit, :size => 6 %>

    +
    + +
    +<%= l(:text_issues_ref_in_commit_messages) %> +

    <%= setting_text_field :commit_ref_keywords, :size => 30 %> +<%= l(:text_comma_separated) %>

    + +

    <%= setting_text_field :commit_fix_keywords, :size => 30 %> + <%= l(:label_applied_status) %>: <%= setting_select :commit_fix_status_id, + [["", 0]] + + IssueStatus.find(:all).collect{ + |status| [status.name, status.id.to_s] + }, + :label => false %> + <%= l(:field_done_ratio) %>: <%= setting_select :commit_fix_done_ratio, + (0..10).to_a.collect {|r| ["#{r*10} %", "#{r*10}"] }, + :blank => :label_no_change_option, + :label => false %> +<%= l(:text_comma_separated) %>

    + +

    <%= setting_check_box :commit_cross_project_ref %>

    + +

    <%= setting_check_box :commit_logtime_enabled, + :onclick => + "if (this.checked) { $('#settings_commit_logtime_activity_id').removeAttr('disabled'); } else { $('#settings_commit_logtime_activity_id').attr('disabled', true); }"%>

    + +

    <%= setting_select :commit_logtime_activity_id, + [[l(:label_default), 0]] + + TimeEntryActivity.shared.active.collect{|activity| [activity.name, activity.id.to_s]}, + :disabled => !Setting.commit_logtime_enabled?%>

    +
    + +<%= submit_tag l(:button_save) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3f/3fe4e91401e89e608109800b1ea5a303da548232.svn-base --- a/.svn/pristine/3f/3fe4e91401e89e608109800b1ea5a303da548232.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,196 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module TimelogHelper - include ApplicationHelper - - def render_timelog_breadcrumb - links = [] - links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil}) - links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project - if @issue - if @issue.visible? - links << link_to_issue(@issue, :subject => false) - else - links << "##{@issue.id}" - end - end - breadcrumb links - end - - # Returns a collection of activities for a select field. time_entry - # is optional and will be used to check if the selected TimeEntryActivity - # is active. - def activity_collection_for_select_options(time_entry=nil, project=nil) - project ||= @project - if project.nil? - activities = TimeEntryActivity.shared.active - else - activities = project.activities - end - - collection = [] - if time_entry && time_entry.activity && !time_entry.activity.active? - collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] - else - collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default) - end - activities.each { |a| collection << [a.name, a.id] } - collection - end - - def select_hours(data, criteria, value) - if value.to_s.empty? - data.select {|row| row[criteria].blank? } - else - data.select {|row| row[criteria].to_s == value.to_s} - end - end - - def sum_hours(data) - sum = 0 - data.each do |row| - sum += row['hours'].to_f - end - sum - end - - def options_for_period_select(value) - options_for_select([[l(:label_all_time), 'all'], - [l(:label_today), 'today'], - [l(:label_yesterday), 'yesterday'], - [l(:label_this_week), 'current_week'], - [l(:label_last_week), 'last_week'], - [l(:label_last_n_days, 7), '7_days'], - [l(:label_this_month), 'current_month'], - [l(:label_last_month), 'last_month'], - [l(:label_last_n_days, 30), '30_days'], - [l(:label_this_year), 'current_year']], - value) - end - - def entries_to_csv(entries) - decimal_separator = l(:general_csv_decimal_separator) - custom_fields = TimeEntryCustomField.find(:all) - export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv| - # csv header fields - headers = [l(:field_spent_on), - l(:field_user), - l(:field_activity), - l(:field_project), - l(:field_issue), - l(:field_tracker), - l(:field_subject), - l(:field_hours), - l(:field_comments) - ] - # Export custom fields - headers += custom_fields.collect(&:name) - - csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8( - c.to_s, - l(:general_csv_encoding) ) } - # csv lines - entries.each do |entry| - fields = [format_date(entry.spent_on), - entry.user, - entry.activity, - entry.project, - (entry.issue ? entry.issue.id : nil), - (entry.issue ? entry.issue.tracker : nil), - (entry.issue ? entry.issue.subject : nil), - entry.hours.to_s.gsub('.', decimal_separator), - entry.comments - ] - fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) } - - csv << fields.collect {|c| Redmine::CodesetUtil.from_utf8( - c.to_s, - l(:general_csv_encoding) ) } - end - end - export - end - - def format_criteria_value(criteria, value) - if value.blank? - l(:label_none) - elsif k = @available_criterias[criteria][:klass] - obj = k.find_by_id(value.to_i) - if obj.is_a?(Issue) - obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}" - else - obj - end - else - format_value(value, @available_criterias[criteria][:format]) - end - end - - def report_to_csv(criterias, periods, hours) - decimal_separator = l(:general_csv_decimal_separator) - export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv| - # Column headers - headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) } - headers += periods - headers << l(:label_total) - csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8( - c.to_s, - l(:general_csv_encoding) ) } - # Content - report_criteria_to_csv(csv, criterias, periods, hours) - # Total row - str_total = Redmine::CodesetUtil.from_utf8(l(:label_total), l(:general_csv_encoding)) - row = [ str_total ] + [''] * (criterias.size - 1) - total = 0 - periods.each do |period| - sum = sum_hours(select_hours(hours, @columns, period.to_s)) - total += sum - row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '') - end - row << ("%.2f" % total).gsub('.',decimal_separator) - csv << row - end - export - end - - def report_criteria_to_csv(csv, criterias, periods, hours, level=0) - decimal_separator = l(:general_csv_decimal_separator) - hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value| - hours_for_value = select_hours(hours, criterias[level], value) - next if hours_for_value.empty? - row = [''] * level - row << Redmine::CodesetUtil.from_utf8( - format_criteria_value(criterias[level], value).to_s, - l(:general_csv_encoding) ) - row += [''] * (criterias.length - level - 1) - total = 0 - periods.each do |period| - sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)) - total += sum - row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '') - end - row << ("%.2f" % total).gsub('.',decimal_separator) - csv << row - if criterias.length > level + 1 - report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1) - end - end - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3f/3feb470624b1737721fc273bdc7bfd57c3f458eb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3f/3feb470624b1737721fc273bdc7bfd57c3f458eb.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,106 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../test_helper', __FILE__) + +class Redmine::CipheringTest < ActiveSupport::TestCase + + def test_password_should_be_encrypted + Redmine::Configuration.with 'database_cipher_key' => 'secret' do + r = Repository::Subversion.create!(:password => 'foo', :url => 'file:///tmp', :identifier => 'svn') + assert_equal 'foo', r.password + assert r.read_attribute(:password).match(/\Aaes-256-cbc:.+\Z/) + end + end + + def test_password_should_be_clear_with_blank_key + Redmine::Configuration.with 'database_cipher_key' => '' do + r = Repository::Subversion.create!(:password => 'foo', :url => 'file:///tmp', :identifier => 'svn') + assert_equal 'foo', r.password + assert_equal 'foo', r.read_attribute(:password) + end + end + + def test_password_should_be_clear_with_nil_key + Redmine::Configuration.with 'database_cipher_key' => nil do + r = Repository::Subversion.create!(:password => 'foo', :url => 'file:///tmp', :identifier => 'svn') + assert_equal 'foo', r.password + assert_equal 'foo', r.read_attribute(:password) + end + end + + def test_blank_password_should_be_clear + Redmine::Configuration.with 'database_cipher_key' => 'secret' do + r = Repository::Subversion.create!(:password => '', :url => 'file:///tmp', :identifier => 'svn') + assert_equal '', r.password + assert_equal '', r.read_attribute(:password) + end + end + + def test_unciphered_password_should_be_readable + Redmine::Configuration.with 'database_cipher_key' => nil do + r = Repository::Subversion.create!(:password => 'clear', :url => 'file:///tmp', :identifier => 'svn') + end + + Redmine::Configuration.with 'database_cipher_key' => 'secret' do + r = Repository.first(:order => 'id DESC') + assert_equal 'clear', r.password + end + end + + def test_ciphered_password_with_no_cipher_key_configured_should_be_returned_ciphered + Redmine::Configuration.with 'database_cipher_key' => 'secret' do + r = Repository::Subversion.create!(:password => 'clear', :url => 'file:///tmp', :identifier => 'svn') + end + + Redmine::Configuration.with 'database_cipher_key' => '' do + r = Repository.first(:order => 'id DESC') + # password can not be deciphered + assert_nothing_raised do + assert r.password.match(/\Aaes-256-cbc:.+\Z/) + end + end + end + + def test_encrypt_all + Repository.delete_all + Redmine::Configuration.with 'database_cipher_key' => nil do + Repository::Subversion.create!(:password => 'foo', :url => 'file:///tmp', :identifier => 'foo') + Repository::Subversion.create!(:password => 'bar', :url => 'file:///tmp', :identifier => 'bar') + end + + Redmine::Configuration.with 'database_cipher_key' => 'secret' do + assert Repository.encrypt_all(:password) + r = Repository.first(:order => 'id DESC') + assert_equal 'bar', r.password + assert r.read_attribute(:password).match(/\Aaes-256-cbc:.+\Z/) + end + end + + def test_decrypt_all + Repository.delete_all + Redmine::Configuration.with 'database_cipher_key' => 'secret' do + Repository::Subversion.create!(:password => 'foo', :url => 'file:///tmp', :identifier => 'foo') + Repository::Subversion.create!(:password => 'bar', :url => 'file:///tmp', :identifier => 'bar') + + assert Repository.decrypt_all(:password) + r = Repository.first(:order => 'id DESC') + assert_equal 'bar', r.password + assert_equal 'bar', r.read_attribute(:password) + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/3f/3fecfd23ccdcab419a5392dfa395ed433f4bc031.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/3f/3fecfd23ccdcab419a5392dfa395ed433f4bc031.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,133 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Acts + module Searchable + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + # Options: + # * :columns - a column or an array of columns to search + # * :project_key - project foreign key (default to project_id) + # * :date_column - name of the datetime column (default to created_on) + # * :sort_order - name of the column used to sort results (default to :date_column or created_on) + # * :permission - permission required to search the model (default to :view_"objects") + def acts_as_searchable(options = {}) + return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods) + + cattr_accessor :searchable_options + self.searchable_options = options + + if searchable_options[:columns].nil? + raise 'No searchable column defined.' + elsif !searchable_options[:columns].is_a?(Array) + searchable_options[:columns] = [] << searchable_options[:columns] + end + + searchable_options[:project_key] ||= "#{table_name}.project_id" + searchable_options[:date_column] ||= "#{table_name}.created_on" + searchable_options[:order_column] ||= searchable_options[:date_column] + + # Should we search custom fields on this model ? + searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil? + + send :include, Redmine::Acts::Searchable::InstanceMethods + end + end + + module InstanceMethods + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + # Searches the model for the given tokens + # projects argument can be either nil (will search all projects), a project or an array of projects + # Returns the results and the results count + def search(tokens, projects=nil, options={}) + if projects.is_a?(Array) && projects.empty? + # no results + return [[], 0] + end + + # TODO: make user an argument + user = User.current + tokens = [] << tokens unless tokens.is_a?(Array) + projects = [] << projects unless projects.nil? || projects.is_a?(Array) + + find_options = {:include => searchable_options[:include]} + find_options[:order] = "#{searchable_options[:order_column]} " + (options[:before] ? 'DESC' : 'ASC') + + limit_options = {} + limit_options[:limit] = options[:limit] if options[:limit] + if options[:offset] + limit_options[:conditions] = "(#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')" + end + + columns = searchable_options[:columns] + columns = columns[0..0] if options[:titles_only] + + token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"} + + if !options[:titles_only] && searchable_options[:search_custom_fields] + searchable_custom_field_ids = CustomField.find(:all, + :select => 'id', + :conditions => { :type => "#{self.name}CustomField", + :searchable => true }).collect(&:id) + if searchable_custom_field_ids.any? + custom_field_sql = "#{table_name}.id IN (SELECT customized_id FROM #{CustomValue.table_name}" + + " WHERE customized_type='#{self.name}' AND customized_id=#{table_name}.id AND LOWER(value) LIKE ?" + + " AND #{CustomValue.table_name}.custom_field_id IN (#{searchable_custom_field_ids.join(',')}))" + token_clauses << custom_field_sql + end + end + + sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') + + find_options[:conditions] = [sql, * (tokens.collect {|w| "%#{w.downcase}%"} * token_clauses.size).sort] + + scope = self + project_conditions = [] + if searchable_options.has_key?(:permission) + project_conditions << Project.allowed_to_condition(user, searchable_options[:permission] || :view_project) + elsif respond_to?(:visible) + scope = scope.visible(user) + else + ActiveSupport::Deprecation.warn "acts_as_searchable with implicit :permission option is deprecated. Add a visible scope to the #{self.name} model or use explicit :permission option." + project_conditions << Project.allowed_to_condition(user, "view_#{self.name.underscore.pluralize}".to_sym) + end + # TODO: use visible scope options instead + project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil? + project_conditions = project_conditions.empty? ? nil : project_conditions.join(' AND ') + + results = [] + results_count = 0 + + scope = scope.scoped({:conditions => project_conditions}).scoped(find_options) + results_count = scope.count(:all) + results = scope.find(:all, limit_options) + + [results, results_count] + end + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/40/40a074fa4b99359e32098a2704385ed2f567928a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/40/40a074fa4b99359e32098a2704385ed2f567928a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,264 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class ProjectsController < ApplicationController + menu_item :overview + menu_item :roadmap, :only => :roadmap + menu_item :settings, :only => :settings + + before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ] + before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy] + before_filter :authorize_global, :only => [:new, :create] + before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ] + accept_rss_auth :index + accept_api_auth :index, :show, :create, :update, :destroy + + after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller| + if controller.request.post? + controller.send :expire_action, :controller => 'welcome', :action => 'robots' + end + end + + helper :sort + include SortHelper + helper :custom_fields + include CustomFieldsHelper + helper :issues + helper :queries + include QueriesHelper + helper :repositories + include RepositoriesHelper + include ProjectsHelper + + # Lists visible projects + def index + respond_to do |format| + format.html { + scope = Project + unless params[:closed] + scope = scope.active + end + @projects = scope.visible.order('lft').all + } + format.api { + @offset, @limit = api_offset_and_limit + @project_count = Project.visible.count + @projects = Project.visible.all(:offset => @offset, :limit => @limit, :order => 'lft') + } + format.atom { + projects = Project.visible.find(:all, :order => 'created_on DESC', + :limit => Setting.feeds_limit.to_i) + render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}") + } + end + end + + def new + @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") + @trackers = Tracker.sorted.all + @project = Project.new + @project.safe_attributes = params[:project] + end + + def create + @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") + @trackers = Tracker.sorted.all + @project = Project.new + @project.safe_attributes = params[:project] + + if validate_parent_id && @project.save + @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') + # Add current user as a project member if he is not admin + unless User.current.admin? + r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first + m = Member.new(:user => User.current, :roles => [r]) + @project.members << m + end + respond_to do |format| + format.html { + flash[:notice] = l(:notice_successful_create) + redirect_to(params[:continue] ? + {:controller => 'projects', :action => 'new', :project => {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}} : + {:controller => 'projects', :action => 'settings', :id => @project} + ) + } + format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) } + end + else + respond_to do |format| + format.html { render :action => 'new' } + format.api { render_validation_errors(@project) } + end + end + + end + + def copy + @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") + @trackers = Tracker.sorted.all + @root_projects = Project.find(:all, + :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}", + :order => 'name') + @source_project = Project.find(params[:id]) + if request.get? + @project = Project.copy_from(@source_project) + @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers? + else + Mailer.with_deliveries(params[:notifications] == '1') do + @project = Project.new + @project.safe_attributes = params[:project] + if validate_parent_id && @project.copy(@source_project, :only => params[:only]) + @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') + flash[:notice] = l(:notice_successful_create) + redirect_to :controller => 'projects', :action => 'settings', :id => @project + elsif !@project.new_record? + # Project was created + # But some objects were not copied due to validation failures + # (eg. issues from disabled trackers) + # TODO: inform about that + redirect_to :controller => 'projects', :action => 'settings', :id => @project + end + end + end + rescue ActiveRecord::RecordNotFound + # source_project not found + render_404 + end + + # Show @project + def show + if params[:jump] + # try to redirect to the requested menu item + redirect_to_project_menu_item(@project, params[:jump]) && return + end + + @users_by_role = @project.users_by_role + @subprojects = @project.children.visible.all + @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") + @trackers = @project.rolled_up_trackers + + cond = @project.project_condition(Setting.display_subprojects_issues?) + + @open_issues_by_tracker = Issue.visible.open.where(cond).count(:group => :tracker) + @total_issues_by_tracker = Issue.visible.where(cond).count(:group => :tracker) + + if User.current.allowed_to?(:view_time_entries, @project) + @total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f + end + + @key = User.current.rss_key + + respond_to do |format| + format.html + format.api + end + end + + def settings + @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") + @issue_category ||= IssueCategory.new + @member ||= @project.members.new + @trackers = Tracker.sorted.all + @wiki ||= @project.wiki + end + + def edit + end + + def update + @project.safe_attributes = params[:project] + if validate_parent_id && @project.save + @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') + respond_to do |format| + format.html { + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'settings', :id => @project + } + format.api { render_api_ok } + end + else + respond_to do |format| + format.html { + settings + render :action => 'settings' + } + format.api { render_validation_errors(@project) } + end + end + end + + def modules + @project.enabled_module_names = params[:enabled_module_names] + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'settings', :id => @project, :tab => 'modules' + end + + def archive + if request.post? + unless @project.archive + flash[:error] = l(:error_can_not_archive_project) + end + end + redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status])) + end + + def unarchive + @project.unarchive if request.post? && !@project.active? + redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status])) + end + + def close + @project.close + redirect_to project_path(@project) + end + + def reopen + @project.reopen + redirect_to project_path(@project) + end + + # Delete @project + def destroy + @project_to_destroy = @project + if api_request? || params[:confirm] + @project_to_destroy.destroy + respond_to do |format| + format.html { redirect_to :controller => 'admin', :action => 'projects' } + format.api { render_api_ok } + end + end + # hide project in layout + @project = nil + end + + private + + # Validates parent_id param according to user's permissions + # TODO: move it to Project model in a validation that depends on User.current + def validate_parent_id + return true if User.current.admin? + parent_id = params[:project] && params[:project][:parent_id] + if parent_id || @project.new_record? + parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i) + unless @project.allowed_parents.include?(parent) + @project.errors.add :parent_id, :invalid + return false + end + end + true + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/40/40d501b98138649467e7f69b1429131c75dbdbeb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/40/40d501b98138649467e7f69b1429131c75dbdbeb.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,134 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingIssuesTest < ActionController::IntegrationTest + def test_issues_rest_actions + assert_routing( + { :method => 'get', :path => "/issues" }, + { :controller => 'issues', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/issues.pdf" }, + { :controller => 'issues', :action => 'index', :format => 'pdf' } + ) + assert_routing( + { :method => 'get', :path => "/issues.atom" }, + { :controller => 'issues', :action => 'index', :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/issues.xml" }, + { :controller => 'issues', :action => 'index', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/issues/64" }, + { :controller => 'issues', :action => 'show', :id => '64' } + ) + assert_routing( + { :method => 'get', :path => "/issues/64.pdf" }, + { :controller => 'issues', :action => 'show', :id => '64', + :format => 'pdf' } + ) + assert_routing( + { :method => 'get', :path => "/issues/64.atom" }, + { :controller => 'issues', :action => 'show', :id => '64', + :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/issues/64.xml" }, + { :controller => 'issues', :action => 'show', :id => '64', + :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/issues.xml" }, + { :controller => 'issues', :action => 'create', :format => 'xml' } + ) + assert_routing( + { :method => 'get', :path => "/issues/64/edit" }, + { :controller => 'issues', :action => 'edit', :id => '64' } + ) + assert_routing( + { :method => 'put', :path => "/issues/1.xml" }, + { :controller => 'issues', :action => 'update', :id => '1', + :format => 'xml' } + ) + assert_routing( + { :method => 'delete', :path => "/issues/1.xml" }, + { :controller => 'issues', :action => 'destroy', :id => '1', + :format => 'xml' } + ) + end + + def test_issues_rest_actions_scoped_under_project + assert_routing( + { :method => 'get', :path => "/projects/23/issues" }, + { :controller => 'issues', :action => 'index', :project_id => '23' } + ) + assert_routing( + { :method => 'get', :path => "/projects/23/issues.pdf" }, + { :controller => 'issues', :action => 'index', :project_id => '23', + :format => 'pdf' } + ) + assert_routing( + { :method => 'get', :path => "/projects/23/issues.atom" }, + { :controller => 'issues', :action => 'index', :project_id => '23', + :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/projects/23/issues.xml" }, + { :controller => 'issues', :action => 'index', :project_id => '23', + :format => 'xml' } + ) + assert_routing( + { :method => 'post', :path => "/projects/23/issues" }, + { :controller => 'issues', :action => 'create', :project_id => '23' } + ) + assert_routing( + { :method => 'get', :path => "/projects/23/issues/new" }, + { :controller => 'issues', :action => 'new', :project_id => '23' } + ) + end + + def test_issues_form_update + ["post", "put"].each do |method| + assert_routing( + { :method => method, :path => "/projects/23/issues/new" }, + { :controller => 'issues', :action => 'new', :project_id => '23' } + ) + end + end + + def test_issues_extra_actions + assert_routing( + { :method => 'get', :path => "/projects/23/issues/64/copy" }, + { :controller => 'issues', :action => 'new', :project_id => '23', + :copy_from => '64' } + ) + # For updating the bulk edit form + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/issues/bulk_edit" }, + { :controller => 'issues', :action => 'bulk_edit' } + ) + end + assert_routing( + { :method => 'post', :path => "/issues/bulk_update" }, + { :controller => 'issues', :action => 'bulk_update' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/41/41040292634caf486eab580143d2c52c3598e8c6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/41/41040292634caf486eab580143d2c52c3598e8c6.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,12 @@ +<% if User.current.allowed_to?(:add_issue_watchers, @project) %> +
    +<%= link_to l(:button_add), + {:controller => 'watchers', :action => 'new', :object_type => watched.class.name.underscore, :object_id => watched}, + :remote => true, + :method => 'get' %> +
    +<% end %> + +

    <%= l(:label_issue_watchers) %> (<%= watched.watcher_users.size %>)

    + +<%= watchers_list(watched) %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/41/4159ae2540b71521a40b7a57fa4731829b5f161b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/41/4159ae2540b71521a40b7a57fa4731829b5f161b.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,129 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class RepositoryDarcsTest < ActiveSupport::TestCase + fixtures :projects + + include Redmine::I18n + + REPOSITORY_PATH = Rails.root.join('tmp/test/darcs_repository').to_s + NUM_REV = 6 + + def setup + @project = Project.find(3) + @repository = Repository::Darcs.create( + :project => @project, + :url => REPOSITORY_PATH, + :log_encoding => 'UTF-8' + ) + assert @repository + end + + def test_blank_path_to_repository_error_message + set_language_if_valid 'en' + repo = Repository::Darcs.new( + :project => @project, + :identifier => 'test', + :log_encoding => 'UTF-8' + ) + assert !repo.save + assert_include "Path to repository can't be blank", + repo.errors.full_messages + end + + def test_blank_path_to_repository_error_message_fr + set_language_if_valid 'fr' + str = "Chemin du d\xc3\xa9p\xc3\xb4t doit \xc3\xaatre renseign\xc3\xa9(e)" + str.force_encoding('UTF-8') if str.respond_to?(:force_encoding) + repo = Repository::Darcs.new( + :project => @project, + :url => "", + :identifier => 'test', + :log_encoding => 'UTF-8' + ) + assert !repo.save + assert_include str, repo.errors.full_messages + end + + if File.directory?(REPOSITORY_PATH) + def test_fetch_changesets_from_scratch + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + + assert_equal NUM_REV, @repository.changesets.count + assert_equal 13, @repository.filechanges.count + assert_equal "Initial commit.", @repository.changesets.find_by_revision('1').comments + end + + def test_fetch_changesets_incremental + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + + # Remove changesets with revision > 3 + @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 3} + @project.reload + assert_equal 3, @repository.changesets.count + + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + end + + def test_entries + entries = @repository.entries + assert_kind_of Redmine::Scm::Adapters::Entries, entries + end + + def test_entries_invalid_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + assert_nil @repository.entries('', '123') + end + + def test_deleted_files_should_not_be_listed + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + entries = @repository.entries('sources') + assert entries.detect {|e| e.name == 'watchers_controller.rb'} + assert_nil entries.detect {|e| e.name == 'welcome_controller.rb'} + end + + def test_cat + if @repository.scm.supports_cat? + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + cat = @repository.cat("sources/welcome_controller.rb", 2) + assert_not_nil cat + assert cat.include?('class WelcomeController < ApplicationController') + end + end + else + puts "Darcs test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/41/41786988141ecdd7b576a4ea632b825b99be0cde.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/41/41786988141ecdd7b576a4ea632b825b99be0cde.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,34 @@ +
    +<%= link_to l(:label_role_new), new_role_path, :class => 'icon icon-add' %> +<%= link_to l(:label_permissions_report), {:action => 'permissions'}, :class => 'icon icon-summary' %> +
    + +

    <%=l(:label_role_plural)%>

    + + + + + + + + +<% for role in @roles %> + "> + + + + +<% end %> + +
    <%=l(:label_role)%><%=l(:button_sort)%>
    <%= content_tag(role.builtin? ? 'em' : 'span', link_to(h(role.name), edit_role_path(role))) %> + <% unless role.builtin? %> + <%= reorder_links('role', {:action => 'update', :id => role}, :put) %> + <% end %> + + <%= link_to l(:button_copy), new_role_path(:copy => role), :class => 'icon icon-copy' %> + <%= delete_link role_path(role) unless role.builtin? %> +
    + +

    <%= pagination_links_full @role_pages %>

    + +<% html_title(l(:label_role_plural)) -%> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/41/41bdbe14ebfd5b9a3ed45b99dc9b8907015a77f4.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/41/41bdbe14ebfd5b9a3ed45b99dc9b8907015a77f4.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,61 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../test_helper', __FILE__) + +class Redmine::MimeTypeTest < ActiveSupport::TestCase + + def test_of + to_test = {'test.unk' => nil, + 'test.txt' => 'text/plain', + 'test.c' => 'text/x-c', + } + to_test.each do |name, expected| + assert_equal expected, Redmine::MimeType.of(name) + end + end + + def test_css_class_of + to_test = {'test.unk' => nil, + 'test.txt' => 'text-plain', + 'test.c' => 'text-x-c', + } + to_test.each do |name, expected| + assert_equal expected, Redmine::MimeType.css_class_of(name) + end + end + + def test_main_mimetype_of + to_test = {'test.unk' => nil, + 'test.txt' => 'text', + 'test.c' => 'text', + } + to_test.each do |name, expected| + assert_equal expected, Redmine::MimeType.main_mimetype_of(name) + end + end + + def test_is_type + to_test = {['text', 'test.unk'] => false, + ['text', 'test.txt'] => true, + ['text', 'test.c'] => true, + } + to_test.each do |args, expected| + assert_equal expected, Redmine::MimeType.is_type?(*args) + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/41/41c73fb4f60b24cc907c3a2ceee1b536d0b32b8a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/41/41c73fb4f60b24cc907c3a2ceee1b536d0b32b8a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,50 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# FileSystem adapter +# File written by Paul Rivier, at Demotera. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/filesystem_adapter' + +class Repository::Filesystem < Repository + attr_protected :root_url + validates_presence_of :url + + def self.human_attribute_name(attribute_key_name, *args) + attr_name = attribute_key_name.to_s + if attr_name == "url" + attr_name = "root_directory" + end + super(attr_name, *args) + end + + def self.scm_adapter_class + Redmine::Scm::Adapters::FilesystemAdapter + end + + def self.scm_name + 'Filesystem' + end + + def supports_all_revisions? + false + end + + def fetch_changesets + nil + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/41/41cb6312d6e48c2a3277ea516aa018704a81dd1c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/41/41cb6312d6e48c2a3277ea516aa018704a81dd1c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,4 @@ +$('#tab-content-users').html('<%= escape_javascript(render :partial => 'groups/users') %>'); +<% @users.each do |user| %> + $('#user-<%= user.id %>').effect("highlight"); +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/41/41dbe19704933be68922a961147886290a8f9c62.svn-base --- a/.svn/pristine/41/41dbe19704933be68922a961147886290a8f9c62.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1148 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -require File.expand_path('../../test_helper', __FILE__) - -class ProjectTest < ActiveSupport::TestCase - fixtures :projects, :trackers, :issue_statuses, :issues, - :enumerations, :users, :issue_categories, - :projects_trackers, - :roles, - :member_roles, - :members, - :enabled_modules, - :workflows, - :versions, - :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, - :groups_users, - :boards - - def setup - @ecookbook = Project.find(1) - @ecookbook_sub1 = Project.find(3) - User.current = nil - end - - should_validate_presence_of :name - should_validate_presence_of :identifier - - should_validate_uniqueness_of :identifier - - context "associations" do - should_have_many :members - should_have_many :users, :through => :members - should_have_many :member_principals - should_have_many :principals, :through => :member_principals - should_have_many :enabled_modules - should_have_many :issues - should_have_many :issue_changes, :through => :issues - should_have_many :versions - should_have_many :time_entries - should_have_many :queries - should_have_many :documents - should_have_many :news - should_have_many :issue_categories - should_have_many :boards - should_have_many :changesets, :through => :repository - - should_have_one :repository - should_have_one :wiki - - should_have_and_belong_to_many :trackers - should_have_and_belong_to_many :issue_custom_fields - end - - def test_truth - assert_kind_of Project, @ecookbook - assert_equal "eCookbook", @ecookbook.name - end - - def test_default_attributes - with_settings :default_projects_public => '1' do - assert_equal true, Project.new.is_public - assert_equal false, Project.new(:is_public => false).is_public - end - - with_settings :default_projects_public => '0' do - assert_equal false, Project.new.is_public - assert_equal true, Project.new(:is_public => true).is_public - end - - with_settings :sequential_project_identifiers => '1' do - assert !Project.new.identifier.blank? - assert Project.new(:identifier => '').identifier.blank? - end - - with_settings :sequential_project_identifiers => '0' do - assert Project.new.identifier.blank? - assert !Project.new(:identifier => 'test').blank? - end - - with_settings :default_projects_modules => ['issue_tracking', 'repository'] do - assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names - end - - assert_equal Tracker.all, Project.new.trackers - assert_equal Tracker.find(1, 3), Project.new(:tracker_ids => [1, 3]).trackers - end - - def test_update - assert_equal "eCookbook", @ecookbook.name - @ecookbook.name = "eCook" - assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ") - @ecookbook.reload - assert_equal "eCook", @ecookbook.name - end - - def test_validate_identifier - to_test = {"abc" => true, - "ab12" => true, - "ab-12" => true, - "12" => false, - "new" => false} - - to_test.each do |identifier, valid| - p = Project.new - p.identifier = identifier - p.valid? - assert_equal valid, p.errors['identifier'].nil? - end - end - - def test_members_should_be_active_users - Project.all.each do |project| - assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) } - end - end - - def test_users_should_be_active_users - Project.all.each do |project| - assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) } - end - end - - def test_archive - user = @ecookbook.members.first.user - @ecookbook.archive - @ecookbook.reload - - assert !@ecookbook.active? - assert @ecookbook.archived? - assert !user.projects.include?(@ecookbook) - # Subproject are also archived - assert !@ecookbook.children.empty? - assert @ecookbook.descendants.active.empty? - end - - def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects - # Assign an issue of a project to a version of a child project - Issue.find(4).update_attribute :fixed_version_id, 4 - - assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do - assert_equal false, @ecookbook.archive - end - @ecookbook.reload - assert @ecookbook.active? - end - - def test_unarchive - user = @ecookbook.members.first.user - @ecookbook.archive - # A subproject of an archived project can not be unarchived - assert !@ecookbook_sub1.unarchive - - # Unarchive project - assert @ecookbook.unarchive - @ecookbook.reload - assert @ecookbook.active? - assert !@ecookbook.archived? - assert user.projects.include?(@ecookbook) - # Subproject can now be unarchived - @ecookbook_sub1.reload - assert @ecookbook_sub1.unarchive - end - - def test_destroy - # 2 active members - assert_equal 2, @ecookbook.members.size - # and 1 is locked - assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size - # some boards - assert @ecookbook.boards.any? - - @ecookbook.destroy - # make sure that the project non longer exists - assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) } - # make sure related data was removed - assert_nil Member.first(:conditions => {:project_id => @ecookbook.id}) - assert_nil Board.first(:conditions => {:project_id => @ecookbook.id}) - assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id}) - end - - def test_destroying_root_projects_should_clear_data - Project.roots.each do |root| - root.destroy - end - - assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}" - assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}" - assert_equal 0, MemberRole.count - assert_equal 0, Issue.count - assert_equal 0, Journal.count - assert_equal 0, JournalDetail.count - assert_equal 0, Attachment.count - assert_equal 0, EnabledModule.count - assert_equal 0, IssueCategory.count - assert_equal 0, IssueRelation.count - assert_equal 0, Board.count - assert_equal 0, Message.count - assert_equal 0, News.count - assert_equal 0, Query.count(:conditions => "project_id IS NOT NULL") - assert_equal 0, Repository.count - assert_equal 0, Changeset.count - assert_equal 0, Change.count - assert_equal 0, Comment.count - assert_equal 0, TimeEntry.count - assert_equal 0, Version.count - assert_equal 0, Watcher.count - assert_equal 0, Wiki.count - assert_equal 0, WikiPage.count - assert_equal 0, WikiContent.count - assert_equal 0, WikiContent::Version.count - assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size - assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size - assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']}) - end - - def test_move_an_orphan_project_to_a_root_project - sub = Project.find(2) - sub.set_parent! @ecookbook - assert_equal @ecookbook.id, sub.parent.id - @ecookbook.reload - assert_equal 4, @ecookbook.children.size - end - - def test_move_an_orphan_project_to_a_subproject - sub = Project.find(2) - assert sub.set_parent!(@ecookbook_sub1) - end - - def test_move_a_root_project_to_a_project - sub = @ecookbook - assert sub.set_parent!(Project.find(2)) - end - - def test_should_not_move_a_project_to_its_children - sub = @ecookbook - assert !(sub.set_parent!(Project.find(3))) - end - - def test_set_parent_should_add_roots_in_alphabetical_order - ProjectCustomField.delete_all - Project.delete_all - Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil) - Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil) - Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil) - Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil) - - assert_equal 4, Project.count - assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft) - end - - def test_set_parent_should_add_children_in_alphabetical_order - ProjectCustomField.delete_all - parent = Project.create!(:name => 'Parent', :identifier => 'parent') - Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent) - Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent) - Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent) - Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent) - - parent.reload - assert_equal 4, parent.children.size - assert_equal parent.children.sort_by(&:name), parent.children - end - - def test_rebuild_should_sort_children_alphabetically - ProjectCustomField.delete_all - parent = Project.create!(:name => 'Parent', :identifier => 'parent') - Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent) - Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent) - Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent) - Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent) - - Project.update_all("lft = NULL, rgt = NULL") - Project.rebuild! - - parent.reload - assert_equal 4, parent.children.size - assert_equal parent.children.sort_by(&:name), parent.children - end - - - def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy - # Parent issue with a hierarchy project's fixed version - parent_issue = Issue.find(1) - parent_issue.update_attribute(:fixed_version_id, 4) - parent_issue.reload - assert_equal 4, parent_issue.fixed_version_id - - # Should keep fixed versions for the issues - issue_with_local_fixed_version = Issue.find(5) - issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4) - issue_with_local_fixed_version.reload - assert_equal 4, issue_with_local_fixed_version.fixed_version_id - - # Local issue with hierarchy fixed_version - issue_with_hierarchy_fixed_version = Issue.find(13) - issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6) - issue_with_hierarchy_fixed_version.reload - assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id - - # Move project out of the issue's hierarchy - moved_project = Project.find(3) - moved_project.set_parent!(Project.find(2)) - parent_issue.reload - issue_with_local_fixed_version.reload - issue_with_hierarchy_fixed_version.reload - - assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project" - assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in" - assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue." - end - - def test_parent - p = Project.find(6).parent - assert p.is_a?(Project) - assert_equal 5, p.id - end - - def test_ancestors - a = Project.find(6).ancestors - assert a.first.is_a?(Project) - assert_equal [1, 5], a.collect(&:id) - end - - def test_root - r = Project.find(6).root - assert r.is_a?(Project) - assert_equal 1, r.id - end - - def test_children - c = Project.find(1).children - assert c.first.is_a?(Project) - assert_equal [5, 3, 4], c.collect(&:id) - end - - def test_descendants - d = Project.find(1).descendants - assert d.first.is_a?(Project) - assert_equal [5, 6, 3, 4], d.collect(&:id) - end - - def test_allowed_parents_should_be_empty_for_non_member_user - Role.non_member.add_permission!(:add_project) - user = User.find(9) - assert user.memberships.empty? - User.current = user - assert Project.new.allowed_parents.compact.empty? - end - - def test_allowed_parents_with_add_subprojects_permission - Role.find(1).remove_permission!(:add_project) - Role.find(1).add_permission!(:add_subprojects) - User.current = User.find(2) - # new project - assert !Project.new.allowed_parents.include?(nil) - assert Project.new.allowed_parents.include?(Project.find(1)) - # existing root project - assert Project.find(1).allowed_parents.include?(nil) - # existing child - assert Project.find(3).allowed_parents.include?(Project.find(1)) - assert !Project.find(3).allowed_parents.include?(nil) - end - - def test_allowed_parents_with_add_project_permission - Role.find(1).add_permission!(:add_project) - Role.find(1).remove_permission!(:add_subprojects) - User.current = User.find(2) - # new project - assert Project.new.allowed_parents.include?(nil) - assert !Project.new.allowed_parents.include?(Project.find(1)) - # existing root project - assert Project.find(1).allowed_parents.include?(nil) - # existing child - assert Project.find(3).allowed_parents.include?(Project.find(1)) - assert Project.find(3).allowed_parents.include?(nil) - end - - def test_allowed_parents_with_add_project_and_subprojects_permission - Role.find(1).add_permission!(:add_project) - Role.find(1).add_permission!(:add_subprojects) - User.current = User.find(2) - # new project - assert Project.new.allowed_parents.include?(nil) - assert Project.new.allowed_parents.include?(Project.find(1)) - # existing root project - assert Project.find(1).allowed_parents.include?(nil) - # existing child - assert Project.find(3).allowed_parents.include?(Project.find(1)) - assert Project.find(3).allowed_parents.include?(nil) - end - - def test_users_by_role - users_by_role = Project.find(1).users_by_role - assert_kind_of Hash, users_by_role - role = Role.find(1) - assert_kind_of Array, users_by_role[role] - assert users_by_role[role].include?(User.find(2)) - end - - def test_rolled_up_trackers - parent = Project.find(1) - parent.trackers = Tracker.find([1,2]) - child = parent.children.find(3) - - assert_equal [1, 2], parent.tracker_ids - assert_equal [2, 3], child.trackers.collect(&:id) - - assert_kind_of Tracker, parent.rolled_up_trackers.first - assert_equal Tracker.find(1), parent.rolled_up_trackers.first - - assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id) - assert_equal [2, 3], child.rolled_up_trackers.collect(&:id) - end - - def test_rolled_up_trackers_should_ignore_archived_subprojects - parent = Project.find(1) - parent.trackers = Tracker.find([1,2]) - child = parent.children.find(3) - child.trackers = Tracker.find([1,3]) - parent.children.each(&:archive) - - assert_equal [1,2], parent.rolled_up_trackers.collect(&:id) - end - - context "#rolled_up_versions" do - setup do - @project = Project.generate! - @parent_version_1 = Version.generate!(:project => @project) - @parent_version_2 = Version.generate!(:project => @project) - end - - should "include the versions for the current project" do - assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions - end - - should "include versions for a subproject" do - @subproject = Project.generate! - @subproject.set_parent!(@project) - @subproject_version = Version.generate!(:project => @subproject) - - assert_same_elements [ - @parent_version_1, - @parent_version_2, - @subproject_version - ], @project.rolled_up_versions - end - - should "include versions for a sub-subproject" do - @subproject = Project.generate! - @subproject.set_parent!(@project) - @sub_subproject = Project.generate! - @sub_subproject.set_parent!(@subproject) - @sub_subproject_version = Version.generate!(:project => @sub_subproject) - - @project.reload - - assert_same_elements [ - @parent_version_1, - @parent_version_2, - @sub_subproject_version - ], @project.rolled_up_versions - end - - should "only check active projects" do - @subproject = Project.generate! - @subproject.set_parent!(@project) - @subproject_version = Version.generate!(:project => @subproject) - assert @subproject.archive - - @project.reload - - assert !@subproject.active? - assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions - end - end - - def test_shared_versions_none_sharing - p = Project.find(5) - v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none') - assert p.shared_versions.include?(v) - assert !p.children.first.shared_versions.include?(v) - assert !p.root.shared_versions.include?(v) - assert !p.siblings.first.shared_versions.include?(v) - assert !p.root.siblings.first.shared_versions.include?(v) - end - - def test_shared_versions_descendants_sharing - p = Project.find(5) - v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants') - assert p.shared_versions.include?(v) - assert p.children.first.shared_versions.include?(v) - assert !p.root.shared_versions.include?(v) - assert !p.siblings.first.shared_versions.include?(v) - assert !p.root.siblings.first.shared_versions.include?(v) - end - - def test_shared_versions_hierarchy_sharing - p = Project.find(5) - v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy') - assert p.shared_versions.include?(v) - assert p.children.first.shared_versions.include?(v) - assert p.root.shared_versions.include?(v) - assert !p.siblings.first.shared_versions.include?(v) - assert !p.root.siblings.first.shared_versions.include?(v) - end - - def test_shared_versions_tree_sharing - p = Project.find(5) - v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree') - assert p.shared_versions.include?(v) - assert p.children.first.shared_versions.include?(v) - assert p.root.shared_versions.include?(v) - assert p.siblings.first.shared_versions.include?(v) - assert !p.root.siblings.first.shared_versions.include?(v) - end - - def test_shared_versions_system_sharing - p = Project.find(5) - v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system') - assert p.shared_versions.include?(v) - assert p.children.first.shared_versions.include?(v) - assert p.root.shared_versions.include?(v) - assert p.siblings.first.shared_versions.include?(v) - assert p.root.siblings.first.shared_versions.include?(v) - end - - def test_shared_versions - parent = Project.find(1) - child = parent.children.find(3) - private_child = parent.children.find(5) - - assert_equal [1,2,3], parent.version_ids.sort - assert_equal [4], child.version_ids - assert_equal [6], private_child.version_ids - assert_equal [7], Version.find_all_by_sharing('system').collect(&:id) - - assert_equal 6, parent.shared_versions.size - parent.shared_versions.each do |version| - assert_kind_of Version, version - end - - assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort - end - - def test_shared_versions_should_ignore_archived_subprojects - parent = Project.find(1) - child = parent.children.find(3) - child.archive - parent.reload - - assert_equal [1,2,3], parent.version_ids.sort - assert_equal [4], child.version_ids - assert !parent.shared_versions.collect(&:id).include?(4) - end - - def test_shared_versions_visible_to_user - user = User.find(3) - parent = Project.find(1) - child = parent.children.find(5) - - assert_equal [1,2,3], parent.version_ids.sort - assert_equal [6], child.version_ids - - versions = parent.shared_versions.visible(user) - - assert_equal 4, versions.size - versions.each do |version| - assert_kind_of Version, version - end - - assert !versions.collect(&:id).include?(6) - end - - def test_next_identifier - ProjectCustomField.delete_all - Project.create!(:name => 'last', :identifier => 'p2008040') - assert_equal 'p2008041', Project.next_identifier - end - - def test_next_identifier_first_project - Project.delete_all - assert_nil Project.next_identifier - end - - def test_enabled_module_names - with_settings :default_projects_modules => ['issue_tracking', 'repository'] do - project = Project.new - - project.enabled_module_names = %w(issue_tracking news) - assert_equal %w(issue_tracking news), project.enabled_module_names.sort - end - end - - context "enabled_modules" do - setup do - @project = Project.find(1) - end - - should "define module by names and preserve ids" do - # Remove one module - modules = @project.enabled_modules.slice(0..-2) - assert modules.any? - assert_difference 'EnabledModule.count', -1 do - @project.enabled_module_names = modules.collect(&:name) - end - @project.reload - # Ids should be preserved - assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort - end - - should "enable a module" do - @project.enabled_module_names = [] - @project.reload - assert_equal [], @project.enabled_module_names - #with string - @project.enable_module!("issue_tracking") - assert_equal ["issue_tracking"], @project.enabled_module_names - #with symbol - @project.enable_module!(:gantt) - assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names - #don't add a module twice - @project.enable_module!("issue_tracking") - assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names - end - - should "disable a module" do - #with string - assert @project.enabled_module_names.include?("issue_tracking") - @project.disable_module!("issue_tracking") - assert ! @project.reload.enabled_module_names.include?("issue_tracking") - #with symbol - assert @project.enabled_module_names.include?("gantt") - @project.disable_module!(:gantt) - assert ! @project.reload.enabled_module_names.include?("gantt") - #with EnabledModule object - first_module = @project.enabled_modules.first - @project.disable_module!(first_module) - assert ! @project.reload.enabled_module_names.include?(first_module.name) - end - end - - def test_enabled_module_names_should_not_recreate_enabled_modules - project = Project.find(1) - # Remove one module - modules = project.enabled_modules.slice(0..-2) - assert modules.any? - assert_difference 'EnabledModule.count', -1 do - project.enabled_module_names = modules.collect(&:name) - end - project.reload - # Ids should be preserved - assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort - end - - def test_copy_from_existing_project - source_project = Project.find(1) - copied_project = Project.copy_from(1) - - assert copied_project - # Cleared attributes - assert copied_project.id.blank? - assert copied_project.name.blank? - assert copied_project.identifier.blank? - - # Duplicated attributes - assert_equal source_project.description, copied_project.description - assert_equal source_project.enabled_modules, copied_project.enabled_modules - assert_equal source_project.trackers, copied_project.trackers - - # Default attributes - assert_equal 1, copied_project.status - end - - def test_activities_should_use_the_system_activities - project = Project.find(1) - assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} ) - end - - - def test_activities_should_use_the_project_specific_activities - project = Project.find(1) - overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project}) - assert overridden_activity.save! - - assert project.activities.include?(overridden_activity), "Project specific Activity not found" - end - - def test_activities_should_not_include_the_inactive_project_specific_activities - project = Project.find(1) - overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false}) - assert overridden_activity.save! - - assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found" - end - - def test_activities_should_not_include_project_specific_activities_from_other_projects - project = Project.find(1) - overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)}) - assert overridden_activity.save! - - assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project" - end - - def test_activities_should_handle_nils - overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)}) - TimeEntryActivity.delete_all - - # No activities - project = Project.find(1) - assert project.activities.empty? - - # No system, one overridden - assert overridden_activity.save! - project.reload - assert_equal [overridden_activity], project.activities - end - - def test_activities_should_override_system_activities_with_project_activities - project = Project.find(1) - parent_activity = TimeEntryActivity.find(:first) - overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity}) - assert overridden_activity.save! - - assert project.activities.include?(overridden_activity), "Project specific Activity not found" - assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden" - end - - def test_activities_should_include_inactive_activities_if_specified - project = Project.find(1) - overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false}) - assert overridden_activity.save! - - assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found" - end - - test 'activities should not include active System activities if the project has an override that is inactive' do - project = Project.find(1) - system_activity = TimeEntryActivity.find_by_name('Design') - assert system_activity.active? - overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false) - assert overridden_activity.save! - - assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found" - assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override" - end - - def test_close_completed_versions - Version.update_all("status = 'open'") - project = Project.find(1) - assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'} - assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'} - project.close_completed_versions - project.reload - assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'} - assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'} - end - - context "Project#copy" do - setup do - ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests - Project.destroy_all :identifier => "copy-test" - @source_project = Project.find(2) - @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test') - @project.trackers = @source_project.trackers - @project.enabled_module_names = @source_project.enabled_modules.collect(&:name) - end - - should "copy issues" do - @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'), - :subject => "copy issue status", - :tracker_id => 1, - :assigned_to_id => 2, - :project_id => @source_project.id) - assert @project.valid? - assert @project.issues.empty? - assert @project.copy(@source_project) - - assert_equal @source_project.issues.size, @project.issues.size - @project.issues.each do |issue| - assert issue.valid? - assert ! issue.assigned_to.blank? - assert_equal @project, issue.project - end - - copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"}) - assert copied_issue - assert copied_issue.status - assert_equal "Closed", copied_issue.status.name - end - - should "change the new issues to use the copied version" do - User.current = User.find(1) - assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open') - @source_project.versions << assigned_version - assert_equal 3, @source_project.versions.size - Issue.generate_for_project!(@source_project, - :fixed_version_id => assigned_version.id, - :subject => "change the new issues to use the copied version", - :tracker_id => 1, - :project_id => @source_project.id) - - assert @project.copy(@source_project) - @project.reload - copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"}) - - assert copied_issue - assert copied_issue.fixed_version - assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name - assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record - end - - should "copy issue relations" do - Setting.cross_project_issue_relations = '1' - - second_issue = Issue.generate!(:status_id => 5, - :subject => "copy issue relation", - :tracker_id => 1, - :assigned_to_id => 2, - :project_id => @source_project.id) - source_relation = IssueRelation.generate!(:issue_from => Issue.find(4), - :issue_to => second_issue, - :relation_type => "relates") - source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1), - :issue_to => second_issue, - :relation_type => "duplicates") - - assert @project.copy(@source_project) - assert_equal @source_project.issues.count, @project.issues.count - copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4 - copied_second_issue = @project.issues.find_by_subject("copy issue relation") - - # First issue with a relation on project - assert_equal 1, copied_issue.relations.size, "Relation not copied" - copied_relation = copied_issue.relations.first - assert_equal "relates", copied_relation.relation_type - assert_equal copied_second_issue.id, copied_relation.issue_to_id - assert_not_equal source_relation.id, copied_relation.id - - # Second issue with a cross project relation - assert_equal 2, copied_second_issue.relations.size, "Relation not copied" - copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first - assert_equal "duplicates", copied_relation.relation_type - assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept" - assert_not_equal source_relation_cross_project.id, copied_relation.id - end - - should "copy memberships" do - assert @project.valid? - assert @project.members.empty? - assert @project.copy(@source_project) - - assert_equal @source_project.memberships.size, @project.memberships.size - @project.memberships.each do |membership| - assert membership - assert_equal @project, membership.project - end - end - - should "copy memberships with groups and additional roles" do - group = Group.create!(:lastname => "Copy group") - user = User.find(7) - group.users << user - # group role - Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2]) - member = Member.find_by_user_id_and_project_id(user.id, @source_project.id) - # additional role - member.role_ids = [1] - - assert @project.copy(@source_project) - member = Member.find_by_user_id_and_project_id(user.id, @project.id) - assert_not_nil member - assert_equal [1, 2], member.role_ids.sort - end - - should "copy project specific queries" do - assert @project.valid? - assert @project.queries.empty? - assert @project.copy(@source_project) - - assert_equal @source_project.queries.size, @project.queries.size - @project.queries.each do |query| - assert query - assert_equal @project, query.project - end - assert_equal @source_project.queries.map(&:user_id).sort, @project.queries.map(&:user_id).sort - end - - should "copy versions" do - @source_project.versions << Version.generate! - @source_project.versions << Version.generate! - - assert @project.versions.empty? - assert @project.copy(@source_project) - - assert_equal @source_project.versions.size, @project.versions.size - @project.versions.each do |version| - assert version - assert_equal @project, version.project - end - end - - should "copy wiki" do - assert_difference 'Wiki.count' do - assert @project.copy(@source_project) - end - - assert @project.wiki - assert_not_equal @source_project.wiki, @project.wiki - assert_equal "Start page", @project.wiki.start_page - end - - should "copy wiki pages and content with hierarchy" do - assert_difference 'WikiPage.count', @source_project.wiki.pages.size do - assert @project.copy(@source_project) - end - - assert @project.wiki - assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size - - @project.wiki.pages.each do |wiki_page| - assert wiki_page.content - assert !@source_project.wiki.pages.include?(wiki_page) - end - - parent = @project.wiki.find_page('Parent_page') - child1 = @project.wiki.find_page('Child_page_1') - child2 = @project.wiki.find_page('Child_page_2') - assert_equal parent, child1.parent - assert_equal parent, child2.parent - end - - should "copy issue categories" do - assert @project.copy(@source_project) - - assert_equal 2, @project.issue_categories.size - @project.issue_categories.each do |issue_category| - assert !@source_project.issue_categories.include?(issue_category) - end - end - - should "copy boards" do - assert @project.copy(@source_project) - - assert_equal 1, @project.boards.size - @project.boards.each do |board| - assert !@source_project.boards.include?(board) - end - end - - should "change the new issues to use the copied issue categories" do - issue = Issue.find(4) - issue.update_attribute(:category_id, 3) - - assert @project.copy(@source_project) - - @project.issues.each do |issue| - assert issue.category - assert_equal "Stock management", issue.category.name # Same name - assert_not_equal IssueCategory.find(3), issue.category # Different record - end - end - - should "limit copy with :only option" do - assert @project.members.empty? - assert @project.issue_categories.empty? - assert @source_project.issues.any? - - assert @project.copy(@source_project, :only => ['members', 'issue_categories']) - - assert @project.members.any? - assert @project.issue_categories.any? - assert @project.issues.empty? - end - - end - - context "#start_date" do - setup do - ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests - @project = Project.generate!(:identifier => 'test0') - @project.trackers << Tracker.generate! - end - - should "be nil if there are no issues on the project" do - assert_nil @project.start_date - end - - should "be tested when issues have no start date" - - should "be the earliest start date of it's issues" do - early = 7.days.ago.to_date - Issue.generate_for_project!(@project, :start_date => Date.today) - Issue.generate_for_project!(@project, :start_date => early) - - assert_equal early, @project.start_date - end - - end - - context "#due_date" do - setup do - ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests - @project = Project.generate!(:identifier => 'test0') - @project.trackers << Tracker.generate! - end - - should "be nil if there are no issues on the project" do - assert_nil @project.due_date - end - - should "be tested when issues have no due date" - - should "be the latest due date of it's issues" do - future = 7.days.from_now.to_date - Issue.generate_for_project!(@project, :due_date => future) - Issue.generate_for_project!(@project, :due_date => Date.today) - - assert_equal future, @project.due_date - end - - should "be the latest due date of it's versions" do - future = 7.days.from_now.to_date - @project.versions << Version.generate!(:effective_date => future) - @project.versions << Version.generate!(:effective_date => Date.today) - - - assert_equal future, @project.due_date - - end - - should "pick the latest date from it's issues and versions" do - future = 7.days.from_now.to_date - far_future = 14.days.from_now.to_date - Issue.generate_for_project!(@project, :due_date => far_future) - @project.versions << Version.generate!(:effective_date => future) - - assert_equal far_future, @project.due_date - end - - end - - context "Project#completed_percent" do - setup do - ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests - @project = Project.generate!(:identifier => 'test0') - @project.trackers << Tracker.generate! - end - - context "no versions" do - should "be 100" do - assert_equal 100, @project.completed_percent - end - end - - context "with versions" do - should "return 0 if the versions have no issues" do - Version.generate!(:project => @project) - Version.generate!(:project => @project) - - assert_equal 0, @project.completed_percent - end - - should "return 100 if the version has only closed issues" do - v1 = Version.generate!(:project => @project) - Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1) - v2 = Version.generate!(:project => @project) - Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2) - - assert_equal 100, @project.completed_percent - end - - should "return the averaged completed percent of the versions (not weighted)" do - v1 = Version.generate!(:project => @project) - Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1) - v2 = Version.generate!(:project => @project) - Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2) - - assert_equal 50, @project.completed_percent - end - - end - end - - context "#notified_users" do - setup do - @project = Project.generate! - @role = Role.generate! - - @user_with_membership_notification = User.generate!(:mail_notification => 'selected') - Member.generate!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true) - - @all_events_user = User.generate!(:mail_notification => 'all') - Member.generate!(:project => @project, :roles => [@role], :principal => @all_events_user) - - @no_events_user = User.generate!(:mail_notification => 'none') - Member.generate!(:project => @project, :roles => [@role], :principal => @no_events_user) - - @only_my_events_user = User.generate!(:mail_notification => 'only_my_events') - Member.generate!(:project => @project, :roles => [@role], :principal => @only_my_events_user) - - @only_assigned_user = User.generate!(:mail_notification => 'only_assigned') - Member.generate!(:project => @project, :roles => [@role], :principal => @only_assigned_user) - - @only_owned_user = User.generate!(:mail_notification => 'only_owner') - Member.generate!(:project => @project, :roles => [@role], :principal => @only_owned_user) - end - - should "include members with a mail notification" do - assert @project.notified_users.include?(@user_with_membership_notification) - end - - should "include users with the 'all' notification option" do - assert @project.notified_users.include?(@all_events_user) - end - - should "not include users with the 'none' notification option" do - assert !@project.notified_users.include?(@no_events_user) - end - - should "not include users with the 'only_my_events' notification option" do - assert !@project.notified_users.include?(@only_my_events_user) - end - - should "not include users with the 'only_assigned' notification option" do - assert !@project.notified_users.include?(@only_assigned_user) - end - - should "not include users with the 'only_owner' notification option" do - assert !@project.notified_users.include?(@only_owned_user) - end - end - -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/42/422262e4b19f8ed961b6e0fe8bcceca37a286f79.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/42/422262e4b19f8ed961b6e0fe8bcceca37a286f79.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1102 @@ +# Turkish translations for Ruby on Rails +# by Ozgun Ataman (ozataman@gmail.com) +# by Burak Yigit Kaya (ben@byk.im) + +tr: + locale: + native_name: Türkçe + address_separator: " " + direction: ltr + date: + formats: + default: "%d.%m.%Y" + numeric: "%d.%m.%Y" + short: "%e %b" + long: "%e %B %Y, %A" + only_day: "%e" + + day_names: [Pazar, Pazartesi, Salı, Çarşamba, Perşembe, Cuma, Cumartesi] + abbr_day_names: [Pzr, Pzt, Sal, Çrş, Prş, Cum, Cts] + month_names: [~, Ocak, Şubat, Mart, Nisan, Mayıs, Haziran, Temmuz, Ağustos, Eylül, Ekim, Kasım, Aralık] + abbr_month_names: [~, Oca, Şub, Mar, Nis, May, Haz, Tem, Ağu, Eyl, Eki, Kas, Ara] + order: + - :day + - :month + - :year + + time: + formats: + default: "%a %d.%b.%y %H:%M" + numeric: "%d.%b.%y %H:%M" + short: "%e %B, %H:%M" + long: "%e %B %Y, %A, %H:%M" + time: "%H:%M" + + am: "öğleden önce" + pm: "öğleden sonra" + + datetime: + distance_in_words: + half_a_minute: 'yarım dakika' + less_than_x_seconds: + zero: '1 saniyeden az' + one: '1 saniyeden az' + other: '%{count} saniyeden az' + x_seconds: + one: '1 saniye' + other: '%{count} saniye' + less_than_x_minutes: + zero: '1 dakikadan az' + one: '1 dakikadan az' + other: '%{count} dakikadan az' + x_minutes: + one: '1 dakika' + other: '%{count} dakika' + about_x_hours: + one: 'yaklaşık 1 saat' + other: 'yaklaşık %{count} saat' + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: '1 gün' + other: '%{count} gün' + about_x_months: + one: 'yaklaşık 1 ay' + other: 'yaklaşık %{count} ay' + x_months: + one: '1 ay' + other: '%{count} ay' + about_x_years: + one: 'yaklaşık 1 yıl' + other: 'yaklaşık %{count} yıl' + over_x_years: + one: '1 yıldan fazla' + other: '%{count} yıldan fazla' + almost_x_years: + one: "neredeyse 1 Yıl" + other: "neredeyse %{count} yıl" + + number: + format: + precision: 2 + separator: ',' + delimiter: '.' + currency: + format: + unit: 'TRY' + format: '%n%u' + separator: ',' + delimiter: '.' + precision: 2 + percentage: + format: + delimiter: '.' + separator: ',' + precision: 2 + precision: + format: + delimiter: '.' + separator: ',' + human: + format: + delimiter: '.' + separator: ',' + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Byte" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + + support: + array: + sentence_connector: "ve" + skip_last_comma: true + + activerecord: + errors: + template: + header: + one: "%{model} girişi kaydedilemedi: 1 hata." + other: "%{model} girişi kadedilemedi: %{count} hata." + body: "Lütfen aşağıdaki hataları düzeltiniz:" + + messages: + inclusion: "kabul edilen bir kelime değil" + exclusion: "kullanılamaz" + invalid: "geçersiz" + confirmation: "teyidi uyuşmamakta" + accepted: "kabul edilmeli" + empty: "doldurulmalı" + blank: "doldurulmalı" + too_long: "çok uzun (en fazla %{count} karakter)" + too_short: "çok kısa (en az %{count} karakter)" + wrong_length: "yanlış uzunlukta (tam olarak %{count} karakter olmalı)" + taken: "hali hazırda kullanılmakta" + not_a_number: "geçerli bir sayı değil" + greater_than: "%{count} sayısından büyük olmalı" + greater_than_or_equal_to: "%{count} sayısına eşit veya büyük olmalı" + equal_to: "tam olarak %{count} olmalı" + less_than: "%{count} sayısından küçük olmalı" + less_than_or_equal_to: "%{count} sayısına eşit veya küçük olmalı" + odd: "tek olmalı" + even: "çift olmalı" + greater_than_start_date: "başlangıç tarihinden büyük olmalı" + not_same_project: "aynı projeye ait değil" + circular_dependency: "Bu ilişki döngüsel bağımlılık meydana getirecektir" + cant_link_an_issue_with_a_descendant: "Bir iş, alt işlerinden birine bağlanamaz" + models: + + actionview_instancetag_blank_option: Lütfen Seçin + + general_text_No: 'Hayır' + general_text_Yes: 'Evet' + general_text_no: 'hayır' + general_text_yes: 'evet' + general_lang_name: 'Türkçe' + general_csv_separator: ',' + general_csv_encoding: ISO-8859-9 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '7' + + notice_account_updated: Hesap başarıyla güncelleştirildi. + notice_account_invalid_creditentials: Geçersiz kullanıcı ya da parola + notice_account_password_updated: Parola başarıyla güncellendi. + notice_account_wrong_password: Yanlış parola + notice_account_register_done: Hesap başarıyla oluşturuldu. Hesabınızı etkinleştirmek için, size gönderilen e-postadaki bağlantıya tıklayın. + notice_account_unknown_email: Tanınmayan kullanıcı. + notice_can_t_change_password: Bu hesap harici bir denetim kaynağı kullanıyor. Parolayı değiştirmek mümkün değil. + notice_account_lost_email_sent: Yeni parola seçme talimatlarını içeren e-postanız gönderildi. + notice_account_activated: Hesabınız etkinleştirildi. Şimdi giriş yapabilirsiniz. + notice_successful_create: Başarıyla oluşturuldu. + notice_successful_update: Başarıyla güncellendi. + notice_successful_delete: Başarıyla silindi. + notice_successful_connection: Bağlantı başarılı. + notice_file_not_found: Erişmek istediğiniz sayfa mevcut değil ya da kaldırılmış. + notice_locking_conflict: Veri başka bir kullanıcı tarafından güncellendi. + notice_not_authorized: Bu sayfaya erişme yetkiniz yok. + notice_email_sent: "E-posta gönderildi %{value}" + notice_email_error: "E-posta gönderilirken bir hata oluştu (%{value})" + notice_feeds_access_key_reseted: RSS erişim anahtarınız sıfırlandı. + notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}." + notice_no_issue_selected: "Seçili iş yok! Lütfen, düzenlemek istediğiniz işleri işaretleyin." + notice_account_pending: "Hesabınız oluşturuldu ve yönetici onayı bekliyor." + notice_default_data_loaded: Varasayılan konfigürasyon başarılıyla yüklendi. + + error_can_t_load_default_data: "Varsayılan konfigürasyon yüklenemedi: %{value}" + error_scm_not_found: "Depoda, giriş ya da değişiklik yok." + error_scm_command_failed: "Depoya erişmeye çalışırken bir hata meydana geldi: %{value}" + error_scm_annotate: "Giriş mevcut değil veya izah edilemedi." + error_issue_not_found_in_project: 'İş bilgisi bulunamadı veya bu projeye ait değil' + + mail_subject_lost_password: "Parolanız %{value}" + mail_body_lost_password: 'Parolanızı değiştirmek için, aşağıdaki bağlantıya tıklayın:' + mail_subject_register: "%{value} hesap aktivasyonu" + mail_body_register: 'Hesabınızı etkinleştirmek için, aşağıdaki bağlantıya tıklayın:' + mail_body_account_information_external: "Hesabınızı %{value} giriş yapmak için kullanabilirsiniz." + mail_body_account_information: Hesap bilgileriniz + mail_subject_account_activation_request: "%{value} hesabı etkinleştirme isteği" + mail_body_account_activation_request: "Yeni bir kullanıcı (%{value}) kaydedildi. Hesap onaylanmayı bekliyor:" + + gui_validation_error: 1 hata + gui_validation_error_plural: "%{count} hata" + + field_name: İsim + field_description: Yorum + field_summary: Özet + field_is_required: Gerekli + field_firstname: Ad + field_lastname: Soyad + field_mail: E-Posta + field_filename: Dosya + field_filesize: Boyut + field_downloads: İndirilenler + field_author: Yazar + field_created_on: Oluşturulma + field_updated_on: Güncellenme + field_field_format: Biçim + field_is_for_all: Tüm projeler için + field_possible_values: Kullanılabilir değerler + field_regexp: Düzenli ifadeler + field_min_length: En az uzunluk + field_max_length: En çok uzunluk + field_value: Değer + field_category: Kategori + field_title: Başlık + field_project: Proje + field_issue: İş + field_status: Durum + field_notes: Notlar + field_is_closed: İş kapatıldı + field_is_default: Varsayılan Değer + field_tracker: İş tipi + field_subject: Konu + field_due_date: Bitiş Tarihi + field_assigned_to: Atanan + field_priority: Öncelik + field_fixed_version: Hedef Sürüm + field_user: Kullanıcı + field_role: Rol + field_homepage: Anasayfa + field_is_public: Genel + field_parent: 'Üst proje: ' + field_is_in_roadmap: Yol haritasında gösterilen işler + field_login: Giriş + field_mail_notification: E-posta uyarıları + field_admin: Yönetici + field_last_login_on: Son Bağlantı + field_language: Dil + field_effective_date: Tarih + field_password: Parola + field_new_password: Yeni Parola + field_password_confirmation: Onay + field_version: Sürüm + field_type: Tip + field_host: Host + field_port: Port + field_account: Hesap + field_base_dn: Base DN + field_attr_login: Giriş Niteliği + field_attr_firstname: Ad Niteliği + field_attr_lastname: Soyad Niteliği + field_attr_mail: E-Posta Niteliği + field_onthefly: Anında kullanıcı oluşturma + field_start_date: Başlangıç Tarihi + field_done_ratio: Tamamlanma yüzdesi + field_auth_source: Kimlik Denetim Modu + field_hide_mail: E-posta adresimi gizle + field_comments: Yorumlar + field_url: URL + field_start_page: Başlangıç Sayfası + field_subproject: Alt Proje + field_hours: Saat + field_activity: Etkinlik + field_spent_on: Tarih + field_identifier: Tanımlayıcı + field_is_filter: filtre olarak kullanılmış + field_issue_to: İlişkili iş + field_delay: Gecikme + field_assignable: Bu role atanabilecek işler + field_redirect_existing_links: Mevcut bağlantıları yönlendir + field_estimated_hours: Kalan zaman + field_column_names: Sütunlar + field_time_zone: Saat dilimi + field_searchable: Aranabilir + field_default_value: Varsayılan değer + field_comments_sorting: Yorumları göster + + setting_app_title: Uygulama Bağlığı + setting_app_subtitle: Uygulama alt başlığı + setting_welcome_text: Hoşgeldin Mesajı + setting_default_language: Varsayılan Dil + setting_login_required: Kimlik denetimi gerekli mi + setting_self_registration: Otomatik kayıt + setting_attachment_max_size: Maksimum ek boyutu + setting_issues_export_limit: İşlerin dışa aktarılma sınırı + setting_mail_from: Gönderici e-posta adresi + setting_bcc_recipients: Alıcıları birbirinden gizle (bcc) + setting_host_name: Host adı + setting_text_formatting: Metin biçimi + setting_wiki_compression: Wiki geçmişini sıkıştır + setting_feeds_limit: Haber yayını içerik limiti + setting_default_projects_public: Yeni projeler varsayılan olarak herkese açık + setting_autofetch_changesets: Otomatik gönderi al + setting_sys_api_enabled: Depo yönetimi için WS'yi etkinleştir + setting_commit_ref_keywords: Başvuru Kelimeleri + setting_commit_fix_keywords: Sabitleme kelimeleri + setting_autologin: Otomatik Giriş + setting_date_format: Tarih Formati + setting_time_format: Zaman Formatı + setting_cross_project_issue_relations: Çapraz-Proje iş ilişkilendirmesine izin ver + setting_issue_list_default_columns: İş listesinde gösterilen varsayılan sütunlar + setting_emails_footer: E-posta dip not + setting_protocol: Protokol + setting_per_page_options: Sayfada başına öğe sayısı + setting_user_format: Kullanıcı gösterim biçimi + setting_activity_days_default: Proje etkinliklerinde gösterilen gün sayısı + setting_display_subprojects_issues: Varsayılan olarak ana projenin iş listesinde alt proje işlerini göster + + project_module_issue_tracking: İş Takibi + project_module_time_tracking: Zaman Takibi + project_module_news: Haberler + project_module_documents: Belgeler + project_module_files: Dosyalar + project_module_wiki: Wiki + project_module_repository: Depo + project_module_boards: Tartışma Alanı + + label_user: Kullanıcı + label_user_plural: Kullanıcılar + label_user_new: Yeni Kullanıcı + label_project: Proje + label_project_new: Yeni proje + label_project_plural: Projeler + label_x_projects: + zero: hiç proje yok + one: 1 proje + other: "%{count} proje" + label_project_all: Tüm Projeler + label_project_latest: En son projeler + label_issue: İş + label_issue_new: Yeni İş + label_issue_plural: İşler + label_issue_view_all: Tüm işleri izle + label_issues_by: "%{value} tarafından gönderilmiş işler" + label_issue_added: İş eklendi + label_issue_updated: İş güncellendi + label_document: Belge + label_document_new: Yeni belge + label_document_plural: Belgeler + label_document_added: Belge eklendi + label_role: Rol + label_role_plural: Roller + label_role_new: Yeni rol + label_role_and_permissions: Roller ve izinler + label_member: Üye + label_member_new: Yeni üye + label_member_plural: Üyeler + label_tracker: İş tipi + label_tracker_plural: İş tipleri + label_tracker_new: Yeni iş tipi + label_workflow: İş akışı + label_issue_status: İş durumu + label_issue_status_plural: İş durumuları + label_issue_status_new: Yeni durum + label_issue_category: İş kategorisi + label_issue_category_plural: İş kategorileri + label_issue_category_new: Yeni kategori + label_custom_field: Özel alan + label_custom_field_plural: Özel alanlar + label_custom_field_new: Yeni özel alan + label_enumerations: Numaralandırmalar + label_enumeration_new: Yeni değer + label_information: Bilgi + label_information_plural: Bilgi + label_please_login: Lütfen giriş yapın + label_register: Kayıt + label_password_lost: Parolamı unuttum + label_home: Anasayfa + label_my_page: Kişisel Sayfam + label_my_account: Hesabım + label_my_projects: Projelerim + label_administration: Yönetim + label_login: Giriş + label_logout: Çıkış + label_help: Yardım + label_reported_issues: Rapor edilmiş işler + label_assigned_to_me_issues: Bana atanmış işler + label_last_login: Son bağlantı + label_registered_on: Kayıt tarihi + label_activity: Etkinlik + label_overall_activity: Tüm etkinlikler + label_new: Yeni + label_logged_as: "Kullanıcı :" + label_environment: Çevre + label_authentication: Kimlik Denetimi + label_auth_source: Kimlik Denetim Modu + label_auth_source_new: Yeni Denetim Modu + label_auth_source_plural: Denetim Modları + label_subproject_plural: Alt Projeler + label_min_max_length: Min - Maks uzunluk + label_list: Liste + label_date: Tarih + label_integer: Tam sayı + label_float: Ondalıklı sayı + label_boolean: "Evet/Hayır" + label_string: Metin + label_text: Uzun Metin + label_attribute: Nitelik + label_attribute_plural: Nitelikler + label_download: "%{count} indirme" + label_download_plural: "%{count} indirme" + label_no_data: Gösterilecek veri yok + label_change_status: Değişim Durumu + label_history: Geçmiş + label_attachment: Dosya + label_attachment_new: Yeni Dosya + label_attachment_delete: Dosyayı Sil + label_attachment_plural: Dosyalar + label_file_added: Eklenen Dosyalar + label_report: Rapor + label_report_plural: Raporlar + label_news: Haber + label_news_new: Haber ekle + label_news_plural: Haber + label_news_latest: Son Haberler + label_news_view_all: Tüm haberleri oku + label_news_added: Haber eklendi + label_settings: Ayarlar + label_overview: Genel + label_version: Sürüm + label_version_new: Yeni sürüm + label_version_plural: Sürümler + label_confirmation: Doğrulamama + label_export_to: "Diğer uygun kaynaklar:" + label_read: "Oku..." + label_public_projects: Genel Projeler + label_open_issues: açık + label_open_issues_plural: açık + label_closed_issues: kapalı + label_closed_issues_plural: kapalı + label_x_open_issues_abbr_on_total: + zero: tamamı kapalı, toplam %{total} + one: 1'i' açık, toplam %{total} + other: "%{count} açık, toplam %{total}" + label_x_open_issues_abbr: + zero: hiç açık yok + one: 1 açık + other: "%{count} açık" + label_x_closed_issues_abbr: + zero: hiç kapalı yok + one: 1 kapalı + other: "%{count} kapalı" + label_total: Toplam + label_permissions: İzinler + label_current_status: Mevcut Durum + label_new_statuses_allowed: Yeni durumlara izin verildi + label_all: Hepsi + label_none: Hiçbiri + label_nobody: Hiçkimse + label_next: Sonraki + label_previous: Önceki + label_used_by: 'Kullanan: ' + label_details: Ayrıntılar + label_add_note: Not ekle + label_per_page: Sayfa başına + label_calendar: Takvim + label_months_from: ay öncesinden itibaren + label_gantt: İş-Zaman Çizelgesi + label_internal: Dahili + label_last_changes: "Son %{count} değişiklik" + label_change_view_all: Tüm Değişiklikleri gör + label_personalize_page: Bu sayfayı kişiselleştir + label_comment: Yorum + label_comment_plural: Yorumlar + label_x_comments: + zero: hiç yorum yok + one: 1 yorum + other: "%{count} yorum" + label_comment_add: Yorum Ekle + label_comment_added: Yorum Eklendi + label_comment_delete: Yorumları sil + label_query: Özel Sorgu + label_query_plural: Özel Sorgular + label_query_new: Yeni Sorgu + label_filter_add: Filtre ekle + label_filter_plural: Filtreler + label_equals: Eşit + label_not_equals: Eşit değil + label_in_less_than: küçüktür + label_in_more_than: büyüktür + label_in: içinde + label_today: bugün + label_all_time: Tüm Zamanlar + label_yesterday: Dün + label_this_week: Bu hafta + label_last_week: Geçen hafta + label_last_n_days: "Son %{count} gün" + label_this_month: Bu ay + label_last_month: Geçen ay + label_this_year: Bu yıl + label_date_range: Tarih aralığı + label_less_than_ago: günler öncesinden az + label_more_than_ago: günler öncesinden fazla + label_ago: gün önce + label_contains: içeriyor + label_not_contains: içermiyor + label_day_plural: Günler + label_repository: Depo + label_repository_plural: Depolar + label_browse: Gözat + label_modification: "%{count} değişim" + label_modification_plural: "%{count} değişim" + label_revision: Değişiklik + label_revision_plural: Değişiklikler + label_associated_revisions: Birleştirilmiş değişiklikler + label_added: eklendi + label_modified: güncellendi + label_deleted: silindi + label_latest_revision: En son değişiklik + label_latest_revision_plural: En son değişiklikler + label_view_revisions: Değişiklikleri izle + label_max_size: En büyük boyut + label_sort_highest: Üste taşı + label_sort_higher: Yukarı taşı + label_sort_lower: Aşağı taşı + label_sort_lowest: Dibe taşı + label_roadmap: Yol Haritası + label_roadmap_due_in: "%{value} içinde bitmeli" + label_roadmap_overdue: "%{value} geç" + label_roadmap_no_issues: Bu sürüm için iş yok + label_search: Ara + label_result_plural: Sonuçlar + label_all_words: Tüm Kelimeler + label_wiki: Wiki + label_wiki_edit: Wiki düzenleme + label_wiki_edit_plural: Wiki düzenlemeleri + label_wiki_page: Wiki sayfası + label_wiki_page_plural: Wiki sayfaları + label_index_by_title: Başlığa göre diz + label_index_by_date: Tarihe göre diz + label_current_version: Güncel sürüm + label_preview: Önizleme + label_feed_plural: Beslemeler + label_changes_details: Bütün değişikliklerin detayları + label_issue_tracking: İş Takibi + label_spent_time: Harcanan zaman + label_f_hour: "%{value} saat" + label_f_hour_plural: "%{value} saat" + label_time_tracking: Zaman Takibi + label_change_plural: Değişiklikler + label_statistics: İstatistikler + label_commits_per_month: Aylık teslim + label_commits_per_author: Yazar başına teslim + label_view_diff: Farkları izle + label_diff_inline: satır içi + label_diff_side_by_side: Yan yana + label_options: Tercihler + label_copy_workflow_from: İşakışı kopyala + label_permissions_report: İzin raporu + label_watched_issues: İzlenmiş işler + label_related_issues: İlişkili işler + label_applied_status: uygulanmış işler + label_loading: Yükleniyor... + label_relation_new: Yeni ilişki + label_relation_delete: İlişkiyi sil + label_relates_to: ilişkili + label_duplicates: yinelenmiş + label_blocks: Engeller + label_blocked_by: Engelleyen + label_precedes: önce gelir + label_follows: sonra gelir + label_end_to_start: sondan başa + label_end_to_end: sondan sona + label_start_to_start: baştan başa + label_start_to_end: baştan sona + label_stay_logged_in: Sürekli bağlı kal + label_disabled: Devredışı + label_show_completed_versions: Tamamlanmış sürümleri göster + label_me: Ben + label_board: Tartışma Alanı + label_board_new: Yeni alan + label_board_plural: Tartışma alanları + label_topic_plural: Konular + label_message_plural: Mesajlar + label_message_last: Son mesaj + label_message_new: Yeni mesaj + label_message_posted: Mesaj eklendi + label_reply_plural: Cevaplar + label_send_information: Hesap bilgisini kullanıcıya gönder + label_year: Yıl + label_month: Ay + label_week: Hafta + label_date_from: Başlangıç + label_date_to: Bitiş + label_language_based: Kullanıcı dili bazlı + label_sort_by: "%{value} göre sırala" + label_send_test_email: Test e-postası gönder + label_feeds_access_key_created_on: "RSS erişim anahtarı %{value} önce oluşturuldu" + label_module_plural: Modüller + label_added_time_by: "%{author} tarafından %{age} önce eklendi" + label_updated_time: "%{value} önce güncellendi" + label_jump_to_a_project: Projeye git... + label_file_plural: Dosyalar + label_changeset_plural: Değişiklik Listeleri + label_default_columns: Varsayılan Sütunlar + label_no_change_option: (Değişiklik yok) + label_bulk_edit_selected_issues: Seçili işleri toplu olarak düzenle + label_theme: Tema + label_default: Varsayılan + label_search_titles_only: Sadece başlıkları ara + label_user_mail_option_all: "Tüm projelerimdeki herhangi bir olay için" + label_user_mail_option_selected: "Sadece seçili projelerdeki herhangi bir olay için" + label_user_mail_no_self_notified: "Kendi yaptığım değişikliklerden haberdar olmak istemiyorum" + label_registration_activation_by_email: e-posta ile hesap etkinleştirme + label_registration_manual_activation: Elle hesap etkinleştirme + label_registration_automatic_activation: Otomatik hesap etkinleştirme + label_display_per_page: "Sayfa başına: %{value}" + label_age: Yaş + label_change_properties: Özellikleri değiştir + label_general: Genel + label_more: Daha fazla + label_scm: KY + label_plugins: Eklentiler + label_ldap_authentication: LDAP Denetimi + label_downloads_abbr: D/L + label_optional_description: İsteğe bağlı açıklama + label_add_another_file: Bir dosya daha ekle + label_preferences: Tercihler + label_chronological_order: Tarih sırasına göre + label_reverse_chronological_order: Ters tarih sırasına göre + label_planning: Planlanıyor + + button_login: Giriş + button_submit: Gönder + button_save: Kaydet + button_check_all: Hepsini işaretle + button_uncheck_all: Tüm işaretleri kaldır + button_delete: Sil + button_create: Oluştur + button_test: Sına + button_edit: Düzenle + button_add: Ekle + button_change: Değiştir + button_apply: Uygula + button_clear: Temizle + button_lock: Kilitle + button_unlock: Kilidi aç + button_download: İndir + button_list: Listele + button_view: Bak + button_move: Taşı + button_back: Geri + button_cancel: İptal + button_activate: Etkinleştir + button_sort: Sırala + button_log_time: Zaman kaydı + button_rollback: Bu sürüme geri al + button_watch: İzle + button_unwatch: İzlemeyi iptal et + button_reply: Cevapla + button_archive: Arşivle + button_unarchive: Arşivlemeyi kaldır + button_reset: Sıfırla + button_rename: Yeniden adlandır + button_change_password: Parolayı değiştir + button_copy: Kopyala + button_annotate: Değişiklik geçmişine göre göster + button_update: Güncelle + button_configure: Yapılandır + + status_active: faal + status_registered: kayıtlı + status_locked: kilitli + + text_select_mail_notifications: Gönderilecek e-posta uyarısına göre hareketi seçin. + text_regexp_info: örn. ^[A-Z0-9]+$ + text_min_max_length_info: 0 sınırlama yok demektir + text_project_destroy_confirmation: Bu projeyi ve bağlantılı verileri silmek istediğinizden emin misiniz? + text_subprojects_destroy_warning: "Ayrıca %{value} alt proje silinecek." + text_workflow_edit: İşakışını düzenlemek için bir rol ve iş tipi seçin + text_are_you_sure: Emin misiniz ? + text_tip_issue_begin_day: Bugün başlayan görevler + text_tip_issue_end_day: Bugün sona eren görevler + text_tip_issue_begin_end_day: Bugün başlayan ve sona eren görevler + text_caracters_maximum: "En çok %{count} karakter." + text_caracters_minimum: "En az %{count} karakter uzunluğunda olmalı." + text_length_between: "%{min} ve %{max} karakterleri arasındaki uzunluk." + text_tracker_no_workflow: Bu iş tipi için işakışı tanımlanmamış + text_unallowed_characters: Yasaklı karakterler + text_comma_separated: Çoklu değer girilebilir(Virgül ile ayrılmış). + text_issues_ref_in_commit_messages: Teslim mesajlarındaki işleri çözme ve başvuruda bulunma + text_issue_added: "İş %{id}, %{author} tarafından rapor edildi." + text_issue_updated: "İş %{id}, %{author} tarafından güncellendi." + text_wiki_destroy_confirmation: bu wikiyi ve tüm içeriğini silmek istediğinizden emin misiniz? + text_issue_category_destroy_question: "Bazı işler (%{count}) bu kategoriye atandı. Ne yapmak istersiniz?" + text_issue_category_destroy_assignments: Kategori atamalarını kaldır + text_issue_category_reassign_to: İşleri bu kategoriye tekrar ata + text_user_mail_option: "Seçili olmayan projeler için, sadece dahil olduğunuz ya da izlediğiniz öğeler hakkında uyarılar alacaksınız (örneğin,yazarı veya atandığınız işler)." + text_no_configuration_data: "Roller, iş tipleri, iş durumları ve işakışı henüz yapılandırılmadı.\nVarsayılan yapılandırılmanın yüklenmesi şiddetle tavsiye edilir. Bir kez yüklendiğinde yapılandırmayı değiştirebileceksiniz." + text_load_default_configuration: Varsayılan yapılandırmayı yükle + text_status_changed_by_changeset: "Değişiklik listesi %{value} içinde uygulandı." + text_issues_destroy_confirmation: 'Seçili işleri silmek istediğinizden emin misiniz ?' + text_select_project_modules: 'Bu proje için etkinleştirmek istediğiniz modülleri seçin:' + text_default_administrator_account_changed: Varsayılan yönetici hesabı değişti + text_file_repository_writable: Dosya deposu yazılabilir + text_rmagick_available: RMagick Kullanılabilir (isteğe bağlı) + text_destroy_time_entries_question: Silmek üzere olduğunuz işler üzerine %{hours} saat raporlandı.Ne yapmak istersiniz ? + text_destroy_time_entries: Raporlanmış süreleri sil + text_assign_time_entries_to_project: Raporlanmış süreleri projeye ata + text_reassign_time_entries: 'Raporlanmış süreleri bu işe tekrar ata:' + + default_role_manager: Yönetici + default_role_developer: Geliştirici + default_role_reporter: Raporlayıcı + default_tracker_bug: Hata + default_tracker_feature: Özellik + default_tracker_support: Destek + default_issue_status_new: Yeni + default_issue_status_in_progress: Yapılıyor + default_issue_status_resolved: Çözüldü + default_issue_status_feedback: Geribildirim + default_issue_status_closed: "Kapatıldı" + default_issue_status_rejected: Reddedildi + default_doc_category_user: Kullanıcı Dökümantasyonu + default_doc_category_tech: Teknik Dökümantasyon + default_priority_low: Düşük + default_priority_normal: Normal + default_priority_high: Yüksek + default_priority_urgent: Acil + default_priority_immediate: Derhal + default_activity_design: Tasarım + default_activity_development: Geliştirme + + enumeration_issue_priorities: İş önceliği + enumeration_doc_categories: Belge Kategorileri + enumeration_activities: Faaliyetler (zaman takibi) + button_quote: Alıntı + setting_enabled_scm: KKY Açık + label_incoming_emails: "Gelen e-postalar" + label_generate_key: "Anahtar oluştur" + setting_sequential_project_identifiers: "Sıralı proje tanımlayıcıları oluştur" + field_parent_title: Üst sayfa + text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/configuration.yml and restart the application to enable them." + text_enumeration_category_reassign_to: 'Hepsini şuna çevir:' + label_issue_watchers: Takipçiler + mail_body_reminder: "Size atanmış olan %{count} iş %{days} gün içerisinde bitirilmeli:" + label_duplicated_by: yineleyen + text_enumeration_destroy_question: "Bu nesneye %{count} değer bağlanmış." + text_user_wrote: "%{value} demiş ki:" + setting_mail_handler_api_enabled: Gelen e-postalar için WS'yi aç + label_and_its_subprojects: "%{value} ve alt projeleri" + mail_subject_reminder: "%{count} iş bir kaç güne bitecek" + setting_mail_handler_api_key: API anahtarı + setting_commit_logs_encoding: Gönderim mesajlarının kodlaması (UTF-8 vs.) + general_csv_decimal_separator: '.' + notice_unable_delete_version: Sürüm silinemiyor + label_renamed: yeniden adlandırılmış + label_copied: kopyalanmış + setting_plain_text_mail: sadece düz metin (HTML yok) + permission_view_files: Dosyaları görme + permission_edit_issues: İşleri düzenleme + permission_edit_own_time_entries: Kendi zaman girişlerini düzenleme + permission_manage_public_queries: Herkese açık sorguları yönetme + permission_add_issues: İş ekleme + permission_log_time: Harcanan zamanı kaydetme + permission_view_changesets: Değişimleri görme(SVN, vs.) + permission_view_time_entries: Harcanan zamanı görme + permission_manage_versions: Sürümleri yönetme + permission_manage_wiki: Wiki'yi yönetme + permission_manage_categories: İş kategorilerini yönetme + permission_protect_wiki_pages: Wiki sayfalarını korumaya alma + permission_comment_news: Haberlere yorum yapma + permission_delete_messages: Mesaj silme + permission_select_project_modules: Proje modüllerini seçme + permission_manage_documents: Belgeleri yönetme + permission_edit_wiki_pages: Wiki sayfalarını düzenleme + permission_add_issue_watchers: Takipçi ekleme + permission_view_gantt: İş-Zaman çizelgesi görme + permission_move_issues: İşlerin yerini değiştirme + permission_manage_issue_relations: İşlerin biribiriyle bağlantılarını yönetme + permission_delete_wiki_pages: Wiki sayfalarını silme + permission_manage_boards: Panoları yönetme + permission_delete_wiki_pages_attachments: Ekleri silme + permission_view_wiki_edits: Wiki geçmişini görme + permission_add_messages: Mesaj gönderme + permission_view_messages: Mesajları görme + permission_manage_files: Dosyaları yönetme + permission_edit_issue_notes: Notları düzenleme + permission_manage_news: Haberleri yönetme + permission_view_calendar: Takvimleri görme + permission_manage_members: Üyeleri yönetme + permission_edit_messages: Mesajları düzenleme + permission_delete_issues: İşleri silme + permission_view_issue_watchers: Takipçi listesini görme + permission_manage_repository: Depo yönetimi + permission_commit_access: Gönderme erişimi + permission_browse_repository: Depoya gözatma + permission_view_documents: Belgeleri görme + permission_edit_project: Projeyi düzenleme + permission_add_issue_notes: Not ekleme + permission_save_queries: Sorgu kaydetme + permission_view_wiki_pages: Wiki görme + permission_rename_wiki_pages: Wiki sayfasının adını değiştirme + permission_edit_time_entries: Zaman kayıtlarını düzenleme + permission_edit_own_issue_notes: Kendi notlarını düzenleme + setting_gravatar_enabled: Kullanıcı resimleri için Gravatar kullan + label_example: Örnek + text_repository_usernames_mapping: "Select ou update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped." + permission_edit_own_messages: Kendi mesajlarını düzenleme + permission_delete_own_messages: Kendi mesajlarını silme + label_user_activity: "%{value} kullanıcısının etkinlikleri" + label_updated_time_by: "%{author} tarafından %{age} önce güncellendi" + text_diff_truncated: '... Bu fark tam olarak gösterilemiyor çünkü gösterim için ayarlanmış üst sınırı aşıyor.' + setting_diff_max_lines_displayed: Gösterilebilecek maksimumu fark satırı + text_plugin_assets_writable: Eklenti yardımcı dosya dizini yazılabilir + warning_attachments_not_saved: "%{count} adet dosya kaydedilemedi." + button_create_and_continue: Oluştur ve devam et + text_custom_field_possible_values_info: 'Her değer için bir satır' + label_display: Göster + field_editable: Düzenlenebilir + setting_repository_log_display_limit: Dosya kaydında gösterilecek maksimum değişim sayısı + setting_file_max_size_displayed: Dahili olarak gösterilecek metin dosyaları için maksimum satır sayısı + field_watcher: Takipçi + setting_openid: Kayıt ve giriş için OpenID'ye izin ver + field_identity_url: OpenID URL + label_login_with_open_id_option: veya OpenID kullanın + field_content: İçerik + label_descending: Azalan + label_sort: Sırala + label_ascending: Artan + label_date_from_to: "%{start} - %{end} arası" + label_greater_or_equal: ">=" + label_less_or_equal: <= + text_wiki_page_destroy_question: Bu sayfanın %{descendants} adet alt sayfası var. Ne yapmak istersiniz? + text_wiki_page_reassign_children: Alt sayfaları bu sayfanın altına bağla + text_wiki_page_nullify_children: Alt sayfaları ana sayfa olarak sakla + text_wiki_page_destroy_children: Alt sayfaları ve onların alt sayfalarını tamamen sil + setting_password_min_length: Minimum parola uzunluğu + field_group_by: Sonuçları grupla + mail_subject_wiki_content_updated: "'%{id}' wiki sayfası güncellendi" + label_wiki_content_added: Wiki sayfası eklendi + mail_subject_wiki_content_added: "'%{id}' wiki sayfası eklendi" + mail_body_wiki_content_added: "'%{id}' wiki sayfası, %{author} tarafından eklendi." + label_wiki_content_updated: Wiki sayfası güncellendi + mail_body_wiki_content_updated: "'%{id}' wiki sayfası, %{author} tarafından güncellendi." + permission_add_project: Proje oluştur + setting_new_project_user_role_id: Yönetici olmayan ancak proje yaratabilen kullanıcıya verilen rol + label_view_all_revisions: Tüm değişiklikleri gör + label_tag: Etiket + label_branch: Kol + error_no_tracker_in_project: Bu projeye bağlanmış bir iş tipi yok. Lütfen proje ayarlarını kontrol edin. + error_no_default_issue_status: Varsayılan iş durumu tanımlanmamış. Lütfen ayarlarınızı kontrol edin ("Yönetim -> İş durumları" sayfasına gidin). + label_group_plural: Gruplar + label_group: Grup + label_group_new: Yeni grup + label_time_entry_plural: Harcanan zaman + text_journal_changed: "%{label}: %{old} -> %{new}" + text_journal_set_to: "%{label} %{value} yapıldı" + text_journal_deleted: "%{label} silindi (%{old})" + text_journal_added: "%{label} %{value} eklendi" + field_active: Etkin + enumeration_system_activity: Sistem Etkinlikleri + permission_delete_issue_watchers: İzleyicileri sil + version_status_closed: kapalı + version_status_locked: kilitli + version_status_open: açık + error_can_not_reopen_issue_on_closed_version: Kapatılmış bir sürüme ait işler tekrar açılamaz + label_user_anonymous: Anonim + button_move_and_follow: Yerini değiştir ve takip et + setting_default_projects_modules: Yeni projeler için varsayılan modüller + setting_gravatar_default: Varsayılan Gravatar resmi + field_sharing: Paylaşım + label_version_sharing_hierarchy: Proje hiyerarşisi ile + label_version_sharing_system: Tüm projeler ile + label_version_sharing_descendants: Alt projeler ile + label_version_sharing_tree: Proje ağacı ile + label_version_sharing_none: Paylaşılmamış + error_can_not_archive_project: Bu proje arşivlenemez + button_duplicate: Yinele + button_copy_and_follow: Kopyala ve takip et + label_copy_source: Kaynak + setting_issue_done_ratio: İş tamamlanma oranını şununla hesapla + setting_issue_done_ratio_issue_status: İş durumunu kullan + error_issue_done_ratios_not_updated: İş tamamlanma oranları güncellenmedi. + error_workflow_copy_target: Lütfen hedef iş tipi ve rolleri seçin + setting_issue_done_ratio_issue_field: İşteki alanı kullan + label_copy_same_as_target: Hedef ile aynı + label_copy_target: Hedef + notice_issue_done_ratios_updated: İş tamamlanma oranları güncellendi. + error_workflow_copy_source: Lütfen kaynak iş tipi ve rolleri seçin + label_update_issue_done_ratios: İş tamamlanma oranlarını güncelle + setting_start_of_week: Takvimleri şundan başlat + permission_view_issues: İşleri Gör + label_display_used_statuses_only: Sadece bu iş tipi tarafından kullanılan durumları göster + label_revision_id: Değişiklik %{value} + label_api_access_key: API erişim anahtarı + label_api_access_key_created_on: API erişim anahtarı %{value} önce oluşturuldu + label_feeds_access_key: RSS erişim anahtarı + notice_api_access_key_reseted: API erişim anahtarınız sıfırlandı. + setting_rest_api_enabled: REST web servisini etkinleştir + label_missing_api_access_key: Bir API erişim anahtarı eksik + label_missing_feeds_access_key: Bir RSS erişim anahtarı eksik + button_show: Göster + text_line_separated: Çoklu değer girilebilir (her satıra bir değer). + setting_mail_handler_body_delimiters: Şu satırların birinden sonra e-postayı sonlandır + permission_add_subprojects: Alt proje yaratma + label_subproject_new: Yeni alt proje + text_own_membership_delete_confirmation: "Projeyi daha sonra düzenleyememenize sebep olacak bazı yetkilerinizi kaldırmak üzeresiniz.\nDevam etmek istediğinize emin misiniz?" + label_close_versions: Tamamlanmış sürümleri kapat + label_board_sticky: Yapışkan + label_board_locked: Kilitli + permission_export_wiki_pages: Wiki sayfalarını dışarı aktar + setting_cache_formatted_text: Biçimlendirilmiş metni önbelleğe al + permission_manage_project_activities: Proje etkinliklerini yönetme + error_unable_delete_issue_status: İş durumu silinemiyor + label_profile: Profil + permission_manage_subtasks: Alt işleri yönetme + field_parent_issue: Üst iş + label_subtask_plural: Alt işler + label_project_copy_notifications: Proje kopyalaması esnasında bilgilendirme e-postaları gönder + error_can_not_delete_custom_field: Özel alan silinemiyor + error_unable_to_connect: Bağlanılamıyor (%{value}) + error_can_not_remove_role: Bu rol kullanımda olduğundan silinemez. + error_can_not_delete_tracker: Bu iş tipi içerisinde iş barındırdığından silinemiyor. + field_principal: Temel + label_my_page_block: Kişisel sayfa bloğum + notice_failed_to_save_members: "Üyeler kaydedilemiyor: %{errors}." + text_zoom_out: Uzaklaş + text_zoom_in: Yakınlaş + notice_unable_delete_time_entry: Zaman kayıt girdisi silinemiyor. + label_overall_spent_time: Toplam harcanan zaman + field_time_entries: Zaman Kayıtları + project_module_gantt: İş-Zaman Çizelgesi + project_module_calendar: Takvim + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Metin alanı + label_user_mail_option_only_owner: Sadece sahibi olduğum şeyler için + setting_default_notification_option: Varsayılan bildirim seçeneği + label_user_mail_option_only_my_events: Sadece takip ettiğim ya da içinde olduğum şeyler için + label_user_mail_option_only_assigned: Sadece bana atanan şeyler için + label_user_mail_option_none: Hiç bir şey için + field_member_of_group: Atananın grubu + field_assigned_to_role: Atananın rolü + notice_not_authorized_archived_project: Erişmeye çalıştığınız proje arşive kaldırılmış. + label_principal_search: "Kullanıcı ya da grup ara:" + label_user_search: "Kullanıcı ara:" + field_visible: Görünür + setting_emails_header: "E-Posta başlığı" + setting_commit_logtime_activity_id: Kaydedilen zaman için etkinlik + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Zaman kaydını etkinleştir + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: İş-Zaman çizelgesinde gösterilecek en fazla öğe sayısı + field_warn_on_leaving_unsaved: Kaydedilmemiş metin bulunan bir sayfadan çıkarken beni uyar + text_warn_on_leaving_unsaved: Bu sayfada terkettiğiniz takdirde kaybolacak kaydedilmemiş metinler var. + label_my_queries: Özel sorgularım + text_journal_changed_no_detail: "%{label} güncellendi" + label_news_comment_added: Bir habere yorum eklendi + button_expand_all: Tümünü genişlet + button_collapse_all: Tümünü daralt + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Seçilen zaman kayıtlarını toplu olarak düzenle + text_time_entries_destroy_confirmation: Seçilen zaman kaydını/kayıtlarını silmek istediğinize emin misiniz? + label_role_anonymous: Anonim + label_role_non_member: Üye Değil + label_issue_note_added: Not eklendi + label_issue_status_updated: Durum güncellendi + label_issue_priority_updated: Öncelik güncellendi + label_issues_visibility_own: Issues created by or assigned to the user + field_issues_visibility: İşlerin görünürlüğü + label_issues_visibility_all: Tüm işler + permission_set_own_issues_private: Set own issues public or private + field_is_private: Özel + permission_set_issues_private: İşleri özel ya da genel olarak işaretleme + label_issues_visibility_public: Özel olmayan tüm işler + text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). + field_commit_logs_encoding: Commit messages encoding + field_scm_path_encoding: Yol kodlaması(encoding) + text_scm_path_encoding_note: "Varsayılan: UTF-8" + field_path_to_repository: Path to repository + field_root_directory: Ana dizin + field_cvs_module: Modül + field_cvsroot: CVSROOT + text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Komut + text_scm_command_version: Sürüm + label_git_report_last_commit: Report last commit for files and directories + text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 İş + one: 1 İş + other: "%{count} İşler" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: Hepsi + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Alt projeler ile + label_cross_project_tree: Proje aÄŸacı ile + label_cross_project_hierarchy: Proje hiyerarÅŸisi ile + label_cross_project_system: Tüm projeler ile + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/42/423f853cb1907a00c1e269d3e1b641a66386a1c1.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/42/423f853cb1907a00c1e269d3e1b641a66386a1c1.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,105 @@ +--- +enabled_modules_001: + name: issue_tracking + project_id: 1 + id: 1 +enabled_modules_002: + name: time_tracking + project_id: 1 + id: 2 +enabled_modules_003: + name: news + project_id: 1 + id: 3 +enabled_modules_004: + name: documents + project_id: 1 + id: 4 +enabled_modules_005: + name: files + project_id: 1 + id: 5 +enabled_modules_006: + name: wiki + project_id: 1 + id: 6 +enabled_modules_007: + name: repository + project_id: 1 + id: 7 +enabled_modules_008: + name: boards + project_id: 1 + id: 8 +enabled_modules_009: + name: repository + project_id: 3 + id: 9 +enabled_modules_010: + name: wiki + project_id: 3 + id: 10 +enabled_modules_011: + name: issue_tracking + project_id: 2 + id: 11 +enabled_modules_012: + name: time_tracking + project_id: 3 + id: 12 +enabled_modules_013: + name: issue_tracking + project_id: 3 + id: 13 +enabled_modules_014: + name: issue_tracking + project_id: 5 + id: 14 +enabled_modules_015: + name: wiki + project_id: 2 + id: 15 +enabled_modules_016: + name: boards + project_id: 2 + id: 16 +enabled_modules_017: + name: calendar + project_id: 1 + id: 17 +enabled_modules_018: + name: gantt + project_id: 1 + id: 18 +enabled_modules_019: + name: calendar + project_id: 2 + id: 19 +enabled_modules_020: + name: gantt + project_id: 2 + id: 20 +enabled_modules_021: + name: calendar + project_id: 3 + id: 21 +enabled_modules_022: + name: gantt + project_id: 3 + id: 22 +enabled_modules_023: + name: calendar + project_id: 5 + id: 23 +enabled_modules_024: + name: gantt + project_id: 5 + id: 24 +enabled_modules_025: + name: news + project_id: 2 + id: 25 +enabled_modules_026: + name: repository + project_id: 2 + id: 26 diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/42/42862198b703389f6caeb94209420fc08fb8d9e9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/42/42862198b703389f6caeb94209420fc08fb8d9e9.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,62 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class DocumentTest < ActiveSupport::TestCase + fixtures :projects, :enumerations, :documents, :attachments, + :enabled_modules, + :users, :members, :member_roles, :roles, + :groups_users + + def test_create + doc = Document.new(:project => Project.find(1), :title => 'New document', :category => Enumeration.find_by_name('User documentation')) + assert doc.save + end + + def test_create_should_send_email_notification + ActionMailer::Base.deliveries.clear + + with_settings :notified_events => %w(document_added) do + doc = Document.new(:project => Project.find(1), :title => 'New document', :category => Enumeration.find_by_name('User documentation')) + assert doc.save + end + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_create_with_default_category + # Sets a default category + e = Enumeration.find_by_name('Technical documentation') + e.update_attributes(:is_default => true) + + doc = Document.new(:project => Project.find(1), :title => 'New document') + assert_equal e, doc.category + assert doc.save + end + + def test_updated_on_with_attachments + d = Document.find(1) + assert d.attachments.any? + assert_equal d.attachments.map(&:created_on).max, d.updated_on + end + + def test_updated_on_without_attachments + d = Document.find(2) + assert d.attachments.empty? + assert_equal d.created_on, d.updated_on + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/43/4336b667481667d3872d0efd1130886aadc2be58.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/43/4336b667481667d3872d0efd1130886aadc2be58.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,116 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/subversion_adapter' + +class Repository::Subversion < Repository + attr_protected :root_url + validates_presence_of :url + validates_format_of :url, :with => /^(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+/i + + def self.scm_adapter_class + Redmine::Scm::Adapters::SubversionAdapter + end + + def self.scm_name + 'Subversion' + end + + def supports_directory_revisions? + true + end + + def repo_log_encoding + 'UTF-8' + end + + def latest_changesets(path, rev, limit=10) + revisions = scm.revisions(path, rev, nil, :limit => limit) + if revisions + identifiers = revisions.collect(&:identifier).compact + changesets.where(:revision => identifiers).reorder("committed_on DESC").includes(:repository, :user).all + else + [] + end + end + + # Returns a path relative to the url of the repository + def relative_path(path) + path.gsub(Regexp.new("^\/?#{Regexp.escape(relative_url)}"), '') + end + + def fetch_changesets + scm_info = scm.info + if scm_info + # latest revision found in database + db_revision = latest_changeset ? latest_changeset.revision.to_i : 0 + # latest revision in the repository + scm_revision = scm_info.lastrev.identifier.to_i + if db_revision < scm_revision + logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug? + identifier_from = db_revision + 1 + while (identifier_from <= scm_revision) + # loads changesets by batches of 200 + identifier_to = [identifier_from + 199, scm_revision].min + revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true) + revisions.reverse_each do |revision| + transaction do + changeset = Changeset.create(:repository => self, + :revision => revision.identifier, + :committer => revision.author, + :committed_on => revision.time, + :comments => revision.message) + + revision.paths.each do |change| + changeset.create_change(change) + end unless changeset.new_record? + end + end unless revisions.nil? + identifier_from = identifier_to + 1 + end + end + end + end + + protected + + def load_entries_changesets(entries) + return unless entries + + entries_with_identifier = entries.select {|entry| entry.lastrev && entry.lastrev.identifier.present?} + identifiers = entries_with_identifier.map {|entry| entry.lastrev.identifier}.compact.uniq + + if identifiers.any? + changesets_by_identifier = changesets.where(:revision => identifiers).includes(:user, :repository).all.group_by(&:revision) + entries_with_identifier.each do |entry| + if m = changesets_by_identifier[entry.lastrev.identifier] + entry.changeset = m.first + end + end + end + end + + private + + # Returns the relative url of the repository + # Eg: root_url = file:///var/svn/foo + # url = file:///var/svn/foo/bar + # => returns /bar + def relative_url + @relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(root_url || scm.root_url)}", Regexp::IGNORECASE), '') + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/43/436fe1a9150c1ebf6103f8d29e99a0c922a7e55f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/43/436fe1a9150c1ebf6103f8d29e99a0c922a7e55f.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1249 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class QueryTest < ActiveSupport::TestCase + include Redmine::I18n + + fixtures :projects, :enabled_modules, :users, :members, + :member_roles, :roles, :trackers, :issue_statuses, + :issue_categories, :enumerations, :issues, + :watchers, :custom_fields, :custom_values, :versions, + :queries, + :projects_trackers, + :custom_fields_trackers + + def test_custom_fields_for_all_projects_should_be_available_in_global_queries + query = Query.new(:project => nil, :name => '_') + assert query.available_filters.has_key?('cf_1') + assert !query.available_filters.has_key?('cf_3') + end + + def test_system_shared_versions_should_be_available_in_global_queries + Version.find(2).update_attribute :sharing, 'system' + query = Query.new(:project => nil, :name => '_') + assert query.available_filters.has_key?('fixed_version_id') + assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'} + end + + def test_project_filter_in_global_queries + query = Query.new(:project => nil, :name => '_') + project_filter = query.available_filters["project_id"] + assert_not_nil project_filter + project_ids = project_filter[:values].map{|p| p[1]} + assert project_ids.include?("1") #public project + assert !project_ids.include?("2") #private project user cannot see + end + + def find_issues_with_query(query) + Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where( + query.statement + ).all + end + + def assert_find_issues_with_query_is_successful(query) + assert_nothing_raised do + find_issues_with_query(query) + end + end + + def assert_query_statement_includes(query, condition) + assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}" + end + + def assert_query_result(expected, query) + assert_nothing_raised do + assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort + assert_equal expected.size, query.issue_count + end + end + + def test_query_should_allow_shared_versions_for_a_project_query + subproject_version = Version.find(4) + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s]) + + assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')") + end + + def test_query_with_multiple_custom_fields + query = Query.find(1) + assert query.valid? + assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')") + issues = find_issues_with_query(query) + assert_equal 1, issues.length + assert_equal Issue.find(3), issues.first + end + + def test_operator_none + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('fixed_version_id', '!*', ['']) + query.add_filter('cf_1', '!*', ['']) + assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL") + assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''") + find_issues_with_query(query) + end + + def test_operator_none_for_integer + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('estimated_hours', '!*', ['']) + issues = find_issues_with_query(query) + assert !issues.empty? + assert issues.all? {|i| !i.estimated_hours} + end + + def test_operator_none_for_date + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('start_date', '!*', ['']) + issues = find_issues_with_query(query) + assert !issues.empty? + assert issues.all? {|i| i.start_date.nil?} + end + + def test_operator_none_for_string_custom_field + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('cf_2', '!*', ['']) + assert query.has_filter?('cf_2') + issues = find_issues_with_query(query) + assert !issues.empty? + assert issues.all? {|i| i.custom_field_value(2).blank?} + end + + def test_operator_all + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('fixed_version_id', '*', ['']) + query.add_filter('cf_1', '*', ['']) + assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL") + assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''") + find_issues_with_query(query) + end + + def test_operator_all_for_date + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('start_date', '*', ['']) + issues = find_issues_with_query(query) + assert !issues.empty? + assert issues.all? {|i| i.start_date.present?} + end + + def test_operator_all_for_string_custom_field + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('cf_2', '*', ['']) + assert query.has_filter?('cf_2') + issues = find_issues_with_query(query) + assert !issues.empty? + assert issues.all? {|i| i.custom_field_value(2).present?} + end + + def test_numeric_filter_should_not_accept_non_numeric_values + query = Query.new(:name => '_') + query.add_filter('estimated_hours', '=', ['a']) + + assert query.has_filter?('estimated_hours') + assert !query.valid? + end + + def test_operator_is_on_float + Issue.update_all("estimated_hours = 171.2", "id=2") + + query = Query.new(:name => '_') + query.add_filter('estimated_hours', '=', ['171.20']) + issues = find_issues_with_query(query) + assert_equal 1, issues.size + assert_equal 2, issues.first.id + end + + def test_operator_is_on_integer_custom_field + f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true) + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7') + CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12') + CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') + + query = Query.new(:name => '_') + query.add_filter("cf_#{f.id}", '=', ['12']) + issues = find_issues_with_query(query) + assert_equal 1, issues.size + assert_equal 2, issues.first.id + end + + def test_operator_is_on_integer_custom_field_should_accept_negative_value + f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true) + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7') + CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12') + CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') + + query = Query.new(:name => '_') + query.add_filter("cf_#{f.id}", '=', ['-12']) + assert query.valid? + issues = find_issues_with_query(query) + assert_equal 1, issues.size + assert_equal 2, issues.first.id + end + + def test_operator_is_on_float_custom_field + f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true) + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3') + CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7') + CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') + + query = Query.new(:name => '_') + query.add_filter("cf_#{f.id}", '=', ['12.7']) + issues = find_issues_with_query(query) + assert_equal 1, issues.size + assert_equal 2, issues.first.id + end + + def test_operator_is_on_float_custom_field_should_accept_negative_value + f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true) + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3') + CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7') + CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') + + query = Query.new(:name => '_') + query.add_filter("cf_#{f.id}", '=', ['-12.7']) + assert query.valid? + issues = find_issues_with_query(query) + assert_equal 1, issues.size + assert_equal 2, issues.first.id + end + + def test_operator_is_on_multi_list_custom_field + f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true, + :possible_values => ['value1', 'value2', 'value3'], :multiple => true) + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1') + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2') + CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1') + + query = Query.new(:name => '_') + query.add_filter("cf_#{f.id}", '=', ['value1']) + issues = find_issues_with_query(query) + assert_equal [1, 3], issues.map(&:id).sort + + query = Query.new(:name => '_') + query.add_filter("cf_#{f.id}", '=', ['value2']) + issues = find_issues_with_query(query) + assert_equal [1], issues.map(&:id).sort + end + + def test_operator_is_not_on_multi_list_custom_field + f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true, + :possible_values => ['value1', 'value2', 'value3'], :multiple => true) + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1') + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2') + CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1') + + query = Query.new(:name => '_') + query.add_filter("cf_#{f.id}", '!', ['value1']) + issues = find_issues_with_query(query) + assert !issues.map(&:id).include?(1) + assert !issues.map(&:id).include?(3) + + query = Query.new(:name => '_') + query.add_filter("cf_#{f.id}", '!', ['value2']) + issues = find_issues_with_query(query) + assert !issues.map(&:id).include?(1) + assert issues.map(&:id).include?(3) + end + + def test_operator_is_on_is_private_field + # is_private filter only available for those who can set issues private + User.current = User.find(2) + + query = Query.new(:name => '_') + assert query.available_filters.key?('is_private') + + query.add_filter("is_private", '=', ['1']) + issues = find_issues_with_query(query) + assert issues.any? + assert_nil issues.detect {|issue| !issue.is_private?} + ensure + User.current = nil + end + + def test_operator_is_not_on_is_private_field + # is_private filter only available for those who can set issues private + User.current = User.find(2) + + query = Query.new(:name => '_') + assert query.available_filters.key?('is_private') + + query.add_filter("is_private", '!', ['1']) + issues = find_issues_with_query(query) + assert issues.any? + assert_nil issues.detect {|issue| issue.is_private?} + ensure + User.current = nil + end + + def test_operator_greater_than + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('done_ratio', '>=', ['40']) + assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0") + find_issues_with_query(query) + end + + def test_operator_greater_than_a_float + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('estimated_hours', '>=', ['40.5']) + assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5") + find_issues_with_query(query) + end + + def test_operator_greater_than_on_int_custom_field + f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true) + CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7') + CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12') + CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '') + + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter("cf_#{f.id}", '>=', ['8']) + issues = find_issues_with_query(query) + assert_equal 1, issues.size + assert_equal 2, issues.first.id + end + + def test_operator_lesser_than + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('done_ratio', '<=', ['30']) + assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0") + find_issues_with_query(query) + end + + def test_operator_lesser_than_on_custom_field + f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true) + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter("cf_#{f.id}", '<=', ['30']) + assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0") + find_issues_with_query(query) + end + + def test_operator_between + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('done_ratio', '><', ['30', '40']) + assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement + find_issues_with_query(query) + end + + def test_operator_between_on_custom_field + f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true) + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter("cf_#{f.id}", '><', ['30', '40']) + assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement + find_issues_with_query(query) + end + + def test_date_filter_should_not_accept_non_date_values + query = Query.new(:name => '_') + query.add_filter('created_on', '=', ['a']) + + assert query.has_filter?('created_on') + assert !query.valid? + end + + def test_date_filter_should_not_accept_invalid_date_values + query = Query.new(:name => '_') + query.add_filter('created_on', '=', ['2011-01-34']) + + assert query.has_filter?('created_on') + assert !query.valid? + end + + def test_relative_date_filter_should_not_accept_non_integer_values + query = Query.new(:name => '_') + query.add_filter('created_on', '>t-', ['a']) + + assert query.has_filter?('created_on') + assert !query.valid? + end + + def test_operator_date_equals + query = Query.new(:name => '_') + query.add_filter('due_date', '=', ['2011-07-10']) + assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement + find_issues_with_query(query) + end + + def test_operator_date_lesser_than + query = Query.new(:name => '_') + query.add_filter('due_date', '<=', ['2011-07-10']) + assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement + find_issues_with_query(query) + end + + def test_operator_date_greater_than + query = Query.new(:name => '_') + query.add_filter('due_date', '>=', ['2011-07-10']) + assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement + find_issues_with_query(query) + end + + def test_operator_date_between + query = Query.new(:name => '_') + query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10']) + assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement + find_issues_with_query(query) + end + + def test_operator_in_more_than + Issue.find(7).update_attribute(:due_date, (Date.today + 15)) + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', '>t+', ['15']) + issues = find_issues_with_query(query) + assert !issues.empty? + issues.each {|issue| assert(issue.due_date >= (Date.today + 15))} + end + + def test_operator_in_less_than + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', ' Project.find(1), :name => '_') + query.add_filter('due_date', '>= Date.today && issue.due_date <= (Date.today + 15))} + end + + def test_operator_less_than_ago + Issue.find(7).update_attribute(:due_date, (Date.today - 3)) + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', '>t-', ['3']) + issues = find_issues_with_query(query) + assert !issues.empty? + issues.each {|issue| assert(issue.due_date >= (Date.today - 3))} + end + + def test_operator_in_the_past_days + Issue.find(7).update_attribute(:due_date, (Date.today - 3)) + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', '>= (Date.today - 3) && issue.due_date <= Date.today)} + end + + def test_operator_more_than_ago + Issue.find(7).update_attribute(:due_date, (Date.today - 10)) + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', ' Project.find(1), :name => '_') + query.add_filter('due_date', 'w', ['']) + assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}" + I18n.locale = :en + end + + def test_range_for_this_week_with_week_starting_on_sunday + I18n.locale = :en + assert_equal '7', I18n.t(:general_first_day_of_week) + + Date.stubs(:today).returns(Date.parse('2011-04-29')) + + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', 'w', ['']) + assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}" + end + + def test_operator_does_not_contains + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('subject', '!~', ['uNable']) + assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'") + find_issues_with_query(query) + end + + def test_filter_assigned_to_me + user = User.find(2) + group = Group.find(10) + User.current = user + i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user) + i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group) + i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11)) + group.users << user + + query = Query.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}}) + result = query.issues + assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id) + + assert result.include?(i1) + assert result.include?(i2) + assert !result.include?(i3) + end + + def test_user_custom_field_filtered_on_me + User.current = User.find(2) + cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1]) + issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1) + issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'}) + + query = Query.new(:name => '_', :project => Project.find(1)) + filter = query.available_filters["cf_#{cf.id}"] + assert_not_nil filter + assert_include 'me', filter[:values].map{|v| v[1]} + + query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}} + result = query.issues + assert_equal 1, result.size + assert_equal issue1, result.first + end + + def test_filter_my_projects + User.current = User.find(2) + query = Query.new(:name => '_') + filter = query.available_filters['project_id'] + assert_not_nil filter + assert_include 'mine', filter[:values].map{|v| v[1]} + + query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}} + result = query.issues + assert_nil result.detect {|issue| !User.current.member_of?(issue.project)} + end + + def test_filter_watched_issues + User.current = User.find(1) + query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}}) + result = find_issues_with_query(query) + assert_not_nil result + assert !result.empty? + assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id) + User.current = nil + end + + def test_filter_unwatched_issues + User.current = User.find(1) + query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}}) + result = find_issues_with_query(query) + assert_not_nil result + assert !result.empty? + assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size) + User.current = nil + end + + def test_filter_on_project_custom_field + field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string') + CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo') + CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo') + + query = Query.new(:name => '_') + filter_name = "project.cf_#{field.id}" + assert_include filter_name, query.available_filters.keys + query.filters = {filter_name => {:operator => '=', :values => ['Foo']}} + assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort + end + + def test_filter_on_author_custom_field + field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string') + CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo') + + query = Query.new(:name => '_') + filter_name = "author.cf_#{field.id}" + assert_include filter_name, query.available_filters.keys + query.filters = {filter_name => {:operator => '=', :values => ['Foo']}} + assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort + end + + def test_filter_on_assigned_to_custom_field + field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string') + CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo') + + query = Query.new(:name => '_') + filter_name = "assigned_to.cf_#{field.id}" + assert_include filter_name, query.available_filters.keys + query.filters = {filter_name => {:operator => '=', :values => ['Foo']}} + assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort + end + + def test_filter_on_fixed_version_custom_field + field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string') + CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo') + + query = Query.new(:name => '_') + filter_name = "fixed_version.cf_#{field.id}" + assert_include filter_name, query.available_filters.keys + query.filters = {filter_name => {:operator => '=', :values => ['Foo']}} + assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort + end + + def test_filter_on_relations_with_a_specific_issue + IssueRelation.delete_all + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2)) + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1)) + + query = Query.new(:name => '_') + query.filters = {"relates" => {:operator => '=', :values => ['1']}} + assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort + + query = Query.new(:name => '_') + query.filters = {"relates" => {:operator => '=', :values => ['2']}} + assert_equal [1], find_issues_with_query(query).map(&:id).sort + end + + def test_filter_on_relations_with_any_issues_in_a_project + IssueRelation.delete_all + with_settings :cross_project_issue_relations => '1' do + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first) + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first) + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first) + end + + query = Query.new(:name => '_') + query.filters = {"relates" => {:operator => '=p', :values => ['2']}} + assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort + + query = Query.new(:name => '_') + query.filters = {"relates" => {:operator => '=p', :values => ['3']}} + assert_equal [1], find_issues_with_query(query).map(&:id).sort + + query = Query.new(:name => '_') + query.filters = {"relates" => {:operator => '=p', :values => ['4']}} + assert_equal [], find_issues_with_query(query).map(&:id).sort + end + + def test_filter_on_relations_with_any_issues_not_in_a_project + IssueRelation.delete_all + with_settings :cross_project_issue_relations => '1' do + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first) + #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first) + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first) + end + + query = Query.new(:name => '_') + query.filters = {"relates" => {:operator => '=!p', :values => ['1']}} + assert_equal [1], find_issues_with_query(query).map(&:id).sort + end + + def test_filter_on_relations_with_no_issues_in_a_project + IssueRelation.delete_all + with_settings :cross_project_issue_relations => '1' do + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first) + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first) + IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3)) + end + + query = Query.new(:name => '_') + query.filters = {"relates" => {:operator => '!p', :values => ['2']}} + ids = find_issues_with_query(query).map(&:id).sort + assert_include 2, ids + assert_not_include 1, ids + assert_not_include 3, ids + end + + def test_filter_on_relations_with_no_issues + IssueRelation.delete_all + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2)) + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1)) + + query = Query.new(:name => '_') + query.filters = {"relates" => {:operator => '!*', :values => ['']}} + ids = find_issues_with_query(query).map(&:id) + assert_equal [], ids & [1, 2, 3] + assert_include 4, ids + end + + def test_filter_on_relations_with_any_issues + IssueRelation.delete_all + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2)) + IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1)) + + query = Query.new(:name => '_') + query.filters = {"relates" => {:operator => '*', :values => ['']}} + assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort + end + + def test_statement_should_be_nil_with_no_filters + q = Query.new(:name => '_') + q.filters = {} + + assert q.valid? + assert_nil q.statement + end + + def test_default_columns + q = Query.new + assert q.columns.any? + assert q.inline_columns.any? + assert q.block_columns.empty? + end + + def test_set_column_names + q = Query.new + q.column_names = ['tracker', :subject, '', 'unknonw_column'] + assert_equal [:tracker, :subject], q.columns.collect {|c| c.name} + c = q.columns.first + assert q.has_column?(c) + end + + def test_inline_and_block_columns + q = Query.new + q.column_names = ['subject', 'description', 'tracker'] + + assert_equal [:subject, :tracker], q.inline_columns.map(&:name) + assert_equal [:description], q.block_columns.map(&:name) + end + + def test_custom_field_columns_should_be_inline + q = Query.new + columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn} + assert columns.any? + assert_nil columns.detect {|column| !column.inline?} + end + + def test_query_should_preload_spent_hours + q = Query.new(:name => '_', :column_names => [:subject, :spent_hours]) + assert q.has_column?(:spent_hours) + issues = q.issues + assert_not_nil issues.first.instance_variable_get("@spent_hours") + end + + def test_groupable_columns_should_include_custom_fields + q = Query.new + column = q.groupable_columns.detect {|c| c.name == :cf_1} + assert_not_nil column + assert_kind_of QueryCustomFieldColumn, column + end + + def test_groupable_columns_should_not_include_multi_custom_fields + field = CustomField.find(1) + field.update_attribute :multiple, true + + q = Query.new + column = q.groupable_columns.detect {|c| c.name == :cf_1} + assert_nil column + end + + def test_groupable_columns_should_include_user_custom_fields + cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user') + + q = Query.new + assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym} + end + + def test_groupable_columns_should_include_version_custom_fields + cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version') + + q = Query.new + assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym} + end + + def test_grouped_with_valid_column + q = Query.new(:group_by => 'status') + assert q.grouped? + assert_not_nil q.group_by_column + assert_equal :status, q.group_by_column.name + assert_not_nil q.group_by_statement + assert_equal 'status', q.group_by_statement + end + + def test_grouped_with_invalid_column + q = Query.new(:group_by => 'foo') + assert !q.grouped? + assert_nil q.group_by_column + assert_nil q.group_by_statement + end + + def test_sortable_columns_should_sort_assignees_according_to_user_format_setting + with_settings :user_format => 'lastname_coma_firstname' do + q = Query.new + assert q.sortable_columns.has_key?('assigned_to') + assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to'] + end + end + + def test_sortable_columns_should_sort_authors_according_to_user_format_setting + with_settings :user_format => 'lastname_coma_firstname' do + q = Query.new + assert q.sortable_columns.has_key?('author') + assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author'] + end + end + + def test_sortable_columns_should_include_custom_field + q = Query.new + assert q.sortable_columns['cf_1'] + end + + def test_sortable_columns_should_not_include_multi_custom_field + field = CustomField.find(1) + field.update_attribute :multiple, true + + q = Query.new + assert !q.sortable_columns['cf_1'] + end + + def test_default_sort + q = Query.new + assert_equal [], q.sort_criteria + end + + def test_set_sort_criteria_with_hash + q = Query.new + q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']} + assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria + end + + def test_set_sort_criteria_with_array + q = Query.new + q.sort_criteria = [['priority', 'desc'], 'tracker'] + assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria + end + + def test_create_query_with_sort + q = Query.new(:name => 'Sorted') + q.sort_criteria = [['priority', 'desc'], 'tracker'] + assert q.save + q.reload + assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria + end + + def test_sort_by_string_custom_field_asc + q = Query.new + c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' } + assert c + assert c.sortable + issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where( + q.statement + ).order("#{c.sortable} ASC").all + values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s} + assert !values.empty? + assert_equal values.sort, values + end + + def test_sort_by_string_custom_field_desc + q = Query.new + c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' } + assert c + assert c.sortable + issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where( + q.statement + ).order("#{c.sortable} DESC").all + values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s} + assert !values.empty? + assert_equal values.sort.reverse, values + end + + def test_sort_by_float_custom_field_asc + q = Query.new + c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' } + assert c + assert c.sortable + issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where( + q.statement + ).order("#{c.sortable} ASC").all + values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact + assert !values.empty? + assert_equal values.sort, values + end + + def test_invalid_query_should_raise_query_statement_invalid_error + q = Query.new + assert_raise Query::StatementInvalid do + q.issues(:conditions => "foo = 1") + end + end + + def test_issue_count + q = Query.new(:name => '_') + issue_count = q.issue_count + assert_equal q.issues.size, issue_count + end + + def test_issue_count_with_archived_issues + p = Project.generate! do |project| + project.status = Project::STATUS_ARCHIVED + end + i = Issue.generate!( :project => p, :tracker => p.trackers.first ) + assert !i.visible? + + test_issue_count + end + + def test_issue_count_by_association_group + q = Query.new(:name => '_', :group_by => 'assigned_to') + count_by_group = q.issue_count_by_group + assert_kind_of Hash, count_by_group + assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort + assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq + assert count_by_group.has_key?(User.find(3)) + end + + def test_issue_count_by_list_custom_field_group + q = Query.new(:name => '_', :group_by => 'cf_1') + count_by_group = q.issue_count_by_group + assert_kind_of Hash, count_by_group + assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort + assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq + assert count_by_group.has_key?('MySQL') + end + + def test_issue_count_by_date_custom_field_group + q = Query.new(:name => '_', :group_by => 'cf_8') + count_by_group = q.issue_count_by_group + assert_kind_of Hash, count_by_group + assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort + assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq + end + + def test_issue_count_with_nil_group_only + Issue.update_all("assigned_to_id = NULL") + + q = Query.new(:name => '_', :group_by => 'assigned_to') + count_by_group = q.issue_count_by_group + assert_kind_of Hash, count_by_group + assert_equal 1, count_by_group.keys.size + assert_nil count_by_group.keys.first + end + + def test_issue_ids + q = Query.new(:name => '_') + order = "issues.subject, issues.id" + issues = q.issues(:order => order) + assert_equal issues.map(&:id), q.issue_ids(:order => order) + end + + def test_label_for + set_language_if_valid 'en' + q = Query.new + assert_equal 'Assignee', q.label_for('assigned_to_id') + end + + def test_label_for_fr + set_language_if_valid 'fr' + q = Query.new + s = "Assign\xc3\xa9 \xc3\xa0" + s.force_encoding('UTF-8') if s.respond_to?(:force_encoding) + assert_equal s, q.label_for('assigned_to_id') + end + + def test_editable_by + admin = User.find(1) + manager = User.find(2) + developer = User.find(3) + + # Public query on project 1 + q = Query.find(1) + assert q.editable_by?(admin) + assert q.editable_by?(manager) + assert !q.editable_by?(developer) + + # Private query on project 1 + q = Query.find(2) + assert q.editable_by?(admin) + assert !q.editable_by?(manager) + assert q.editable_by?(developer) + + # Private query for all projects + q = Query.find(3) + assert q.editable_by?(admin) + assert !q.editable_by?(manager) + assert q.editable_by?(developer) + + # Public query for all projects + q = Query.find(4) + assert q.editable_by?(admin) + assert !q.editable_by?(manager) + assert !q.editable_by?(developer) + end + + def test_visible_scope + query_ids = Query.visible(User.anonymous).map(&:id) + + assert query_ids.include?(1), 'public query on public project was not visible' + assert query_ids.include?(4), 'public query for all projects was not visible' + assert !query_ids.include?(2), 'private query on public project was visible' + assert !query_ids.include?(3), 'private query for all projects was visible' + assert !query_ids.include?(7), 'public query on private project was visible' + end + + context "#available_filters" do + setup do + @query = Query.new(:name => "_") + end + + should "include users of visible projects in cross-project view" do + users = @query.available_filters["assigned_to_id"] + assert_not_nil users + assert users[:values].map{|u|u[1]}.include?("3") + end + + should "include users of subprojects" do + user1 = User.generate! + user2 = User.generate! + project = Project.find(1) + Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1]) + @query.project = project + + users = @query.available_filters["assigned_to_id"] + assert_not_nil users + assert users[:values].map{|u|u[1]}.include?(user1.id.to_s) + assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s) + end + + should "include visible projects in cross-project view" do + projects = @query.available_filters["project_id"] + assert_not_nil projects + assert projects[:values].map{|u|u[1]}.include?("1") + end + + context "'member_of_group' filter" do + should "be present" do + assert @query.available_filters.keys.include?("member_of_group") + end + + should "be an optional list" do + assert_equal :list_optional, @query.available_filters["member_of_group"][:type] + end + + should "have a list of the groups as values" do + Group.destroy_all # No fixtures + group1 = Group.generate!.reload + group2 = Group.generate!.reload + + expected_group_list = [ + [group1.name, group1.id.to_s], + [group2.name, group2.id.to_s] + ] + assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort + end + + end + + context "'assigned_to_role' filter" do + should "be present" do + assert @query.available_filters.keys.include?("assigned_to_role") + end + + should "be an optional list" do + assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type] + end + + should "have a list of the Roles as values" do + assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1']) + assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2']) + assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3']) + end + + should "not include the built in Roles as values" do + assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4']) + assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5']) + end + + end + + end + + context "#statement" do + context "with 'member_of_group' filter" do + setup do + Group.destroy_all # No fixtures + @user_in_group = User.generate! + @second_user_in_group = User.generate! + @user_in_group2 = User.generate! + @user_not_in_group = User.generate! + + @group = Group.generate!.reload + @group.users << @user_in_group + @group.users << @second_user_in_group + + @group2 = Group.generate!.reload + @group2.users << @user_in_group2 + + end + + should "search assigned to for users in the group" do + @query = Query.new(:name => '_') + @query.add_filter('member_of_group', '=', [@group.id.to_s]) + + assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')" + assert_find_issues_with_query_is_successful @query + end + + should "search not assigned to any group member (none)" do + @query = Query.new(:name => '_') + @query.add_filter('member_of_group', '!*', ['']) + + # Users not in a group + assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')" + assert_find_issues_with_query_is_successful @query + end + + should "search assigned to any group member (all)" do + @query = Query.new(:name => '_') + @query.add_filter('member_of_group', '*', ['']) + + # Only users in a group + assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')" + assert_find_issues_with_query_is_successful @query + end + + should "return an empty set with = empty group" do + @empty_group = Group.generate! + @query = Query.new(:name => '_') + @query.add_filter('member_of_group', '=', [@empty_group.id.to_s]) + + assert_equal [], find_issues_with_query(@query) + end + + should "return issues with ! empty group" do + @empty_group = Group.generate! + @query = Query.new(:name => '_') + @query.add_filter('member_of_group', '!', [@empty_group.id.to_s]) + + assert_find_issues_with_query_is_successful @query + end + end + + context "with 'assigned_to_role' filter" do + setup do + @manager_role = Role.find_by_name('Manager') + @developer_role = Role.find_by_name('Developer') + + @project = Project.generate! + @manager = User.generate! + @developer = User.generate! + @boss = User.generate! + @guest = User.generate! + User.add_to_project(@manager, @project, @manager_role) + User.add_to_project(@developer, @project, @developer_role) + User.add_to_project(@boss, @project, [@manager_role, @developer_role]) + + @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id) + @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id) + @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id) + @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id) + @issue5 = Issue.generate!(:project => @project) + end + + should "search assigned to for users with the Role" do + @query = Query.new(:name => '_', :project => @project) + @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s]) + + assert_query_result [@issue1, @issue3], @query + end + + should "search assigned to for users with the Role on the issue project" do + other_project = Project.generate! + User.add_to_project(@developer, other_project, @manager_role) + + @query = Query.new(:name => '_', :project => @project) + @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s]) + + assert_query_result [@issue1, @issue3], @query + end + + should "return an empty set with empty role" do + @empty_role = Role.generate! + @query = Query.new(:name => '_', :project => @project) + @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s]) + + assert_query_result [], @query + end + + should "search assigned to for users without the Role" do + @query = Query.new(:name => '_', :project => @project) + @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s]) + + assert_query_result [@issue2, @issue4, @issue5], @query + end + + should "search assigned to for users not assigned to any Role (none)" do + @query = Query.new(:name => '_', :project => @project) + @query.add_filter('assigned_to_role', '!*', ['']) + + assert_query_result [@issue4, @issue5], @query + end + + should "search assigned to for users assigned to any Role (all)" do + @query = Query.new(:name => '_', :project => @project) + @query.add_filter('assigned_to_role', '*', ['']) + + assert_query_result [@issue1, @issue2, @issue3], @query + end + + should "return issues with ! empty role" do + @empty_role = Role.generate! + @query = Query.new(:name => '_', :project => @project) + @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s]) + + assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query + end + end + end + +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/43/43ca2f7eab22cf20860813dd19a4f9772ff091c7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/43/43ca2f7eab22cf20860813dd19a4f9772ff091c7.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,23 @@ +/* Hungarian initialisation for the jQuery UI date picker plugin. */ +/* Written by Istvan Karaszi (jquery@spam.raszi.hu). */ +jQuery(function($){ + $.datepicker.regional['hu'] = { + closeText: 'bezár', + prevText: 'vissza', + nextText: 'elÅ‘re', + currentText: 'ma', + monthNames: ['Január', 'Február', 'Március', 'Ãprilis', 'Május', 'Június', + 'Július', 'Augusztus', 'Szeptember', 'Október', 'November', 'December'], + monthNamesShort: ['Jan', 'Feb', 'Már', 'Ãpr', 'Máj', 'Jún', + 'Júl', 'Aug', 'Szep', 'Okt', 'Nov', 'Dec'], + dayNames: ['Vasárnap', 'HétfÅ‘', 'Kedd', 'Szerda', 'Csütörtök', 'Péntek', 'Szombat'], + dayNamesShort: ['Vas', 'Hét', 'Ked', 'Sze', 'Csü', 'Pén', 'Szo'], + dayNamesMin: ['V', 'H', 'K', 'Sze', 'Cs', 'P', 'Szo'], + weekHeader: 'Hét', + dateFormat: 'yy.mm.dd.', + firstDay: 1, + isRTL: false, + showMonthAfterYear: true, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['hu']); +}); diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/44/442d530956f824ed60db31e2a2348cc1a58252b8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/44/442d530956f824ed60db31e2a2348cc1a58252b8.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,81 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +module RedminePmTest + class TestCase < ActiveSupport::TestCase + attr_reader :command, :response, :status, :username, :password + + # Cannot use transactional fixtures here: database + # will be accessed from Redmine.pm with its own connection + self.use_transactional_fixtures = false + + def test_dummy + end + + protected + + def assert_response(expected, msg=nil) + case expected + when :success + assert_equal 0, status, + (msg || "The command failed (exit: #{status}):\n #{command}\nOutput was:\n#{formatted_response}") + when :failure + assert_not_equal 0, status, + (msg || "The command succeed (exit: #{status}):\n #{command}\nOutput was:\n#{formatted_response}") + else + assert_equal expected, status, msg + end + end + + def assert_success(*args) + execute *args + assert_response :success + end + + def assert_failure(*args) + execute *args + assert_response :failure + end + + def with_credentials(username, password) + old_username, old_password = @username, @password + @username, @password = username, password + yield if block_given? + ensure + @username, @password = old_username, old_password + end + + def execute(*args) + @command = args.join(' ') + @status = nil + IO.popen("#{command} 2>&1") do |io| + @response = io.read + end + @status = $?.exitstatus + end + + def formatted_response + "#{'='*40}\n#{response}#{'='*40}" + end + + def random_filename + Redmine::Utils.random_hex(16) + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/44/44b6598d90ff6acd9319545fe1b04d55d8804065.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/44/44b6598d90ff6acd9319545fe1b04d55d8804065.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,12 @@ +

    <%= l(:label_statistics) %>

    + +

    +<%= tag("embed", :width => 800, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :repository_id => @repository.identifier_param, :graph => "commits_per_month")) %> +

    +

    +<%= tag("embed", :width => 800, :height => 400, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :repository_id => @repository.identifier_param, :graph => "commits_per_author")) %> +

    + +

    <%= link_to l(:button_back), :action => 'show', :id => @project %>

    + +<% html_title(l(:label_repository), l(:label_statistics)) -%> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/45/45138bdf05ded76c3f2a90a6c79d88267b6a81a0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/45/45138bdf05ded76c3f2a90a6c79d88267b6a81a0.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,9 @@ +class AddWorkflowsType < ActiveRecord::Migration + def up + add_column :workflows, :type, :string, :limit => 30 + end + + def down + remove_column :workflows, :type + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/45/451613f5e947795f045fad0d3c035a4d108e9d4d.svn-base --- a/.svn/pristine/45/451613f5e947795f045fad0d3c035a4d108e9d4d.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1020 +0,0 @@ -html {overflow-y:scroll;} -body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; } - -h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;} -h1 {margin:0; padding:0; font-size: 24px;} -h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;} -h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;} -h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;} - -/***** Layout *****/ -#wrapper {background: white;} - -#top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;} -#top-menu ul {margin: 0; padding: 0;} -#top-menu li { - float:left; - list-style-type:none; - margin: 0px 0px 0px 0px; - padding: 0px 0px 0px 0px; - white-space:nowrap; -} -#top-menu a {color: #fff; margin-right: 8px; font-weight: bold;} -#top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; } - -#account {float:right;} - -#header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;} -#header a {color:#f8f8f8;} -#header h1 a.ancestor { font-size: 80%; } -#quick-search {float:right;} - -#main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;} -#main-menu ul {margin: 0; padding: 0;} -#main-menu li { - float:left; - list-style-type:none; - margin: 0px 2px 0px 0px; - padding: 0px 0px 0px 0px; - white-space:nowrap; -} -#main-menu li a { - display: block; - color: #fff; - text-decoration: none; - font-weight: bold; - margin: 0; - padding: 4px 10px 4px 10px; -} -#main-menu li a:hover {background:#759FCF; color:#fff;} -#main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;} - -#admin-menu ul {margin: 0; padding: 0;} -#admin-menu li {margin: 0; padding: 0 0 12px 0; list-style-type:none;} - -#admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;} -#admin-menu a.projects { background-image: url(../images/projects.png); } -#admin-menu a.users { background-image: url(../images/user.png); } -#admin-menu a.groups { background-image: url(../images/group.png); } -#admin-menu a.roles { background-image: url(../images/database_key.png); } -#admin-menu a.trackers { background-image: url(../images/ticket.png); } -#admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); } -#admin-menu a.workflows { background-image: url(../images/ticket_go.png); } -#admin-menu a.custom_fields { background-image: url(../images/textfield.png); } -#admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); } -#admin-menu a.settings { background-image: url(../images/changeset.png); } -#admin-menu a.plugins { background-image: url(../images/plugin.png); } -#admin-menu a.info { background-image: url(../images/help.png); } -#admin-menu a.server_authentication { background-image: url(../images/server_key.png); } - -#main {background-color:#EEEEEE;} - -#sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;} -* html #sidebar{ width: 22%; } -#sidebar h3{ font-size: 14px; margin-top:14px; color: #666; } -#sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; } -* html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; } -#sidebar .contextual { margin-right: 1em; } - -#content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; } -* html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;} -html>body #content { min-height: 600px; } -* html body #content { height: 600px; } /* IE */ - -#main.nosidebar #sidebar{ display: none; } -#main.nosidebar #content{ width: auto; border-right: 0; } - -#footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;} - -#login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; } -#login-form table td {padding: 6px;} -#login-form label {font-weight: bold;} -#login-form input#username, #login-form input#password { width: 300px; } - -#modalbg {position:absolute; top:0; left:0; width:100%; height:100%; background:#ccc; z-index:49; opacity:0.5;} -html>body #modalbg {position:fixed;} -div.modal { border-radius:5px; position:absolute; top:25%; background:#fff; border:2px solid #759FCF; z-index:50; padding:0px; padding:8px;} -div.modal h3.title {background:#759FCF; color:#fff; border:0; padding-left:8px; margin:-8px; margin-bottom: 1em; border-top-left-radius:2px;border-top-right-radius:2px;} -div.modal p.buttons {text-align:right; margin-bottom:0;} -html>body div.modal {position:fixed;} - -input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; } - -.clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; } - -/***** Links *****/ -a, a:link, a:visited{ color: #2A5685; text-decoration: none; } -a:hover, a:active{ color: #c61a1a; text-decoration: underline;} -a img{ border: 0; } - -a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; } - -/***** Tables *****/ -table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; } -table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; } -table.list td { vertical-align: top; } -table.list td.id { width: 2%; text-align: center;} -table.list td.checkbox { width: 15px; padding: 2px 0 0 0; } -table.list td.checkbox input {padding:0px;} -table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; } -table.list td.buttons a { padding-right: 0.6em; } -table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; } - -tr.project td.name a { white-space:nowrap; } - -tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;} -tr.project.idnt-1 td.name {padding-left: 0.5em;} -tr.project.idnt-2 td.name {padding-left: 2em;} -tr.project.idnt-3 td.name {padding-left: 3.5em;} -tr.project.idnt-4 td.name {padding-left: 5em;} -tr.project.idnt-5 td.name {padding-left: 6.5em;} -tr.project.idnt-6 td.name {padding-left: 8em;} -tr.project.idnt-7 td.name {padding-left: 9.5em;} -tr.project.idnt-8 td.name {padding-left: 11em;} -tr.project.idnt-9 td.name {padding-left: 12.5em;} - -tr.issue { text-align: center; white-space: nowrap; } -tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text { white-space: normal; } -tr.issue td.subject { text-align: left; } -tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;} - -tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;} -tr.issue.idnt-1 td.subject {padding-left: 0.5em;} -tr.issue.idnt-2 td.subject {padding-left: 2em;} -tr.issue.idnt-3 td.subject {padding-left: 3.5em;} -tr.issue.idnt-4 td.subject {padding-left: 5em;} -tr.issue.idnt-5 td.subject {padding-left: 6.5em;} -tr.issue.idnt-6 td.subject {padding-left: 8em;} -tr.issue.idnt-7 td.subject {padding-left: 9.5em;} -tr.issue.idnt-8 td.subject {padding-left: 11em;} -tr.issue.idnt-9 td.subject {padding-left: 12.5em;} - -tr.entry { border: 1px solid #f8f8f8; } -tr.entry td { white-space: nowrap; } -tr.entry td.filename { width: 30%; } -tr.entry td.filename_no_report { width: 70%; } -tr.entry td.size { text-align: right; font-size: 90%; } -tr.entry td.revision, tr.entry td.author { text-align: center; } -tr.entry td.age { text-align: right; } -tr.entry.file td.filename a { margin-left: 16px; } -tr.entry.file td.filename_no_report a { margin-left: 16px; } - -tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;} -tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);} - -tr.changeset { height: 20px } -tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; } -tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; } -tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;} -tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;} -tr.changeset td.comments_nowrap { width: 45%; white-space:nowrap;} - -table.files tr.file td { text-align: center; } -table.files tr.file td.filename { text-align: left; padding-left: 24px; } -table.files tr.file td.digest { font-size: 80%; } - -table.members td.roles, table.memberships td.roles { width: 45%; } - -tr.message { height: 2.6em; } -tr.message td.subject { padding-left: 20px; } -tr.message td.created_on { white-space: nowrap; } -tr.message td.last_message { font-size: 80%; white-space: nowrap; } -tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; } -tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; } - -tr.version.closed, tr.version.closed a { color: #999; } -tr.version td.name { padding-left: 20px; } -tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; } -tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; } - -tr.user td { width:13%; } -tr.user td.email { width:18%; } -tr.user td { white-space: nowrap; } -tr.user.locked, tr.user.registered { color: #aaa; } -tr.user.locked a, tr.user.registered a { color: #aaa; } - -tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;} - -tr.time-entry { text-align: center; white-space: nowrap; } -tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; } -td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; } -td.hours .hours-dec { font-size: 0.9em; } - -table.plugins td { vertical-align: middle; } -table.plugins td.configure { text-align: right; padding-right: 1em; } -table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; } -table.plugins span.description { display: block; font-size: 0.9em; } -table.plugins span.url { display: block; font-size: 0.9em; } - -table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; } -table.list tbody tr.group span.count { color: #aaa; font-size: 80%; } -tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;} -tr.group:hover a.toggle-all { display:inline;} -a.toggle-all:hover {text-decoration:none;} - -table.list tbody tr:hover { background-color:#ffffdd; } -table.list tbody tr.group:hover { background-color:inherit; } -table td {padding:2px;} -table p {margin:0;} -.odd {background-color:#f6f7f8;} -.even {background-color: #fff;} - -a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; } -a.sort.asc { background-image: url(../images/sort_asc.png); } -a.sort.desc { background-image: url(../images/sort_desc.png); } - -table.attributes { width: 100% } -table.attributes th { vertical-align: top; text-align: left; } -table.attributes td { vertical-align: top; } - -table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; } - -td.center {text-align:center;} - -h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; } - -div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; } -div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; } -div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; } -div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; } - -#watchers ul {margin: 0; padding: 0;} -#watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;} -#watchers select {width: 95%; display: block;} -#watchers a.delete {opacity: 0.4;} -#watchers a.delete:hover {opacity: 1;} -#watchers img.gravatar {vertical-align: middle;margin: 0 4px 2px 0;} - -.highlight { background-color: #FCFD8D;} -.highlight.token-1 { background-color: #faa;} -.highlight.token-2 { background-color: #afa;} -.highlight.token-3 { background-color: #aaf;} - -.box{ -padding:6px; -margin-bottom: 10px; -background-color:#f6f6f6; -color:#505050; -line-height:1.5em; -border: 1px solid #e4e4e4; -} - -div.square { - border: 1px solid #999; - float: left; - margin: .3em .4em 0 .4em; - overflow: hidden; - width: .6em; height: .6em; -} -.contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;} -.contextual input, .contextual select {font-size:0.9em;} -.message .contextual { margin-top: 0; } - -.splitcontentleft{float:left; width:49%;} -.splitcontentright{float:right; width:49%;} -form {display: inline;} -input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;} -fieldset {border: 1px solid #e4e4e4; margin:0;} -legend {color: #484848;} -hr { width: 100%; height: 1px; background: #ccc; border: 0;} -blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;} -blockquote blockquote { margin-left: 0;} -acronym { border-bottom: 1px dotted; cursor: help; } -textarea.wiki-edit { width: 99%; } -li p {margin-top: 0;} -div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;} -p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;} -p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; } -p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; } - -div.issue div.subject div div { padding-left: 16px; } -div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;} -div.issue div.subject>div>p { margin-top: 0.5em; } -div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;} -div.issue span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px; -moz-border-radius: 2px;} - -#issue_tree table.issues, #relations table.issues { border: 0; } -#issue_tree td.checkbox, #relations td.checkbox {display:none;} -#relations td.buttons {padding:0;} - -fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; } -fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; } -fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); } - -fieldset#date-range p { margin: 2px 0 2px 0; } -fieldset#filters table { border-collapse: collapse; } -fieldset#filters table td { padding: 0; vertical-align: middle; } -fieldset#filters tr.filter { height: 2em; } -fieldset#filters td.field { width:200px; } -fieldset#filters td.operator { width:170px; } -fieldset#filters td.values { white-space:nowrap; } -fieldset#filters td.values img { vertical-align: bottom; } -fieldset#filters td.add-filter { text-align: right; vertical-align: top; } -.buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; } - -div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;} -div#issue-changesets div.changeset { padding: 4px;} -div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; } -div#issue-changesets p { margin-top: 0; margin-bottom: 1em;} - -div#activity dl, #search-results { margin-left: 2em; } -div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; } -div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } -div#activity dt.me .time { border-bottom: 1px solid #999; } -div#activity dt .time { color: #777; font-size: 80%; } -div#activity dd .description, #search-results dd .description { font-style: italic; } -div#activity span.project:after, #search-results span.project:after { content: " -"; } -div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; } - -#search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; } - -div#search-results-counts {float:right;} -div#search-results-counts ul { margin-top: 0.5em; } -div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; } - -dt.issue { background-image: url(../images/ticket.png); } -dt.issue-edit { background-image: url(../images/ticket_edit.png); } -dt.issue-closed { background-image: url(../images/ticket_checked.png); } -dt.issue-note { background-image: url(../images/ticket_note.png); } -dt.changeset { background-image: url(../images/changeset.png); } -dt.news { background-image: url(../images/news.png); } -dt.message { background-image: url(../images/message.png); } -dt.reply { background-image: url(../images/comments.png); } -dt.wiki-page { background-image: url(../images/wiki_edit.png); } -dt.attachment { background-image: url(../images/attachment.png); } -dt.document { background-image: url(../images/document.png); } -dt.project { background-image: url(../images/projects.png); } -dt.time-entry { background-image: url(../images/time.png); } - -#search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); } - -div#roadmap .related-issues { margin-bottom: 1em; } -div#roadmap .related-issues td.checkbox { display: none; } -div#roadmap .wiki h1:first-child { display: none; } -div#roadmap .wiki h1 { font-size: 120%; } -div#roadmap .wiki h2 { font-size: 110%; } -body.controller-versions.action-show div#roadmap .related-issues {width:auto;} - -div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; } -div#version-summary fieldset { margin-bottom: 1em; } -div#version-summary .total-hours { text-align: right; } - -table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; } -table#time-report tbody tr { font-style: italic; color: #777; } -table#time-report tbody tr.last-level { font-style: normal; color: #555; } -table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; } -table#time-report .hours-dec { font-size: 0.9em; } - -div.wiki-page .contextual a {opacity: 0.4} -div.wiki-page .contextual a:hover {opacity: 1} - -form .attributes select { width: 60%; } -input#issue_subject { width: 99%; } -select#issue_done_ratio { width: 95px; } - -ul.projects { margin: 0; padding-left: 1em; } -ul.projects.root { margin: 0; padding: 0; } -ul.projects ul.projects { border-left: 3px solid #e0e0e0; } -ul.projects li.root { list-style-type:none; margin-bottom: 1em; } -ul.projects li.child { list-style-type:none; margin-top: 1em;} -ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; } -.my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; } - -#tracker_project_ids ul { margin: 0; padding-left: 1em; } -#tracker_project_ids li { list-style-type:none; } - -ul.properties {padding:0; font-size: 0.9em; color: #777;} -ul.properties li {list-style-type:none;} -ul.properties li span {font-style:italic;} - -.total-hours { font-size: 110%; font-weight: bold; } -.total-hours span.hours-int { font-size: 120%; } - -.autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;} -#user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; } - -#workflow_copy_form select { width: 200px; } - -textarea#custom_field_possible_values {width: 99%} - -.pagination {font-size: 90%} -p.pagination {margin-top:8px;} - -/***** Tabular forms ******/ -.tabular p{ -margin: 0; -padding: 3px 0 3px 0; -padding-left: 180px; /* width of left column containing the label elements */ -height: 1%; -clear:left; -} - -html>body .tabular p {overflow:hidden;} - -.tabular label{ -font-weight: bold; -float: left; -text-align: right; -/* width of left column */ -margin-left: -180px; -/* width of labels. Should be smaller than left column to create some right margin */ -width: 175px; -} - -.tabular label.floating{ -font-weight: normal; -margin-left: 0px; -text-align: left; -width: 270px; -} - -.tabular label.block{ -font-weight: normal; -margin-left: 0px !important; -text-align: left; -float: none; -display: block; -width: auto; -} - -.tabular label.inline{ -float:none; -margin-left: 5px !important; -width: auto; -} - -label.no-css { - font-weight: inherit; - float:none; - text-align:left; - margin-left:0px; - width:auto; -} -input#time_entry_comments { width: 90%;} - -#preview fieldset {margin-top: 1em; background: url(../images/draft.png)} - -.tabular.settings p{ padding-left: 300px; } -.tabular.settings label{ margin-left: -300px; width: 295px; } -.tabular.settings textarea { width: 99%; } - -.settings.enabled_scm table {width:100%} -.settings.enabled_scm td.scm_name{ font-weight: bold; } - -fieldset.settings label { display: block; } -fieldset#notified_events .parent { padding-left: 20px; } - -.required {color: #bb0000;} -.summary {font-style: italic;} - -#attachments_fields input[type=text] {margin-left: 8px; } -#attachments_fields span {display:block; white-space:nowrap;} -#attachments_fields img {vertical-align: middle;} - -div.attachments { margin-top: 12px; } -div.attachments p { margin:4px 0 2px 0; } -div.attachments img { vertical-align: middle; } -div.attachments span.author { font-size: 0.9em; color: #888; } - -p.other-formats { text-align: right; font-size:0.9em; color: #666; } -.other-formats span + span:before { content: "| "; } - -a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; } - -/* Project members tab */ -div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% } -div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% } -div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; } -div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; } -div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; } -div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; } - -table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; } - -input#principal_search, input#user_search {width:100%} - -* html div#tab-content-members fieldset div { height: 450px; } - -/***** Flash & error messages ****/ -#errorExplanation, div.flash, .nodata, .warning { - padding: 4px 4px 4px 30px; - margin-bottom: 12px; - font-size: 1.1em; - border: 2px solid; -} - -div.flash {margin-top: 8px;} - -div.flash.error, #errorExplanation { - background: url(../images/exclamation.png) 8px 50% no-repeat; - background-color: #ffe3e3; - border-color: #dd0000; - color: #880000; -} - -div.flash.notice { - background: url(../images/true.png) 8px 5px no-repeat; - background-color: #dfffdf; - border-color: #9fcf9f; - color: #005f00; -} - -div.flash.warning { - background: url(../images/warning.png) 8px 5px no-repeat; - background-color: #FFEBC1; - border-color: #FDBF3B; - color: #A6750C; - text-align: left; -} - -.nodata, .warning { - text-align: center; - background-color: #FFEBC1; - border-color: #FDBF3B; - color: #A6750C; -} - -span.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;} - -#errorExplanation ul { font-size: 0.9em;} -#errorExplanation h2, #errorExplanation p { display: none; } - -/***** Ajax indicator ******/ -#ajax-indicator { -position: absolute; /* fixed not supported by IE */ -background-color:#eee; -border: 1px solid #bbb; -top:35%; -left:40%; -width:20%; -font-weight:bold; -text-align:center; -padding:0.6em; -z-index:100; -opacity: 0.5; -} - -html>body #ajax-indicator { position: fixed; } - -#ajax-indicator span { -background-position: 0% 40%; -background-repeat: no-repeat; -background-image: url(../images/loading.gif); -padding-left: 26px; -vertical-align: bottom; -} - -/***** Calendar *****/ -table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;} -table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; } -table.cal thead th.week-number {width: auto;} -table.cal tbody tr {height: 100px;} -table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;} -table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;} -table.cal td p.day-num {font-size: 1.1em; text-align:right;} -table.cal td.odd p.day-num {color: #bbb;} -table.cal td.today {background:#ffffdd;} -table.cal td.today p.day-num {font-weight: bold;} -table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;} -table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;} -table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;} -p.cal.legend span {display:block;} - -/***** Tooltips ******/ -.tooltip{position:relative;z-index:24;} -.tooltip:hover{z-index:25;color:#000;} -.tooltip span.tip{display: none; text-align:left;} - -div.tooltip:hover span.tip{ -display:block; -position:absolute; -top:12px; left:24px; width:270px; -border:1px solid #555; -background-color:#fff; -padding: 4px; -font-size: 0.8em; -color:#505050; -} - -/***** Progress bar *****/ -table.progress { - border: 1px solid #D7D7D7; - border-collapse: collapse; - border-spacing: 0pt; - empty-cells: show; - text-align: center; - float:left; - margin: 1px 6px 1px 0px; -} - -table.progress td { height: 0.9em; } -table.progress td.closed { background: #BAE0BA none repeat scroll 0%; } -table.progress td.done { background: #DEF0DE none repeat scroll 0%; } -table.progress td.open { background: #FFF none repeat scroll 0%; } -p.pourcent {font-size: 80%;} -p.progress-info {clear: left; font-style: italic; font-size: 80%;} - -/***** Tabs *****/ -#content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;} -#content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:1em; width: 2000px; border-bottom: 1px solid #bbbbbb;} -#content .tabs ul li { -float:left; -list-style-type:none; -white-space:nowrap; -margin-right:8px; -background:#fff; -position:relative; -margin-bottom:-1px; -} -#content .tabs ul li a{ -display:block; -font-size: 0.9em; -text-decoration:none; -line-height:1.3em; -padding:4px 6px 4px 6px; -border: 1px solid #ccc; -border-bottom: 1px solid #bbbbbb; -background-color: #eeeeee; -color:#777; -font-weight:bold; -} - -#content .tabs ul li a:hover { -background-color: #ffffdd; -text-decoration:none; -} - -#content .tabs ul li a.selected { -background-color: #fff; -border: 1px solid #bbbbbb; -border-bottom: 1px solid #fff; -} - -#content .tabs ul li a.selected:hover { -background-color: #fff; -} - -div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; } - -button.tab-left, button.tab-right { - font-size: 0.9em; - cursor: pointer; - height:24px; - border: 1px solid #ccc; - border-bottom: 1px solid #bbbbbb; - position:absolute; - padding:4px; - width: 20px; - bottom: -1px; -} - -button.tab-left { - right: 20px; - background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%; -} - -button.tab-right { - right: 0; - background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%; -} - -/***** Auto-complete *****/ -div.autocomplete { - position:absolute; - width:400px; - margin:0; - padding:0; -} -div.autocomplete ul { - list-style-type:none; - margin:0; - padding:0; -} -div.autocomplete ul li { - list-style-type:none; - display:block; - margin:-1px 0 0 0; - padding:2px; - cursor:pointer; - font-size: 90%; - border: 1px solid #ccc; - border-left: 1px solid #ccc; - border-right: 1px solid #ccc; - background-color:white; -} -div.autocomplete ul li.selected { background-color: #ffb;} -div.autocomplete ul li span.informal { - font-size: 80%; - color: #aaa; -} - -#parent_issue_candidates ul li {width: 500px;} -#related_issue_candidates ul li {width: 500px;} - -/***** Diff *****/ -.diff_out { background: #fcc; } -.diff_out span { background: #faa; } -.diff_in { background: #cfc; } -.diff_in span { background: #afa; } - -.text-diff { -padding: 1em; -background-color:#f6f6f6; -color:#505050; -border: 1px solid #e4e4e4; -} - -/***** Wiki *****/ -div.wiki table { - border: 1px solid #505050; - border-collapse: collapse; - margin-bottom: 1em; -} - -div.wiki table, div.wiki td, div.wiki th { - border: 1px solid #bbb; - padding: 4px; -} - -div.wiki .external { - background-position: 0% 60%; - background-repeat: no-repeat; - padding-left: 12px; - background-image: url(../images/external.png); -} - -div.wiki a.new { - color: #b73535; -} - -div.wiki ul, div.wiki ol {margin-bottom:1em;} - -div.wiki pre { - margin: 1em 1em 1em 1.6em; - padding: 2px 2px 2px 0; - background-color: #fafafa; - border: 1px solid #dadada; - width:auto; - overflow-x: auto; - overflow-y: hidden; -} - -div.wiki ul.toc { - background-color: #ffffdd; - border: 1px solid #e4e4e4; - padding: 4px; - line-height: 1.2em; - margin-bottom: 12px; - margin-right: 12px; - margin-left: 0; - display: table -} -* html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */ - -div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; } -div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; } -div.wiki ul.toc ul { margin: 0; padding: 0; } -div.wiki ul.toc li { list-style-type:none; margin: 0;} -div.wiki ul.toc li li { margin-left: 1.5em; } -div.wiki ul.toc li li li { font-size: 0.8em; } - -div.wiki ul.toc a { - font-size: 0.9em; - font-weight: normal; - text-decoration: none; - color: #606060; -} -div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;} - -a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; } -a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; } -h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; } - -div.wiki img { vertical-align: middle; } - -/***** My page layout *****/ -.block-receiver { -border:1px dashed #c0c0c0; -margin-bottom: 20px; -padding: 15px 0 15px 0; -} - -.mypage-box { -margin:0 0 20px 0; -color:#505050; -line-height:1.5em; -} - -.handle { -cursor: move; -} - -a.close-icon { -display:block; -margin-top:3px; -overflow:hidden; -width:12px; -height:12px; -background-repeat: no-repeat; -cursor:pointer; -background-image:url('../images/close.png'); -} - -a.close-icon:hover { -background-image:url('../images/close_hl.png'); -} - -/***** Gantt chart *****/ -.gantt_hdr { - position:absolute; - top:0; - height:16px; - border-top: 1px solid #c0c0c0; - border-bottom: 1px solid #c0c0c0; - border-right: 1px solid #c0c0c0; - text-align: center; - overflow: hidden; -} - -.gantt_subjects { font-size: 0.8em; } -.gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; } - -.task { - position: absolute; - height:8px; - font-size:0.8em; - color:#888; - padding:0; - margin:0; - line-height:16px; - white-space:nowrap; -} - -.task.label {width:100%;} -.task.label.project, .task.label.version { font-weight: bold; } - -.task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; } -.task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; } -.task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; } - -.task_todo.parent { background: #888; border: 1px solid #888; height: 3px;} -.task_late.parent, .task_done.parent { height: 3px;} -.task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;} -.task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;} - -.version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} -.version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} -.version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} -.version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; } - -.project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} -.project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} -.project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} -.project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; } - -.version-behind-schedule a, .issue-behind-schedule a {color: #f66914;} -.version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;} - -/***** Icons *****/ -.icon { -background-position: 0% 50%; -background-repeat: no-repeat; -padding-left: 20px; -padding-top: 2px; -padding-bottom: 3px; -} - -.icon-add { background-image: url(../images/add.png); } -.icon-edit { background-image: url(../images/edit.png); } -.icon-copy { background-image: url(../images/copy.png); } -.icon-duplicate { background-image: url(../images/duplicate.png); } -.icon-del { background-image: url(../images/delete.png); } -.icon-move { background-image: url(../images/move.png); } -.icon-save { background-image: url(../images/save.png); } -.icon-cancel { background-image: url(../images/cancel.png); } -.icon-multiple { background-image: url(../images/table_multiple.png); } -.icon-folder { background-image: url(../images/folder.png); } -.open .icon-folder { background-image: url(../images/folder_open.png); } -.icon-package { background-image: url(../images/package.png); } -.icon-user { background-image: url(../images/user.png); } -.icon-projects { background-image: url(../images/projects.png); } -.icon-help { background-image: url(../images/help.png); } -.icon-attachment { background-image: url(../images/attachment.png); } -.icon-history { background-image: url(../images/history.png); } -.icon-time { background-image: url(../images/time.png); } -.icon-time-add { background-image: url(../images/time_add.png); } -.icon-stats { background-image: url(../images/stats.png); } -.icon-warning { background-image: url(../images/warning.png); } -.icon-fav { background-image: url(../images/fav.png); } -.icon-fav-off { background-image: url(../images/fav_off.png); } -.icon-reload { background-image: url(../images/reload.png); } -.icon-lock { background-image: url(../images/locked.png); } -.icon-unlock { background-image: url(../images/unlock.png); } -.icon-checked { background-image: url(../images/true.png); } -.icon-details { background-image: url(../images/zoom_in.png); } -.icon-report { background-image: url(../images/report.png); } -.icon-comment { background-image: url(../images/comment.png); } -.icon-summary { background-image: url(../images/lightning.png); } -.icon-server-authentication { background-image: url(../images/server_key.png); } -.icon-issue { background-image: url(../images/ticket.png); } -.icon-zoom-in { background-image: url(../images/zoom_in.png); } -.icon-zoom-out { background-image: url(../images/zoom_out.png); } -.icon-passwd { background-image: url(../images/textfield_key.png); } - -.icon-file { background-image: url(../images/files/default.png); } -.icon-file.text-plain { background-image: url(../images/files/text.png); } -.icon-file.text-x-c { background-image: url(../images/files/c.png); } -.icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); } -.icon-file.text-x-php { background-image: url(../images/files/php.png); } -.icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); } -.icon-file.text-xml { background-image: url(../images/files/xml.png); } -.icon-file.image-gif { background-image: url(../images/files/image.png); } -.icon-file.image-jpeg { background-image: url(../images/files/image.png); } -.icon-file.image-png { background-image: url(../images/files/image.png); } -.icon-file.image-tiff { background-image: url(../images/files/image.png); } -.icon-file.application-pdf { background-image: url(../images/files/pdf.png); } -.icon-file.application-zip { background-image: url(../images/files/zip.png); } -.icon-file.application-x-gzip { background-image: url(../images/files/zip.png); } - -img.gravatar { - padding: 2px; - border: solid 1px #d5d5d5; - background: #fff; -} - -div.issue img.gravatar { - float: right; - margin: 0 0 0 1em; - padding: 5px; -} - -div.issue table img.gravatar { - height: 14px; - width: 14px; - padding: 2px; - float: left; - margin: 0 0.5em 0 0; -} - -h2 img.gravatar { - padding: 3px; - margin: -2px 4px -4px 0; - vertical-align: top; -} - -h4 img.gravatar { - padding: 3px; - margin: -6px 0 -4px 0; - vertical-align: top; -} - -td.username img.gravatar { - margin: 0 0.5em 0 0; - vertical-align: top; -} - -#activity dt img.gravatar { - float: left; - margin: 0 1em 1em 0; -} - -/* Used on 12px Gravatar img tags without the icon background */ -.icon-gravatar { - float: left; - margin-right: 4px; -} - -#activity dt, -.journal { - clear: left; -} - -.journal-link { - float: right; -} - -h2 img { vertical-align:middle; } - -.hascontextmenu { cursor: context-menu; } - -/***** Media print specific styles *****/ -@media print { - #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; } - #main { background: #fff; } - #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;} - #wiki_add_attachment { display:none; } - .hide-when-print { display: none; } - .autoscroll {overflow-x: visible;} - table.list {margin-top:0.5em;} - table.list th, table.list td {border: 1px solid #aaa;} -} - -/* Accessibility specific styles */ -.hidden-for-sighted { - position:absolute; - left:-10000px; - top:auto; - width:1px; - height:1px; - overflow:hidden; -} diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/45/455894853af15224c178defd4436e0297eee3c97.svn-base --- a/.svn/pristine/45/455894853af15224c178defd4436e0297eee3c97.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,258 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module Redmine - # Class used to parse unified diffs - class UnifiedDiff < Array - attr_reader :diff_type - - def initialize(diff, options={}) - options.assert_valid_keys(:type, :max_lines) - diff = diff.split("\n") if diff.is_a?(String) - @diff_type = options[:type] || 'inline' - lines = 0 - @truncated = false - diff_table = DiffTable.new(@diff_type) - diff.each do |line| - line_encoding = nil - if line.respond_to?(:force_encoding) - line_encoding = line.encoding - # TODO: UTF-16 and Japanese CP932 which is imcompatible with ASCII - # In Japan, diffrence between file path encoding - # and file contents encoding is popular. - line.force_encoding('ASCII-8BIT') - end - unless diff_table.add_line line - line.force_encoding(line_encoding) if line_encoding - self << diff_table if diff_table.length > 0 - diff_table = DiffTable.new(diff_type) - end - lines += 1 - if options[:max_lines] && lines > options[:max_lines] - @truncated = true - break - end - end - self << diff_table unless diff_table.empty? - self - end - - def truncated?; @truncated; end - end - - # Class that represents a file diff - class DiffTable < Array - attr_reader :file_name - - # Initialize with a Diff file and the type of Diff View - # The type view must be inline or sbs (side_by_side) - def initialize(type="inline") - @parsing = false - @added = 0 - @removed = 0 - @type = type - end - - # Function for add a line of this Diff - # Returns false when the diff ends - def add_line(line) - unless @parsing - if line =~ /^(---|\+\+\+) (.*)$/ - @file_name = $2 - elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/ - @line_num_l = $2.to_i - @line_num_r = $5.to_i - @parsing = true - end - else - if line =~ /^[^\+\-\s@\\]/ - @parsing = false - return false - elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/ - @line_num_l = $2.to_i - @line_num_r = $5.to_i - else - parse_line(line, @type) - end - end - return true - end - - def each_line - prev_line_left, prev_line_right = nil, nil - each do |line| - spacing = prev_line_left && prev_line_right && (line.nb_line_left != prev_line_left+1) && (line.nb_line_right != prev_line_right+1) - yield spacing, line - prev_line_left = line.nb_line_left.to_i if line.nb_line_left.to_i > 0 - prev_line_right = line.nb_line_right.to_i if line.nb_line_right.to_i > 0 - end - end - - def inspect - puts '### DIFF TABLE ###' - puts "file : #{file_name}" - self.each do |d| - d.inspect - end - end - - private - - # Escape the HTML for the diff - def escapeHTML(line) - CGI.escapeHTML(line) - end - - def diff_for_added_line - if @type == 'sbs' && @removed > 0 && @added < @removed - self[-(@removed - @added)] - else - diff = Diff.new - self << diff - diff - end - end - - def parse_line(line, type="inline") - if line[0, 1] == "+" - diff = diff_for_added_line - diff.line_right = escapeHTML line[1..-1] - diff.nb_line_right = @line_num_r - diff.type_diff_right = 'diff_in' - @line_num_r += 1 - @added += 1 - true - elsif line[0, 1] == "-" - diff = Diff.new - diff.line_left = escapeHTML line[1..-1] - diff.nb_line_left = @line_num_l - diff.type_diff_left = 'diff_out' - self << diff - @line_num_l += 1 - @removed += 1 - true - else - write_offsets - if line[0, 1] =~ /\s/ - diff = Diff.new - diff.line_right = escapeHTML line[1..-1] - diff.nb_line_right = @line_num_r - diff.line_left = escapeHTML line[1..-1] - diff.nb_line_left = @line_num_l - self << diff - @line_num_l += 1 - @line_num_r += 1 - true - elsif line[0, 1] = "\\" - true - else - false - end - end - end - - def write_offsets - if @added > 0 && @added == @removed - @added.times do |i| - line = self[-(1 + i)] - removed = (@type == 'sbs') ? line : self[-(1 + @added + i)] - offsets = offsets(removed.line_left, line.line_right) - removed.offsets = line.offsets = offsets - end - end - @added = 0 - @removed = 0 - end - - def offsets(line_left, line_right) - if line_left.present? && line_right.present? && line_left != line_right - max = [line_left.size, line_right.size].min - starting = 0 - while starting < max && line_left[starting] == line_right[starting] - starting += 1 - end - ending = -1 - while ending >= -(max - starting) && line_left[ending] == line_right[ending] - ending -= 1 - end - unless starting == 0 && ending == -1 - [starting, ending] - end - end - end - end - - # A line of diff - class Diff - attr_accessor :nb_line_left - attr_accessor :line_left - attr_accessor :nb_line_right - attr_accessor :line_right - attr_accessor :type_diff_right - attr_accessor :type_diff_left - attr_accessor :offsets - - def initialize() - self.nb_line_left = '' - self.nb_line_right = '' - self.line_left = '' - self.line_right = '' - self.type_diff_right = '' - self.type_diff_left = '' - end - - def type_diff - type_diff_right == 'diff_in' ? type_diff_right : type_diff_left - end - - def line - type_diff_right == 'diff_in' ? line_right : line_left - end - - def html_line_left - if offsets - line_left.dup.insert(offsets.first, '').insert(offsets.last, '') - else - line_left - end - end - - def html_line_right - if offsets - line_right.dup.insert(offsets.first, '').insert(offsets.last, '') - else - line_right - end - end - - def html_line - if offsets - line.dup.insert(offsets.first, '').insert(offsets.last, '') - else - line - end - end - - def inspect - puts '### Start Line Diff ###' - puts self.nb_line_left - puts self.line_left - puts self.nb_line_right - puts self.line_right - end - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/45/45d753a9939bdfcb8226948e626d205016fffdcd.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/45/45d753a9939bdfcb8226948e626d205016fffdcd.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,128 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/bazaar_adapter' + +class Repository::Bazaar < Repository + attr_protected :root_url + validates_presence_of :url, :log_encoding + + def self.human_attribute_name(attribute_key_name, *args) + attr_name = attribute_key_name.to_s + if attr_name == "url" + attr_name = "path_to_repository" + end + super(attr_name, *args) + end + + def self.scm_adapter_class + Redmine::Scm::Adapters::BazaarAdapter + end + + def self.scm_name + 'Bazaar' + end + + def entry(path=nil, identifier=nil) + scm.bzr_path_encodig = log_encoding + scm.entry(path, identifier) + end + + def cat(path, identifier=nil) + scm.bzr_path_encodig = log_encoding + scm.cat(path, identifier) + end + + def annotate(path, identifier=nil) + scm.bzr_path_encodig = log_encoding + scm.annotate(path, identifier) + end + + def diff(path, rev, rev_to) + scm.bzr_path_encodig = log_encoding + scm.diff(path, rev, rev_to) + end + + def entries(path=nil, identifier=nil) + scm.bzr_path_encodig = log_encoding + entries = scm.entries(path, identifier) + if entries + entries.each do |e| + next if e.lastrev.revision.blank? + # Set the filesize unless browsing a specific revision + if identifier.nil? && e.is_file? + full_path = File.join(root_url, e.path) + e.size = File.stat(full_path).size if File.file?(full_path) + end + c = Change.find( + :first, + :include => :changeset, + :conditions => [ + "#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?", + e.lastrev.revision, + id + ], + :order => "#{Changeset.table_name}.revision DESC") + if c + e.lastrev.identifier = c.changeset.revision + e.lastrev.name = c.changeset.revision + e.lastrev.author = c.changeset.committer + end + end + end + load_entries_changesets(entries) + entries + end + + def fetch_changesets + scm.bzr_path_encodig = log_encoding + scm_info = scm.info + if scm_info + # latest revision found in database + db_revision = latest_changeset ? latest_changeset.revision.to_i : 0 + # latest revision in the repository + scm_revision = scm_info.lastrev.identifier.to_i + if db_revision < scm_revision + logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug? + identifier_from = db_revision + 1 + while (identifier_from <= scm_revision) + # loads changesets by batches of 200 + identifier_to = [identifier_from + 199, scm_revision].min + revisions = scm.revisions('', identifier_to, identifier_from) + transaction do + revisions.reverse_each do |revision| + changeset = Changeset.create(:repository => self, + :revision => revision.identifier, + :committer => revision.author, + :committed_on => revision.time, + :scmid => revision.scmid, + :comments => revision.message) + + revision.paths.each do |change| + Change.create(:changeset => changeset, + :action => change[:action], + :path => change[:path], + :revision => change[:revision]) + end + end + end unless revisions.nil? + identifier_from = identifier_to + 1 + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/45/45db780cec4e0c72778412b47b7cf78b370991c8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/45/45db780cec4e0c72778412b47b7cf78b370991c8.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,476 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class ChangesetTest < ActiveSupport::TestCase + fixtures :projects, :repositories, + :issues, :issue_statuses, :issue_categories, + :changesets, :changes, + :enumerations, + :custom_fields, :custom_values, + :users, :members, :member_roles, :trackers, + :enabled_modules, :roles + + def test_ref_keywords_any + ActionMailer::Base.deliveries.clear + Setting.commit_fix_status_id = IssueStatus.find( + :first, :conditions => ["is_closed = ?", true]).id + Setting.commit_fix_done_ratio = '90' + Setting.commit_ref_keywords = '*' + Setting.commit_fix_keywords = 'fixes , closes' + + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'New commit (#2). Fixes #1', + :revision => '12345') + assert c.save + assert_equal [1, 2], c.issue_ids.sort + fixed = Issue.find(1) + assert fixed.closed? + assert_equal 90, fixed.done_ratio + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_ref_keywords + Setting.commit_ref_keywords = 'refs' + Setting.commit_fix_keywords = '' + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'Ignores #2. Refs #1', + :revision => '12345') + assert c.save + assert_equal [1], c.issue_ids.sort + end + + def test_ref_keywords_any_only + Setting.commit_ref_keywords = '*' + Setting.commit_fix_keywords = '' + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'Ignores #2. Refs #1', + :revision => '12345') + assert c.save + assert_equal [1, 2], c.issue_ids.sort + end + + def test_ref_keywords_any_with_timelog + Setting.commit_ref_keywords = '*' + Setting.commit_logtime_enabled = '1' + + { + '2' => 2.0, + '2h' => 2.0, + '2hours' => 2.0, + '15m' => 0.25, + '15min' => 0.25, + '3h15' => 3.25, + '3h15m' => 3.25, + '3h15min' => 3.25, + '3:15' => 3.25, + '3.25' => 3.25, + '3.25h' => 3.25, + '3,25' => 3.25, + '3,25h' => 3.25, + }.each do |syntax, expected_hours| + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => 24.hours.ago, + :comments => "Worked on this issue #1 @#{syntax}", + :revision => '520', + :user => User.find(2)) + assert_difference 'TimeEntry.count' do + c.scan_comment_for_issue_ids + end + assert_equal [1], c.issue_ids.sort + + time = TimeEntry.first(:order => 'id desc') + assert_equal 1, time.issue_id + assert_equal 1, time.project_id + assert_equal 2, time.user_id + assert_equal expected_hours, time.hours, + "@#{syntax} should be logged as #{expected_hours} hours but was #{time.hours}" + assert_equal Date.yesterday, time.spent_on + assert time.activity.is_default? + assert time.comments.include?('r520'), + "r520 was expected in time_entry comments: #{time.comments}" + end + end + + def test_ref_keywords_closing_with_timelog + Setting.commit_fix_status_id = IssueStatus.find( + :first, :conditions => ["is_closed = ?", true]).id + Setting.commit_ref_keywords = '*' + Setting.commit_fix_keywords = 'fixes , closes' + Setting.commit_logtime_enabled = '1' + + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'This is a comment. Fixes #1 @4.5, #2 @1', + :user => User.find(2)) + assert_difference 'TimeEntry.count', 2 do + c.scan_comment_for_issue_ids + end + + assert_equal [1, 2], c.issue_ids.sort + assert Issue.find(1).closed? + assert Issue.find(2).closed? + + times = TimeEntry.all(:order => 'id desc', :limit => 2) + assert_equal [1, 2], times.collect(&:issue_id).sort + end + + def test_ref_keywords_any_line_start + Setting.commit_ref_keywords = '*' + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => '#1 is the reason of this commit', + :revision => '12345') + assert c.save + assert_equal [1], c.issue_ids.sort + end + + def test_ref_keywords_allow_brackets_around_a_issue_number + Setting.commit_ref_keywords = '*' + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => '[#1] Worked on this issue', + :revision => '12345') + assert c.save + assert_equal [1], c.issue_ids.sort + end + + def test_ref_keywords_allow_brackets_around_multiple_issue_numbers + Setting.commit_ref_keywords = '*' + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => '[#1 #2, #3] Worked on these', + :revision => '12345') + assert c.save + assert_equal [1,2,3], c.issue_ids.sort + end + + def test_commit_referencing_a_subproject_issue + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'refs #5, a subproject issue', + :revision => '12345') + assert c.save + assert_equal [5], c.issue_ids.sort + assert c.issues.first.project != c.project + end + + def test_commit_closing_a_subproject_issue + with_settings :commit_fix_status_id => 5, :commit_fix_keywords => 'closes', + :default_language => 'en' do + issue = Issue.find(5) + assert !issue.closed? + assert_difference 'Journal.count' do + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'closes #5, a subproject issue', + :revision => '12345') + assert c.save + end + assert issue.reload.closed? + journal = Journal.first(:order => 'id DESC') + assert_equal issue, journal.issue + assert_include "Applied in changeset ecookbook:r12345.", journal.notes + end + end + + def test_commit_referencing_a_parent_project_issue + # repository of child project + r = Repository::Subversion.create!( + :project => Project.find(3), + :url => 'svn://localhost/test') + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :comments => 'refs #2, an issue of a parent project', + :revision => '12345') + assert c.save + assert_equal [2], c.issue_ids.sort + assert c.issues.first.project != c.project + end + + def test_commit_referencing_a_project_with_commit_cross_project_ref_disabled + r = Repository::Subversion.create!( + :project => Project.find(3), + :url => 'svn://localhost/test') + + with_settings :commit_cross_project_ref => '0' do + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :comments => 'refs #4, an issue of a different project', + :revision => '12345') + assert c.save + assert_equal [], c.issue_ids + end + end + + def test_commit_referencing_a_project_with_commit_cross_project_ref_enabled + r = Repository::Subversion.create!( + :project => Project.find(3), + :url => 'svn://localhost/test') + + with_settings :commit_cross_project_ref => '1' do + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :comments => 'refs #4, an issue of a different project', + :revision => '12345') + assert c.save + assert_equal [4], c.issue_ids + end + end + + def test_text_tag_revision + c = Changeset.new(:revision => '520') + assert_equal 'r520', c.text_tag + end + + def test_text_tag_revision_with_same_project + c = Changeset.new(:revision => '520', :repository => Project.find(1).repository) + assert_equal 'r520', c.text_tag(Project.find(1)) + end + + def test_text_tag_revision_with_different_project + c = Changeset.new(:revision => '520', :repository => Project.find(1).repository) + assert_equal 'ecookbook:r520', c.text_tag(Project.find(2)) + end + + def test_text_tag_revision_with_repository_identifier + r = Repository::Subversion.create!( + :project_id => 1, + :url => 'svn://localhost/test', + :identifier => 'documents') + + c = Changeset.new(:revision => '520', :repository => r) + assert_equal 'documents|r520', c.text_tag + assert_equal 'ecookbook:documents|r520', c.text_tag(Project.find(2)) + end + + def test_text_tag_hash + c = Changeset.new( + :scmid => '7234cb2750b63f47bff735edc50a1c0a433c2518', + :revision => '7234cb2750b63f47bff735edc50a1c0a433c2518') + assert_equal 'commit:7234cb2750b63f47bff735edc50a1c0a433c2518', c.text_tag + end + + def test_text_tag_hash_with_same_project + c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => Project.find(1).repository) + assert_equal 'commit:7234cb27', c.text_tag(Project.find(1)) + end + + def test_text_tag_hash_with_different_project + c = Changeset.new(:revision => '7234cb27', :scmid => '7234cb27', :repository => Project.find(1).repository) + assert_equal 'ecookbook:commit:7234cb27', c.text_tag(Project.find(2)) + end + + def test_text_tag_hash_all_number + c = Changeset.new(:scmid => '0123456789', :revision => '0123456789') + assert_equal 'commit:0123456789', c.text_tag + end + + def test_previous + changeset = Changeset.find_by_revision('3') + assert_equal Changeset.find_by_revision('2'), changeset.previous + end + + def test_previous_nil + changeset = Changeset.find_by_revision('1') + assert_nil changeset.previous + end + + def test_next + changeset = Changeset.find_by_revision('2') + assert_equal Changeset.find_by_revision('3'), changeset.next + end + + def test_next_nil + changeset = Changeset.find_by_revision('10') + assert_nil changeset.next + end + + def test_comments_should_be_converted_to_utf8 + proj = Project.find(3) + # str = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt") + str = "Texte encod\xe9 en ISO-8859-1." + str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'ISO-8859-1' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => str) + assert( c.save ) + str_utf8 = "Texte encod\xc3\xa9 en ISO-8859-1." + str_utf8.force_encoding("UTF-8") if str_utf8.respond_to?(:force_encoding) + assert_equal str_utf8, c.comments + end + + def test_invalid_utf8_sequences_in_comments_should_be_replaced_latin1 + proj = Project.find(3) + # str = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt") + str1 = "Texte encod\xe9 en ISO-8859-1." + str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test" + str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding) + str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'UTF-8' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => str1, + :committer => str2) + assert( c.save ) + assert_equal "Texte encod? en ISO-8859-1.", c.comments + assert_equal "?a?b?c?d?e test", c.committer + end + + def test_invalid_utf8_sequences_in_comments_should_be_replaced_ja_jis + proj = Project.find(3) + str = "test\xb5\xfetest\xb5\xfe" + if str.respond_to?(:force_encoding) + str.force_encoding('ASCII-8BIT') + end + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'ISO-2022-JP' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => str) + assert( c.save ) + assert_equal "test??test??", c.comments + end + + def test_comments_should_be_converted_all_latin1_to_utf8 + s1 = "\xC2\x80" + s2 = "\xc3\x82\xc2\x80" + s4 = s2.dup + if s1.respond_to?(:force_encoding) + s3 = s1.dup + s1.force_encoding('ASCII-8BIT') + s2.force_encoding('ASCII-8BIT') + s3.force_encoding('ISO-8859-1') + s4.force_encoding('UTF-8') + assert_equal s3.encode('UTF-8'), s4 + end + proj = Project.find(3) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'ISO-8859-1' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => s1) + assert( c.save ) + assert_equal s4, c.comments + end + + def test_invalid_utf8_sequences_in_paths_should_be_replaced + proj = Project.find(3) + str1 = "Texte encod\xe9 en ISO-8859-1" + str2 = "\xe9a\xe9b\xe9c\xe9d\xe9e test" + str1.force_encoding("UTF-8") if str1.respond_to?(:force_encoding) + str2.force_encoding("ASCII-8BIT") if str2.respond_to?(:force_encoding) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'UTF-8' ) + assert r + cs = Changeset.new( + :repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => "test") + assert(cs.save) + ch = Change.new( + :changeset => cs, + :action => "A", + :path => str1, + :from_path => str2, + :from_revision => "345") + assert(ch.save) + assert_equal "Texte encod? en ISO-8859-1", ch.path + assert_equal "?a?b?c?d?e test", ch.from_path + end + + def test_comments_nil + proj = Project.find(3) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'ISO-8859-1' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => nil, + :committer => nil) + assert( c.save ) + assert_equal "", c.comments + assert_equal nil, c.committer + if c.comments.respond_to?(:force_encoding) + assert_equal "UTF-8", c.comments.encoding.to_s + end + end + + def test_comments_empty + proj = Project.find(3) + r = Repository::Bazaar.create!( + :project => proj, + :url => '/tmp/test/bazaar', + :log_encoding => 'ISO-8859-1' ) + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '12345', + :comments => "", + :committer => "") + assert( c.save ) + assert_equal "", c.comments + assert_equal "", c.committer + if c.comments.respond_to?(:force_encoding) + assert_equal "UTF-8", c.comments.encoding.to_s + assert_equal "UTF-8", c.committer.encoding.to_s + end + end + + def test_identifier + c = Changeset.find_by_revision('1') + assert_equal c.revision, c.identifier + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/46/460e71f2237160ca7193af1edf7625b3c1f0d25b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/46/460e71f2237160ca7193af1edf7625b3c1f0d25b.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1 @@ +<%= principals_check_box_tags 'watcher[user_ids][]', @users %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/46/460ecc87859ea5b7914cc2b5325e7c6c1beafc5d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/46/460ecc87859ea5b7914cc2b5325e7c6c1beafc5d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,6 @@ +

    <%= link_to l(:label_issue_status_plural), issue_statuses_path %> » <%=h @issue_status %>

    + +<%= labelled_form_for @issue_status do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_save) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/46/46c5de8338ae67ca1d4c913350410a5c21e8dc3e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/46/46c5de8338ae67ca1d4c913350410a5c21e8dc3e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,95 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class ActivityTest < ActiveSupport::TestCase + fixtures :projects, :versions, :attachments, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details, + :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages + + def setup + @project = Project.find(1) + end + + def test_activity_without_subprojects + events = find_events(User.anonymous, :project => @project) + assert_not_nil events + + assert events.include?(Issue.find(1)) + assert !events.include?(Issue.find(4)) + # subproject issue + assert !events.include?(Issue.find(5)) + end + + def test_activity_with_subprojects + events = find_events(User.anonymous, :project => @project, :with_subprojects => 1) + assert_not_nil events + + assert events.include?(Issue.find(1)) + # subproject issue + assert events.include?(Issue.find(5)) + end + + def test_global_activity_anonymous + events = find_events(User.anonymous) + assert_not_nil events + + assert events.include?(Issue.find(1)) + assert events.include?(Message.find(5)) + # Issue of a private project + assert !events.include?(Issue.find(4)) + # Private issue and comment + assert !events.include?(Issue.find(14)) + assert !events.include?(Journal.find(5)) + end + + def test_global_activity_logged_user + events = find_events(User.find(2)) # manager + assert_not_nil events + + assert events.include?(Issue.find(1)) + # Issue of a private project the user belongs to + assert events.include?(Issue.find(4)) + end + + def test_user_activity + user = User.find(2) + events = Redmine::Activity::Fetcher.new(User.anonymous, :author => user).events(nil, nil, :limit => 10) + + assert(events.size > 0) + assert(events.size <= 10) + assert_nil(events.detect {|e| e.event_author != user}) + end + + def test_files_activity + f = Redmine::Activity::Fetcher.new(User.anonymous, :project => Project.find(1)) + f.scope = ['files'] + events = f.events + + assert_kind_of Array, events + assert events.include?(Attachment.find_by_container_type_and_container_id('Project', 1)) + assert events.include?(Attachment.find_by_container_type_and_container_id('Version', 1)) + assert_equal [Attachment], events.collect(&:class).uniq + assert_equal %w(Project Version), events.collect(&:container_type).uniq.sort + end + + private + + def find_events(user, options={}) + Redmine::Activity::Fetcher.new(user, options).events(Date.today - 30, Date.today + 1) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/47/471e676fbbcffe85f92bd11d39035aca0032fdca.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/47/471e676fbbcffe85f92bd11d39035aca0032fdca.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,20 @@ +

    <%=l(:label_project_new)%>

    + +<%= labelled_form_for @project, :url => { :action => "copy" } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> + +
    <%= l(:button_copy) %> + + + + + + + + <%= hidden_field_tag 'only[]', '' %> +
    + +
    + +<%= submit_tag l(:button_copy) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/47/475e48920f645a29c9410b5f2003c06d6f0a84d4.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/47/475e48920f645a29c9410b5f2003c06d6f0a84d4.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,347 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) +require 'pp' +class ApiTest::UsersTest < ActionController::IntegrationTest + fixtures :users, :members, :member_roles, :roles, :projects + + def setup + Setting.rest_api_enabled = '1' + end + + context "GET /users" do + should_allow_api_authentication(:get, "/users.xml") + should_allow_api_authentication(:get, "/users.json") + end + + context "GET /users/2" do + context ".xml" do + should "return requested user" do + get '/users/2.xml' + + assert_response :success + assert_tag :tag => 'user', + :child => {:tag => 'id', :content => '2'} + end + + context "with include=memberships" do + should "include memberships" do + get '/users/2.xml?include=memberships' + + assert_response :success + assert_tag :tag => 'memberships', + :parent => {:tag => 'user'}, + :children => {:count => 1} + end + end + end + + context ".json" do + should "return requested user" do + get '/users/2.json' + + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_kind_of Hash, json['user'] + assert_equal 2, json['user']['id'] + end + + context "with include=memberships" do + should "include memberships" do + get '/users/2.json?include=memberships' + + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Array, json['user']['memberships'] + assert_equal [{ + "id"=>1, + "project"=>{"name"=>"eCookbook", "id"=>1}, + "roles"=>[{"name"=>"Manager", "id"=>1}] + }], json['user']['memberships'] + end + end + end + end + + context "GET /users/current" do + context ".xml" do + should "require authentication" do + get '/users/current.xml' + + assert_response 401 + end + + should "return current user" do + get '/users/current.xml', {}, credentials('jsmith') + + assert_tag :tag => 'user', + :child => {:tag => 'id', :content => '2'} + end + end + end + + context "POST /users" do + context "with valid parameters" do + setup do + @parameters = { + :user => { + :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname', + :mail => 'foo@example.net', :password => 'secret123', + :mail_notification => 'only_assigned' + } + } + end + + context ".xml" do + should_allow_api_authentication(:post, + '/users.xml', + {:user => { + :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname', + :mail => 'foo@example.net', :password => 'secret123' + }}, + {:success_code => :created}) + + should "create a user with the attributes" do + assert_difference('User.count') do + post '/users.xml', @parameters, credentials('admin') + end + + user = User.first(:order => 'id DESC') + assert_equal 'foo', user.login + assert_equal 'Firstname', user.firstname + assert_equal 'Lastname', user.lastname + assert_equal 'foo@example.net', user.mail + assert_equal 'only_assigned', user.mail_notification + assert !user.admin? + assert user.check_password?('secret123') + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_tag 'user', :child => {:tag => 'id', :content => user.id.to_s} + end + end + + context ".json" do + should_allow_api_authentication(:post, + '/users.json', + {:user => { + :login => 'foo', :firstname => 'Firstname', :lastname => 'Lastname', + :mail => 'foo@example.net' + }}, + {:success_code => :created}) + + should "create a user with the attributes" do + assert_difference('User.count') do + post '/users.json', @parameters, credentials('admin') + end + + user = User.first(:order => 'id DESC') + assert_equal 'foo', user.login + assert_equal 'Firstname', user.firstname + assert_equal 'Lastname', user.lastname + assert_equal 'foo@example.net', user.mail + assert !user.admin? + + assert_response :created + assert_equal 'application/json', @response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert_kind_of Hash, json['user'] + assert_equal user.id, json['user']['id'] + end + end + end + + context "with invalid parameters" do + setup do + @parameters = {:user => {:login => 'foo', :lastname => 'Lastname', :mail => 'foo'}} + end + + context ".xml" do + should "return errors" do + assert_no_difference('User.count') do + post '/users.xml', @parameters, credentials('admin') + end + + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag 'errors', :child => { + :tag => 'error', + :content => "First name can't be blank" + } + end + end + + context ".json" do + should "return errors" do + assert_no_difference('User.count') do + post '/users.json', @parameters, credentials('admin') + end + + assert_response :unprocessable_entity + assert_equal 'application/json', @response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert json.has_key?('errors') + assert_kind_of Array, json['errors'] + end + end + end + end + + context "PUT /users/2" do + context "with valid parameters" do + setup do + @parameters = { + :user => { + :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed', + :mail => 'jsmith@somenet.foo' + } + } + end + + context ".xml" do + should_allow_api_authentication(:put, + '/users/2.xml', + {:user => { + :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed', + :mail => 'jsmith@somenet.foo' + }}, + {:success_code => :ok}) + + should "update user with the attributes" do + assert_no_difference('User.count') do + put '/users/2.xml', @parameters, credentials('admin') + end + + user = User.find(2) + assert_equal 'jsmith', user.login + assert_equal 'John', user.firstname + assert_equal 'Renamed', user.lastname + assert_equal 'jsmith@somenet.foo', user.mail + assert !user.admin? + + assert_response :ok + assert_equal '', @response.body + end + end + + context ".json" do + should_allow_api_authentication(:put, + '/users/2.json', + {:user => { + :login => 'jsmith', :firstname => 'John', :lastname => 'Renamed', + :mail => 'jsmith@somenet.foo' + }}, + {:success_code => :ok}) + + should "update user with the attributes" do + assert_no_difference('User.count') do + put '/users/2.json', @parameters, credentials('admin') + end + + user = User.find(2) + assert_equal 'jsmith', user.login + assert_equal 'John', user.firstname + assert_equal 'Renamed', user.lastname + assert_equal 'jsmith@somenet.foo', user.mail + assert !user.admin? + + assert_response :ok + assert_equal '', @response.body + end + end + end + + context "with invalid parameters" do + setup do + @parameters = { + :user => { + :login => 'jsmith', :firstname => '', :lastname => 'Lastname', + :mail => 'foo' + } + } + end + + context ".xml" do + should "return errors" do + assert_no_difference('User.count') do + put '/users/2.xml', @parameters, credentials('admin') + end + + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_tag 'errors', :child => { + :tag => 'error', + :content => "First name can't be blank" + } + end + end + + context ".json" do + should "return errors" do + assert_no_difference('User.count') do + put '/users/2.json', @parameters, credentials('admin') + end + + assert_response :unprocessable_entity + assert_equal 'application/json', @response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Hash, json + assert json.has_key?('errors') + assert_kind_of Array, json['errors'] + end + end + end + end + + context "DELETE /users/2" do + context ".xml" do + should_allow_api_authentication(:delete, + '/users/2.xml', + {}, + {:success_code => :ok}) + + should "delete user" do + assert_difference('User.count', -1) do + delete '/users/2.xml', {}, credentials('admin') + end + + assert_response :ok + assert_equal '', @response.body + end + end + + context ".json" do + should_allow_api_authentication(:delete, + '/users/2.xml', + {}, + {:success_code => :ok}) + + should "delete user" do + assert_difference('User.count', -1) do + delete '/users/2.json', {}, credentials('admin') + end + + assert_response :ok + assert_equal '', @response.body + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/47/4777ed41e91ac8f84180a4f6bd7516a1b9346e0e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/47/4777ed41e91ac8f84180a4f6bd7516a1b9346e0e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,59 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../test_helper', __FILE__) + +class Redmine::AccessControlTest < ActiveSupport::TestCase + + def setup + @access_module = Redmine::AccessControl + end + + def test_permissions + perms = @access_module.permissions + assert perms.is_a?(Array) + assert perms.first.is_a?(Redmine::AccessControl::Permission) + end + + def test_module_permission + perm = @access_module.permission(:view_issues) + assert perm.is_a?(Redmine::AccessControl::Permission) + assert_equal :view_issues, perm.name + assert_equal :issue_tracking, perm.project_module + assert perm.actions.is_a?(Array) + assert perm.actions.include?('issues/index') + end + + def test_no_module_permission + perm = @access_module.permission(:edit_project) + assert perm.is_a?(Redmine::AccessControl::Permission) + assert_equal :edit_project, perm.name + assert_nil perm.project_module + assert perm.actions.is_a?(Array) + assert perm.actions.include?('projects/settings') + end + + def test_read_action_should_return_true_for_read_actions + assert_equal true, @access_module.read_action?(:view_project) + assert_equal true, @access_module.read_action?(:controller => 'projects', :action => 'show') + end + + def test_read_action_should_return_false_for_update_actions + assert_equal false, @access_module.read_action?(:edit_project) + assert_equal false, @access_module.read_action?(:controller => 'projects', :action => 'edit') + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/47/478eea0d533e8053ee16800d8a6228965a404db6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/47/478eea0d533e8053ee16800d8a6228965a404db6.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,159 @@ +require 'uri' +require 'openid' +require 'rack/openid' + +module OpenIdAuthentication + def self.new(app) + store = OpenIdAuthentication.store + if store.nil? + Rails.logger.warn "OpenIdAuthentication.store is nil. Using in-memory store." + end + + ::Rack::OpenID.new(app, OpenIdAuthentication.store) + end + + def self.store + @@store + end + + def self.store=(*store_option) + store, *parameters = *([ store_option ].flatten) + + @@store = case store + when :memory + require 'openid/store/memory' + OpenID::Store::Memory.new + when :file + require 'openid/store/filesystem' + OpenID::Store::Filesystem.new(Rails.root.join('tmp/openids')) + when :memcache + require 'memcache' + require 'openid/store/memcache' + OpenID::Store::Memcache.new(MemCache.new(parameters)) + else + store + end + end + + self.store = nil + + class InvalidOpenId < StandardError + end + + class Result + ERROR_MESSAGES = { + :missing => "Sorry, the OpenID server couldn't be found", + :invalid => "Sorry, but this does not appear to be a valid OpenID", + :canceled => "OpenID verification was canceled", + :failed => "OpenID verification failed", + :setup_needed => "OpenID verification needs setup" + } + + def self.[](code) + new(code) + end + + def initialize(code) + @code = code + end + + def status + @code + end + + ERROR_MESSAGES.keys.each { |state| define_method("#{state}?") { @code == state } } + + def successful? + @code == :successful + end + + def unsuccessful? + ERROR_MESSAGES.keys.include?(@code) + end + + def message + ERROR_MESSAGES[@code] + end + end + + # normalizes an OpenID according to http://openid.net/specs/openid-authentication-2_0.html#normalization + def self.normalize_identifier(identifier) + # clean up whitespace + identifier = identifier.to_s.strip + + # if an XRI has a prefix, strip it. + identifier.gsub!(/xri:\/\//i, '') + + # dodge XRIs -- TODO: validate, don't just skip. + unless ['=', '@', '+', '$', '!', '('].include?(identifier.at(0)) + # does it begin with http? if not, add it. + identifier = "http://#{identifier}" unless identifier =~ /^http/i + + # strip any fragments + identifier.gsub!(/\#(.*)$/, '') + + begin + uri = URI.parse(identifier) + uri.scheme = uri.scheme.downcase if uri.scheme # URI should do this + identifier = uri.normalize.to_s + rescue URI::InvalidURIError + raise InvalidOpenId.new("#{identifier} is not an OpenID identifier") + end + end + + return identifier + end + + protected + # The parameter name of "openid_identifier" is used rather than + # the Rails convention "open_id_identifier" because that's what + # the specification dictates in order to get browser auto-complete + # working across sites + def using_open_id?(identifier = nil) #:doc: + identifier ||= open_id_identifier + !identifier.blank? || request.env[Rack::OpenID::RESPONSE] + end + + def authenticate_with_open_id(identifier = nil, options = {}, &block) #:doc: + identifier ||= open_id_identifier + + if request.env[Rack::OpenID::RESPONSE] + complete_open_id_authentication(&block) + else + begin_open_id_authentication(identifier, options, &block) + end + end + + private + def open_id_identifier + params[:openid_identifier] || params[:openid_url] + end + + def begin_open_id_authentication(identifier, options = {}) + options[:identifier] = identifier + value = Rack::OpenID.build_header(options) + response.headers[Rack::OpenID::AUTHENTICATE_HEADER] = value + head :unauthorized + end + + def complete_open_id_authentication + response = request.env[Rack::OpenID::RESPONSE] + identifier = response.display_identifier + + case response.status + when OpenID::Consumer::SUCCESS + yield Result[:successful], identifier, + OpenID::SReg::Response.from_success_response(response) + when :missing + yield Result[:missing], identifier, nil + when :invalid + yield Result[:invalid], identifier, nil + when OpenID::Consumer::CANCEL + yield Result[:canceled], identifier, nil + when OpenID::Consumer::FAILURE + yield Result[:failed], identifier, nil + when OpenID::Consumer::SETUP_NEEDED + yield Result[:setup_needed], response.setup_url, nil + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/47/47acf6e2f5829588f1351ffca0545494eaf63df2.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/47/47acf6e2f5829588f1351ffca0545494eaf63df2.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,55 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingAuthSourcesTest < ActionController::IntegrationTest + def test_auth_sources + assert_routing( + { :method => 'get', :path => "/auth_sources" }, + { :controller => 'auth_sources', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/auth_sources/new" }, + { :controller => 'auth_sources', :action => 'new' } + ) + assert_routing( + { :method => 'post', :path => "/auth_sources" }, + { :controller => 'auth_sources', :action => 'create' } + ) + assert_routing( + { :method => 'get', :path => "/auth_sources/1234/edit" }, + { :controller => 'auth_sources', :action => 'edit', + :id => '1234' } + ) + assert_routing( + { :method => 'put', :path => "/auth_sources/1234" }, + { :controller => 'auth_sources', :action => 'update', + :id => '1234' } + ) + assert_routing( + { :method => 'delete', :path => "/auth_sources/1234" }, + { :controller => 'auth_sources', :action => 'destroy', + :id => '1234' } + ) + assert_routing( + { :method => 'get', :path => "/auth_sources/1234/test_connection" }, + { :controller => 'auth_sources', :action => 'test_connection', + :id => '1234' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/47/47db8804bc6b23e367b3fe0fd5a73e75ce399c46.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/47/47db8804bc6b23e367b3fe0fd5a73e75ce399c46.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,47 @@ +<%= call_hook :view_account_login_top %> +
    +<%= form_tag(signin_path) do %> +<%= back_url_hidden_field_tag %> + + + + + + + + + +<% if Setting.openid? %> + + + + +<% end %> + + + + + + + + +
    <%= text_field_tag 'username', params[:username], :tabindex => '1' %>
    <%= password_field_tag 'password', nil, :tabindex => '2' %>
    <%= text_field_tag "openid_url", nil, :tabindex => '3' %>
    + <% if Setting.autologin? %> + + <% end %> +
    + <% if Setting.lost_password? %> + <%= link_to l(:label_password_lost), lost_password_path %> + <% end %> + + +
    +<% end %> +
    +<%= call_hook :view_account_login_bottom %> + +<% if params[:username].present? %> +<%= javascript_tag "$('#password').focus();" %> +<% else %> +<%= javascript_tag "$('#username').focus();" %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/48/4805a0a6d5e1e33d18b85a4122fe5703ab1634d6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/48/4805a0a6d5e1e33d18b85a4122fe5703ab1634d6.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,37 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingPreviewsTest < ActionController::IntegrationTest + def test_previews + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/issues/preview/new/123" }, + { :controller => 'previews', :action => 'issue', :project_id => '123' } + ) + assert_routing( + { :method => method, :path => "/issues/preview/edit/321" }, + { :controller => 'previews', :action => 'issue', :id => '321' } + ) + end + assert_routing( + { :method => 'get', :path => "/news/preview" }, + { :controller => 'previews', :action => 'news' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/48/489ce25cc8ec8ef6e82cc4ec227ed1a0b48a4030.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/48/489ce25cc8ec8ef6e82cc4ec227ed1a0b48a4030.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,6 @@ +<% if @membership.valid? %> + $('#tab-content-memberships').html('<%= escape_javascript(render :partial => 'groups/memberships') %>'); + $('#member-<%= @membership.id %>').effect("highlight"); +<% else %> + alert('<%= raw(escape_javascript(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))) %>'); +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/48/48ff18e334062d6b11594a2a05769b3f109ed7d6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/48/48ff18e334062d6b11594a2a05769b3f109ed7d6.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,11 @@ +<% if @user.auth_source %> +

    <%= l(:mail_body_account_information_external, h(@user.auth_source.name)) %>

    +<% else %> +

    <%= l(:mail_body_account_information) %>:

    +
      +
    • <%= l(:field_login) %>: <%=h @user.login %>
    • +
    • <%= l(:field_password) %>: <%=h @password %>
    • +
    +<% end %> + +

    <%= l(:label_login) %>: <%= link_to h(@login_url), @login_url %>

    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/49/49262a733952cf86794ef87f8a818fffd12082ee.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/49/49262a733952cf86794ef87f8a818fffd12082ee.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,9 @@ +require File.expand_path('../../test_helper', __FILE__) + +class <%= @model_class %>Test < ActiveSupport::TestCase + + # Replace this with your real tests. + def test_truth + assert true + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/49/49694bf6e76d7faeababb71ee86881f8b3a033fa.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/49/49694bf6e76d7faeababb71ee86881f8b3a033fa.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,236 @@ +var contextMenuObserving; +var contextMenuUrl; + +function contextMenuRightClick(event) { + var target = $(event.target); + if (target.is('a')) {return;} + var tr = target.parents('tr').first(); + if (!tr.hasClass('hascontextmenu')) {return;} + event.preventDefault(); + if (!contextMenuIsSelected(tr)) { + contextMenuUnselectAll(); + contextMenuAddSelection(tr); + contextMenuSetLastSelected(tr); + } + contextMenuShow(event); +} + +function contextMenuClick(event) { + var target = $(event.target); + var lastSelected; + + if (target.is('a') && target.hasClass('submenu')) { + event.preventDefault(); + return; + } + contextMenuHide(); + if (target.is('a') || target.is('img')) { return; } + if (event.which == 1 || (navigator.appVersion.match(/\bMSIE\b/))) { + var tr = target.parents('tr').first(); + if (tr.length && tr.hasClass('hascontextmenu')) { + // a row was clicked, check if the click was on checkbox + if (target.is('input')) { + // a checkbox may be clicked + if (target.attr('checked')) { + tr.addClass('context-menu-selection'); + } else { + tr.removeClass('context-menu-selection'); + } + } else { + if (event.ctrlKey || event.metaKey) { + contextMenuToggleSelection(tr); + } else if (event.shiftKey) { + lastSelected = contextMenuLastSelected(); + if (lastSelected.length) { + var toggling = false; + $('.hascontextmenu').each(function(){ + if (toggling || $(this).is(tr)) { + contextMenuAddSelection($(this)); + } + if ($(this).is(tr) || $(this).is(lastSelected)) { + toggling = !toggling; + } + }); + } else { + contextMenuAddSelection(tr); + } + } else { + contextMenuUnselectAll(); + contextMenuAddSelection(tr); + } + contextMenuSetLastSelected(tr); + } + } else { + // click is outside the rows + if (target.is('a') && (target.hasClass('disabled') || target.hasClass('submenu'))) { + event.preventDefault(); + } else { + contextMenuUnselectAll(); + } + } + } +} + +function contextMenuCreate() { + if ($('#context-menu').length < 1) { + var menu = document.createElement("div"); + menu.setAttribute("id", "context-menu"); + menu.setAttribute("style", "display:none;"); + document.getElementById("content").appendChild(menu); + } +} + +function contextMenuShow(event) { + var mouse_x = event.pageX; + var mouse_y = event.pageY; + var render_x = mouse_x; + var render_y = mouse_y; + var dims; + var menu_width; + var menu_height; + var window_width; + var window_height; + var max_width; + var max_height; + + $('#context-menu').css('left', (render_x + 'px')); + $('#context-menu').css('top', (render_y + 'px')); + $('#context-menu').html(''); + + $.ajax({ + url: contextMenuUrl, + data: $(event.target).parents('form').first().serialize(), + success: function(data, textStatus, jqXHR) { + $('#context-menu').html(data); + menu_width = $('#context-menu').width(); + menu_height = $('#context-menu').height(); + max_width = mouse_x + 2*menu_width; + max_height = mouse_y + menu_height; + + var ws = window_size(); + window_width = ws.width; + window_height = ws.height; + + /* display the menu above and/or to the left of the click if needed */ + if (max_width > window_width) { + render_x -= menu_width; + $('#context-menu').addClass('reverse-x'); + } else { + $('#context-menu').removeClass('reverse-x'); + } + if (max_height > window_height) { + render_y -= menu_height; + $('#context-menu').addClass('reverse-y'); + } else { + $('#context-menu').removeClass('reverse-y'); + } + if (render_x <= 0) render_x = 1; + if (render_y <= 0) render_y = 1; + $('#context-menu').css('left', (render_x + 'px')); + $('#context-menu').css('top', (render_y + 'px')); + $('#context-menu').show(); + + //if (window.parseStylesheets) { window.parseStylesheets(); } // IE + + } + }); +} + +function contextMenuSetLastSelected(tr) { + $('.cm-last').removeClass('cm-last'); + tr.addClass('cm-last'); +} + +function contextMenuLastSelected() { + return $('.cm-last').first(); +} + +function contextMenuUnselectAll() { + $('.hascontextmenu').each(function(){ + contextMenuRemoveSelection($(this)); + }); + $('.cm-last').removeClass('cm-last'); +} + +function contextMenuHide() { + $('#context-menu').hide(); +} + +function contextMenuToggleSelection(tr) { + if (contextMenuIsSelected(tr)) { + contextMenuRemoveSelection(tr); + } else { + contextMenuAddSelection(tr); + } +} + +function contextMenuAddSelection(tr) { + tr.addClass('context-menu-selection'); + contextMenuCheckSelectionBox(tr, true); + contextMenuClearDocumentSelection(); +} + +function contextMenuRemoveSelection(tr) { + tr.removeClass('context-menu-selection'); + contextMenuCheckSelectionBox(tr, false); +} + +function contextMenuIsSelected(tr) { + return tr.hasClass('context-menu-selection'); +} + +function contextMenuCheckSelectionBox(tr, checked) { + tr.find('input[type=checkbox]').attr('checked', checked); +} + +function contextMenuClearDocumentSelection() { + // TODO + if (document.selection) { + document.selection.clear(); // IE + } else { + window.getSelection().removeAllRanges(); + } +} + +function contextMenuInit(url) { + contextMenuUrl = url; + contextMenuCreate(); + contextMenuUnselectAll(); + + if (!contextMenuObserving) { + $(document).click(contextMenuClick); + $(document).contextmenu(contextMenuRightClick); + contextMenuObserving = true; + } +} + +function toggleIssuesSelection(el) { + var boxes = $(el).parents('form').find('input[type=checkbox]'); + var all_checked = true; + boxes.each(function(){ if (!$(this).attr('checked')) { all_checked = false; } }); + boxes.each(function(){ + if (all_checked) { + $(this).removeAttr('checked'); + $(this).parents('tr').removeClass('context-menu-selection'); + } else if (!$(this).attr('checked')) { + $(this).attr('checked', true); + $(this).parents('tr').addClass('context-menu-selection'); + } + }); +} + +function window_size() { + var w; + var h; + if (window.innerWidth) { + w = window.innerWidth; + h = window.innerHeight; + } else if (document.documentElement) { + w = document.documentElement.clientWidth; + h = document.documentElement.clientHeight; + } else { + w = document.body.clientWidth; + h = document.body.clientHeight; + } + return {width: w, height: h}; +} diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/49/499813da1df06a99345ed293383035e1bc859046.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/49/499813da1df06a99345ed293383035e1bc859046.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,434 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../../../test_helper', __FILE__) +begin + require 'mocha' + + class MercurialAdapterTest < ActiveSupport::TestCase + HELPERS_DIR = Redmine::Scm::Adapters::MercurialAdapter::HELPERS_DIR + TEMPLATE_NAME = Redmine::Scm::Adapters::MercurialAdapter::TEMPLATE_NAME + TEMPLATE_EXTENSION = Redmine::Scm::Adapters::MercurialAdapter::TEMPLATE_EXTENSION + + REPOSITORY_PATH = Rails.root.join('tmp/test/mercurial_repository').to_s + CHAR_1_HEX = "\xc3\x9c" + + if File.directory?(REPOSITORY_PATH) + def setup + adapter_class = Redmine::Scm::Adapters::MercurialAdapter + assert adapter_class + assert adapter_class.client_command + assert_equal true, adapter_class.client_available + assert_equal true, adapter_class.client_version_above?([0, 9, 5]) + + @adapter = Redmine::Scm::Adapters::MercurialAdapter.new( + REPOSITORY_PATH, + nil, + nil, + nil, + 'ISO-8859-1') + @diff_c_support = true + @char_1 = CHAR_1_HEX.dup + @tag_char_1 = "tag-#{CHAR_1_HEX}-00" + @branch_char_0 = "branch-#{CHAR_1_HEX}-00" + @branch_char_1 = "branch-#{CHAR_1_HEX}-01" + if @tag_char_1.respond_to?(:force_encoding) + @char_1.force_encoding('UTF-8') + @tag_char_1.force_encoding('UTF-8') + @branch_char_0.force_encoding('UTF-8') + @branch_char_1.force_encoding('UTF-8') + end + end + + def test_hgversion + to_test = { "Mercurial Distributed SCM (version 0.9.5)\n" => [0,9,5], + "Mercurial Distributed SCM (1.0)\n" => [1,0], + "Mercurial Distributed SCM (1e4ddc9ac9f7+20080325)\n" => nil, + "Mercurial Distributed SCM (1.0.1+20080525)\n" => [1,0,1], + "Mercurial Distributed SCM (1916e629a29d)\n" => nil, + "Mercurial SCM Distribuito (versione 0.9.5)\n" => [0,9,5], + "(1.6)\n(1.7)\n(1.8)" => [1,6], + "(1.7.1)\r\n(1.8.1)\r\n(1.9.1)" => [1,7,1]} + + to_test.each do |s, v| + test_hgversion_for(s, v) + end + end + + def test_template_path + to_test = { + [1,2] => "1.0", + [] => "1.0", + [1,2,1] => "1.0", + [1,7] => "1.0", + [1,7,1] => "1.0", + [2,0] => "1.0", + } + to_test.each do |v, template| + test_template_path_for(v, template) + end + end + + def test_info + [REPOSITORY_PATH, REPOSITORY_PATH + "/", + REPOSITORY_PATH + "//"].each do |repo| + adp = Redmine::Scm::Adapters::MercurialAdapter.new(repo) + repo_path = adp.info.root_url.gsub(/\\/, "/") + assert_equal REPOSITORY_PATH, repo_path + assert_equal '31', adp.info.lastrev.revision + assert_equal '31eeee7395c8',adp.info.lastrev.scmid + end + end + + def test_revisions + revisions = @adapter.revisions(nil, 2, 4) + assert_equal 3, revisions.size + assert_equal '2', revisions[0].revision + assert_equal '400bb8672109', revisions[0].scmid + assert_equal '4', revisions[2].revision + assert_equal 'def6d2f1254a', revisions[2].scmid + + revisions = @adapter.revisions(nil, 2, 4, {:limit => 2}) + assert_equal 2, revisions.size + assert_equal '2', revisions[0].revision + assert_equal '400bb8672109', revisions[0].scmid + end + + def test_parents + revs1 = @adapter.revisions(nil, 0, 0) + assert_equal 1, revs1.size + assert_equal [], revs1[0].parents + revs2 = @adapter.revisions(nil, 1, 1) + assert_equal 1, revs2.size + assert_equal 1, revs2[0].parents.size + assert_equal "0885933ad4f6", revs2[0].parents[0] + revs3 = @adapter.revisions(nil, 30, 30) + assert_equal 1, revs3.size + assert_equal 2, revs3[0].parents.size + assert_equal "a94b0528f24f", revs3[0].parents[0] + assert_equal "3a330eb32958", revs3[0].parents[1] + end + + def test_diff + if @adapter.class.client_version_above?([1, 2]) + assert_nil @adapter.diff(nil, '100000') + end + assert_nil @adapter.diff(nil, '100000', '200000') + [2, '400bb8672109', '400', 400].each do |r1| + diff1 = @adapter.diff(nil, r1) + if @diff_c_support + assert_equal 28, diff1.size + buf = diff1[24].gsub(/\r\n|\r|\n/, "") + assert_equal "+ return true unless klass.respond_to?('watched_by')", buf + else + assert_equal 0, diff1.size + end + [4, 'def6d2f1254a'].each do |r2| + diff2 = @adapter.diff(nil, r1, r2) + assert_equal 49, diff2.size + buf = diff2[41].gsub(/\r\n|\r|\n/, "") + assert_equal "+class WelcomeController < ApplicationController", buf + diff3 = @adapter.diff('sources/watchers_controller.rb', r1, r2) + assert_equal 20, diff3.size + buf = diff3[12].gsub(/\r\n|\r|\n/, "") + assert_equal "+ @watched.remove_watcher(user)", buf + + diff4 = @adapter.diff(nil, r2, r1) + assert_equal 49, diff4.size + buf = diff4[41].gsub(/\r\n|\r|\n/, "") + assert_equal "-class WelcomeController < ApplicationController", buf + diff5 = @adapter.diff('sources/watchers_controller.rb', r2, r1) + assert_equal 20, diff5.size + buf = diff5[9].gsub(/\r\n|\r|\n/, "") + assert_equal "- @watched.remove_watcher(user)", buf + end + end + end + + def test_diff_made_by_revision + if @diff_c_support + [24, '24', '4cddb4e45f52'].each do |r1| + diff1 = @adapter.diff(nil, r1) + assert_equal 5, diff1.size + buf = diff1[4].gsub(/\r\n|\r|\n/, "") + assert_equal '+0885933ad4f68d77c2649cd11f8311276e7ef7ce tag-init-revision', buf + end + end + end + + def test_cat + [2, '400bb8672109', '400', 400].each do |r| + buf = @adapter.cat('sources/welcome_controller.rb', r) + assert buf + lines = buf.split("\r\n") + assert_equal 25, lines.length + assert_equal 'class WelcomeController < ApplicationController', lines[17] + end + assert_nil @adapter.cat('sources/welcome_controller.rb') + end + + def test_annotate + assert_equal [], @adapter.annotate("sources/welcome_controller.rb").lines + [2, '400bb8672109', '400', 400].each do |r| + ann = @adapter.annotate('sources/welcome_controller.rb', r) + assert ann + assert_equal '1', ann.revisions[17].revision + assert_equal '9d5b5b004199', ann.revisions[17].identifier + assert_equal 'jsmith', ann.revisions[0].author + assert_equal 25, ann.lines.length + assert_equal 'class WelcomeController < ApplicationController', ann.lines[17] + end + end + + def test_entries + assert_nil @adapter.entries(nil, '100000') + + assert_equal 1, @adapter.entries("sources", 3).size + assert_equal 1, @adapter.entries("sources", 'b3a615152df8').size + + [2, '400bb8672109', '400', 400].each do |r| + entries1 = @adapter.entries(nil, r) + assert entries1 + assert_equal 3, entries1.size + assert_equal 'sources', entries1[1].name + assert_equal 'sources', entries1[1].path + assert_equal 'dir', entries1[1].kind + readme = entries1[2] + assert_equal 'README', readme.name + assert_equal 'README', readme.path + assert_equal 'file', readme.kind + assert_equal 27, readme.size + assert_equal '1', readme.lastrev.revision + assert_equal '9d5b5b004199', readme.lastrev.identifier + # 2007-12-14 10:24:01 +0100 + assert_equal Time.gm(2007, 12, 14, 9, 24, 1), readme.lastrev.time + + entries2 = @adapter.entries('sources', r) + assert entries2 + assert_equal 2, entries2.size + assert_equal 'watchers_controller.rb', entries2[0].name + assert_equal 'sources/watchers_controller.rb', entries2[0].path + assert_equal 'file', entries2[0].kind + assert_equal 'welcome_controller.rb', entries2[1].name + assert_equal 'sources/welcome_controller.rb', entries2[1].path + assert_equal 'file', entries2[1].kind + end + end + + def test_entries_tag + entries1 = @adapter.entries(nil, 'tag_test.00') + assert entries1 + assert_equal 3, entries1.size + assert_equal 'sources', entries1[1].name + assert_equal 'sources', entries1[1].path + assert_equal 'dir', entries1[1].kind + readme = entries1[2] + assert_equal 'README', readme.name + assert_equal 'README', readme.path + assert_equal 'file', readme.kind + assert_equal 21, readme.size + assert_equal '0', readme.lastrev.revision + assert_equal '0885933ad4f6', readme.lastrev.identifier + # 2007-12-14 10:22:52 +0100 + assert_equal Time.gm(2007, 12, 14, 9, 22, 52), readme.lastrev.time + end + + def test_entries_branch + entries1 = @adapter.entries(nil, 'test-branch-00') + assert entries1 + assert_equal 5, entries1.size + assert_equal 'sql_escape', entries1[2].name + assert_equal 'sql_escape', entries1[2].path + assert_equal 'dir', entries1[2].kind + readme = entries1[4] + assert_equal 'README', readme.name + assert_equal 'README', readme.path + assert_equal 'file', readme.kind + assert_equal 365, readme.size + assert_equal '8', readme.lastrev.revision + assert_equal 'c51f5bb613cd', readme.lastrev.identifier + # 2001-02-01 00:00:00 -0900 + assert_equal Time.gm(2001, 2, 1, 9, 0, 0), readme.lastrev.time + end + + def test_locate_on_outdated_repository + assert_equal 1, @adapter.entries("images", 0).size + assert_equal 2, @adapter.entries("images").size + assert_equal 2, @adapter.entries("images", 2).size + end + + def test_access_by_nodeid + path = 'sources/welcome_controller.rb' + assert_equal @adapter.cat(path, 2), @adapter.cat(path, '400bb8672109') + end + + def test_access_by_fuzzy_nodeid + path = 'sources/welcome_controller.rb' + # falls back to nodeid + assert_equal @adapter.cat(path, 2), @adapter.cat(path, '400') + end + + def test_tags + assert_equal [@tag_char_1, 'tag_test.00', 'tag-init-revision'], @adapter.tags + end + + def test_tagmap + tm = { + @tag_char_1 => 'adf805632193', + 'tag_test.00' => '6987191f453a', + 'tag-init-revision' => '0885933ad4f6', + } + assert_equal tm, @adapter.tagmap + end + + def test_branches + brs = [] + @adapter.branches.each do |b| + brs << b + end + assert_equal 7, brs.length + assert_equal 'default', brs[0].to_s + assert_equal '31', brs[0].revision + assert_equal '31eeee7395c8', brs[0].scmid + assert_equal 'test-branch-01', brs[1].to_s + assert_equal '30', brs[1].revision + assert_equal 'ad4dc4f80284', brs[1].scmid + assert_equal @branch_char_1, brs[2].to_s + assert_equal '27', brs[2].revision + assert_equal '7bbf4c738e71', brs[2].scmid + assert_equal 'branch (1)[2]&,%.-3_4', brs[3].to_s + assert_equal '25', brs[3].revision + assert_equal 'afc61e85bde7', brs[3].scmid + assert_equal @branch_char_0, brs[4].to_s + assert_equal '23', brs[4].revision + assert_equal 'c8d3e4887474', brs[4].scmid + assert_equal 'test_branch.latin-1', brs[5].to_s + assert_equal '22', brs[5].revision + assert_equal 'c2ffe7da686a', brs[5].scmid + assert_equal 'test-branch-00', brs[6].to_s + assert_equal '13', brs[6].revision + assert_equal '3a330eb32958', brs[6].scmid + end + + def test_branchmap + bm = { + 'default' => '31eeee7395c8', + 'test_branch.latin-1' => 'c2ffe7da686a', + 'branch (1)[2]&,%.-3_4' => 'afc61e85bde7', + 'test-branch-00' => '3a330eb32958', + "test-branch-01" => 'ad4dc4f80284', + @branch_char_0 => 'c8d3e4887474', + @branch_char_1 => '7bbf4c738e71', + } + assert_equal bm, @adapter.branchmap + end + + def test_path_space + p = 'README (1)[2]&,%.-3_4' + [15, '933ca60293d7'].each do |r1| + assert @adapter.diff(p, r1) + assert @adapter.cat(p, r1) + assert_equal 1, @adapter.annotate(p, r1).lines.length + [25, 'afc61e85bde7'].each do |r2| + assert @adapter.diff(p, r1, r2) + end + end + end + + def test_tag_non_ascii + p = "latin-1-dir/test-#{@char_1}-1.txt" + assert @adapter.cat(p, @tag_char_1) + assert_equal 1, @adapter.annotate(p, @tag_char_1).lines.length + end + + def test_branch_non_ascii + p = "latin-1-dir/test-#{@char_1}-subdir/test-#{@char_1}-1.txt" + assert @adapter.cat(p, @branch_char_1) + assert_equal 1, @adapter.annotate(p, @branch_char_1).lines.length + end + + def test_nodes_in_branch + [ + 'default', + @branch_char_1, + 'branch (1)[2]&,%.-3_4', + @branch_char_0, + 'test_branch.latin-1', + 'test-branch-00', + ].each do |bra| + nib0 = @adapter.nodes_in_branch(bra) + assert nib0 + nib1 = @adapter.nodes_in_branch(bra, :limit => 1) + assert_equal 1, nib1.size + case bra + when 'branch (1)[2]&,%.-3_4' + if @adapter.class.client_version_above?([1, 6]) + assert_equal 3, nib0.size + assert_equal nib0[0], 'afc61e85bde7' + nib2 = @adapter.nodes_in_branch(bra, :limit => 2) + assert_equal 2, nib2.size + assert_equal nib2[1], '933ca60293d7' + end + when @branch_char_1 + if @adapter.class.client_version_above?([1, 6]) + assert_equal 2, nib0.size + assert_equal nib0[1], '08ff3227303e' + nib2 = @adapter.nodes_in_branch(bra, :limit => 1) + assert_equal 1, nib2.size + assert_equal nib2[0], '7bbf4c738e71' + end + end + end + end + + def test_path_encoding_default_utf8 + adpt1 = Redmine::Scm::Adapters::MercurialAdapter.new( + REPOSITORY_PATH + ) + assert_equal "UTF-8", adpt1.path_encoding + adpt2 = Redmine::Scm::Adapters::MercurialAdapter.new( + REPOSITORY_PATH, + nil, + nil, + nil, + "" + ) + assert_equal "UTF-8", adpt2.path_encoding + end + + private + + def test_hgversion_for(hgversion, version) + @adapter.class.expects(:hgversion_from_command_line).returns(hgversion) + assert_equal version, @adapter.class.hgversion + end + + def test_template_path_for(version, template) + assert_equal "#{HELPERS_DIR}/#{TEMPLATE_NAME}-#{template}.#{TEMPLATE_EXTENSION}", + @adapter.class.template_path_for(version) + assert File.exist?(@adapter.class.template_path_for(version)) + end + else + puts "Mercurial test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end + end +rescue LoadError + class MercurialMochaFake < ActiveSupport::TestCase + def test_fake; assert(false, "Requires mocha to run those tests") end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4a/4a4b44d1fe4074726f660952b0f9f7b047919f31.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4a/4a4b44d1fe4074726f660952b0f9f7b047919f31.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1083 @@ +ar: + # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) + direction: rtl + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%m/%d/%Y" + short: "%b %d" + long: "%B %d, %Y" + + day_names: [الاحد, الاثنين, الثلاثاء, الاربعاء, الخميس, الجمعة, السبت] + abbr_day_names: [أح, اث, Ø«, ار, Ø®, ج, س] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, كانون الثاني, شباط, آذار, نيسان, أيار, حزيران, تموز, آب, أيلول, تشرين الأول, تشرين الثاني, كانون الأول] + abbr_month_names: [~, كانون الثاني, شباط, آذار, نيسان, أيار, حزيران, تموز, آب, أيلول, تشرين الأول, تشرين الثاني, كانون الأول] + # Used in date_select and datime_select. + order: + - :السنة + - :الشهر + - :اليوم + + time: + formats: + default: "%m/%d/%Y %I:%M %p" + time: "%I:%M %p" + short: "%d %b %H:%M" + long: "%B %d, %Y %H:%M" + am: "صباحا" + pm: "مساءا" + + datetime: + distance_in_words: + half_a_minute: "نص٠دقيقة" + less_than_x_seconds: + one: "أقل من ثانية" + other: "ثواني %{count}أقل من " + x_seconds: + one: "ثانية" + other: "%{count}ثواني " + less_than_x_minutes: + one: "أقل من دقيقة" + other: "دقائق%{count}أقل من " + x_minutes: + one: "دقيقة" + other: "%{count} دقائق" + about_x_hours: + one: "حوالي ساعة" + other: "ساعات %{count}حوالي " + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "يوم" + other: "%{count} أيام" + about_x_months: + one: "حوالي شهر" + other: "أشهر %{count} حوالي" + x_months: + one: "شهر" + other: "%{count} أشهر" + about_x_years: + one: "حوالي سنة" + other: "سنوات %{count}حوالي " + over_x_years: + one: "اكثر من سنة" + other: "سنوات %{count}أكثر من " + almost_x_years: + one: "تقريبا سنة" + other: "سنوات %{count} نقريبا" + number: + format: + separator: "." + delimiter: "" + precision: 3 + + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + +# Used in array.to_sentence. + support: + array: + sentence_connector: "Ùˆ" + skip_last_comma: خطأ + + activerecord: + errors: + template: + header: + one: " %{model} خطأ يمنع تخزين" + other: " %{model} يمنع تخزين%{count}خطأ رقم " + messages: + inclusion: "غير مدرجة على القائمة" + exclusion: "محجوز" + invalid: "غير صالح" + confirmation: "غير متطابق" + accepted: "مقبولة" + empty: "لا يمكن ان تكون ÙØ§Ø±ØºØ©" + blank: "لا يمكن ان تكون ÙØ§Ø±ØºØ©" + too_long: " %{count}طويلة جدا، الحد الاقصى هو )" + too_short: " %{count}قصيرة جدا، الحد الادنى هو)" + wrong_length: " %{count}خطأ ÙÙŠ الطول، يجب ان يكون )" + taken: "لقد اتخذت سابقا" + not_a_number: "ليس رقما" + not_a_date: "ليس تاريخا صالحا" + greater_than: "%{count}يجب ان تكون اكثر من " + greater_than_or_equal_to: "%{count}يجب ان تكون اكثر من او تساوي" + equal_to: "%{count}يجب ان تساوي" + less_than: " %{count}يجب ان تكون اقل من" + less_than_or_equal_to: " %{count}يجب ان تكون اقل من او تساوي" + odd: "must be odd" + even: "must be even" + greater_than_start_date: "يجب ان تكون اكثر من تاريخ البداية" + not_same_project: "لا ينتمي الى Ù†ÙØ³ المشروع" + circular_dependency: "هذه العلاقة سو٠تخلق علاقة تبعية دائرية" + cant_link_an_issue_with_a_descendant: "لا يمكن ان تكون المشكلة مرتبطة بواحدة من المهام Ø§Ù„ÙØ±Ø¹ÙŠØ©" + + actionview_instancetag_blank_option: الرجاء التحديد + + general_text_No: 'لا' + general_text_Yes: 'نعم' + general_text_no: 'لا' + general_text_yes: 'نعم' + general_lang_name: 'Arabic (عربي)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: ISO-8859-1 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '7' + + notice_account_updated: لقد تم تجديد الحساب بنجاح. + notice_account_invalid_creditentials: اسم المستخدم او كلمة المرور غير صحيحة + notice_account_password_updated: لقد تم تجديد كلمة المرور بنجاح. + notice_account_wrong_password: كلمة المرور غير صحيحة + notice_account_register_done: لقد تم انشاء حسابك بنجاح، الرجاء تأكيد الطلب من البريد الالكتروني + notice_account_unknown_email: مستخدم غير معروÙ. + notice_can_t_change_password: هذا الحساب يستخدم جهاز خارجي غير مصرح به لا يمكن تغير كلمة المرور + notice_account_lost_email_sent: لقد تم ارسال رسالة على بريدك بالتعليمات اللازمة لتغير كلمة المرور + notice_account_activated: لقد تم ØªÙØ¹ÙŠÙ„ حسابك، يمكنك الدخول الان + notice_successful_create: لقد تم الانشاء بنجاح + notice_successful_update: لقد تم التحديث بنجاح + notice_successful_delete: لقد تم الحذ٠بنجاح + notice_successful_connection: لقد تم الربط بنجاح + notice_file_not_found: Ø§Ù„ØµÙØ­Ø© التي تحاول الدخول اليها غير موجوده او تم حذÙها + notice_locking_conflict: تم تحديث البيانات عن طريق مستخدم آخر. + notice_not_authorized: غير مصرح لك الدخول الى هذه المنطقة. + notice_not_authorized_archived_project: المشروع الذي تحاول الدخول اليه تم Ø§Ø±Ø´ÙØªÙ‡ + notice_email_sent: "%{value}تم ارسال رسالة الى " + notice_email_error: " (%{value})لقد حدث خطأ ما اثناء ارسال الرسالة الى " + notice_feeds_access_key_reseted: كلمة الدخول RSSلقد تم تعديل . + notice_api_access_key_reseted: كلمة الدخولAPIلقد تم تعديل . + notice_failed_to_save_issues: "ÙØ´Ù„ ÙÙŠ Ø­ÙØ¸ الملÙ" + notice_failed_to_save_members: "ÙØ´Ù„ ÙÙŠ Ø­ÙØ¸ الاعضاء: %{errors}." + notice_no_issue_selected: "لم يتم تحديد شيء، الرجاء تحديد المسألة التي تريد" + notice_account_pending: "لقد تم انشاء حسابك، الرجاء الانتظار حتى تتم المواÙقة" + notice_default_data_loaded: تم تحميل التكوين Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠ Ø¨Ù†Ø¬Ø§Ø­ + notice_unable_delete_version: غير قادر على مسح النسخة. + notice_unable_delete_time_entry: غير قادر على مسح وقت الدخول. + notice_issue_done_ratios_updated: لقد تم تحديث النسب. + notice_gantt_chart_truncated: " (%{max})لقد تم اقتطاع الرسم البياني لانه تجاوز الاحد الاقصى لعدد العناصر المسموح عرضها " + notice_issue_successful_create: "%{id}لقد تم انشاء " + + + error_can_t_load_default_data: "لم يتم تحميل التكوين Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠ ÙƒØ§Ù…Ù„Ø§ %{value}" + error_scm_not_found: "لم يتم العثور على ادخال ÙÙŠ المستودع" + error_scm_command_failed: "حدث خطأ عند محاولة الوصول الى المستودع: %{value}" + error_scm_annotate: "الادخال غير موجود." + error_scm_annotate_big_text_file: "لا يمكن Ø­ÙØ¸ الادخال لانه تجاوز الحد الاقصى لحجم الملÙ." + error_issue_not_found_in_project: 'لم يتم العثور على المخرج او انه ينتمي الى مشروع اخر' + error_no_tracker_in_project: 'لا يوجد متتبع لهذا المشروع، الرجاء التحقق من إعدادات المشروع. ' + error_no_default_issue_status: 'لم يتم التعر٠على اي وضع Ø§ÙØªØ±Ø§Ø¶ÙŠØŒ الرجاء التحقق من التكوين الخاص بك (اذهب الى إدارة-إصدار الحالات)' + error_can_not_delete_custom_field: غير قادر على حذ٠الحقل المظلل + error_can_not_delete_tracker: "هذا المتتبع يحتوي على مسائل نشطة ولا يمكن حذÙÙ‡" + error_can_not_remove_role: "هذا الدور قيد الاستخدام، لا يمكن حذÙÙ‡" + error_can_not_reopen_issue_on_closed_version: 'لا يمكن إعادة ÙØªØ­ قضية معينه لاصدار مقÙÙ„' + error_can_not_archive_project: لا يمكن Ø§Ø±Ø´ÙØ© هذا المشروع + error_issue_done_ratios_not_updated: "لم يتم تحديث النسب" + error_workflow_copy_source: 'الرجاء اختيار المتتبع او الادوار' + error_workflow_copy_target: 'الرجاء اختيار هد٠المتتبع او هد٠الادوار' + error_unable_delete_issue_status: 'غير قادر على حذ٠حالة القضية' + error_unable_to_connect: "تعذر الاتصال(%{value})" + error_attachment_too_big: " (%{max_size})لا يمكن تحميل هذا Ø§Ù„Ù…Ù„ÙØŒ لقد تجاوز الحد الاقصى المسموح به " + warning_attachments_not_saved: "%{count}تعذر Ø­ÙØ¸ الملÙ" + + mail_subject_lost_password: " %{value}كلمة المرور الخاصة بك " + mail_body_lost_password: 'لتغير كلمة المرور، انقر على الروابط التالية:' + mail_subject_register: " %{value}ØªÙØ¹ÙŠÙ„ حسابك " + mail_body_register: 'Ù„ØªÙØ¹ÙŠÙ„ حسابك، انقر على الروابط التالية:' + mail_body_account_information_external: " %{value}اصبح بامكانك استخدام حسابك للدخول" + mail_body_account_information: معلومات حسابك + mail_subject_account_activation_request: "%{value}طلب ØªÙØ¹ÙŠÙ„ الحساب " + mail_body_account_activation_request: " (%{value})تم تسجيل حساب جديد، بانتظار المواÙقة:" + mail_subject_reminder: "%{count}تم تأجيل المهام التالية " + mail_body_reminder: "%{count}يجب ان تقوم بتسليم المهام التالية :" + mail_subject_wiki_content_added: "'%{id}' تم Ø§Ø¶Ø§ÙØ© ØµÙØ­Ø© ويكي" + mail_body_wiki_content_added: "The '%{id}' تم Ø§Ø¶Ø§ÙØ© ØµÙØ­Ø© ويكي من قبل %{author}." + mail_subject_wiki_content_updated: "'%{id}' تم تحديث ØµÙØ­Ø© ويكي" + mail_body_wiki_content_updated: "The '%{id}'تم تحديث ØµÙØ­Ø© ويكي من قبل %{author}." + + gui_validation_error: خطأ + gui_validation_error_plural: "%{count}أخطاء" + + field_name: الاسم + field_description: الوص٠+ field_summary: الملخص + field_is_required: مطلوب + field_firstname: الاسم الاول + field_lastname: الاسم الاخير + field_mail: البريد الالكتروني + field_filename: اسم المل٠+ field_filesize: حجم المل٠+ field_downloads: التنزيل + field_author: المؤل٠+ field_created_on: تم الانشاء ÙÙŠ + field_updated_on: تم التحديث + field_field_format: تنسيق الحقل + field_is_for_all: لكل المشروعات + field_possible_values: قيم محتملة + field_regexp: التعبير العادي + field_min_length: الحد الادنى للطول + field_max_length: الحد الاعلى للطول + field_value: القيمة + field_category: Ø§Ù„ÙØ¦Ø© + field_title: العنوان + field_project: المشروع + field_issue: القضية + field_status: الحالة + field_notes: ملاحظات + field_is_closed: القضية مغلقة + field_is_default: القيمة Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠØ© + field_tracker: المتتبع + field_subject: الموضوع + field_due_date: تاريخ الاستحقاق + field_assigned_to: المحال اليه + field_priority: الأولوية + field_fixed_version: الاصدار المستهد٠+ field_user: المستخدم + field_principal: الرئيسي + field_role: دور + field_homepage: Ø§Ù„ØµÙØ­Ø© الرئيسية + field_is_public: عام + field_parent: مشروع ÙØ±Ø¹ÙŠ Ù…Ù† + field_is_in_roadmap: القضايا المعروضة ÙÙŠ خارطة الطريق + field_login: تسجيل الدخول + field_mail_notification: ملاحظات على البريد الالكتروني + field_admin: المدير + field_last_login_on: اخر اتصال + field_language: لغة + field_effective_date: تاريخ + field_password: كلمة المرور + field_new_password: كلمة المرور الجديدة + field_password_confirmation: تأكيد + field_version: إصدار + field_type: نوع + field_host: المضي٠+ field_port: Ø§Ù„Ù…Ù†ÙØ° + field_account: الحساب + field_base_dn: DN قاعدة + field_attr_login: سمة الدخول + field_attr_firstname: سمة الاسم الاول + field_attr_lastname: سمة الاسم الاخير + field_attr_mail: سمة البريد الالكتروني + field_onthefly: إنشاء حساب مستخدم على تحرك + field_start_date: تاريخ البدية + field_done_ratio: "% تم" + field_auth_source: وضع المصادقة + field_hide_mail: Ø¥Ø®ÙØ§Ø¡ بريدي الإلكتروني + field_comments: تعليق + field_url: رابط + field_start_page: ØµÙØ­Ø© البداية + field_subproject: المشروع Ø§Ù„ÙØ±Ø¹ÙŠ + field_hours: ساعات + field_activity: النشاط + field_spent_on: تاريخ + field_identifier: المعر٠+ field_is_filter: استخدم كتصÙية + field_issue_to: القضايا المتصلة + field_delay: تأخير + field_assignable: يمكن ان تستند القضايا الى هذا الدور + field_redirect_existing_links: إعادة توجيه الروابط الموجودة + field_estimated_hours: الوقت المتوقع + field_column_names: أعمدة + field_time_entries: وقت الدخول + field_time_zone: المنطقة الزمنية + field_searchable: يمكن البحث Ùيه + field_default_value: القيمة Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠØ© + field_comments_sorting: اعرض التعليقات + field_parent_title: ØµÙØ­Ø© الوالدين + field_editable: يمكن اعادة تحريره + field_watcher: مراقب + field_identity_url: Ø§ÙØªØ­ الرابط الخاص بالهوية الشخصية + field_content: المحتويات + field_group_by: مجموعة النتائج عن طريق + field_sharing: مشاركة + field_parent_issue: مهمة الوالدين + field_member_of_group: "مجموعة المحال" + field_assigned_to_role: "دور المحال" + field_text: حقل نصي + field_visible: غير مرئي + field_warn_on_leaving_unsaved: "الرجاء التحذير عند مغادرة ØµÙØ­Ø© والنص غير محÙوظ" + field_issues_visibility: القضايا المرئية + field_is_private: خاص + field_commit_logs_encoding: رسائل الترميز + field_scm_path_encoding: ترميز المسار + field_path_to_repository: مسار المستودع + field_root_directory: دليل الجذر + field_cvsroot: CVSجذر + field_cvs_module: وحدة + + setting_app_title: عنوان التطبيق + setting_app_subtitle: العنوان Ø§Ù„ÙØ±Ø¹ÙŠ Ù„Ù„ØªØ·Ø¨ÙŠÙ‚ + setting_welcome_text: نص الترحيب + setting_default_language: اللغة Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠØ© + setting_login_required: مطلوب المصادقة + setting_self_registration: التسجيل الذاتي + setting_attachment_max_size: الحد الاقصى Ù„Ù„Ù…Ù„ÙØ§Øª المرÙقة + setting_issues_export_limit: الحد الاقصى لقضايا التصدير + setting_mail_from: انبعاثات عنوان بريدك + setting_bcc_recipients: مستلمين النسخ المخÙية (bcc) + setting_plain_text_mail: نص عادي (no HTML) + setting_host_name: اسم ومسار المستخدم + setting_text_formatting: تنسيق النص + setting_wiki_compression: ضغط تاريخ الويكي + setting_feeds_limit: Atom feeds الحد الاقصى لعدد البنود ÙÙŠ + setting_default_projects_public: المشاريع الجديده متاحة للجميع Ø§ÙØªØ±Ø§Ø¶ÙŠØ§ + setting_autofetch_changesets: الإحضار التلقائي + setting_sys_api_enabled: من ادارة المستودع WS تمكين + setting_commit_ref_keywords: مرجعية الكلمات Ø§Ù„Ù…ÙØªØ§Ø­ÙŠØ© + setting_commit_fix_keywords: تصحيح الكلمات Ø§Ù„Ù…ÙØªØ§Ø­ÙŠØ© + setting_autologin: الدخول التلقائي + setting_date_format: تنسيق التاريخ + setting_time_format: تنسيق الوقت + setting_cross_project_issue_relations: السماح بادارج القضايا ÙÙŠ هذا المشروع + setting_issue_list_default_columns: الاعمدة Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠØ© المعروضة ÙÙŠ قائمة القضية + setting_repositories_encodings: ترميز المرÙقات والمستودعات + setting_emails_header: رأس رسائل البريد الإلكتروني + setting_emails_footer: ذيل رسائل البريد الإلكتروني + setting_protocol: بروتوكول + setting_per_page_options: الكائنات لكل خيارات Ø§Ù„ØµÙØ­Ø© + setting_user_format: تنسيق عرض المستخدم + setting_activity_days_default: الايام المعروضة على نشاط المشروع + setting_display_subprojects_issues: عرض القضايا Ø§Ù„ÙØ±Ø¹ÙŠØ© للمشارع الرئيسية بشكل Ø§ÙØªØ±Ø§Ø¶ÙŠ + setting_enabled_scm: SCM تمكين + setting_mail_handler_body_delimiters: "اقتطاع رسائل البريد الإلكتروني بعد هذه الخطوط" + setting_mail_handler_api_enabled: للرسائل الواردةWS تمكين + setting_mail_handler_api_key: API Ù…ÙØªØ§Ø­ + setting_sequential_project_identifiers: انشاء Ù…Ø¹Ø±ÙØ§Øª المشروع المتسلسلة + setting_gravatar_enabled: كأيقونة مستخدمGravatar استخدام + setting_gravatar_default: Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠØ©Gravatar صورة + setting_diff_max_lines_displayed: الحد الاقصى لعدد الخطوط + setting_file_max_size_displayed: الحد الأقصى لحجم النص المعروض على Ø§Ù„Ù…Ù„ÙØ§Øª المرÙقة + setting_repository_log_display_limit: الحد الاقصى لعدد التنقيحات المعروضة على مل٠السجل + setting_openid: السماح بدخول اسم المستخدم Ø§Ù„Ù…ÙØªÙˆØ­ والتسجيل + setting_password_min_length: الحد الادني لطول كلمة المرور + setting_new_project_user_role_id: الدور المسند الى المستخدم غير المسؤول الذي يقوم بإنشاء المشروع + setting_default_projects_modules: تمكين الوحدات النمطية للمشاريع الجديدة بشكل Ø§ÙØªØ±Ø§Ø¶ÙŠ + setting_issue_done_ratio: حساب نسبة القضية المنتهية + setting_issue_done_ratio_issue_field: استخدم حقل القضية + setting_issue_done_ratio_issue_status: استخدم وضع القضية + setting_start_of_week: بدأ التقويم + setting_rest_api_enabled: تمكين باقي خدمات الويب + setting_cache_formatted_text: النص المسبق تنسيقه ÙÙŠ ذاكرة التخزين المؤقت + setting_default_notification_option: خيار الاعلام Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠ + setting_commit_logtime_enabled: تميكن وقت الدخول + setting_commit_logtime_activity_id: النشاط ÙÙŠ وقت الدخول + setting_gantt_items_limit: الحد الاقصى لعدد العناصر المعروضة على المخطط + setting_issue_group_assignment: السماح للإحالة الى المجموعات + setting_default_issue_start_date_to_creation_date: استخدام التاريخ الحالي كتاريخ بدأ للقضايا الجديدة + + permission_add_project: إنشاء مشروع + permission_add_subprojects: إنشاء مشاريع ÙØ±Ø¹ÙŠØ© + permission_edit_project: تعديل مشروع + permission_select_project_modules: تحديد شكل المشروع + permission_manage_members: إدارة الاعضاء + permission_manage_project_activities: ادارة اصدارات المشروع + permission_manage_versions: ادارة الاصدارات + permission_manage_categories: ادارة انواع القضايا + permission_view_issues: عرض القضايا + permission_add_issues: Ø§Ø¶Ø§ÙØ© القضايا + permission_edit_issues: تعديل القضايا + permission_manage_issue_relations: ادارة علاقات القضايا + permission_set_issues_private: تعين قضايا عامة او خاصة + permission_set_own_issues_private: تعين القضايا الخاصة بك كقضايا عامة او خاصة + permission_add_issue_notes: Ø§Ø¶Ø§ÙØ© ملاحظات + permission_edit_issue_notes: تعديل ملاحظات + permission_edit_own_issue_notes: تعديل ملاحظاتك + permission_move_issues: تحريك القضايا + permission_delete_issues: حذ٠القضايا + permission_manage_public_queries: ادارة الاستعلامات العامة + permission_save_queries: Ø­ÙØ¸ الاستعلامات + permission_view_gantt: عرض طريقة"جانت" + permission_view_calendar: عرض التقويم + permission_view_issue_watchers: عرض قائمة المراقبين + permission_add_issue_watchers: Ø§Ø¶Ø§ÙØ© مراقبين + permission_delete_issue_watchers: حذ٠مراقبين + permission_log_time: الوقت المستغرق بالدخول + permission_view_time_entries: عرض الوقت المستغرق + permission_edit_time_entries: تعديل الدخولات الزمنية + permission_edit_own_time_entries: تعديل الدخولات الشخصية + permission_manage_news: ادارة الاخبار + permission_comment_news: اخبار التعليقات + permission_manage_documents: ادارة المستندات + permission_view_documents: عرض المستندات + permission_manage_files: ادارة Ø§Ù„Ù…Ù„ÙØ§Øª + permission_view_files: عرض Ø§Ù„Ù…Ù„ÙØ§Øª + permission_manage_wiki: ادارة ويكي + permission_rename_wiki_pages: اعادة تسمية ØµÙØ­Ø§Øª ويكي + permission_delete_wiki_pages: حذق ØµÙØ­Ø§Øª ويكي + permission_view_wiki_pages: عرض ويكي + permission_view_wiki_edits: عرض تاريخ ويكي + permission_edit_wiki_pages: تعديل ØµÙØ­Ø§Øª ويكي + permission_delete_wiki_pages_attachments: حذ٠المرÙقات + permission_protect_wiki_pages: حماية ØµÙØ­Ø§Øª ويكي + permission_manage_repository: ادارة المستودعات + permission_browse_repository: استعراض المستودعات + permission_view_changesets: عرض طاقم التغيير + permission_commit_access: الوصول + permission_manage_boards: ادارة المنتديات + permission_view_messages: عرض الرسائل + permission_add_messages: نشر الرسائل + permission_edit_messages: تحرير الرسائل + permission_edit_own_messages: تحرير الرسائل الخاصة + permission_delete_messages: حذ٠الرسائل + permission_delete_own_messages: حذ٠الرسائل الخاصة + permission_export_wiki_pages: تصدير ØµÙØ­Ø§Øª ويكي + permission_manage_subtasks: ادارة المهام Ø§Ù„ÙØ±Ø¹ÙŠØ© + + project_module_issue_tracking: تعقب القضايا + project_module_time_tracking: التعقب الزمني + project_module_news: الاخبار + project_module_documents: المستندات + project_module_files: Ø§Ù„Ù…Ù„ÙØ§Øª + project_module_wiki: ويكي + project_module_repository: المستودع + project_module_boards: المنتديات + project_module_calendar: التقويم + project_module_gantt: جانت + + label_user: المستخدم + label_user_plural: المستخدمين + label_user_new: مستخدم جديد + label_user_anonymous: مجهول الهوية + label_project: مشروع + label_project_new: مشروع جديد + label_project_plural: مشاريع + label_x_projects: + zero: لا يوجد مشاريع + one: مشروع واحد + other: "%{count} مشاريع" + label_project_all: كل المشاريع + label_project_latest: احدث المشاريع + label_issue: قضية + label_issue_new: قضية جديدة + label_issue_plural: قضايا + label_issue_view_all: عرض كل القضايا + label_issues_by: " %{value}القضية لصحابها" + label_issue_added: تم Ø§Ø¶Ø§ÙØ© القضية + label_issue_updated: تم تحديث القضية + label_issue_note_added: تم Ø§Ø¶Ø§ÙØ© الملاحظة + label_issue_status_updated: تم تحديث الحالة + label_issue_priority_updated: تم تحديث الاولويات + label_document: مستند + label_document_new: مستند جديد + label_document_plural: مستندات + label_document_added: تم Ø§Ø¶Ø§ÙØ© مستند + label_role: دور + label_role_plural: ادوار + label_role_new: دور جديد + label_role_and_permissions: الادوار والاذن + label_role_anonymous: مجهول الهوية + label_role_non_member: ليس عضو + label_member: عضو + label_member_new: عضو جديد + label_member_plural: اعضاء + label_tracker: المتتبع + label_tracker_plural: المتتبعين + label_tracker_new: متتبع جديد + label_workflow: سير العمل + label_issue_status: وضع القضية + label_issue_status_plural: اوضاع القضية + label_issue_status_new: وضع جديد + label_issue_category: نوع القضية + label_issue_category_plural: انواع القضايا + label_issue_category_new: نوع جديد + label_custom_field: تخصيص حقل + label_custom_field_plural: تخصيص حقول + label_custom_field_new: حقل مخصص جديد + label_enumerations: التعدادات + label_enumeration_new: قيمة جديدة + label_information: معلومة + label_information_plural: معلومات + label_please_login: برجى تسجيل الدخول + label_register: تسجيل + label_login_with_open_id_option: او الدخول بهوية Ù…ÙØªÙˆØ­Ø© + label_password_lost: Ùقدت كلمة السر + label_home: Ø§Ù„ØµÙØ­Ø© الرئيسية + label_my_page: Ø§Ù„ØµÙØ­Ø© الخاصة بي + label_my_account: حسابي + label_my_projects: مشاريعي الخاصة + label_my_page_block: حجب ØµÙØ­ØªÙŠ Ø§Ù„Ø®Ø§ØµØ© + label_administration: الإدارة + label_login: تسجيل الدخول + label_logout: تسجيل الخروج + label_help: مساعدة + label_reported_issues: أبلغ القضايا + label_assigned_to_me_issues: المسائل المعنية إلى + label_last_login: آخر اتصال + label_registered_on: مسجل على + label_activity: النشاط + label_overall_activity: النشاط العام + label_user_activity: "قيمة النشاط" + label_new: جديدة + label_logged_as: تم تسجيل دخولك + label_environment: البيئة + label_authentication: المصادقة + label_auth_source: وضع المصادقة + label_auth_source_new: وضع مصادقة جديدة + label_auth_source_plural: أوضاع المصادقة + label_subproject_plural: مشاريع ÙØ±Ø¹ÙŠØ© + label_subproject_new: مشروع ÙØ±Ø¹ÙŠ Ø¬Ø¯ÙŠØ¯ + label_and_its_subprojects: "قيمةالمشاريع Ø§Ù„ÙØ±Ø¹ÙŠØ© الخاصة بك" + label_min_max_length: الحد الاقصى والادنى للطول + label_list: قائمة + label_date: تاريخ + label_integer: عدد صحيح + label_float: تعويم + label_boolean: منطقية + label_string: النص + label_text: نص طويل + label_attribute: سمة + label_attribute_plural: السمات + label_download: "تحميل" + label_download_plural: "تحميل" + label_no_data: لا توجد بيانات للعرض + label_change_status: تغيير الوضع + label_history: التاريخ + label_attachment: المل٠+ label_attachment_new: مل٠جديد + label_attachment_delete: حذ٠المل٠+ label_attachment_plural: Ø§Ù„Ù…Ù„ÙØ§Øª + label_file_added: المل٠المضا٠+ label_report: تقرير + label_report_plural: التقارير + label_news: الأخبار + label_news_new: Ø¥Ø¶Ø§ÙØ© الأخبار + label_news_plural: الأخبار + label_news_latest: آخر الأخبار + label_news_view_all: عرض كل الأخبار + label_news_added: الأخبار Ø§Ù„Ù…Ø¶Ø§ÙØ© + label_news_comment_added: Ø¥Ø¶Ø§ÙØ© التعليقات على أخبار + label_settings: إعدادات + label_overview: لمحة عامة + label_version: الإصدار + label_version_new: الإصدار الجديد + label_version_plural: الإصدارات + label_close_versions: أكملت إغلاق الإصدارات + label_confirmation: تأكيد + label_export_to: 'Ù…ØªÙˆÙØ±Ø© أيضا ÙÙŠ:' + label_read: القراءة... + label_public_projects: المشاريع العامة + label_open_issues: ÙØªØ­ قضية + label_open_issues_plural: ÙØªØ­ قضايا + label_closed_issues: قضية مغلقة + label_closed_issues_plural: قضايا مغلقة + label_x_open_issues_abbr_on_total: + zero: 0 Ù…ÙØªÙˆØ­ / %{total} + one: 1 Ù…ÙØªÙˆØ­ / %{total} + other: "%{count} Ù…ÙØªÙˆØ­ / %{total}" + label_x_open_issues_abbr: + zero: 0 Ù…ÙØªÙˆØ­ + one: 1 مقتوح + other: "%{count} Ù…ÙØªÙˆØ­" + label_x_closed_issues_abbr: + zero: 0 مغلق + one: 1 مغلق + other: "%{count} مغلق" + label_total: الإجمالي + label_permissions: أذونات + label_current_status: الوضع الحالي + label_new_statuses_allowed: يسمح بادراج حالات جديدة + label_all: جميع + label_none: لا شيء + label_nobody: لا أحد + label_next: القادم + label_previous: السابق + label_used_by: التي يستخدمها + label_details: Ø§Ù„ØªÙØ§ØµÙŠÙ„ + label_add_note: Ø¥Ø¶Ø§ÙØ© ملاحظة + label_per_page: كل ØµÙØ­Ø© + label_calendar: التقويم + label_months_from: بعد أشهر من + label_gantt: جانت + label_internal: الداخلية + label_last_changes: "آخر التغييرات %{count}" + label_change_view_all: عرض ÙƒØ§ÙØ© التغييرات + label_personalize_page: تخصيص هذه Ø§Ù„ØµÙØ­Ø© + label_comment: تعليق + label_comment_plural: تعليقات + label_x_comments: + zero: لا يوجد تعليقات + one: تعليق واحد + other: "%{count} تعليقات" + label_comment_add: Ø¥Ø¶Ø§ÙØ© تعليق + label_comment_added: تم Ø¥Ø¶Ø§ÙØ© التعليق + label_comment_delete: حذ٠التعليقات + label_query: استعلام مخصص + label_query_plural: استعلامات مخصصة + label_query_new: استعلام جديد + label_my_queries: استعلاماتي المخصصة + label_filter_add: Ø¥Ø¶Ø§ÙØ© عامل تصÙية + label_filter_plural: عوامل التصÙية + label_equals: يساوي + label_not_equals: لا يساوي + label_in_less_than: ÙÙŠ أقل من + label_in_more_than: ÙÙŠ أكثر من + label_greater_or_equal: '>=' + label_less_or_equal: '< =' + label_between: بين + label_in: ÙÙŠ + label_today: اليوم + label_all_time: كل الوقت + label_yesterday: بالأمس + label_this_week: هذا الأسبوع + label_last_week: الأسبوع الماضي + label_last_n_days: "ايام %{count} اخر" + label_this_month: هذا الشهر + label_last_month: الشهر الماضي + label_this_year: هذا العام + label_date_range: نطاق التاريخ + label_less_than_ago: أقل من قبل أيام + label_more_than_ago: أكثر من قبل أيام + label_ago: منذ أيام + label_contains: يحتوي على + label_not_contains: لا يحتوي على + label_day_plural: أيام + label_repository: المستودع + label_repository_plural: المستودعات + label_browse: ØªØµÙØ­ + label_modification: "%{count} تغير" + label_modification_plural: "%{count}تغيرات " + label_branch: ÙØ±Ø¹ + label_tag: ربط + label_revision: مراجعة + label_revision_plural: تنقيحات + label_revision_id: " %{value}مراجعة" + label_associated_revisions: التنقيحات المرتبطة + label_added: Ø¥Ø¶Ø§ÙØ© + label_modified: تعديل + label_copied: نسخ + label_renamed: إعادة تسمية + label_deleted: حذ٠+ label_latest_revision: آخر تنقيح + label_latest_revision_plural: أحدث المراجعات + label_view_revisions: عرض التنقيحات + label_view_all_revisions: عرض ÙƒØ§ÙØ© المراجعات + label_max_size: الحد الأقصى للحجم + label_sort_highest: التحرك إلى أعلى + label_sort_higher: تحريك لأعلى + label_sort_lower: تحريك لأسÙÙ„ + label_sort_lowest: الانتقال إلى أسÙÙ„ + label_roadmap: خارطة الطريق + label_roadmap_due_in: " %{value}تستحق ÙÙŠ " + label_roadmap_overdue: "%{value}تأخير" + label_roadmap_no_issues: لا يوجد قضايا لهذا الإصدار + label_search: البحث + label_result_plural: النتائج + label_all_words: كل الكلمات + label_wiki: ويكي + label_wiki_edit: تحرير ويكي + label_wiki_edit_plural: عمليات تحرير ويكي + label_wiki_page: ØµÙØ­Ø© ويكي + label_wiki_page_plural: ويكي ØµÙØ­Ø§Øª + label_index_by_title: الÙهرس حسب العنوان + label_index_by_date: الÙهرس حسب التاريخ + label_current_version: الإصدار الحالي + label_preview: معاينة + label_feed_plural: موجز ويب + label_changes_details: ØªÙØ§ØµÙŠÙ„ جميع التغييرات + label_issue_tracking: تعقب القضايا + label_spent_time: أمضى بعض الوقت + label_overall_spent_time: الوقت الذي تم Ø§Ù†ÙØ§Ù‚Ù‡ كاملا + label_f_hour: "%{value} ساعة" + label_f_hour_plural: "%{value} ساعات" + label_time_tracking: تعقب الوقت + label_change_plural: التغييرات + label_statistics: إحصاءات + label_commits_per_month: يثبت ÙÙŠ الشهر + label_commits_per_author: يثبت لكل مؤل٠+ label_diff: Ø§Ù„Ø§Ø®ØªÙ„Ø§ÙØ§Øª + label_view_diff: عرض Ø§Ù„Ø§Ø®ØªÙ„Ø§ÙØ§Øª + label_diff_inline: مضمنة + label_diff_side_by_side: جنبا إلى جنب + label_options: خيارات + label_copy_workflow_from: نسخ سير العمل من + label_permissions_report: تقرير أذونات + label_watched_issues: شاهد القضايا + label_related_issues: القضايا ذات الصلة + label_applied_status: تطبيق مركز + label_loading: تحميل... + label_relation_new: علاقة جديدة + label_relation_delete: حذ٠العلاقة + label_relates_to: ذات الصلة إلى + label_duplicates: التكرارات + label_duplicated_by: ازدواج + label_blocks: حظر + label_blocked_by: حظر بواسطة + label_precedes: يسبق + label_follows: يتبع + label_end_to_start: نهاية لبدء + label_end_to_end: نهاية إلى نهاية + label_start_to_start: بدء إلى بدء + label_start_to_end: بداية لنهاية + label_stay_logged_in: تسجيل الدخول ÙÙŠ + label_disabled: تعطيل + label_show_completed_versions: أكملت إظهار إصدارات + label_me: لي + label_board: المنتدى + label_board_new: منتدى جديد + label_board_plural: المنتديات + label_board_locked: تأمين + label_board_sticky: لزجة + label_topic_plural: المواضيع + label_message_plural: رسائل + label_message_last: آخر رسالة + label_message_new: رسالة جديدة + label_message_posted: تم Ø§Ø¶Ø§ÙØ© الرسالة + label_reply_plural: الردود + label_send_information: إرسال معلومات الحساب للمستخدم + label_year: سنة + label_month: شهر + label_week: أسبوع + label_date_from: من + label_date_to: إلى + label_language_based: استناداً إلى لغة المستخدم + label_sort_by: " %{value}الترتيب حسب " + label_send_test_email: ارسل رسالة الكترونية كاختبار + label_feeds_access_key: RSS Ù…ÙØªØ§Ø­ دخول + label_missing_feeds_access_key: Ù…ÙقودRSS Ù…ÙØªØ§Ø­ دخول + label_feeds_access_key_created_on: "RSS تم انشاء Ù…ÙØªØ§Ø­ %{value} منذ" + label_module_plural: الوحدات النمطية + label_added_time_by: " تم Ø§Ø¶Ø§ÙØªÙ‡ من قبل%{author} %{age} منذ" + label_updated_time_by: " تم تحديثه من قبل%{author} %{age} منذ" + label_updated_time: "تم التحديث %{value} منذ" + label_jump_to_a_project: الانتقال إلى مشروع... + label_file_plural: Ø§Ù„Ù…Ù„ÙØ§Øª + label_changeset_plural: اعدادات التغير + label_default_columns: الاعمدة Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠØ© + label_no_change_option: (أي تغيير) + label_bulk_edit_selected_issues: تحرير القضايا المظللة + label_bulk_edit_selected_time_entries: تعديل كل الإدخالات ÙÙŠ كل الاوقات + label_theme: الموضوع + label_default: Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠ + label_search_titles_only: البحث ÙÙŠ العناوين Ùقط + label_user_mail_option_all: "جميع الخيارات" + label_user_mail_option_selected: "الخيارات المظللة Ùقط" + label_user_mail_option_none: "لم يتم تحديد اي خيارات" + label_user_mail_option_only_my_events: "السماح لي Ùقط بمشاهدة الاحداث الخاصة" + label_user_mail_option_only_assigned: "Ùقط الخيارات التي تم تعيينها" + label_user_mail_option_only_owner: "Ùقط للخيارات التي املكها" + label_user_mail_no_self_notified: "لا تريد اعلامك بالتغيرات التي تجريها Ø¨Ù†ÙØ³Ùƒ" + label_registration_activation_by_email: حساب التنشيط عبر البريد الإلكتروني + label_registration_manual_activation: تنشيط الحساب اليدوي + label_registration_automatic_activation: تنشيط الحساب التلقائي + label_display_per_page: "لكل ØµÙØ­Ø©: %{value}" + label_age: العمر + label_change_properties: تغيير الخصائص + label_general: عامة + label_more: أكثر + label_scm: scm + label_plugins: Ø§Ù„Ø¥Ø¶Ø§ÙØ§Øª + label_ldap_authentication: مصادقة LDAP + label_downloads_abbr: D/L + label_optional_description: وص٠اختياري + label_add_another_file: Ø¥Ø¶Ø§ÙØ© مل٠آخر + label_preferences: ØªÙØ¶ÙŠÙ„ات + label_chronological_order: ÙÙŠ ترتيب زمني + label_reverse_chronological_order: ÙÙŠ ترتيب زمني عكسي + label_planning: التخطيط + label_incoming_emails: رسائل البريد الإلكتروني الوارد + label_generate_key: إنشاء Ù…ÙØªØ§Ø­ + label_issue_watchers: المراقبون + label_example: مثال + label_display: العرض + label_sort: ÙØ±Ø² + label_ascending: تصاعدي + label_descending: تنازلي + label_date_from_to: من %{start} الى %{end} + label_wiki_content_added: Ø¥Ø¶Ø§ÙØ© ØµÙØ­Ø© ويكي + label_wiki_content_updated: تحديث ØµÙØ­Ø© ويكي + label_group: مجموعة + label_group_plural: المجموعات + label_group_new: مجموعة جديدة + label_time_entry_plural: أمضى بعض الوقت + label_version_sharing_none: لم يشارك + label_version_sharing_descendants: يشارك + label_version_sharing_hierarchy: مع التسلسل الهرمي للمشروع + label_version_sharing_tree: مع شجرة المشروع + label_version_sharing_system: مع جميع المشاريع + label_update_issue_done_ratios: تحديث قضيةالنسب + label_copy_source: مصدر + label_copy_target: الهد٠+ label_copy_same_as_target: Ù†ÙØ³ الهد٠+ label_display_used_statuses_only: عرض الحالات المستخدمة من قبل هذا "تعقب" Ùقط + label_api_access_key: Ù…ÙØªØ§Ø­ الوصول إلى API + label_missing_api_access_key: API لم يتم الحصول على Ù…ÙØªØ§Ø­ الوصول + label_api_access_key_created_on: " API إنشاء Ù…ÙØªØ§Ø­ الوصول إلى" + label_profile: المل٠الشخصي + label_subtask_plural: المهام Ø§Ù„ÙØ±Ø¹ÙŠØ© + label_project_copy_notifications: إرسال إشعار الى البريد الإلكتروني عند نسخ المشروع + label_principal_search: "البحث عن مستخدم أو مجموعة:" + label_user_search: "البحث عن المستخدم:" + label_additional_workflow_transitions_for_author: الانتقالات الإضاÙية المسموح بها عند المستخدم صاحب البلاغ + label_additional_workflow_transitions_for_assignee: الانتقالات الإضاÙية المسموح بها عند المستخدم المحال إليه + label_issues_visibility_all: جميع القضايا + label_issues_visibility_public: جميع القضايا الخاصة + label_issues_visibility_own: القضايا التي أنشأها المستخدم + label_git_report_last_commit: اعتماد التقرير الأخير Ù„Ù„Ù…Ù„ÙØ§Øª والدلائل + label_parent_revision: الوالدين + label_child_revision: الطÙÙ„ + label_export_options: "%{export_format} خيارات التصدير" + + button_login: دخول + button_submit: تثبيت + button_save: Ø­ÙØ¸ + button_check_all: نحديد الكل + button_uncheck_all: عدم تحديد الكل + button_collapse_all: تقليص الكل + button_expand_all: عرض الكل + button_delete: حذ٠+ button_create: انشاء + button_create_and_continue: انشاء واستمرار + button_test: اختبار + button_edit: تعديل + button_edit_associated_wikipage: "تغير ØµÙØ­Ø© ويكي: %{page_title}" + button_add: Ø§Ø¶Ø§ÙØ© + button_change: تغير + button_apply: تطبيق + button_clear: واضح + button_lock: Ù‚ÙÙ„ + button_unlock: الغاء القÙÙ„ + button_download: تنزيل + button_list: قائمة + button_view: عرض + button_move: تحرك + button_move_and_follow: تحرك واتبع + button_back: رجوع + button_cancel: إلغاء + button_activate: تنشيط + button_sort: ترتيب + button_log_time: وقت الدخول + button_rollback: الرجوع الى هذا الاصدار + button_watch: يشاهد + button_unwatch: إلغاء المشاهدة + button_reply: رد + button_archive: الارشي٠+ button_unarchive: إلغاء Ø§Ù„Ø§Ø±Ø´ÙØ© + button_reset: إعادة + button_rename: إعادة التسمية + button_change_password: تغير كلمة المرور + button_copy: نسخ + button_copy_and_follow: نسخ واتباع + button_annotate: تعليق + button_update: تحديث + button_configure: تكوين + button_quote: يقتبس + button_duplicate: يضاع٠+ button_show: يظهر + button_edit_section: يعدل هذا الجزء + button_export: يستورد + + status_active: نشيط + status_registered: مسجل + status_locked: مقÙÙ„ + + version_status_open: Ù…ÙØªÙˆØ­ + version_status_locked: مقÙÙ„ + version_status_closed: مغلق + + field_active: ÙØ¹Ø§Ù„ + + text_select_mail_notifications: حدد الامور التي يجب ابلاغك بها عن طريق البريد الالكتروني + text_regexp_info: مثال. ^[A-Z0-9]+$ + text_min_max_length_info: الحد الاقصى والادني لطول المعلومات + text_project_destroy_confirmation: هل أنت متأكد من أنك تريد حذ٠هذا المشروع والبيانات ذات الصلة؟ + text_subprojects_destroy_warning: "subproject(s): سيتم حذ٠أيضا." + text_workflow_edit: حدد دوراً وتعقب لتحرير سير العمل + text_are_you_sure: هل أنت متأكد؟ + text_journal_changed: "%{label} تغير %{old} الى %{new}" + text_journal_changed_no_detail: "%{label} تم التحديث" + text_journal_set_to: "%{label} تغير الى %{value}" + text_journal_deleted: "%{label} تم الحذ٠(%{old})" + text_journal_added: "%{label} %{value} تم Ø§Ù„Ø§Ø¶Ø§ÙØ©" + text_tip_issue_begin_day: قضية بدأت اليوم + text_tip_issue_end_day: قضية انتهت اليوم + text_tip_issue_begin_end_day: قضية بدأت وانتهت اليوم + text_caracters_maximum: "%{count} الحد الاقصى." + text_caracters_minimum: "الحد الادنى %{count}" + text_length_between: "الطول %{min} بين %{max} رمز" + text_tracker_no_workflow: لم يتم تحديد سير العمل لهذا المتتبع + text_unallowed_characters: رموز غير مسموحة + text_comma_separated: مسموح رموز متنوعة ÙŠÙØµÙ„ها ÙØ§ØµÙ„Ø© . + text_line_separated: مسموح رموز متنوعة ÙŠÙØµÙ„ها سطور + text_issues_ref_in_commit_messages: الرجوع واصلاح القضايا ÙÙŠ رسائل المشتكين + text_issue_added: "القضية %{id} تم ابلاغها عن طريق %{author}." + text_issue_updated: "القضية %{id} تم تحديثها عن طريق %{author}." + text_wiki_destroy_confirmation: هل انت متأكد من رغبتك ÙÙŠ حذ٠هذا الويكي ومحتوياته؟ + text_issue_category_destroy_question: "بعض القضايا (%{count}) مرتبطة بهذه Ø§Ù„ÙØ¦Ø©ØŒ ماذا تريد ان ØªÙØ¹Ù„ بها؟" + text_issue_category_destroy_assignments: Ø­Ø°Ù Ø§Ù„ÙØ¦Ø© + text_issue_category_reassign_to: اعادة تثبيت البنود ÙÙŠ Ø§Ù„ÙØ¦Ø© + text_user_mail_option: "بالنسبة للمشاريع غير المحددة، سو٠يتم ابلاغك عن المشاريع التي تشاهدها او تشارك بها Ùقط!" + text_no_configuration_data: "الادوار والمتتبع وحالات القضية ومخطط سير العمل لم يتم تحديد وضعها Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠ Ø¨Ø¹Ø¯. " + text_load_default_configuration: احمل الاعدادات Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠØ© + text_status_changed_by_changeset: " طبق التغيرات المعينة على %{value}." + text_time_logged_by_changeset: "تم تطبيق التغيرات المعينة على %{value}." + text_issues_destroy_confirmation: هل انت متأكد من حذ٠البنود المظللة؟' + text_issues_destroy_descendants_confirmation: "سو٠يؤدي هذا الى حذ٠%{count} المهام Ø§Ù„ÙØ±Ø¹ÙŠØ© ايضا." + text_time_entries_destroy_confirmation: "هل انت متأكد من رغبتك ÙÙŠ حذ٠الادخالات الزمنية المحددة؟" + text_select_project_modules: قم بتحديد الوضع المناسب لهذا المشروع:' + text_default_administrator_account_changed: تم تعديل الاعدادات Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠØ© لحساب المدير + text_file_repository_writable: المرÙقات قابلة للكتابة + text_plugin_assets_writable: الدليل المساعد قابل للكتابة + text_destroy_time_entries_question: " ساعة على القضية التي تود حذÙها، ماذا تريد ان ØªÙØ¹Ù„ØŸ %{hours} تم تثبيت" + text_destroy_time_entries: قم بحذ٠الساعات المسجلة + text_assign_time_entries_to_project: ثبت الساعات المسجلة على التقرير + text_reassign_time_entries: 'اعادة تثبيت الساعات المسجلة لهذه القضية:' + text_user_wrote: "%{value} كتب:" + text_enumeration_destroy_question: "%{count} الكائنات المعنية لهذه القيمة" + text_enumeration_category_reassign_to: اعادة تثبيت الكائنات التالية لهذه القيمة:' + text_email_delivery_not_configured: "لم يتم تسليم البريد الالكتروني" + text_diff_truncated: '... لقد تم اقتطلع هذا الجزء لانه تجاوز الحد الاقصى المسموح بعرضه' + text_custom_field_possible_values_info: 'سطر لكل قيمة' + text_wiki_page_nullify_children: "Ø§Ù„Ø§Ø­ØªÙØ§Ø¸ Ø¨ØµÙØ­Ø§Øª الطÙÙ„ ÙƒØµÙØ­Ø§Øª جذر" + text_wiki_page_destroy_children: "Ø­Ø°Ù ØµÙØ­Ø§Øª الطÙÙ„ وجميع أولادهم" + text_wiki_page_reassign_children: "إعادة تعيين ØµÙØ­Ø§Øª تابعة لهذه Ø§Ù„ØµÙØ­Ø© الأصلية" + text_own_membership_delete_confirmation: "انت على وشك إزالة بعض أو ÙƒØ§ÙØ© الأذونات الخاصة بك، لن تكون قادراً على تحرير هذا المشروع بعد ذلك. هل أنت متأكد من أنك تريد المتابعة؟" + text_zoom_in: تصغير + text_zoom_out: تكبير + text_warn_on_leaving_unsaved: "Ø§Ù„ØµÙØ­Ø© تحتوي على نص غير مخزن، سو٠يÙقد النص اذا تم الخروج من Ø§Ù„ØµÙØ­Ø©." + text_scm_path_encoding_note: "Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠ: UTF-8" + text_git_repository_note: مستودع ÙØ§Ø±Øº ومحلي + text_mercurial_repository_note: مستودع محلي + text_scm_command: امر + text_scm_command_version: اصدار + text_scm_config: الرجاء اعادة تشغيل التطبيق + text_scm_command_not_available: الامر غير Ù…ØªÙˆÙØ±ØŒ الرجاء التحقق من لوحة التحكم + + default_role_manager: مدير + default_role_developer: مطور + default_role_reporter: مراسل + default_tracker_bug: الشوائب + default_tracker_feature: خاصية + default_tracker_support: دعم + default_issue_status_new: جديد + default_issue_status_in_progress: جاري التحميل + default_issue_status_resolved: الحل + default_issue_status_feedback: التغذية الراجعة + default_issue_status_closed: مغلق + default_issue_status_rejected: مرÙوض + default_doc_category_user: مستندات المستخدم + default_doc_category_tech: المستندات التقنية + default_priority_low: قليل + default_priority_normal: عادي + default_priority_high: عالي + default_priority_urgent: طارئ + default_priority_immediate: مباشرة + default_activity_design: تصميم + default_activity_development: تطوير + + enumeration_issue_priorities: الاولويات + enumeration_doc_categories: تصني٠المستندات + enumeration_activities: الانشطة + enumeration_system_activity: نشاط النظام + description_filter: Ùلترة + description_search: حقل البحث + description_choose_project: مشاريع + description_project_scope: مجال البحث + description_notes: ملاحظات + description_message_content: محتويات الرسالة + description_query_sort_criteria_attribute: نوع الترتيب + description_query_sort_criteria_direction: اتجاه الترتيب + description_user_mail_notification: إعدادات البريد الالكتروني + description_available_columns: الاعمدة Ø§Ù„Ù…ØªÙˆÙØ±Ø© + description_selected_columns: الاعمدة المحددة + description_all_columns: كل الاعمدة + description_issue_category_reassign: اختر التصني٠+ description_wiki_subpages_reassign: اختر ØµÙØ­Ø© جديدة + description_date_range_list: اختر المجال من القائمة + description_date_range_interval: اختر المدة عن طريق اختيار تاريخ البداية والنهاية + description_date_from: ادخل تاريخ البداية + description_date_to: ادخل تاريخ الانتهاء + text_rmagick_available: RMagick available (optional) + text_wiki_page_destroy_question: This page has %{descendants} child page(s) and descendant(s). What do you want to do? + text_repository_usernames_mapping: |- + Select or update the Redmine user mapped to each username found in the repository log. + Users with the same Redmine and repository username or email are automatically mapped. + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 قضية + one: 1 قضية + other: "%{count} قضايا" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: جميع + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: يشارك + label_cross_project_tree: مع شجرة المشروع + label_cross_project_hierarchy: مع التسلسل الهرمي للمشروع + label_cross_project_system: مع جميع المشاريع + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4a/4ab5b67d5253cebf9b8f519318b769978fbb9a97.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4a/4ab5b67d5253cebf9b8f519318b769978fbb9a97.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,9 @@ +

    <%=l(:label_issue_category_new)%>

    + +<%= labelled_form_for @category, :as => 'issue_category', :url => project_issue_categories_path(@project), :remote => true do |f| %> +<%= render :partial => 'issue_categories/form', :locals => { :f => f } %> +

    + <%= submit_tag l(:button_create), :name => nil %> + <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %> +

    +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4a/4aea9e1c65822b8a8b3989a99a10ff20323a66ea.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4a/4aea9e1c65822b8a8b3989a99a10ff20323a66ea.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1089 @@ +# Portuguese localization for Ruby on Rails +# by Ricardo Otero +# by Alberto Ferreira +# by Rui Rebelo +pt: + support: + array: + sentence_connector: "e" + skip_last_comma: true + + direction: ltr + date: + formats: + default: "%d/%m/%Y" + short: "%d de %B" + long: "%d de %B de %Y" + only_day: "%d" + day_names: [Domingo, Segunda, Terça, Quarta, Quinta, Sexta, Sábado] + abbr_day_names: [Dom, Seg, Ter, Qua, Qui, Sex, Sáb] + month_names: [~, Janeiro, Fevereiro, Março, Abril, Maio, Junho, Julho, Agosto, Setembro, Outubro, Novembro, Dezembro] + abbr_month_names: [~, Jan, Fev, Mar, Abr, Mai, Jun, Jul, Ago, Set, Out, Nov, Dez] + order: + - :day + - :month + - :year + + time: + formats: + default: "%A, %d de %B de %Y, %H:%Mh" + time: "%H:%M" + short: "%d/%m, %H:%M hs" + long: "%A, %d de %B de %Y, %H:%Mh" + am: '' + pm: '' + + datetime: + distance_in_words: + half_a_minute: "meio minuto" + less_than_x_seconds: + one: "menos de 1 segundo" + other: "menos de %{count} segundos" + x_seconds: + one: "1 segundo" + other: "%{count} segundos" + less_than_x_minutes: + one: "menos de um minuto" + other: "menos de %{count} minutos" + x_minutes: + one: "1 minuto" + other: "%{count} minutos" + about_x_hours: + one: "aproximadamente 1 hora" + other: "aproximadamente %{count} horas" + x_hours: + one: "1 hora" + other: "%{count} horas" + x_days: + one: "1 dia" + other: "%{count} dias" + about_x_months: + one: "aproximadamente 1 mês" + other: "aproximadamente %{count} meses" + x_months: + one: "1 mês" + other: "%{count} meses" + about_x_years: + one: "aproximadamente 1 ano" + other: "aproximadamente %{count} anos" + over_x_years: + one: "mais de 1 ano" + other: "mais de %{count} anos" + almost_x_years: + one: "quase 1 ano" + other: "quase %{count} anos" + + number: + format: + precision: 3 + separator: ',' + delimiter: '.' + currency: + format: + unit: '€' + precision: 2 + format: "%u %n" + separator: ',' + delimiter: '.' + percentage: + format: + delimiter: '' + precision: + format: + delimiter: '' + human: + format: + precision: 3 + delimiter: '' + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + + activerecord: + errors: + template: + header: + one: "Não foi possível guardar %{model}: 1 erro" + other: "Não foi possível guardar %{model}: %{count} erros" + body: "Por favor, verifique os seguintes campos:" + messages: + inclusion: "não está incluído na lista" + exclusion: "não está disponível" + invalid: "não é válido" + confirmation: "não está de acordo com a confirmação" + accepted: "precisa de ser aceite" + empty: "não pode estar em branco" + blank: "não pode estar em branco" + too_long: "tem demasiados caracteres (máximo: %{count} caracteres)" + too_short: "tem poucos caracteres (mínimo: %{count} caracteres)" + wrong_length: "não é do tamanho correcto (necessita de ter %{count} caracteres)" + taken: "não está disponível" + not_a_number: "não é um número" + greater_than: "tem de ser maior do que %{count}" + greater_than_or_equal_to: "tem de ser maior ou igual a %{count}" + equal_to: "tem de ser igual a %{count}" + less_than: "tem de ser menor do que %{count}" + less_than_or_equal_to: "tem de ser menor ou igual a %{count}" + odd: "tem de ser ímpar" + even: "tem de ser par" + greater_than_start_date: "deve ser maior que a data inicial" + not_same_project: "não pertence ao mesmo projecto" + circular_dependency: "Esta relação iria criar uma dependência circular" + cant_link_an_issue_with_a_descendant: "Não é possível ligar uma tarefa a uma sub-tarefa que lhe é pertencente" + + ## Translated by: Pedro Araújo + actionview_instancetag_blank_option: Seleccione + + general_text_No: 'Não' + general_text_Yes: 'Sim' + general_text_no: 'não' + general_text_yes: 'sim' + general_lang_name: 'Português' + general_csv_separator: ';' + general_csv_decimal_separator: ',' + general_csv_encoding: ISO-8859-15 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: A conta foi actualizada com sucesso. + notice_account_invalid_creditentials: Utilizador ou palavra-chave inválidos. + notice_account_password_updated: A palavra-chave foi alterada com sucesso. + notice_account_wrong_password: Palavra-chave errada. + notice_account_register_done: A conta foi criada com sucesso. + notice_account_unknown_email: Utilizador desconhecido. + notice_can_t_change_password: Esta conta utiliza uma fonte de autenticação externa. Não é possível alterar a palavra-chave. + notice_account_lost_email_sent: Foi-lhe enviado um e-mail com as instruções para escolher uma nova palavra-chave. + notice_account_activated: A sua conta foi activada. É agora possível autenticar-se. + notice_successful_create: Criado com sucesso. + notice_successful_update: Alterado com sucesso. + notice_successful_delete: Apagado com sucesso. + notice_successful_connection: Ligado com sucesso. + notice_file_not_found: A página que está a tentar aceder não existe ou foi removida. + notice_locking_conflict: Os dados foram actualizados por outro utilizador. + notice_not_authorized: Não está autorizado a visualizar esta página. + notice_email_sent: "Foi enviado um e-mail para %{value}" + notice_email_error: "Ocorreu um erro ao enviar o e-mail (%{value})" + notice_feeds_access_key_reseted: A sua chave de RSS foi inicializada. + notice_failed_to_save_issues: "Não foi possível guardar %{count} tarefa(s) das %{total} seleccionadas: %{ids}." + notice_no_issue_selected: "Nenhuma tarefa seleccionada! Por favor, seleccione as tarefas que quer editar." + notice_account_pending: "A sua conta foi criada e está agora à espera de aprovação do administrador." + notice_default_data_loaded: Configuração padrão carregada com sucesso. + notice_unable_delete_version: Não foi possível apagar a versão. + + error_can_t_load_default_data: "Não foi possível carregar a configuração padrão: %{value}" + error_scm_not_found: "A entrada ou revisão não foi encontrada no repositório." + error_scm_command_failed: "Ocorreu um erro ao tentar aceder ao repositório: %{value}" + error_scm_annotate: "A entrada não existe ou não pode ser anotada." + error_issue_not_found_in_project: 'A tarefa não foi encontrada ou não pertence a este projecto.' + + mail_subject_lost_password: "Palavra-chave de %{value}" + mail_body_lost_password: 'Para mudar a sua palavra-chave, clique na ligação abaixo:' + mail_subject_register: "Activação de conta de %{value}" + mail_body_register: 'Para activar a sua conta, clique na ligação abaixo:' + mail_body_account_information_external: "Pode utilizar a conta %{value} para autenticar-se." + mail_body_account_information: Informação da sua conta + mail_subject_account_activation_request: "Pedido de activação da conta %{value}" + mail_body_account_activation_request: "Um novo utilizador (%{value}) registou-se. A sua conta está à espera de aprovação:" + mail_subject_reminder: "%{count} tarefa(s) para entregar nos próximos %{days} dias" + mail_body_reminder: "%{count} tarefa(s) que estão atribuídas a si estão agendadas para estarem completas nos próximos %{days} dias:" + + gui_validation_error: 1 erro + gui_validation_error_plural: "%{count} erros" + + field_name: Nome + field_description: Descrição + field_summary: Sumário + field_is_required: Obrigatório + field_firstname: Nome + field_lastname: Apelido + field_mail: E-mail + field_filename: Ficheiro + field_filesize: Tamanho + field_downloads: Downloads + field_author: Autor + field_created_on: Criado + field_updated_on: Alterado + field_field_format: Formato + field_is_for_all: Para todos os projectos + field_possible_values: Valores possíveis + field_regexp: Expressão regular + field_min_length: Tamanho mínimo + field_max_length: Tamanho máximo + field_value: Valor + field_category: Categoria + field_title: Título + field_project: Projecto + field_issue: Tarefa + field_status: Estado + field_notes: Notas + field_is_closed: Tarefa fechada + field_is_default: Valor por omissão + field_tracker: Tipo + field_subject: Assunto + field_due_date: Data fim + field_assigned_to: Atribuído a + field_priority: Prioridade + field_fixed_version: Versão + field_user: Utilizador + field_role: Função + field_homepage: Página + field_is_public: Público + field_parent: Sub-projecto de + field_is_in_roadmap: Tarefas mostradas no mapa de planificação + field_login: Nome de utilizador + field_mail_notification: Notificações por e-mail + field_admin: Administrador + field_last_login_on: Última visita + field_language: Língua + field_effective_date: Data + field_password: Palavra-chave + field_new_password: Nova palavra-chave + field_password_confirmation: Confirmação + field_version: Versão + field_type: Tipo + field_host: Servidor + field_port: Porta + field_account: Conta + field_base_dn: Base DN + field_attr_login: Atributo utilizador + field_attr_firstname: Atributo nome próprio + field_attr_lastname: Atributo último nome + field_attr_mail: Atributo e-mail + field_onthefly: Criação imediata de utilizadores + field_start_date: Data início + field_done_ratio: "% Completo" + field_auth_source: Modo de autenticação + field_hide_mail: Esconder endereço de e-mail + field_comments: Comentário + field_url: URL + field_start_page: Página inicial + field_subproject: Subprojecto + field_hours: Horas + field_activity: Actividade + field_spent_on: Data + field_identifier: Identificador + field_is_filter: Usado como filtro + field_issue_to: Tarefa relacionada + field_delay: Atraso + field_assignable: As tarefas podem ser associadas a esta função + field_redirect_existing_links: Redireccionar ligações existentes + field_estimated_hours: Tempo estimado + field_column_names: Colunas + field_time_zone: Fuso horário + field_searchable: Procurável + field_default_value: Valor por omissão + field_comments_sorting: Mostrar comentários + field_parent_title: Página pai + + setting_app_title: Título da aplicação + setting_app_subtitle: Sub-título da aplicação + setting_welcome_text: Texto de boas vindas + setting_default_language: Língua por omissão + setting_login_required: Autenticação obrigatória + setting_self_registration: Auto-registo + setting_attachment_max_size: Tamanho máximo do anexo + setting_issues_export_limit: Limite de exportação das tarefas + setting_mail_from: E-mail enviado de + setting_bcc_recipients: Recipientes de BCC + setting_host_name: Hostname + setting_text_formatting: Formatação do texto + setting_wiki_compression: Compressão do histórico do Wiki + setting_feeds_limit: Limite de conteúdo do feed + setting_default_projects_public: Projectos novos são públicos por omissão + setting_autofetch_changesets: Buscar automaticamente commits + setting_sys_api_enabled: Activar Web Service para gestão do repositório + setting_commit_ref_keywords: Palavras-chave de referência + setting_commit_fix_keywords: Palavras-chave de fecho + setting_autologin: Login automático + setting_date_format: Formato da data + setting_time_format: Formato do tempo + setting_cross_project_issue_relations: Permitir relações entre tarefas de projectos diferentes + setting_issue_list_default_columns: Colunas na lista de tarefas por omissão + setting_emails_footer: Rodapé do e-mails + setting_protocol: Protocolo + setting_per_page_options: Opções de objectos por página + setting_user_format: Formato de apresentaão de utilizadores + setting_activity_days_default: Dias mostrados na actividade do projecto + setting_display_subprojects_issues: Mostrar as tarefas dos sub-projectos nos projectos principais + setting_enabled_scm: Activar SCM + setting_mail_handler_api_enabled: Activar Web Service para e-mails recebidos + setting_mail_handler_api_key: Chave da API + setting_sequential_project_identifiers: Gerar identificadores de projecto sequênciais + + project_module_issue_tracking: Tarefas + project_module_time_tracking: Registo de tempo + project_module_news: Notícias + project_module_documents: Documentos + project_module_files: Ficheiros + project_module_wiki: Wiki + project_module_repository: Repositório + project_module_boards: Forum + + label_user: Utilizador + label_user_plural: Utilizadores + label_user_new: Novo utilizador + label_project: Projecto + label_project_new: Novo projecto + label_project_plural: Projectos + label_x_projects: + zero: no projects + one: 1 project + other: "%{count} projects" + label_project_all: Todos os projectos + label_project_latest: Últimos projectos + label_issue: Tarefa + label_issue_new: Nova tarefa + label_issue_plural: Tarefas + label_issue_view_all: Ver todas as tarefas + label_issues_by: "Tarefas por %{value}" + label_issue_added: Tarefa adicionada + label_issue_updated: Tarefa actualizada + label_document: Documento + label_document_new: Novo documento + label_document_plural: Documentos + label_document_added: Documento adicionado + label_role: Função + label_role_plural: Funções + label_role_new: Nova função + label_role_and_permissions: Funções e permissões + label_member: Membro + label_member_new: Novo membro + label_member_plural: Membros + label_tracker: Tipo + label_tracker_plural: Tipos + label_tracker_new: Novo tipo + label_workflow: Fluxo de trabalho + label_issue_status: Estado da tarefa + label_issue_status_plural: Estados da tarefa + label_issue_status_new: Novo estado + label_issue_category: Categoria de tarefa + label_issue_category_plural: Categorias de tarefa + label_issue_category_new: Nova categoria + label_custom_field: Campo personalizado + label_custom_field_plural: Campos personalizados + label_custom_field_new: Novo campo personalizado + label_enumerations: Enumerações + label_enumeration_new: Novo valor + label_information: Informação + label_information_plural: Informações + label_please_login: Por favor autentique-se + label_register: Registar + label_password_lost: Perdi a palavra-chave + label_home: Página Inicial + label_my_page: Página Pessoal + label_my_account: Minha conta + label_my_projects: Meus projectos + label_administration: Administração + label_login: Entrar + label_logout: Sair + label_help: Ajuda + label_reported_issues: Tarefas criadas + label_assigned_to_me_issues: Tarefas atribuídas a mim + label_last_login: Último acesso + label_registered_on: Registado em + label_activity: Actividade + label_overall_activity: Actividade geral + label_new: Novo + label_logged_as: Ligado como + label_environment: Ambiente + label_authentication: Autenticação + label_auth_source: Modo de autenticação + label_auth_source_new: Novo modo de autenticação + label_auth_source_plural: Modos de autenticação + label_subproject_plural: Sub-projectos + label_and_its_subprojects: "%{value} e sub-projectos" + label_min_max_length: Tamanho mínimo-máximo + label_list: Lista + label_date: Data + label_integer: Inteiro + label_float: Decimal + label_boolean: Booleano + label_string: Texto + label_text: Texto longo + label_attribute: Atributo + label_attribute_plural: Atributos + label_download: "%{count} Download" + label_download_plural: "%{count} Downloads" + label_no_data: Sem dados para mostrar + label_change_status: Mudar estado + label_history: Histórico + label_attachment: Ficheiro + label_attachment_new: Novo ficheiro + label_attachment_delete: Apagar ficheiro + label_attachment_plural: Ficheiros + label_file_added: Ficheiro adicionado + label_report: Relatório + label_report_plural: Relatórios + label_news: Notícia + label_news_new: Nova notícia + label_news_plural: Notícias + label_news_latest: Últimas notícias + label_news_view_all: Ver todas as notícias + label_news_added: Notícia adicionada + label_settings: Configurações + label_overview: Visão geral + label_version: Versão + label_version_new: Nova versão + label_version_plural: Versões + label_confirmation: Confirmação + label_export_to: 'Também disponível em:' + label_read: Ler... + label_public_projects: Projectos públicos + label_open_issues: aberto + label_open_issues_plural: abertos + label_closed_issues: fechado + label_closed_issues_plural: fechados + label_x_open_issues_abbr_on_total: + zero: 0 abertas / %{total} + one: 1 aberta / %{total} + other: "%{count} abertas / %{total}" + label_x_open_issues_abbr: + zero: 0 abertas + one: 1 aberta + other: "%{count} abertas" + label_x_closed_issues_abbr: + zero: 0 fechadas + one: 1 fechada + other: "%{count} fechadas" + label_total: Total + label_permissions: Permissões + label_current_status: Estado actual + label_new_statuses_allowed: Novos estados permitidos + label_all: todos + label_none: nenhum + label_nobody: ninguém + label_next: Próximo + label_previous: Anterior + label_used_by: Usado por + label_details: Detalhes + label_add_note: Adicionar nota + label_per_page: Por página + label_calendar: Calendário + label_months_from: meses de + label_gantt: Gantt + label_internal: Interno + label_last_changes: "últimas %{count} alterações" + label_change_view_all: Ver todas as alterações + label_personalize_page: Personalizar esta página + label_comment: Comentário + label_comment_plural: Comentários + label_x_comments: + zero: sem comentários + one: 1 comentário + other: "%{count} comentários" + label_comment_add: Adicionar comentário + label_comment_added: Comentário adicionado + label_comment_delete: Apagar comentários + label_query: Consulta personalizada + label_query_plural: Consultas personalizadas + label_query_new: Nova consulta + label_filter_add: Adicionar filtro + label_filter_plural: Filtros + label_equals: é + label_not_equals: não é + label_in_less_than: em menos de + label_in_more_than: em mais de + label_in: em + label_today: hoje + label_all_time: sempre + label_yesterday: ontem + label_this_week: esta semana + label_last_week: semana passada + label_last_n_days: "últimos %{count} dias" + label_this_month: este mês + label_last_month: mês passado + label_this_year: este ano + label_date_range: Date range + label_less_than_ago: menos de dias atrás + label_more_than_ago: mais de dias atrás + label_ago: dias atrás + label_contains: contém + label_not_contains: não contém + label_day_plural: dias + label_repository: Repositório + label_repository_plural: Repositórios + label_browse: Navegar + label_modification: "%{count} alteração" + label_modification_plural: "%{count} alterações" + label_revision: Revisão + label_revision_plural: Revisões + label_associated_revisions: Revisões associadas + label_added: adicionado + label_modified: modificado + label_copied: copiado + label_renamed: renomeado + label_deleted: apagado + label_latest_revision: Última revisão + label_latest_revision_plural: Últimas revisões + label_view_revisions: Ver revisões + label_max_size: Tamanho máximo + label_sort_highest: Mover para o início + label_sort_higher: Mover para cima + label_sort_lower: Mover para baixo + label_sort_lowest: Mover para o fim + label_roadmap: Planificação + label_roadmap_due_in: "Termina em %{value}" + label_roadmap_overdue: "Atrasado %{value}" + label_roadmap_no_issues: Sem tarefas para esta versão + label_search: Procurar + label_result_plural: Resultados + label_all_words: Todas as palavras + label_wiki: Wiki + label_wiki_edit: Edição da Wiki + label_wiki_edit_plural: Edições da Wiki + label_wiki_page: Página da Wiki + label_wiki_page_plural: Páginas da Wiki + label_index_by_title: Ãndice por título + label_index_by_date: Ãndice por data + label_current_version: Versão actual + label_preview: Pré-visualizar + label_feed_plural: Feeds + label_changes_details: Detalhes de todas as mudanças + label_issue_tracking: Tarefas + label_spent_time: Tempo gasto + label_f_hour: "%{value} hora" + label_f_hour_plural: "%{value} horas" + label_time_tracking: Registo de tempo + label_change_plural: Mudanças + label_statistics: Estatísticas + label_commits_per_month: Commits por mês + label_commits_per_author: Commits por autor + label_view_diff: Ver diferenças + label_diff_inline: inline + label_diff_side_by_side: lado a lado + label_options: Opções + label_copy_workflow_from: Copiar fluxo de trabalho de + label_permissions_report: Relatório de permissões + label_watched_issues: Tarefas observadas + label_related_issues: Tarefas relacionadas + label_applied_status: Estado aplicado + label_loading: A carregar... + label_relation_new: Nova relação + label_relation_delete: Apagar relação + label_relates_to: relacionado a + label_duplicates: duplica + label_duplicated_by: duplicado por + label_blocks: bloqueia + label_blocked_by: bloqueado por + label_precedes: precede + label_follows: segue + label_end_to_start: fim a início + label_end_to_end: fim a fim + label_start_to_start: início a início + label_start_to_end: início a fim + label_stay_logged_in: Guardar sessão + label_disabled: desactivado + label_show_completed_versions: Mostrar versões acabadas + label_me: eu + label_board: Forum + label_board_new: Novo forum + label_board_plural: Forums + label_topic_plural: Tópicos + label_message_plural: Mensagens + label_message_last: Última mensagem + label_message_new: Nova mensagem + label_message_posted: Mensagem adicionada + label_reply_plural: Respostas + label_send_information: Enviar dados da conta para o utilizador + label_year: Ano + label_month: mês + label_week: Semana + label_date_from: De + label_date_to: Para + label_language_based: Baseado na língua do utilizador + label_sort_by: "Ordenar por %{value}" + label_send_test_email: enviar um e-mail de teste + label_feeds_access_key_created_on: "Chave RSS criada há %{value} atrás" + label_module_plural: Módulos + label_added_time_by: "Adicionado por %{author} há %{age} atrás" + label_updated_time: "Alterado há %{value} atrás" + label_jump_to_a_project: Ir para o projecto... + label_file_plural: Ficheiros + label_changeset_plural: Changesets + label_default_columns: Colunas por omissão + label_no_change_option: (sem alteração) + label_bulk_edit_selected_issues: Editar tarefas seleccionadas em conjunto + label_theme: Tema + label_default: Padrão + label_search_titles_only: Procurar apenas em títulos + label_user_mail_option_all: "Para qualquer evento em todos os meus projectos" + label_user_mail_option_selected: "Para qualquer evento apenas nos projectos seleccionados..." + label_user_mail_no_self_notified: "Não quero ser notificado de alterações feitas por mim" + label_registration_activation_by_email: Activação da conta por e-mail + label_registration_manual_activation: Activação manual da conta + label_registration_automatic_activation: Activação automática da conta + label_display_per_page: "Por página: %{value}" + label_age: Idade + label_change_properties: Mudar propriedades + label_general: Geral + label_more: Mais + label_scm: SCM + label_plugins: Extensões + label_ldap_authentication: Autenticação LDAP + label_downloads_abbr: D/L + label_optional_description: Descrição opcional + label_add_another_file: Adicionar outro ficheiro + label_preferences: Preferências + label_chronological_order: Em ordem cronológica + label_reverse_chronological_order: Em ordem cronológica inversa + label_planning: Planeamento + label_incoming_emails: E-mails a chegar + label_generate_key: Gerar uma chave + label_issue_watchers: Observadores + + button_login: Entrar + button_submit: Submeter + button_save: Guardar + button_check_all: Marcar tudo + button_uncheck_all: Desmarcar tudo + button_delete: Apagar + button_create: Criar + button_test: Testar + button_edit: Editar + button_add: Adicionar + button_change: Alterar + button_apply: Aplicar + button_clear: Limpar + button_lock: Bloquear + button_unlock: Desbloquear + button_download: Download + button_list: Listar + button_view: Ver + button_move: Mover + button_back: Voltar + button_cancel: Cancelar + button_activate: Activar + button_sort: Ordenar + button_log_time: Tempo de trabalho + button_rollback: Voltar para esta versão + button_watch: Observar + button_unwatch: Deixar de observar + button_reply: Responder + button_archive: Arquivar + button_unarchive: Desarquivar + button_reset: Reinicializar + button_rename: Renomear + button_change_password: Mudar palavra-chave + button_copy: Copiar + button_annotate: Anotar + button_update: Actualizar + button_configure: Configurar + button_quote: Citar + + status_active: activo + status_registered: registado + status_locked: bloqueado + + text_select_mail_notifications: Seleccionar as acções que originam uma notificação por e-mail. + text_regexp_info: ex. ^[A-Z0-9]+$ + text_min_max_length_info: 0 siginifica sem restrição + text_project_destroy_confirmation: Tem a certeza que deseja apagar o projecto e todos os dados relacionados? + text_subprojects_destroy_warning: "O(s) seu(s) sub-projecto(s): %{value} também será/serão apagado(s)." + text_workflow_edit: Seleccione uma função e um tipo de tarefa para editar o fluxo de trabalho + text_are_you_sure: Tem a certeza? + text_tip_issue_begin_day: tarefa a começar neste dia + text_tip_issue_end_day: tarefa a acabar neste dia + text_tip_issue_begin_end_day: tarefa a começar e acabar neste dia + text_caracters_maximum: "máximo %{count} caracteres." + text_caracters_minimum: "Deve ter pelo menos %{count} caracteres." + text_length_between: "Deve ter entre %{min} e %{max} caracteres." + text_tracker_no_workflow: Sem fluxo de trabalho definido para este tipo de tarefa. + text_unallowed_characters: Caracteres não permitidos + text_comma_separated: Permitidos múltiplos valores (separados por vírgula). + text_issues_ref_in_commit_messages: Referenciando e fechando tarefas em mensagens de commit + text_issue_added: "Tarefa %{id} foi criada por %{author}." + text_issue_updated: "Tarefa %{id} foi actualizada por %{author}." + text_wiki_destroy_confirmation: Tem a certeza que deseja apagar este wiki e todo o seu conteúdo? + text_issue_category_destroy_question: "Algumas tarefas (%{count}) estão atribuídas a esta categoria. O que quer fazer?" + text_issue_category_destroy_assignments: Remover as atribuições à categoria + text_issue_category_reassign_to: Re-atribuir as tarefas para esta categoria + text_user_mail_option: "Para projectos não seleccionados, apenas receberá notificações acerca de coisas que está a observar ou está envolvido (ex. tarefas das quais foi o criador ou lhes foram atribuídas)." + text_no_configuration_data: "Perfis, tipos de tarefas, estados das tarefas e workflows ainda não foram configurados.\nÉ extremamente recomendado carregar as configurações padrão. Será capaz de as modificar depois de estarem carregadas." + text_load_default_configuration: Carregar as configurações padrão + text_status_changed_by_changeset: "Aplicado no changeset %{value}." + text_issues_destroy_confirmation: 'Tem a certeza que deseja apagar a(s) tarefa(s) seleccionada(s)?' + text_select_project_modules: 'Seleccione os módulos a activar para este projecto:' + text_default_administrator_account_changed: Conta default de administrador alterada. + text_file_repository_writable: Repositório de ficheiros com permissões de escrita + text_rmagick_available: RMagick disponível (opcional) + text_destroy_time_entries_question: "%{hours} horas de trabalho foram atribuídas a estas tarefas que vai apagar. O que deseja fazer?" + text_destroy_time_entries: Apagar as horas + text_assign_time_entries_to_project: Atribuir as horas ao projecto + text_reassign_time_entries: 'Re-atribuir as horas para esta tarefa:' + text_user_wrote: "%{value} escreveu:" + text_enumeration_destroy_question: "%{count} objectos estão atribuídos a este valor." + text_enumeration_category_reassign_to: 'Re-atribuí-los para este valor:' + text_email_delivery_not_configured: "Entrega por e-mail não está configurada, e as notificação estão desactivadas.\nConfigure o seu servidor de SMTP em config/configuration.yml e reinicie a aplicação para activar estas funcionalidades." + + default_role_manager: Gestor + default_role_developer: Programador + default_role_reporter: Repórter + default_tracker_bug: Bug + default_tracker_feature: Funcionalidade + default_tracker_support: Suporte + default_issue_status_new: Novo + default_issue_status_in_progress: Em curso + default_issue_status_resolved: Resolvido + default_issue_status_feedback: Feedback + default_issue_status_closed: Fechado + default_issue_status_rejected: Rejeitado + default_doc_category_user: Documentação de utilizador + default_doc_category_tech: Documentação técnica + default_priority_low: Baixa + default_priority_normal: Normal + default_priority_high: Alta + default_priority_urgent: Urgente + default_priority_immediate: Imediata + default_activity_design: Planeamento + default_activity_development: Desenvolvimento + + enumeration_issue_priorities: Prioridade de tarefas + enumeration_doc_categories: Categorias de documentos + enumeration_activities: Actividades (Registo de tempo) + setting_plain_text_mail: Apenas texto simples (sem HTML) + permission_view_files: Ver ficheiros + permission_edit_issues: Editar tarefas + permission_edit_own_time_entries: Editar horas pessoais + permission_manage_public_queries: Gerir queries públicas + permission_add_issues: Adicionar tarefas + permission_log_time: Registar tempo gasto + permission_view_changesets: Ver changesets + permission_view_time_entries: Ver tempo gasto + permission_manage_versions: Gerir versões + permission_manage_wiki: Gerir wiki + permission_manage_categories: Gerir categorias de tarefas + permission_protect_wiki_pages: Proteger páginas de wiki + permission_comment_news: Comentar notícias + permission_delete_messages: Apagar mensagens + permission_select_project_modules: Seleccionar módulos do projecto + permission_manage_documents: Gerir documentos + permission_edit_wiki_pages: Editar páginas de wiki + permission_add_issue_watchers: Adicionar observadores + permission_view_gantt: ver diagrama de Gantt + permission_move_issues: Mover tarefas + permission_manage_issue_relations: Gerir relações de tarefas + permission_delete_wiki_pages: Apagar páginas de wiki + permission_manage_boards: Gerir forums + permission_delete_wiki_pages_attachments: Apagar anexos + permission_view_wiki_edits: Ver histórico da wiki + permission_add_messages: Submeter mensagens + permission_view_messages: Ver mensagens + permission_manage_files: Gerir ficheiros + permission_edit_issue_notes: Editar notas de tarefas + permission_manage_news: Gerir notícias + permission_view_calendar: Ver calendário + permission_manage_members: Gerir membros + permission_edit_messages: Editar mensagens + permission_delete_issues: Apagar tarefas + permission_view_issue_watchers: Ver lista de observadores + permission_manage_repository: Gerir repositório + permission_commit_access: Acesso a submissão + permission_browse_repository: Navegar em repositório + permission_view_documents: Ver documentos + permission_edit_project: Editar projecto + permission_add_issue_notes: Adicionar notas a tarefas + permission_save_queries: Guardar queries + permission_view_wiki_pages: Ver wiki + permission_rename_wiki_pages: Renomear páginas de wiki + permission_edit_time_entries: Editar entradas de tempo + permission_edit_own_issue_notes: Editar as prórpias notas + setting_gravatar_enabled: Utilizar ícones Gravatar + label_example: Exemplo + text_repository_usernames_mapping: "Seleccionar ou actualizar o utilizador de Redmine mapeado a cada nome de utilizador encontrado no repositório.\nUtilizadores com o mesmo nome de utilizador ou email no Redmine e no repositório são mapeados automaticamente." + permission_edit_own_messages: Editar as próprias mensagens + permission_delete_own_messages: Apagar as próprias mensagens + label_user_activity: "Actividade de %{value}" + label_updated_time_by: "Actualizado por %{author} há %{age}" + text_diff_truncated: '... Este diff foi truncado porque excede o tamanho máximo que pode ser mostrado.' + setting_diff_max_lines_displayed: Número máximo de linhas de diff mostradas + text_plugin_assets_writable: Escrita na pasta de activos dos módulos de extensão possível + warning_attachments_not_saved: "Não foi possível gravar %{count} ficheiro(s) ." + button_create_and_continue: Criar e continuar + text_custom_field_possible_values_info: 'Uma linha para cada valor' + label_display: Mostrar + field_editable: Editável + setting_repository_log_display_limit: Número máximo de revisões exibido no relatório de ficheiro + setting_file_max_size_displayed: Tamanho máximo dos ficheiros de texto exibidos inline + field_watcher: Observador + setting_openid: Permitir início de sessão e registo com OpenID + field_identity_url: URL do OpenID + label_login_with_open_id_option: ou início de sessão com OpenID + field_content: Conteúdo + label_descending: Descendente + label_sort: Ordenar + label_ascending: Ascendente + label_date_from_to: De %{start} a %{end} + label_greater_or_equal: ">=" + label_less_or_equal: <= + text_wiki_page_destroy_question: Esta página tem %{descendants} página(s) subordinada(s) e descendente(s). O que deseja fazer? + text_wiki_page_reassign_children: Reatribuir páginas subordinadas a esta página principal + text_wiki_page_nullify_children: Manter páginas subordinadas como páginas raíz + text_wiki_page_destroy_children: Apagar as páginas subordinadas e todos os seus descendentes + setting_password_min_length: Tamanho mínimo de palavra-chave + field_group_by: Agrupar resultados por + mail_subject_wiki_content_updated: "A página Wiki '%{id}' foi actualizada" + label_wiki_content_added: Página Wiki adicionada + mail_subject_wiki_content_added: "A página Wiki '%{id}' foi adicionada" + mail_body_wiki_content_added: A página Wiki '%{id}' foi adicionada por %{author}. + label_wiki_content_updated: Página Wiki actualizada + mail_body_wiki_content_updated: A página Wiki '%{id}' foi actualizada por %{author}. + permission_add_project: Criar projecto + setting_new_project_user_role_id: Função atribuída a um utilizador não-administrador que cria um projecto + label_view_all_revisions: Ver todas as revisões + label_tag: Etiqueta + label_branch: Ramo + error_no_tracker_in_project: Este projecto não tem associado nenhum tipo de tarefas. Verifique as definições do projecto. + error_no_default_issue_status: Não está definido um estado padrão para as tarefas. Verifique a sua configuração (dirija-se a "Administração -> Estados da tarefa"). + label_group_plural: Grupos + label_group: Grupo + label_group_new: Novo grupo + label_time_entry_plural: Tempo registado + text_journal_changed: "%{label} alterado de %{old} para %{new}" + text_journal_set_to: "%{label} configurado como %{value}" + text_journal_deleted: "%{label} apagou (%{old})" + text_journal_added: "%{label} %{value} adicionado" + field_active: Activo + enumeration_system_activity: Actividade de sistema + permission_delete_issue_watchers: Apagar observadores + version_status_closed: fechado + version_status_locked: protegido + version_status_open: aberto + error_can_not_reopen_issue_on_closed_version: Não é possível voltar a abrir uma tarefa atribuída a uma versão fechada + label_user_anonymous: Anónimo + button_move_and_follow: Mover e seguir + setting_default_projects_modules: Módulos activos por predefinição para novos projectos + setting_gravatar_default: Imagem Gravatar predefinida + field_sharing: Partilha + label_version_sharing_hierarchy: Com hierarquia do projecto + label_version_sharing_system: Com todos os projectos + label_version_sharing_descendants: Com os sub-projectos + label_version_sharing_tree: Com árvore do projecto + label_version_sharing_none: Não partilhado + error_can_not_archive_project: Não é possível arquivar este projecto + button_duplicate: Duplicar + button_copy_and_follow: Copiar e seguir + label_copy_source: Origem + setting_issue_done_ratio: Calcular a percentagem de progresso da tarefa + setting_issue_done_ratio_issue_status: Através do estado da tarefa + error_issue_done_ratios_not_updated: Percentagens de progresso da tarefa não foram actualizadas. + error_workflow_copy_target: Seleccione os tipos de tarefas e funções desejadas + setting_issue_done_ratio_issue_field: Através do campo da tarefa + label_copy_same_as_target: Mesmo que o alvo + label_copy_target: Alvo + notice_issue_done_ratios_updated: Percentagens de progresso da tarefa actualizadas. + error_workflow_copy_source: Seleccione um tipo de tarefa ou função de origem + label_update_issue_done_ratios: Actualizar percentagens de progresso da tarefa + setting_start_of_week: Iniciar calendários a + permission_view_issues: Ver tarefas + label_display_used_statuses_only: Só exibir estados empregues por este tipo de tarefa + label_revision_id: Revisão %{value} + label_api_access_key: Chave de acesso API + label_api_access_key_created_on: Chave de acesso API criada há %{value} + label_feeds_access_key: Chave de acesso RSS + notice_api_access_key_reseted: A sua chave de acesso API foi reinicializada. + setting_rest_api_enabled: Activar serviço Web REST + label_missing_api_access_key: Chave de acesso API em falta + label_missing_feeds_access_key: Chave de acesso RSS em falta + button_show: Mostrar + text_line_separated: Vários valores permitidos (uma linha para cada valor). + setting_mail_handler_body_delimiters: Truncar mensagens de correio electrónico após uma destas linhas + permission_add_subprojects: Criar sub-projectos + label_subproject_new: Novo sub-projecto + text_own_membership_delete_confirmation: |- + Está prestes a eliminar parcial ou totalmente as suas permissões. É possível que não possa editar o projecto após esta acção. + Tem a certeza de que deseja continuar? + label_close_versions: Fechar versões completas + label_board_sticky: Fixar mensagem + label_board_locked: Proteger + permission_export_wiki_pages: Exportar páginas Wiki + setting_cache_formatted_text: Colocar formatação do texto na memória cache + permission_manage_project_activities: Gerir actividades do projecto + error_unable_delete_issue_status: Não foi possível apagar o estado da tarefa + label_profile: Perfil + permission_manage_subtasks: Gerir sub-tarefas + field_parent_issue: Tarefa principal + label_subtask_plural: Sub-tarefa + label_project_copy_notifications: Enviar notificações por e-mail durante a cópia do projecto + error_can_not_delete_custom_field: Não foi possível apagar o campo personalizado + error_unable_to_connect: Não foi possível ligar (%{value}) + error_can_not_remove_role: Esta função está actualmente em uso e não pode ser apagada. + error_can_not_delete_tracker: Existem ainda tarefas nesta categoria. Não é possível apagar este tipo de tarefa. + field_principal: Principal + label_my_page_block: Bloco da minha página + notice_failed_to_save_members: "Erro ao guardar o(s) membro(s): %{errors}." + text_zoom_out: Ampliar + text_zoom_in: Reduzir + notice_unable_delete_time_entry: Não foi possível apagar a entrada de tempo registado. + label_overall_spent_time: Total de tempo registado + field_time_entries: Tempo registado + project_module_gantt: Gantt + project_module_calendar: Calendário + button_edit_associated_wikipage: "Editar página Wiki associada: %{page_title}" + field_text: Campo de texto + label_user_mail_option_only_owner: Apenas para tarefas das quais sou proprietário + setting_default_notification_option: Opção predefinida de notificação + label_user_mail_option_only_my_events: Apenas para tarefas que observo ou em que estou envolvido + label_user_mail_option_only_assigned: Apenas para tarefas que me foram atribuídas + label_user_mail_option_none: Sem eventos + field_member_of_group: Grupo do detentor de atribuição + field_assigned_to_role: Papel do detentor de atribuição + notice_not_authorized_archived_project: O projecto a que tentou aceder foi arquivado. + label_principal_search: "Procurar utilizador ou grupo:" + label_user_search: "Procurar utilizador:" + field_visible: Visível + setting_emails_header: Cabeçalho dos e-mails + setting_commit_logtime_activity_id: Actividade para tempo registado + text_time_logged_by_changeset: Aplicado no conjunto de alterações %{value}. + setting_commit_logtime_enabled: Activar registo de tempo + notice_gantt_chart_truncated: O gráfico foi truncado porque excede o número máximo de itens visível (%{máx.}) + setting_gantt_items_limit: Número máximo de itens exibidos no gráfico Gantt + field_warn_on_leaving_unsaved: Avisar-me quando deixar uma página com texto por salvar + text_warn_on_leaving_unsaved: A página actual contém texto por salvar que será perdido caso saia desta página. + label_my_queries: As minhas consultas + text_journal_changed_no_detail: "%{label} actualizada" + label_news_comment_added: Comentário adicionado a uma notícia + button_expand_all: Expandir todos + button_collapse_all: Minimizar todos + label_additional_workflow_transitions_for_assignee: Transições adicionais permitidas quando a tarefa está atribuida ao utilizador + label_additional_workflow_transitions_for_author: Transições adicionais permitidas quando o utilizador é o autor da tarefa + label_bulk_edit_selected_time_entries: Edição em massa de registos de tempo + text_time_entries_destroy_confirmation: Têm a certeza que pretende apagar o(s) registo(s) de tempo selecionado(s)? + label_role_anonymous: Anónimo + label_role_non_member: Não membro + label_issue_note_added: Nota adicionada + label_issue_status_updated: Estado actualizado + label_issue_priority_updated: Prioridade adicionada + label_issues_visibility_own: Tarefas criadas ou atribuídas ao utilizador + field_issues_visibility: Visibilidade das tarefas + label_issues_visibility_all: Todas as tarefas + permission_set_own_issues_private: Configurar as suas tarefas como públicas ou privadas + field_is_private: Privado + permission_set_issues_private: Configurar tarefas como públicas ou privadas + label_issues_visibility_public: Todas as tarefas públicas + text_issues_destroy_descendants_confirmation: Irá apagar também %{count} subtarefa(s). + field_commit_logs_encoding: Codificação das mensagens de commit + field_scm_path_encoding: Codificação do caminho + text_scm_path_encoding_note: "Por omissão: UTF-8" + field_path_to_repository: Caminho para o repositório + field_root_directory: Raíz do directório + field_cvs_module: Módulo + field_cvsroot: CVSROOT + text_mercurial_repository_note: "Repositório local (ex: /hgrepo, c:\\hgrepo)" + text_scm_command: Comando + text_scm_command_version: Versão + label_git_report_last_commit: Analisar último commit por ficheiros e pastas + text_scm_config: Pode configurar os comando SCM em config/configuration.yml. Por favor reinicie a aplicação depois de alterar o ficheiro. + text_scm_command_not_available: O comando SCM não está disponível. Por favor verifique as configurações no painel de administração. + notice_issue_successful_create: Tarefa %{id} criada. + label_between: entre + setting_issue_group_assignment: Permitir atribuir tarefas a grupos + label_diff: diferença + text_git_repository_note: O repositório é local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Direcção da ordenação + description_project_scope: Âmbito da pesquisa + description_filter: Filtro + description_user_mail_notification: Configurações das notificações por email + description_date_from: Introduza data de início + description_message_content: Conteúdo da mensagem + description_available_columns: Colunas disponíveis + description_date_range_interval: Escolha o intervalo seleccionando a data de início e de fim + description_issue_category_reassign: Escolha a categoria da tarefa + description_search: Campo de pesquisa + description_notes: Notas + description_date_range_list: Escolha o intervalo da lista + description_choose_project: Projecto + description_date_to: Introduza data de fim + description_query_sort_criteria_attribute: Ordenar atributos + description_wiki_subpages_reassign: Escolha nova página pai + description_selected_columns: Colunas seleccionadas + label_parent_revision: Pai + label_child_revision: Filha + error_scm_annotate_big_text_file: Esta entrada não pode ser anotada, excede o tamanha máximo. + setting_default_issue_start_date_to_creation_date: Utilizar a data actual como data de início para novas tarefas + button_edit_section: Editar esta secção + setting_repositories_encodings: Codificação dos anexos e repositórios + description_all_columns: Todas as colunas + button_export: Exportar + label_export_options: "%{export_format} opções de exportação" + error_attachment_too_big: Este ficheiro não pode ser carregado pois excede o tamanho máximo permitido por ficheiro (%{max_size}) + notice_failed_to_save_time_entries: "Falha ao guardar %{count} registo(s) de tempo dos %{total} seleccionados: %{ids}." + label_x_issues: + zero: 0 tarefa + one: 1 tarefa + other: "%{count} tarefas" + label_repository_new: Novo repositório + field_repository_is_default: Repositório principal + label_copy_attachments: Copiar anexos + label_item_position: "%{position}/%{count}" + label_completed_versions: Versões completas + text_project_identifier_info: Apenas letras minúsculas (a-z), números, traços e sublinhados são permitidos.
    Depois de guardar não é possível alterar. + field_multiple: Múltiplos valores + setting_commit_cross_project_ref: Permitir que tarefas dos restantes projectos sejam referenciadas e resolvidas + text_issue_conflict_resolution_add_notes: Adicionar as minhas notas e descartar as minhas restantes alterações + text_issue_conflict_resolution_overwrite: Aplicar as minhas alterações (notas antigas serão mantidas mas algumas alterações podem se perder) + notice_issue_update_conflict: Esta tarefa foi actualizada por outro utilizador enquanto estava a edita-la. + text_issue_conflict_resolution_cancel: Descartar todas as minhas alterações e actualizar %{link} + permission_manage_related_issues: Gerir tarefas relacionadas + field_auth_source_ldap_filter: Filtro LDAP + label_search_for_watchers: Pesquisar por observadores para adicionar + notice_account_deleted: A sua conta foi apagada permanentemente. + setting_unsubscribe: Permitir aos utilizadores apagarem a sua própria conta + button_delete_my_account: Apagar a minha conta + text_account_destroy_confirmation: |- + Têm a certeza que pretende avançar? + A sua conta vai ser permanentemente apagada, não será possível recupera-la. + error_session_expired: A sua sessão expirou. Por-favor autentique-se novamente. + text_session_expiration_settings: "Atenção: alterar estas configurações pode fazer expirar as sessões em curso, incluíndo a sua." + setting_session_lifetime: Duração máxima da sessão + setting_session_timeout: Tempo limite de inactividade da sessão + label_session_expiration: Expiração da sessão + permission_close_project: Fechar / re-abrir o projecto + label_show_closed_projects: Ver os projectos fechados + button_close: Fechar + button_reopen: Re-abrir + project_status_active: activo + project_status_closed: fechado + project_status_archived: arquivado + text_project_closed: Este projecto está fechado e é apenas de leitura. + notice_user_successful_create: Utilizador %{id} criado. + field_core_fields: Campos padrão + field_timeout: Tempo limite (em segundos) + setting_thumbnails_enabled: Apresentar miniaturas dos anexos + setting_thumbnails_size: Tamanho das miniaturas (em pixeis) + label_status_transitions: Estado das transições + label_fields_permissions: Permissões do campo + label_readonly: Apenas de leitura + label_required: Obrigatório + text_repository_identifier_info: Apenas letras minúsculas (a-z), números, traços e sublinhados são permitidos.
    Depois de guardar não é possível alterar. + field_board_parent: Fórum pai + label_attribute_of_project: "%{name} do Projecto" + label_attribute_of_author: "%{name} do Autor" + label_attribute_of_assigned_to: "%{name} do atribuído" + label_attribute_of_fixed_version: "%{name} da Versão" + label_copy_subtasks: Copiar sub-tarefas + label_copied_to: copiado para + label_copied_from: copiado de + label_any_issues_in_project: tarefas do projecto + label_any_issues_not_in_project: tarefas sem projecto + field_private_notes: Notas privadas + permission_view_private_notes: Ver notas privadas + permission_set_notes_private: Configurar notas como privadas + label_no_issues_in_project: sem tarefas no projecto + label_any: todos + label_last_n_weeks: últimas %{count} semanas + setting_cross_project_subtasks: Permitir sub-tarefas entre projectos + label_cross_project_descendants: Com os sub-projectos + label_cross_project_tree: Com árvore do projecto + label_cross_project_hierarchy: Com hierarquia do projecto + label_cross_project_system: Com todos os projectos + button_hide: Esconder + setting_non_working_week_days: Dias não úteis + label_in_the_next_days: no futuro + label_in_the_past_days: no passado diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4a/4aecbba22c99f4a330e5a4959bf39381e6cebf33.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4a/4aecbba22c99f4a330e5a4959bf39381e6cebf33.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,21 @@ +--- +issue_categories_001: + name: Printing + project_id: 1 + assigned_to_id: 2 + id: 1 +issue_categories_002: + name: Recipes + project_id: 1 + assigned_to_id: + id: 2 +issue_categories_003: + name: Stock management + project_id: 2 + assigned_to_id: + id: 3 +issue_categories_004: + name: Printing + project_id: 2 + assigned_to_id: + id: 4 diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4a/4aed51d47566631f82dec657795de1c08f373cee.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4a/4aed51d47566631f82dec657795de1c08f373cee.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,62 @@ +require File.expand_path('../boot', __FILE__) + +require 'rails/all' + +if defined?(Bundler) + # If you precompile assets before deploying to production, use this line + Bundler.require(*Rails.groups(:assets => %w(development test))) + # If you want your assets lazily compiled in production, use this line + # Bundler.require(:default, :assets, Rails.env) +end + +module RedmineApp + class Application < Rails::Application + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + + # Custom directories with classes and modules you want to be autoloadable. + config.autoload_paths += %W(#{config.root}/lib) + + # Only load the plugins named here, in the order given (default is alphabetical). + # :all can be used as a placeholder for all plugins not explicitly named. + # config.plugins = [ :exception_notification, :ssl_requirement, :all ] + + # Activate observers that should always be running. + config.active_record.observers = :message_observer, :issue_observer, :journal_observer, :news_observer, :document_observer, :wiki_content_observer, :comment_observer + + config.active_record.store_full_sti_class = true + config.active_record.default_timezone = :local + + # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. + # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. + # config.time_zone = 'Central Time (US & Canada)' + + # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. + # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + # config.i18n.default_locale = :de + + # Configure the default encoding used in templates for Ruby 1.9. + config.encoding = "utf-8" + + # Configure sensitive parameters which will be filtered from the log file. + config.filter_parameters += [:password] + + # Enable the asset pipeline + config.assets.enabled = false + + # Version of your assets, change this if you want to expire all your assets + config.assets.version = '1.0' + + config.action_mailer.perform_deliveries = false + + # Do not include all helpers + config.action_controller.include_all_helpers = false + + config.session_store :cookie_store, :key => '_redmine_session' + + if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb')) + instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb')) + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4b/4b6ad4ee97c77f142f2ea07d73404f963b296d82.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4b/4b6ad4ee97c77f142f2ea07d73404f963b296d82.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1939 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class IssueTest < ActiveSupport::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, + :groups_users, + :trackers, :projects_trackers, + :enabled_modules, + :versions, + :issue_statuses, :issue_categories, :issue_relations, :workflows, + :enumerations, + :issues, :journals, :journal_details, + :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values, + :time_entries + + include Redmine::I18n + + def teardown + User.current = nil + end + + def test_create + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, + :status_id => 1, :priority => IssuePriority.all.first, + :subject => 'test_create', + :description => 'IssueTest#test_create', :estimated_hours => '1:30') + assert issue.save + issue.reload + assert_equal 1.5, issue.estimated_hours + end + + def test_create_minimal + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, + :status_id => 1, :priority => IssuePriority.all.first, + :subject => 'test_create') + assert issue.save + assert issue.description.nil? + assert_nil issue.estimated_hours + end + + def test_start_date_format_should_be_validated + set_language_if_valid 'en' + ['2012', 'ABC', '2012-15-20'].each do |invalid_date| + issue = Issue.new(:start_date => invalid_date) + assert !issue.valid? + assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}" + end + end + + def test_due_date_format_should_be_validated + set_language_if_valid 'en' + ['2012', 'ABC', '2012-15-20'].each do |invalid_date| + issue = Issue.new(:due_date => invalid_date) + assert !issue.valid? + assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}" + end + end + + def test_due_date_lesser_than_start_date_should_not_validate + set_language_if_valid 'en' + issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02') + assert !issue.valid? + assert_include 'Due date must be greater than start date', issue.errors.full_messages + end + + def test_create_with_required_custom_field + set_language_if_valid 'en' + field = IssueCustomField.find_by_name('Database') + field.update_attribute(:is_required, true) + + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, + :status_id => 1, :subject => 'test_create', + :description => 'IssueTest#test_create_with_required_custom_field') + assert issue.available_custom_fields.include?(field) + # No value for the custom field + assert !issue.save + assert_equal ["Database can't be blank"], issue.errors.full_messages + # Blank value + issue.custom_field_values = { field.id => '' } + assert !issue.save + assert_equal ["Database can't be blank"], issue.errors.full_messages + # Invalid value + issue.custom_field_values = { field.id => 'SQLServer' } + assert !issue.save + assert_equal ["Database is not included in the list"], issue.errors.full_messages + # Valid value + issue.custom_field_values = { field.id => 'PostgreSQL' } + assert issue.save + issue.reload + assert_equal 'PostgreSQL', issue.custom_value_for(field).value + end + + def test_create_with_group_assignment + with_settings :issue_group_assignment => '1' do + assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1, + :subject => 'Group assignment', + :assigned_to_id => 11).save + issue = Issue.first(:order => 'id DESC') + assert_kind_of Group, issue.assigned_to + assert_equal Group.find(11), issue.assigned_to + end + end + + def test_create_with_parent_issue_id + issue = Issue.new(:project_id => 1, :tracker_id => 1, + :author_id => 1, :subject => 'Group assignment', + :parent_issue_id => 1) + assert_save issue + assert_equal 1, issue.parent_issue_id + assert_equal Issue.find(1), issue.parent + end + + def test_create_with_sharp_parent_issue_id + issue = Issue.new(:project_id => 1, :tracker_id => 1, + :author_id => 1, :subject => 'Group assignment', + :parent_issue_id => "#1") + assert_save issue + assert_equal 1, issue.parent_issue_id + assert_equal Issue.find(1), issue.parent + end + + def test_create_with_invalid_parent_issue_id + set_language_if_valid 'en' + issue = Issue.new(:project_id => 1, :tracker_id => 1, + :author_id => 1, :subject => 'Group assignment', + :parent_issue_id => '01ABC') + assert !issue.save + assert_equal '01ABC', issue.parent_issue_id + assert_include 'Parent task is invalid', issue.errors.full_messages + end + + def test_create_with_invalid_sharp_parent_issue_id + set_language_if_valid 'en' + issue = Issue.new(:project_id => 1, :tracker_id => 1, + :author_id => 1, :subject => 'Group assignment', + :parent_issue_id => '#01ABC') + assert !issue.save + assert_equal '#01ABC', issue.parent_issue_id + assert_include 'Parent task is invalid', issue.errors.full_messages + end + + def assert_visibility_match(user, issues) + assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort + end + + def test_visible_scope_for_anonymous + # Anonymous user should see issues of public projects only + issues = Issue.visible(User.anonymous).all + assert issues.any? + assert_nil issues.detect {|issue| !issue.project.is_public?} + assert_nil issues.detect {|issue| issue.is_private?} + assert_visibility_match User.anonymous, issues + end + + def test_visible_scope_for_anonymous_without_view_issues_permissions + # Anonymous user should not see issues without permission + Role.anonymous.remove_permission!(:view_issues) + issues = Issue.visible(User.anonymous).all + assert issues.empty? + assert_visibility_match User.anonymous, issues + end + + def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default + assert Role.anonymous.update_attribute(:issues_visibility, 'default') + issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true) + assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first + assert !issue.visible?(User.anonymous) + end + + def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own + assert Role.anonymous.update_attribute(:issues_visibility, 'own') + issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true) + assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first + assert !issue.visible?(User.anonymous) + end + + def test_visible_scope_for_non_member + user = User.find(9) + assert user.projects.empty? + # Non member user should see issues of public projects only + issues = Issue.visible(user).all + assert issues.any? + assert_nil issues.detect {|issue| !issue.project.is_public?} + assert_nil issues.detect {|issue| issue.is_private?} + assert_visibility_match user, issues + end + + def test_visible_scope_for_non_member_with_own_issues_visibility + Role.non_member.update_attribute :issues_visibility, 'own' + Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member') + user = User.find(9) + + issues = Issue.visible(user).all + assert issues.any? + assert_nil issues.detect {|issue| issue.author != user} + assert_visibility_match user, issues + end + + def test_visible_scope_for_non_member_without_view_issues_permissions + # Non member user should not see issues without permission + Role.non_member.remove_permission!(:view_issues) + user = User.find(9) + assert user.projects.empty? + issues = Issue.visible(user).all + assert issues.empty? + assert_visibility_match user, issues + end + + def test_visible_scope_for_member + user = User.find(9) + # User should see issues of projects for which he has view_issues permissions only + Role.non_member.remove_permission!(:view_issues) + Member.create!(:principal => user, :project_id => 3, :role_ids => [2]) + issues = Issue.visible(user).all + assert issues.any? + assert_nil issues.detect {|issue| issue.project_id != 3} + assert_nil issues.detect {|issue| issue.is_private?} + assert_visibility_match user, issues + end + + def test_visible_scope_for_member_with_groups_should_return_assigned_issues + user = User.find(8) + assert user.groups.any? + Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2]) + Role.non_member.remove_permission!(:view_issues) + + issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, + :status_id => 1, :priority => IssuePriority.all.first, + :subject => 'Assignment test', + :assigned_to => user.groups.first, + :is_private => true) + + Role.find(2).update_attribute :issues_visibility, 'default' + issues = Issue.visible(User.find(8)).all + assert issues.any? + assert issues.include?(issue) + + Role.find(2).update_attribute :issues_visibility, 'own' + issues = Issue.visible(User.find(8)).all + assert issues.any? + assert issues.include?(issue) + end + + def test_visible_scope_for_admin + user = User.find(1) + user.members.each(&:destroy) + assert user.projects.empty? + issues = Issue.visible(user).all + assert issues.any? + # Admin should see issues on private projects that he does not belong to + assert issues.detect {|issue| !issue.project.is_public?} + # Admin should see private issues of other users + assert issues.detect {|issue| issue.is_private? && issue.author != user} + assert_visibility_match user, issues + end + + def test_visible_scope_with_project + project = Project.find(1) + issues = Issue.visible(User.find(2), :project => project).all + projects = issues.collect(&:project).uniq + assert_equal 1, projects.size + assert_equal project, projects.first + end + + def test_visible_scope_with_project_and_subprojects + project = Project.find(1) + issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all + projects = issues.collect(&:project).uniq + assert projects.size > 1 + assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)} + end + + def test_visible_and_nested_set_scopes + assert_equal 0, Issue.find(1).descendants.visible.all.size + end + + def test_open_scope + issues = Issue.open.all + assert_nil issues.detect(&:closed?) + end + + def test_open_scope_with_arg + issues = Issue.open(false).all + assert_equal issues, issues.select(&:closed?) + end + + def test_errors_full_messages_should_include_custom_fields_errors + field = IssueCustomField.find_by_name('Database') + + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, + :status_id => 1, :subject => 'test_create', + :description => 'IssueTest#test_create_with_required_custom_field') + assert issue.available_custom_fields.include?(field) + # Invalid value + issue.custom_field_values = { field.id => 'SQLServer' } + + assert !issue.valid? + assert_equal 1, issue.errors.full_messages.size + assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", + issue.errors.full_messages.first + end + + def test_update_issue_with_required_custom_field + field = IssueCustomField.find_by_name('Database') + field.update_attribute(:is_required, true) + + issue = Issue.find(1) + assert_nil issue.custom_value_for(field) + assert issue.available_custom_fields.include?(field) + # No change to custom values, issue can be saved + assert issue.save + # Blank value + issue.custom_field_values = { field.id => '' } + assert !issue.save + # Valid value + issue.custom_field_values = { field.id => 'PostgreSQL' } + assert issue.save + issue.reload + assert_equal 'PostgreSQL', issue.custom_value_for(field).value + end + + def test_should_not_update_attributes_if_custom_fields_validation_fails + issue = Issue.find(1) + field = IssueCustomField.find_by_name('Database') + assert issue.available_custom_fields.include?(field) + + issue.custom_field_values = { field.id => 'Invalid' } + issue.subject = 'Should be not be saved' + assert !issue.save + + issue.reload + assert_equal "Can't print recipes", issue.subject + end + + def test_should_not_recreate_custom_values_objects_on_update + field = IssueCustomField.find_by_name('Database') + + issue = Issue.find(1) + issue.custom_field_values = { field.id => 'PostgreSQL' } + assert issue.save + custom_value = issue.custom_value_for(field) + issue.reload + issue.custom_field_values = { field.id => 'MySQL' } + assert issue.save + issue.reload + assert_equal custom_value.id, issue.custom_value_for(field).id + end + + def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields + issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, + :status_id => 1, :subject => 'Test', + :custom_field_values => {'2' => 'Test'}) + assert !Tracker.find(2).custom_field_ids.include?(2) + + issue = Issue.find(issue.id) + issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}} + + issue = Issue.find(issue.id) + custom_value = issue.custom_value_for(2) + assert_not_nil custom_value + assert_equal 'Test', custom_value.value + end + + def test_assigning_tracker_id_should_reload_custom_fields_values + issue = Issue.new(:project => Project.find(1)) + assert issue.custom_field_values.empty? + issue.tracker_id = 1 + assert issue.custom_field_values.any? + end + + def test_assigning_attributes_should_assign_project_and_tracker_first + seq = sequence('seq') + issue = Issue.new + issue.expects(:project_id=).in_sequence(seq) + issue.expects(:tracker_id=).in_sequence(seq) + issue.expects(:subject=).in_sequence(seq) + issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'} + end + + def test_assigning_tracker_and_custom_fields_should_assign_custom_fields + attributes = ActiveSupport::OrderedHash.new + attributes['custom_field_values'] = { '1' => 'MySQL' } + attributes['tracker_id'] = '1' + issue = Issue.new(:project => Project.find(1)) + issue.attributes = attributes + assert_equal 'MySQL', issue.custom_field_value(1) + end + + def test_should_update_issue_with_disabled_tracker + p = Project.find(1) + issue = Issue.find(1) + + p.trackers.delete(issue.tracker) + assert !p.trackers.include?(issue.tracker) + + issue.reload + issue.subject = 'New subject' + assert issue.save + end + + def test_should_not_set_a_disabled_tracker + p = Project.find(1) + p.trackers.delete(Tracker.find(2)) + + issue = Issue.find(1) + issue.tracker_id = 2 + issue.subject = 'New subject' + assert !issue.save + assert_not_nil issue.errors[:tracker_id] + end + + def test_category_based_assignment + issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, + :status_id => 1, :priority => IssuePriority.all.first, + :subject => 'Assignment test', + :description => 'Assignment test', :category_id => 1) + assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to + end + + def test_new_statuses_allowed_to + WorkflowTransition.delete_all + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, + :old_status_id => 1, :new_status_id => 2, + :author => false, :assignee => false) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, + :old_status_id => 1, :new_status_id => 3, + :author => true, :assignee => false) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, + :new_status_id => 4, :author => false, + :assignee => true) + WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, + :old_status_id => 1, :new_status_id => 5, + :author => true, :assignee => true) + status = IssueStatus.find(1) + role = Role.find(1) + tracker = Tracker.find(1) + user = User.find(2) + + issue = Issue.generate!(:tracker => tracker, :status => status, + :project_id => 1, :author_id => 1) + assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id) + + issue = Issue.generate!(:tracker => tracker, :status => status, + :project_id => 1, :author => user) + assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id) + + issue = Issue.generate!(:tracker => tracker, :status => status, + :project_id => 1, :author_id => 1, + :assigned_to => user) + assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id) + + issue = Issue.generate!(:tracker => tracker, :status => status, + :project_id => 1, :author => user, + :assigned_to => user) + assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id) + end + + def test_new_statuses_allowed_to_should_return_all_transitions_for_admin + admin = User.find(1) + issue = Issue.find(1) + assert !admin.member_of?(issue.project) + expected_statuses = [issue.status] + + WorkflowTransition.find_all_by_old_status_id( + issue.status_id).map(&:new_status).uniq.sort + assert_equal expected_statuses, issue.new_statuses_allowed_to(admin) + end + + def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying + issue = Issue.find(1).copy + assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id) + + issue = Issue.find(2).copy + assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id) + end + + def test_safe_attributes_names_should_not_include_disabled_field + tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id)) + + issue = Issue.new(:tracker => tracker) + assert_include 'tracker_id', issue.safe_attribute_names + assert_include 'status_id', issue.safe_attribute_names + assert_include 'subject', issue.safe_attribute_names + assert_include 'description', issue.safe_attribute_names + assert_include 'custom_field_values', issue.safe_attribute_names + assert_include 'custom_fields', issue.safe_attribute_names + assert_include 'lock_version', issue.safe_attribute_names + + tracker.core_fields.each do |field| + assert_include field, issue.safe_attribute_names + end + + tracker.disabled_core_fields.each do |field| + assert_not_include field, issue.safe_attribute_names + end + end + + def test_safe_attributes_should_ignore_disabled_fields + tracker = Tracker.find(1) + tracker.core_fields = %w(assigned_to_id due_date) + tracker.save! + + issue = Issue.new(:tracker => tracker) + issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'} + assert_nil issue.start_date + assert_equal Date.parse('2012-07-14'), issue.due_date + end + + def test_safe_attributes_should_accept_target_tracker_enabled_fields + source = Tracker.find(1) + source.core_fields = [] + source.save! + target = Tracker.find(2) + target.core_fields = %w(assigned_to_id due_date) + target.save! + + issue = Issue.new(:tracker => source) + issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'} + assert_equal target, issue.tracker + assert_equal Date.parse('2012-07-14'), issue.due_date + end + + def test_safe_attributes_should_not_include_readonly_fields + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 1, :field_name => 'due_date', + :rule => 'readonly') + user = User.find(2) + + issue = Issue.new(:project_id => 1, :tracker_id => 1) + assert_equal %w(due_date), issue.read_only_attribute_names(user) + assert_not_include 'due_date', issue.safe_attribute_names(user) + + issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user + assert_equal Date.parse('2012-07-14'), issue.start_date + assert_nil issue.due_date + end + + def test_safe_attributes_should_not_include_readonly_custom_fields + cf1 = IssueCustomField.create!(:name => 'Writable field', + :field_format => 'string', + :is_for_all => true, :tracker_ids => [1]) + cf2 = IssueCustomField.create!(:name => 'Readonly field', + :field_format => 'string', + :is_for_all => true, :tracker_ids => [1]) + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 1, :field_name => cf2.id.to_s, + :rule => 'readonly') + user = User.find(2) + issue = Issue.new(:project_id => 1, :tracker_id => 1) + assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user) + assert_not_include cf2.id.to_s, issue.safe_attribute_names(user) + + issue.send :safe_attributes=, {'custom_field_values' => { + cf1.id.to_s => 'value1', cf2.id.to_s => 'value2' + }}, user + assert_equal 'value1', issue.custom_field_value(cf1) + assert_nil issue.custom_field_value(cf2) + + issue.send :safe_attributes=, {'custom_fields' => [ + {'id' => cf1.id.to_s, 'value' => 'valuea'}, + {'id' => cf2.id.to_s, 'value' => 'valueb'} + ]}, user + assert_equal 'valuea', issue.custom_field_value(cf1) + assert_nil issue.custom_field_value(cf2) + end + + def test_editable_custom_field_values_should_return_non_readonly_custom_values + cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string', + :is_for_all => true, :tracker_ids => [1, 2]) + cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string', + :is_for_all => true, :tracker_ids => [1, 2]) + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1, + :field_name => cf2.id.to_s, :rule => 'readonly') + user = User.find(2) + + issue = Issue.new(:project_id => 1, :tracker_id => 1) + values = issue.editable_custom_field_values(user) + assert values.detect {|value| value.custom_field == cf1} + assert_nil values.detect {|value| value.custom_field == cf2} + + issue.tracker_id = 2 + values = issue.editable_custom_field_values(user) + assert values.detect {|value| value.custom_field == cf1} + assert values.detect {|value| value.custom_field == cf2} + end + + def test_safe_attributes_should_accept_target_tracker_writable_fields + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 1, :field_name => 'due_date', + :rule => 'readonly') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, + :role_id => 1, :field_name => 'start_date', + :rule => 'readonly') + user = User.find(2) + + issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1) + + issue.send :safe_attributes=, {'start_date' => '2012-07-12', + 'due_date' => '2012-07-14'}, user + assert_equal Date.parse('2012-07-12'), issue.start_date + assert_nil issue.due_date + + issue.send :safe_attributes=, {'start_date' => '2012-07-15', + 'due_date' => '2012-07-16', + 'tracker_id' => 2}, user + assert_equal Date.parse('2012-07-12'), issue.start_date + assert_equal Date.parse('2012-07-16'), issue.due_date + end + + def test_safe_attributes_should_accept_target_status_writable_fields + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 1, :field_name => 'due_date', + :rule => 'readonly') + WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1, + :role_id => 1, :field_name => 'start_date', + :rule => 'readonly') + user = User.find(2) + + issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1) + + issue.send :safe_attributes=, {'start_date' => '2012-07-12', + 'due_date' => '2012-07-14'}, + user + assert_equal Date.parse('2012-07-12'), issue.start_date + assert_nil issue.due_date + + issue.send :safe_attributes=, {'start_date' => '2012-07-15', + 'due_date' => '2012-07-16', + 'status_id' => 2}, + user + assert_equal Date.parse('2012-07-12'), issue.start_date + assert_equal Date.parse('2012-07-16'), issue.due_date + end + + def test_required_attributes_should_be_validated + cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', + :is_for_all => true, :tracker_ids => [1, 2]) + + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 1, :field_name => 'due_date', + :rule => 'required') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 1, :field_name => 'category_id', + :rule => 'required') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 1, :field_name => cf.id.to_s, + :rule => 'required') + + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, + :role_id => 1, :field_name => 'start_date', + :rule => 'required') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, + :role_id => 1, :field_name => cf.id.to_s, + :rule => 'required') + user = User.find(2) + + issue = Issue.new(:project_id => 1, :tracker_id => 1, + :status_id => 1, :subject => 'Required fields', + :author => user) + assert_equal [cf.id.to_s, "category_id", "due_date"], + issue.required_attribute_names(user).sort + assert !issue.save, "Issue was saved" + assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"], + issue.errors.full_messages.sort + + issue.tracker_id = 2 + assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort + assert !issue.save, "Issue was saved" + assert_equal ["Foo can't be blank", "Start date can't be blank"], + issue.errors.full_messages.sort + + issue.start_date = Date.today + issue.custom_field_values = {cf.id.to_s => 'bar'} + assert issue.save + end + + def test_required_attribute_names_for_multiple_roles_should_intersect_rules + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 1, :field_name => 'due_date', + :rule => 'required') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 1, :field_name => 'start_date', + :rule => 'required') + user = User.find(2) + member = Member.find(1) + issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1) + + assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort + + member.role_ids = [1, 2] + member.save! + assert_equal [], issue.required_attribute_names(user.reload) + + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 2, :field_name => 'due_date', + :rule => 'required') + assert_equal %w(due_date), issue.required_attribute_names(user) + + member.role_ids = [1, 2, 3] + member.save! + assert_equal [], issue.required_attribute_names(user.reload) + + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 2, :field_name => 'due_date', + :rule => 'readonly') + # required + readonly => required + assert_equal %w(due_date), issue.required_attribute_names(user) + end + + def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules + WorkflowPermission.delete_all + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 1, :field_name => 'due_date', + :rule => 'readonly') + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 1, :field_name => 'start_date', + :rule => 'readonly') + user = User.find(2) + member = Member.find(1) + issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1) + + assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort + + member.role_ids = [1, 2] + member.save! + assert_equal [], issue.read_only_attribute_names(user.reload) + + WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, + :role_id => 2, :field_name => 'due_date', + :rule => 'readonly') + assert_equal %w(due_date), issue.read_only_attribute_names(user) + end + + def test_copy + issue = Issue.new.copy_from(1) + assert issue.copy? + assert issue.save + issue.reload + orig = Issue.find(1) + assert_equal orig.subject, issue.subject + assert_equal orig.tracker, issue.tracker + assert_equal "125", issue.custom_value_for(2).value + end + + def test_copy_should_copy_status + orig = Issue.find(8) + assert orig.status != IssueStatus.default + + issue = Issue.new.copy_from(orig) + assert issue.save + issue.reload + assert_equal orig.status, issue.status + end + + def test_copy_should_add_relation_with_copied_issue + copied = Issue.find(1) + issue = Issue.new.copy_from(copied) + assert issue.save + issue.reload + + assert_equal 1, issue.relations.size + relation = issue.relations.first + assert_equal 'copied_to', relation.relation_type + assert_equal copied, relation.issue_from + assert_equal issue, relation.issue_to + end + + def test_copy_should_copy_subtasks + issue = Issue.generate_with_descendants! + + copy = issue.reload.copy + copy.author = User.find(7) + assert_difference 'Issue.count', 1+issue.descendants.count do + assert copy.save + end + copy.reload + assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort + child_copy = copy.children.detect {|c| c.subject == 'Child1'} + assert_equal %w(Child11), child_copy.children.map(&:subject).sort + assert_equal copy.author, child_copy.author + end + + def test_copy_should_copy_subtasks_to_target_project + issue = Issue.generate_with_descendants! + + copy = issue.copy(:project_id => 3) + assert_difference 'Issue.count', 1+issue.descendants.count do + assert copy.save + end + assert_equal [3], copy.reload.descendants.map(&:project_id).uniq + end + + def test_copy_should_not_copy_subtasks_twice_when_saving_twice + issue = Issue.generate_with_descendants! + + copy = issue.reload.copy + assert_difference 'Issue.count', 1+issue.descendants.count do + assert copy.save + assert copy.save + end + end + + def test_should_not_call_after_project_change_on_creation + issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1, + :subject => 'Test', :author_id => 1) + issue.expects(:after_project_change).never + issue.save! + end + + def test_should_not_call_after_project_change_on_update + issue = Issue.find(1) + issue.project = Project.find(1) + issue.subject = 'No project change' + issue.expects(:after_project_change).never + issue.save! + end + + def test_should_call_after_project_change_on_project_change + issue = Issue.find(1) + issue.project = Project.find(2) + issue.expects(:after_project_change).once + issue.save! + end + + def test_adding_journal_should_update_timestamp + issue = Issue.find(1) + updated_on_was = issue.updated_on + + issue.init_journal(User.first, "Adding notes") + assert_difference 'Journal.count' do + assert issue.save + end + issue.reload + + assert_not_equal updated_on_was, issue.updated_on + end + + def test_should_close_duplicates + # Create 3 issues + issue1 = Issue.generate! + issue2 = Issue.generate! + issue3 = Issue.generate! + + # 2 is a dupe of 1 + IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, + :relation_type => IssueRelation::TYPE_DUPLICATES) + # And 3 is a dupe of 2 + IssueRelation.create!(:issue_from => issue3, :issue_to => issue2, + :relation_type => IssueRelation::TYPE_DUPLICATES) + # And 3 is a dupe of 1 (circular duplicates) + IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, + :relation_type => IssueRelation::TYPE_DUPLICATES) + + assert issue1.reload.duplicates.include?(issue2) + + # Closing issue 1 + issue1.init_journal(User.find(:first), "Closing issue1") + issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true} + assert issue1.save + # 2 and 3 should be also closed + assert issue2.reload.closed? + assert issue3.reload.closed? + end + + def test_should_not_close_duplicated_issue + issue1 = Issue.generate! + issue2 = Issue.generate! + + # 2 is a dupe of 1 + IssueRelation.create(:issue_from => issue2, :issue_to => issue1, + :relation_type => IssueRelation::TYPE_DUPLICATES) + # 2 is a dup of 1 but 1 is not a duplicate of 2 + assert !issue2.reload.duplicates.include?(issue1) + + # Closing issue 2 + issue2.init_journal(User.find(:first), "Closing issue2") + issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true} + assert issue2.save + # 1 should not be also closed + assert !issue1.reload.closed? + end + + def test_assignable_versions + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, + :status_id => 1, :fixed_version_id => 1, + :subject => 'New issue') + assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq + end + + def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, + :status_id => 1, :fixed_version_id => 1, + :subject => 'New issue') + assert !issue.save + assert_not_nil issue.errors[:fixed_version_id] + end + + def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, + :status_id => 1, :fixed_version_id => 2, + :subject => 'New issue') + assert !issue.save + assert_not_nil issue.errors[:fixed_version_id] + end + + def test_should_be_able_to_assign_a_new_issue_to_an_open_version + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, + :status_id => 1, :fixed_version_id => 3, + :subject => 'New issue') + assert issue.save + end + + def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version + issue = Issue.find(11) + assert_equal 'closed', issue.fixed_version.status + issue.subject = 'Subject changed' + assert issue.save + end + + def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version + issue = Issue.find(11) + issue.status_id = 1 + assert !issue.save + assert_not_nil issue.errors[:base] + end + + def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version + issue = Issue.find(11) + issue.status_id = 1 + issue.fixed_version_id = 3 + assert issue.save + end + + def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version + issue = Issue.find(12) + assert_equal 'locked', issue.fixed_version.status + issue.status_id = 1 + assert issue.save + end + + def test_should_not_be_able_to_keep_unshared_version_when_changing_project + issue = Issue.find(2) + assert_equal 2, issue.fixed_version_id + issue.project_id = 3 + assert_nil issue.fixed_version_id + issue.fixed_version_id = 2 + assert !issue.save + assert_include 'Target version is not included in the list', issue.errors.full_messages + end + + def test_should_keep_shared_version_when_changing_project + Version.find(2).update_attribute :sharing, 'tree' + + issue = Issue.find(2) + assert_equal 2, issue.fixed_version_id + issue.project_id = 3 + assert_equal 2, issue.fixed_version_id + assert issue.save + end + + def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled + assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2)) + end + + def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled + Project.find(2).disable_module! :issue_tracking + assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2)) + end + + def test_move_to_another_project_with_same_category + issue = Issue.find(1) + issue.project = Project.find(2) + assert issue.save + issue.reload + assert_equal 2, issue.project_id + # Category changes + assert_equal 4, issue.category_id + # Make sure time entries were move to the target project + assert_equal 2, issue.time_entries.first.project_id + end + + def test_move_to_another_project_without_same_category + issue = Issue.find(2) + issue.project = Project.find(2) + assert issue.save + issue.reload + assert_equal 2, issue.project_id + # Category cleared + assert_nil issue.category_id + end + + def test_move_to_another_project_should_clear_fixed_version_when_not_shared + issue = Issue.find(1) + issue.update_attribute(:fixed_version_id, 1) + issue.project = Project.find(2) + assert issue.save + issue.reload + assert_equal 2, issue.project_id + # Cleared fixed_version + assert_equal nil, issue.fixed_version + end + + def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project + issue = Issue.find(1) + issue.update_attribute(:fixed_version_id, 4) + issue.project = Project.find(5) + assert issue.save + issue.reload + assert_equal 5, issue.project_id + # Keep fixed_version + assert_equal 4, issue.fixed_version_id + end + + def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project + issue = Issue.find(1) + issue.update_attribute(:fixed_version_id, 1) + issue.project = Project.find(5) + assert issue.save + issue.reload + assert_equal 5, issue.project_id + # Cleared fixed_version + assert_equal nil, issue.fixed_version + end + + def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide + issue = Issue.find(1) + issue.update_attribute(:fixed_version_id, 7) + issue.project = Project.find(2) + assert issue.save + issue.reload + assert_equal 2, issue.project_id + # Keep fixed_version + assert_equal 7, issue.fixed_version_id + end + + def test_move_to_another_project_should_keep_parent_if_valid + issue = Issue.find(1) + issue.update_attribute(:parent_issue_id, 2) + issue.project = Project.find(3) + assert issue.save + issue.reload + assert_equal 2, issue.parent_id + end + + def test_move_to_another_project_should_clear_parent_if_not_valid + issue = Issue.find(1) + issue.update_attribute(:parent_issue_id, 2) + issue.project = Project.find(2) + assert issue.save + issue.reload + assert_nil issue.parent_id + end + + def test_move_to_another_project_with_disabled_tracker + issue = Issue.find(1) + target = Project.find(2) + target.tracker_ids = [3] + target.save + issue.project = target + assert issue.save + issue.reload + assert_equal 2, issue.project_id + assert_equal 3, issue.tracker_id + end + + def test_copy_to_the_same_project + issue = Issue.find(1) + copy = issue.copy + assert_difference 'Issue.count' do + copy.save! + end + assert_kind_of Issue, copy + assert_equal issue.project, copy.project + assert_equal "125", copy.custom_value_for(2).value + end + + def test_copy_to_another_project_and_tracker + issue = Issue.find(1) + copy = issue.copy(:project_id => 3, :tracker_id => 2) + assert_difference 'Issue.count' do + copy.save! + end + copy.reload + assert_kind_of Issue, copy + assert_equal Project.find(3), copy.project + assert_equal Tracker.find(2), copy.tracker + # Custom field #2 is not associated with target tracker + assert_nil copy.custom_value_for(2) + end + + context "#copy" do + setup do + @issue = Issue.find(1) + end + + should "not create a journal" do + copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3) + copy.save! + assert_equal 0, copy.reload.journals.size + end + + should "allow assigned_to changes" do + copy = @issue.copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3) + assert_equal 3, copy.assigned_to_id + end + + should "allow status changes" do + copy = @issue.copy(:project_id => 3, :tracker_id => 2, :status_id => 2) + assert_equal 2, copy.status_id + end + + should "allow start date changes" do + date = Date.today + copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date) + assert_equal date, copy.start_date + end + + should "allow due date changes" do + date = Date.today + copy = @issue.copy(:project_id => 3, :tracker_id => 2, :due_date => date) + assert_equal date, copy.due_date + end + + should "set current user as author" do + User.current = User.find(9) + copy = @issue.copy(:project_id => 3, :tracker_id => 2) + assert_equal User.current, copy.author + end + + should "create a journal with notes" do + date = Date.today + notes = "Notes added when copying" + copy = @issue.copy(:project_id => 3, :tracker_id => 2, :start_date => date) + copy.init_journal(User.current, notes) + copy.save! + + assert_equal 1, copy.journals.size + journal = copy.journals.first + assert_equal 0, journal.details.size + assert_equal notes, journal.notes + end + end + + def test_valid_parent_project + issue = Issue.find(1) + issue_in_same_project = Issue.find(2) + issue_in_child_project = Issue.find(5) + issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1) + issue_in_other_child_project = Issue.find(6) + issue_in_different_tree = Issue.find(4) + + with_settings :cross_project_subtasks => '' do + assert_equal true, issue.valid_parent_project?(issue_in_same_project) + assert_equal false, issue.valid_parent_project?(issue_in_child_project) + assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project) + assert_equal false, issue.valid_parent_project?(issue_in_different_tree) + end + + with_settings :cross_project_subtasks => 'system' do + assert_equal true, issue.valid_parent_project?(issue_in_same_project) + assert_equal true, issue.valid_parent_project?(issue_in_child_project) + assert_equal true, issue.valid_parent_project?(issue_in_different_tree) + end + + with_settings :cross_project_subtasks => 'tree' do + assert_equal true, issue.valid_parent_project?(issue_in_same_project) + assert_equal true, issue.valid_parent_project?(issue_in_child_project) + assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project) + assert_equal false, issue.valid_parent_project?(issue_in_different_tree) + + assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project) + assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project) + end + + with_settings :cross_project_subtasks => 'descendants' do + assert_equal true, issue.valid_parent_project?(issue_in_same_project) + assert_equal false, issue.valid_parent_project?(issue_in_child_project) + assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project) + assert_equal false, issue.valid_parent_project?(issue_in_different_tree) + + assert_equal true, issue_in_child_project.valid_parent_project?(issue) + assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project) + end + end + + def test_recipients_should_include_previous_assignee + user = User.find(3) + user.members.update_all ["mail_notification = ?", false] + user.update_attribute :mail_notification, 'only_assigned' + + issue = Issue.find(2) + issue.assigned_to = nil + assert_include user.mail, issue.recipients + issue.save! + assert !issue.recipients.include?(user.mail) + end + + def test_recipients_should_not_include_users_that_cannot_view_the_issue + issue = Issue.find(12) + assert issue.recipients.include?(issue.author.mail) + # copy the issue to a private project + copy = issue.copy(:project_id => 5, :tracker_id => 2) + # author is not a member of project anymore + assert !copy.recipients.include?(copy.author.mail) + end + + def test_recipients_should_include_the_assigned_group_members + group_member = User.generate! + group = Group.generate! + group.users << group_member + + issue = Issue.find(12) + issue.assigned_to = group + assert issue.recipients.include?(group_member.mail) + end + + def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue + user = User.find(3) + issue = Issue.find(9) + Watcher.create!(:user => user, :watchable => issue) + assert issue.watched_by?(user) + assert !issue.watcher_recipients.include?(user.mail) + end + + def test_issue_destroy + Issue.find(1).destroy + assert_nil Issue.find_by_id(1) + assert_nil TimeEntry.find_by_issue_id(1) + end + + def test_destroying_a_deleted_issue_should_not_raise_an_error + issue = Issue.find(1) + Issue.find(1).destroy + + assert_nothing_raised do + assert_no_difference 'Issue.count' do + issue.destroy + end + assert issue.destroyed? + end + end + + def test_destroying_a_stale_issue_should_not_raise_an_error + issue = Issue.find(1) + Issue.find(1).update_attribute :subject, "Updated" + + assert_nothing_raised do + assert_difference 'Issue.count', -1 do + issue.destroy + end + assert issue.destroyed? + end + end + + def test_blocked + blocked_issue = Issue.find(9) + blocking_issue = Issue.find(10) + + assert blocked_issue.blocked? + assert !blocking_issue.blocked? + end + + def test_blocked_issues_dont_allow_closed_statuses + blocked_issue = Issue.find(9) + + allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002)) + assert !allowed_statuses.empty? + closed_statuses = allowed_statuses.select {|st| st.is_closed?} + assert closed_statuses.empty? + end + + def test_unblocked_issues_allow_closed_statuses + blocking_issue = Issue.find(10) + + allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002)) + assert !allowed_statuses.empty? + closed_statuses = allowed_statuses.select {|st| st.is_closed?} + assert !closed_statuses.empty? + end + + def test_reschedule_an_issue_without_dates + with_settings :non_working_week_days => [] do + issue = Issue.new(:start_date => nil, :due_date => nil) + issue.reschedule_on '2012-10-09'.to_date + assert_equal '2012-10-09'.to_date, issue.start_date + assert_equal '2012-10-09'.to_date, issue.due_date + end + + with_settings :non_working_week_days => %w(6 7) do + issue = Issue.new(:start_date => nil, :due_date => nil) + issue.reschedule_on '2012-10-09'.to_date + assert_equal '2012-10-09'.to_date, issue.start_date + assert_equal '2012-10-09'.to_date, issue.due_date + + issue = Issue.new(:start_date => nil, :due_date => nil) + issue.reschedule_on '2012-10-13'.to_date + assert_equal '2012-10-15'.to_date, issue.start_date + assert_equal '2012-10-15'.to_date, issue.due_date + end + end + + def test_reschedule_an_issue_with_start_date + with_settings :non_working_week_days => [] do + issue = Issue.new(:start_date => '2012-10-09', :due_date => nil) + issue.reschedule_on '2012-10-13'.to_date + assert_equal '2012-10-13'.to_date, issue.start_date + assert_equal '2012-10-13'.to_date, issue.due_date + end + + with_settings :non_working_week_days => %w(6 7) do + issue = Issue.new(:start_date => '2012-10-09', :due_date => nil) + issue.reschedule_on '2012-10-11'.to_date + assert_equal '2012-10-11'.to_date, issue.start_date + assert_equal '2012-10-11'.to_date, issue.due_date + + issue = Issue.new(:start_date => '2012-10-09', :due_date => nil) + issue.reschedule_on '2012-10-13'.to_date + assert_equal '2012-10-15'.to_date, issue.start_date + assert_equal '2012-10-15'.to_date, issue.due_date + end + end + + def test_reschedule_an_issue_with_start_and_due_dates + with_settings :non_working_week_days => [] do + issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15') + issue.reschedule_on '2012-10-13'.to_date + assert_equal '2012-10-13'.to_date, issue.start_date + assert_equal '2012-10-19'.to_date, issue.due_date + end + + with_settings :non_working_week_days => %w(6 7) do + issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days + issue.reschedule_on '2012-10-11'.to_date + assert_equal '2012-10-11'.to_date, issue.start_date + assert_equal '2012-10-23'.to_date, issue.due_date + + issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') + issue.reschedule_on '2012-10-13'.to_date + assert_equal '2012-10-15'.to_date, issue.start_date + assert_equal '2012-10-25'.to_date, issue.due_date + end + end + + def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue + issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17') + issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17') + IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, + :relation_type => IssueRelation::TYPE_PRECEDES) + assert_equal Date.parse('2012-10-18'), issue2.reload.start_date + + issue1.due_date = '2012-10-23' + issue1.save! + issue2.reload + assert_equal Date.parse('2012-10-24'), issue2.start_date + assert_equal Date.parse('2012-10-26'), issue2.due_date + end + + def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue + issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17') + issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17') + IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, + :relation_type => IssueRelation::TYPE_PRECEDES) + assert_equal Date.parse('2012-10-18'), issue2.reload.start_date + + issue1.start_date = '2012-09-17' + issue1.due_date = '2012-09-18' + issue1.save! + issue2.reload + assert_equal Date.parse('2012-09-19'), issue2.start_date + assert_equal Date.parse('2012-09-21'), issue2.due_date + end + + def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues + issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17') + issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17') + issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02') + IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, + :relation_type => IssueRelation::TYPE_PRECEDES) + IssueRelation.create!(:issue_from => issue3, :issue_to => issue2, + :relation_type => IssueRelation::TYPE_PRECEDES) + assert_equal Date.parse('2012-10-18'), issue2.reload.start_date + + issue1.start_date = '2012-09-17' + issue1.due_date = '2012-09-18' + issue1.save! + issue2.reload + # Issue 2 must start after Issue 3 + assert_equal Date.parse('2012-10-03'), issue2.start_date + assert_equal Date.parse('2012-10-05'), issue2.due_date + end + + def test_rescheduling_a_stale_issue_should_not_raise_an_error + with_settings :non_working_week_days => [] do + stale = Issue.find(1) + issue = Issue.find(1) + issue.subject = "Updated" + issue.save! + date = 10.days.from_now.to_date + assert_nothing_raised do + stale.reschedule_on!(date) + end + assert_equal date, stale.reload.start_date + end + end + + def test_overdue + assert Issue.new(:due_date => 1.day.ago.to_date).overdue? + assert !Issue.new(:due_date => Date.today).overdue? + assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue? + assert !Issue.new(:due_date => nil).overdue? + assert !Issue.new(:due_date => 1.day.ago.to_date, + :status => IssueStatus.find(:first, + :conditions => {:is_closed => true}) + ).overdue? + end + + context "#behind_schedule?" do + should "be false if the issue has no start_date" do + assert !Issue.new(:start_date => nil, + :due_date => 1.day.from_now.to_date, + :done_ratio => 0).behind_schedule? + end + + should "be false if the issue has no end_date" do + assert !Issue.new(:start_date => 1.day.from_now.to_date, + :due_date => nil, + :done_ratio => 0).behind_schedule? + end + + should "be false if the issue has more done than it's calendar time" do + assert !Issue.new(:start_date => 50.days.ago.to_date, + :due_date => 50.days.from_now.to_date, + :done_ratio => 90).behind_schedule? + end + + should "be true if the issue hasn't been started at all" do + assert Issue.new(:start_date => 1.day.ago.to_date, + :due_date => 1.day.from_now.to_date, + :done_ratio => 0).behind_schedule? + end + + should "be true if the issue has used more calendar time than it's done ratio" do + assert Issue.new(:start_date => 100.days.ago.to_date, + :due_date => Date.today, + :done_ratio => 90).behind_schedule? + end + end + + context "#assignable_users" do + should "be Users" do + assert_kind_of User, Issue.find(1).assignable_users.first + end + + should "include the issue author" do + non_project_member = User.generate! + issue = Issue.generate!(:author => non_project_member) + + assert issue.assignable_users.include?(non_project_member) + end + + should "include the current assignee" do + user = User.generate! + issue = Issue.generate!(:assigned_to => user) + user.lock! + + assert Issue.find(issue.id).assignable_users.include?(user) + end + + should "not show the issue author twice" do + assignable_user_ids = Issue.find(1).assignable_users.collect(&:id) + assert_equal 2, assignable_user_ids.length + + assignable_user_ids.each do |user_id| + assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, + "User #{user_id} appears more or less than once" + end + end + + context "with issue_group_assignment" do + should "include groups" do + issue = Issue.new(:project => Project.find(2)) + + with_settings :issue_group_assignment => '1' do + assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort + assert issue.assignable_users.include?(Group.find(11)) + end + end + end + + context "without issue_group_assignment" do + should "not include groups" do + issue = Issue.new(:project => Project.find(2)) + + with_settings :issue_group_assignment => '0' do + assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort + assert !issue.assignable_users.include?(Group.find(11)) + end + end + end + end + + def test_create_should_send_email_notification + ActionMailer::Base.deliveries.clear + issue = Issue.new(:project_id => 1, :tracker_id => 1, + :author_id => 3, :status_id => 1, + :priority => IssuePriority.all.first, + :subject => 'test_create', :estimated_hours => '1:30') + + assert issue.save + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_stale_issue_should_not_send_email_notification + ActionMailer::Base.deliveries.clear + issue = Issue.find(1) + stale = Issue.find(1) + + issue.init_journal(User.find(1)) + issue.subject = 'Subjet update' + assert issue.save + assert_equal 1, ActionMailer::Base.deliveries.size + ActionMailer::Base.deliveries.clear + + stale.init_journal(User.find(1)) + stale.subject = 'Another subjet update' + assert_raise ActiveRecord::StaleObjectError do + stale.save + end + assert ActionMailer::Base.deliveries.empty? + end + + def test_journalized_description + IssueCustomField.delete_all + + i = Issue.first + old_description = i.description + new_description = "This is the new description" + + i.init_journal(User.find(2)) + i.description = new_description + assert_difference 'Journal.count', 1 do + assert_difference 'JournalDetail.count', 1 do + i.save! + end + end + + detail = JournalDetail.first(:order => 'id DESC') + assert_equal i, detail.journal.journalized + assert_equal 'attr', detail.property + assert_equal 'description', detail.prop_key + assert_equal old_description, detail.old_value + assert_equal new_description, detail.value + end + + def test_blank_descriptions_should_not_be_journalized + IssueCustomField.delete_all + Issue.update_all("description = NULL", "id=1") + + i = Issue.find(1) + i.init_journal(User.find(2)) + i.subject = "blank description" + i.description = "\r\n" + + assert_difference 'Journal.count', 1 do + assert_difference 'JournalDetail.count', 1 do + i.save! + end + end + end + + def test_journalized_multi_custom_field + field = IssueCustomField.create!(:name => 'filter', :field_format => 'list', + :is_filter => true, :is_for_all => true, + :tracker_ids => [1], + :possible_values => ['value1', 'value2', 'value3'], + :multiple => true) + + issue = Issue.create!(:project_id => 1, :tracker_id => 1, + :subject => 'Test', :author_id => 1) + + assert_difference 'Journal.count' do + assert_difference 'JournalDetail.count' do + issue.init_journal(User.first) + issue.custom_field_values = {field.id => ['value1']} + issue.save! + end + assert_difference 'JournalDetail.count' do + issue.init_journal(User.first) + issue.custom_field_values = {field.id => ['value1', 'value2']} + issue.save! + end + assert_difference 'JournalDetail.count', 2 do + issue.init_journal(User.first) + issue.custom_field_values = {field.id => ['value3', 'value2']} + issue.save! + end + assert_difference 'JournalDetail.count', 2 do + issue.init_journal(User.first) + issue.custom_field_values = {field.id => nil} + issue.save! + end + end + end + + def test_description_eol_should_be_normalized + i = Issue.new(:description => "CR \r LF \n CRLF \r\n") + assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description + end + + def test_saving_twice_should_not_duplicate_journal_details + i = Issue.find(:first) + i.init_journal(User.find(2), 'Some notes') + # initial changes + i.subject = 'New subject' + i.done_ratio = i.done_ratio + 10 + assert_difference 'Journal.count' do + assert i.save + end + # 1 more change + i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id]) + assert_no_difference 'Journal.count' do + assert_difference 'JournalDetail.count', 1 do + i.save + end + end + # no more change + assert_no_difference 'Journal.count' do + assert_no_difference 'JournalDetail.count' do + i.save + end + end + end + + def test_all_dependent_issues + IssueRelation.delete_all + assert IssueRelation.create!(:issue_from => Issue.find(1), + :issue_to => Issue.find(2), + :relation_type => IssueRelation::TYPE_PRECEDES) + assert IssueRelation.create!(:issue_from => Issue.find(2), + :issue_to => Issue.find(3), + :relation_type => IssueRelation::TYPE_PRECEDES) + assert IssueRelation.create!(:issue_from => Issue.find(3), + :issue_to => Issue.find(8), + :relation_type => IssueRelation::TYPE_PRECEDES) + + assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort + end + + def test_all_dependent_issues_with_persistent_circular_dependency + IssueRelation.delete_all + assert IssueRelation.create!(:issue_from => Issue.find(1), + :issue_to => Issue.find(2), + :relation_type => IssueRelation::TYPE_PRECEDES) + assert IssueRelation.create!(:issue_from => Issue.find(2), + :issue_to => Issue.find(3), + :relation_type => IssueRelation::TYPE_PRECEDES) + + r = IssueRelation.create!(:issue_from => Issue.find(3), + :issue_to => Issue.find(7), + :relation_type => IssueRelation::TYPE_PRECEDES) + IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id]) + + assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort + end + + def test_all_dependent_issues_with_persistent_multiple_circular_dependencies + IssueRelation.delete_all + assert IssueRelation.create!(:issue_from => Issue.find(1), + :issue_to => Issue.find(2), + :relation_type => IssueRelation::TYPE_RELATES) + assert IssueRelation.create!(:issue_from => Issue.find(2), + :issue_to => Issue.find(3), + :relation_type => IssueRelation::TYPE_RELATES) + assert IssueRelation.create!(:issue_from => Issue.find(3), + :issue_to => Issue.find(8), + :relation_type => IssueRelation::TYPE_RELATES) + + r = IssueRelation.create!(:issue_from => Issue.find(8), + :issue_to => Issue.find(7), + :relation_type => IssueRelation::TYPE_RELATES) + IssueRelation.update_all("issue_to_id = 2", ["id = ?", r.id]) + + r = IssueRelation.create!(:issue_from => Issue.find(3), + :issue_to => Issue.find(7), + :relation_type => IssueRelation::TYPE_RELATES) + IssueRelation.update_all("issue_to_id = 1", ["id = ?", r.id]) + + assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort + end + + context "#done_ratio" do + setup do + @issue = Issue.find(1) + @issue_status = IssueStatus.find(1) + @issue_status.update_attribute(:default_done_ratio, 50) + @issue2 = Issue.find(2) + @issue_status2 = IssueStatus.find(2) + @issue_status2.update_attribute(:default_done_ratio, 0) + end + + teardown do + Setting.issue_done_ratio = 'issue_field' + end + + context "with Setting.issue_done_ratio using the issue_field" do + setup do + Setting.issue_done_ratio = 'issue_field' + end + + should "read the issue's field" do + assert_equal 0, @issue.done_ratio + assert_equal 30, @issue2.done_ratio + end + end + + context "with Setting.issue_done_ratio using the issue_status" do + setup do + Setting.issue_done_ratio = 'issue_status' + end + + should "read the Issue Status's default done ratio" do + assert_equal 50, @issue.done_ratio + assert_equal 0, @issue2.done_ratio + end + end + end + + context "#update_done_ratio_from_issue_status" do + setup do + @issue = Issue.find(1) + @issue_status = IssueStatus.find(1) + @issue_status.update_attribute(:default_done_ratio, 50) + @issue2 = Issue.find(2) + @issue_status2 = IssueStatus.find(2) + @issue_status2.update_attribute(:default_done_ratio, 0) + end + + context "with Setting.issue_done_ratio using the issue_field" do + setup do + Setting.issue_done_ratio = 'issue_field' + end + + should "not change the issue" do + @issue.update_done_ratio_from_issue_status + @issue2.update_done_ratio_from_issue_status + + assert_equal 0, @issue.read_attribute(:done_ratio) + assert_equal 30, @issue2.read_attribute(:done_ratio) + end + end + + context "with Setting.issue_done_ratio using the issue_status" do + setup do + Setting.issue_done_ratio = 'issue_status' + end + + should "change the issue's done ratio" do + @issue.update_done_ratio_from_issue_status + @issue2.update_done_ratio_from_issue_status + + assert_equal 50, @issue.read_attribute(:done_ratio) + assert_equal 0, @issue2.read_attribute(:done_ratio) + end + end + end + + test "#by_tracker" do + User.current = User.anonymous + groups = Issue.by_tracker(Project.find(1)) + assert_equal 3, groups.size + assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i} + end + + test "#by_version" do + User.current = User.anonymous + groups = Issue.by_version(Project.find(1)) + assert_equal 3, groups.size + assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i} + end + + test "#by_priority" do + User.current = User.anonymous + groups = Issue.by_priority(Project.find(1)) + assert_equal 4, groups.size + assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i} + end + + test "#by_category" do + User.current = User.anonymous + groups = Issue.by_category(Project.find(1)) + assert_equal 2, groups.size + assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i} + end + + test "#by_assigned_to" do + User.current = User.anonymous + groups = Issue.by_assigned_to(Project.find(1)) + assert_equal 2, groups.size + assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i} + end + + test "#by_author" do + User.current = User.anonymous + groups = Issue.by_author(Project.find(1)) + assert_equal 4, groups.size + assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i} + end + + test "#by_subproject" do + User.current = User.anonymous + groups = Issue.by_subproject(Project.find(1)) + # Private descendant not visible + assert_equal 1, groups.size + assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i} + end + + def test_recently_updated_scope + #should return the last updated issue + assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first + end + + def test_on_active_projects_scope + assert Project.find(2).archive + + before = Issue.on_active_project.length + # test inclusion to results + issue = Issue.generate!(:tracker => Project.find(2).trackers.first) + assert_equal before + 1, Issue.on_active_project.length + + # Move to an archived project + issue.project = Project.find(2) + assert issue.save + assert_equal before, Issue.on_active_project.length + end + + context "Issue#recipients" do + setup do + @project = Project.find(1) + @author = User.generate! + @assignee = User.generate! + @issue = Issue.generate!(:project => @project, :assigned_to => @assignee, :author => @author) + end + + should "include project recipients" do + assert @project.recipients.present? + @project.recipients.each do |project_recipient| + assert @issue.recipients.include?(project_recipient) + end + end + + should "include the author if the author is active" do + assert @issue.author, "No author set for Issue" + assert @issue.recipients.include?(@issue.author.mail) + end + + should "include the assigned to user if the assigned to user is active" do + assert @issue.assigned_to, "No assigned_to set for Issue" + assert @issue.recipients.include?(@issue.assigned_to.mail) + end + + should "not include users who opt out of all email" do + @author.update_attribute(:mail_notification, :none) + + assert !@issue.recipients.include?(@issue.author.mail) + end + + should "not include the issue author if they are only notified of assigned issues" do + @author.update_attribute(:mail_notification, :only_assigned) + + assert !@issue.recipients.include?(@issue.author.mail) + end + + should "not include the assigned user if they are only notified of owned issues" do + @assignee.update_attribute(:mail_notification, :only_owner) + + assert !@issue.recipients.include?(@issue.assigned_to.mail) + end + end + + def test_last_journal_id_with_journals_should_return_the_journal_id + assert_equal 2, Issue.find(1).last_journal_id + end + + def test_last_journal_id_without_journals_should_return_nil + assert_nil Issue.find(3).last_journal_id + end + + def test_journals_after_should_return_journals_with_greater_id + assert_equal [Journal.find(2)], Issue.find(1).journals_after('1') + assert_equal [], Issue.find(1).journals_after('2') + end + + def test_journals_after_with_blank_arg_should_return_all_journals + assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('') + end + + def test_css_classes_should_include_priority + issue = Issue.new(:priority => IssuePriority.find(8)) + classes = issue.css_classes.split(' ') + assert_include 'priority-8', classes + assert_include 'priority-highest', classes + end + + def test_save_attachments_with_hash_should_save_attachments_in_keys_order + set_tmp_attachments_directory + issue = Issue.generate! + issue.save_attachments({ + 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')}, + '3' => {'file' => mock_file_with_options(:original_filename => 'bar')}, + '1' => {'file' => mock_file_with_options(:original_filename => 'foo')} + }) + issue.attach_saved_attachments + + assert_equal 3, issue.reload.attachments.count + assert_equal %w(upload foo bar), issue.attachments.map(&:filename) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4b/4b6f435406424f568a3074e07ecf7adb9983d62f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4b/4b6f435406424f568a3074e07ecf7adb9983d62f.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,64 @@ +# The MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# This implements native php methods used by tcpdf, which have had to be +# reimplemented within Ruby. + +module RFPDF + + # http://uk2.php.net/getimagesize + def getimagesize(filename) + image = Magick::ImageList.new(filename) + + out = Hash.new + out[0] = image.columns + out[1] = image.rows + + # These are actually meant to return integer values But I couldn't seem to find anything saying what those values are. + # So for now they return strings. The only place that uses this at the moment is the parsejpeg method, so I've changed that too. + case image.mime_type + when "image/gif" + out[2] = "GIF" + when "image/jpeg" + out[2] = "JPEG" + when "image/png" + out[2] = "PNG" + when " image/vnd.wap.wbmp" + out[2] = "WBMP" + when "image/x-xpixmap" + out[2] = "XPM" + end + out[3] = "height=\"#{image.rows}\" width=\"#{image.columns}\"" + out['mime'] = image.mime_type + + # This needs work to cover more situations + # I can't see how to just list the number of channels with ImageMagick / rmagick + if image.colorspace.to_s == "CMYKColorspace" + out['channels'] = 4 + elsif image.colorspace.to_s == "RGBColorspace" + out['channels'] = 3 + end + + out['bits'] = image.channel_depth + + out + end + +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4b/4b74e44b97b87b2ad397389fda8790b5d01b0fe4.svn-base Binary file .svn/pristine/4b/4b74e44b97b87b2ad397389fda8790b5d01b0fe4.svn-base has changed diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4b/4bc7e4b0cb93062cfdf78f8e648d19270494056a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4b/4bc7e4b0cb93062cfdf78f8e648d19270494056a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,413 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module IssuesHelper + include ApplicationHelper + + def issue_list(issues, &block) + ancestors = [] + issues.each do |issue| + while (ancestors.any? && !issue.is_descendant_of?(ancestors.last)) + ancestors.pop + end + yield issue, ancestors.size + ancestors << issue unless issue.leaf? + end + end + + # Renders a HTML/CSS tooltip + # + # To use, a trigger div is needed. This is a div with the class of "tooltip" + # that contains this method wrapped in a span with the class of "tip" + # + #
    <%= link_to_issue(issue) %> + # <%= render_issue_tooltip(issue) %> + #
    + # + def render_issue_tooltip(issue) + @cached_label_status ||= l(:field_status) + @cached_label_start_date ||= l(:field_start_date) + @cached_label_due_date ||= l(:field_due_date) + @cached_label_assigned_to ||= l(:field_assigned_to) + @cached_label_priority ||= l(:field_priority) + @cached_label_project ||= l(:field_project) + + link_to_issue(issue) + "

    ".html_safe + + "#{@cached_label_project}: #{link_to_project(issue.project)}
    ".html_safe + + "#{@cached_label_status}: #{h(issue.status.name)}
    ".html_safe + + "#{@cached_label_start_date}: #{format_date(issue.start_date)}
    ".html_safe + + "#{@cached_label_due_date}: #{format_date(issue.due_date)}
    ".html_safe + + "#{@cached_label_assigned_to}: #{h(issue.assigned_to)}
    ".html_safe + + "#{@cached_label_priority}: #{h(issue.priority.name)}".html_safe + end + + def issue_heading(issue) + h("#{issue.tracker} ##{issue.id}") + end + + def render_issue_subject_with_tree(issue) + s = '' + ancestors = issue.root? ? [] : issue.ancestors.visible.all + ancestors.each do |ancestor| + s << '
    ' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id))) + end + s << '
    ' + subject = h(issue.subject) + if issue.is_private? + subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject + end + s << content_tag('h3', subject) + s << '
    ' * (ancestors.size + 1) + s.html_safe + end + + def render_descendants_tree(issue) + s = '
    ' + issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level| + css = "issue issue-#{child.id} hascontextmenu" + css << " idnt idnt-#{level}" if level > 0 + s << content_tag('tr', + content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') + + content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') + + content_tag('td', h(child.status)) + + content_tag('td', link_to_user(child.assigned_to)) + + content_tag('td', progress_bar(child.done_ratio, :width => '80px')), + :class => css) + end + s << '
    ' + s.html_safe + end + + # Returns a link for adding a new subtask to the given issue + def link_to_new_subtask(issue) + attrs = { + :tracker_id => issue.tracker, + :parent_issue_id => issue + } + link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs)) + end + + class IssueFieldsRows + include ActionView::Helpers::TagHelper + + def initialize + @left = [] + @right = [] + end + + def left(*args) + args.any? ? @left << cells(*args) : @left + end + + def right(*args) + args.any? ? @right << cells(*args) : @right + end + + def size + @left.size > @right.size ? @left.size : @right.size + end + + def to_html + html = ''.html_safe + blank = content_tag('th', '') + content_tag('td', '') + size.times do |i| + left = @left[i] || blank + right = @right[i] || blank + html << content_tag('tr', left + right) + end + html + end + + def cells(label, text, options={}) + content_tag('th', "#{label}:", options) + content_tag('td', text, options) + end + end + + def issue_fields_rows + r = IssueFieldsRows.new + yield r + r.to_html + end + + def render_custom_fields_rows(issue) + return if issue.custom_field_values.empty? + ordered_values = [] + half = (issue.custom_field_values.size / 2.0).ceil + half.times do |i| + ordered_values << issue.custom_field_values[i] + ordered_values << issue.custom_field_values[i + half] + end + s = "\n" + n = 0 + ordered_values.compact.each do |value| + s << "\n\n" if n > 0 && (n % 2) == 0 + s << "\t#{ h(value.custom_field.name) }:#{ simple_format_without_paragraph(h(show_value(value))) }\n" + n += 1 + end + s << "\n" + s.html_safe + end + + def issues_destroy_confirmation_message(issues) + issues = [issues] unless issues.is_a?(Array) + message = l(:text_issues_destroy_confirmation) + descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2} + if descendant_count > 0 + issues.each do |issue| + next if issue.root? + issues.each do |other_issue| + descendant_count -= 1 if issue.is_descendant_of?(other_issue) + end + end + if descendant_count > 0 + message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count) + end + end + message + end + + def sidebar_queries + unless @sidebar_queries + @sidebar_queries = Query.visible.all( + :order => "#{Query.table_name}.name ASC", + # Project specific queries and global queries + :conditions => (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]) + ) + end + @sidebar_queries + end + + def query_links(title, queries) + # links to #index on issues/show + url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params + + content_tag('h3', h(title)) + + queries.collect {|query| + css = 'query' + css << ' selected' if query == @query + link_to(h(query.name), url_params.merge(:query_id => query), :class => css) + }.join('
    ').html_safe + end + + def render_sidebar_queries + out = ''.html_safe + queries = sidebar_queries.select {|q| !q.is_public?} + out << query_links(l(:label_my_queries), queries) if queries.any? + queries = sidebar_queries.select {|q| q.is_public?} + out << query_links(l(:label_query_plural), queries) if queries.any? + out + end + + # Returns the textual representation of a journal details + # as an array of strings + def details_to_strings(details, no_html=false, options={}) + options[:only_path] = (options[:only_path] == false ? false : true) + strings = [] + values_by_field = {} + details.each do |detail| + if detail.property == 'cf' + field_id = detail.prop_key + field = CustomField.find_by_id(field_id) + if field && field.multiple? + values_by_field[field_id] ||= {:added => [], :deleted => []} + if detail.old_value + values_by_field[field_id][:deleted] << detail.old_value + end + if detail.value + values_by_field[field_id][:added] << detail.value + end + next + end + end + strings << show_detail(detail, no_html, options) + end + values_by_field.each do |field_id, changes| + detail = JournalDetail.new(:property => 'cf', :prop_key => field_id) + if changes[:added].any? + detail.value = changes[:added] + strings << show_detail(detail, no_html, options) + elsif changes[:deleted].any? + detail.old_value = changes[:deleted] + strings << show_detail(detail, no_html, options) + end + end + strings + end + + # Returns the textual representation of a single journal detail + def show_detail(detail, no_html=false, options={}) + multiple = false + case detail.property + when 'attr' + field = detail.prop_key.to_s.gsub(/\_id$/, "") + label = l(("field_" + field).to_sym) + case detail.prop_key + when 'due_date', 'start_date' + value = format_date(detail.value.to_date) if detail.value + old_value = format_date(detail.old_value.to_date) if detail.old_value + + when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id', + 'priority_id', 'category_id', 'fixed_version_id' + value = find_name_by_reflection(field, detail.value) + old_value = find_name_by_reflection(field, detail.old_value) + + when 'estimated_hours' + value = "%0.02f" % detail.value.to_f unless detail.value.blank? + old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank? + + when 'parent_id' + label = l(:field_parent_issue) + value = "##{detail.value}" unless detail.value.blank? + old_value = "##{detail.old_value}" unless detail.old_value.blank? + + when 'is_private' + value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank? + old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank? + end + when 'cf' + custom_field = CustomField.find_by_id(detail.prop_key) + if custom_field + multiple = custom_field.multiple? + label = custom_field.name + value = format_value(detail.value, custom_field.field_format) if detail.value + old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value + end + when 'attachment' + label = l(:label_attachment) + end + call_hook(:helper_issues_show_detail_after_setting, + {:detail => detail, :label => label, :value => value, :old_value => old_value }) + + label ||= detail.prop_key + value ||= detail.value + old_value ||= detail.old_value + + unless no_html + label = content_tag('strong', label) + old_value = content_tag("i", h(old_value)) if detail.old_value + old_value = content_tag("del", old_value) if detail.old_value and detail.value.blank? + if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key) + # Link to the attachment if it has not been removed + value = link_to_attachment(atta, :download => true, :only_path => options[:only_path]) + if options[:only_path] != false && atta.is_text? + value += link_to( + image_tag('magnifier.png'), + :controller => 'attachments', :action => 'show', + :id => atta, :filename => atta.filename + ) + end + else + value = content_tag("i", h(value)) if value + end + end + + if detail.property == 'attr' && detail.prop_key == 'description' + s = l(:text_journal_changed_no_detail, :label => label) + unless no_html + diff_link = link_to 'diff', + {:controller => 'journals', :action => 'diff', :id => detail.journal_id, + :detail_id => detail.id, :only_path => options[:only_path]}, + :title => l(:label_view_diff) + s << " (#{ diff_link })" + end + s.html_safe + elsif detail.value.present? + case detail.property + when 'attr', 'cf' + if detail.old_value.present? + l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe + elsif multiple + l(:text_journal_added, :label => label, :value => value).html_safe + else + l(:text_journal_set_to, :label => label, :value => value).html_safe + end + when 'attachment' + l(:text_journal_added, :label => label, :value => value).html_safe + end + else + l(:text_journal_deleted, :label => label, :old => old_value).html_safe + end + end + + # Find the name of an associated record stored in the field attribute + def find_name_by_reflection(field, id) + association = Issue.reflect_on_association(field.to_sym) + if association + record = association.class_name.constantize.find_by_id(id) + if record + record.name.force_encoding('UTF-8') if record.name.respond_to?(:force_encoding) + return record.name + end + end + end + + # Renders issue children recursively + def render_api_issue_children(issue, api) + return if issue.leaf? + api.array :children do + issue.children.each do |child| + api.issue(:id => child.id) do + api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil? + api.subject child.subject + render_api_issue_children(child, api) + end + end + end + end + + def issues_to_csv(issues, project, query, options={}) + decimal_separator = l(:general_csv_decimal_separator) + encoding = l(:general_csv_encoding) + columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns) + if options[:description] + if description = query.available_columns.detect {|q| q.name == :description} + columns << description + end + end + + export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv| + # csv header fields + csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) } + + # csv lines + issues.each do |issue| + col_values = columns.collect do |column| + s = if column.is_a?(QueryCustomFieldColumn) + cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id} + show_value(cv) + else + value = column.value(issue) + if value.is_a?(Date) + format_date(value) + elsif value.is_a?(Time) + format_time(value) + elsif value.is_a?(Float) + ("%.2f" % value).gsub('.', decimal_separator) + else + value + end + end + s.to_s + end + csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } + end + end + export + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4b/4bfc330a9d4cbc673ab502ead9aa3a656ef7cc84.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4b/4bfc330a9d4cbc673ab502ead9aa3a656ef7cc84.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,22 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class TimeEntryActivityCustomField < CustomField + def type_name + :enumeration_activities + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4b/4bfe6e502822b6bab23493339bf9eb8eb1ffb47c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4b/4bfe6e502822b6bab23493339bf9eb8eb1ffb47c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,66 @@ + + + + + +Wiki formatting + + + + +

    Wiki Syntax Quick Reference

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Font Styles
    Strong*Strong*Strong
    Italic_Italic_Italic
    Underline+Underline+Underline
    Deleted-Deleted-Deleted
    ??Quote??Quote
    Inline Code@Inline Code@Inline Code
    Preformatted text<pre>
     lines
     of code
    </pre>
    +
    + lines
    + of code
    +
    +
    Lists
    Unordered list* Item 1
    * Item 2
    • Item 1
    • Item 2
    Ordered list# Item 1
    # Item 2
    1. Item 1
    2. Item 2
    Headings
    Heading 1h1. Title 1

    Title 1

    Heading 2h2. Title 2

    Title 2

    Heading 3h3. Title 3

    Title 3

    Links
    http://foo.barhttp://foo.bar
    "Foo":http://foo.barFoo
    Redmine links
    Link to a Wiki page[[Wiki page]]Wiki page
    Issue #12Issue #12
    Revision r43Revision r43
    commit:f30e13e43f30e13e4
    source:some/filesource:some/file
    Inline images
    Image!image_url!
    !attached_image!
    + +

    More Information

    + + + diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4c/4c88c6058655e1eb05ec3a1a293a556537a3380e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4c/4c88c6058655e1eb05ec3a1a293a556537a3380e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,65 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class QueriesHelperTest < ActionView::TestCase + include QueriesHelper + include Redmine::I18n + + fixtures :projects, :enabled_modules, :users, :members, + :member_roles, :roles, :trackers, :issue_statuses, + :issue_categories, :enumerations, :issues, + :watchers, :custom_fields, :custom_values, :versions, + :queries, + :projects_trackers, + :custom_fields_trackers + + def test_order + User.current = User.find_by_login('admin') + query = Query.new(:project => nil, :name => '_') + assert_equal 30, query.available_filters.size + fo = filters_options(query) + assert_equal 31, fo.size + assert_equal [], fo[0] + assert_equal "status_id", fo[1][1] + assert_equal "project_id", fo[2][1] + assert_equal "tracker_id", fo[3][1] + assert_equal "priority_id", fo[4][1] + assert_equal "watcher_id", fo[17][1] + assert_equal "is_private", fo[18][1] + end + + def test_order_custom_fields + set_language_if_valid 'en' + field = UserCustomField.new( + :name => 'order test', :field_format => 'string', + :is_for_all => true, :is_filter => true + ) + assert field.save + User.current = User.find_by_login('admin') + query = Query.new(:project => nil, :name => '_') + assert_equal 32, query.available_filters.size + fo = filters_options(query) + assert_equal 33, fo.size + assert_equal "Searchable field", fo[19][0] + assert_equal "Database", fo[20][0] + assert_equal "Project's Development status", fo[21][0] + assert_equal "Assignee's order test", fo[22][0] + assert_equal "Author's order test", fo[23][0] + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4c/4c8a16342f0eed8d7b98e1ecd16d21a60462a3df.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4c/4c8a16342f0eed8d7b98e1ecd16d21a60462a3df.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,15 @@ +

    Hello

    + +

    <%= l(:text_say_hello) %>

    + +

    : <%= @value %>

    + +<%= link_to('Good bye', :action => 'say_goodbye', :id => @project) if User.current.allowed_to?(:example_say_goodbye, @project) %> + +<% content_for :sidebar do %> +

    Adding content to the sidebar...

    +<% end %> + +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'example', :plugin => 'sample_plugin', :media => "screen" %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4c/4c8e100ee7d3597387e1269a2cf346b1bfb61a50.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4c/4c8e100ee7d3597387e1269a2cf346b1bfb61a50.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,29 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class UsersTest < ActionController::IntegrationTest + fixtures :users + + def test_destroy_should_not_accept_get_requests + assert_no_difference 'User.count' do + get '/users/destroy/2', {}, credentials('admin') + assert_response 404 + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4c/4c998cfe872582fc9c34765173f146ee719b1024.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4c/4c998cfe872582fc9c34765173f146ee719b1024.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,165 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class RepositoriesDarcsControllerTest < ActionController::TestCase + tests RepositoriesController + + fixtures :projects, :users, :roles, :members, :member_roles, + :repositories, :enabled_modules + + REPOSITORY_PATH = Rails.root.join('tmp/test/darcs_repository').to_s + PRJ_ID = 3 + NUM_REV = 6 + + def setup + User.current = nil + @project = Project.find(PRJ_ID) + @repository = Repository::Darcs.create( + :project => @project, + :url => REPOSITORY_PATH, + :log_encoding => 'UTF-8' + ) + assert @repository + end + + if File.directory?(REPOSITORY_PATH) + def test_get_new + @request.session[:user_id] = 1 + @project.repository.destroy + get :new, :project_id => 'subproject1', :repository_scm => 'Darcs' + assert_response :success + assert_template 'new' + assert_kind_of Repository::Darcs, assigns(:repository) + assert assigns(:repository).new_record? + end + + def test_browse_root + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal 3, assigns(:entries).size + assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'} + end + + def test_browse_directory + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param] + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name) + entry = assigns(:entries).detect {|e| e.name == 'edit.png'} + assert_not_nil entry + assert_equal 'file', entry.kind + assert_equal 'images/edit.png', entry.path + end + + def test_browse_at_given_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param], + :rev => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['delete.png'], assigns(:entries).collect(&:name) + end + + def test_changes + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :changes, :id => PRJ_ID, + :path => repository_path_hash(['images', 'edit.png'])[:param] + assert_response :success + assert_template 'changes' + assert_tag :tag => 'h2', :content => 'edit.png' + end + + def test_diff + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + # Full diff of changeset 5 + ['inline', 'sbs'].each do |dt| + get :diff, :id => PRJ_ID, :rev => 5, :type => dt + assert_response :success + assert_template 'diff' + # Line 22 removed + assert_tag :tag => 'th', + :content => '22', + :sibling => { :tag => 'td', + :attributes => { :class => /diff_out/ }, + :content => /def remove/ } + end + end + + def test_destroy_valid_repository + @request.session[:user_id] = 1 # admin + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + + def test_destroy_invalid_repository + @request.session[:user_id] = 1 # admin + @project.repository.destroy + @repository = Repository::Darcs.create!( + :project => @project, + :url => "/invalid", + :log_encoding => 'UTF-8' + ) + @repository.fetch_changesets + @project.reload + assert_equal 0, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + else + puts "Darcs test repository NOT FOUND. Skipping functional tests !!!" + def test_fake; assert true end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4c/4cd6357962628f4e7adbb5bc6619f8171595a014.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4c/4cd6357962628f4e7adbb5bc6619f8171595a014.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,72 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Search + + mattr_accessor :available_search_types + + @@available_search_types = [] + + class << self + def map(&block) + yield self + end + + # Registers a search provider + def register(search_type, options={}) + search_type = search_type.to_s + @@available_search_types << search_type unless @@available_search_types.include?(search_type) + end + end + + module Controller + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + @@default_search_scopes = Hash.new {|hash, key| hash[key] = {:default => nil, :actions => {}}} + mattr_accessor :default_search_scopes + + # Set the default search scope for a controller or specific actions + # Examples: + # * search_scope :issues # => sets the search scope to :issues for the whole controller + # * search_scope :issues, :only => :index + # * search_scope :issues, :only => [:index, :show] + def default_search_scope(id, options = {}) + if actions = options[:only] + actions = [] << actions unless actions.is_a?(Array) + actions.each {|a| default_search_scopes[controller_name.to_sym][:actions][a.to_sym] = id.to_s} + else + default_search_scopes[controller_name.to_sym][:default] = id.to_s + end + end + end + + def default_search_scopes + self.class.default_search_scopes + end + + # Returns the default search scope according to the current action + def default_search_scope + @default_search_scope ||= default_search_scopes[controller_name.to_sym][:actions][action_name.to_sym] || + default_search_scopes[controller_name.to_sym][:default] + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4d/4d303c92e6935bfe0bbc913277addc1ba08aef2e.svn-base --- a/.svn/pristine/4d/4d303c92e6935bfe0bbc913277addc1ba08aef2e.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,272 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class ProjectsController < ApplicationController - menu_item :overview - menu_item :roadmap, :only => :roadmap - menu_item :settings, :only => :settings - - before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ] - before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy] - before_filter :authorize_global, :only => [:new, :create] - before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ] - accept_rss_auth :index - accept_api_auth :index, :show, :create, :update, :destroy - - after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller| - if controller.request.post? - controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt' - end - end - - helper :sort - include SortHelper - helper :custom_fields - include CustomFieldsHelper - helper :issues - helper :queries - include QueriesHelper - helper :repositories - include RepositoriesHelper - include ProjectsHelper - - # Lists visible projects - def index - respond_to do |format| - format.html { - @projects = Project.visible.find(:all, :order => 'lft') - } - format.api { - @offset, @limit = api_offset_and_limit - @project_count = Project.visible.count - @projects = Project.visible.all(:offset => @offset, :limit => @limit, :order => 'lft') - } - format.atom { - projects = Project.visible.find(:all, :order => 'created_on DESC', - :limit => Setting.feeds_limit.to_i) - render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}") - } - end - end - - def new - @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") - @trackers = Tracker.all - @project = Project.new(params[:project]) - end - - verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed } - def create - @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") - @trackers = Tracker.all - @project = Project.new - @project.safe_attributes = params[:project] - - if validate_parent_id && @project.save - @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') - # Add current user as a project member if he is not admin - unless User.current.admin? - r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first - m = Member.new(:user => User.current, :roles => [r]) - @project.members << m - end - respond_to do |format| - format.html { - flash[:notice] = l(:notice_successful_create) - redirect_to(params[:continue] ? - {:controller => 'projects', :action => 'new', :project => {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}} : - {:controller => 'projects', :action => 'settings', :id => @project} - ) - } - format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) } - end - else - respond_to do |format| - format.html { render :action => 'new' } - format.api { render_validation_errors(@project) } - end - end - - end - - def copy - @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") - @trackers = Tracker.all - @root_projects = Project.find(:all, - :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}", - :order => 'name') - @source_project = Project.find(params[:id]) - if request.get? - @project = Project.copy_from(@source_project) - if @project - @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers? - else - redirect_to :controller => 'admin', :action => 'projects' - end - else - Mailer.with_deliveries(params[:notifications] == '1') do - @project = Project.new - @project.safe_attributes = params[:project] - if validate_parent_id && @project.copy(@source_project, :only => params[:only]) - @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') - flash[:notice] = l(:notice_successful_create) - redirect_to :controller => 'projects', :action => 'settings', :id => @project - elsif !@project.new_record? - # Project was created - # But some objects were not copied due to validation failures - # (eg. issues from disabled trackers) - # TODO: inform about that - redirect_to :controller => 'projects', :action => 'settings', :id => @project - end - end - end - rescue ActiveRecord::RecordNotFound - redirect_to :controller => 'admin', :action => 'projects' - end - - # Show @project - def show - if params[:jump] - # try to redirect to the requested menu item - redirect_to_project_menu_item(@project, params[:jump]) && return - end - - @users_by_role = @project.users_by_role - @subprojects = @project.children.visible.all - @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") - @trackers = @project.rolled_up_trackers - - cond = @project.project_condition(Setting.display_subprojects_issues?) - - @open_issues_by_tracker = Issue.visible.count(:group => :tracker, - :include => [:project, :status, :tracker], - :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false]) - @total_issues_by_tracker = Issue.visible.count(:group => :tracker, - :include => [:project, :status, :tracker], - :conditions => cond) - - if User.current.allowed_to?(:view_time_entries, @project) - @total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f - end - - @key = User.current.rss_key - - respond_to do |format| - format.html - format.api - end - end - - def settings - @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") - @issue_category ||= IssueCategory.new - @member ||= @project.members.new - @trackers = Tracker.all - @repository ||= @project.repository - @wiki ||= @project.wiki - end - - def edit - end - - # TODO: convert to PUT only - verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed } - def update - @project.safe_attributes = params[:project] - if validate_parent_id && @project.save - @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') - respond_to do |format| - format.html { - flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'settings', :id => @project - } - format.api { head :ok } - end - else - respond_to do |format| - format.html { - settings - render :action => 'settings' - } - format.api { render_validation_errors(@project) } - end - end - end - - verify :method => :post, :only => :modules, :render => {:nothing => true, :status => :method_not_allowed } - def modules - @project.enabled_module_names = params[:enabled_module_names] - flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'settings', :id => @project, :tab => 'modules' - end - - def archive - if request.post? - unless @project.archive - flash[:error] = l(:error_can_not_archive_project) - end - end - redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status])) - end - - def unarchive - @project.unarchive if request.post? && !@project.active? - redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status])) - end - - # Delete @project - def destroy - @project_to_destroy = @project - if request.get? - # display confirmation view - else - if api_request? || params[:confirm] - @project_to_destroy.destroy - respond_to do |format| - format.html { redirect_to :controller => 'admin', :action => 'projects' } - format.api { head :ok } - end - end - end - # hide project in layout - @project = nil - end - -private - def find_optional_project - return true unless params[:id] - @project = Project.find(params[:id]) - authorize - rescue ActiveRecord::RecordNotFound - render_404 - end - - # Validates parent_id param according to user's permissions - # TODO: move it to Project model in a validation that depends on User.current - def validate_parent_id - return true if User.current.admin? - parent_id = params[:project] && params[:project][:parent_id] - if parent_id || @project.new_record? - parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i) - unless @project.allowed_parents.include?(parent) - @project.errors.add :parent_id, :invalid - return false - end - end - true - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4e/4e41004227c3289cb2b10e8f3de41eb2d30436ae.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4e/4e41004227c3289cb2b10e8f3de41eb2d30436ae.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,62 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'net/pop' + +module Redmine + module POP3 + class << self + def check(pop_options={}, options={}) + host = pop_options[:host] || '127.0.0.1' + port = pop_options[:port] || '110' + apop = (pop_options[:apop].to_s == '1') + delete_unprocessed = (pop_options[:delete_unprocessed].to_s == '1') + + pop = Net::POP3.APOP(apop).new(host,port) + logger.debug "Connecting to #{host}..." if logger && logger.debug? + pop.start(pop_options[:username], pop_options[:password]) do |pop_session| + if pop_session.mails.empty? + logger.debug "No email to process" if logger && logger.debug? + else + logger.debug "#{pop_session.mails.size} email(s) to process..." if logger && logger.debug? + pop_session.each_mail do |msg| + message = msg.pop + message_id = (message =~ /^Message-I[dD]: (.*)/ ? $1 : '').strip + if MailHandler.receive(message, options) + msg.delete + logger.debug "--> Message #{message_id} processed and deleted from the server" if logger && logger.debug? + else + if delete_unprocessed + msg.delete + logger.debug "--> Message #{message_id} NOT processed and deleted from the server" if logger && logger.debug? + else + logger.debug "--> Message #{message_id} NOT processed and left on the server" if logger && logger.debug? + end + end + end + end + end + end + + private + + def logger + ::Rails.logger + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4e/4e65aa6af49f7dc6aa8157532f6bed6bfe2ae6d6.svn-base --- a/.svn/pristine/4e/4e65aa6af49f7dc6aa8157532f6bed6bfe2ae6d6.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,636 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -require "digest/sha1" - -class User < Principal - include Redmine::SafeAttributes - - # Account statuses - STATUS_ANONYMOUS = 0 - STATUS_ACTIVE = 1 - STATUS_REGISTERED = 2 - STATUS_LOCKED = 3 - - # Different ways of displaying/sorting users - USER_FORMATS = { - :firstname_lastname => {:string => '#{firstname} #{lastname}', :order => %w(firstname lastname id)}, - :firstname => {:string => '#{firstname}', :order => %w(firstname id)}, - :lastname_firstname => {:string => '#{lastname} #{firstname}', :order => %w(lastname firstname id)}, - :lastname_coma_firstname => {:string => '#{lastname}, #{firstname}', :order => %w(lastname firstname id)}, - :username => {:string => '#{login}', :order => %w(login id)}, - } - - MAIL_NOTIFICATION_OPTIONS = [ - ['all', :label_user_mail_option_all], - ['selected', :label_user_mail_option_selected], - ['only_my_events', :label_user_mail_option_only_my_events], - ['only_assigned', :label_user_mail_option_only_assigned], - ['only_owner', :label_user_mail_option_only_owner], - ['none', :label_user_mail_option_none] - ] - - has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)}, - :after_remove => Proc.new {|user, group| group.user_removed(user)} - has_many :changesets, :dependent => :nullify - has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' - has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'" - has_one :api_token, :class_name => 'Token', :conditions => "action='api'" - belongs_to :auth_source - - # Active non-anonymous users scope - named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}" - - acts_as_customizable - - attr_accessor :password, :password_confirmation - attr_accessor :last_before_login_on - # Prevents unauthorized assignments - attr_protected :login, :admin, :password, :password_confirmation, :hashed_password - - validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } - validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false - validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false - # Login must contain lettres, numbers, underscores only - validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i - validates_length_of :login, :maximum => 30 - validates_length_of :firstname, :lastname, :maximum => 30 - validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_blank => true - validates_length_of :mail, :maximum => 60, :allow_nil => true - validates_confirmation_of :password, :allow_nil => true - validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true - validate :validate_password_length - - before_create :set_mail_notification - before_save :update_hashed_password - before_destroy :remove_references_before_destroy - - named_scope :in_group, lambda {|group| - group_id = group.is_a?(Group) ? group.id : group.to_i - { :conditions => ["#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] } - } - named_scope :not_in_group, lambda {|group| - group_id = group.is_a?(Group) ? group.id : group.to_i - { :conditions => ["#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] } - } - - def set_mail_notification - self.mail_notification = Setting.default_notification_option if self.mail_notification.blank? - true - end - - def update_hashed_password - # update hashed_password if password was set - if self.password && self.auth_source_id.blank? - salt_password(password) - end - end - - def reload(*args) - @name = nil - @projects_by_role = nil - super - end - - def mail=(arg) - write_attribute(:mail, arg.to_s.strip) - end - - def identity_url=(url) - if url.blank? - write_attribute(:identity_url, '') - else - begin - write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url)) - rescue OpenIdAuthentication::InvalidOpenId - # Invlaid url, don't save - end - end - self.read_attribute(:identity_url) - end - - # Returns the user that matches provided login and password, or nil - def self.try_to_login(login, password) - # Make sure no one can sign in with an empty password - return nil if password.to_s.empty? - user = find_by_login(login) - if user - # user is already in local database - return nil if !user.active? - if user.auth_source - # user has an external authentication method - return nil unless user.auth_source.authenticate(login, password) - else - # authentication with local password - return nil unless user.check_password?(password) - end - else - # user is not yet registered, try to authenticate with available sources - attrs = AuthSource.authenticate(login, password) - if attrs - user = new(attrs) - user.login = login - user.language = Setting.default_language - if user.save - user.reload - logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source - end - end - end - user.update_attribute(:last_login_on, Time.now) if user && !user.new_record? - user - rescue => text - raise text - end - - # Returns the user who matches the given autologin +key+ or nil - def self.try_to_autologin(key) - tokens = Token.find_all_by_action_and_value('autologin', key) - # Make sure there's only 1 token that matches the key - if tokens.size == 1 - token = tokens.first - if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active? - token.user.update_attribute(:last_login_on, Time.now) - token.user - end - end - end - - def self.name_formatter(formatter = nil) - USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname] - end - - # Returns an array of fields names than can be used to make an order statement for users - # according to how user names are displayed - # Examples: - # - # User.fields_for_order_statement => ['users.login', 'users.id'] - # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id'] - def self.fields_for_order_statement(table=nil) - table ||= table_name - name_formatter[:order].map {|field| "#{table}.#{field}"} - end - - # Return user's full name for display - def name(formatter = nil) - f = self.class.name_formatter(formatter) - if formatter - eval('"' + f[:string] + '"') - else - @name ||= eval('"' + f[:string] + '"') - end - end - - def active? - self.status == STATUS_ACTIVE - end - - def registered? - self.status == STATUS_REGISTERED - end - - def locked? - self.status == STATUS_LOCKED - end - - def activate - self.status = STATUS_ACTIVE - end - - def register - self.status = STATUS_REGISTERED - end - - def lock - self.status = STATUS_LOCKED - end - - def activate! - update_attribute(:status, STATUS_ACTIVE) - end - - def register! - update_attribute(:status, STATUS_REGISTERED) - end - - def lock! - update_attribute(:status, STATUS_LOCKED) - end - - # Returns true if +clear_password+ is the correct user's password, otherwise false - def check_password?(clear_password) - if auth_source_id.present? - auth_source.authenticate(self.login, clear_password) - else - User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password - end - end - - # Generates a random salt and computes hashed_password for +clear_password+ - # The hashed password is stored in the following form: SHA1(salt + SHA1(password)) - def salt_password(clear_password) - self.salt = User.generate_salt - self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}") - end - - # Does the backend storage allow this user to change their password? - def change_password_allowed? - return true if auth_source_id.blank? - return auth_source.allow_password_changes? - end - - # Generate and set a random password. Useful for automated user creation - # Based on Token#generate_token_value - # - def random_password - chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a - password = '' - 40.times { |i| password << chars[rand(chars.size-1)] } - self.password = password - self.password_confirmation = password - self - end - - def pref - self.preference ||= UserPreference.new(:user => self) - end - - def time_zone - @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone]) - end - - def wants_comments_in_reverse_order? - self.pref[:comments_sorting] == 'desc' - end - - # Return user's RSS key (a 40 chars long string), used to access feeds - def rss_key - token = self.rss_token || Token.create(:user => self, :action => 'feeds') - token.value - end - - # Return user's API key (a 40 chars long string), used to access the API - def api_key - token = self.api_token || self.create_api_token(:action => 'api') - token.value - end - - # Return an array of project ids for which the user has explicitly turned mail notifications on - def notified_projects_ids - @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id) - end - - def notified_project_ids=(ids) - Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id]) - Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty? - @notified_projects_ids = nil - notified_projects_ids - end - - def valid_notification_options - self.class.valid_notification_options(self) - end - - # Only users that belong to more than 1 project can select projects for which they are notified - def self.valid_notification_options(user=nil) - # Note that @user.membership.size would fail since AR ignores - # :include association option when doing a count - if user.nil? || user.memberships.length < 1 - MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'} - else - MAIL_NOTIFICATION_OPTIONS - end - end - - # Find a user account by matching the exact login and then a case-insensitive - # version. Exact matches will be given priority. - def self.find_by_login(login) - # force string comparison to be case sensitive on MySQL - type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : '' - - # First look for an exact match - user = first(:conditions => ["#{type_cast} login = ?", login]) - # Fail over to case-insensitive if none was found - user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase]) - end - - def self.find_by_rss_key(key) - token = Token.find_by_value(key) - token && token.user.active? ? token.user : nil - end - - def self.find_by_api_key(key) - token = Token.find_by_action_and_value('api', key) - token && token.user.active? ? token.user : nil - end - - # Makes find_by_mail case-insensitive - def self.find_by_mail(mail) - find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase]) - end - - def to_s - name - end - - # Returns the current day according to user's time zone - def today - if time_zone.nil? - Date.today - else - Time.now.in_time_zone(time_zone).to_date - end - end - - def logged? - true - end - - def anonymous? - !logged? - end - - # Return user's roles for project - def roles_for_project(project) - roles = [] - # No role on archived projects - return roles unless project && project.active? - if logged? - # Find project membership - membership = memberships.detect {|m| m.project_id == project.id} - if membership - roles = membership.roles - else - @role_non_member ||= Role.non_member - roles << @role_non_member - end - else - @role_anonymous ||= Role.anonymous - roles << @role_anonymous - end - roles - end - - # Return true if the user is a member of project - def member_of?(project) - !roles_for_project(project).detect {|role| role.member?}.nil? - end - - # Returns a hash of user's projects grouped by roles - def projects_by_role - return @projects_by_role if @projects_by_role - - @projects_by_role = Hash.new {|h,k| h[k]=[]} - memberships.each do |membership| - membership.roles.each do |role| - @projects_by_role[role] << membership.project if membership.project - end - end - @projects_by_role.each do |role, projects| - projects.uniq! - end - - @projects_by_role - end - - # Returns true if user is arg or belongs to arg - def is_or_belongs_to?(arg) - if arg.is_a?(User) - self == arg - elsif arg.is_a?(Group) - arg.users.include?(self) - else - false - end - end - - # Return true if the user is allowed to do the specified action on a specific context - # Action can be: - # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') - # * a permission Symbol (eg. :edit_project) - # Context can be: - # * a project : returns true if user is allowed to do the specified action on this project - # * an array of projects : returns true if user is allowed on every project - # * nil with options[:global] set : check if user has at least one role allowed for this action, - # or falls back to Non Member / Anonymous permissions depending if the user is logged - def allowed_to?(action, context, options={}, &block) - if context && context.is_a?(Project) - # No action allowed on archived projects - return false unless context.active? - # No action allowed on disabled modules - return false unless context.allows_to?(action) - # Admin users are authorized for anything else - return true if admin? - - roles = roles_for_project(context) - return false unless roles - roles.detect {|role| - (context.is_public? || role.member?) && - role.allowed_to?(action) && - (block_given? ? yield(role, self) : true) - } - elsif context && context.is_a?(Array) - # Authorize if user is authorized on every element of the array - context.map do |project| - allowed_to?(action, project, options, &block) - end.inject do |memo,allowed| - memo && allowed - end - elsif options[:global] - # Admin users are always authorized - return true if admin? - - # authorize if user has at least one role that has this permission - roles = memberships.collect {|m| m.roles}.flatten.uniq - roles << (self.logged? ? Role.non_member : Role.anonymous) - roles.detect {|role| - role.allowed_to?(action) && - (block_given? ? yield(role, self) : true) - } - else - false - end - end - - # Is the user allowed to do the specified action on any project? - # See allowed_to? for the actions and valid options. - def allowed_to_globally?(action, options, &block) - allowed_to?(action, nil, options.reverse_merge(:global => true), &block) - end - - safe_attributes 'login', - 'firstname', - 'lastname', - 'mail', - 'mail_notification', - 'language', - 'custom_field_values', - 'custom_fields', - 'identity_url' - - safe_attributes 'status', - 'auth_source_id', - :if => lambda {|user, current_user| current_user.admin?} - - safe_attributes 'group_ids', - :if => lambda {|user, current_user| current_user.admin? && !user.new_record?} - - # Utility method to help check if a user should be notified about an - # event. - # - # TODO: only supports Issue events currently - def notify_about?(object) - case mail_notification - when 'all' - true - when 'selected' - # user receives notifications for created/assigned issues on unselected projects - if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to)) - true - else - false - end - when 'none' - false - when 'only_my_events' - if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to)) - true - else - false - end - when 'only_assigned' - if object.is_a?(Issue) && is_or_belongs_to?(object.assigned_to) - true - else - false - end - when 'only_owner' - if object.is_a?(Issue) && object.author == self - true - else - false - end - else - false - end - end - - def self.current=(user) - @current_user = user - end - - def self.current - @current_user ||= User.anonymous - end - - # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only - # one anonymous user per database. - def self.anonymous - anonymous_user = AnonymousUser.find(:first) - if anonymous_user.nil? - anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0) - raise 'Unable to create the anonymous user.' if anonymous_user.new_record? - end - anonymous_user - end - - # Salts all existing unsalted passwords - # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password)) - # This method is used in the SaltPasswords migration and is to be kept as is - def self.salt_unsalted_passwords! - transaction do - User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user| - next if user.hashed_password.blank? - salt = User.generate_salt - hashed_password = User.hash_password("#{salt}#{user.hashed_password}") - User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] ) - end - end - end - - protected - - def validate_password_length - # Password length validation based on setting - if !password.nil? && password.size < Setting.password_min_length.to_i - errors.add(:password, :too_short, :count => Setting.password_min_length.to_i) - end - end - - private - - # Removes references that are not handled by associations - # Things that are not deleted are reassociated with the anonymous user - def remove_references_before_destroy - return if self.id.nil? - - substitute = User.anonymous - Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id] - Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] - JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s] - JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s] - Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - # Remove private queries and keep public ones - Query.delete_all ['user_id = ? AND is_public = ?', id, false] - Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] - TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] - Token.delete_all ['user_id = ?', id] - Watcher.delete_all ['user_id = ?', id] - WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - end - - # Return password digest - def self.hash_password(clear_password) - Digest::SHA1.hexdigest(clear_password || "") - end - - # Returns a 128bits random salt as a hex string (32 chars long) - def self.generate_salt - ActiveSupport::SecureRandom.hex(16) - end - -end - -class AnonymousUser < User - - def validate_on_create - # There should be only one AnonymousUser in the database - errors.add :base, 'An anonymous user already exists.' if AnonymousUser.find(:first) - end - - def available_custom_fields - [] - end - - # Overrides a few properties - def logged?; false end - def admin; false end - def name(*args); I18n.t(:label_user_anonymous) end - def mail; nil end - def time_zone; nil end - def rss_key; nil end - - # Anonymous user can not be destroyed - def destroy - false - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4e/4e6c628c00f1ea0177c61db44ecdad4a9212f134.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4e/4e6c628c00f1ea0177c61db44ecdad4a9212f134.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,145 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class RoleTest < ActiveSupport::TestCase + fixtures :roles, :workflows, :trackers + + def test_sorted_scope + assert_equal Role.all.sort, Role.sorted.all + end + + def test_givable_scope + assert_equal Role.all.reject(&:builtin?).sort, Role.givable.all + end + + def test_builtin_scope + assert_equal Role.all.select(&:builtin?).sort, Role.builtin(true).all.sort + assert_equal Role.all.reject(&:builtin?).sort, Role.builtin(false).all.sort + end + + def test_copy_from + role = Role.find(1) + copy = Role.new.copy_from(role) + + assert_nil copy.id + assert_equal '', copy.name + assert_equal role.permissions, copy.permissions + + copy.name = 'Copy' + assert copy.save + end + + def test_copy_workflows + source = Role.find(1) + assert_equal 90, source.workflow_rules.size + + target = Role.new(:name => 'Target') + assert target.save + target.workflow_rules.copy(source) + target.reload + assert_equal 90, target.workflow_rules.size + end + + def test_permissions_should_be_unserialized_with_its_coder + Role::PermissionsAttributeCoder.expects(:load).once + Role.find(1).permissions + end + + def test_add_permission + role = Role.find(1) + size = role.permissions.size + role.add_permission!("apermission", "anotherpermission") + role.reload + assert role.permissions.include?(:anotherpermission) + assert_equal size + 2, role.permissions.size + end + + def test_remove_permission + role = Role.find(1) + size = role.permissions.size + perm = role.permissions[0..1] + role.remove_permission!(*perm) + role.reload + assert ! role.permissions.include?(perm[0]) + assert_equal size - 2, role.permissions.size + end + + def test_name + I18n.locale = 'fr' + assert_equal 'Manager', Role.find(1).name + assert_equal 'Anonyme', Role.anonymous.name + assert_equal 'Non membre', Role.non_member.name + end + + def test_find_all_givable + assert_equal Role.all.reject(&:builtin?).sort, Role.find_all_givable + end + + context "#anonymous" do + should "return the anonymous role" do + role = Role.anonymous + assert role.builtin? + assert_equal Role::BUILTIN_ANONYMOUS, role.builtin + end + + context "with a missing anonymous role" do + setup do + Role.delete_all("builtin = #{Role::BUILTIN_ANONYMOUS}") + end + + should "create a new anonymous role" do + assert_difference('Role.count') do + Role.anonymous + end + end + + should "return the anonymous role" do + role = Role.anonymous + assert role.builtin? + assert_equal Role::BUILTIN_ANONYMOUS, role.builtin + end + end + end + + context "#non_member" do + should "return the non-member role" do + role = Role.non_member + assert role.builtin? + assert_equal Role::BUILTIN_NON_MEMBER, role.builtin + end + + context "with a missing non-member role" do + setup do + Role.delete_all("builtin = #{Role::BUILTIN_NON_MEMBER}") + end + + should "create a new non-member role" do + assert_difference('Role.count') do + Role.non_member + end + end + + should "return the non-member role" do + role = Role.non_member + assert role.builtin? + assert_equal Role::BUILTIN_NON_MEMBER, role.builtin + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4e/4e8ea0df2bcad47fe62855e09a9ca0f2f38daa1e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4e/4e8ea0df2bcad47fe62855e09a9ca0f2f38daa1e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,10 @@ +class AddRolePosition < ActiveRecord::Migration + def self.up + add_column :roles, :position, :integer, :default => 1 + Role.all.each_with_index {|role, i| role.update_attribute(:position, i+1)} + end + + def self.down + remove_column :roles, :position + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4e/4e92a0fc74ebc49a3b3786a7872bd71f9a74effb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4e/4e92a0fc74ebc49a3b3786a7872bd71f9a74effb.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,7 @@ +$('#all_attributes').html('<%= escape_javascript(render :partial => 'form') %>'); + +<% if User.current.allowed_to?(:log_time, @issue.project) %> + $('#log_time').show(); +<% else %> + $('#log_time').hide(); +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4e/4e9dba91f9f1e14e761643510fc2e1252decf990.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4e/4e9dba91f9f1e14e761643510fc2e1252decf990.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,69 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../../../test_helper', __FILE__) +begin + require 'mocha' + + class DarcsAdapterTest < ActiveSupport::TestCase + REPOSITORY_PATH = Rails.root.join('tmp/test/darcs_repository').to_s + + if File.directory?(REPOSITORY_PATH) + def setup + @adapter = Redmine::Scm::Adapters::DarcsAdapter.new(REPOSITORY_PATH) + end + + def test_darcsversion + to_test = { "1.0.9 (release)\n" => [1,0,9] , + "2.2.0 (release)\n" => [2,2,0] } + to_test.each do |s, v| + test_darcsversion_for(s, v) + end + end + + def test_revisions + id1 = '20080308225258-98289-761f654d669045eabee90b91b53a21ce5593cadf.gz' + revs = @adapter.revisions('', nil, nil, {:with_path => true}) + assert_equal 6, revs.size + assert_equal id1, revs[5].scmid + paths = revs[5].paths + assert_equal 5, paths.size + assert_equal 'A', paths[0][:action] + assert_equal '/README', paths[0][:path] + assert_equal 'A', paths[1][:action] + assert_equal '/images', paths[1][:path] + end + + private + + def test_darcsversion_for(darcsversion, version) + @adapter.class.expects(:darcs_binary_version_from_command_line).returns(darcsversion) + assert_equal version, @adapter.class.darcs_binary_version + end + + else + puts "Darcs test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end + end + +rescue LoadError + class DarcsMochaFake < ActiveSupport::TestCase + def test_fake; assert(false, "Requires mocha to run those tests") end + end +end + diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4e/4ed99aa85a5cef8603d38788ee6606bb9b047f2c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4e/4ed99aa85a5cef8603d38788ee6606bb9b047f2c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,284 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class QueriesControllerTest < ActionController::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries, :enabled_modules + + def setup + User.current = nil + end + + def test_new_project_query + @request.session[:user_id] = 2 + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query[is_public]', + :checked => nil } + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query_is_for_all', + :checked => nil, + :disabled => nil } + assert_select 'select[name=?]', 'c[]' do + assert_select 'option[value=tracker]' + assert_select 'option[value=subject]' + end + end + + def test_new_global_query + @request.session[:user_id] = 2 + get :new + assert_response :success + assert_template 'new' + assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query[is_public]' } + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query_is_for_all', + :checked => 'checked', + :disabled => nil } + end + + def test_new_on_invalid_project + @request.session[:user_id] = 2 + get :new, :project_id => 'invalid' + assert_response 404 + end + + def test_create_project_public_query + @request.session[:user_id] = 2 + post :create, + :project_id => 'ecookbook', + :default_columns => '1', + :f => ["status_id", "assigned_to_id"], + :op => {"assigned_to_id" => "=", "status_id" => "o"}, + :v => { "assigned_to_id" => ["1"], "status_id" => ["1"]}, + :query => {"name" => "test_new_project_public_query", "is_public" => "1"} + + q = Query.find_by_name('test_new_project_public_query') + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q + assert q.is_public? + assert q.has_default_columns? + assert q.valid? + end + + def test_create_project_private_query + @request.session[:user_id] = 3 + post :create, + :project_id => 'ecookbook', + :default_columns => '1', + :fields => ["status_id", "assigned_to_id"], + :operators => {"assigned_to_id" => "=", "status_id" => "o"}, + :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]}, + :query => {"name" => "test_new_project_private_query", "is_public" => "1"} + + q = Query.find_by_name('test_new_project_private_query') + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q + assert !q.is_public? + assert q.has_default_columns? + assert q.valid? + end + + def test_create_global_private_query_with_custom_columns + @request.session[:user_id] = 3 + post :create, + :fields => ["status_id", "assigned_to_id"], + :operators => {"assigned_to_id" => "=", "status_id" => "o"}, + :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]}, + :query => {"name" => "test_new_global_private_query", "is_public" => "1"}, + :c => ["", "tracker", "subject", "priority", "category"] + + q = Query.find_by_name('test_new_global_private_query') + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q + assert !q.is_public? + assert !q.has_default_columns? + assert_equal [:tracker, :subject, :priority, :category], q.columns.collect {|c| c.name} + assert q.valid? + end + + def test_create_global_query_with_custom_filters + @request.session[:user_id] = 3 + post :create, + :fields => ["assigned_to_id"], + :operators => {"assigned_to_id" => "="}, + :values => { "assigned_to_id" => ["me"]}, + :query => {"name" => "test_new_global_query"} + + q = Query.find_by_name('test_new_global_query') + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q + assert !q.has_filter?(:status_id) + assert_equal ['assigned_to_id'], q.filters.keys + assert q.valid? + end + + def test_create_with_sort + @request.session[:user_id] = 1 + post :create, + :default_columns => '1', + :operators => {"status_id" => "o"}, + :values => {"status_id" => ["1"]}, + :query => {:name => "test_new_with_sort", + :is_public => "1", + :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}} + + query = Query.find_by_name("test_new_with_sort") + assert_not_nil query + assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria + end + + def test_create_with_failure + @request.session[:user_id] = 2 + assert_no_difference '::Query.count' do + post :create, :project_id => 'ecookbook', :query => {:name => ''} + end + assert_response :success + assert_template 'new' + assert_select 'input[name=?]', 'query[name]' + end + + def test_edit_global_public_query + @request.session[:user_id] = 1 + get :edit, :id => 4 + assert_response :success + assert_template 'edit' + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query[is_public]', + :checked => 'checked' } + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query_is_for_all', + :checked => 'checked', + :disabled => 'disabled' } + end + + def test_edit_global_private_query + @request.session[:user_id] = 3 + get :edit, :id => 3 + assert_response :success + assert_template 'edit' + assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query[is_public]' } + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query_is_for_all', + :checked => 'checked', + :disabled => 'disabled' } + end + + def test_edit_project_private_query + @request.session[:user_id] = 3 + get :edit, :id => 2 + assert_response :success + assert_template 'edit' + assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query[is_public]' } + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query_is_for_all', + :checked => nil, + :disabled => nil } + end + + def test_edit_project_public_query + @request.session[:user_id] = 2 + get :edit, :id => 1 + assert_response :success + assert_template 'edit' + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query[is_public]', + :checked => 'checked' + } + assert_tag :tag => 'input', :attributes => { :type => 'checkbox', + :name => 'query_is_for_all', + :checked => nil, + :disabled => 'disabled' } + end + + def test_edit_sort_criteria + @request.session[:user_id] = 1 + get :edit, :id => 5 + assert_response :success + assert_template 'edit' + assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' }, + :child => { :tag => 'option', :attributes => { :value => 'priority', + :selected => 'selected' } } + assert_tag :tag => 'select', :attributes => { :name => 'query[sort_criteria][0][]' }, + :child => { :tag => 'option', :attributes => { :value => 'desc', + :selected => 'selected' } } + end + + def test_edit_invalid_query + @request.session[:user_id] = 2 + get :edit, :id => 99 + assert_response 404 + end + + def test_udpate_global_private_query + @request.session[:user_id] = 3 + put :update, + :id => 3, + :default_columns => '1', + :fields => ["status_id", "assigned_to_id"], + :operators => {"assigned_to_id" => "=", "status_id" => "o"}, + :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]}, + :query => {"name" => "test_edit_global_private_query", "is_public" => "1"} + + assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3 + q = Query.find_by_name('test_edit_global_private_query') + assert !q.is_public? + assert q.has_default_columns? + assert q.valid? + end + + def test_update_global_public_query + @request.session[:user_id] = 1 + put :update, + :id => 4, + :default_columns => '1', + :fields => ["status_id", "assigned_to_id"], + :operators => {"assigned_to_id" => "=", "status_id" => "o"}, + :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]}, + :query => {"name" => "test_edit_global_public_query", "is_public" => "1"} + + assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4 + q = Query.find_by_name('test_edit_global_public_query') + assert q.is_public? + assert q.has_default_columns? + assert q.valid? + end + + def test_update_with_failure + @request.session[:user_id] = 1 + put :update, :id => 4, :query => {:name => ''} + assert_response :success + assert_template 'edit' + end + + def test_destroy + @request.session[:user_id] = 2 + delete :destroy, :id => 1 + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil + assert_nil Query.find_by_id(1) + end + + def test_backslash_should_be_escaped_in_filters + @request.session[:user_id] = 2 + get :new, :subject => 'foo/bar' + assert_response :success + assert_template 'new' + assert_include 'addFilter("subject", "=", ["foo\/bar"]);', response.body + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4e/4edcfd6c816045bb7c9936fb062160ba608a38e6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4e/4edcfd6c816045bb7c9936fb062160ba608a38e6.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,12 @@ +module RFPDF + module TemplateHandlers + # class Base < ::ActionView::TemplateHandlers::ERB + + # def compile(template) + # src = "_rfpdf_compile_setup;" + super + # end + # end + end +end + + diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4f/4f45518942b2798afa2e5ae495723f19edb35bdd.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4f/4f45518942b2798afa2e5ae495723f19edb35bdd.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,601 @@ +module CollectiveIdea #:nodoc: + module Acts #:nodoc: + module NestedSet #:nodoc: + + # This acts provides Nested Set functionality. Nested Set is a smart way to implement + # an _ordered_ tree, with the added feature that you can select the children and all of their + # descendants with a single query. The drawback is that insertion or move need some complex + # sql queries. But everything is done here by this module! + # + # Nested sets are appropriate each time you want either an orderd tree (menus, + # commercial categories) or an efficient way of querying big trees (threaded posts). + # + # == API + # + # Methods names are aligned with acts_as_tree as much as possible to make replacment from one + # by another easier. + # + # item.children.create(:name => "child1") + # + + # Configuration options are: + # + # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id) + # * +:left_column+ - column name for left boundry data, default "lft" + # * +:right_column+ - column name for right boundry data, default "rgt" + # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id" + # (if it hasn't been already) and use that as the foreign key restriction. You + # can also pass an array to scope by multiple attributes. + # Example: acts_as_nested_set :scope => [:notable_id, :notable_type] + # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the + # child objects are destroyed alongside this object by calling their destroy + # method. If set to :delete_all (default), all the child objects are deleted + # without calling their destroy method. + # * +:counter_cache+ adds a counter cache for the number of children. + # defaults to false. + # Example: acts_as_nested_set :counter_cache => :children_count + # + # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and + # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added + # to acts_as_nested_set models + def acts_as_nested_set(options = {}) + options = { + :parent_column => 'parent_id', + :left_column => 'lft', + :right_column => 'rgt', + :dependent => :delete_all, # or :destroy + :counter_cache => false, + :order => 'id' + }.merge(options) + + if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/ + options[:scope] = "#{options[:scope]}_id".intern + end + + class_attribute :acts_as_nested_set_options + self.acts_as_nested_set_options = options + + include CollectiveIdea::Acts::NestedSet::Model + include Columns + extend Columns + + belongs_to :parent, :class_name => self.base_class.to_s, + :foreign_key => parent_column_name, + :counter_cache => options[:counter_cache], + :inverse_of => :children + has_many :children, :class_name => self.base_class.to_s, + :foreign_key => parent_column_name, :order => left_column_name, + :inverse_of => :parent, + :before_add => options[:before_add], + :after_add => options[:after_add], + :before_remove => options[:before_remove], + :after_remove => options[:after_remove] + + attr_accessor :skip_before_destroy + + before_create :set_default_left_and_right + before_save :store_new_parent + after_save :move_to_new_parent + before_destroy :destroy_descendants + + # no assignment to structure fields + [left_column_name, right_column_name].each do |column| + module_eval <<-"end_eval", __FILE__, __LINE__ + def #{column}=(x) + raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead." + end + end_eval + end + + define_model_callbacks :move + end + + module Model + extend ActiveSupport::Concern + + module ClassMethods + # Returns the first root + def root + roots.first + end + + def roots + where(parent_column_name => nil).order(quoted_left_column_name) + end + + def leaves + where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name) + end + + def valid? + left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid? + end + + def left_and_rights_valid? + joins("LEFT OUTER JOIN #{quoted_table_name} AS parent ON " + + "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}"). + where( + "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " + + "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " + + "#{quoted_table_name}.#{quoted_left_column_name} >= " + + "#{quoted_table_name}.#{quoted_right_column_name} OR " + + "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " + + "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " + + "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))" + ).count == 0 + end + + def no_duplicates_for_columns? + scope_string = Array(acts_as_nested_set_options[:scope]).map do |c| + connection.quote_column_name(c) + end.push(nil).join(", ") + [quoted_left_column_name, quoted_right_column_name].all? do |column| + # No duplicates + select("#{scope_string}#{column}, COUNT(#{column})"). + group("#{scope_string}#{column}"). + having("COUNT(#{column}) > 1"). + first.nil? + end + end + + # Wrapper for each_root_valid? that can deal with scope. + def all_roots_valid? + if acts_as_nested_set_options[:scope] + roots.group(scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots| + each_root_valid?(grouped_roots) + end + else + each_root_valid?(roots) + end + end + + def each_root_valid?(roots_to_validate) + left = right = 0 + roots_to_validate.all? do |root| + (root.left > left && root.right > right).tap do + left = root.left + right = root.right + end + end + end + + # Rebuilds the left & rights if unset or invalid. + # Also very useful for converting from acts_as_tree. + def rebuild!(validate_nodes = true) + # Don't rebuild a valid tree. + return true if valid? + + scope = lambda{|node|} + if acts_as_nested_set_options[:scope] + scope = lambda{|node| + scope_column_names.inject(""){|str, column_name| + str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} " + } + } + end + indices = {} + + set_left_and_rights = lambda do |node| + # set left + node[left_column_name] = indices[scope.call(node)] += 1 + # find + where(["#{quoted_parent_column_name} = ? #{scope.call(node)}", node]).order(acts_as_nested_set_options[:order]).each{|n| set_left_and_rights.call(n) } + # set right + node[right_column_name] = indices[scope.call(node)] += 1 + node.save!(:validate => validate_nodes) + end + + # Find root node(s) + root_nodes = where("#{quoted_parent_column_name} IS NULL").order(acts_as_nested_set_options[:order]).each do |root_node| + # setup index for this scope + indices[scope.call(root_node)] ||= 0 + set_left_and_rights.call(root_node) + end + end + + # Iterates over tree elements and determines the current level in the tree. + # Only accepts default ordering, odering by an other column than lft + # does not work. This method is much more efficent than calling level + # because it doesn't require any additional database queries. + # + # Example: + # Category.each_with_level(Category.root.self_and_descendants) do |o, level| + # + def each_with_level(objects) + path = [nil] + objects.each do |o| + if o.parent_id != path.last + # we are on a new level, did we decent or ascent? + if path.include?(o.parent_id) + # remove wrong wrong tailing paths elements + path.pop while path.last != o.parent_id + else + path << o.parent_id + end + end + yield(o, path.length - 1) + end + end + end + + # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder. + # + # category.self_and_descendants.count + # category.ancestors.find(:all, :conditions => "name like '%foo%'") + + # Value of the parent column + def parent_id + self[parent_column_name] + end + + # Value of the left column + def left + self[left_column_name] + end + + # Value of the right column + def right + self[right_column_name] + end + + # Returns true if this is a root node. + def root? + parent_id.nil? + end + + def leaf? + new_record? || (right - left == 1) + end + + # Returns true is this is a child node + def child? + !parent_id.nil? + end + + # Returns root + def root + self_and_ancestors.where(parent_column_name => nil).first + end + + # Returns the array of all parents and self + def self_and_ancestors + nested_set_scope.where([ + "#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right + ]) + end + + # Returns an array of all parents + def ancestors + without_self self_and_ancestors + end + + # Returns the array of all children of the parent, including self + def self_and_siblings + nested_set_scope.where(parent_column_name => parent_id) + end + + # Returns the array of all children of the parent, except self + def siblings + without_self self_and_siblings + end + + # Returns a set of all of its nested children which do not have children + def leaves + descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1") + end + + # Returns the level of this object in the tree + # root level is 0 + def level + parent_id.nil? ? 0 : ancestors.count + end + + # Returns a set of itself and all of its nested children + def self_and_descendants + nested_set_scope.where([ + "#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right + ]) + end + + # Returns a set of all of its children and nested children + def descendants + without_self self_and_descendants + end + + def is_descendant_of?(other) + other.left < self.left && self.left < other.right && same_scope?(other) + end + + def is_or_is_descendant_of?(other) + other.left <= self.left && self.left < other.right && same_scope?(other) + end + + def is_ancestor_of?(other) + self.left < other.left && other.left < self.right && same_scope?(other) + end + + def is_or_is_ancestor_of?(other) + self.left <= other.left && other.left < self.right && same_scope?(other) + end + + # Check if other model is in the same scope + def same_scope?(other) + Array(acts_as_nested_set_options[:scope]).all? do |attr| + self.send(attr) == other.send(attr) + end + end + + # Find the first sibling to the left + def left_sibling + siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]). + order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last + end + + # Find the first sibling to the right + def right_sibling + siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first + end + + # Shorthand method for finding the left sibling and moving to the left of it. + def move_left + move_to_left_of left_sibling + end + + # Shorthand method for finding the right sibling and moving to the right of it. + def move_right + move_to_right_of right_sibling + end + + # Move the node to the left of another node (you can pass id only) + def move_to_left_of(node) + move_to node, :left + end + + # Move the node to the left of another node (you can pass id only) + def move_to_right_of(node) + move_to node, :right + end + + # Move the node to the child of another node (you can pass id only) + def move_to_child_of(node) + move_to node, :child + end + + # Move the node to root nodes + def move_to_root + move_to nil, :root + end + + def move_possible?(target) + self != target && # Can't target self + same_scope?(target) && # can't be in different scopes + # !(left..right).include?(target.left..target.right) # this needs tested more + # detect impossible move + !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right)) + end + + def to_text + self_and_descendants.map do |node| + "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})" + end.join("\n") + end + + protected + + def without_self(scope) + scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self]) + end + + # All nested set queries should use this nested_set_scope, which performs finds on + # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set + # declaration. + def nested_set_scope(options = {}) + options = {:order => "#{self.class.quoted_table_name}.#{quoted_left_column_name}"}.merge(options) + scopes = Array(acts_as_nested_set_options[:scope]) + options[:conditions] = scopes.inject({}) do |conditions,attr| + conditions.merge attr => self[attr] + end unless scopes.empty? + self.class.base_class.scoped options + end + + def store_new_parent + @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false + true # force callback to return true + end + + def move_to_new_parent + if @move_to_new_parent_id.nil? + move_to_root + elsif @move_to_new_parent_id + move_to_child_of(@move_to_new_parent_id) + end + end + + # on creation, set automatically lft and rgt to the end of the tree + def set_default_left_and_right + highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").find(:first, :limit => 1,:lock => true ) + maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0 + # adds the new node to the right of all existing nodes + self[left_column_name] = maxright + 1 + self[right_column_name] = maxright + 2 + end + + def in_tenacious_transaction(&block) + retry_count = 0 + begin + transaction(&block) + rescue ActiveRecord::StatementInvalid => error + raise unless connection.open_transactions.zero? + raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/ + raise unless retry_count < 10 + retry_count += 1 + logger.info "Deadlock detected on retry #{retry_count}, restarting transaction" + sleep(rand(retry_count)*0.1) # Aloha protocol + retry + end + end + + # Prunes a branch off of the tree, shifting all of the elements on the right + # back to the left so the counts still work. + def destroy_descendants + return if right.nil? || left.nil? || skip_before_destroy + + in_tenacious_transaction do + reload_nested_set + # select the rows in the model that extend past the deletion point and apply a lock + self.class.base_class.find(:all, + :select => "id", + :conditions => ["#{quoted_left_column_name} >= ?", left], + :lock => true + ) + + if acts_as_nested_set_options[:dependent] == :destroy + descendants.each do |model| + model.skip_before_destroy = true + model.destroy + end + else + nested_set_scope.delete_all( + ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", + left, right] + ) + end + + # update lefts and rights for remaining nodes + diff = right - left + 1 + nested_set_scope.update_all( + ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff], + ["#{quoted_left_column_name} > ?", right] + ) + nested_set_scope.update_all( + ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff], + ["#{quoted_right_column_name} > ?", right] + ) + +reload + # Don't allow multiple calls to destroy to corrupt the set + self.skip_before_destroy = true + end + end + + # reload left, right, and parent + def reload_nested_set + reload( + :select => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{quoted_parent_column_name}", + :lock => true + ) + end + + def move_to(target, position) + raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record? + run_callbacks :move do + in_tenacious_transaction do + if target.is_a? self.class.base_class + target.reload_nested_set + elsif position != :root + # load object if node is not an object + target = nested_set_scope.find(target) + end + self.reload_nested_set + + unless position == :root || move_possible?(target) + raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree." + end + + bound = case position + when :child; target[right_column_name] + when :left; target[left_column_name] + when :right; target[right_column_name] + 1 + when :root; 1 + else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)." + end + + if bound > self[right_column_name] + bound = bound - 1 + other_bound = self[right_column_name] + 1 + else + other_bound = self[left_column_name] - 1 + end + + # there would be no change + return if bound == self[right_column_name] || bound == self[left_column_name] + + # we have defined the boundaries of two non-overlapping intervals, + # so sorting puts both the intervals and their boundaries in order + a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort + + # select the rows in the model between a and d, and apply a lock + self.class.base_class.select('id').lock(true).where( + ["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}] + ) + + new_parent = case position + when :child; target.id + when :root; nil + else target[parent_column_name] + end + + self.nested_set_scope.update_all([ + "#{quoted_left_column_name} = CASE " + + "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " + + "THEN #{quoted_left_column_name} + :d - :b " + + "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " + + "THEN #{quoted_left_column_name} + :a - :c " + + "ELSE #{quoted_left_column_name} END, " + + "#{quoted_right_column_name} = CASE " + + "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " + + "THEN #{quoted_right_column_name} + :d - :b " + + "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " + + "THEN #{quoted_right_column_name} + :a - :c " + + "ELSE #{quoted_right_column_name} END, " + + "#{quoted_parent_column_name} = CASE " + + "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " + + "ELSE #{quoted_parent_column_name} END", + {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent} + ]) + end + target.reload_nested_set if target + self.reload_nested_set + end + end + + end + + # Mixed into both classes and instances to provide easy access to the column names + module Columns + def left_column_name + acts_as_nested_set_options[:left_column] + end + + def right_column_name + acts_as_nested_set_options[:right_column] + end + + def parent_column_name + acts_as_nested_set_options[:parent_column] + end + + def scope_column_names + Array(acts_as_nested_set_options[:scope]) + end + + def quoted_left_column_name + connection.quote_column_name(left_column_name) + end + + def quoted_right_column_name + connection.quote_column_name(right_column_name) + end + + def quoted_parent_column_name + connection.quote_column_name(parent_column_name) + end + + def quoted_scope_column_names + scope_column_names.collect {|column_name| connection.quote_column_name(column_name) } + end + end + + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4f/4fc979098dadbbbce4dbcbf4d7f01469121ba53b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4f/4fc979098dadbbbce4dbcbf4d7f01469121ba53b.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,33 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Views + class OtherFormatsBuilder + def initialize(view) + @view = view + end + + def link_to(name, options={}) + url = { :format => name.to_s.downcase }.merge(options.delete(:url) || {}).except('page') + caption = options.delete(:caption) || name + html_options = { :class => name.to_s.downcase, :rel => 'nofollow' }.merge(options) + @view.content_tag('span', @view.link_to(caption, url, html_options)) + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4f/4fcd36ce1f588dcf9f4f40389b563648885b7d2b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4f/4fcd36ce1f588dcf9f4f40389b563648885b7d2b.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,27 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../test_helper', __FILE__) + +class Redmine::InfoTest < ActiveSupport::TestCase + def test_environment + env = Redmine::Info.environment + + assert_kind_of String, env + assert_match 'Redmine version', env + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4f/4fd49f19416f116fed41d83a702a2ea191e23a0e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4f/4fd49f19416f116fed41d83a702a2ea191e23a0e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,7 @@ +--- +groups_users_001: + group_id: 10 + user_id: 8 +groups_users_002: + group_id: 11 + user_id: 8 diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/4f/4fdf97ac9fb70913b0f0a7ee691b6415c785a5ae.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/4f/4fdf97ac9fb70913b0f0a7ee691b6415c785a5ae.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,13 @@ +api.array :wiki_pages do + @pages.each do |page| + api.wiki_page do + api.title page.title + if page.parent + api.parent :title => page.parent.title + end + api.version page.version + api.created_on page.created_on + api.updated_on page.updated_on + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/50/500090a8cee3e18d3330d97210a96eb6f5618b4a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/50/500090a8cee3e18d3330d97210a96eb6f5618b4a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,41 @@ +<% if @project.shared_versions.any? %> + + + + + + + + + + + +<% for version in @project.shared_versions.sort %> + + + + + + + + + +<% end; reset_cycle %> + +
    <%= l(:label_version) %><%= l(:field_effective_date) %><%= l(:field_description) %><%= l(:field_status) %><%= l(:field_sharing) %><%= l(:label_wiki_page) %>
    <%= link_to_version version %><%= format_date(version.effective_date) %><%=h version.description %><%= l("version_status_#{version.status}") %><%= link_to_if_authorized(h(version.wiki_page_title), {:controller => 'wiki', :action => 'show', :project_id => version.project, :id => Wiki.titleize(version.wiki_page_title)}) || h(version.wiki_page_title) unless version.wiki_page_title.blank? || version.project.wiki.nil? %> + <% if version.project == @project && User.current.allowed_to?(:manage_versions, @project) %> + <%= link_to l(:button_edit), edit_version_path(version), :class => 'icon icon-edit' %> + <%= delete_link version_path(version) %> + <% end %> +
    +<% else %> +

    <%= l(:label_no_data) %>

    +<% end %> + +
    +<% if @project.versions.any? %> + <%= link_to l(:label_close_versions), close_completed_project_versions_path(@project), :method => :put %> +<% end %> +
    + +

    <%= link_to l(:label_version_new), new_project_version_path(@project, :back_url => ''), :class => 'icon icon-add' if User.current.allowed_to?(:manage_versions, @project) %>

    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/50/5011cfcaf73ed9e80ee3628efe2190f52626265d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/50/5011cfcaf73ed9e80ee3628efe2190f52626265d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,105 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class BoardsController < ApplicationController + default_search_scope :messages + before_filter :find_project_by_project_id, :find_board_if_available, :authorize + accept_rss_auth :index, :show + + helper :sort + include SortHelper + helper :watchers + + def index + @boards = @project.boards.includes(:last_message => :author).all + # show the board if there is only one + if @boards.size == 1 + @board = @boards.first + show + end + end + + def show + respond_to do |format| + format.html { + sort_init 'updated_on', 'desc' + sort_update 'created_on' => "#{Message.table_name}.created_on", + 'replies' => "#{Message.table_name}.replies_count", + 'updated_on' => "#{Message.table_name}.updated_on" + + @topic_count = @board.topics.count + @topic_pages = Paginator.new self, @topic_count, per_page_option, params['page'] + @topics = @board.topics.reorder("#{Message.table_name}.sticky DESC").order(sort_clause).all( + :include => [:author, {:last_reply => :author}], + :limit => @topic_pages.items_per_page, + :offset => @topic_pages.current.offset) + @message = Message.new(:board => @board) + render :action => 'show', :layout => !request.xhr? + } + format.atom { + @messages = @board.messages.find :all, :order => 'created_on DESC', + :include => [:author, :board], + :limit => Setting.feeds_limit.to_i + render_feed(@messages, :title => "#{@project}: #{@board}") + } + end + end + + def new + @board = @project.boards.build + @board.safe_attributes = params[:board] + end + + def create + @board = @project.boards.build + @board.safe_attributes = params[:board] + if @board.save + flash[:notice] = l(:notice_successful_create) + redirect_to_settings_in_projects + else + render :action => 'new' + end + end + + def edit + end + + def update + @board.safe_attributes = params[:board] + if @board.save + redirect_to_settings_in_projects + else + render :action => 'edit' + end + end + + def destroy + @board.destroy + redirect_to_settings_in_projects + end + +private + def redirect_to_settings_in_projects + redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards' + end + + def find_board_if_available + @board = @project.boards.find(params[:id]) if params[:id] + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/50/504856dc790e00e3ac8d8064fe856cf6af4e3121.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/50/504856dc790e00e3ac8d8064fe856cf6af4e3121.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,26 @@ +Date: Tue, 20 Nov 2012 23:08:25 +0900 +Message-ID: +Subject: test +From: John Smith +To: redmine@somenet.foo +Content-Type: multipart/mixed; boundary=14dae93a13bf76ca5d04ceedc458 + +--14dae93a13bf76ca5d04ceedc458 +Content-Type: text/plain; charset=ISO-8859-1 + +test + +--14dae93a13bf76ca5d04ceedc458 +Content-Type: text/plain; charset=US-ASCII; + name="=?ISO-8859-1?B?xOTW9tz8xOTW9tz8xOTW9tz8xOTW9tz8xOTW9tw=?= + =?ISO-8859-1?B?/MTk1vbc/MTk1vbc/MTk1vbc/MTk1vbc/MTk1vbc?= + =?ISO-8859-1?B?/MTk1vbc/C50eHQ=?=" +Content-Disposition: attachment; + filename="=?ISO-8859-1?B?xOTW9tz8xOTW9tz8xOTW9tz8xOTW9tz8xOTW9tw=?= + =?ISO-8859-1?B?/MTk1vbc/MTk1vbc/MTk1vbc/MTk1vbc/MTk1vbc?= + =?ISO-8859-1?B?/MTk1vbc/C50eHQ=?=" +Content-Transfer-Encoding: base64 +X-Attachment-Id: f_h9r3mcjz0 + +dGVzdAo= +--14dae93a13bf76ca5d04ceedc458-- diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/50/50beaf110ddcbb163c71b8aaaf14269834bbd169.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/50/50beaf110ddcbb163c71b8aaaf14269834bbd169.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,122 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) +require 'members_controller' + +# Re-raise errors caught by the controller. +class MembersController; def rescue_action(e) raise e end; end + + +class MembersControllerTest < ActionController::TestCase + fixtures :projects, :members, :member_roles, :roles, :users + + def setup + @controller = MembersController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + @request.session[:user_id] = 2 + end + + def test_create + assert_difference 'Member.count' do + post :create, :project_id => 1, :membership => {:role_ids => [1], :user_id => 7} + end + assert_redirected_to '/projects/ecookbook/settings/members' + assert User.find(7).member_of?(Project.find(1)) + end + + def test_create_multiple + assert_difference 'Member.count', 3 do + post :create, :project_id => 1, :membership => {:role_ids => [1], :user_ids => [7, 8, 9]} + end + assert_redirected_to '/projects/ecookbook/settings/members' + assert User.find(7).member_of?(Project.find(1)) + end + + def test_xhr_create + assert_difference 'Member.count', 3 do + xhr :post, :create, :project_id => 1, :membership => {:role_ids => [1], :user_ids => [7, 8, 9]} + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + end + assert User.find(7).member_of?(Project.find(1)) + assert User.find(8).member_of?(Project.find(1)) + assert User.find(9).member_of?(Project.find(1)) + assert_include 'tab-content-members', response.body + end + + def test_xhr_create_with_failure + assert_no_difference 'Member.count' do + xhr :post, :create, :project_id => 1, :membership => {:role_ids => [], :user_ids => [7, 8, 9]} + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + end + assert_match /alert/, response.body, "Alert message not sent" + end + + def test_edit + assert_no_difference 'Member.count' do + put :update, :id => 2, :membership => {:role_ids => [1], :user_id => 3} + end + assert_redirected_to '/projects/ecookbook/settings/members' + end + + def test_xhr_edit + assert_no_difference 'Member.count' do + xhr :put, :update, :id => 2, :membership => {:role_ids => [1], :user_id => 3} + assert_response :success + assert_template 'update' + assert_equal 'text/javascript', response.content_type + end + member = Member.find(2) + assert_equal [1], member.role_ids + assert_equal 3, member.user_id + assert_include 'tab-content-members', response.body + end + + def test_destroy + assert_difference 'Member.count', -1 do + delete :destroy, :id => 2 + end + assert_redirected_to '/projects/ecookbook/settings/members' + assert !User.find(3).member_of?(Project.find(1)) + end + + def test_xhr_destroy + assert_difference 'Member.count', -1 do + xhr :delete, :destroy, :id => 2 + assert_response :success + assert_template 'destroy' + assert_equal 'text/javascript', response.content_type + end + assert_nil Member.find_by_id(2) + assert_include 'tab-content-members', response.body + end + + def test_autocomplete + get :autocomplete, :project_id => 1, :q => 'mis' + assert_response :success + assert_template 'autocomplete' + + assert_tag :label, :content => /User Misc/, + :child => { :tag => 'input', :attributes => { :name => 'membership[user_ids][]', :value => '8' } } + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/51/510551ee86f1117b9ae6fc077057fee69fe027dd.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/51/510551ee86f1117b9ae6fc077057fee69fe027dd.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,8 @@ +class BuildProjectsTree < ActiveRecord::Migration + def self.up + Project.rebuild_tree! + end + + def self.down + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/51/51126f0ba4e88016e644efd5331f25d6c8908ff9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/51/51126f0ba4e88016e644efd5331f25d6c8908ff9.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,6 @@ +require 'rubygems' + +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) + +require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/52/52085347244afd004a84d9566613bc12acfd3185.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/52/52085347244afd004a84d9566613bc12acfd3185.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,18 @@ +api.array :memberships, api_meta(:total_count => @member_count, :offset => @offset, :limit => @limit) do + @members.each do |membership| + api.membership do + api.id membership.id + api.project :id => membership.project.id, :name => membership.project.name + api.__send__ membership.principal.class.name.underscore, :id => membership.principal.id, :name => membership.principal.name + api.array :roles do + membership.member_roles.each do |member_role| + if member_role.role + attrs = {:id => member_role.role.id, :name => member_role.role.name} + attrs.merge!(:inherited => true) if member_role.inherited_from.present? + api.role attrs + end + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/52/524fd46290a5b042d52b6965c04b076d495b7d24.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/52/524fd46290a5b042d52b6965c04b076d495b7d24.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,2 @@ +<% selector = ".#{watcher_css(watched)}" %> +$("<%= selector %>").each(function(){$(this).html("<%= escape_javascript watcher_link(watched, user) %>")}); diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/52/5254040f55f9795eeba2da7fed7e73136e2f000e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/52/5254040f55f9795eeba2da7fed7e73136e2f000e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,36 @@ +
    +<%= watcher_tag(@wiki, User.current) %> +
    + +

    <%= l(:label_index_by_date) %>

    + +<% if @pages.empty? %> +

    <%= l(:label_no_data) %>

    +<% end %> + +<% @pages_by_date.keys.sort.reverse.each do |date| %> +

    <%= format_date(date) %>

    +
      +<% @pages_by_date[date].each do |page| %> +
    • <%= link_to h(page.pretty_title), :action => 'show', :id => page.title, :project_id => page.project %>
    • +<% end %> +
    +<% end %> + +<% content_for :sidebar do %> + <%= render :partial => 'sidebar' %> +<% end %> + +<% unless @pages.empty? %> +<% other_formats_links do |f| %> + <%= f.link_to 'Atom', :url => {:controller => 'activities', :action => 'index', :id => @project, :show_wiki_edits => 1, :key => User.current.rss_key} %> + <% if User.current.allowed_to?(:export_wiki_pages, @project) %> + <%= f.link_to('PDF', :url => {:action => 'export', :format => 'pdf'}) %> + <%= f.link_to('HTML', :url => {:action => 'export'}) %> + <% end %> +<% end %> +<% end %> + +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:atom, :controller => 'activities', :action => 'index', :id => @project, :show_wiki_edits => 1, :format => 'atom', :key => User.current.rss_key) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/52/526a207c3d794968f2a7c5b89f01aadb8abb89d2.svn-base --- a/.svn/pristine/52/526a207c3d794968f2a7c5b89f01aadb8abb89d2.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4332 +0,0 @@ -#============================================================+ -# File name : tcpdf.rb -# Begin : 2002-08-03 -# Last Update : 2007-03-20 -# Author : Nicola Asuni -# Version : 1.53.0.TC031 -# License : GNU LGPL (http://www.gnu.org/copyleft/lesser.html) -# -# Description : This is a Ruby class for generating PDF files -# on-the-fly without requiring external -# extensions. -# -# IMPORTANT: -# This class is an extension and improvement of the Public Domain -# FPDF class by Olivier Plathey (http://www.fpdf.org). -# -# Main changes by Nicola Asuni: -# Ruby porting; -# UTF-8 Unicode support; -# code refactoring; -# source code clean up; -# code style and formatting; -# source code documentation using phpDocumentor (www.phpdoc.org); -# All ISO page formats were included; -# image scale factor; -# includes methods to parse and printsome XHTML code, supporting the following elements: h1, h2, h3, h4, h5, h6, b, u, i, a, img, p, br, strong, em, font, blockquote, li, ul, ol, hr, td, th, tr, table, sup, sub, small; -# includes a method to print various barcode formats using an improved version of "Generic Barcode Render Class" by Karim Mribti (http://www.mribti.com/barcode/) (require GD library: http://www.boutell.com/gd/); -# defines standard Header() and Footer() methods. -# -# Ported to Ruby by Ed Moss 2007-08-06 -# -#============================================================+ - -# -# TCPDF Class. -# @package com.tecnick.tcpdf -# - -@@version = "1.53.0.TC031" -@@fpdf_charwidths = {} - -PDF_PRODUCER = 'TCPDF via RFPDF 1.53.0.TC031 (http://tcpdf.sourceforge.net)' - -module TCPDFFontDescriptor - @@descriptors = { 'freesans' => {} } - @@font_name = 'freesans' - - def self.font(font_name) - @@descriptors[font_name.gsub(".rb", "")] - end - - def self.define(font_name = 'freesans') - @@descriptors[font_name] ||= {} - yield @@descriptors[font_name] - end -end - -# This is a Ruby class for generating PDF files on-the-fly without requiring external extensions.
    -# This class is an extension and improvement of the FPDF class by Olivier Plathey (http://www.fpdf.org).
    -# This version contains some changes: [porting to Ruby, support for UTF-8 Unicode, code style and formatting, php documentation (www.phpdoc.org), ISO page formats, minor improvements, image scale factor]
    -# TCPDF project (http://tcpdf.sourceforge.net) is based on the Public Domain FPDF class by Olivier Plathey (http://www.fpdf.org).
    -# To add your own TTF fonts please read /fonts/README.TXT -# @name TCPDF -# @package com.tecnick.tcpdf -# @@version 1.53.0.TC031 -# @author Nicola Asuni -# @link http://tcpdf.sourceforge.net -# @license http://www.gnu.org/copyleft/lesser.html LGPL -# -class TCPDF - include RFPDF - include Core::RFPDF - include RFPDF::Math - - def logger - Rails.logger - end - - cattr_accessor :k_cell_height_ratio - @@k_cell_height_ratio = 1.25 - - cattr_accessor :k_blank_image - @@k_blank_image = "" - - cattr_accessor :k_small_ratio - @@k_small_ratio = 2/3.0 - - cattr_accessor :k_path_cache - @@k_path_cache = Rails.root.join('tmp') - - cattr_accessor :k_path_url_cache - @@k_path_url_cache = Rails.root.join('tmp') - - cattr_accessor :decoder - - attr_accessor :barcode - - attr_accessor :buffer - - attr_accessor :diffs - - attr_accessor :color_flag - - attr_accessor :default_table_columns - - attr_accessor :max_table_columns - - attr_accessor :default_font - - attr_accessor :draw_color - - attr_accessor :encoding - - attr_accessor :fill_color - - attr_accessor :fonts - - attr_accessor :font_family - - attr_accessor :font_files - - cattr_accessor :font_path - - attr_accessor :font_style - - attr_accessor :font_size_pt - - attr_accessor :header_width - - attr_accessor :header_logo - - attr_accessor :header_logo_width - - attr_accessor :header_title - - attr_accessor :header_string - - attr_accessor :images - - attr_accessor :img_scale - - attr_accessor :in_footer - - attr_accessor :is_unicode - - attr_accessor :lasth - - attr_accessor :links - - attr_accessor :list_ordered - - attr_accessor :list_count - - attr_accessor :li_spacer - - attr_accessor :n - - attr_accessor :offsets - - attr_accessor :orientation_changes - - attr_accessor :page - - attr_accessor :page_links - - attr_accessor :pages - - attr_accessor :pdf_version - - attr_accessor :prevfill_color - - attr_accessor :prevtext_color - - attr_accessor :print_header - - attr_accessor :print_footer - - attr_accessor :state - - attr_accessor :tableborder - - attr_accessor :tdbegin - - attr_accessor :tdwidth - - attr_accessor :tdheight - - attr_accessor :tdalign - - attr_accessor :tdfill - - attr_accessor :tempfontsize - - attr_accessor :text_color - - attr_accessor :underline - - attr_accessor :ws - - # - # This is the class constructor. - # It allows to set up the page format, the orientation and - # the measure unit used in all the methods (except for the font sizes). - # @since 1.0 - # @param string :orientation page orientation. Possible values are (case insensitive):
    • P or Portrait (default)
    • L or Landscape
    - # @param string :unit User measure unit. Possible values are:
    • pt: point
    • mm: millimeter (default)
    • cm: centimeter
    • in: inch

    A point equals 1/72 of inch, that is to say about 0.35 mm (an inch being 2.54 cm). This is a very common unit in typography; font sizes are expressed in that unit. - # @param mixed :format The format used for pages. It can be either one of the following values (case insensitive) or a custom format in the form of a two-element array containing the width and the height (expressed in the unit given by unit).
    • 4A0
    • 2A0
    • A0
    • A1
    • A2
    • A3
    • A4 (default)
    • A5
    • A6
    • A7
    • A8
    • A9
    • A10
    • B0
    • B1
    • B2
    • B3
    • B4
    • B5
    • B6
    • B7
    • B8
    • B9
    • B10
    • C0
    • C1
    • C2
    • C3
    • C4
    • C5
    • C6
    • C7
    • C8
    • C9
    • C10
    • RA0
    • RA1
    • RA2
    • RA3
    • RA4
    • SRA0
    • SRA1
    • SRA2
    • SRA3
    • SRA4
    • LETTER
    • LEGAL
    • EXECUTIVE
    • FOLIO
    - # @param boolean :unicode TRUE means that the input text is unicode (default = true) - # @param String :encoding charset encoding; default is UTF-8 - # - def initialize(orientation = 'P', unit = 'mm', format = 'A4', unicode = true, encoding = "UTF-8") - - # Set internal character encoding to ASCII# - #FIXME 2007-05-25 (EJM) Level=0 - - # if (respond_to?("mb_internal_encoding") and mb_internal_encoding()) - # @internal_encoding = mb_internal_encoding(); - # mb_internal_encoding("ASCII"); - # } - - #Some checks - dochecks(); - - begin - @@decoder = HTMLEntities.new - rescue - @@decoder = nil - end - - #Initialization of properties - @barcode ||= false - @buffer ||= '' - @diffs ||= [] - @color_flag ||= false - @default_table_columns ||= 4 - @table_columns ||= 0 - @max_table_columns ||= [] - @tr_id ||= 0 - @max_td_page ||= [] - @max_td_y ||= [] - @t_columns ||= 0 - @default_font ||= "FreeSans" if unicode - @default_font ||= "Helvetica" - @draw_color ||= '0 G' - @encoding ||= "UTF-8" - @fill_color ||= '0 g' - @fonts ||= {} - @font_family ||= '' - @font_files ||= {} - @font_style ||= '' - @font_size ||= 12 - @font_size_pt ||= 12 - @header_width ||= 0 - @header_logo ||= "" - @header_logo_width ||= 30 - @header_title ||= "" - @header_string ||= "" - @images ||= {} - @img_scale ||= 1 - @in_footer ||= false - @is_unicode = unicode - @lasth ||= 0 - @links ||= [] - @list_ordered ||= [] - @list_count ||= [] - @li_spacer ||= "" - @li_count ||= 0 - @spacer ||= "" - @quote_count ||= 0 - @prevquote_count ||= 0 - @quote_top ||= [] - @quote_page ||= [] - @n ||= 2 - @offsets ||= [] - @orientation_changes ||= [] - @page ||= 0 - @page_links ||= {} - @pages ||= [] - @pdf_version ||= "1.3" - @prevfill_color ||= [255,255,255] - @prevtext_color ||= [0,0,0] - @print_header ||= false - @print_footer ||= false - @state ||= 0 - @tableborder ||= 0 - @tdbegin ||= false - @tdwidth ||= 0 - @tdheight ||= 0 - @tdalign ||= "L" - @tdfill ||= 0 - @tempfontsize ||= 10 - @text_color ||= '0 g' - @underline ||= false - @deleted ||= false - @ws ||= 0 - - #Standard Unicode fonts - @core_fonts = { - 'courier'=>'Courier', - 'courierB'=>'Courier-Bold', - 'courierI'=>'Courier-Oblique', - 'courierBI'=>'Courier-BoldOblique', - 'helvetica'=>'Helvetica', - 'helveticaB'=>'Helvetica-Bold', - 'helveticaI'=>'Helvetica-Oblique', - 'helveticaBI'=>'Helvetica-BoldOblique', - 'times'=>'Times-Roman', - 'timesB'=>'Times-Bold', - 'timesI'=>'Times-Italic', - 'timesBI'=>'Times-BoldItalic', - 'symbol'=>'Symbol', - 'zapfdingbats'=>'ZapfDingbats'} - - #Scale factor - case unit.downcase - when 'pt' ; @k=1 - when 'mm' ; @k=72/25.4 - when 'cm' ; @k=72/2.54 - when 'in' ; @k=72 - else Error("Incorrect unit: #{unit}") - end - - #Page format - if format.is_a?(String) - # Page formats (45 standard ISO paper formats and 4 american common formats). - # Paper cordinates are calculated in this way: (inches# 72) where (1 inch = 2.54 cm) - case (format.upcase) - when '4A0' ; format = [4767.87,6740.79] - when '2A0' ; format = [3370.39,4767.87] - when 'A0' ; format = [2383.94,3370.39] - when 'A1' ; format = [1683.78,2383.94] - when 'A2' ; format = [1190.55,1683.78] - when 'A3' ; format = [841.89,1190.55] - when 'A4' ; format = [595.28,841.89] # ; default - when 'A5' ; format = [419.53,595.28] - when 'A6' ; format = [297.64,419.53] - when 'A7' ; format = [209.76,297.64] - when 'A8' ; format = [147.40,209.76] - when 'A9' ; format = [104.88,147.40] - when 'A10' ; format = [73.70,104.88] - when 'B0' ; format = [2834.65,4008.19] - when 'B1' ; format = [2004.09,2834.65] - when 'B2' ; format = [1417.32,2004.09] - when 'B3' ; format = [1000.63,1417.32] - when 'B4' ; format = [708.66,1000.63] - when 'B5' ; format = [498.90,708.66] - when 'B6' ; format = [354.33,498.90] - when 'B7' ; format = [249.45,354.33] - when 'B8' ; format = [175.75,249.45] - when 'B9' ; format = [124.72,175.75] - when 'B10' ; format = [87.87,124.72] - when 'C0' ; format = [2599.37,3676.54] - when 'C1' ; format = [1836.85,2599.37] - when 'C2' ; format = [1298.27,1836.85] - when 'C3' ; format = [918.43,1298.27] - when 'C4' ; format = [649.13,918.43] - when 'C5' ; format = [459.21,649.13] - when 'C6' ; format = [323.15,459.21] - when 'C7' ; format = [229.61,323.15] - when 'C8' ; format = [161.57,229.61] - when 'C9' ; format = [113.39,161.57] - when 'C10' ; format = [79.37,113.39] - when 'RA0' ; format = [2437.80,3458.27] - when 'RA1' ; format = [1729.13,2437.80] - when 'RA2' ; format = [1218.90,1729.13] - when 'RA3' ; format = [864.57,1218.90] - when 'RA4' ; format = [609.45,864.57] - when 'SRA0' ; format = [2551.18,3628.35] - when 'SRA1' ; format = [1814.17,2551.18] - when 'SRA2' ; format = [1275.59,1814.17] - when 'SRA3' ; format = [907.09,1275.59] - when 'SRA4' ; format = [637.80,907.09] - when 'LETTER' ; format = [612.00,792.00] - when 'LEGAL' ; format = [612.00,1008.00] - when 'EXECUTIVE' ; format = [521.86,756.00] - when 'FOLIO' ; format = [612.00,936.00] - #else then Error("Unknown page format: #{format}" - end - @fw_pt = format[0] - @fh_pt = format[1] - else - @fw_pt = format[0]*@k - @fh_pt = format[1]*@k - end - - @fw = @fw_pt/@k - @fh = @fh_pt/@k - - #Page orientation - orientation = orientation.downcase - if orientation == 'p' or orientation == 'portrait' - @def_orientation = 'P' - @w_pt = @fw_pt - @h_pt = @fh_pt - elsif orientation == 'l' or orientation == 'landscape' - @def_orientation = 'L' - @w_pt = @fh_pt - @h_pt = @fw_pt - else - Error("Incorrect orientation: #{orientation}") - end - - @cur_orientation = @def_orientation - @w = @w_pt/@k - @h = @h_pt/@k - #Page margins (1 cm) - margin = 28.35/@k - SetMargins(margin, margin) - #Interior cell margin (1 mm) - @c_margin = margin / 10 - #Line width (0.2 mm) - @line_width = 0.567 / @k - #Automatic page break - SetAutoPageBreak(true, 2 * margin) - #Full width display mode - SetDisplayMode('fullwidth') - #Compression - SetCompression(true) - #Set default PDF version number - @pdf_version = "1.3" - - @encoding = encoding - @b = 0 - @i = 0 - @u = 0 - @href = '' - @fontlist = ["arial", "times", "courier", "helvetica", "symbol"] - @issetfont = false - @issetcolor = false - - SetFillColor(200, 200, 200, true) - SetTextColor(0, 0, 0, true) - end - - # - # Set the image scale. - # @param float :scale image scale. - # @author Nicola Asuni - # @since 1.5.2 - # - def SetImageScale(scale) - @img_scale = scale; - end - alias_method :set_image_scale, :SetImageScale - - # - # Returns the image scale. - # @return float image scale. - # @author Nicola Asuni - # @since 1.5.2 - # - def GetImageScale() - return @img_scale; - end - alias_method :get_image_scale, :GetImageScale - - # - # Returns the page width in units. - # @return int page width. - # @author Nicola Asuni - # @since 1.5.2 - # - def GetPageWidth() - return @w; - end - alias_method :get_page_width, :GetPageWidth - - # - # Returns the page height in units. - # @return int page height. - # @author Nicola Asuni - # @since 1.5.2 - # - def GetPageHeight() - return @h; - end - alias_method :get_page_height, :GetPageHeight - - # - # Returns the page break margin. - # @return int page break margin. - # @author Nicola Asuni - # @since 1.5.2 - # - def GetBreakMargin() - return @b_margin; - end - alias_method :get_break_margin, :GetBreakMargin - - # - # Returns the scale factor (number of points in user unit). - # @return int scale factor. - # @author Nicola Asuni - # @since 1.5.2 - # - def GetScaleFactor() - return @k; - end - alias_method :get_scale_factor, :GetScaleFactor - - # - # Defines the left, top and right margins. By default, they equal 1 cm. Call this method to change them. - # @param float :left Left margin. - # @param float :top Top margin. - # @param float :right Right margin. Default value is the left one. - # @since 1.0 - # @see SetLeftMargin(), SetTopMargin(), SetRightMargin(), SetAutoPageBreak() - # - def SetMargins(left, top, right=-1) - #Set left, top and right margins - @l_margin = left - @t_margin = top - if (right == -1) - right = left - end - @r_margin = right - end - alias_method :set_margins, :SetMargins - - # - # Defines the left margin. The method can be called before creating the first page. If the current abscissa gets out of page, it is brought back to the margin. - # @param float :margin The margin. - # @since 1.4 - # @see SetTopMargin(), SetRightMargin(), SetAutoPageBreak(), SetMargins() - # - def SetLeftMargin(margin) - #Set left margin - @l_margin = margin - if ((@page>0) and (@x < margin)) - @x = margin - end - end - alias_method :set_left_margin, :SetLeftMargin - - # - # Defines the top margin. The method can be called before creating the first page. - # @param float :margin The margin. - # @since 1.5 - # @see SetLeftMargin(), SetRightMargin(), SetAutoPageBreak(), SetMargins() - # - def SetTopMargin(margin) - #Set top margin - @t_margin = margin - end - alias_method :set_top_margin, :SetTopMargin - - # - # Defines the right margin. The method can be called before creating the first page. - # @param float :margin The margin. - # @since 1.5 - # @see SetLeftMargin(), SetTopMargin(), SetAutoPageBreak(), SetMargins() - # - def SetRightMargin(margin) - #Set right margin - @r_margin = margin - end - alias_method :set_right_margin, :SetRightMargin - - # - # Enables or disables the automatic page breaking mode. When enabling, the second parameter is the distance from the bottom of the page that defines the triggering limit. By default, the mode is on and the margin is 2 cm. - # @param boolean :auto Boolean indicating if mode should be on or off. - # @param float :margin Distance from the bottom of the page. - # @since 1.0 - # @see Cell(), MultiCell(), AcceptPageBreak() - # - def SetAutoPageBreak(auto, margin=0) - #Set auto page break mode and triggering margin - @auto_page_break = auto - @b_margin = margin - @page_break_trigger = @h - margin - end - alias_method :set_auto_page_break, :SetAutoPageBreak - - # - # Defines the way the document is to be displayed by the viewer. The zoom level can be set: pages can be displayed entirely on screen, occupy the full width of the window, use real size, be scaled by a specific zooming factor or use viewer default (configured in the Preferences menu of Acrobat). The page layout can be specified too: single at once, continuous display, two columns or viewer default. By default, documents use the full width mode with continuous display. - # @param mixed :zoom The zoom to use. It can be one of the following string values or a number indicating the zooming factor to use.
    • fullpage: displays the entire page on screen
    • fullwidth: uses maximum width of window
    • real: uses real size (equivalent to 100% zoom)
    • default: uses viewer default mode
    - # @param string :layout The page layout. Possible values are:
    • single: displays one page at once
    • continuous: displays pages continuously (default)
    • two: displays two pages on two columns
    • default: uses viewer default mode
    - # @since 1.2 - # - def SetDisplayMode(zoom, layout = 'continuous') - #Set display mode in viewer - if (zoom == 'fullpage' or zoom == 'fullwidth' or zoom == 'real' or zoom == 'default' or !zoom.is_a?(String)) - @zoom_mode = zoom - else - Error("Incorrect zoom display mode: #{zoom}") - end - if (layout == 'single' or layout == 'continuous' or layout == 'two' or layout == 'default') - @layout_mode = layout - else - Error("Incorrect layout display mode: #{layout}") - end - end - alias_method :set_display_mode, :SetDisplayMode - - # - # Activates or deactivates page compression. When activated, the internal representation of each page is compressed, which leads to a compression ratio of about 2 for the resulting document. Compression is on by default. - # Note: the Zlib extension is required for this feature. If not present, compression will be turned off. - # @param boolean :compress Boolean indicating if compression must be enabled. - # @since 1.4 - # - def SetCompression(compress) - #Set page compression - if (respond_to?('gzcompress')) - @compress = compress - else - @compress = false - end - end - alias_method :set_compression, :SetCompression - - # - # Defines the title of the document. - # @param string :title The title. - # @since 1.2 - # @see SetAuthor(), SetCreator(), SetKeywords(), SetSubject() - # - def SetTitle(title) - #Title of document - @title = title - end - alias_method :set_title, :SetTitle - - # - # Defines the subject of the document. - # @param string :subject The subject. - # @since 1.2 - # @see SetAuthor(), SetCreator(), SetKeywords(), SetTitle() - # - def SetSubject(subject) - #Subject of document - @subject = subject - end - alias_method :set_subject, :SetSubject - - # - # Defines the author of the document. - # @param string :author The name of the author. - # @since 1.2 - # @see SetCreator(), SetKeywords(), SetSubject(), SetTitle() - # - def SetAuthor(author) - #Author of document - @author = author - end - alias_method :set_author, :SetAuthor - - # - # Associates keywords with the document, generally in the form 'keyword1 keyword2 ...'. - # @param string :keywords The list of keywords. - # @since 1.2 - # @see SetAuthor(), SetCreator(), SetSubject(), SetTitle() - # - def SetKeywords(keywords) - #Keywords of document - @keywords = keywords - end - alias_method :set_keywords, :SetKeywords - - # - # Defines the creator of the document. This is typically the name of the application that generates the PDF. - # @param string :creator The name of the creator. - # @since 1.2 - # @see SetAuthor(), SetKeywords(), SetSubject(), SetTitle() - # - def SetCreator(creator) - #Creator of document - @creator = creator - end - alias_method :set_creator, :SetCreator - - # - # Defines an alias for the total number of pages. It will be substituted as the document is closed.
    - # Example:
    - #
    -	# class PDF extends TCPDF {
    -	# 	def Footer()
    -	# 		#Go to 1.5 cm from bottom
    -	# 		SetY(-15);
    -	# 		#Select Arial italic 8
    -	# 		SetFont('Arial','I',8);
    -	# 		#Print current and total page numbers
    -	# 		Cell(0,10,'Page '.PageNo().'/{nb}',0,0,'C');
    -	# 	end
    -	# }
    -	# :pdf=new PDF();
    -	# :pdf->alias_nb_pages();
    -	# 
    - # @param string :alias The alias. Default valuenb}. - # @since 1.4 - # @see PageNo(), Footer() - # - def AliasNbPages(alias_nb ='{nb}') - #Define an alias for total number of pages - @alias_nb_pages = escapetext(alias_nb) - end - alias_method :alias_nb_pages, :AliasNbPages - - # - # This method is automatically called in case of fatal error; it simply outputs the message and halts the execution. An inherited class may override it to customize the error handling but should always halt the script, or the resulting document would probably be invalid. - # 2004-06-11 :: Nicola Asuni : changed bold tag with strong - # @param string :msg The error message - # @since 1.0 - # - def Error(msg) - #Fatal error - raise ("TCPDF error: #{msg}") - end - alias_method :error, :Error - - # - # This method begins the generation of the PDF document. It is not necessary to call it explicitly because AddPage() does it automatically. - # Note: no page is created by this method - # @since 1.0 - # @see AddPage(), Close() - # - def Open() - #Begin document - @state = 1 - end - # alias_method :open, :Open - - # - # Terminates the PDF document. It is not necessary to call this method explicitly because Output() does it automatically. If the document contains no page, AddPage() is called to prevent from getting an invalid document. - # @since 1.0 - # @see Open(), Output() - # - def Close() - #Terminate document - if (@state==3) - return; - end - if (@page==0) - AddPage(); - end - #Page footer - @in_footer=true; - Footer(); - @in_footer=false; - #Close page - endpage(); - #Close document - enddoc(); - end - # alias_method :close, :Close - - # - # Adds a new page to the document. If a page is already present, the Footer() method is called first to output the footer. Then the page is added, the current position set to the top-left corner according to the left and top margins, and Header() is called to display the header. - # The font which was set before calling is automatically restored. There is no need to call SetFont() again if you want to continue with the same font. The same is true for colors and line width. - # The origin of the coordinate system is at the top-left corner and increasing ordinates go downwards. - # @param string :orientation Page orientation. Possible values are (case insensitive):
    • P or Portrait
    • L or Landscape
    The default value is the one passed to the constructor. - # @since 1.0 - # @see TCPDF(), Header(), Footer(), SetMargins() - # - def AddPage(orientation='') - #Start a new page - if (@state==0) - Open(); - end - family=@font_family; - style=@font_style + (@underline ? 'U' : '') + (@deleted ? 'D' : ''); - size=@font_size_pt; - lw=@line_width; - dc=@draw_color; - fc=@fill_color; - tc=@text_color; - cf=@color_flag; - if (@page>0) - #Page footer - @in_footer=true; - Footer(); - @in_footer=false; - #Close page - endpage(); - end - #Start new page - beginpage(orientation); - #Set line cap style to square - out('2 J'); - #Set line width - @line_width = lw; - out(sprintf('%.2f w', lw*@k)); - #Set font - if (family) - SetFont(family, style, size); - end - #Set colors - @draw_color = dc; - if (dc!='0 G') - out(dc); - end - @fill_color = fc; - if (fc!='0 g') - out(fc); - end - @text_color = tc; - @color_flag = cf; - #Page header - Header(); - #Restore line width - if (@line_width != lw) - @line_width = lw; - out(sprintf('%.2f w', lw*@k)); - end - #Restore font - if (family) - SetFont(family, style, size); - end - #Restore colors - if (@draw_color != dc) - @draw_color = dc; - out(dc); - end - if (@fill_color != fc) - @fill_color = fc; - out(fc); - end - @text_color = tc; - @color_flag = cf; - end - alias_method :add_page, :AddPage - - # - # Rotate object. - # @param float :angle angle in degrees for counter-clockwise rotation - # @param int :x abscissa of the rotation center. Default is current x position - # @param int :y ordinate of the rotation center. Default is current y position - # - def Rotate(angle, x="", y="") - - if (x == '') - x = @x; - end - - if (y == '') - y = @y; - end - - if (@rtl) - x = @w - x; - angle = -@angle; - end - - y = (@h - y) * @k; - x *= @k; - - # calculate elements of transformation matrix - tm = [] - tm[0] = ::Math::cos(deg2rad(angle)); - tm[1] = ::Math::sin(deg2rad(angle)); - tm[2] = -tm[1]; - tm[3] = tm[0]; - tm[4] = x + tm[1] * y - tm[0] * x; - tm[5] = y - tm[0] * y - tm[1] * x; - - # generate the transformation matrix - Transform(tm); - end - alias_method :rotate, :Rotate - - # - # Starts a 2D tranformation saving current graphic state. - # This function must be called before scaling, mirroring, translation, rotation and skewing. - # Use StartTransform() before, and StopTransform() after the transformations to restore the normal behavior. - # - def StartTransform - out('q'); - end - alias_method :start_transform, :StartTransform - - # - # Stops a 2D tranformation restoring previous graphic state. - # This function must be called after scaling, mirroring, translation, rotation and skewing. - # Use StartTransform() before, and StopTransform() after the transformations to restore the normal behavior. - # - def StopTransform - out('Q'); - end - alias_method :stop_transform, :StopTransform - - # - # Apply graphic transformations. - # @since 2.1.000 (2008-01-07) - # @see StartTransform(), StopTransform() - # - def Transform(tm) - x = out(sprintf('%.3f %.3f %.3f %.3f %.3f %.3f cm', tm[0], tm[1], tm[2], tm[3], tm[4], tm[5])); - end - alias_method :transform, :Transform - - # - # Set header data. - # @param string :ln header image logo - # @param string :lw header image logo width in mm - # @param string :ht string to print as title on document header - # @param string :hs string to print on document header - # - def SetHeaderData(ln="", lw=0, ht="", hs="") - @header_logo = ln || "" - @header_logo_width = lw || 0 - @header_title = ht || "" - @header_string = hs || "" - end - alias_method :set_header_data, :SetHeaderData - - # - # Set header margin. - # (minimum distance between header and top page margin) - # @param int :hm distance in millimeters - # - def SetHeaderMargin(hm=10) - @header_margin = hm; - end - alias_method :set_header_margin, :SetHeaderMargin - - # - # Set footer margin. - # (minimum distance between footer and bottom page margin) - # @param int :fm distance in millimeters - # - def SetFooterMargin(fm=10) - @footer_margin = fm; - end - alias_method :set_footer_margin, :SetFooterMargin - - # - # Set a flag to print page header. - # @param boolean :val set to true to print the page header (default), false otherwise. - # - def SetPrintHeader(val=true) - @print_header = val; - end - alias_method :set_print_header, :SetPrintHeader - - # - # Set a flag to print page footer. - # @param boolean :value set to true to print the page footer (default), false otherwise. - # - def SetPrintFooter(val=true) - @print_footer = val; - end - alias_method :set_print_footer, :SetPrintFooter - - # - # This method is used to render the page header. - # It is automatically called by AddPage() and could be overwritten in your own inherited class. - # - def Header() - if (@print_header) - if (@original_l_margin.nil?) - @original_l_margin = @l_margin; - end - if (@original_r_margin.nil?) - @original_r_margin = @r_margin; - end - - #set current position - SetXY(@original_l_margin, @header_margin); - - if ((@header_logo) and (@header_logo != @@k_blank_image)) - Image(@header_logo, @original_l_margin, @header_margin, @header_logo_width); - else - @img_rb_y = GetY(); - end - - cell_height = ((@@k_cell_height_ratio * @header_font[2]) / @k).round(2) - - header_x = @original_l_margin + (@header_logo_width * 1.05); #set left margin for text data cell - - # header title - SetFont(@header_font[0], 'B', @header_font[2] + 1); - SetX(header_x); - Cell(@header_width, cell_height, @header_title, 0, 1, 'L'); - - # header string - SetFont(@header_font[0], @header_font[1], @header_font[2]); - SetX(header_x); - MultiCell(@header_width, cell_height, @header_string, 0, 'L', 0); - - # print an ending header line - if (@header_width) - #set style for cell border - SetLineWidth(0.3); - SetDrawColor(0, 0, 0); - SetY(1 + (@img_rb_y > GetY() ? @img_rb_y : GetY())); - SetX(@original_l_margin); - Cell(0, 0, '', 'T', 0, 'C'); - end - - #restore position - SetXY(@original_l_margin, @t_margin); - end - end - alias_method :header, :Header - - # - # This method is used to render the page footer. - # It is automatically called by AddPage() and could be overwritten in your own inherited class. - # - def Footer() - if (@print_footer) - - if (@original_l_margin.nil?) - @original_l_margin = @l_margin; - end - if (@original_r_margin.nil?) - @original_r_margin = @r_margin; - end - - #set font - SetFont(@footer_font[0], @footer_font[1] , @footer_font[2]); - #set style for cell border - line_width = 0.3; - SetLineWidth(line_width); - SetDrawColor(0, 0, 0); - - footer_height = ((@@k_cell_height_ratio * @footer_font[2]) / @k).round; #footer height, was , 2) - #get footer y position - footer_y = @h - @footer_margin - footer_height; - #set current position - SetXY(@original_l_margin, footer_y); - - #print document barcode - if (@barcode) - Ln(); - barcode_width = ((@w - @original_l_margin - @original_r_margin)).round; #max width - writeBarcode(@original_l_margin, footer_y + line_width, barcode_width, footer_height - line_width, "C128B", false, false, 2, @barcode); - end - - SetXY(@original_l_margin, footer_y); - - #Print page number - Cell(0, footer_height, @l['w_page'] + " " + PageNo().to_s + ' / {nb}', 'T', 0, 'R'); - end - end - alias_method :footer, :Footer - - # - # Returns the current page number. - # @return int page number - # @since 1.0 - # @see alias_nb_pages() - # - def PageNo() - #Get current page number - return @page; - end - alias_method :page_no, :PageNo - - # - # Defines the color used for all drawing operations (lines, rectangles and cell borders). It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page. - # @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255 - # @param int :g Green component (between 0 and 255) - # @param int :b Blue component (between 0 and 255) - # @since 1.3 - # @see SetFillColor(), SetTextColor(), Line(), Rect(), Cell(), MultiCell() - # - def SetDrawColor(r, g=-1, b=-1) - #Set color for all stroking operations - if ((r==0 and g==0 and b==0) or g==-1) - @draw_color=sprintf('%.3f G', r/255.0); - else - @draw_color=sprintf('%.3f %.3f %.3f RG', r/255.0, g/255.0, b/255.0); - end - if (@page>0) - out(@draw_color); - end - end - alias_method :set_draw_color, :SetDrawColor - - # - # Defines the color used for all filling operations (filled rectangles and cell backgrounds). It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page. - # @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255 - # @param int :g Green component (between 0 and 255) - # @param int :b Blue component (between 0 and 255) - # @param boolean :storeprev if true stores the RGB array on :prevfill_color variable. - # @since 1.3 - # @see SetDrawColor(), SetTextColor(), Rect(), Cell(), MultiCell() - # - def SetFillColor(r, g=-1, b=-1, storeprev=false) - #Set color for all filling operations - if ((r==0 and g==0 and b==0) or g==-1) - @fill_color=sprintf('%.3f g', r/255.0); - else - @fill_color=sprintf('%.3f %.3f %.3f rg', r/255.0, g/255.0, b/255.0); - end - @color_flag=(@fill_color!=@text_color); - if (@page>0) - out(@fill_color); - end - if (storeprev) - # store color as previous value - @prevfill_color = [r, g, b] - end - end - alias_method :set_fill_color, :SetFillColor - - # This hasn't been ported from tcpdf, it's a variation on SetTextColor for setting cmyk colors - def SetCmykFillColor(c, m, y, k, storeprev=false) - #Set color for all filling operations - @fill_color=sprintf('%.3f %.3f %.3f %.3f k', c, m, y, k); - @color_flag=(@fill_color!=@text_color); - if (storeprev) - # store color as previous value - @prevtext_color = [c, m, y, k] - end - if (@page>0) - out(@fill_color); - end - end - alias_method :set_cmyk_fill_color, :SetCmykFillColor - - # - # Defines the color used for text. It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page. - # @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255 - # @param int :g Green component (between 0 and 255) - # @param int :b Blue component (between 0 and 255) - # @param boolean :storeprev if true stores the RGB array on :prevtext_color variable. - # @since 1.3 - # @see SetDrawColor(), SetFillColor(), Text(), Cell(), MultiCell() - # - def SetTextColor(r, g=-1, b=-1, storeprev=false) - #Set color for text - if ((r==0 and :g==0 and :b==0) or :g==-1) - @text_color=sprintf('%.3f g', r/255.0); - else - @text_color=sprintf('%.3f %.3f %.3f rg', r/255.0, g/255.0, b/255.0); - end - @color_flag=(@fill_color!=@text_color); - if (storeprev) - # store color as previous value - @prevtext_color = [r, g, b] - end - end - alias_method :set_text_color, :SetTextColor - - # This hasn't been ported from tcpdf, it's a variation on SetTextColor for setting cmyk colors - def SetCmykTextColor(c, m, y, k, storeprev=false) - #Set color for text - @text_color=sprintf('%.3f %.3f %.3f %.3f k', c, m, y, k); - @color_flag=(@fill_color!=@text_color); - if (storeprev) - # store color as previous value - @prevtext_color = [c, m, y, k] - end - end - alias_method :set_cmyk_text_color, :SetCmykTextColor - - # - # Returns the length of a string in user unit. A font must be selected.
    - # Support UTF-8 Unicode [Nicola Asuni, 2005-01-02] - # @param string :s The string whose length is to be computed - # @return int - # @since 1.2 - # - def GetStringWidth(s) - #Get width of a string in the current font - s = s.to_s; - cw = @current_font['cw'] - w = 0; - if (@is_unicode) - unicode = UTF8StringToArray(s); - unicode.each do |char| - if (!cw[char].nil?) - w += cw[char]; - # This should not happen. UTF8StringToArray should guarentee the array is ascii values. - # elsif (c!cw[char[0]].nil?) - # w += cw[char[0]]; - # elsif (!cw[char.chr].nil?) - # w += cw[char.chr]; - elsif (!@current_font['desc']['MissingWidth'].nil?) - w += @current_font['desc']['MissingWidth']; # set default size - else - w += 500; - end - end - else - s.each_byte do |c| - if cw[c.chr] - w += cw[c.chr]; - elsif cw[?c.chr] - w += cw[?c.chr] - end - end - end - return (w * @font_size / 1000.0); - end - alias_method :get_string_width, :GetStringWidth - - # - # Defines the line width. By default, the value equals 0.2 mm. The method can be called before the first page is created and the value is retained from page to page. - # @param float :width The width. - # @since 1.0 - # @see Line(), Rect(), Cell(), MultiCell() - # - def SetLineWidth(width) - #Set line width - @line_width = width; - if (@page>0) - out(sprintf('%.2f w', width*@k)); - end - end - alias_method :set_line_width, :SetLineWidth - - # - # Draws a line between two points. - # @param float :x1 Abscissa of first point - # @param float :y1 Ordinate of first point - # @param float :x2 Abscissa of second point - # @param float :y2 Ordinate of second point - # @since 1.0 - # @see SetLineWidth(), SetDrawColor() - # - def Line(x1, y1, x2, y2) - #Draw a line - out(sprintf('%.2f %.2f m %.2f %.2f l S', x1 * @k, (@h - y1) * @k, x2 * @k, (@h - y2) * @k)); - end - alias_method :line, :Line - - def Circle(mid_x, mid_y, radius, style='') - mid_y = (@h-mid_y)*@k - out(sprintf("q\n")) # postscript content in pdf - # init line type etc. with /GSD gs G g (grey) RG rg (RGB) w=line witdh etc. - out(sprintf("1 j\n")) # line join - # translate ("move") circle to mid_y, mid_y - out(sprintf("1 0 0 1 %f %f cm", mid_x, mid_y)) - kappa = 0.5522847498307933984022516322796 - # Quadrant 1 - x_s = 0.0 # 12 o'clock - y_s = 0.0 + radius - x_e = 0.0 + radius # 3 o'clock - y_e = 0.0 - out(sprintf("%f %f m\n", x_s, y_s)) # move to 12 o'clock - # cubic bezier control point 1, start height and kappa * radius to the right - bx_e1 = x_s + (radius * kappa) - by_e1 = y_s - # cubic bezier control point 2, end and kappa * radius above - bx_e2 = x_e - by_e2 = y_e + (radius * kappa) - # draw cubic bezier from current point to x_e/y_e with bx_e1/by_e1 and bx_e2/by_e2 as bezier control points - out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e)) - # Quadrant 2 - x_s = x_e - y_s = y_e # 3 o'clock - x_e = 0.0 - y_e = 0.0 - radius # 6 o'clock - bx_e1 = x_s # cubic bezier point 1 - by_e1 = y_s - (radius * kappa) - bx_e2 = x_e + (radius * kappa) # cubic bezier point 2 - by_e2 = y_e - out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e)) - # Quadrant 3 - x_s = x_e - y_s = y_e # 6 o'clock - x_e = 0.0 - radius - y_e = 0.0 # 9 o'clock - bx_e1 = x_s - (radius * kappa) # cubic bezier point 1 - by_e1 = y_s - bx_e2 = x_e # cubic bezier point 2 - by_e2 = y_e - (radius * kappa) - out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e)) - # Quadrant 4 - x_s = x_e - y_s = y_e # 9 o'clock - x_e = 0.0 - y_e = 0.0 + radius # 12 o'clock - bx_e1 = x_s # cubic bezier point 1 - by_e1 = y_s + (radius * kappa) - bx_e2 = x_e - (radius * kappa) # cubic bezier point 2 - by_e2 = y_e - out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e)) - if style=='F' - op='f' - elsif style=='FD' or style=='DF' - op='b' - else - op='s' - end - out(sprintf("#{op}\n")) # stroke circle, do not fill and close path - # for filling etc. b, b*, f, f* - out(sprintf("Q\n")) # finish postscript in PDF - end - alias_method :circle, :Circle - - # - # Outputs a rectangle. It can be drawn (border only), filled (with no border) or both. - # @param float :x Abscissa of upper-left corner - # @param float :y Ordinate of upper-left corner - # @param float :w Width - # @param float :h Height - # @param string :style Style of rendering. Possible values are:
    • D or empty string: draw (default)
    • F: fill
    • DF or FD: draw and fill
    - # @since 1.0 - # @see SetLineWidth(), SetDrawColor(), SetFillColor() - # - def Rect(x, y, w, h, style='') - #Draw a rectangle - if (style=='F') - op='f'; - elsif (style=='FD' or style=='DF') - op='B'; - else - op='S'; - end - out(sprintf('%.2f %.2f %.2f %.2f re %s', x * @k, (@h - y) * @k, w * @k, -h * @k, op)); - end - alias_method :rect, :Rect - - # - # Imports a TrueType or Type1 font and makes it available. It is necessary to generate a font definition file first with the makefont.rb utility. The definition file (and the font file itself when embedding) must be present either in the current directory or in the one indicated by FPDF_FONTPATH if the constant is defined. If it could not be found, the error "Could not include font definition file" is generated. - # Support UTF-8 Unicode [Nicola Asuni, 2005-01-02]. - # Example:
    - #
    -	# :pdf->AddFont('Comic','I');
    -	# # is equivalent to:
    -	# :pdf->AddFont('Comic','I','comici.rb');
    -	# 
    - # @param string :family Font family. The name can be chosen arbitrarily. If it is a standard family name, it will override the corresponding font. - # @param string :style Font style. Possible values are (case insensitive):
    • empty string: regular (default)
    • B: bold
    • I: italic
    • BI or IB: bold italic
    - # @param string :file The font definition file. By default, the name is built from the family and style, in lower case with no space. - # @since 1.5 - # @see SetFont() - # - def AddFont(family, style='', file='') - if (family.empty?) - return; - end - - #Add a TrueType or Type1 font - family = family.downcase - if ((!@is_unicode) and (family == 'arial')) - family = 'helvetica'; - end - - style=style.upcase - style=style.gsub('U',''); - style=style.gsub('D',''); - if (style == 'IB') - style = 'BI'; - end - - fontkey = family + style; - # check if the font has been already added - if !@fonts[fontkey].nil? - return; - end - - if (file=='') - file = family.gsub(' ', '') + style.downcase + '.rb'; - end - font_file_name = getfontpath(file) - if (font_file_name.nil?) - # try to load the basic file without styles - file = family.gsub(' ', '') + '.rb'; - font_file_name = getfontpath(file) - end - if font_file_name.nil? - Error("Could not find font #{file}.") - end - require(getfontpath(file)) - font_desc = TCPDFFontDescriptor.font(file) - - if (font_desc[:name].nil? and @@fpdf_charwidths.nil?) - Error('Could not include font definition file'); - end - - i = @fonts.length+1; - if (@is_unicode) - @fonts[fontkey] = {'i' => i, 'type' => font_desc[:type], 'name' => font_desc[:name], 'desc' => font_desc[:desc], 'up' => font_desc[:up], 'ut' => font_desc[:ut], 'cw' => font_desc[:cw], 'enc' => font_desc[:enc], 'file' => font_desc[:file], 'ctg' => font_desc[:ctg], 'cMap' => font_desc[:cMap], 'registry' => font_desc[:registry]} - @@fpdf_charwidths[fontkey] = font_desc[:cw]; - else - @fonts[fontkey]={'i' => i, 'type'=>'core', 'name'=>@core_fonts[fontkey], 'up'=>-100, 'ut'=>50, 'cw' => font_desc[:cw]} - @@fpdf_charwidths[fontkey] = font_desc[:cw]; - end - - if (!font_desc[:diff].nil? and (!font_desc[:diff].empty?)) - #Search existing encodings - d=0; - nb=@diffs.length; - 1.upto(nb) do |i| - if (@diffs[i]== font_desc[:diff]) - d = i; - break; - end - end - if (d==0) - d = nb+1; - @diffs[d] = font_desc[:diff]; - end - @fonts[fontkey]['diff'] = d; - end - if (font_desc[:file] and font_desc[:file].length > 0) - if (font_desc[:type] == "TrueType") or (font_desc[:type] == "TrueTypeUnicode") - @font_files[font_desc[:file]] = {'length1' => font_desc[:originalsize]} - else - @font_files[font_desc[:file]] = {'length1' => font_desc[:size1], 'length2' => font_desc[:size2]} - end - end - end - alias_method :add_font, :AddFont - - # - # Sets the font used to print character strings. It is mandatory to call this method at least once before printing text or the resulting document would not be valid. - # The font can be either a standard one or a font added via the AddFont() method. Standard fonts use Windows encoding cp1252 (Western Europe). - # The method can be called before the first page is created and the font is retained from page to page. - # If you just wish to change the current font size, it is simpler to call SetFontSize(). - # Note: for the standard fonts, the font metric files must be accessible. There are three possibilities for this:
    • They are in the current directory (the one where the running script lies)
    • They are in one of the directories defined by the include_path parameter
    • They are in the directory defined by the FPDF_FONTPATH constant

    - # Example for the last case (note the trailing slash):
    - #
    -	# define('FPDF_FONTPATH','/home/www/font/');
    -	# require('tcpdf.rb');
    -	#
    -	# #Times regular 12
    -	# :pdf->SetFont('Times');
    -	# #Arial bold 14
    -	# :pdf->SetFont('Arial','B',14);
    -	# #Removes bold
    -	# :pdf->SetFont('');
    -	# #Times bold, italic and underlined 14
    -	# :pdf->SetFont('Times','BIUD');
    -	# 

    - # If the file corresponding to the requested font is not found, the error "Could not include font metric file" is generated. - # @param string :family Family font. It can be either a name defined by AddFont() or one of the standard families (case insensitive):
    • Courier (fixed-width)
    • Helvetica or Arial (synonymous; sans serif)
    • Times (serif)
    • Symbol (symbolic)
    • ZapfDingbats (symbolic)
    It is also possible to pass an empty string. In that case, the current family is retained. - # @param string :style Font style. Possible values are (case insensitive):
    • empty string: regular
    • B: bold
    • I: italic
    • U: underline
    or any combination. The default value is regular. Bold and italic styles do not apply to Symbol and ZapfDingbats - # @param float :size Font size in points. The default value is the current size. If no size has been specified since the beginning of the document, the value taken is 12 - # @since 1.0 - # @see AddFont(), SetFontSize(), Cell(), MultiCell(), Write() - # - def SetFont(family, style='', size=0) - # save previous values - @prevfont_family = @font_family; - @prevfont_style = @font_style; - - family=family.downcase; - if (family=='') - family=@font_family; - end - if ((!@is_unicode) and (family == 'arial')) - family = 'helvetica'; - elsif ((family=="symbol") or (family=="zapfdingbats")) - style=''; - end - - style=style.upcase; - - if (style.include?('U')) - @underline=true; - style= style.gsub('U',''); - else - @underline=false; - end - if (style.include?('D')) - @deleted=true; - style= style.gsub('D',''); - else - @deleted=false; - end - if (style=='IB') - style='BI'; - end - if (size==0) - size=@font_size_pt; - end - - # try to add font (if not already added) - AddFont(family, style); - - #Test if font is already selected - if ((@font_family == family) and (@font_style == style) and (@font_size_pt == size)) - return; - end - - fontkey = family + style; - style = '' if (@fonts[fontkey].nil? and !@fonts[family].nil?) - - #Test if used for the first time - if (@fonts[fontkey].nil?) - #Check if one of the standard fonts - if (!@core_fonts[fontkey].nil?) - if @@fpdf_charwidths[fontkey].nil? - #Load metric file - file = family; - if ((family!='symbol') and (family!='zapfdingbats')) - file += style.downcase; - end - if (getfontpath(file + '.rb').nil?) - # try to load the basic file without styles - file = family; - fontkey = family; - end - require(getfontpath(file + '.rb')); - font_desc = TCPDFFontDescriptor.font(file) - if ((@is_unicode and ctg.nil?) or ((!@is_unicode) and (@@fpdf_charwidths[fontkey].nil?)) ) - Error("Could not include font metric file [" + fontkey + "]: " + getfontpath(file + ".rb")); - end - end - i = @fonts.length + 1; - - if (@is_unicode) - @fonts[fontkey] = {'i' => i, 'type' => font_desc[:type], 'name' => font_desc[:name], 'desc' => font_desc[:desc], 'up' => font_desc[:up], 'ut' => font_desc[:ut], 'cw' => font_desc[:cw], 'enc' => font_desc[:enc], 'file' => font_desc[:file], 'ctg' => font_desc[:ctg]} - @@fpdf_charwidths[fontkey] = font_desc[:cw]; - else - @fonts[fontkey] = {'i' => i, 'type'=>'core', 'name'=>@core_fonts[fontkey], 'up'=>-100, 'ut'=>50, 'cw' => font_desc[:cw]} - @@fpdf_charwidths[fontkey] = font_desc[:cw]; - end - else - Error('Undefined font: ' + family + ' ' + style); - end - end - #Select it - @font_family = family; - @font_style = style; - @font_size_pt = size; - @font_size = size / @k; - @current_font = @fonts[fontkey]; # was & may need deep copy? - if (@page>0) - out(sprintf('BT /F%d %.2f Tf ET', @current_font['i'], @font_size_pt)); - end - end - alias_method :set_font, :SetFont - - # - # Defines the size of the current font. - # @param float :size The size (in points) - # @since 1.0 - # @see SetFont() - # - def SetFontSize(size) - #Set font size in points - if (@font_size_pt== size) - return; - end - @font_size_pt = size; - @font_size = size.to_f / @k; - if (@page > 0) - out(sprintf('BT /F%d %.2f Tf ET', @current_font['i'], @font_size_pt)); - end - end - alias_method :set_font_size, :SetFontSize - - # - # Creates a new internal link and returns its identifier. An internal link is a clickable area which directs to another place within the document.
    - # The identifier can then be passed to Cell(), Write(), Image() or Link(). The destination is defined with SetLink(). - # @since 1.5 - # @see Cell(), Write(), Image(), Link(), SetLink() - # - def AddLink() - #Create a new internal link - n=@links.length+1; - @links[n]=[0,0]; - return n; - end - alias_method :add_link, :AddLink - - # - # Defines the page and position a link points to - # @param int :link The link identifier returned by AddLink() - # @param float :y Ordinate of target position; -1 indicates the current position. The default value is 0 (top of page) - # @param int :page Number of target page; -1 indicates the current page. This is the default value - # @since 1.5 - # @see AddLink() - # - def SetLink(link, y=0, page=-1) - #Set destination of internal link - if (y==-1) - y=@y; - end - if (page==-1) - page=@page; - end - @links[link] = [page, y] - end - alias_method :set_link, :SetLink - - # - # Puts a link on a rectangular area of the page. Text or image links are generally put via Cell(), Write() or Image(), but this method can be useful for instance to define a clickable area inside an image. - # @param float :x Abscissa of the upper-left corner of the rectangle - # @param float :y Ordinate of the upper-left corner of the rectangle - # @param float :w Width of the rectangle - # @param float :h Height of the rectangle - # @param mixed :link URL or identifier returned by AddLink() - # @since 1.5 - # @see AddLink(), Cell(), Write(), Image() - # - def Link(x, y, w, h, link) - #Put a link on the page - @page_links ||= Array.new - @page_links[@page] ||= Array.new - @page_links[@page].push([x * @k, @h_pt - y * @k, w * @k, h*@k, link]); - end - alias_method :link, :Link - - # - # Prints a character string. The origin is on the left of the first charcter, on the baseline. This method allows to place a string precisely on the page, but it is usually easier to use Cell(), MultiCell() or Write() which are the standard methods to print text. - # @param float :x Abscissa of the origin - # @param float :y Ordinate of the origin - # @param string :txt String to print - # @since 1.0 - # @see SetFont(), SetTextColor(), Cell(), MultiCell(), Write() - # - def Text(x, y, txt) - #Output a string - s=sprintf('BT %.2f %.2f Td (%s) Tj ET', x * @k, (@h-y) * @k, escapetext(txt)); - if (@underline and (txt!='')) - s += ' ' + dolinetxt(x, y, txt); - end - if (@color_flag) - s='q ' + @text_color + ' ' + s + ' Q'; - end - out(s); - end - alias_method :text, :Text - - # - # Whenever a page break condition is met, the method is called, and the break is issued or not depending on the returned value. The default implementation returns a value according to the mode selected by SetAutoPageBreak().
    - # This method is called automatically and should not be called directly by the application.
    - # Example:
    - # The method is overriden in an inherited class in order to obtain a 3 column layout:
    - #
    -	# class PDF extends TCPDF {
    -	# 	var :col=0;
    -	#
    -	# 	def SetCol(col)
    -	# 		#Move position to a column
    -	# 		@col = col;
    -	# 		:x=10+:col*65;
    -	# 		SetLeftMargin(x);
    -	# 		SetX(x);
    -	# 	end
    -	#
    -	# 	def AcceptPageBreak()
    -	# 		if (@col<2)
    -	# 			#Go to next column
    -	# 			SetCol(@col+1);
    -	# 			SetY(10);
    -	# 			return false;
    -	# 		end
    -	# 		else
    -	# 			#Go back to first column and issue page break
    -	# 			SetCol(0);
    -	# 			return true;
    -	# 		end
    -	# 	end
    -	# }
    -	#
    -	# :pdf=new PDF();
    -	# :pdf->Open();
    -	# :pdf->AddPage();
    -	# :pdf->SetFont('Arial','',12);
    -	# for(i=1;:i<=300;:i++)
    -	#     :pdf->Cell(0,5,"Line :i",0,1);
    -	# }
    -	# :pdf->Output();
    -	# 
    - # @return boolean - # @since 1.4 - # @see SetAutoPageBreak() - # - def AcceptPageBreak() - #Accept automatic page break or not - return @auto_page_break; - end - alias_method :accept_page_break, :AcceptPageBreak - - def BreakThePage?(h) - if ((@y + h) > @page_break_trigger and !@in_footer and AcceptPageBreak()) - true - else - false - end - end - alias_method :break_the_page?, :BreakThePage? - # - # Prints a cell (rectangular area) with optional borders, background color and character string. The upper-left corner of the cell corresponds to the current position. The text can be aligned or centered. After the call, the current position moves to the right or to the next line. It is possible to put a link on the text.
    - # If automatic page breaking is enabled and the cell goes beyond the limit, a page break is done before outputting. - # @param float :w Cell width. If 0, the cell extends up to the right margin. - # @param float :h Cell height. Default value: 0. - # @param string :txt String to print. Default value: empty string. - # @param mixed :border Indicates if borders must be drawn around the cell. The value can be either a number:
    • 0: no border (default)
    • 1: frame
    or a string containing some or all of the following characters (in any order):
    • L: left
    • T: top
    • R: right
    • B: bottom
    - # @param int :ln Indicates where the current position should go after the call. Possible values are:
    • 0: to the right
    • 1: to the beginning of the next line
    • 2: below
    - # Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: 0. - # @param string :align Allows to center or align the text. Possible values are:
    • L or empty string: left align (default value)
    • C: center
    • R: right align
    - # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. - # @param mixed :link URL or identifier returned by AddLink(). - # @since 1.0 - # @see SetFont(), SetDrawColor(), SetFillColor(), SetTextColor(), SetLineWidth(), AddLink(), Ln(), MultiCell(), Write(), SetAutoPageBreak() - # - def Cell(w, h=0, txt='', border=0, ln=0, align='', fill=0, link=nil) - #Output a cell - k=@k; - if ((@y + h) > @page_break_trigger and !@in_footer and AcceptPageBreak()) - #Automatic page break - if @pages[@page+1].nil? - x = @x; - ws = @ws; - if (ws > 0) - @ws = 0; - out('0 Tw'); - end - AddPage(@cur_orientation); - @x = x; - if (ws > 0) - @ws = ws; - out(sprintf('%.3f Tw', ws * k)); - end - else - @page += 1; - @y=@t_margin; - end - end - - if (w == 0) - w = @w - @r_margin - @x; - end - s = ''; - if ((fill.to_i == 1) or (border.to_i == 1)) - if (fill.to_i == 1) - op = (border.to_i == 1) ? 'B' : 'f'; - else - op = 'S'; - end - s = sprintf('%.2f %.2f %.2f %.2f re %s ', @x * k, (@h - @y) * k, w * k, -h * k, op); - end - if (border.is_a?(String)) - x=@x; - y=@y; - if (border.include?('L')) - s<0) - # Go to next line - @y += h; - if (ln == 1) - @x = @l_margin; - end - else - @x += w; - end - end - alias_method :cell, :Cell - - # - # This method allows printing text with line breaks. They can be automatic (as soon as the text reaches the right border of the cell) or explicit (via the \n character). As many cells as necessary are output, one below the other.
    - # Text can be aligned, centered or justified. The cell block can be framed and the background painted. - # @param float :w Width of cells. If 0, they extend up to the right margin of the page. - # @param float :h Height of cells. - # @param string :txt String to print - # @param mixed :border Indicates if borders must be drawn around the cell block. The value can be either a number:
    • 0: no border (default)
    • 1: frame
    or a string containing some or all of the following characters (in any order):
    • L: left
    • T: top
    • R: right
    • B: bottom
    - # @param string :align Allows to center or align the text. Possible values are:
    • L or empty string: left align
    • C: center
    • R: right align
    • J: justification (default value)
    - # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. - # @param int :ln Indicates where the current position should go after the call. Possible values are:
    • 0: to the right
    • 1: to the beginning of the next line [DEFAULT]
    • 2: below
    - # @since 1.3 - # @see SetFont(), SetDrawColor(), SetFillColor(), SetTextColor(), SetLineWidth(), Cell(), Write(), SetAutoPageBreak() - # - def MultiCell(w, h, txt, border=0, align='J', fill=0, ln=1) - - # save current position - prevx = @x; - prevy = @y; - prevpage = @page; - - #Output text with automatic or explicit line breaks - - if (w == 0) - w = @w - @r_margin - @x; - end - - wmax = (w - 2 * @c_margin); - - s = txt.gsub("\r", ''); # remove carriage returns - nb = s.length; - - b=0; - if (border) - if (border==1) - border='LTRB'; - b='LRT'; - b2='LR'; - elsif border.is_a?(String) - b2=''; - if (border.include?('L')) - b2<<'L'; - end - if (border.include?('R')) - b2<<'R'; - end - b=(border.include?('T')) ? b2 + 'T' : b2; - end - end - sep=-1; - to_index=0; - from_j=0; - l=0; - ns=0; - nl=1; - - while to_index < nb - #Get next character - c = s[to_index]; - if c == "\n"[0] - #Explicit line break - if @ws > 0 - @ws = 0 - out('0 Tw') - end - #Ed Moss - change begin - end_i = to_index == 0 ? 0 : to_index - 1 - # Changed from s[from_j..to_index] to fix bug reported by Hans Allis. - from_j = to_index == 0 ? 1 : from_j - Cell(w, h, s[from_j..end_i], b, 2, align, fill) - #change end - to_index += 1 - sep=-1 - from_j=to_index - l=0 - ns=0 - nl += 1 - b = b2 if border and nl==2 - next - end - if (c == " "[0]) - sep = to_index; - ls = l; - ns += 1; - end - - l = GetStringWidth(s[from_j, to_index - from_j + 1]); - - if (l > wmax) - #Automatic line break - if (sep == -1) - if (to_index == from_j) - to_index += 1; - end - if (@ws > 0) - @ws = 0; - out('0 Tw'); - end - Cell(w, h, s[from_j..to_index-1], b, 2, align, fill) # my FPDF version - else - if (align=='J' || align=='justify' || align=='justified') - @ws = (ns>1) ? (wmax-ls)/(ns-1) : 0; - out(sprintf('%.3f Tw', @ws * @k)); - end - Cell(w, h, s[from_j..sep], b, 2, align, fill); - to_index = sep + 1; - end - sep=-1; - from_j = to_index; - l=0; - ns=0; - nl += 1; - if (border and (nl==2)) - b = b2; - end - else - to_index += 1; - end - end - #Last chunk - if (@ws>0) - @ws=0; - out('0 Tw'); - end - if (border.is_a?(String) and border.include?('B')) - b<<'B'; - end - Cell(w, h, s[from_j, to_index-from_j], b, 2, align, fill); - - # move cursor to specified position - # since 2007-03-03 - if (ln == 1) - # go to the beginning of the next line - @x = @l_margin; - elsif (ln == 0) - # go to the top-right of the cell - @page = prevpage; - @y = prevy; - @x = prevx + w; - elsif (ln == 2) - # go to the bottom-left of the cell - @x = prevx; - end - end - alias_method :multi_cell, :MultiCell - - # - # This method prints text from the current position. When the right margin is reached (or the \n character is met) a line break occurs and text continues from the left margin. Upon method exit, the current position is left just at the end of the text. It is possible to put a link on the text.
    - # Example:
    - #
    -	# #Begin with regular font
    -	# :pdf->SetFont('Arial','',14);
    -	# :pdf->Write(5,'Visit ');
    -	# #Then put a blue underlined link
    -	# :pdf->SetTextColor(0,0,255);
    -	# :pdf->SetFont('','U');
    -	# :pdf->Write(5,'www.tecnick.com','http://www.tecnick.com');
    -	# 
    - # @param float :h Line height - # @param string :txt String to print - # @param mixed :link URL or identifier returned by AddLink() - # @param int :fill Indicates if the background must be painted (1) or transparent (0). Default value: 0. - # @since 1.5 - # @see SetFont(), SetTextColor(), AddLink(), MultiCell(), SetAutoPageBreak() - # - def Write(h, txt, link=nil, fill=0) - - #Output text in flowing mode - w = @w - @r_margin - @x; - wmax = (w - 2 * @c_margin); - - s = txt.gsub("\r", ''); - nb = s.length; - - # handle single space character - if ((nb==1) and (s == " ")) - @x += GetStringWidth(s); - return; - end - - sep=-1; - i=0; - j=0; - l=0; - nl=1; - while(i wmax) - #Automatic line break (word wrapping) - if (sep == -1) - if (@x > @l_margin) - #Move to next line - @x = @l_margin; - @y += h; - w=@w - @r_margin - @x; - wmax=(w - 2 * @c_margin); - i += 1 - nl += 1 - next - end - if (i == j) - i += 1 - end - Cell(w, h, s[j, (i-1)], 0, 2, '', fill, link); - else - Cell(w, h, s[j, (sep-j)], 0, 2, '', fill, link); - i = sep+1; - end - sep = -1; - j = i; - l = 0; - if (nl==1) - @x = @l_margin; - w = @w - @r_margin - @x; - wmax = (w - 2 * @c_margin); - end - nl += 1; - else - i += 1; - end - end - #Last chunk - if (i != j) - Cell(GetStringWidth(s[j..i]), h, s[j..i], 0, 0, '', fill, link); - end - end - alias_method :write, :Write - - # - # Puts an image in the page. The upper-left corner must be given. The dimensions can be specified in different ways:
    • explicit width and height (expressed in user unit)
    • one explicit dimension, the other being calculated automatically in order to keep the original proportions
    • no explicit dimension, in which case the image is put at 72 dpi
    - # Supported formats are JPEG and PNG. - # For JPEG, all flavors are allowed:
    • gray scales
    • true colors (24 bits)
    • CMYK (32 bits)
    - # For PNG, are allowed:
    • gray scales on at most 8 bits (256 levels)
    • indexed colors
    • true colors (24 bits)
    - # but are not supported:
    • Interlacing
    • Alpha channel
    - # If a transparent color is defined, it will be taken into account (but will be only interpreted by Acrobat 4 and above).
    - # The format can be specified explicitly or inferred from the file extension.
    - # It is possible to put a link on the image.
    - # Remark: if an image is used several times, only one copy will be embedded in the file.
    - # @param string :file Name of the file containing the image. - # @param float :x Abscissa of the upper-left corner. - # @param float :y Ordinate of the upper-left corner. - # @param float :w Width of the image in the page. If not specified or equal to zero, it is automatically calculated. - # @param float :h Height of the image in the page. If not specified or equal to zero, it is automatically calculated. - # @param string :type Image format. Possible values are (case insensitive): JPG, JPEG, PNG. If not specified, the type is inferred from the file extension. - # @param mixed :link URL or identifier returned by AddLink(). - # @since 1.1 - # @see AddLink() - # - def Image(file, x, y, w=0, h=0, type='', link=nil) - #Put an image on the page - if (@images[file].nil?) - #First use of image, get info - if (type == '') - pos = File::basename(file).rindex('.'); - if (pos.nil? or pos == 0) - Error('Image file has no extension and no type was specified: ' + file); - end - pos = file.rindex('.'); - type = file[pos+1..-1]; - end - type.downcase! - if (type == 'jpg' or type == 'jpeg') - info=parsejpg(file); - elsif (type == 'png' or type == 'gif') - img = Magick::ImageList.new(file) - img.format = "PNG" # convert to PNG from gif - img.opacity = 0 # PNG alpha channel delete - File.open( @@k_path_cache + File::basename(file), 'w'){|f| - f.binmode - f.print img.to_blob - f.close - } - info=parsepng( @@k_path_cache + File::basename(file)); - File.delete( @@k_path_cache + File::basename(file)) - else - #Allow for additional formats - mtd='parse' + type; - if (!self.respond_to?(mtd)) - Error('Unsupported image type: ' + type); - end - info=send(mtd, file); - end - info['i']=@images.length+1; - @images[file] = info; - else - info=@images[file]; - end - #Automatic width and height calculation if needed - if ((w == 0) and (h == 0)) - rescale_x = (@w - @r_margin - x) / (info['w'] / (@img_scale * @k)) - rescale_x = 1 if rescale_x >= 1 - if (y + info['h'] * rescale_x / (@img_scale * @k) > @page_break_trigger and !@in_footer and AcceptPageBreak()) - #Automatic page break - if @pages[@page+1].nil? - ws = @ws; - if (ws > 0) - @ws = 0; - out('0 Tw'); - end - AddPage(@cur_orientation); - if (ws > 0) - @ws = ws; - out(sprintf('%.3f Tw', ws * @k)); - end - else - @page += 1; - end - y=@t_margin; - end - rescale_y = (@page_break_trigger - y) / (info['h'] / (@img_scale * @k)) - rescale_y = 1 if rescale_y >= 1 - rescale = rescale_y >= rescale_x ? rescale_x : rescale_y - - #Put image at 72 dpi - # 2004-06-14 :: Nicola Asuni, scale factor where added - w = info['w'] * rescale / (@img_scale * @k); - h = info['h'] * rescale / (@img_scale * @k); - elsif (w == 0) - w = h * info['w'] / info['h']; - elsif (h == 0) - h = w * info['h'] / info['w']; - end - out(sprintf('q %.2f 0 0 %.2f %.2f %.2f cm /I%d Do Q', w*@k, h*@k, x*@k, (@h-(y+h))*@k, info['i'])); - if (link) - Link(x, y, w, h, link); - end - - #2002-07-31 - Nicola Asuni - # set right-bottom corner coordinates - @img_rb_x = x + w; - @img_rb_y = y + h; - end - alias_method :image, :Image - - # - # Performs a line break. The current abscissa goes back to the left margin and the ordinate increases by the amount passed in parameter. - # @param float :h The height of the break. By default, the value equals the height of the last printed cell. - # @since 1.0 - # @see Cell() - # - def Ln(h='') - #Line feed; default value is last cell height - @x=@l_margin; - if (h.is_a?(String)) - @y += @lasth; - else - @y += h; - end - - k=@k; - if (@y > @page_break_trigger and !@in_footer and AcceptPageBreak()) - #Automatic page break - if @pages[@page+1].nil? - x = @x; - ws = @ws; - if (ws > 0) - @ws = 0; - out('0 Tw'); - end - AddPage(@cur_orientation); - @x = x; - if (ws > 0) - @ws = ws; - out(sprintf('%.3f Tw', ws * k)); - end - else - @page += 1; - @y=@t_margin; - end - end - - end - alias_method :ln, :Ln - - # - # Returns the abscissa of the current position. - # @return float - # @since 1.2 - # @see SetX(), GetY(), SetY() - # - def GetX() - #Get x position - return @x; - end - alias_method :get_x, :GetX - - # - # Defines the abscissa of the current position. If the passed value is negative, it is relative to the right of the page. - # @param float :x The value of the abscissa. - # @since 1.2 - # @see GetX(), GetY(), SetY(), SetXY() - # - def SetX(x) - #Set x position - if (x>=0) - @x = x; - else - @x=@w+x; - end - end - alias_method :set_x, :SetX - - # - # Returns the ordinate of the current position. - # @return float - # @since 1.0 - # @see SetY(), GetX(), SetX() - # - def GetY() - #Get y position - return @y; - end - alias_method :get_y, :GetY - - # - # Moves the current abscissa back to the left margin and sets the ordinate. If the passed value is negative, it is relative to the bottom of the page. - # @param float :y The value of the ordinate. - # @since 1.0 - # @see GetX(), GetY(), SetY(), SetXY() - # - def SetY(y) - #Set y position and reset x - @x=@l_margin; - if (y>=0) - @y = y; - else - @y=@h+y; - end - end - alias_method :set_y, :SetY - - # - # Defines the abscissa and ordinate of the current position. If the passed values are negative, they are relative respectively to the right and bottom of the page. - # @param float :x The value of the abscissa - # @param float :y The value of the ordinate - # @since 1.2 - # @see SetX(), SetY() - # - def SetXY(x, y) - #Set x and y positions - SetY(y); - SetX(x); - end - alias_method :set_xy, :SetXY - - # - # Send the document to a given destination: string, local file or browser. In the last case, the plug-in may be used (if present) or a download ("Save as" dialog box) may be forced.
    - # The method first calls Close() if necessary to terminate the document. - # @param string :name The name of the file. If not given, the document will be sent to the browser (destination I) with the name doc.pdf. - # @param string :dest Destination where to send the document. It can take one of the following values:
    • I: send the file inline to the browser. The plug-in is used if available. The name given by name is used when one selects the "Save as" option on the link generating the PDF.
    • D: send to the browser and force a file download with the name given by name.
    • F: save to a local file with the name given by name.
    • S: return the document as a string. name is ignored.
    If the parameter is not specified but a name is given, destination is F. If no parameter is specified at all, destination is I.
    - # @since 1.0 - # @see Close() - # - def Output(name='', dest='') - #Output PDF to some destination - #Finish document if necessary - if (@state < 3) - Close(); - end - #Normalize parameters - # Boolean no longer supported - # if (dest.is_a?(Boolean)) - # dest = dest ? 'D' : 'F'; - # end - dest = dest.upcase - if (dest=='') - if (name=='') - name='doc.pdf'; - dest='I'; - else - dest='F'; - end - end - case (dest) - when 'I' - # This is PHP specific code - ##Send to standard output - # if (ob_get_contents()) - # Error('Some data has already been output, can\'t send PDF file'); - # end - # if (php_sapi_name()!='cli') - # #We send to a browser - # header('Content-Type: application/pdf'); - # if (headers_sent()) - # Error('Some data has already been output to browser, can\'t send PDF file'); - # end - # header('Content-Length: ' + @buffer.length); - # header('Content-disposition: inline; filename="' + name + '"'); - # end - return @buffer; - - when 'D' - # PHP specific - #Download file - # if (ob_get_contents()) - # Error('Some data has already been output, can\'t send PDF file'); - # end - # if (!_SERVER['HTTP_USER_AGENT'].nil? && SERVER['HTTP_USER_AGENT'].include?('MSIE')) - # header('Content-Type: application/force-download'); - # else - # header('Content-Type: application/octet-stream'); - # end - # if (headers_sent()) - # Error('Some data has already been output to browser, can\'t send PDF file'); - # end - # header('Content-Length: '+ @buffer.length); - # header('Content-disposition: attachment; filename="' + name + '"'); - return @buffer; - - when 'F' - open(name,'wb') do |f| - f.write(@buffer) - end - # PHP code - # #Save to local file - # f=open(name,'wb'); - # if (!f) - # Error('Unable to create output file: ' + name); - # end - # fwrite(f,@buffer,@buffer.length); - # f.close - - when 'S' - #Return as a string - return @buffer; - else - Error('Incorrect output destination: ' + dest); - - end - return ''; - end - alias_method :output, :Output - - # Protected methods - - # - # Check for locale-related bug - # @access protected - # - def dochecks() - #Check for locale-related bug - if (1.1==1) - Error('Don\'t alter the locale before including class file'); - end - #Check for decimal separator - if (sprintf('%.1f',1.0)!='1.0') - setlocale(LC_NUMERIC,'C'); - end - end - - # - # Return fonts path - # @access protected - # - def getfontpath(file) - # Is it in the @@font_path? - if @@font_path - fpath = File.join @@font_path, file - if File.exists?(fpath) - return fpath - end - end - # Is it in this plugin's font folder? - fpath = File.join File.dirname(__FILE__), 'fonts', file - if File.exists?(fpath) - return fpath - end - # Could not find it. - nil - end - - # - # Start document - # @access protected - # - def begindoc() - #Start document - @state=1; - out('%PDF-1.3'); - end - - # - # putpages - # @access protected - # - def putpages() - nb = @page; - if (@alias_nb_pages) - nbstr = UTF8ToUTF16BE(nb.to_s, false); - #Replace number of pages - 1.upto(nb) do |n| - @pages[n].gsub!(@alias_nb_pages, nbstr) - end - end - if @def_orientation=='P' - w_pt=@fw_pt - h_pt=@fh_pt - else - w_pt=@fh_pt - h_pt=@fw_pt - end - filter=(@compress) ? '/Filter /FlateDecode ' : '' - 1.upto(nb) do |n| - #Page - newobj - out('<>>>'; - else - l=@links[pl[4]]; - h=!@orientation_changes[l[0]].nil? ? w_pt : h_pt; - annots<>',1+2*l[0], h-l[1]*@k); - end - end - out(annots + ']'); - end - out('/Contents ' + (@n+1).to_s + ' 0 R>>'); - out('endobj'); - #Page content - p=(@compress) ? gzcompress(@pages[n]) : @pages[n]; - newobj(); - out('<<' + filter + '/Length '+ p.length.to_s + '>>'); - putstream(p); - out('endobj'); - end - #Pages root - @offsets[1]=@buffer.length; - out('1 0 obj'); - out('<>'); - out('endobj'); - end - - # - # Adds fonts - # putfonts - # @access protected - # - def putfonts() - nf=@n; - @diffs.each do |diff| - #Encodings - newobj(); - out('<>'); - out('endobj'); - end - @font_files.each do |file, info| - #Font file embedding - newobj(); - @font_files[file]['n']=@n; - font=''; - open(getfontpath(file),'rb') do |f| - font = f.read(); - end - compressed=(file[-2,2]=='.z'); - if (!compressed && !info['length2'].nil?) - header=((font[0][0])==128); - if (header) - #Strip first binary header - font=font[6]; - end - if header && (font[info['length1']][0] == 128) - #Strip second binary header - font=font[0..info['length1']] + font[info['length1']+6]; - end - end - out('<>'); - open(getfontpath(file),'rb') do |f| - putstream(font) - end - out('endobj'); - end - @fonts.each do |k, font| - #Font objects - @fonts[k]['n']=@n+1; - type = font['type']; - name = font['name']; - if (type=='core') - #Standard font - newobj(); - out('<>'); - out('endobj'); - elsif type == 'Type0' - putType0(font) - elsif (type=='Type1' || type=='TrueType') - #Additional Type1 or TrueType font - newobj(); - out('<>'); - out('endobj'); - #Widths - newobj(); - cw=font['cw']; # & - s='['; - 32.upto(255) do |i| - s << cw[i.chr] + ' '; - end - out(s + ']'); - out('endobj'); - #Descriptor - newobj(); - s='<>'); - out('endobj'); - else - #Allow for additional types - mtd='put' + type.downcase; - if (!self.respond_to?(mtd)) - Error('Unsupported font type: ' + type) - else - self.send(mtd,font) - end - end - end - end - - def putType0(font) - #Type0 - newobj(); - out('<>') - out('endobj') - #CIDFont - newobj() - out('<>') - out('/FontDescriptor '+(@n+1).to_s+' 0 R') - w='/W [1 [' - font['cw'].keys.sort.each {|key| - w+=font['cw'][key].to_s + " " -# ActionController::Base::logger.debug key.to_s -# ActionController::Base::logger.debug font['cw'][key].to_s - } - out(w+'] 231 325 500 631 [500] 326 389 500]') - out('>>') - out('endobj') - #Font descriptor - newobj() - out('<>') - out('endobj') - end - - # - # putimages - # @access protected - # - def putimages() - filter=(@compress) ? '/Filter /FlateDecode ' : ''; - @images.each do |file, info| # was while(list(file, info)=each(@images)) - newobj(); - @images[file]['n']=@n; - out('<>'); - putstream(info['data']); - @images[file]['data']=nil - out('endobj'); - #Palette - if (info['cs']=='Indexed') - newobj(); - pal=(@compress) ? gzcompress(info['pal']) : info['pal']; - out('<<' + filter + '/Length ' + pal.length.to_s + '>>'); - putstream(pal); - out('endobj'); - end - end - end - - # - # putxobjectdict - # @access protected - # - def putxobjectdict() - @images.each_value do |image| - out('/I' + image['i'].to_s + ' ' + image['n'].to_s + ' 0 R'); - end - end - - # - # putresourcedict - # @access protected - # - def putresourcedict() - out('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); - out('/Font <<'); - @fonts.each_value do |font| - out('/F' + font['i'].to_s + ' ' + font['n'].to_s + ' 0 R'); - end - out('>>'); - out('/XObject <<'); - putxobjectdict(); - out('>>'); - end - - # - # putresources - # @access protected - # - def putresources() - putfonts(); - putimages(); - #Resource dictionary - @offsets[2]=@buffer.length; - out('2 0 obj'); - out('<<'); - putresourcedict(); - out('>>'); - out('endobj'); - end - - # - # putinfo - # @access protected - # - def putinfo() - out('/Producer ' + textstring(PDF_PRODUCER)); - if (!@title.nil?) - out('/Title ' + textstring(@title)); - end - if (!@subject.nil?) - out('/Subject ' + textstring(@subject)); - end - if (!@author.nil?) - out('/Author ' + textstring(@author)); - end - if (!@keywords.nil?) - out('/Keywords ' + textstring(@keywords)); - end - if (!@creator.nil?) - out('/Creator ' + textstring(@creator)); - end - out('/CreationDate ' + textstring('D:' + Time.now.strftime('%Y%m%d%H%M%S'))); - end - - # - # putcatalog - # @access protected - # - def putcatalog() - out('/Type /Catalog'); - out('/Pages 1 0 R'); - if (@zoom_mode=='fullpage') - out('/OpenAction [3 0 R /Fit]'); - elsif (@zoom_mode=='fullwidth') - out('/OpenAction [3 0 R /FitH null]'); - elsif (@zoom_mode=='real') - out('/OpenAction [3 0 R /XYZ null null 1]'); - elsif (!@zoom_mode.is_a?(String)) - out('/OpenAction [3 0 R /XYZ null null ' + (@zoom_mode/100) + ']'); - end - if (@layout_mode=='single') - out('/PageLayout /SinglePage'); - elsif (@layout_mode=='continuous') - out('/PageLayout /OneColumn'); - elsif (@layout_mode=='two') - out('/PageLayout /TwoColumnLeft'); - end - end - - # - # puttrailer - # @access protected - # - def puttrailer() - out('/Size ' + (@n+1).to_s); - out('/Root ' + @n.to_s + ' 0 R'); - out('/Info ' + (@n-1).to_s + ' 0 R'); - end - - # - # putheader - # @access protected - # - def putheader() - out('%PDF-' + @pdf_version); - end - - # - # enddoc - # @access protected - # - def enddoc() - putheader(); - putpages(); - putresources(); - #Info - newobj(); - out('<<'); - putinfo(); - out('>>'); - out('endobj'); - #Catalog - newobj(); - out('<<'); - putcatalog(); - out('>>'); - out('endobj'); - #Cross-ref - o=@buffer.length; - out('xref'); - out('0 ' + (@n+1).to_s); - out('0000000000 65535 f '); - 1.upto(@n) do |i| - out(sprintf('%010d 00000 n ',@offsets[i])); - end - #Trailer - out('trailer'); - out('<<'); - puttrailer(); - out('>>'); - out('startxref'); - out(o); - out('%%EOF'); - @state=3; - end - - # - # beginpage - # @access protected - # - def beginpage(orientation) - @page += 1; - @pages[@page]=''; - @state=2; - @x=@l_margin; - @y=@t_margin; - @font_family=''; - #Page orientation - if (orientation.empty?) - orientation=@def_orientation; - else - orientation.upcase! - if (orientation!=@def_orientation) - @orientation_changes[@page]=true; - end - end - if (orientation!=@cur_orientation) - #Change orientation - if (orientation=='P') - @w_pt=@fw_pt; - @h_pt=@fh_pt; - @w=@fw; - @h=@fh; - else - @w_pt=@fh_pt; - @h_pt=@fw_pt; - @w=@fh; - @h=@fw; - end - @page_break_trigger=@h-@b_margin; - @cur_orientation = orientation; - end - end - - # - # End of page contents - # @access protected - # - def endpage() - @state=1; - end - - # - # Begin a new object - # @access protected - # - def newobj() - @n += 1; - @offsets[@n]=@buffer.length; - out(@n.to_s + ' 0 obj'); - end - - # - # Underline and Deleted text - # @access protected - # - def dolinetxt(x, y, txt) - up = @current_font['up']; - ut = @current_font['ut']; - w = GetStringWidth(txt) + @ws * txt.count(' '); - sprintf('%.2f %.2f %.2f %.2f re f', x * @k, (@h - (y - up / 1000.0 * @font_size)) * @k, w * @k, -ut / 1000.0 * @font_size_pt); - end - - # - # Extract info from a JPEG file - # @access protected - # - def parsejpg(file) - a=getimagesize(file); - if (a.empty?) - Error('Missing or incorrect image file: ' + file); - end - if (!a[2].nil? and a[2]!='JPEG') - Error('Not a JPEG file: ' + file); - end - if (a['channels'].nil? or a['channels']==3) - colspace='DeviceRGB'; - elsif (a['channels']==4) - colspace='DeviceCMYK'; - else - colspace='DeviceGray'; - end - bpc=!a['bits'].nil? ? a['bits'] : 8; - #Read whole file - data=''; - - open( @@k_path_cache + File::basename(file),'rb') do |f| - data< a[0],'h' => a[1],'cs' => colspace,'bpc' => bpc,'f'=>'DCTDecode','data' => data} - end - - # - # Extract info from a PNG file - # @access protected - # - def parsepng(file) - f=open(file,'rb'); - #Check signature - if (f.read(8)!=137.chr + 'PNG' + 13.chr + 10.chr + 26.chr + 10.chr) - Error('Not a PNG file: ' + file); - end - #Read header chunk - f.read(4); - if (f.read(4)!='IHDR') - Error('Incorrect PNG file: ' + file); - end - w=freadint(f); - h=freadint(f); - bpc=f.read(1).unpack('C')[0]; - if (bpc>8) - Error('16-bit depth not supported: ' + file); - end - ct=f.read(1).unpack('C')[0]; - if (ct==0) - colspace='DeviceGray'; - elsif (ct==2) - colspace='DeviceRGB'; - elsif (ct==3) - colspace='Indexed'; - else - Error('Alpha channel not supported: ' + file); - end - if (f.read(1).unpack('C')[0] != 0) - Error('Unknown compression method: ' + file); - end - if (f.read(1).unpack('C')[0] != 0) - Error('Unknown filter method: ' + file); - end - if (f.read(1).unpack('C')[0] != 0) - Error('Interlacing not supported: ' + file); - end - f.read(4); - parms='/DecodeParms <>'; - #Scan chunks looking for palette, transparency and image data - pal=''; - trns=''; - data=''; - begin - n=freadint(f); - type=f.read(4); - if (type=='PLTE') - #Read palette - pal=f.read( n); - f.read(4); - elsif (type=='tRNS') - #Read transparency info - t=f.read( n); - if (ct==0) - trns = t[1].unpack('C')[0] - elsif (ct==2) - trns = t[[1].unpack('C')[0], t[3].unpack('C')[0], t[5].unpack('C')[0]] - else - pos=t.include?(0.chr); - if (pos!=false) - trns = [pos] - end - end - f.read(4); - elsif (type=='IDAT') - #Read image data block - data< w, 'h' => h, 'cs' => colspace, 'bpc' => bpc, 'f'=>'FlateDecode', 'parms' => parms, 'pal' => pal, 'trns' => trns, 'data' => data} - end - - # - # Read a 4-byte integer from file - # @access protected - # - def freadint(f) - # Read a 4-byte integer from file - a = f.read(4).unpack('N') - return a[0] - end - - # - # Format a text string - # @access protected - # - def textstring(s) - if (@is_unicode) - #Convert string to UTF-16BE - s = UTF8ToUTF16BE(s, true); - end - return '(' + escape(s) + ')'; - end - - # - # Format a text string - # @access protected - # - def escapetext(s) - if (@is_unicode) - #Convert string to UTF-16BE - s = UTF8ToUTF16BE(s, false); - end - return escape(s); - end - - # - # Add \ before \, ( and ) - # @access protected - # - def escape(s) - # Add \ before \, ( and ) - s.gsub('\\','\\\\\\').gsub('(','\\(').gsub(')','\\)').gsub(13.chr, '\r') - end - - # - # - # @access protected - # - def putstream(s) - out('stream'); - out(s); - out('endstream'); - end - - # - # Add a line to the document - # @access protected - # - def out(s) - if (@state==2) - @pages[@page] << s.to_s + "\n"; - else - @buffer << s.to_s + "\n"; - end - end - - # - # Adds unicode fonts.
    - # Based on PDF Reference 1.3 (section 5) - # @access protected - # @author Nicola Asuni - # @since 1.52.0.TC005 (2005-01-05) - # - def puttruetypeunicode(font) - # Type0 Font - # A composite font composed of other fonts, organized hierarchically - newobj(); - out('<>'); - out('endobj'); - - # CIDFontType2 - # A CIDFont whose glyph descriptions are based on TrueType font technology - newobj(); - out('<>'); - out('endobj'); - - # ToUnicode - # is a stream object that contains the definition of the CMap - # (PDF Reference 1.3 chap. 5.9) - newobj(); - out('<>'); - out('stream'); - out('/CIDInit /ProcSet findresource begin'); - out('12 dict begin'); - out('begincmap'); - out('/CIDSystemInfo'); - out('<> def'); - out('/CMapName /Adobe-Identity-UCS def'); - out('/CMapType 2 def'); - out('1 begincodespacerange'); - out('<0000> '); - out('endcodespacerange'); - out('1 beginbfrange'); - out('<0000> <0000>'); - out('endbfrange'); - out('endcmap'); - out('CMapName currentdict /CMap defineresource pop'); - out('end'); - out('end'); - out('endstream'); - out('endobj'); - - # CIDSystemInfo dictionary - # A dictionary containing entries that define the character collection of the CIDFont. - newobj(); - out('<>'); - out('endobj'); - - # Font descriptor - # A font descriptor describing the CIDFont default metrics other than its glyph widths - newobj(); - out('<>'); - out('endobj'); - - # Embed CIDToGIDMap - # A specification of the mapping from CIDs to glyph indices - newobj(); - ctgfile = getfontpath(font['ctg']) - if (!ctgfile) - Error('Font file not found: ' + ctgfile); - end - size = File.size(ctgfile); - out('<>'); - open(ctgfile, "rb") do |f| - putstream(f.read()) - end - out('endobj'); - end - - # - # Converts UTF-8 strings to codepoints array.
    - # Invalid byte sequences will be replaced with 0xFFFD (replacement character)
    - # Based on: http://www.faqs.org/rfcs/rfc3629.html - #
    -	# 	  Char. number range  |        UTF-8 octet sequence
    -	#       (hexadecimal)    |              (binary)
    -	#    --------------------+-----------------------------------------------
    -	#    0000 0000-0000 007F | 0xxxxxxx
    -	#    0000 0080-0000 07FF | 110xxxxx 10xxxxxx
    -	#    0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
    -	#    0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
    -	#    ---------------------------------------------------------------------
    -	#
    -	#   ABFN notation:
    -	#   ---------------------------------------------------------------------
    -	#   UTF8-octets =#( UTF8-char )
    -	#   UTF8-char   = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
    -	#   UTF8-1      = %x00-7F
    -	#   UTF8-2      = %xC2-DF UTF8-tail
    -	#
    -	#   UTF8-3      = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /
    -	#                 %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
    -	#   UTF8-4      = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /
    -	#                 %xF4 %x80-8F 2( UTF8-tail )
    -	#   UTF8-tail   = %x80-BF
    -	#   ---------------------------------------------------------------------
    -	# 
    - # @param string :str string to process. - # @return array containing codepoints (UTF-8 characters values) - # @access protected - # @author Nicola Asuni - # @since 1.53.0.TC005 (2005-01-05) - # - def UTF8StringToArray(str) - if (!@is_unicode) - return str; # string is not in unicode - end - - unicode = [] # array containing unicode values - bytes = [] # array containing single character byte sequences - numbytes = 1; # number of octetc needed to represent the UTF-8 character - - str = str.to_s; # force :str to be a string - - str.each_byte do |char| - if (bytes.length == 0) # get starting octect - if (char <= 0x7F) - unicode << char # use the character "as is" because is ASCII - numbytes = 1 - elsif ((char >> 0x05) == 0x06) # 2 bytes character (0x06 = 110 BIN) - bytes << ((char - 0xC0) << 0x06) - numbytes = 2 - elsif ((char >> 0x04) == 0x0E) # 3 bytes character (0x0E = 1110 BIN) - bytes << ((char - 0xE0) << 0x0C) - numbytes = 3 - elsif ((char >> 0x03) == 0x1E) # 4 bytes character (0x1E = 11110 BIN) - bytes << ((char - 0xF0) << 0x12) - numbytes = 4 - else - # use replacement character for other invalid sequences - unicode << 0xFFFD - bytes = [] - numbytes = 1 - end - elsif ((char >> 0x06) == 0x02) # bytes 2, 3 and 4 must start with 0x02 = 10 BIN - bytes << (char - 0x80) - if (bytes.length == numbytes) - # compose UTF-8 bytes to a single unicode value - char = bytes[0] - 1.upto(numbytes-1) do |j| - char += (bytes[j] << ((numbytes - j - 1) * 0x06)) - end - if (((char >= 0xD800) and (char <= 0xDFFF)) or (char >= 0x10FFFF)) - # The definition of UTF-8 prohibits encoding character numbers between - # U+D800 and U+DFFF, which are reserved for use with the UTF-16 - # encoding form (as surrogate pairs) and do not directly represent - # characters - unicode << 0xFFFD; # use replacement character - else - unicode << char; # add char to array - end - # reset data for next char - bytes = [] - numbytes = 1; - end - else - # use replacement character for other invalid sequences - unicode << 0xFFFD; - bytes = [] - numbytes = 1; - end - end - return unicode; - end - - # - # Converts UTF-8 strings to UTF16-BE.
    - # Based on: http://www.faqs.org/rfcs/rfc2781.html - #
    -	#   Encoding UTF-16:
    -	# 
    -		#   Encoding of a single character from an ISO 10646 character value to
    -	#    UTF-16 proceeds as follows. Let U be the character number, no greater
    -	#    than 0x10FFFF.
    -	# 
    -	#    1) If U < 0x10000, encode U as a 16-bit unsigned integer and
    -	#       terminate.
    -	# 
    -	#    2) Let U' = U - 0x10000. Because U is less than or equal to 0x10FFFF,
    -	#       U' must be less than or equal to 0xFFFFF. That is, U' can be
    -	#       represented in 20 bits.
    -	# 
    -	#    3) Initialize two 16-bit unsigned integers, W1 and W2, to 0xD800 and
    -	#       0xDC00, respectively. These integers each have 10 bits free to
    -	#       encode the character value, for a total of 20 bits.
    -	# 
    -	#    4) Assign the 10 high-order bits of the 20-bit U' to the 10 low-order
    -	#       bits of W1 and the 10 low-order bits of U' to the 10 low-order
    -	#       bits of W2. Terminate.
    -	# 
    -	#    Graphically, steps 2 through 4 look like:
    -	#    U' = yyyyyyyyyyxxxxxxxxxx
    -	#    W1 = 110110yyyyyyyyyy
    -	#    W2 = 110111xxxxxxxxxx
    -	# 
    - # @param string :str string to process. - # @param boolean :setbom if true set the Byte Order Mark (BOM = 0xFEFF) - # @return string - # @access protected - # @author Nicola Asuni - # @since 1.53.0.TC005 (2005-01-05) - # @uses UTF8StringToArray - # - def UTF8ToUTF16BE(str, setbom=true) - if (!@is_unicode) - return str; # string is not in unicode - end - outstr = ""; # string to be returned - unicode = UTF8StringToArray(str); # array containing UTF-8 unicode values - numitems = unicode.length; - - if (setbom) - outstr << "\xFE\xFF"; # Byte Order Mark (BOM) - end - unicode.each do |char| - if (char == 0xFFFD) - outstr << "\xFF\xFD"; # replacement character - elsif (char < 0x10000) - outstr << (char >> 0x08).chr; - outstr << (char & 0xFF).chr; - else - char -= 0x10000; - w1 = 0xD800 | (char >> 0x10); - w2 = 0xDC00 | (char & 0x3FF); - outstr << (w1 >> 0x08).chr; - outstr << (w1 & 0xFF).chr; - outstr << (w2 >> 0x08).chr; - outstr << (w2 & 0xFF).chr; - end - end - return outstr; - end - - # ==================================================== - - # - # Set header font. - # @param array :font font - # @since 1.1 - # - def SetHeaderFont(font) - @header_font = font; - end - alias_method :set_header_font, :SetHeaderFont - - # - # Set footer font. - # @param array :font font - # @since 1.1 - # - def SetFooterFont(font) - @footer_font = font; - end - alias_method :set_footer_font, :SetFooterFont - - # - # Set language array. - # @param array :language - # @since 1.1 - # - def SetLanguageArray(language) - @l = language; - end - alias_method :set_language_array, :SetLanguageArray - # - # Set document barcode. - # @param string :bc barcode - # - def SetBarcode(bc="") - @barcode = bc; - end - - # - # Print Barcode. - # @param int :x x position in user units - # @param int :y y position in user units - # @param int :w width in user units - # @param int :h height position in user units - # @param string :type type of barcode (I25, C128A, C128B, C128C, C39) - # @param string :style barcode style - # @param string :font font for text - # @param int :xres x resolution - # @param string :code code to print - # - def writeBarcode(x, y, w, h, type, style, font, xres, code) - require(File.dirname(__FILE__) + "/barcode/barcode.rb"); - require(File.dirname(__FILE__) + "/barcode/i25object.rb"); - require(File.dirname(__FILE__) + "/barcode/c39object.rb"); - require(File.dirname(__FILE__) + "/barcode/c128aobject.rb"); - require(File.dirname(__FILE__) + "/barcode/c128bobject.rb"); - require(File.dirname(__FILE__) + "/barcode/c128cobject.rb"); - - if (code.empty?) - return; - end - - if (style.empty?) - style = BCS_ALIGN_LEFT; - style |= BCS_IMAGE_PNG; - style |= BCS_TRANSPARENT; - #:style |= BCS_BORDER; - #:style |= BCS_DRAW_TEXT; - #:style |= BCS_STRETCH_TEXT; - #:style |= BCS_REVERSE_COLOR; - end - if (font.empty?) then font = BCD_DEFAULT_FONT; end - if (xres.empty?) then xres = BCD_DEFAULT_XRES; end - - scale_factor = 1.5 * xres * @k; - bc_w = (w * scale_factor).round #width in points - bc_h = (h * scale_factor).round #height in points - - case (type.upcase) - when "I25" - obj = I25Object.new(bc_w, bc_h, style, code); - when "C128A" - obj = C128AObject.new(bc_w, bc_h, style, code); - when "C128B" - obj = C128BObject.new(bc_w, bc_h, style, code); - when "C128C" - obj = C128CObject.new(bc_w, bc_h, style, code); - when "C39" - obj = C39Object.new(bc_w, bc_h, style, code); - end - - obj.SetFont(font); - obj.DrawObject(xres); - - #use a temporary file.... - tmpName = tempnam(@@k_path_cache,'img'); - imagepng(obj.getImage(), tmpName); - Image(tmpName, x, y, w, h, 'png'); - obj.DestroyObject(); - obj = nil - unlink(tmpName); - end - - # - # Returns the PDF data. - # - def GetPDFData() - if (@state < 3) - Close(); - end - return @buffer; - end - - # --- HTML PARSER FUNCTIONS --- - - # - # Allows to preserve some HTML formatting.
    - # Supports: h1, h2, h3, h4, h5, h6, b, u, i, a, img, p, br, strong, em, ins, del, font, blockquote, li, ul, ol, hr, td, th, tr, table, sup, sub, small - # @param string :html text to display - # @param boolean :ln if true add a new line after text (default = true) - # @param int :fill Indicates if the background must be painted (1) or transparent (0). Default value: 0. - # - def writeHTML(html, ln=true, fill=0, h=0) - - @lasth = h if h > 0 - if (@lasth == 0) - #set row height - @lasth = @font_size * @@k_cell_height_ratio; - end - - @href = nil - @style = ""; - @t_cells = [[]]; - @table_id = 0; - - # pre calculate - html.split(/(<[^>]+>)/).each do |element| - if "<" == element[0,1] - #Tag - if (element[1, 1] == '/') - closedHTMLTagCalc(element[2..-2].downcase); - else - #Extract attributes - # get tag name - tag = element.scan(/([a-zA-Z0-9]*)/).flatten.delete_if {|x| x.length == 0} - tag = tag[0].downcase; - - # get attributes - attr_array = element.scan(/([^=\s]*)=["\']?([^"\']*)["\']?/) - attrs = {} - attr_array.each do |name, value| - attrs[name.downcase] = value; - end - openHTMLTagCalc(tag, attrs); - end - end - end - @table_id = 0; - - html.split(/(<[A-Za-z!?\/][^>]*?>)/).each do |element| - if "<" == element[0,1] - #Tag - if (element[1, 1] == '/') - closedHTMLTagHandler(element[2..-2].downcase); - else - #Extract attributes - # get tag name - tag = element.scan(/([a-zA-Z0-9]*)/).flatten.delete_if {|x| x.length == 0} - tag = tag[0].downcase; - - # get attributes - attr_array = element.scan(/([^=\s]*)=["\']?([^"\']*)["\']?/) - attrs = {} - attr_array.each do |name, value| - attrs[name.downcase] = value; - end - openHTMLTagHandler(tag, attrs, fill); - end - - else - #Text - if (@href) - element.gsub!(/[\t\r\n\f]/, ""); - addHtmlLink(@href, element, fill); - elsif (@tdbegin) - element.gsub!(/[\t\r\n\f]/, ""); - element.gsub!(/ /, " "); - base_page = @page; - base_x = @x; - base_y = @y; - - MultiCell(@tdwidth, @tdheight, unhtmlentities(element.strip), @tableborder, @tdalign, @tdfill, 1); - tr_end = @t_cells[@table_id][@tr_id][@td_id]['j1'] + 1; - if @max_td_page[tr_end].nil? or (@max_td_page[tr_end] < @page) - @max_td_page[tr_end] = @page - @max_td_y[tr_end] = @y - elsif (@max_td_page[tr_end] == @page) - @max_td_y[tr_end] = @y if @max_td_y[tr_end].nil? or (@max_td_y[tr_end] < @y) - end - - @page = base_page; - @x = base_x + @tdwidth; - @y = base_y; - elsif (@pre_state == true and element.length > 0) - Write(@lasth, unhtmlentities(element), '', fill); - elsif (element.strip.length > 0) - element.gsub!(/[\t\r\n\f]/, ""); - element.gsub!(/ /, " "); - Write(@lasth, unhtmlentities(element), '', fill); - end - end - end - - if (ln) - Ln(@lasth); - end - end - alias_method :write_html, :writeHTML - - # - # Prints a cell (rectangular area) with optional borders, background color and html text string. The upper-left corner of the cell corresponds to the current position. After the call, the current position moves to the right or to the next line.
    - # If automatic page breaking is enabled and the cell goes beyond the limit, a page break is done before outputting. - # @param float :w Cell width. If 0, the cell extends up to the right margin. - # @param float :h Cell minimum height. The cell extends automatically if needed. - # @param float :x upper-left corner X coordinate - # @param float :y upper-left corner Y coordinate - # @param string :html html text to print. Default value: empty string. - # @param mixed :border Indicates if borders must be drawn around the cell. The value can be either a number:
    • 0: no border (default)
    • 1: frame
    or a string containing some or all of the following characters (in any order):
    • L: left
    • T: top
    • R: right
    • B: bottom
    - # @param int :ln Indicates where the current position should go after the call. Possible values are:
    • 0: to the right
    • 1: to the beginning of the next line
    • 2: below
    -# Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: 0. - # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. - # @see Cell() - # - def writeHTMLCell(w, h, x, y, html='', border=0, ln=1, fill=0) - - if (@lasth == 0) - #set row height - @lasth = @font_size * @@k_cell_height_ratio; - end - - if (x == 0) - x = GetX(); - end - if (y == 0) - y = GetY(); - end - - # get current page number - pagenum = @page; - - SetX(x); - SetY(y); - - if (w == 0) - w = @fw - x - @r_margin; - end - - b=0; - if (border) - if (border==1) - border='LTRB'; - b='LRT'; - b2='LR'; - elsif border.is_a?(String) - b2=''; - if (border.include?('L')) - b2<<'L'; - end - if (border.include?('R')) - b2<<'R'; - end - b=(border.include?('T')) ? b2 + 'T' : b2; - end - end - - # store original margin values - l_margin = @l_margin; - r_margin = @r_margin; - - # set new margin values - SetLeftMargin(x); - SetRightMargin(@fw - x - w); - - # calculate remaining vertical space on page - restspace = GetPageHeight() - GetY() - GetBreakMargin(); - - writeHTML(html, true, fill); # write html text - - currentY = GetY(); - - @auto_page_break = false; - # check if a new page has been created - if (@page > pagenum) - # design a cell around the text on first page - currentpage = @page; - @page = pagenum; - SetY(GetPageHeight() - restspace - GetBreakMargin()); - Cell(w, restspace - 1, "", b, 0, 'L', 0); - b = b2; - @page += 1; - while @page < currentpage - SetY(@t_margin); # put cursor at the beginning of text - Cell(w, @page_break_trigger - @t_margin, "", b, 0, 'L', 0); - @page += 1; - end - if (border.is_a?(String) and border.include?('B')) - b<<'B'; - end - # design a cell around the text on last page - SetY(@t_margin); # put cursor at the beginning of text - Cell(w, currentY - @t_margin, "", b, 0, 'L', 0); - else - SetY(y); # put cursor at the beginning of text - # design a cell around the text - Cell(w, [h, (currentY - y)].max, "", border, 0, 'L', 0); - end - @auto_page_break = true; - - # restore original margin values - SetLeftMargin(l_margin); - SetRightMargin(r_margin); - - @lasth = h - - # move cursor to specified position - if (ln == 0) - # go to the top-right of the cell - @x = x + w; - @y = y; - elsif (ln == 1) - # go to the beginning of the next line - @x = @l_margin; - @y = currentY; - elsif (ln == 2) - # go to the bottom-left of the cell (below) - @x = x; - @y = currentY; - end - end - alias_method :write_html_cell, :writeHTMLCell - - # - # Check html table tag position. - # - # @param array :table potision array - # @param int :current tr tag id number - # @param int :current td tag id number - # @access private - # @return int : next td_id position. - # value 0 mean that can use position. - # - def checkTableBlockingCellPosition(table, tr_id, td_id ) - 0.upto(tr_id) do |j| - 0.upto(@t_cells[table][j].size - 1) do |i| - if @t_cells[table][j][i]['i0'] <= td_id and td_id <= @t_cells[table][j][i]['i1'] - if @t_cells[table][j][i]['j0'] <= tr_id and tr_id <= @t_cells[table][j][i]['j1'] - return @t_cells[table][j][i]['i1'] - td_id + 1; - end - end - end - end - return 0; - end - - # - # Calculate opening tags. - # - # html table cell array : @t_cells - # - # i0: table cell start position - # i1: table cell end position - # j0: table row start position - # j1: table row end position - # - # +------+ - # |i0,j0 | - # | i1,j1| - # +------+ - # - # example html: - # - # - # - # - # - #
    - # - # i: 0 1 2 - # j+----+----+----+ - # :|0,0 |1,0 |2,0 | - # 0| 0,0| 1,0| 2,0| - # +----+----+----+ - # |0,1 |2,1 | - # 1| 1,1| 2,1| - # +----+----+----+ - # |0,2 |1,2 |2,2 | - # 2| | 1,2| 2,2| - # + +----+----+ - # | |1,3 |2,3 | - # 3| 0,3| 1,3| 2,3| - # +----+----+----+ - # - # html table cell array : - # [[[i0=>0,j0=>0,i1=>0,j1=>0],[i0=>1,j0=>0,i1=>1,j1=>0],[i0=>2,j0=>0,i1=>2,j1=>0]], - # [[i0=>0,j0=>1,i1=>1,j1=>1],[i0=>2,j0=>1,i1=>2,j1=>1]], - # [[i0=>0,j0=>2,i1=>0,j1=>3],[i0=>1,j0=>2,i1=>1,j1=>2],[i0=>2,j0=>2,i1=>2,j1=>2]] - # [[i0=>1,j0=>3,i1=>1,j1=>3],[i0=>2,j0=>3,i1=>2,j1=>3]]] - # - # @param string :tag tag name (in upcase) - # @param string :attr tag attribute (in upcase) - # @access private - # - def openHTMLTagCalc(tag, attrs) - #Opening tag - case (tag) - when 'table' - @max_table_columns[@table_id] = 0; - @t_columns = 0; - @tr_id = -1; - when 'tr' - if @max_table_columns[@table_id] < @t_columns - @max_table_columns[@table_id] = @t_columns; - end - @t_columns = 0; - @tr_id += 1; - @td_id = -1; - @t_cells[@table_id].push [] - when 'td', 'th' - @td_id += 1; - if attrs['colspan'].nil? or attrs['colspan'] == '' - colspan = 1; - else - colspan = attrs['colspan'].to_i; - end - if attrs['rowspan'].nil? or attrs['rowspan'] == '' - rowspan = 1; - else - rowspan = attrs['rowspan'].to_i; - end - - i = 0; - while true - next_i_distance = checkTableBlockingCellPosition(@table_id, @tr_id, @td_id + i); - if next_i_distance == 0 - @t_cells[@table_id][@tr_id].push "i0"=>@td_id + i, "j0"=>@tr_id, "i1"=>(@td_id + i + colspan - 1), "j1"=>@tr_id + rowspan - 1 - break; - end - i += next_i_distance; - end - - @t_columns += colspan; - end - end - - # - # Calculate closing tags. - # @param string :tag tag name (in upcase) - # @access private - # - def closedHTMLTagCalc(tag) - #Closing tag - case (tag) - when 'table' - if @max_table_columns[@table_id] < @t_columns - @max_table_columns[@table_id] = @t_columns; - end - @table_id += 1; - @t_cells.push [] - end - end - - # - # Convert to accessible file path - # @param string :attrname image file name - # - def getImageFilename( attrname ) - nil - end - - # - # Process opening tags. - # @param string :tag tag name (in upcase) - # @param string :attr tag attribute (in upcase) - # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. - # @access private - # - def openHTMLTagHandler(tag, attrs, fill=0) - #Opening tag - case (tag) - when 'pre' - @pre_state = true; - @l_margin += 5; - @r_margin += 5; - @x += 5; - - when 'table' - if @default_table_columns < @max_table_columns[@table_id] - @table_columns = @max_table_columns[@table_id]; - else - @table_columns = @default_table_columns; - end - @l_margin += 5; - @r_margin += 5; - @x += 5; - - if attrs['border'].nil? or attrs['border'] == '' - @tableborder = 0; - else - @tableborder = attrs['border']; - end - @tr_id = -1; - @max_td_page[0] = @page; - @max_td_y[0] = @y; - - when 'tr', 'td', 'th' - if tag == 'th' - SetStyle('b', true); - @tdalign = "C"; - end - if ((!attrs['width'].nil?) and (attrs['width'] != '')) - @tdwidth = (attrs['width'].to_i/4); - else - @tdwidth = ((@w - @l_margin - @r_margin) / @table_columns); - end - - if tag == 'tr' - @tr_id += 1; - @td_id = -1; - else - @td_id += 1; - @x = @l_margin + @tdwidth * @t_cells[@table_id][@tr_id][@td_id]['i0']; - end - - if attrs['colspan'].nil? or attrs['border'] == '' - @colspan = 1; - else - @colspan = attrs['colspan'].to_i; - end - @tdwidth *= @colspan; - if ((!attrs['height'].nil?) and (attrs['height'] != '')) - @tdheight=(attrs['height'].to_i / @k); - else - @tdheight = @lasth; - end - if ((!attrs['align'].nil?) and (attrs['align'] != '')) - case (attrs['align']) - when 'center' - @tdalign = "C"; - when 'right' - @tdalign = "R"; - when 'left' - @tdalign = "L"; - end - end - if ((!attrs['bgcolor'].nil?) and (attrs['bgcolor'] != '')) - coul = convertColorHexToDec(attrs['bgcolor']); - SetFillColor(coul['R'], coul['G'], coul['B']); - @tdfill=1; - end - @tdbegin=true; - - when 'hr' - margin = 1; - if ((!attrs['width'].nil?) and (attrs['width'] != '')) - hrWidth = attrs['width']; - else - hrWidth = @w - @l_margin - @r_margin - margin; - end - SetLineWidth(0.2); - Line(@x + margin, @y, @x + hrWidth, @y); - Ln(); - - when 'strong' - SetStyle('b', true); - - when 'em' - SetStyle('i', true); - - when 'ins' - SetStyle('u', true); - - when 'del' - SetStyle('d', true); - - when 'b', 'i', 'u' - SetStyle(tag, true); - - when 'a' - @href = attrs['href']; - - when 'img' - if (!attrs['src'].nil?) - # Only generates image include a pdf if RMagick is avalaible - unless Object.const_defined?(:Magick) - Write(@lasth, attrs['src'], '', fill); - return - end - file = getImageFilename(attrs['src']) - if (file.nil?) - Write(@lasth, attrs['src'], '', fill); - return - end - - if (attrs['width'].nil?) - attrs['width'] = 0; - end - if (attrs['height'].nil?) - attrs['height'] = 0; - end - - begin - Image(file, GetX(),GetY(), pixelsToMillimeters(attrs['width']), pixelsToMillimeters(attrs['height'])); - #SetX(@img_rb_x); - SetY(@img_rb_y); - rescue => err - logger.error "pdf: Image: error: #{err.message}" - Write(@lasth, attrs['src'], '', fill); - if File.file?( @@k_path_cache + File::basename(file)) - File.delete( @@k_path_cache + File::basename(file)) - end - end - end - - when 'ul', 'ol' - if @li_count == 0 - Ln() if @prevquote_count == @quote_count; # insert Ln for keeping quote lines - @prevquote_count = @quote_count; - end - if @li_state == true - Ln(); - @li_state = false; - end - if tag == 'ul' - @list_ordered[@li_count] = false; - else - @list_ordered[@li_count] = true; - end - @list_count[@li_count] = 0; - @li_count += 1 - - when 'li' - Ln() if @li_state == true - if (@list_ordered[@li_count - 1]) - @list_count[@li_count - 1] += 1; - @li_spacer = " " * @li_count + (@list_count[@li_count - 1]).to_s + ". "; - else - #unordered list simbol - @li_spacer = " " * @li_count + "- "; - end - Write(@lasth, @spacer + @li_spacer, '', fill); - @li_state = true; - - when 'blockquote' - if (@quote_count == 0) - SetStyle('i', true); - @l_margin += 5; - else - @l_margin += 5 / 2; - end - @x = @l_margin; - @quote_top[@quote_count] = @y; - @quote_page[@quote_count] = @page; - @quote_count += 1 - when 'br' - Ln(); - - if (@li_spacer.length > 0) - @x += GetStringWidth(@li_spacer); - end - - when 'p' - Ln(); - 0.upto(@quote_count - 1) do |i| - if @quote_page[i] == @page; - if @quote_top[i] == @y - @lasth; # fix start line - @quote_top[i] = @y; - end - else - if @quote_page[i] == @page - 1; - @quote_page[i] = @page; # fix start line - @quote_top[i] = @t_margin; - end - end - end - - when 'sup' - currentfont_size = @font_size; - @tempfontsize = @font_size_pt; - SetFontSize(@font_size_pt * @@k_small_ratio); - SetXY(GetX(), GetY() - ((currentfont_size - @font_size)*(@@k_small_ratio))); - - when 'sub' - currentfont_size = @font_size; - @tempfontsize = @font_size_pt; - SetFontSize(@font_size_pt * @@k_small_ratio); - SetXY(GetX(), GetY() + ((currentfont_size - @font_size)*(@@k_small_ratio))); - - when 'small' - currentfont_size = @font_size; - @tempfontsize = @font_size_pt; - SetFontSize(@font_size_pt * @@k_small_ratio); - SetXY(GetX(), GetY() + ((currentfont_size - @font_size)/3)); - - when 'font' - if (!attrs['color'].nil? and attrs['color']!='') - coul = convertColorHexToDec(attrs['color']); - SetTextColor(coul['R'], coul['G'], coul['B']); - @issetcolor=true; - end - if (!attrs['face'].nil? and @fontlist.include?(attrs['face'].downcase)) - SetFont(attrs['face'].downcase); - @issetfont=true; - end - if (!attrs['size'].nil?) - headsize = attrs['size'].to_i; - else - headsize = 0; - end - currentfont_size = @font_size; - @tempfontsize = @font_size_pt; - SetFontSize(@font_size_pt + headsize); - @lasth = @font_size * @@k_cell_height_ratio; - - when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' - Ln(); - headsize = (4 - tag[1,1].to_f) * 2 - @tempfontsize = @font_size_pt; - SetFontSize(@font_size_pt + headsize); - SetStyle('b', true); - @lasth = @font_size * @@k_cell_height_ratio; - - end - end - - # - # Process closing tags. - # @param string :tag tag name (in upcase) - # @access private - # - def closedHTMLTagHandler(tag) - #Closing tag - case (tag) - when 'pre' - @pre_state = false; - @l_margin -= 5; - @r_margin -= 5; - @x = @l_margin; - Ln(); - - when 'td','th' - @tdbegin = false; - @tdwidth = 0; - @tdheight = 0; - @tdalign = "L"; - SetStyle('b', false); - @tdfill = 0; - SetFillColor(@prevfill_color[0], @prevfill_color[1], @prevfill_color[2]); - - when 'tr' - @y = @max_td_y[@tr_id + 1]; - @x = @l_margin; - @page = @max_td_page[@tr_id + 1]; - - when 'table' - # Write Table Line - width = (@w - @l_margin - @r_margin) / @table_columns; - 0.upto(@t_cells[@table_id].size - 1) do |j| - 0.upto(@t_cells[@table_id][j].size - 1) do |i| - @page = @max_td_page[j] - i0=@t_cells[@table_id][j][i]['i0']; - j0=@t_cells[@table_id][j][i]['j0']; - i1=@t_cells[@table_id][j][i]['i1']; - j1=@t_cells[@table_id][j][i]['j1']; - - Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * (i1+1), @max_td_y[j0]) # top - if ( @page == @max_td_page[j1 + 1]) - Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * i0, @max_td_y[j1+1]) # left - Line(@l_margin + width * (i1+1), @max_td_y[j0], @l_margin + width * (i1+1), @max_td_y[j1+1]) # right - else - Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * i0, @page_break_trigger) # left - Line(@l_margin + width * (i1+1), @max_td_y[j0], @l_margin + width * (i1+1), @page_break_trigger) # right - @page += 1; - while @page < @max_td_page[j1 + 1] - Line(@l_margin + width * i0, @t_margin, @l_margin + width * i0, @page_break_trigger) # left - Line(@l_margin + width * (i1+1), @t_margin, @l_margin + width * (i1+1), @page_break_trigger) # right - @page += 1; - end - Line(@l_margin + width * i0, @t_margin, @l_margin + width * i0, @max_td_y[j1+1]) # left - Line(@l_margin + width * (i1+1), @t_margin, @l_margin + width * (i1+1), @max_td_y[j1+1]) # right - end - Line(@l_margin + width * i0, @max_td_y[j1+1], @l_margin + width * (i1+1), @max_td_y[j1+1]) # bottom - end - end - - @l_margin -= 5; - @r_margin -= 5; - @tableborder=0; - Ln(); - @table_id += 1; - - when 'strong' - SetStyle('b', false); - - when 'em' - SetStyle('i', false); - - when 'ins' - SetStyle('u', false); - - when 'del' - SetStyle('d', false); - - when 'b', 'i', 'u' - SetStyle(tag, false); - - when 'a' - @href = nil; - - when 'p' - Ln(); - - when 'sup' - currentfont_size = @font_size; - SetFontSize(@tempfontsize); - @tempfontsize = @font_size_pt; - SetXY(GetX(), GetY() - ((currentfont_size - @font_size)*(@@k_small_ratio))); - - when 'sub' - currentfont_size = @font_size; - SetFontSize(@tempfontsize); - @tempfontsize = @font_size_pt; - SetXY(GetX(), GetY() + ((currentfont_size - @font_size)*(@@k_small_ratio))); - - when 'small' - currentfont_size = @font_size; - SetFontSize(@tempfontsize); - @tempfontsize = @font_size_pt; - SetXY(GetX(), GetY() - ((@font_size - currentfont_size)/3)); - - when 'font' - if (@issetcolor == true) - SetTextColor(@prevtext_color[0], @prevtext_color[1], @prevtext_color[2]); - end - if (@issetfont) - @font_family = @prevfont_family; - @font_style = @prevfont_style; - SetFont(@font_family); - @issetfont = false; - end - currentfont_size = @font_size; - SetFontSize(@tempfontsize); - @tempfontsize = @font_size_pt; - #@text_color = @prevtext_color; - @lasth = @font_size * @@k_cell_height_ratio; - - when 'blockquote' - @quote_count -= 1 - if (@quote_page[@quote_count] == @page) - Line(@l_margin - 1, @quote_top[@quote_count], @l_margin - 1, @y) # quoto line - else - cur_page = @page; - cur_y = @y; - @page = @quote_page[@quote_count]; - if (@quote_top[@quote_count] < @page_break_trigger) - Line(@l_margin - 1, @quote_top[@quote_count], @l_margin - 1, @page_break_trigger) # quoto line - end - @page += 1; - while @page < cur_page - Line(@l_margin - 1, @t_margin, @l_margin - 1, @page_break_trigger) # quoto line - @page += 1; - end - @y = cur_y; - Line(@l_margin - 1, @t_margin, @l_margin - 1, @y) # quoto line - end - if (@quote_count <= 0) - SetStyle('i', false); - @l_margin -= 5; - else - @l_margin -= 5 / 2; - end - @x = @l_margin; - Ln() if @quote_count == 0 - - when 'ul', 'ol' - @li_count -= 1 - if @li_state == true - Ln(); - @li_state = false; - end - - when 'li' - @li_spacer = ""; - if @li_state == true - Ln(); - @li_state = false; - end - - when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' - SetFontSize(@tempfontsize); - @tempfontsize = @font_size_pt; - SetStyle('b', false); - Ln(); - @lasth = @font_size * @@k_cell_height_ratio; - - if tag == 'h1' or tag == 'h2' or tag == 'h3' or tag == 'h4' - margin = 1; - hrWidth = @w - @l_margin - @r_margin - margin; - if tag == 'h1' or tag == 'h2' - SetLineWidth(0.2); - else - SetLineWidth(0.1); - end - Line(@x + margin, @y, @x + hrWidth, @y); - end - end - end - - # - # Sets font style. - # @param string :tag tag name (in lowercase) - # @param boolean :enable - # @access private - # - def SetStyle(tag, enable) - #Modify style and select corresponding font - ['b', 'i', 'u', 'd'].each do |s| - if tag.downcase == s - if enable - @style << s if ! @style.include?(s) - else - @style = @style.gsub(s,'') - end - end - end - SetFont('', @style); - end - - # - # Output anchor link. - # @param string :url link URL - # @param string :name link name - # @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0. - # @access public - # - def addHtmlLink(url, name, fill=0) - #Put a hyperlink - SetTextColor(0, 0, 255); - SetStyle('u', true); - Write(@lasth, name, url, fill); - SetStyle('u', false); - SetTextColor(0); - end - - # - # Returns an associative array (keys: R,G,B) from - # a hex html code (e.g. #3FE5AA). - # @param string :color hexadecimal html color [#rrggbb] - # @return array - # @access private - # - def convertColorHexToDec(color = "#000000") - tbl_color = {} - tbl_color['R'] = color[1,2].hex.to_i; - tbl_color['G'] = color[3,2].hex.to_i; - tbl_color['B'] = color[5,2].hex.to_i; - return tbl_color; - end - - # - # Converts pixels to millimeters in 72 dpi. - # @param int :px pixels - # @return float millimeters - # @access private - # - def pixelsToMillimeters(px) - return px.to_f * 25.4 / 72; - end - - # - # Reverse function for htmlentities. - # Convert entities in UTF-8. - # - # @param :text_to_convert Text to convert. - # @return string converted - # - def unhtmlentities(string) - if @@decoder.nil? - CGI.unescapeHTML(string) - else - @@decoder.decode(string) - end - end - -end # END OF CLASS - -#TODO 2007-05-25 (EJM) Level=0 - -#Handle special IE contype request -# if (!_SERVER['HTTP_USER_AGENT'].nil? and (_SERVER['HTTP_USER_AGENT']=='contype')) -# header('Content-Type: application/pdf'); -# exit; -# } diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/52/526af1c2bfa92da73d02577e9ad25a11ff60e17c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/52/526af1c2bfa92da73d02577e9ad25a11ff60e17c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,47 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class DocumentCategoryTest < ActiveSupport::TestCase + fixtures :enumerations, :documents, :issues + + def test_should_be_an_enumeration + assert DocumentCategory.ancestors.include?(Enumeration) + end + + def test_objects_count + assert_equal 2, DocumentCategory.find_by_name("Uncategorized").objects_count + assert_equal 0, DocumentCategory.find_by_name("User documentation").objects_count + end + + def test_option_name + assert_equal :enumeration_doc_categories, DocumentCategory.new.option_name + end + + def test_default + assert_nil DocumentCategory.find(:first, :conditions => { :is_default => true }) + e = Enumeration.find_by_name('Technical documentation') + e.update_attributes(:is_default => true) + assert_equal 3, DocumentCategory.default.id + end + + def test_force_default + assert_nil DocumentCategory.find(:first, :conditions => { :is_default => true }) + assert_equal 1, DocumentCategory.default.id + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/53/5324579083fdcc5b99f25f25551b161fc6c450c1.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/53/5324579083fdcc5b99f25f25551b161fc6c450c1.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,64 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class MemberRole < ActiveRecord::Base + belongs_to :member + belongs_to :role + + after_destroy :remove_member_if_empty + + after_create :add_role_to_group_users + after_destroy :remove_role_from_group_users + + validates_presence_of :role + validate :validate_role_member + + def validate_role_member + errors.add :role_id, :invalid if role && !role.member? + end + + def inherited? + !inherited_from.nil? + end + + private + + def remove_member_if_empty + if member.roles.empty? + member.destroy + end + end + + def add_role_to_group_users + if member.principal.is_a?(Group) + member.principal.users.each do |user| + user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id) + user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id) + user_member.save! + end + end + end + + def remove_role_from_group_users + MemberRole.find(:all, :conditions => { :inherited_from => id }).group_by(&:member).each do |member, member_roles| + member_roles.each(&:destroy) + if member && member.user + Watcher.prune(:user => member.user, :project => member.project) + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/53/5351113c903a72a9248a00117b5118ac4bdf3d73.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/53/5351113c903a72a9248a00117b5118ac4bdf3d73.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,24 @@ +/* Bulgarian initialisation for the jQuery UI date picker plugin. */ +/* Written by Stoyan Kyosev (http://svest.org). */ +jQuery(function($){ + $.datepicker.regional['bg'] = { + closeText: 'затвори', + prevText: '<назад', + nextText: 'напред>', + nextBigText: '>>', + currentText: 'днеÑ', + monthNames: ['Януари','Февруари','Март','Ðприл','Май','Юни', + 'Юли','ÐвгуÑÑ‚','Септември','Октомври','Ðоември','Декември'], + monthNamesShort: ['Яну','Фев','Мар','Ðпр','Май','Юни', + 'Юли','Ðвг','Сеп','Окт','Ðов','Дек'], + dayNames: ['ÐеделÑ','Понеделник','Вторник','СрÑда','Четвъртък','Петък','Събота'], + dayNamesShort: ['Ðед','Пон','Вто','СрÑ','Чет','Пет','Съб'], + dayNamesMin: ['Ðе','По','Ð’Ñ‚','Ср','Че','Пе','Съ'], + weekHeader: 'Wk', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['bg']); +}); diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/53/537c76b0b6db46cea1550518e810dd1d4d5b897a.svn-base --- a/.svn/pristine/53/537c76b0b6db46cea1550518e810dd1d4d5b897a.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -class Document < ActiveRecord::Base - generator_for :title, :start => 'Document001' - -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/53/53b5f242938d157ff2dae7c897102294dd6da41c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/53/53b5f242938d157ff2dae7c897102294dd6da41c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,51 @@ +
    +<%= link_to(l(:button_change_password), {:action => 'password'}, :class => 'icon icon-passwd') if @user.change_password_allowed? %> +<%= call_hook(:view_my_account_contextual, :user => @user)%> +
    + +

    <%=l(:label_my_account)%>

    +<%= error_messages_for 'user' %> + +<%= labelled_form_for :user, @user, + :url => { :action => "account" }, + :html => { :id => 'my_account_form', + :method => :post } do |f| %> +
    +
    + <%=l(:label_information_plural)%> +

    <%= f.text_field :firstname, :required => true %>

    +

    <%= f.text_field :lastname, :required => true %>

    +

    <%= f.text_field :mail, :required => true %>

    +

    <%= f.select :language, lang_options_for_select %>

    + <% if Setting.openid? %> +

    <%= f.text_field :identity_url %>

    + <% end %> + + <% @user.custom_field_values.select(&:editable?).each do |value| %> +

    <%= custom_field_tag_with_label :user, value %>

    + <% end %> + <%= call_hook(:view_my_account, :user => @user, :form => f) %> +
    + +<%= submit_tag l(:button_save) %> +
    + +
    +
    + <%=l(:field_mail_notification)%> + <%= render :partial => 'users/mail_notifications' %> +
    + +
    + <%=l(:label_preferences)%> + <%= render :partial => 'users/preferences' %> +
    + +
    +<% end %> + +<% content_for :sidebar do %> +<%= render :partial => 'sidebar' %> +<% end %> + +<% html_title(l(:label_my_account)) -%> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/53/53eb3f2e1882327db7415ab09e0ccb8d4b2a4d5f.svn-base --- a/.svn/pristine/53/53eb3f2e1882327db7415ab09e0ccb8d4b2a4d5f.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1850 +0,0 @@ -== Redmine changelog - -Redmine - project management software -Copyright (C) 2006-2012 Jean-Philippe Lang -http://www.redmine.org/ - -== 2012-02-06 v1.3.1 - -* Defect #9775: app/views/repository/_revision_graph.html.erb sets window.onload directly.. -* Defect #9792: Ruby 1.9: [v1.3.0] Error: incompatible character encodings for it translation on Calendar page -* Defect #9793: Bad spacing between numbered list and heading (recently broken). -* Defect #9795: Unrelated error message when creating a group with an invalid name -* Defect #9832: Revision graph height should depend on height of rows in revisions table -* Defect #9937: Repository settings are not saved when all SCM are disabled -* Defect #9961: Ukrainian "default_tracker_bug" is wrong -* Defect #10013: Rest API - Create Version -> Internal server error 500 -* Defect #10115: Javascript error - Can't attach more than 1 file on IE 6 and 7 -* Defect #10130: Broken italic text style in edited comment preview -* Defect #10152: Attachment diff type is not saved in user preference -* Feature #9943: Arabic translation -* Patch #9874: pt-BR translation updates -* Patch #9922: Spanish translation updated -* Patch #10137: Korean language file ko.yml updated to Redmine 1.3.0 - -== 2011-12-10 v1.3.0 - -* Defect #2109: Context menu is being submitted twice per right click -* Defect #7717: MailHandler user creation for unknown_user impossible due to diverging length-limits of login and email fields -* Defect #7917: Creating users via email fails if user real name containes special chars -* Defect #7966: MailHandler does not include JournalDetail for attached files -* Defect #8368: Bad decimal separator in time entry CSV -* Defect #8371: MySQL error when filtering a custom field using the REST api -* Defect #8549: Export CSV has character encoding error -* Defect #8573: Do not show inactive Enumerations where not needed -* Defect #8611: rake/rdoctask is deprecated -* Defect #8751: Email notification: bug, when number of recipients more then 8 -* Defect #8894: Private issues - make it more obvious in the UI? -* Defect #8994: Hardcoded French string "anonyme" -* Defect #9043: Hardcoded string "diff" in Wiki#show and Repositories_Helper -* Defect #9051: wrong "text_issue_added" in russian translation. -* Defect #9108: Custom query not saving status filter -* Defect #9252: Regression: application title escaped 2 times -* Defect #9264: Bad Portuguese translation -* Defect #9470: News list is missing Avatars -* Defect #9471: Inline markup broken in Wiki link labels -* Defect #9489: Label all input field and control tags -* Defect #9534: Precedence: bulk email header is non standard and discouraged -* Defect #9540: Issue filter by assigned_to_role is not project specific -* Defect #9619: Time zone ignored when logging time while editing ticket -* Defect #9638: Inconsistent image filename extensions -* Defect #9669: Issue list doesn't sort assignees/authors regarding user display format -* Defect #9672: Message-quoting in forums module broken -* Defect #9719: Filtering by numeric custom field types broken after update to master -* Defect #9724: Can't remote add new categories -* Defect #9738: Setting of cross-project custom query is not remembered inside project -* Defect #9748: Error about configuration.yml validness should mention file path -* Feature #69: Textilized description in PDF -* Feature #401: Add pdf export for WIKI page -* Feature #1567: Make author column sortable and groupable -* Feature #2222: Single section edit. -* Feature #2269: Default issue start date should become configurable. -* Feature #2371: character encoding for attachment file -* Feature #2964: Ability to assign issues to groups -* Feature #3033: Bug Reporting: Using "Create and continue" should show bug id of saved bug -* Feature #3261: support attachment images in PDF export -* Feature #4264: Update CodeRay to 1.0 final -* Feature #4324: Redmine renames my files, it shouldn't. -* Feature #4729: Add Date-Based Filters for Issues List -* Feature #4742: CSV export: option to export selected or all columns -* Feature #4976: Allow rdm-mailhandler to read the API key from a file -* Feature #5501: Git: Mercurial: Adding visual merge/branch history to repository view -* Feature #5634: Export issue to PDF does not include Subtasks and Related Issues -* Feature #5670: Cancel option for file upload -* Feature #5737: Custom Queries available through the REST Api -* Feature #6180: Searchable custom fields do not provide adequate operators -* Feature #6954: Filter from date to date -* Feature #7180: List of statuses in REST API -* Feature #7181: List of trackers in REST API -* Feature #7366: REST API for Issue Relations -* Feature #7403: REST API for Versions -* Feature #7671: REST API for reading attachments -* Feature #7832: Ability to assign issue categories to groups -* Feature #8420: Consider removing #7013 workaround -* Feature #9196: Improve logging in MailHandler when user creation fails -* Feature #9496: Adds an option in mailhandler to disable server certificate verification -* Feature #9553: CRUD operations for "Issue categories" in REST API -* Feature #9593: HTML title should be reordered -* Feature #9600: Wiki links for news and forums -* Feature #9607: Filter for issues without start date (or any another field based on date type) -* Feature #9609: Upgrade to Rails 2.3.14 -* Feature #9612: "side by side" and "inline" patch view for attachments -* Feature #9667: Check attachment size before upload -* Feature #9690: Link in notification pointing to the actual update -* Feature #9720: Add note number for single issue's PDF -* Patch #8617: Indent subject of subtask ticket in exported issues PDF -* Patch #8778: Traditional Chinese 'issue' translation change -* Patch #9053: Fix up Russian translation -* Patch #9129: Improve wording of Git repository note at project setting -* Patch #9148: Better handling of field_due_date italian translation -* Patch #9273: Fix typos in russian localization -* Patch #9484: Limit SCM annotate to text files under the maximum file size for viewing -* Patch #9659: Indexing rows in auth_sources/index view -* Patch #9692: Fix Textilized description in PDF for CodeRay - -== 2011-12-10 v1.2.3 - -* Defect #8707: Reposman: wrong constant name -* Defect #8809: Table in timelog report overflows -* Defect #9055: Version files in Files module cannot be downloaded if issue tracking is disabled -* Defect #9137: db:encrypt fails to handle repositories with blank password -* Defect #9394: Custom date field only validating on regex and not a valid date -* Defect #9405: Any user with :log_time permission can edit time entries via context menu -* Defect #9448: The attached images are not shown in documents -* Defect #9520: Copied private query not visible after project copy -* Defect #9552: Error when reading ciphered text from the database without cipher key configured -* Defect #9566: Redmine.pm considers all projects private when login_required is enabled -* Defect #9567: Redmine.pm potential security issue with cache credential enabled and subversion -* Defect #9577: Deleting a subtasks doesn't update parent's rgt & lft values -* Defect #9597: Broken version links in wiki annotate history -* Defect #9682: Wiki HTML Export only useful when Access history is accessible -* Defect #9737: Custom values deleted before issue submit -* Defect #9741: calendar-hr.js (Croatian) is not UTF-8 -* Patch #9558: Simplified Chinese translation for 1.2.2 updated -* Patch #9695: Bulgarian translation (r7942) - -== 2011-11-11 v1.2.2 - -* Defect #3276: Incorrect handling of anchors in Wiki to HTML export -* Defect #7215: Wiki formatting mangles links to internal headers -* Defect #7613: Generated test instances may share the same attribute value object -* Defect #8411: Can't remove "Project" column on custom query -* Defect #8615: Custom 'version' fields don't show shared versions -* Defect #8633: Pagination counts non visible issues -* Defect #8651: Email attachments are not added to issues any more in v1.2 -* Defect #8825: JRuby + Windows: SCMs do not work on Redmine 1.2 -* Defect #8836: Additional workflow transitions not available when set to both author and assignee -* Defect #8865: Custom field regular expression is not validated -* Defect #8880: Error deleting issue with grandchild -* Defect #8884: Assignee is cleared when updating issue with locked assignee -* Defect #8892: Unused fonts in rfpdf plugin folder -* Defect #9161: pt-BR field_warn_on_leaving_unsaved has a small gramatical error -* Defect #9308: Search fails when a role haven't "view wiki" permission -* Defect #9465: Mercurial: can't browse named branch below Mercurial 1.5 - -== 2011-07-11 v1.2.1 - -* Defect #5089: i18N error on truncated revision diff view -* Defect #7501: Search options get lost after clicking on a specific result type -* Defect #8229: "project.xml" response does not include the parent ID -* Defect #8449: Wiki annotated page does not display author of version 1 -* Defect #8467: Missing german translation - Warn me when leaving a page with unsaved text -* Defect #8468: No warning when leaving page with unsaved text that has not lost focus -* Defect #8472: Private checkbox ignored on issue creation with "Set own issues public or private" permission -* Defect #8510: JRuby: Can't open administrator panel if scm command is not available -* Defect #8512: Syntax highlighter on Welcome page -* Defect #8554: Translation missing error on custom field validation -* Defect #8565: JRuby: Japanese PDF export error -* Defect #8566: Exported PDF UTF-8 Vietnamese not correct -* Defect #8569: JRuby: PDF export error with TypeError -* Defect #8576: Missing german translation - different things -* Defect #8616: Circular relations -* Defect #8646: Russian translation "label_follows" and "label_follows" are wrong -* Defect #8712: False 'Description updated' journal details messages -* Defect #8729: Not-public queries are not private -* Defect #8737: Broken line of long issue description on issue PDF. -* Defect #8738: Missing revision number/id of associated revisions on issue PDF -* Defect #8739: Workflow copy does not copy advanced workflow settings -* Defect #8759: Setting issue attributes from mail should be case-insensitive -* Defect #8777: Mercurial: Not able to Resetting Redmine project respository - -== 2011-05-30 v1.2.0 - -* Defect #61: Broken character encoding in pdf export -* Defect #1965: Redmine is not Tab Safe -* Defect #2274: Filesystem Repository path encoding of non UTF-8 characters -* Defect #2664: Mercurial: Repository path encoding of non UTF-8 characters -* Defect #3421: Mercurial reads files from working dir instead of changesets -* Defect #3462: CVS: Repository path encoding of non UTF-8 characters -* Defect #3715: Login page should not show projects link and search box if authentication is required -* Defect #3724: Mercurial repositories display revision ID instead of changeset ID -* Defect #3761: Most recent CVS revisions are missing in "revisions" view -* Defect #4270: CVS Repository view in Project doesn't show Author, Revision, Comment -* Defect #5138: Don't use Ajax for pagination -* Defect #5152: Cannot use certain characters for user and role names. -* Defect #5251: Git: Repository path encoding of non UTF-8 characters -* Defect #5373: Translation missing when adding invalid watchers -* Defect #5817: Shared versions not shown in subproject's gantt chart -* Defect #6013: git tab,browsing, very slow -- even after first time -* Defect #6148: Quoting, newlines, and nightmares... -* Defect #6256: Redmine considers non ASCII and UTF-16 text files as binary in SCM -* Defect #6476: Subproject's issues are not shown in the subproject's gantt -* Defect #6496: Remove i18n 0.3.x/0.4.x hack for Rails 2.3.5 -* Defect #6562: Context-menu deletion of issues deletes all subtasks too without explicit prompt -* Defect #6604: Issues targeted at parent project versions' are not shown on gantt chart -* Defect #6706: Resolving issues with the commit message produces the wrong comment with CVS -* Defect #6901: Copy/Move an issue does not give any history of who actually did the action. -* Defect #6905: Specific heading-content breaks CSS -* Defect #7000: Project filter not applied on versions in Gantt chart -* Defect #7097: Starting day of week cannot be set to Saturday -* Defect #7114: New gantt doesn't display some projects -* Defect #7146: Git adapter lost commits before 7 days from database latest changeset -* Defect #7218: Date range error on issue query -* Defect #7257: "Issues by" version links bad criterias -* Defect #7279: CSS class ".icon-home" is not used. -* Defect #7320: circular dependency >2 issues -* Defect #7352: Filters not working in Gantt charts -* Defect #7367: Receiving pop3 email should not output debug messages -* Defect #7373: Error with PDF output and ruby 1.9.2 -* Defect #7379: Remove extraneous hidden_field on wiki history -* Defect #7516: Redmine does not work with RubyGems 1.5.0 -* Defect #7518: Mercurial diff can be wrong if the previous changeset isn't the parent -* Defect #7581: Not including a spent time value on the main issue update screen causes silent data loss -* Defect #7582: hiding form pages from search engines -* Defect #7597: Subversion and Mercurial log have the possibility to miss encoding -* Defect #7604: ActionView::TemplateError (undefined method `name' for nil:NilClass) -* Defect #7605: Using custom queries always redirects to "Issues" tab -* Defect #7615: CVS diffs do not handle new files properly -* Defect #7618: SCM diffs do not handle one line new files properly -* Defect #7639: Some date fields do not have requested format. -* Defect #7657: Wrong commit range in git log command on Windows -* Defect #7818: Wiki pages don't use the local timezone to display the "Updated ? hours ago" mouseover -* Defect #7821: Git "previous" and "next" revisions are incorrect -* Defect #7827: CVS: Age column on repository view is off by timezone delta -* Defect #7843: Add a relation between issues = explicit login window ! (basic authentication popup is prompted on AJAX request) -* Defect #8011: {{toc}} does not display headlines with inline code markup -* Defect #8029: List of users for adding to a group may be empty if 100 first users have been added -* Defect #8064: Text custom fields do not wrap on the issue list -* Defect #8071: Watching a subtask from the context menu updates main issue watch link -* Defect #8072: Two untranslatable default role names -* Defect #8075: Some "notifiable" names are not i18n-enabled -* Defect #8081: GIT: Commits missing when user has the "decorate" git option enabled -* Defect #8088: Colorful indentation of subprojects must be on right in RTL locales -* Defect #8239: notes field is not propagated during issue copy -* Defect #8356: GET /time_entries.xml ignores limit/offset parameters -* Defect #8432: Private issues information shows up on Activity page for unauthorized users -* Feature #746: Versioned issue descriptions -* Feature #1067: Differentiate public/private saved queries in the sidebar -* Feature #1236: Make destination folder for attachment uploads configurable -* Feature #1735: Per project repository log encoding setting -* Feature #1763: Autologin-cookie should be configurable -* Feature #1981: display mercurial tags -* Feature #2074: Sending email notifications when comments are added in the news section -* Feature #2096: Custom fields referencing system tables (users and versions) -* Feature #2732: Allow additional workflow transitions for author and assignee -* Feature #2910: Warning on leaving edited issue/wiki page without saving -* Feature #3396: Git: use --encoding=UTF-8 in "git log" -* Feature #4273: SCM command availability automatic check in administration panel -* Feature #4477: Use mime types in downloading from repository -* Feature #5518: Graceful fallback for "missing translation" needed -* Feature #5520: Text format buttons and preview link missing when editing comment -* Feature #5831: Parent Task to Issue Bulk Edit -* Feature #6887: Upgrade to Rails 2.3.11 -* Feature #7139: Highlight changes inside diff lines -* Feature #7236: Collapse All for Groups -* Feature #7246: Handle "named branch" for mercurial -* Feature #7296: Ability for admin to delete users -* Feature #7318: Add user agent to Redmine Mailhandler -* Feature #7408: Add an application configuration file -* Feature #7409: Cross project Redmine links -* Feature #7410: Add salt to user passwords -* Feature #7411: Option to cipher LDAP ans SCM passwords stored in the database -* Feature #7412: Add an issue visibility level to each role -* Feature #7414: Private issues -* Feature #7517: Configurable path of executable for scm adapters -* Feature #7640: Add "mystery man" gravatar to options -* Feature #7858: RubyGems 1.6 support -* Feature #7893: Group filter on the users list -* Feature #7899: Box for editing comments should open with the formatting toolbar -* Feature #7921: issues by pulldown should have 'status' option -* Feature #7996: Bulk edit and context menu for time entries -* Feature #8006: Right click context menu for Related Issues -* Feature #8209: I18n YAML files not parsable with psych yaml library -* Feature #8345: Link to user profile from account page -* Feature #8365: Git: per project setting to report last commit or not in repository tree -* Patch #5148: metaKey not handled in issues selection -* Patch #5629: Wrap text fields properly in PDF -* Patch #7418: Redmine Persian Translation -* Patch #8295: Wrap title fields properly in PDF -* Patch #8310: fixes automatic line break problem with TCPDF -* Patch #8312: Switch to TCPDF from FPDF for PDF export - -== 2011-04-29 v1.1.3 - -* Defect #5773: Email reminders are sent to locked users -* Defect #6590: Wrong file list link in email notification on new file upload -* Defect #7589: Wiki page with backslash in title can not be found -* Defect #7785: Mailhandler keywords are not removed when updating issues -* Defect #7794: Internal server error on formatting an issue as a PDF in Japanese -* Defect #7838: Gantt- Issues does not show up in green when start and end date are the same -* Defect #7846: Headers (h1, etc.) containing backslash followed by a digit are not displayed correctly -* Defect #7875: CSV export separators in polish locale (pl.yml) -* Defect #7890: Internal server error when referencing an issue without project in commit message -* Defect #7904: Subprojects not properly deleted when deleting a parent project -* Defect #7939: Simultaneous Wiki Updates Cause Internal Error -* Defect #7951: Atom links broken on wiki index -* Defect #7954: IE 9 can not select issues, does not display context menu -* Defect #7985: Trying to do a bulk edit results in "Internal Error" -* Defect #8003: Error raised by reposman.rb under Windows server 2003 -* Defect #8012: Wrong selection of modules when adding new project after validation error -* Defect #8038: Associated Revisions OL/LI items are not styled properly in issue view -* Defect #8067: CSV exporting in Italian locale -* Defect #8235: bulk edit issues and copy issues error in es, gl and ca locales -* Defect #8244: selected modules are not activated when copying a project -* Patch #7278: Update Simplified Chinese translation to 1.1 -* Patch #7390: Fixes in Czech localization -* Patch #7963: Reminder email: Link for show all issues does not sort - -== 2011-03-07 v1.1.2 - -* Defect #3132: Bulk editing menu non-functional in Opera browser -* Defect #6090: Most binary files become corrupted when downloading from CVS repository browser when Redmine is running on a Windows server -* Defect #7280: Issues subjects wrap in Gantt -* Defect #7288: Non ASCII filename downloaded from repo is broken on Internet Explorer. -* Defect #7317: Gantt tab gives internal error due to nil avatar icon -* Defect #7497: Aptana Studio .project file added to version 1.1.1-stable -* Defect #7611: Workflow summary shows X icon for workflow with exactly 1 status transition -* Defect #7625: Syntax highlighting unavailable from board new topic or topic edit preview -* Defect #7630: Spent time in commits not recognized -* Defect #7656: MySQL SQL Syntax Error when filtering issues by Assignee's Group -* Defect #7718: Minutes logged in commit message are converted to hours -* Defect #7763: Email notification are sent to watchers even if 'No events' setting is chosen -* Feature #7608: Add "retro" gravatars -* Patch #7598: Extensible MailHandler -* Patch #7795: Internal server error at journals#index with custom fields - -== 2011-01-30 v1.1.1 - -* Defect #4899: Redmine fails to list files for darcs repository -* Defect #7245: Wiki fails to find pages with cyrillic characters using postgresql -* Defect #7256: redmine/public/.htaccess must be moved for non-fastcgi installs/upgrades -* Defect #7258: Automatic spent time logging does not work properly with SQLite3 -* Defect #7259: Released 1.1.0 uses "devel" label inside admin information -* Defect #7265: "Loading..." icon does not disappear after add project member -* Defect #7266: Test test_due_date_distance_in_words fail due to undefined locale -* Defect #7274: CSV value separator in dutch locale -* Defect #7277: Enabling gravatas causes usernames to overlap first name field in user list -* Defect #7294: "Notifiy for only project I select" is not available anymore in 1.1.0 -* Defect #7307: HTTP 500 error on query for empty revision -* Defect #7313: Label not translated in french in Settings/Email Notification tab -* Defect #7329: with long strings may hang server -* Defect #7337: My page french translation -* Defect #7348: French Translation of "Connection" -* Defect #7385: Error when viewing an issue which was related to a deleted subtask -* Defect #7386: NoMethodError on pdf export -* Defect #7415: Darcs adapter recognizes new files as modified files above Darcs 2.4 -* Defect #7421: no email sent with 'Notifiy for any event on the selected projects only' -* Feature #5344: Update to latest CodeRay 0.9.x - -== 2011-01-09 v1.1.0 - -* Defect #2038: Italics in wiki headers show-up wrong in the toc -* Defect #3449: Redmine Takes Too Long On Large Mercurial Repository -* Defect #3567: Sorting for changesets might go wrong on Mercurial repos -* Defect #3707: {{toc}} doesn't work with {{include}} -* Defect #5096: Redmine hangs up while browsing Git repository -* Defect #6000: Safe Attributes prevents plugin extension of Issue model... -* Defect #6064: Modules not assigned to projects created via API -* Defect #6110: MailHandler should allow updating Issue Priority and Custom fields -* Defect #6136: JSON API holds less information than XML API -* Defect #6345: xml used by rest API is invalid -* Defect #6348: Gantt chart PDF rendering errors -* Defect #6403: Updating an issue with custom fields fails -* Defect #6467: "Member of role", "Member of group" filter not work correctly -* Defect #6473: New gantt broken after clearing issue filters -* Defect #6541: Email notifications send to everybody -* Defect #6549: Notification settings not migrated properly -* Defect #6591: Acronyms must have a minimum of three characters -* Defect #6674: Delete time log broken after changes to REST -* Defect #6681: Mercurial, Bazaar and Darcs auto close issue text should be commit id instead of revision number -* Defect #6724: Wiki uploads does not work anymore (SVN 4266) -* Defect #6746: Wiki links are broken on Activity page -* Defect #6747: Wiki diff does not work since r4265 -* Defect #6763: New gantt charts: subject displayed twice on issues -* Defect #6826: Clicking "Add" twice creates duplicate member record -* Defect #6844: Unchecking status filter on the issue list has no effect -* Defect #6895: Wrong Polish translation of "blocks" -* Defect #6943: Migration from boolean to varchar fails on PostgreSQL 8.1 -* Defect #7064: Mercurial adapter does not recognize non alphabetic nor numeric in UTF-8 copied files -* Defect #7128: New gantt chart does not render subtasks under parent task -* Defect #7135: paging mechanism returns the same last page forever -* Defect #7188: Activity page not refreshed when changing language -* Defect #7195: Apply CLI-supplied defaults for incoming mail only to new issues not replies -* Defect #7197: Tracker reset to default when replying to an issue email -* Defect #7213: Copy project does not copy all roles and permissions -* Defect #7225: Project settings: Trackers & Custom fields only relevant if module Issue tracking is active -* Feature #630: Allow non-unique names for projects -* Feature #1738: Add a "Visible" flag to project/user custom fields -* Feature #2803: Support for Javascript in Themes -* Feature #2852: Clean Incoming Email of quoted text "----- Reply above this line ------" -* Feature #2995: Improve error message when trying to access an archived project -* Feature #3170: Autocomplete issue relations on subject -* Feature #3503: Administrator Be Able To Modify Email settings Of Users -* Feature #4155: Automatic spent time logging from commit messages -* Feature #5136: Parent select on Wiki rename page -* Feature #5338: Descendants (subtasks) should be available via REST API -* Feature #5494: Wiki TOC should display heading from level 4 -* Feature #5594: Improve MailHandler's keyword handling -* Feature #5622: Allow version to be set via incoming email -* Feature #5712: Reload themes -* Feature #5869: Issue filters by Group and Role -* Feature #6092: Truncate Git revision labels in Activity page/feed and allow configurable length -* Feature #6112: Accept localized keywords when receiving emails -* Feature #6140: REST issues response with issue count limit and offset -* Feature #6260: REST API for Users -* Feature #6276: Gantt Chart rewrite -* Feature #6446: Remove length limits on project identifier and name -* Feature #6628: Improvements in truncate email -* Feature #6779: Project JSON API -* Feature #6823: REST API for time tracker. -* Feature #7072: REST API for news -* Feature #7111: Expose more detail on journal entries -* Feature #7141: REST API: get information about current user -* Patch #4807: Allow to set the done_ratio field with the incoming mail system -* Patch #5441: Initialize TimeEntry attributes with params[:time_entry] -* Patch #6762: Use GET instead of POST to retrieve context_menu -* Patch #7160: French translation ofr "not_a_date" is missing -* Patch #7212: Missing remove_index in AddUniqueIndexOnMembers down migration - - -== 2010-12-23 v1.0.5 - -* #6656: Mercurial adapter loses seconds of commit times -* #6996: Migration trac(sqlite3) -> redmine(postgresql) doesnt escape ' char -* #7013: v-1.0.4 trunk - see {{count}} in page display rather than value -* #7016: redundant 'field_start_date' in ja.yml -* #7018: 'undefined method `reschedule_after' for nil:NilClass' on new issues -* #7024: E-mail notifications about Wiki changes. -* #7033: 'class' attribute of
     tag shouldn't be truncate
    -* #7035: CSV value separator in russian
    -* #7122: Issue-description Quote-button missing
    -* #7144: custom queries making use of deleted custom fields cause a 500 error
    -* #7162: Multiply defined label in french translation
    -
    -== 2010-11-28 v1.0.4
    -
    -* #5324: Git not working if color.ui is enabled
    -* #6447: Issues API doesn't allow full key auth for all actions
    -* #6457: Edit User group problem
    -* #6575: start date being filled with current date even when blank value is submitted
    -* #6740: Max attachment size, incorrect usage of 'KB'
    -* #6760: Select box sorted by ID instead of name in Issue Category
    -* #6766: Changing target version name can cause an internal error
    -* #6784: Redmine not working with i18n gem 0.4.2
    -* #6839: Hardcoded absolute links in my/page_layout
    -* #6841: Projects API doesn't allow full key auth for all actions
    -* #6860: svn: Write error: Broken pipe when browsing repository
    -* #6874: API should return XML description when creating a project
    -* #6932: submitting wrong parent task input creates a 500 error
    -* #6966: Records of Forums are remained, deleting project
    -* #6990: Layout problem in workflow overview
    -* #5117: mercurial_adapter should ensure the right LANG environment variable
    -* #6782: Traditional Chinese language file (to r4352)
    -* #6783: Swedish Translation for r4352
    -* #6804: Bugfix: spelling fixes
    -* #6814: Japanese Translation for r4362
    -* #6948: Bulgarian translation
    -* #6973: Update es.yml
    -
    -== 2010-10-31 v1.0.3
    -
    -* #4065: Redmine.pm doesn't work with LDAPS and a non-standard port
    -* #4416: Link from version details page to edit the wiki.
    -* #5484: Add new issue as subtask to an existing ticket
    -* #5948: Update help/wiki_syntax_detailed.html with more link options
    -* #6494: Typo in pt_BR translation for 1.0.2
    -* #6508: Japanese translation update
    -* #6509: Localization pt-PT (new strings)
    -* #6511: Rake task to test email
    -* #6525: Traditional Chinese language file (to r4225)
    -* #6536: Patch for swedish translation
    -* #6548: Rake tasks to add/remove i18n strings
    -* #6569: Updated Hebrew translation
    -* #6570: Japanese Translation for r4231
    -* #6596: pt-BR translation updates
    -* #6629: Change field-name of issues start date
    -* #6669: Bulgarian translation
    -* #6731: Macedonian translation fix
    -* #6732: Japanese Translation for r4287
    -* #6735: Add user-agent to reposman
    -* #6736: Traditional Chinese language file (to r4288)
    -* #6739: Swedish Translation for r4288
    -* #6765: Traditional Chinese language file (to r4302)
    -* Fixed #5324: Git not working if color.ui is enabled
    -* Fixed #5652: Bad URL parsing in the wiki when it ends with right-angle-bracket(greater-than mark).
    -* Fixed #5803: Precedes/Follows Relationships Broke
    -* Fixed #6435: Links to wikipages bound to versions do not respect version-sharing in Settings -> Versions
    -* Fixed #6438: Autologin cannot be disabled again once it's enabled
    -* Fixed #6513: "Move" and "Copy" are not displayed when deployed in subdirectory
    -* Fixed #6521: Tooltip/label for user "search-refinment" field on group/project member list
    -* Fixed #6563: i18n-issues on calendar view
    -* Fixed #6598: Wrong caption for button_create_and_continue in German language file
    -* Fixed #6607: Unclear caption for german button_update
    -* Fixed #6612: SortHelper missing from CalendarsController
    -* Fixed #6740: Max attachment size, incorrect usage of 'KB'
    -* Fixed #6750: ActionView::TemplateError (undefined method `empty?' for nil:NilClass) on line #12 of app/views/context_menus/issues.html.erb:
    -
    -== 2010-09-26 v1.0.2
    -
    -* #2285: issue-refinement: pressing enter should result to an "apply"
    -* #3411: Allow mass status update trough context menu
    -* #5929: https-enabled gravatars when called over https
    -* #6189: Japanese Translation for r4011
    -* #6197: Traditional Chinese language file (to r4036)
    -* #6198: Updated german translation
    -* #6208: Macedonian translation
    -* #6210: Swedish Translation for r4039
    -* #6248: nl translation update for r4050
    -* #6263: Catalan translation update
    -* #6275: After submitting a related issue, the Issue field should be re-focused
    -* #6289: Checkboxes in issues list shouldn't be displayed when printing
    -* #6290: Make journals theming easier
    -* #6291: User#allowed_to? is not tested
    -* #6306: Traditional Chinese language file (to r4061)
    -* #6307: Korean translation update for 4066(4061)
    -* #6316: pt_BR update
    -* #6339: SERBIAN Updated
    -* #6358: Updated Polish translation
    -* #6363: Japanese Translation for r4080
    -* #6365: Traditional Chinese language file (to r4081)
    -* #6382: Issue PDF export variable usage
    -* #6428: Interim solution for i18n >= 0.4
    -* #6441: Japanese Translation for r4162
    -* #6451: Traditional Chinese language file (to r4167)
    -* #6465: Japanese Translation for r4171
    -* #6466: Traditional Chinese language file (to r4171)
    -* #6490: pt-BR translation for 1.0.2
    -* Fixed #3935: stylesheet_link_tag with plugin doesn't take into account relative_url_root
    -* Fixed #4998: Global issue list's context menu has enabled options for parent menus but there are no valid selections
    -* Fixed #5170: Done ratio can not revert to 0% if status is used for done ratio
    -* Fixed #5608: broken with i18n 0.4.0
    -* Fixed #6054: Error 500 on filenames with whitespace in git reposities
    -* Fixed #6135: Default logger configuration grows without bound.
    -* Fixed #6191: Deletion of a main task deletes all subtasks
    -* Fixed #6195: Missing move issues between projects
    -* Fixed #6242: can't switch between inline and side-by-side diff
    -* Fixed #6249: Create and continue returns 404
    -* Fixed #6267: changing the authentication mode from ldap to internal with setting the password
    -* Fixed #6270: diff coderay malformed in the "news" page
    -* Fixed #6278: missing "cant_link_an_issue_with_a_descendant"from locale files
    -* Fixed #6333: Create and continue results in a 404 Error
    -* Fixed #6346: Age column on repository view is skewed for git, probably CVS too
    -* Fixed #6351: Context menu on roadmap broken
    -* Fixed #6388: New Subproject leads to a 404
    -* Fixed #6392: Updated/Created links to activity broken
    -* Fixed #6413: Error in SQL
    -* Fixed #6443: Redirect to project settings after Copying a Project
    -* Fixed #6448: Saving a wiki page with no content has a translation missing
    -* Fixed #6452: Unhandled exception on creating File
    -* Fixed #6471: Typo in label_report in Czech translation
    -* Fixed #6479: Changing tracker type will lose watchers
    -* Fixed #6499: Files with leading or trailing whitespace are not shown in git.
    -
    -== 2010-08-22 v1.0.1
    -
    -* #819: Add a body ID and class to all pages
    -* #871: Commit new CSS styles!
    -* #3301: Add favicon to base layout
    -* #4656: On Issue#show page, clicking on “Add related issue� should focus on the input
    -* #4896: Project identifier should be a limited field
    -* #5084: Filter all isssues by projects
    -* #5477: Replace Test::Unit::TestCase with ActiveSupport::TestCase
    -* #5591: 'calendar' action is used with 'issue' controller in issue/sidebar
    -* #5735: Traditional Chinese language file (to r3810)
    -* #5740: Swedish Translation for r3810
    -* #5785: pt-BR translation update
    -* #5898: Projects should be displayed as links in users/memberships
    -* #5910: Chinese translation to redmine-1.0.0
    -* #5912: Translation update for french locale
    -* #5962: Hungarian translation update to r3892
    -* #5971: Remove falsly applied chrome on revision links
    -* #5972: Updated Hebrew translation for 1.0.0
    -* #5982: Updated german translation
    -* #6008: Move admin_menu to Redmine::MenuManager
    -* #6012: RTL layout
    -* #6021: Spanish translation 1.0.0-RC
    -* #6025: nl translation updated for r3905
    -* #6030: Japanese Translation for r3907
    -* #6074: sr-CY.yml contains DOS-type newlines (\r\n)
    -* #6087: SERBIAN translation updated
    -* #6093: Updated italian translation
    -* #6142: Swedish Translation for r3940
    -* #6153: Move view_calendar and view_gantt to own modules
    -* #6169: Add issue status to issue tooltip
    -* Fixed #3834: Add a warning when not choosing a member role
    -* Fixed #3922: Bad english arround "Assigned to" text in journal entries
    -* Fixed #5158: Simplified Chinese language file zh.yml updated to r3608
    -* Fixed #5162: translation missing: zh-TW, field_time_entrie
    -* Fixed #5297: openid not validated correctly
    -* Fixed #5628: Wrong commit range in git log command
    -* Fixed #5760: Assigned_to and author filters in "Projects>View all issues" should be based on user's project visibility
    -* Fixed #5771: Problem when importing git repository
    -* Fixed #5775: ldap authentication in admin menu should have an icon
    -* Fixed #5811: deleting statuses doesnt delete workflow entries
    -* Fixed #5834: Emails with trailing spaces incorrectly detected as invalid
    -* Fixed #5846: ChangeChangesPathLengthLimit does not remove default for MySQL
    -* Fixed #5861: Vertical scrollbar always visible in Wiki "code" blocks in Chrome.
    -* Fixed #5883: correct label_project_latest Chinese translation
    -* Fixed #5892: Changing status from contextual menu opens the ticket instead
    -* Fixed #5904: Global gantt PDF and PNG should display project names
    -* Fixed #5925: parent task's priority edit should be disabled through shortcut menu in issues list page
    -* Fixed #5935: Add Another file to ticket doesn't work in IE Internet Explorer
    -* Fixed #5937: Harmonize french locale "zero" translation with other locales
    -* Fixed #5945: Forum message permalinks don't take pagination into account
    -* Fixed #5978: Debug code still remains
    -* Fixed #6009: When using "English (British)", the repository browser (svn) shows files over 1000 bytes as floating point (2.334355)
    -* Fixed #6045: Repository file Diff view sometimes shows more than selected file
    -* Fixed #6079: German Translation error in TimeEntryActivity
    -* Fixed #6100: User's profile should display all visible projects
    -* Fixed #6132: Allow Key based authentication in the Boards atom feed
    -* Fixed #6163: Bad CSS class for calendar project menu_item
    -* Fixed #6172: Browsing to a missing user's page shows the admin sidebar
    -
    -== 2010-07-18 v1.0.0 (Release candidate)
    -
    -* #443: Adds context menu to the roadmap issue lists
    -* #443: Subtasking
    -* #741: Description preview while editing an issue
    -* #1131: Add support for alternate (non-LDAP) authentication
    -* #1214: REST API for Issues
    -* #1223: File upload on wiki edit form
    -* #1755: add "blocked by" as a related issues option
    -* #2420: Fetching emails from an POP server
    -* #2482: Named scopes in Issue and ActsAsWatchable plus some view refactoring (logic extraction).
    -* #2924: Make the right click menu more discoverable using a cursor property
    -* #2985: Make syntax highlighting pluggable
    -* #3201: Workflow Check/Uncheck All Rows/Columns
    -* #3359: Update CodeRay 0.9
    -* #3706: Allow assigned_to field configuration on Issue creation by email
    -* #3936: configurable list of models to include in search
    -* #4480: Create a link to the user profile from the administration interface
    -* #4482: Cache textile rendering
    -* #4572: Make it harder to ruin your database
    -* #4573: Move github gems to Gemcutter
    -* #4664: Add pagination to forum threads
    -* #4732: Make login case-insensitive also for PostgreSQL
    -* #4812: Create links to other projects
    -* #4819: Replace images with smushed ones for speed
    -* #4945: Allow custom fields attached to project to be searchable
    -* #5121: Fix issues list layout overflow
    -* #5169: Issue list view hook request
    -* #5208: Aibility to edit wiki sidebar
    -* #5281: Remove empty ul tags in the issue history
    -* #5291: Updated basque translations
    -* #5328: Automatically add "Repository" menu_item after repository creation
    -* #5415: Fewer SQL statements generated for watcher_recipients
    -* #5416: Exclude "fields_for" from overridden methods in TabularFormBuilder
    -* #5573: Allow issue assignment in email
    -* #5595: Allow start date and due dates to be set via incoming email
    -* #5752: The projects view (/projects) renders ul's wrong
    -* #5781: Allow to use more macros on the welcome page and project list
    -* Fixed #1288: Unable to past escaped wiki syntax in an issue description
    -* Fixed #1334: Wiki formatting character *_ and _*
    -* Fixed #1416: Inline code with less-then/greater-than produces @lt; and @gt; respectively
    -* Fixed #2473: Login and mail should not be case sensitive
    -* Fixed #2990: Ruby 1.9 - wrong number of arguments (1 for 0) on rake db:migrate
    -* Fixed #3089: Text formatting sometimes breaks when combined
    -* Fixed #3690: Status change info duplicates on the issue screen
    -* Fixed #3691: Redmine allows two files with the same file name to be uploaded to the same issue
    -* Fixed #3764: ApplicationHelperTest fails with JRuby
    -* Fixed #4265: Unclosed code tags in issue descriptions affects main UI
    -* Fixed #4745: Bug in index.xml.builder (issues)
    -* Fixed #4852: changing user/roles of project member not possible without javascript
    -* Fixed #4857: Week number calculation in date picker is wrong if a week starts with Sunday
    -* Fixed #4883: Bottom "contextual" placement in issue with associated changeset
    -* Fixed #4918: Revisions r3453 and r3454 broke On-the-fly user creation with LDAP
    -* Fixed #4935: Navigation to the Master Timesheet page (time_entries)
    -* Fixed #5043: Flash messages are not displayed after the project settings[module/activity] saved
    -* Fixed #5081: Broken links on public/help/wiki_syntax_detailed.html
    -* Fixed #5104: Description of document not wikified on documents index
    -* Fixed #5108: Issue linking fails inside of []s
    -* Fixed #5199: diff code coloring using coderay
    -* Fixed #5233: Add a hook to the issue report (Summary) view
    -* Fixed #5265: timetracking: subtasks time is added to the main task
    -* Fixed #5343: acts_as_event Doesn't Accept Outside URLs
    -* Fixed #5440: UI Inconsistency : Administration > Enumerations table row headers should be enclosed in 
    -* Fixed #5463: 0.9.4 INSTALL and/or UPGRADE, missing session_store.rb
    -* Fixed #5524: Update_parent_attributes doesn't work for the old parent issue when reparenting
    -* Fixed #5548: SVN Repository: Can not list content of a folder which includes square brackets.
    -* Fixed #5589: "with subproject" malfunction
    -* Fixed #5676: Search for Numeric Value
    -* Fixed #5696: Redmine + PostgreSQL 8.4.4 fails on _dir_list_content.rhtml
    -* Fixed #5698: redmine:email:receive_imap fails silently for mails with subject longer than 255 characters
    -* Fixed #5700: TimelogController#destroy assumes success
    -* Fixed #5751: developer role is mispelled
    -* Fixed #5769: Popup Calendar doesn't Advance in Chrome
    -* Fixed #5771: Problem when importing git repository
    -* Fixed #5823: Error in comments in plugin.rb
    -
    -
    -== 2010-07-07 v0.9.6
    -
    -* Fixed: Redmine.pm access by unauthorized users
    -
    -== 2010-06-24 v0.9.5
    -
    -* Linkify folder names on revision view
    -* "fiters" and "options" should be hidden in print view via css
    -* Fixed: NoMethodError when no issue params are submitted
    -* Fixed: projects.atom with required authentication
    -* Fixed: External links not correctly displayed in Wiki TOC
    -* Fixed: Member role forms in project settings are not hidden after member added
    -* Fixed: pre can't be inside p
    -* Fixed: session cookie path does not respect RAILS_RELATIVE_URL_ROOT
    -* Fixed: mail handler fails when the from address is empty
    -
    -
    -== 2010-05-01 v0.9.4
    -
    -* Filters collapsed by default on issues index page for a saved query
    -* Fixed: When categories list is too big the popup menu doesn't adjust (ex. in the issue list)
    -* Fixed: remove "main-menu" div when the menu is empty
    -* Fixed: Code syntax highlighting not working in Document page
    -* Fixed: Git blame/annotate fails on moved files
    -* Fixed: Failing test in test_show_atom
    -* Fixed: Migrate from trac - not displayed Wikis
    -* Fixed: Email notifications on file upload sent to empty recipient list
    -* Fixed: Migrating from trac is not possible, fails to allocate memory
    -* Fixed: Lost password no longer flashes a confirmation message
    -* Fixed: Crash while deleting in-use enumeration
    -* Fixed: Hard coded English string at the selection of issue watchers
    -* Fixed: Bazaar v2.1.0 changed behaviour
    -* Fixed: Roadmap display can raise an exception if no trackers are selected
    -* Fixed: Gravatar breaks layout of "logged in" page
    -* Fixed: Reposman.rb on Windows
    -* Fixed: Possible error 500 while moving an issue to another project with SQLite
    -* Fixed: backslashes in issue description/note should be escaped when quoted
    -* Fixed: Long text in 
     disrupts Associated revisions
    -* Fixed: Links to missing wiki pages not red on project overview page
    -* Fixed: Cannot delete a project with subprojects that shares versions
    -* Fixed: Update of Subversion changesets broken under Solaris
    -* Fixed: "Move issues" permission not working for Non member
    -* Fixed: Sidebar overlap on Users tab of Group editor
    -* Fixed: Error on db:migrate with table prefix set (hardcoded name in principal.rb)
    -* Fixed: Report shows sub-projects for non-members
    -* Fixed: 500 internal error when browsing any Redmine page in epiphany
    -* Fixed: Watchers selection lost when issue creation fails
    -* Fixed: When copying projects, redmine should not generate an email to people who created issues
    -* Fixed: Issue "#" table cells should have a class attribute to enable fine-grained CSS theme
    -* Fixed: Plugin generators should display help if no parameter is given
    -
    -
    -== 2010-02-28 v0.9.3
    -
    -* Adds filter for system shared versions on the cross project issue list
    -* Makes project identifiers searchable
    -* Remove invalid utf8 sequences from commit comments and author name
    -* Fixed: Wrong link when "http" not included in project "Homepage" link
    -* Fixed: Escaping in html email templates
    -* Fixed: Pound (#) followed by number with leading zero (0) removes leading zero when rendered in wiki
    -* Fixed: Deselecting textile text formatting causes interning empty string errors
    -* Fixed: error with postgres when entering a non-numeric id for an issue relation
    -* Fixed: div.task incorrectly wrapping on Gantt Chart
    -* Fixed: Project copy loses wiki pages hierarchy
    -* Fixed: parent project field doesn't include blank value when a member with 'add subproject' permission edits a child project
    -* Fixed: Repository.fetch_changesets tries to fetch changesets for archived projects
    -* Fixed: Duplicated project name for subproject version on gantt chart
    -* Fixed: roadmap shows subprojects issues even if subprojects is unchecked
    -* Fixed: IndexError if all the :last menu items are deleted from a menu
    -* Fixed: Very high CPU usage for a long time when fetching commits from a large Git repository
    -
    -
    -== 2010-02-07 v0.9.2
    -
    -* Fixed: Sub-project repository commits not displayed on parent project issues
    -* Fixed: Potential security leak on my page calendar
    -* Fixed: Project tree structure is broken by deleting the project with the subproject
    -* Fixed: Error message shown duplicated when creating a new group
    -* Fixed: Firefox cuts off large pages
    -* Fixed: Invalid format parameter returns a DoubleRenderError on issues index
    -* Fixed: Unnecessary Quote button on locked forum message
    -* Fixed: Error raised when trying to view the gantt or calendar with a grouped query
    -* Fixed: PDF support for Korean locale
    -* Fixed: Deprecation warning in extra/svn/reposman.rb
    -
    -
    -== 2010-01-30 v0.9.1
    -
    -* Vertical alignment for inline images in formatted text set to 'middle'
    -* Fixed: Redmine.pm error "closing dbh with active statement handles at /usr/lib/perl5/Apache/Redmine.pm"
    -* Fixed: copyright year in footer set to 2010
    -* Fixed: Trac migration script may not output query lines
    -* Fixed: Email notifications may affect language of notice messages on the UI
    -* Fixed: Can not search for 2 letters word
    -* Fixed: Attachments get saved on issue update even if validation fails
    -* Fixed: Tab's 'border-bottom' not absent when selected
    -* Fixed: Issue summary tables that list by user are not sorted
    -* Fixed: Issue pdf export fails if target version is set
    -* Fixed: Issue list export to PDF breaks when issues are sorted by a custom field
    -* Fixed: SQL error when adding a group
    -* Fixes: Min password length during password reset always displays as 4 chars
    -
    -
    -== 2010-01-09 v0.9.0 (Release candidate)
    -
    -* Unlimited subproject nesting
    -* Multiple roles per user per project
    -* User groups
    -* Inheritence of versions
    -* OpenID login
    -* "Watched by me" issue filter
    -* Project copy
    -* Project creation by non admin users
    -* Accept emails from anyone on a private project
    -* Add email notification on Wiki changes
    -* Make issue description non-required field
    -* Custom fields for Versions
    -* Being able to sort the issue list by custom fields
    -* Ability to close versions
    -* User display/editing of custom fields attached to their user profile
    -* Add "follows" issue relation
    -* Copy workflows between trackers and roles
    -* Defaults enabled modules list for project creation
    -* Weighted version completion percentage on the roadmap
    -* Autocreate user account when user submits email that creates new issue
    -* CSS class on overdue issues on the issue list
    -* Enable tracker update on issue edit form
    -* Remove issue watchers
    -* Ability to move threads between project forums
    -* Changed custom field "Possible values" to a textarea
    -* Adds projects association on tracker form
    -* Set session store to cookie store by default
    -* Set a default wiki page on project creation
    -* Roadmap for main project should see Roadmaps for sub projects
    -* Ticket grouping on the issue list
    -* Hierarchical Project links in the page header
    -* Allow My Page blocks to be added to from a plugin
    -* Sort issues by multiple columns
    -* Filters of saved query are now visible and be adjusted without editing the query
    -* Saving "sort order" in custom queries
    -* Url to fetch changesets for a repository
    -* Managers able to create subprojects
    -* Issue Totals on My Page Modules
    -* Convert Enumerations to single table inheritance (STI)
    -* Allow custom my_page blocks to define drop-down names
    -* "View Issues" user permission added
    -* Ask user what to do with child pages when deleting a parent wiki page
    -* Contextual quick search
    -* Allow resending of password by email
    -* Change reply subject to be a link to the reply itself
    -* Include Logged Time as part of the project's Activity history
    -* REST API for authentication
    -* Browse through Git branches
    -* Setup Object Daddy to replace test fixtures
    -* Setup shoulda to make it easier to test
    -* Custom fields and overrides on Enumerations
    -* Add or remove columns from the issue list
    -* Ability to add new version from issues screen
    -* Setting to choose which day calendars start
    -* Asynchronous email delivery method
    -* RESTful URLs for (almost) everything
    -* Include issue status in search results and activity pages
    -* Add email to admin user search filter
    -* Proper content type for plain text mails
    -* Default value of project jump box
    -* Tree based menus
    -* Ability to use issue status to update percent done
    -* Second set of issue "Action Links" at the bottom of an issue page
    -* Proper exist status code for rdm-mailhandler.rb
    -* Remove incoming email body via a delimiter
    -* Fixed: Custom querry 'Export to PDF' ignores field selection
    -* Fixed: Related e-mail notifications aren't threaded
    -* Fixed: No warning when the creation of a categories from the issue form fails
    -* Fixed: Actually block issues from closing when relation 'blocked by' isn't closed
    -* Fixed: Include both first and last name when sorting by users
    -* Fixed: Table cell with multiple line text
    -* Fixed: Project overview page shows disabled trackers
    -* Fixed: Cross project issue relations and user permissions
    -* Fixed: My page shows tickets the user doesn't have access to
    -* Fixed: TOC does not parse wiki page reference links with description
    -* Fixed: Target version-list on bulk edit form is incorrectly sorted
    -* Fixed: Cannot modify/delete project named "Documents"
    -* Fixed: Email address in brackets breaks html
    -* Fixed: Timelog detail loose issue filter passing to report tab
    -* Fixed: Inform about custom field's name maximum length
    -* Fixed: Activity page and Atom feed links contain project id instead of identifier
    -* Fixed: no Atom key for forums with only 1 forum
    -* Fixed: When reading RSS feed in MS Outlook, the inline links are broken.
    -* Fixed: Sometimes new posts don't show up in the topic list of a forum.
    -* Fixed: The all/active filter selection in the project view does not stick.
    -* Fixed: Login box has Different width
    -* Fixed: User removed from project - still getting project update emails
    -* Fixed: Project with the identifier of 'new' cannot be viewed
    -* Fixed: Artefacts in search view (Cyrillic)
    -* Fixed: Allow [#id] as subject to reply by email
    -* Fixed: Wrong language used when closing an issue via a commit message
    -* Fixed: email handler drops emails for new issues with no subject
    -* Fixed: Calendar misspelled under Roles/Permissions
    -* Fixed: Emails from no-reply redmine's address hell cycle
    -* Fixed: child_pages macro fails on wiki page history
    -* Fixed: Pre-filled time tracking date ignores timezone
    -* Fixed: Links on locked users lead to 404 page
    -* Fixed: Page changes in issue-list when using context menu
    -* Fixed: diff parser removes lines starting with multiple dashes
    -* Fixed: Quoting in forums resets message subject
    -* Fixed: Editing issue comment removes quote link
    -* Fixed: Redmine.pm ignore browse_repository permission
    -* Fixed: text formatting breaks on [msg1][msg2]
    -* Fixed: Spent Time Default Value of 0.0
    -* Fixed: Wiki pages in search results are referenced by project number, not by project identifier.
    -* Fixed: When logging in via an autologin cookie the user's last_login_on should be updated
    -* Fixed: 50k users cause problems in project->settings->members screen
    -* Fixed: Document timestamp needs to show updated timestamps
    -* Fixed: Users getting notifications for issues they are no longer allowed to view
    -* Fixed: issue summary counts should link to the issue list without subprojects
    -* Fixed: 'Delete' link on LDAP list has no effect
    -
    -
    -== 2009-11-15 v0.8.7
    -
    -* Fixed: Hide paragraph terminator at the end of headings on html export
    -* Fixed: pre tags containing "
    "+(g[0]>0&&N==g[1]-1?'
    ':""):""),M+=Q}K+=M}return K+=x+($.browser.msie&&parseInt($.browser.version,10)<7&&!a.inline?'':""),a._keyEvent=!1,K},_generateMonthYearHeader:function(a,b,c,d,e,f,g,h){var i=this._get(a,"changeMonth"),j=this._get(a,"changeYear"),k=this._get(a,"showMonthAfterYear"),l='
    ',m="";if(f||!i)m+=''+g[b]+"";else{var n=d&&d.getFullYear()==c,o=e&&e.getFullYear()==c;m+='"}k||(l+=m+(f||!i||!j?" ":""));if(!a.yearshtml){a.yearshtml="";if(f||!j)l+=''+c+"";else{var q=this._get(a,"yearRange").split(":"),r=(new Date).getFullYear(),s=function(a){var b=a.match(/c[+-].*/)?c+parseInt(a.substring(1),10):a.match(/[+-].*/)?r+parseInt(a,10):parseInt(a,10);return isNaN(b)?r:b},t=s(q[0]),u=Math.max(t,s(q[1]||""));t=d?Math.max(t,d.getFullYear()):t,u=e?Math.min(u,e.getFullYear()):u,a.yearshtml+='",l+=a.yearshtml,a.yearshtml=null}}return l+=this._get(a,"yearSuffix"),k&&(l+=(f||!i||!j?" ":"")+m),l+="
    ",l},_adjustInstDate:function(a,b,c){var d=a.drawYear+(c=="Y"?b:0),e=a.drawMonth+(c=="M"?b:0),f=Math.min(a.selectedDay,this._getDaysInMonth(d,e))+(c=="D"?b:0),g=this._restrictMinMax(a,this._daylightSavingAdjust(new Date(d,e,f)));a.selectedDay=g.getDate(),a.drawMonth=a.selectedMonth=g.getMonth(),a.drawYear=a.selectedYear=g.getFullYear(),(c=="M"||c=="Y")&&this._notifyChange(a)},_restrictMinMax:function(a,b){var c=this._getMinMaxDate(a,"min"),d=this._getMinMaxDate(a,"max"),e=c&&bd?d:e,e},_notifyChange:function(a){var b=this._get(a,"onChangeMonthYear");b&&b.apply(a.input?a.input[0]:null,[a.selectedYear,a.selectedMonth+1,a])},_getNumberOfMonths:function(a){var b=this._get(a,"numberOfMonths");return b==null?[1,1]:typeof b=="number"?[1,b]:b},_getMinMaxDate:function(a,b){return this._determineDate(a,this._get(a,b+"Date"),null)},_getDaysInMonth:function(a,b){return 32-this._daylightSavingAdjust(new Date(a,b,32)).getDate()},_getFirstDayOfMonth:function(a,b){return(new Date(a,b,1)).getDay()},_canAdjustMonth:function(a,b,c,d){var e=this._getNumberOfMonths(a),f=this._daylightSavingAdjust(new Date(c,d+(b<0?b:e[0]*e[1]),1));return b<0&&f.setDate(this._getDaysInMonth(f.getFullYear(),f.getMonth())),this._isInRange(a,f)},_isInRange:function(a,b){var c=this._getMinMaxDate(a,"min"),d=this._getMinMaxDate(a,"max");return(!c||b.getTime()>=c.getTime())&&(!d||b.getTime()<=d.getTime())},_getFormatConfig:function(a){var b=this._get(a,"shortYearCutoff");return b=typeof b!="string"?b:(new Date).getFullYear()%100+parseInt(b,10),{shortYearCutoff:b,dayNamesShort:this._get(a,"dayNamesShort"),dayNames:this._get(a,"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,d){b||(a.currentDay=a.selectedDay,a.currentMonth=a.selectedMonth,a.currentYear=a.selectedYear);var e=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(d,c,b)):this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),e,this._getFormatConfig(a))}}),$.fn.datepicker=function(a){if(!this.length)return this;$.datepicker.initialized||($(document).mousedown($.datepicker._checkExternalClick).find("body").append($.datepicker.dpDiv),$.datepicker.initialized=!0);var b=Array.prototype.slice.call(arguments,1);return typeof a!="string"||a!="isDisabled"&&a!="getDate"&&a!="widget"?a=="option"&&arguments.length==2&&typeof arguments[1]=="string"?$.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this[0]].concat(b)):this.each(function(){typeof a=="string"?$.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this].concat(b)):$.datepicker._attachDatepicker(this,a)}):$.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this[0]].concat(b))},$.datepicker=new Datepicker,$.datepicker.initialized=!1,$.datepicker.uuid=(new Date).getTime(),$.datepicker.version="1.8.21",window["DP_jQuery_"+dpuuid]=$})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.ui.progressbar.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){a.widget("ui.progressbar",{options:{value:0,max:100},min:0,_create:function(){this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min,"aria-valuemax":this.options.max,"aria-valuenow":this._value()}),this.valueDiv=a("
    ").appendTo(this.element),this.oldValue=this._value(),this._refreshValue()},destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.valueDiv.remove(),a.Widget.prototype.destroy.apply(this,arguments)},value:function(a){return a===b?this._value():(this._setOption("value",a),this)},_setOption:function(b,c){b==="value"&&(this.options.value=c,this._refreshValue(),this._value()===this.options.max&&this._trigger("complete")),a.Widget.prototype._setOption.apply(this,arguments)},_value:function(){var a=this.options.value;return typeof a!="number"&&(a=0),Math.min(this.options.max,Math.max(this.min,a))},_percentage:function(){return 100*this._value()/this.options.max},_refreshValue:function(){var a=this.value(),b=this._percentage();this.oldValue!==a&&(this.oldValue=a,this._trigger("change")),this.valueDiv.toggle(a>this.min).toggleClass("ui-corner-right",a===this.options.max).width(b.toFixed(0)+"%"),this.element.attr("aria-valuenow",a)}}),a.extend(a.ui.progressbar,{version:"1.8.21"})})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.effects.core.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +jQuery.effects||function(a,b){function c(b){var c;return b&&b.constructor==Array&&b.length==3?b:(c=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(b))?[parseInt(c[1],10),parseInt(c[2],10),parseInt(c[3],10)]:(c=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(b))?[parseFloat(c[1])*2.55,parseFloat(c[2])*2.55,parseFloat(c[3])*2.55]:(c=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(b))?[parseInt(c[1],16),parseInt(c[2],16),parseInt(c[3],16)]:(c=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(b))?[parseInt(c[1]+c[1],16),parseInt(c[2]+c[2],16),parseInt(c[3]+c[3],16)]:(c=/rgba\(0, 0, 0, 0\)/.exec(b))?e.transparent:e[a.trim(b).toLowerCase()]}function d(b,d){var e;do{e=a.curCSS(b,d);if(e!=""&&e!="transparent"||a.nodeName(b,"body"))break;d="backgroundColor"}while(b=b.parentNode);return c(e)}function h(){var a=document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle,b={},c,d;if(a&&a.length&&a[0]&&a[a[0]]){var e=a.length;while(e--)c=a[e],typeof a[c]=="string"&&(d=c.replace(/\-(\w)/g,function(a,b){return b.toUpperCase()}),b[d]=a[c])}else for(c in a)typeof a[c]=="string"&&(b[c]=a[c]);return b}function i(b){var c,d;for(c in b)d=b[c],(d==null||a.isFunction(d)||c in g||/scrollbar/.test(c)||!/color/i.test(c)&&isNaN(parseFloat(d)))&&delete b[c];return b}function j(a,b){var c={_:0},d;for(d in b)a[d]!=b[d]&&(c[d]=b[d]);return c}function k(b,c,d,e){typeof b=="object"&&(e=c,d=null,c=b,b=c.effect),a.isFunction(c)&&(e=c,d=null,c={});if(typeof c=="number"||a.fx.speeds[c])e=d,d=c,c={};return a.isFunction(d)&&(e=d,d=null),c=c||{},d=d||c.duration,d=a.fx.off?0:typeof d=="number"?d:d in a.fx.speeds?a.fx.speeds[d]:a.fx.speeds._default,e=e||c.complete,[b,c,d,e]}function l(b){return!b||typeof b=="number"||a.fx.speeds[b]?!0:typeof b=="string"&&!a.effects[b]?!0:!1}a.effects={},a.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor","borderTopColor","borderColor","color","outlineColor"],function(b,e){a.fx.step[e]=function(a){a.colorInit||(a.start=d(a.elem,e),a.end=c(a.end),a.colorInit=!0),a.elem.style[e]="rgb("+Math.max(Math.min(parseInt(a.pos*(a.end[0]-a.start[0])+a.start[0],10),255),0)+","+Math.max(Math.min(parseInt(a.pos*(a.end[1]-a.start[1])+a.start[1],10),255),0)+","+Math.max(Math.min(parseInt(a.pos*(a.end[2]-a.start[2])+a.start[2],10),255),0)+")"}});var e={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},f=["add","remove","toggle"],g={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};a.effects.animateClass=function(b,c,d,e){return a.isFunction(d)&&(e=d,d=null),this.queue(function(){var g=a(this),k=g.attr("style")||" ",l=i(h.call(this)),m,n=g.attr("class")||"";a.each(f,function(a,c){b[c]&&g[c+"Class"](b[c])}),m=i(h.call(this)),g.attr("class",n),g.animate(j(l,m),{queue:!1,duration:c,easing:d,complete:function(){a.each(f,function(a,c){b[c]&&g[c+"Class"](b[c])}),typeof g.attr("style")=="object"?(g.attr("style").cssText="",g.attr("style").cssText=k):g.attr("style",k),e&&e.apply(this,arguments),a.dequeue(this)}})})},a.fn.extend({_addClass:a.fn.addClass,addClass:function(b,c,d,e){return c?a.effects.animateClass.apply(this,[{add:b},c,d,e]):this._addClass(b)},_removeClass:a.fn.removeClass,removeClass:function(b,c,d,e){return c?a.effects.animateClass.apply(this,[{remove:b},c,d,e]):this._removeClass(b)},_toggleClass:a.fn.toggleClass,toggleClass:function(c,d,e,f,g){return typeof d=="boolean"||d===b?e?a.effects.animateClass.apply(this,[d?{add:c}:{remove:c},e,f,g]):this._toggleClass(c,d):a.effects.animateClass.apply(this,[{toggle:c},d,e,f])},switchClass:function(b,c,d,e,f){return a.effects.animateClass.apply(this,[{add:c,remove:b},d,e,f])}}),a.extend(a.effects,{version:"1.8.21",save:function(a,b){for(var c=0;c
    ").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),e=document.activeElement;try{e.id}catch(f){e=document.body}return b.wrap(d),(b[0]===e||a.contains(b[0],e))&&a(e).focus(),d=b.parent(),b.css("position")=="static"?(d.css({position:"relative"}),b.css({position:"relative"})):(a.extend(c,{position:b.css("position"),zIndex:b.css("z-index")}),a.each(["top","left","bottom","right"],function(a,d){c[d]=b.css(d),isNaN(parseInt(c[d],10))&&(c[d]="auto")}),b.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),d.css(c).show()},removeWrapper:function(b){var c,d=document.activeElement;return b.parent().is(".ui-effects-wrapper")?(c=b.parent().replaceWith(b),(b[0]===d||a.contains(b[0],d))&&a(d).focus(),c):b},setTransition:function(b,c,d,e){return e=e||{},a.each(c,function(a,c){var f=b.cssUnit(c);f[0]>0&&(e[c]=f[0]*d+f[1])}),e}}),a.fn.extend({effect:function(b,c,d,e){var f=k.apply(this,arguments),g={options:f[1],duration:f[2],callback:f[3]},h=g.options.mode,i=a.effects[b];return a.fx.off||!i?h?this[h](g.duration,g.callback):this.each(function(){g.callback&&g.callback.call(this)}):i.call(this,g)},_show:a.fn.show,show:function(a){if(l(a))return this._show.apply(this,arguments);var b=k.apply(this,arguments);return b[1].mode="show",this.effect.apply(this,b)},_hide:a.fn.hide,hide:function(a){if(l(a))return this._hide.apply(this,arguments);var b=k.apply(this,arguments);return b[1].mode="hide",this.effect.apply(this,b)},__toggle:a.fn.toggle,toggle:function(b){if(l(b)||typeof b=="boolean"||a.isFunction(b))return this.__toggle.apply(this,arguments);var c=k.apply(this,arguments);return c[1].mode="toggle",this.effect.apply(this,c)},cssUnit:function(b){var c=this.css(b),d=[];return a.each(["em","px","%","pt"],function(a,b){c.indexOf(b)>0&&(d=[parseFloat(c),b])}),d}}),a.easing.jswing=a.easing.swing,a.extend(a.easing,{def:"easeOutQuad",swing:function(b,c,d,e,f){return a.easing[a.easing.def](b,c,d,e,f)},easeInQuad:function(a,b,c,d,e){return d*(b/=e)*b+c},easeOutQuad:function(a,b,c,d,e){return-d*(b/=e)*(b-2)+c},easeInOutQuad:function(a,b,c,d,e){return(b/=e/2)<1?d/2*b*b+c:-d/2*(--b*(b-2)-1)+c},easeInCubic:function(a,b,c,d,e){return d*(b/=e)*b*b+c},easeOutCubic:function(a,b,c,d,e){return d*((b=b/e-1)*b*b+1)+c},easeInOutCubic:function(a,b,c,d,e){return(b/=e/2)<1?d/2*b*b*b+c:d/2*((b-=2)*b*b+2)+c},easeInQuart:function(a,b,c,d,e){return d*(b/=e)*b*b*b+c},easeOutQuart:function(a,b,c,d,e){return-d*((b=b/e-1)*b*b*b-1)+c},easeInOutQuart:function(a,b,c,d,e){return(b/=e/2)<1?d/2*b*b*b*b+c:-d/2*((b-=2)*b*b*b-2)+c},easeInQuint:function(a,b,c,d,e){return d*(b/=e)*b*b*b*b+c},easeOutQuint:function(a,b,c,d,e){return d*((b=b/e-1)*b*b*b*b+1)+c},easeInOutQuint:function(a,b,c,d,e){return(b/=e/2)<1?d/2*b*b*b*b*b+c:d/2*((b-=2)*b*b*b*b+2)+c},easeInSine:function(a,b,c,d,e){return-d*Math.cos(b/e*(Math.PI/2))+d+c},easeOutSine:function(a,b,c,d,e){return d*Math.sin(b/e*(Math.PI/2))+c},easeInOutSine:function(a,b,c,d,e){return-d/2*(Math.cos(Math.PI*b/e)-1)+c},easeInExpo:function(a,b,c,d,e){return b==0?c:d*Math.pow(2,10*(b/e-1))+c},easeOutExpo:function(a,b,c,d,e){return b==e?c+d:d*(-Math.pow(2,-10*b/e)+1)+c},easeInOutExpo:function(a,b,c,d,e){return b==0?c:b==e?c+d:(b/=e/2)<1?d/2*Math.pow(2,10*(b-1))+c:d/2*(-Math.pow(2,-10*--b)+2)+c},easeInCirc:function(a,b,c,d,e){return-d*(Math.sqrt(1-(b/=e)*b)-1)+c},easeOutCirc:function(a,b,c,d,e){return d*Math.sqrt(1-(b=b/e-1)*b)+c},easeInOutCirc:function(a,b,c,d,e){return(b/=e/2)<1?-d/2*(Math.sqrt(1-b*b)-1)+c:d/2*(Math.sqrt(1-(b-=2)*b)+1)+c},easeInElastic:function(a,b,c,d,e){var f=1.70158,g=0,h=d;if(b==0)return c;if((b/=e)==1)return c+d;g||(g=e*.3);if(h").css({position:"absolute",visibility:"visible",left:-j*(g/d),top:-i*(h/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:g/d,height:h/c,left:f.left+j*(g/d)+(b.options.mode=="show"?(j-Math.floor(d/2))*(g/d):0),top:f.top+i*(h/c)+(b.options.mode=="show"?(i-Math.floor(c/2))*(h/c):0),opacity:b.options.mode=="show"?0:1}).animate({left:f.left+j*(g/d)+(b.options.mode=="show"?0:(j-Math.floor(d/2))*(g/d)),top:f.top+i*(h/c)+(b.options.mode=="show"?0:(i-Math.floor(c/2))*(h/c)),opacity:b.options.mode=="show"?1:0},b.duration||500);setTimeout(function(){b.options.mode=="show"?e.css({visibility:"visible"}):e.css({visibility:"visible"}).hide(),b.callback&&b.callback.apply(e[0]),e.dequeue(),a("div.ui-effects-explode").remove()},b.duration||500)})}})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.effects.fade.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){a.effects.fade=function(b){return this.queue(function(){var c=a(this),d=a.effects.setMode(c,b.options.mode||"hide");c.animate({opacity:d},{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.effects.fold.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){a.effects.fold=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"hide"),f=b.options.size||15,g=!!b.options.horizFirst,h=b.duration?b.duration/2:a.fx.speeds._default/2;a.effects.save(c,d),c.show();var i=a.effects.createWrapper(c).css({overflow:"hidden"}),j=e=="show"!=g,k=j?["width","height"]:["height","width"],l=j?[i.width(),i.height()]:[i.height(),i.width()],m=/([0-9]+)%/.exec(f);m&&(f=parseInt(m[1],10)/100*l[e=="hide"?0:1]),e=="show"&&i.css(g?{height:0,width:f}:{height:f,width:0});var n={},p={};n[k[0]]=e=="show"?l[0]:f,p[k[1]]=e=="show"?l[1]:0,i.animate(n,h,b.options.easing).animate(p,h,b.options.easing,function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(c[0],arguments),c.dequeue()})})}})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.effects.highlight.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){a.effects.highlight=function(b){return this.queue(function(){var c=a(this),d=["backgroundImage","backgroundColor","opacity"],e=a.effects.setMode(c,b.options.mode||"show"),f={backgroundColor:c.css("backgroundColor")};e=="hide"&&(f.opacity=0),a.effects.save(c,d),c.show().css({backgroundImage:"none",backgroundColor:b.options.color||"#ffff99"}).animate(f,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){e=="hide"&&c.hide(),a.effects.restore(c,d),e=="show"&&!a.support.opacity&&this.style.removeAttribute("filter"),b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.effects.pulsate.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){a.effects.pulsate=function(b){return this.queue(function(){var c=a(this),d=a.effects.setMode(c,b.options.mode||"show"),e=(b.options.times||5)*2-1,f=b.duration?b.duration/2:a.fx.speeds._default/2,g=c.is(":visible"),h=0;g||(c.css("opacity",0).show(),h=1),(d=="hide"&&g||d=="show"&&!g)&&e--;for(var i=0;i').appendTo(document.body).addClass(b.options.className).css({top:g.top,left:g.left,height:c.innerHeight(),width:c.innerWidth(),position:"absolute"}).animate(f,b.duration,b.options.easing,function(){h.remove(),b.callback&&b.callback.apply(c[0],arguments),c.dequeue()})})}})(jQuery);; + +/* JQuery UJS 2.0.3 */ +(function(a,b){var c=function(){var b=a(document).data("events");return b&&b.click&&a.grep(b.click,function(a){return a.namespace==="rails"}).length};if(c()){a.error("jquery-ujs has already been loaded!")}var d;a.rails=d={linkClickSelector:"a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]",inputChangeSelector:"select[data-remote], input[data-remote], textarea[data-remote]",formSubmitSelector:"form",formInputClickSelector:"form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])",disableSelector:"input[data-disable-with], button[data-disable-with], textarea[data-disable-with]",enableSelector:"input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled",requiredInputSelector:"input[name][required]:not([disabled]),textarea[name][required]:not([disabled])",fileInputSelector:"input:file",linkDisableSelector:"a[data-disable-with]",CSRFProtection:function(b){var c=a('meta[name="csrf-token"]').attr("content");if(c)b.setRequestHeader("X-CSRF-Token",c)},fire:function(b,c,d){var e=a.Event(c);b.trigger(e,d);return e.result!==false},confirm:function(a){return confirm(a)},ajax:function(b){return a.ajax(b)},href:function(a){return a.attr("href")},handleRemote:function(c){var e,f,g,h,i,j,k,l;if(d.fire(c,"ajax:before")){h=c.data("cross-domain");i=h===b?null:h;j=c.data("with-credentials")||null;k=c.data("type")||a.ajaxSettings&&a.ajaxSettings.dataType;if(c.is("form")){e=c.attr("method");f=c.attr("action");g=c.serializeArray();var m=c.data("ujs:submit-button");if(m){g.push(m);c.data("ujs:submit-button",null)}}else if(c.is(d.inputChangeSelector)){e=c.data("method");f=c.data("url");g=c.serialize();if(c.data("params"))g=g+"&"+c.data("params")}else{e=c.data("method");f=d.href(c);g=c.data("params")||null}l={type:e||"GET",data:g,dataType:k,beforeSend:function(a,e){if(e.dataType===b){a.setRequestHeader("accept","*/*;q=0.5, "+e.accepts.script)}return d.fire(c,"ajax:beforeSend",[a,e])},success:function(a,b,d){c.trigger("ajax:success",[a,b,d])},complete:function(a,b){c.trigger("ajax:complete",[a,b])},error:function(a,b,d){c.trigger("ajax:error",[a,b,d])},xhrFields:{withCredentials:j},crossDomain:i};if(f){l.url=f}var n=d.ajax(l);c.trigger("ajax:send",n);return n}else{return false}},handleMethod:function(c){var e=d.href(c),f=c.data("method"),g=c.attr("target"),h=a("meta[name=csrf-token]").attr("content"),i=a("meta[name=csrf-param]").attr("content"),j=a('
    '),k='';if(i!==b&&h!==b){k+=''}if(g){j.attr("target",g)}j.hide().append(k).appendTo("body");j.submit()},disableFormElements:function(b){b.find(d.disableSelector).each(function(){var b=a(this),c=b.is("button")?"html":"val";b.data("ujs:enable-with",b[c]());b[c](b.data("disable-with"));b.prop("disabled",true)})},enableFormElements:function(b){b.find(d.enableSelector).each(function(){var b=a(this),c=b.is("button")?"html":"val";if(b.data("ujs:enable-with"))b[c](b.data("ujs:enable-with"));b.prop("disabled",false)})},allowAction:function(a){var b=a.data("confirm"),c=false,e;if(!b){return true}if(d.fire(a,"confirm")){c=d.confirm(b);e=d.fire(a,"confirm:complete",[c])}return c&&e},blankInputs:function(b,c,d){var e=a(),f,g,h=c||"input,textarea";b.find(h).each(function(){f=a(this);g=f.is(":checkbox,:radio")?f.is(":checked"):f.val();if(g==!!d){e=e.add(f)}});return e.length?e:false},nonBlankInputs:function(a,b){return d.blankInputs(a,b,true)},stopEverything:function(b){a(b.target).trigger("ujs:everythingStopped");b.stopImmediatePropagation();return false},callFormSubmitBindings:function(c,d){var e=c.data("events"),f=true;if(e!==b&&e["submit"]!==b){a.each(e["submit"],function(a,b){if(typeof b.handler==="function")return f=b.handler(d)})}return f},disableElement:function(a){a.data("ujs:enable-with",a.html());a.html(a.data("disable-with"));a.bind("click.railsDisable",function(a){return d.stopEverything(a)})},enableElement:function(a){if(a.data("ujs:enable-with")!==b){a.html(a.data("ujs:enable-with"));a.data("ujs:enable-with",false)}a.unbind("click.railsDisable")}};if(d.fire(a(document),"rails:attachBindings")){a.ajaxPrefilter(function(a,b,c){if(!a.crossDomain){d.CSRFProtection(c)}});a(document).delegate(d.linkDisableSelector,"ajax:complete",function(){d.enableElement(a(this))});a(document).delegate(d.linkClickSelector,"click.rails",function(c){var e=a(this),f=e.data("method"),g=e.data("params");if(!d.allowAction(e))return d.stopEverything(c);if(e.is(d.linkDisableSelector))d.disableElement(e);if(e.data("remote")!==b){if((c.metaKey||c.ctrlKey)&&(!f||f==="GET")&&!g){return true}if(d.handleRemote(e)===false){d.enableElement(e)}return false}else if(e.data("method")){d.handleMethod(e);return false}});a(document).delegate(d.inputChangeSelector,"change.rails",function(b){var c=a(this);if(!d.allowAction(c))return d.stopEverything(b);d.handleRemote(c);return false});a(document).delegate(d.formSubmitSelector,"submit.rails",function(c){var e=a(this),f=e.data("remote")!==b,g=d.blankInputs(e,d.requiredInputSelector),h=d.nonBlankInputs(e,d.fileInputSelector);if(!d.allowAction(e))return d.stopEverything(c);if(g&&e.attr("novalidate")==b&&d.fire(e,"ajax:aborted:required",[g])){return d.stopEverything(c)}if(f){if(h){setTimeout(function(){d.disableFormElements(e)},13);return d.fire(e,"ajax:aborted:file",[h])}if(!a.support.submitBubbles&&a().jquery<"1.7"&&d.callFormSubmitBindings(e,c)===false)return d.stopEverything(c);d.handleRemote(e);return false}else{setTimeout(function(){d.disableFormElements(e)},13)}});a(document).delegate(d.formInputClickSelector,"click.rails",function(b){var c=a(this);if(!d.allowAction(c))return d.stopEverything(b);var e=c.attr("name"),f=e?{name:e,value:c.val()}:null;c.closest("form").data("ujs:submit-button",f)});a(document).delegate(d.formSubmitSelector,"ajax:beforeSend.rails",function(b){if(this==b.target)d.disableFormElements(a(this))});a(document).delegate(d.formSubmitSelector,"ajax:complete.rails",function(b){if(this==b.target)d.enableFormElements(a(this))});a(function(){csrf_token=a("meta[name=csrf-token]").attr("content");csrf_param=a("meta[name=csrf-param]").attr("content");a('form input[name="'+csrf_param+'"]').val(csrf_token)})}})(jQuery) diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/62/62c4b160302c81fa7e92e1eaccb2050c921551bd.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/62/62c4b160302c81fa7e92e1eaccb2050c921551bd.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,27 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class IssueCustomField < CustomField + has_and_belongs_to_many :projects, :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :foreign_key => "custom_field_id" + has_and_belongs_to_many :trackers, :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :foreign_key => "custom_field_id" + has_many :issues, :through => :issue_custom_values + + def type_name + :label_issue_plural + end +end + diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/62/62dfb084268adacb34dd2413eee0ac2877c9ac56.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/62/62dfb084268adacb34dd2413eee0ac2877c9ac56.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,6 @@ +

    <%= l(:label_spent_time) %>

    + +<%= labelled_form_for @time_entry, :url => project_time_entry_path(@time_entry.project, @time_entry) do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_save) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/63/630d406caecc95039e8e8128a0f4598a5d568098.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/63/630d406caecc95039e8e8128a0f4598a5d568098.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,19 @@ +

    <%= link_to l(:label_custom_field_plural), :controller => 'custom_fields', :action => 'index' %> + » <%= link_to l(@custom_field.type_name), :controller => 'custom_fields', :action => 'index', :tab => @custom_field.class.name %> + » <%= l(:label_custom_field_new) %>

    + +<%= labelled_form_for :custom_field, @custom_field, :url => custom_fields_path, :html => {:id => 'custom_field_form'} do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= hidden_field_tag 'type', @custom_field.type %> +<%= submit_tag l(:button_save) %> +<% end %> + +<%= javascript_tag do %> +$('#custom_field_field_format').change(function(){ + $.ajax({ + url: '<%= new_custom_field_path(:format => 'js') %>', + type: 'get', + data: $('#custom_field_form').serialize() + }); +}); +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/63/6333ad0c337c31df3be9da7c68a4f1ab890059c1.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/63/6333ad0c337c31df3be9da7c68a4f1ab890059c1.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,49 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class CustomValue < ActiveRecord::Base + belongs_to :custom_field + belongs_to :customized, :polymorphic => true + + def initialize(attributes=nil, *args) + super + if new_record? && custom_field && (customized_type.blank? || (customized && customized.new_record?)) + self.value ||= custom_field.default_value + end + end + + # Returns true if the boolean custom value is true + def true? + self.value == '1' + end + + def editable? + custom_field.editable? + end + + def visible? + custom_field.visible? + end + + def required? + custom_field.is_required? + end + + def to_s + value.to_s + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/63/6360196bfabbf1ba6bec0417e53823e42158b575.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/63/6360196bfabbf1ba6bec0417e53823e42158b575.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,130 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class GroupTest < ActiveSupport::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :workflows, + :groups_users + + include Redmine::I18n + + def test_create + g = Group.new(:name => 'New group') + assert g.save + g.reload + assert_equal 'New group', g.name + end + + def test_blank_name_error_message + set_language_if_valid 'en' + g = Group.new + assert !g.save + assert_include "Name can't be blank", g.errors.full_messages + end + + def test_blank_name_error_message_fr + set_language_if_valid 'fr' + str = "Nom doit \xc3\xaatre renseign\xc3\xa9(e)" + str.force_encoding('UTF-8') if str.respond_to?(:force_encoding) + g = Group.new + assert !g.save + assert_include str, g.errors.full_messages + end + + def test_group_roles_should_be_given_to_added_user + group = Group.find(11) + user = User.find(9) + project = Project.first + + Member.create!(:principal => group, :project => project, :role_ids => [1, 2]) + group.users << user + assert user.member_of?(project) + end + + def test_new_roles_should_be_given_to_existing_user + group = Group.find(11) + user = User.find(9) + project = Project.first + + group.users << user + m = Member.create!(:principal => group, :project => project, :role_ids => [1, 2]) + assert user.member_of?(project) + end + + def test_user_roles_should_updated_when_updating_user_ids + group = Group.find(11) + user = User.find(9) + project = Project.first + + Member.create!(:principal => group, :project => project, :role_ids => [1, 2]) + group.user_ids = [user.id] + group.save! + assert User.find(9).member_of?(project) + + group.user_ids = [1] + group.save! + assert !User.find(9).member_of?(project) + end + + def test_user_roles_should_updated_when_updating_group_roles + group = Group.find(11) + user = User.find(9) + project = Project.first + group.users << user + m = Member.create!(:principal => group, :project => project, :role_ids => [1]) + assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort + + m.role_ids = [1, 2] + assert_equal [1, 2], user.reload.roles_for_project(project).collect(&:id).sort + + m.role_ids = [2] + assert_equal [2], user.reload.roles_for_project(project).collect(&:id).sort + + m.role_ids = [1] + assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort + end + + def test_user_memberships_should_be_removed_when_removing_group_membership + assert User.find(8).member_of?(Project.find(5)) + Member.find_by_project_id_and_user_id(5, 10).destroy + assert !User.find(8).member_of?(Project.find(5)) + end + + def test_user_roles_should_be_removed_when_removing_user_from_group + assert User.find(8).member_of?(Project.find(5)) + User.find(8).groups = [] + assert !User.find(8).member_of?(Project.find(5)) + end + + def test_destroy_should_unassign_issues + group = Group.first + Issue.update_all(["assigned_to_id = ?", group.id], 'id = 1') + + assert group.destroy + assert group.destroyed? + + assert_equal nil, Issue.find(1).assigned_to_id + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/63/638f9e19fd163d15dde9713bbaf44b74fb5217b7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/63/638f9e19fd163d15dde9713bbaf44b74fb5217b7.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1107 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class QueryColumn + attr_accessor :name, :sortable, :groupable, :default_order + include Redmine::I18n + + def initialize(name, options={}) + self.name = name + self.sortable = options[:sortable] + self.groupable = options[:groupable] || false + if groupable == true + self.groupable = name.to_s + end + self.default_order = options[:default_order] + @inline = options.key?(:inline) ? options[:inline] : true + @caption_key = options[:caption] || "field_#{name}" + end + + def caption + l(@caption_key) + end + + # Returns true if the column is sortable, otherwise false + def sortable? + !@sortable.nil? + end + + def sortable + @sortable.is_a?(Proc) ? @sortable.call : @sortable + end + + def inline? + @inline + end + + def value(issue) + issue.send name + end + + def css_classes + name + end +end + +class QueryCustomFieldColumn < QueryColumn + + def initialize(custom_field) + self.name = "cf_#{custom_field.id}".to_sym + self.sortable = custom_field.order_statement || false + self.groupable = custom_field.group_statement || false + @inline = true + @cf = custom_field + end + + def caption + @cf.name + end + + def custom_field + @cf + end + + def value(issue) + cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)} + cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first + end + + def css_classes + @css_classes ||= "#{name} #{@cf.field_format}" + end +end + +class Query < ActiveRecord::Base + class StatementInvalid < ::ActiveRecord::StatementInvalid + end + + belongs_to :project + belongs_to :user + serialize :filters + serialize :column_names + serialize :sort_criteria, Array + + attr_protected :project_id, :user_id + + validates_presence_of :name + validates_length_of :name, :maximum => 255 + validate :validate_query_filters + + @@operators = { "=" => :label_equals, + "!" => :label_not_equals, + "o" => :label_open_issues, + "c" => :label_closed_issues, + "!*" => :label_none, + "*" => :label_any, + ">=" => :label_greater_or_equal, + "<=" => :label_less_or_equal, + "><" => :label_between, + " :label_in_less_than, + ">t+" => :label_in_more_than, + "> :label_in_the_next_days, + "t+" => :label_in, + "t" => :label_today, + "w" => :label_this_week, + ">t-" => :label_less_than_ago, + " :label_more_than_ago, + "> :label_in_the_past_days, + "t-" => :label_ago, + "~" => :label_contains, + "!~" => :label_not_contains, + "=p" => :label_any_issues_in_project, + "=!p" => :label_any_issues_not_in_project, + "!p" => :label_no_issues_in_project} + + cattr_reader :operators + + @@operators_by_filter_type = { :list => [ "=", "!" ], + :list_status => [ "o", "=", "!", "c", "*" ], + :list_optional => [ "=", "!", "!*", "*" ], + :list_subprojects => [ "*", "!*", "=" ], + :date => [ "=", ">=", "<=", "><", "t+", ">t-", " [ "=", ">=", "<=", "><", ">t-", " [ "=", "~", "!", "!~", "!*", "*" ], + :text => [ "~", "!~", "!*", "*" ], + :integer => [ "=", ">=", "<=", "><", "!*", "*" ], + :float => [ "=", ">=", "<=", "><", "!*", "*" ], + :relation => ["=", "=p", "=!p", "!p", "!*", "*"]} + + cattr_reader :operators_by_filter_type + + @@available_columns = [ + QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true), + QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true), + QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue), + QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true), + QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true), + QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"), + QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true), + QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true), + QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'), + QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true), + QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true), + QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), + QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), + QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), + QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), + QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), + QueryColumn.new(:relations, :caption => :label_related_issues), + QueryColumn.new(:description, :inline => false) + ] + cattr_reader :available_columns + + scope :visible, lambda {|*args| + user = args.shift || User.current + base = Project.allowed_to_condition(user, :view_issues, *args) + user_id = user.logged? ? user.id : 0 + { + :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id], + :include => :project + } + } + + def initialize(attributes=nil, *args) + super attributes + self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } + @is_for_all = project.nil? + end + + def validate_query_filters + filters.each_key do |field| + if values_for(field) + case type_for(field) + when :integer + add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) } + when :float + add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) } + when :date, :date_past + case operator_for(field) + when "=", ">=", "<=", "><" + add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) } + when ">t-", "t+", " 'activerecord.errors.messages') + errors.add(:base, m) + end + + # Returns true if the query is visible to +user+ or the current user. + def visible?(user=User.current) + (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id) + end + + def editable_by?(user) + return false unless user + # Admin can edit them all and regular users can edit their private queries + return true if user.admin? || (!is_public && self.user_id == user.id) + # Members can not edit public queries that are for all project (only admin is allowed to) + is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project) + end + + def trackers + @trackers ||= project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers + end + + # Returns a hash of localized labels for all filter operators + def self.operators_labels + operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h} + end + + def available_filters + return @available_filters if @available_filters + @available_filters = { + "status_id" => { + :type => :list_status, :order => 0, + :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } + }, + "tracker_id" => { + :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } + }, + "priority_id" => { + :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } + }, + "subject" => { :type => :text, :order => 8 }, + "created_on" => { :type => :date_past, :order => 9 }, + "updated_on" => { :type => :date_past, :order => 10 }, + "start_date" => { :type => :date, :order => 11 }, + "due_date" => { :type => :date, :order => 12 }, + "estimated_hours" => { :type => :float, :order => 13 }, + "done_ratio" => { :type => :integer, :order => 14 } + } + IssueRelation::TYPES.each do |relation_type, options| + @available_filters[relation_type] = { + :type => :relation, :order => @available_filters.size + 100, + :label => options[:name] + } + end + principals = [] + if project + principals += project.principals.sort + unless project.leaf? + subprojects = project.descendants.visible.all + if subprojects.any? + @available_filters["subproject_id"] = { + :type => :list_subprojects, :order => 13, + :values => subprojects.collect{|s| [s.name, s.id.to_s] } + } + principals += Principal.member_of(subprojects) + end + end + else + if all_projects.any? + # members of visible projects + principals += Principal.member_of(all_projects) + # project filter + project_values = [] + if User.current.logged? && User.current.memberships.any? + project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"] + end + project_values += all_projects_values + @available_filters["project_id"] = { + :type => :list, :order => 1, :values => project_values + } unless project_values.empty? + end + end + principals.uniq! + principals.sort! + users = principals.select {|p| p.is_a?(User)} + + assigned_to_values = [] + assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? + assigned_to_values += (Setting.issue_group_assignment? ? + principals : users).collect{|s| [s.name, s.id.to_s] } + @available_filters["assigned_to_id"] = { + :type => :list_optional, :order => 4, :values => assigned_to_values + } unless assigned_to_values.empty? + + author_values = [] + author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? + author_values += users.collect{|s| [s.name, s.id.to_s] } + @available_filters["author_id"] = { + :type => :list, :order => 5, :values => author_values + } unless author_values.empty? + + group_values = Group.all.collect {|g| [g.name, g.id.to_s] } + @available_filters["member_of_group"] = { + :type => :list_optional, :order => 6, :values => group_values + } unless group_values.empty? + + role_values = Role.givable.collect {|r| [r.name, r.id.to_s] } + @available_filters["assigned_to_role"] = { + :type => :list_optional, :order => 7, :values => role_values + } unless role_values.empty? + + if User.current.logged? + @available_filters["watcher_id"] = { + :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] + } + end + + if project + # project specific filters + categories = project.issue_categories.all + unless categories.empty? + @available_filters["category_id"] = { + :type => :list_optional, :order => 6, + :values => categories.collect{|s| [s.name, s.id.to_s] } + } + end + versions = project.shared_versions.all + unless versions.empty? + @available_filters["fixed_version_id"] = { + :type => :list_optional, :order => 7, + :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } + } + end + add_custom_fields_filters(project.all_issue_custom_fields) + else + # global filters for cross project issue list + system_shared_versions = Version.visible.find_all_by_sharing('system') + unless system_shared_versions.empty? + @available_filters["fixed_version_id"] = { + :type => :list_optional, :order => 7, + :values => system_shared_versions.sort.collect{|s| + ["#{s.project.name} - #{s.name}", s.id.to_s] + } + } + end + add_custom_fields_filters( + IssueCustomField.find(:all, + :conditions => { + :is_filter => true, + :is_for_all => true + })) + end + add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version + if User.current.allowed_to?(:set_issues_private, nil, :global => true) || + User.current.allowed_to?(:set_own_issues_private, nil, :global => true) + @available_filters["is_private"] = { + :type => :list, :order => 16, + :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] + } + end + Tracker.disabled_core_fields(trackers).each {|field| + @available_filters.delete field + } + @available_filters.each do |field, options| + options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, '')) + end + @available_filters + end + + # Returns a representation of the available filters for JSON serialization + def available_filters_as_json + json = {} + available_filters.each do |field, options| + json[field] = options.slice(:type, :name, :values).stringify_keys + end + json + end + + def all_projects + @all_projects ||= Project.visible.all + end + + def all_projects_values + return @all_projects_values if @all_projects_values + + values = [] + Project.project_tree(all_projects) do |p, level| + prefix = (level > 0 ? ('--' * level + ' ') : '') + values << ["#{prefix}#{p.name}", p.id.to_s] + end + @all_projects_values = values + end + + def add_filter(field, operator, values) + # values must be an array + return unless values.nil? || values.is_a?(Array) + # check if field is defined as an available filter + if available_filters.has_key? field + filter_options = available_filters[field] + # check if operator is allowed for that filter + #if @@operators_by_filter_type[filter_options[:type]].include? operator + # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]}) + # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator + #end + filters[field] = {:operator => operator, :values => (values || [''])} + end + end + + def add_short_filter(field, expression) + return unless expression && available_filters.has_key?(field) + field_type = available_filters[field][:type] + @@operators_by_filter_type[field_type].sort.reverse.detect do |operator| + next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/ + add_filter field, operator, $1.present? ? $1.split('|') : [''] + end || add_filter(field, '=', expression.split('|')) + end + + # Add multiple filters using +add_filter+ + def add_filters(fields, operators, values) + if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash)) + fields.each do |field| + add_filter(field, operators[field], values && values[field]) + end + end + end + + def has_filter?(field) + filters and filters[field] + end + + def type_for(field) + available_filters[field][:type] if available_filters.has_key?(field) + end + + def operator_for(field) + has_filter?(field) ? filters[field][:operator] : nil + end + + def values_for(field) + has_filter?(field) ? filters[field][:values] : nil + end + + def value_for(field, index=0) + (values_for(field) || [])[index] + end + + def label_for(field) + label = available_filters[field][:name] if available_filters.has_key?(field) + label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field) + end + + def available_columns + return @available_columns if @available_columns + @available_columns = ::Query.available_columns.dup + @available_columns += (project ? + project.all_issue_custom_fields : + IssueCustomField.find(:all) + ).collect {|cf| QueryCustomFieldColumn.new(cf) } + + if User.current.allowed_to?(:view_time_entries, project, :global => true) + index = nil + @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours} + index = (index ? index + 1 : -1) + # insert the column after estimated_hours or at the end + @available_columns.insert index, QueryColumn.new(:spent_hours, + :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)", + :default_order => 'desc', + :caption => :label_spent_time + ) + end + + if User.current.allowed_to?(:set_issues_private, nil, :global => true) || + User.current.allowed_to?(:set_own_issues_private, nil, :global => true) + @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private") + end + + disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')} + @available_columns.reject! {|column| + disabled_fields.include?(column.name.to_s) + } + + @available_columns + end + + def self.available_columns=(v) + self.available_columns = (v) + end + + def self.add_available_column(column) + self.available_columns << (column) if column.is_a?(QueryColumn) + end + + # Returns an array of columns that can be used to group the results + def groupable_columns + available_columns.select {|c| c.groupable} + end + + # Returns a Hash of columns and the key for sorting + def sortable_columns + {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column| + h[column.name.to_s] = column.sortable + h + }) + end + + def columns + # preserve the column_names order + (has_default_columns? ? default_columns_names : column_names).collect do |name| + available_columns.find { |col| col.name == name } + end.compact + end + + def inline_columns + columns.select(&:inline?) + end + + def block_columns + columns.reject(&:inline?) + end + + def available_inline_columns + available_columns.select(&:inline?) + end + + def available_block_columns + available_columns.reject(&:inline?) + end + + def default_columns_names + @default_columns_names ||= begin + default_columns = Setting.issue_list_default_columns.map(&:to_sym) + + project.present? ? default_columns : [:project] | default_columns + end + end + + def column_names=(names) + if names + names = names.select {|n| n.is_a?(Symbol) || !n.blank? } + names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } + # Set column_names to nil if default columns + if names == default_columns_names + names = nil + end + end + write_attribute(:column_names, names) + end + + def has_column?(column) + column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column) + end + + def has_default_columns? + column_names.nil? || column_names.empty? + end + + def sort_criteria=(arg) + c = [] + if arg.is_a?(Hash) + arg = arg.keys.sort.collect {|k| arg[k]} + end + c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']} + write_attribute(:sort_criteria, c) + end + + def sort_criteria + read_attribute(:sort_criteria) || [] + end + + def sort_criteria_key(arg) + sort_criteria && sort_criteria[arg] && sort_criteria[arg].first + end + + def sort_criteria_order(arg) + sort_criteria && sort_criteria[arg] && sort_criteria[arg].last + end + + def sort_criteria_order_for(key) + sort_criteria.detect {|k, order| key.to_s == k}.try(:last) + end + + # Returns the SQL sort order that should be prepended for grouping + def group_by_sort_order + if grouped? && (column = group_by_column) + order = sort_criteria_order_for(column.name) || column.default_order + column.sortable.is_a?(Array) ? + column.sortable.collect {|s| "#{s} #{order}"}.join(',') : + "#{column.sortable} #{order}" + end + end + + # Returns true if the query is a grouped query + def grouped? + !group_by_column.nil? + end + + def group_by_column + groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by} + end + + def group_by_statement + group_by_column.try(:groupable) + end + + def project_statement + project_clauses = [] + if project && !project.descendants.active.empty? + ids = [project.id] + if has_filter?("subproject_id") + case operator_for("subproject_id") + when '=' + # include the selected subprojects + ids += values_for("subproject_id").each(&:to_i) + when '!*' + # main project only + else + # all subprojects + ids += project.descendants.collect(&:id) + end + elsif Setting.display_subprojects_issues? + ids += project.descendants.collect(&:id) + end + project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') + elsif project + project_clauses << "#{Project.table_name}.id = %d" % project.id + end + project_clauses.any? ? project_clauses.join(' AND ') : nil + end + + def statement + # filters clauses + filters_clauses = [] + filters.each_key do |field| + next if field == "subproject_id" + v = values_for(field).clone + next unless v and !v.empty? + operator = operator_for(field) + + # "me" value subsitution + if %w(assigned_to_id author_id watcher_id).include?(field) + if v.delete("me") + if User.current.logged? + v.push(User.current.id.to_s) + v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id' + else + v.push("0") + end + end + end + + if field == 'project_id' + if v.delete('mine') + v += User.current.memberships.map(&:project_id).map(&:to_s) + end + end + + if field =~ /cf_(\d+)$/ + # custom field + filters_clauses << sql_for_custom_field(field, operator, v, $1) + elsif respond_to?("sql_for_#{field}_field") + # specific statement + filters_clauses << send("sql_for_#{field}_field", field, operator, v) + else + # regular field + filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')' + end + end if filters and valid? + + filters_clauses << project_statement + filters_clauses.reject!(&:blank?) + + filters_clauses.any? ? filters_clauses.join(' AND ') : nil + end + + # Returns the issue count + def issue_count + Issue.visible.count(:include => [:status, :project], :conditions => statement) + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + # Returns the issue count by group or nil if query is not grouped + def issue_count_by_group + r = nil + if grouped? + begin + # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value + r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement) + rescue ActiveRecord::RecordNotFound + r = {nil => issue_count} + end + c = group_by_column + if c.is_a?(QueryCustomFieldColumn) + r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h} + end + end + r + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + # Returns the issues + # Valid options are :order, :offset, :limit, :include, :conditions + def issues(options={}) + order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',') + order_option = nil if order_option.blank? + + issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq, + :conditions => statement, + :order => order_option, + :joins => joins_for_order_statement(order_option), + :limit => options[:limit], + :offset => options[:offset] + + if has_column?(:spent_hours) + Issue.load_visible_spent_hours(issues) + end + if has_column?(:relations) + Issue.load_visible_relations(issues) + end + issues + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + # Returns the issues ids + def issue_ids(options={}) + order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',') + order_option = nil if order_option.blank? + + Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq, + :conditions => statement, + :order => order_option, + :joins => joins_for_order_statement(order_option), + :limit => options[:limit], + :offset => options[:offset]).find_ids + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + # Returns the journals + # Valid options are :order, :offset, :limit + def journals(options={}) + Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}], + :conditions => statement, + :order => options[:order], + :limit => options[:limit], + :offset => options[:offset] + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + # Returns the versions + # Valid options are :conditions + def versions(options={}) + Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + def sql_for_watcher_id_field(field, operator, value) + db_table = Watcher.table_name + "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " + + sql_for_field(field, '=', value, db_table, 'user_id') + ')' + end + + def sql_for_member_of_group_field(field, operator, value) + if operator == '*' # Any group + groups = Group.all + operator = '=' # Override the operator since we want to find by assigned_to + elsif operator == "!*" + groups = Group.all + operator = '!' # Override the operator since we want to find by assigned_to + else + groups = Group.find_all_by_id(value) + end + groups ||= [] + + members_of_groups = groups.inject([]) {|user_ids, group| + if group && group.user_ids.present? + user_ids << group.user_ids + end + user_ids.flatten.uniq.compact + }.sort.collect(&:to_s) + + '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')' + end + + def sql_for_assigned_to_role_field(field, operator, value) + case operator + when "*", "!*" # Member / Not member + sw = operator == "!*" ? 'NOT' : '' + nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : '' + "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" + + " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))" + when "=", "!" + role_cond = value.any? ? + "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" : + "1=0" + + sw = operator == "!" ? 'NOT' : '' + nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : '' + "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" + + " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))" + end + end + + def sql_for_is_private_field(field, operator, value) + op = (operator == "=" ? 'IN' : 'NOT IN') + va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',') + + "#{Issue.table_name}.is_private #{op} (#{va})" + end + + def sql_for_relations(field, operator, value, options={}) + relation_options = IssueRelation::TYPES[field] + return relation_options unless relation_options + + relation_type = field + join_column, target_join_column = "issue_from_id", "issue_to_id" + if relation_options[:reverse] || options[:reverse] + relation_type = relation_options[:reverse] || relation_type + join_column, target_join_column = target_join_column, join_column + end + + sql = case operator + when "*", "!*" + op = (operator == "*" ? 'IN' : 'NOT IN') + "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')" + when "=", "!" + op = (operator == "=" ? 'IN' : 'NOT IN') + "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})" + when "=p", "=!p", "!p" + op = (operator == "!p" ? 'NOT IN' : 'IN') + comp = (operator == "=!p" ? '<>' : '=') + "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})" + end + + if relation_options[:sym] == field && !options[:reverse] + sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)] + sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ") + else + sql + end + end + + IssueRelation::TYPES.keys.each do |relation_type| + alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations + end + + private + + def sql_for_custom_field(field, operator, value, custom_field_id) + db_table = CustomValue.table_name + db_field = 'value' + filter = @available_filters[field] + return nil unless filter + if filter[:format] == 'user' + if value.delete('me') + value.push User.current.id.to_s + end + end + not_in = nil + if operator == '!' + # Makes ! operator work for custom fields with multiple values + operator = '=' + not_in = 'NOT' + end + customized_key = "id" + customized_class = Issue + if field =~ /^(.+)\.cf_/ + assoc = $1 + customized_key = "#{assoc}_id" + customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil + raise "Unknown Issue association #{assoc}" unless customized_class + end + "#{Issue.table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " + + sql_for_field(field, operator, value, db_table, db_field, true) + ')' + end + + # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+ + def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false) + sql = '' + case operator + when "=" + if value.any? + case type_for(field) + when :date, :date_past + sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil)) + when :integer + if is_custom_filter + sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})" + else + sql = "#{db_table}.#{db_field} = #{value.first.to_i}" + end + when :float + if is_custom_filter + sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})" + else + sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}" + end + else + sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" + end + else + # IN an empty set + sql = "1=0" + end + when "!" + if value.any? + sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))" + else + # NOT IN an empty set + sql = "1=1" + end + when "!*" + sql = "#{db_table}.#{db_field} IS NULL" + sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter + when "*" + sql = "#{db_table}.#{db_field} IS NOT NULL" + sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter + when ">=" + if [:date, :date_past].include?(type_for(field)) + sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil) + else + if is_custom_filter + sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})" + else + sql = "#{db_table}.#{db_field} >= #{value.first.to_f}" + end + end + when "<=" + if [:date, :date_past].include?(type_for(field)) + sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil)) + else + if is_custom_filter + sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})" + else + sql = "#{db_table}.#{db_field} <= #{value.first.to_f}" + end + end + when "><" + if [:date, :date_past].include?(type_for(field)) + sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil)) + else + if is_custom_filter + sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})" + else + sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}" + end + end + when "o" + sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id" + when "c" + sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id" + when ">t-" + # >= today - n days + sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil) + when "t+" + # >= today + n days + sql = relative_date_clause(db_table, db_field, value.first.to_i, nil) + when "= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) + sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6) + when "~" + sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" + when "!~" + sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" + else + raise "Unknown query operator #{operator}" + end + + return sql + end + + def add_custom_fields_filters(custom_fields, assoc=nil) + return unless custom_fields.present? + @available_filters ||= {} + + custom_fields.select(&:is_filter?).each do |field| + case field.field_format + when "text" + options = { :type => :text, :order => 20 } + when "list" + options = { :type => :list_optional, :values => field.possible_values, :order => 20} + when "date" + options = { :type => :date, :order => 20 } + when "bool" + options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 } + when "int" + options = { :type => :integer, :order => 20 } + when "float" + options = { :type => :float, :order => 20 } + when "user", "version" + next unless project + values = field.possible_values_options(project) + if User.current.logged? && field.field_format == 'user' + values.unshift ["<< #{l(:label_me)} >>", "me"] + end + options = { :type => :list_optional, :values => values, :order => 20} + else + options = { :type => :string, :order => 20 } + end + filter_id = "cf_#{field.id}" + filter_name = field.name + if assoc.present? + filter_id = "#{assoc}.#{filter_id}" + filter_name = l("label_attribute_of_#{assoc}", :name => filter_name) + end + @available_filters[filter_id] = options.merge({ + :name => filter_name, + :format => field.field_format, + :field => field + }) + end + end + + def add_associations_custom_fields_filters(*associations) + fields_by_class = CustomField.where(:is_filter => true).group_by(&:class) + associations.each do |assoc| + association_klass = Issue.reflect_on_association(assoc).klass + fields_by_class.each do |field_class, fields| + if field_class.customized_class <= association_klass + add_custom_fields_filters(fields, assoc) + end + end + end + end + + # Returns a SQL clause for a date or datetime field. + def date_clause(table, field, from, to) + s = [] + if from + from_yesterday = from - 1 + from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day) + if self.class.default_timezone == :utc + from_yesterday_time = from_yesterday_time.utc + end + s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)]) + end + if to + to_time = Time.local(to.year, to.month, to.day) + if self.class.default_timezone == :utc + to_time = to_time.utc + end + s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)]) + end + s.join(' AND ') + end + + # Returns a SQL clause for a date or datetime field using relative dates. + def relative_date_clause(table, field, days_from, days_to) + date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil)) + end + + # Additional joins required for the given sort options + def joins_for_order_statement(order_options) + joins = [] + + if order_options + if order_options.include?('authors') + joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id" + end + order_options.scan(/cf_\d+/).uniq.each do |name| + column = available_columns.detect {|c| c.name.to_s == name} + join = column && column.custom_field.join_for_order_statement + if join + joins << join + end + end + end + + joins.any? ? joins.join(' ') : nil + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/63/63e19c582d1f53a61db553943551ffbcf0c89c80.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/63/63e19c582d1f53a61db553943551ffbcf0c89c80.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,39 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class WorkflowTransition < WorkflowRule + validates_presence_of :new_status + + # Returns workflow transitions count by tracker and role + def self.count_by_tracker_and_role + counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{table_name} WHERE type = 'WorkflowTransition' GROUP BY role_id, tracker_id") + roles = Role.sorted.all + trackers = Tracker.sorted.all + + result = [] + trackers.each do |tracker| + t = [] + roles.each do |role| + row = counts.detect {|c| c['role_id'].to_s == role.id.to_s && c['tracker_id'].to_s == tracker.id.to_s} + t << [role, (row.nil? ? 0 : row['c'].to_i)] + end + result << [tracker, t] + end + + result + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/63/63f25f4ef8ab7fbc281f704add922dfda9fdcc3a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/63/63f25f4ef8ab7fbc281f704add922dfda9fdcc3a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,6 @@ +

    <%= link_to l(:label_role_plural), roles_path %> » <%=l(:label_role_new)%>

    + +<%= labelled_form_for @role do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/64/640e9b9b1f5ed20db55e3cc83e9d72decdd3d7de.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/64/640e9b9b1f5ed20db55e3cc83e9d72decdd3d7de.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,22 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class DocumentCategoryCustomField < CustomField + def type_name + :enumeration_doc_categories + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/64/6453475106e156ddd20fc32236e1e2b88b1df72a.svn-base --- a/.svn/pristine/64/6453475106e156ddd20fc32236e1e2b88b1df72a.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,110 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module ProjectsHelper - def link_to_version(version, options = {}) - return '' unless version && version.is_a?(Version) - link_to_if version.visible?, format_version_name(version), { :controller => 'versions', :action => 'show', :id => version }, options - end - - def project_settings_tabs - tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural}, - {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural}, - {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural}, - {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural}, - {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural}, - {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki}, - {:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository}, - {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural}, - {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities} - ] - tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)} - end - - def parent_project_select_tag(project) - selected = project.parent - # retrieve the requested parent project - parent_id = (params[:project] && params[:project][:parent_id]) || params[:parent_id] - if parent_id - selected = (parent_id.blank? ? nil : Project.find(parent_id)) - end - - options = '' - options << "" if project.allowed_parents.include?(nil) - options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected) - content_tag('select', options.html_safe, :name => 'project[parent_id]', :id => 'project_parent_id') - end - - # Renders a tree of projects as a nested set of unordered lists - # The given collection may be a subset of the whole project tree - # (eg. some intermediate nodes are private and can not be seen) - def render_project_hierarchy(projects) - s = '' - if projects.any? - ancestors = [] - original_project = @project - projects.each do |project| - # set the project environment to please macros. - @project = project - if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) - s << "
      \n" - else - ancestors.pop - s << "" - while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) - ancestors.pop - s << "
    \n" - end - end - classes = (ancestors.empty? ? 'root' : 'child') - s << "
  • " + - link_to_project(project, {}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}") - s << "
    #{textilizable(project.short_description, :project => project)}
    " unless project.description.blank? - s << "
    \n" - ancestors << project - end - s << ("
  • \n" * ancestors.size) - @project = original_project - end - s.html_safe - end - - # Returns a set of options for a select field, grouped by project. - def version_options_for_select(versions, selected=nil) - grouped = Hash.new {|h,k| h[k] = []} - versions.each do |version| - grouped[version.project.name] << [version.name, version.id] - end - # Add in the selected - if selected && !versions.include?(selected) - grouped[selected.project.name] << [selected.name, selected.id] - end - - if grouped.keys.size > 1 - grouped_options_for_select(grouped, selected && selected.id) - else - options_for_select((grouped.values.first || []), selected && selected.id) - end - end - - def format_version_sharing(sharing) - sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing) - l("label_version_sharing_#{sharing}") - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/64/64784c45a16457462e464054951ebfd4e505741a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/64/64784c45a16457462e464054951ebfd4e505741a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,48 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class IssueCategory < ActiveRecord::Base + include Redmine::SafeAttributes + belongs_to :project + belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id' + has_many :issues, :foreign_key => 'category_id', :dependent => :nullify + + validates_presence_of :name + validates_uniqueness_of :name, :scope => [:project_id] + validates_length_of :name, :maximum => 30 + + safe_attributes 'name', 'assigned_to_id' + + scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)} + + alias :destroy_without_reassign :destroy + + # Destroy the category + # If a category is specified, issues are reassigned to this category + def destroy(reassign_to = nil) + if reassign_to && reassign_to.is_a?(IssueCategory) && reassign_to.project == self.project + Issue.update_all("category_id = #{reassign_to.id}", "category_id = #{id}") + end + destroy_without_reassign + end + + def <=>(category) + name <=> category.name + end + + def to_s; name end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/64/64df060146961cbad6d4318b0ef4f4a5bb0a59fb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/64/64df060146961cbad6d4318b0ef4f4a5bb0a59fb.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,7 @@ +
    +<%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "issue_notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %> +<%= link_to l(:button_log_time), new_issue_time_entry_path(@issue), :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project) %> +<%= watcher_tag(@issue, User.current) %> +<%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, :class => 'icon icon-copy' %> +<%= link_to l(:button_delete), issue_path(@issue), :data => {:confirm => issues_destroy_confirmation_message(@issue)}, :method => :delete, :class => 'icon icon-del' if User.current.allowed_to?(:delete_issues, @project) %> +
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/65/6514caff2f9e9a210142d5cdd20b0807c7fb9b2f.svn-base --- a/.svn/pristine/65/6514caff2f9e9a210142d5cdd20b0807c7fb9b2f.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module IssueCategoriesHelper -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/65/6521265256bb36fe92b152231305d87ec785bbb9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/65/6521265256bb36fe92b152231305d87ec785bbb9.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,184 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module I18n + def self.included(base) + base.extend Redmine::I18n + end + + def l(*args) + case args.size + when 1 + ::I18n.t(*args) + when 2 + if args.last.is_a?(Hash) + ::I18n.t(*args) + elsif args.last.is_a?(String) + ::I18n.t(args.first, :value => args.last) + else + ::I18n.t(args.first, :count => args.last) + end + else + raise "Translation string with multiple values: #{args.first}" + end + end + + def l_or_humanize(s, options={}) + k = "#{options[:prefix]}#{s}".to_sym + ::I18n.t(k, :default => s.to_s.humanize) + end + + def l_hours(hours) + hours = hours.to_f + l((hours < 2.0 ? :label_f_hour : :label_f_hour_plural), :value => ("%.2f" % hours.to_f)) + end + + def ll(lang, str, value=nil) + ::I18n.t(str.to_s, :value => value, :locale => lang.to_s.gsub(%r{(.+)\-(.+)$}) { "#{$1}-#{$2.upcase}" }) + end + + def format_date(date) + return nil unless date + options = {} + options[:format] = Setting.date_format unless Setting.date_format.blank? + options[:locale] = User.current.language unless User.current.language.blank? + ::I18n.l(date.to_date, options) + end + + def format_time(time, include_date = true) + return nil unless time + options = {} + options[:format] = (Setting.time_format.blank? ? :time : Setting.time_format) + options[:locale] = User.current.language unless User.current.language.blank? + time = time.to_time if time.is_a?(String) + zone = User.current.time_zone + local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time) + (include_date ? "#{format_date(local)} " : "") + ::I18n.l(local, options) + end + + def day_name(day) + ::I18n.t('date.day_names')[day % 7] + end + + def day_letter(day) + ::I18n.t('date.abbr_day_names')[day % 7].first + end + + def month_name(month) + ::I18n.t('date.month_names')[month] + end + + def valid_languages + ::I18n.available_locales + end + + # Returns an array of languages names and code sorted by names, example: + # [["Deutsch", "de"], ["English", "en"] ...] + # + # The result is cached to prevent from loading all translations files. + def languages_options + ActionController::Base.cache_store.fetch "i18n/languages_options" do + valid_languages.map {|lang| [ll(lang.to_s, :general_lang_name), lang.to_s]}.sort {|x,y| x.first <=> y.first } + end + end + + def find_language(lang) + @@languages_lookup = valid_languages.inject({}) {|k, v| k[v.to_s.downcase] = v; k } + @@languages_lookup[lang.to_s.downcase] + end + + def set_language_if_valid(lang) + if l = find_language(lang) + ::I18n.locale = l + end + end + + def current_language + ::I18n.locale + end + + # Custom backend based on I18n::Backend::Simple with the following changes: + # * lazy loading of translation files + # * available_locales are determined by looking at translation file names + class Backend + (class << self; self; end).class_eval { public :include } + + module Implementation + include ::I18n::Backend::Base + + # Stores translations for the given locale in memory. + # This uses a deep merge for the translations hash, so existing + # translations will be overwritten by new ones only at the deepest + # level of the hash. + def store_translations(locale, data, options = {}) + locale = locale.to_sym + translations[locale] ||= {} + data = data.deep_symbolize_keys + translations[locale].deep_merge!(data) + end + + # Get available locales from the translations filenames + def available_locales + @available_locales ||= ::I18n.load_path.map {|path| File.basename(path, '.*')}.uniq.sort.map(&:to_sym) + end + + # Clean up translations + def reload! + @translations = nil + @available_locales = nil + super + end + + protected + + def init_translations(locale) + locale = locale.to_s + paths = ::I18n.load_path.select {|path| File.basename(path, '.*') == locale} + load_translations(paths) + translations[locale] ||= {} + end + + def translations + @translations ||= {} + end + + # Looks up a translation from the translations hash. Returns nil if + # eiher key is nil, or locale, scope or key do not exist as a key in the + # nested translations hash. Splits keys or scopes containing dots + # into multiple keys, i.e. currency.format is regarded the same as + # %w(currency format). + def lookup(locale, key, scope = [], options = {}) + init_translations(locale) unless translations.key?(locale) + keys = ::I18n.normalize_keys(locale, key, scope, options[:separator]) + + keys.inject(translations) do |result, _key| + _key = _key.to_sym + return nil unless result.is_a?(Hash) && result.has_key?(_key) + result = result[_key] + result = resolve(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol) + result + end + end + end + + include Implementation + # Adds fallback to default locale for untranslated strings + include ::I18n::Backend::Fallbacks + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/65/654a3e4d61187374fd83543621c54137d6380420.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/65/654a3e4d61187374fd83543621c54137d6380420.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,11 @@ +require File.dirname(__FILE__) + '/string/conversions' +require File.dirname(__FILE__) + '/string/inflections' + +class String #:nodoc: + include Redmine::CoreExtensions::String::Conversions + include Redmine::CoreExtensions::String::Inflections + + def is_binary_data? + ( self.count( "^ -~", "^\r\n" ).fdiv(self.size) > 0.3 || self.index( "\x00" ) ) unless empty? + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/65/6585cf898d7ab88d915dc6705a06f8b1775d8422.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/65/6585cf898d7ab88d915dc6705a06f8b1775d8422.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,498 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class MailHandler < ActionMailer::Base + include ActionView::Helpers::SanitizeHelper + include Redmine::I18n + + class UnauthorizedAction < StandardError; end + class MissingInformation < StandardError; end + + attr_reader :email, :user + + def self.receive(email, options={}) + @@handler_options = options.dup + + @@handler_options[:issue] ||= {} + + if @@handler_options[:allow_override].is_a?(String) + @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) + end + @@handler_options[:allow_override] ||= [] + # Project needs to be overridable if not specified + @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project) + # Status overridable by default + @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status) + + @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false) + + email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding) + super(email) + end + + def logger + Rails.logger + end + + cattr_accessor :ignored_emails_headers + @@ignored_emails_headers = { + 'X-Auto-Response-Suppress' => 'oof', + 'Auto-Submitted' => /^auto-/ + } + + # Processes incoming emails + # Returns the created object (eg. an issue, a message) or false + def receive(email) + @email = email + sender_email = email.from.to_a.first.to_s.strip + # Ignore emails received from the application emission address to avoid hell cycles + if sender_email.downcase == Setting.mail_from.to_s.strip.downcase + if logger && logger.info + logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" + end + return false + end + # Ignore auto generated emails + self.class.ignored_emails_headers.each do |key, ignored_value| + value = email.header[key] + if value + value = value.to_s.downcase + if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value + if logger && logger.info + logger.info "MailHandler: ignoring email with #{key}:#{value} header" + end + return false + end + end + end + @user = User.find_by_mail(sender_email) if sender_email.present? + if @user && !@user.active? + if logger && logger.info + logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" + end + return false + end + if @user.nil? + # Email was submitted by an unknown user + case @@handler_options[:unknown_user] + when 'accept' + @user = User.anonymous + when 'create' + @user = create_user_from_email + if @user + if logger && logger.info + logger.info "MailHandler: [#{@user.login}] account created" + end + Mailer.account_information(@user, @user.password).deliver + else + if logger && logger.error + logger.error "MailHandler: could not create account for [#{sender_email}]" + end + return false + end + else + # Default behaviour, emails from unknown users are ignored + if logger && logger.info + logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" + end + return false + end + end + User.current = @user + dispatch + end + + private + + MESSAGE_ID_RE = %r{^ e + # TODO: send a email to the user + logger.error e.message if logger + false + rescue MissingInformation => e + logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger + false + rescue UnauthorizedAction => e + logger.error "MailHandler: unauthorized attempt from #{user}" if logger + false + end + + def dispatch_to_default + receive_issue + end + + # Creates a new issue + def receive_issue + project = target_project + # check permission + unless @@handler_options[:no_permission_check] + raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) + end + + issue = Issue.new(:author => user, :project => project) + issue.safe_attributes = issue_attributes_from_keywords(issue) + issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} + issue.subject = cleaned_up_subject + if issue.subject.blank? + issue.subject = '(no subject)' + end + issue.description = cleaned_up_text_body + + # add To and Cc as watchers before saving so the watchers can reply to Redmine + add_watchers(issue) + issue.save! + add_attachments(issue) + logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info + issue + end + + # Adds a note to an existing issue + def receive_issue_reply(issue_id, from_journal=nil) + issue = Issue.find_by_id(issue_id) + return unless issue + # check permission + unless @@handler_options[:no_permission_check] + unless user.allowed_to?(:add_issue_notes, issue.project) || + user.allowed_to?(:edit_issues, issue.project) + raise UnauthorizedAction + end + end + + # ignore CLI-supplied defaults for new issues + @@handler_options[:issue].clear + + journal = issue.init_journal(user) + if from_journal && from_journal.private_notes? + # If the received email was a reply to a private note, make the added note private + issue.private_notes = true + end + issue.safe_attributes = issue_attributes_from_keywords(issue) + issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} + journal.notes = cleaned_up_text_body + add_attachments(issue) + issue.save! + if logger && logger.info + logger.info "MailHandler: issue ##{issue.id} updated by #{user}" + end + journal + end + + # Reply will be added to the issue + def receive_journal_reply(journal_id) + journal = Journal.find_by_id(journal_id) + if journal && journal.journalized_type == 'Issue' + receive_issue_reply(journal.journalized_id, journal) + end + end + + # Receives a reply to a forum message + def receive_message_reply(message_id) + message = Message.find_by_id(message_id) + if message + message = message.root + + unless @@handler_options[:no_permission_check] + raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project) + end + + if !message.locked? + reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip, + :content => cleaned_up_text_body) + reply.author = user + reply.board = message.board + message.children << reply + add_attachments(reply) + reply + else + if logger && logger.info + logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" + end + end + end + end + + def add_attachments(obj) + if email.attachments && email.attachments.any? + email.attachments.each do |attachment| + filename = attachment.filename + unless filename.respond_to?(:encoding) + # try to reencode to utf8 manually with ruby1.8 + h = attachment.header['Content-Disposition'] + unless h.nil? + begin + if m = h.value.match(/filename\*[0-9\*]*=([^=']+)'/) + filename = Redmine::CodesetUtil.to_utf8(filename, m[1]) + elsif m = h.value.match(/filename=.*=\?([^\?]+)\?[BbQq]\?/) + # http://tools.ietf.org/html/rfc2047#section-4 + filename = Redmine::CodesetUtil.to_utf8(filename, m[1]) + end + rescue + # nop + end + end + end + obj.attachments << Attachment.create(:container => obj, + :file => attachment.decoded, + :filename => filename, + :author => user, + :content_type => attachment.mime_type) + end + end + end + + # Adds To and Cc as watchers of the given object if the sender has the + # appropriate permission + def add_watchers(obj) + if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project) + addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase} + unless addresses.empty? + watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses]) + watchers.each {|w| obj.add_watcher(w)} + end + end + end + + def get_keyword(attr, options={}) + @keywords ||= {} + if @keywords.has_key?(attr) + @keywords[attr] + else + @keywords[attr] = begin + if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && + (v = extract_keyword!(plain_text_body, attr, options[:format])) + v + elsif !@@handler_options[:issue][attr].blank? + @@handler_options[:issue][attr] + end + end + end + end + + # Destructively extracts the value for +attr+ in +text+ + # Returns nil if no matching keyword found + def extract_keyword!(text, attr, format=nil) + keys = [attr.to_s.humanize] + if attr.is_a?(Symbol) + if user && user.language.present? + keys << l("field_#{attr}", :default => '', :locale => user.language) + end + if Setting.default_language.present? + keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) + end + end + keys.reject! {|k| k.blank?} + keys.collect! {|k| Regexp.escape(k)} + format ||= '.+' + keyword = nil + regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i + if m = text.match(regexp) + keyword = m[2].strip + text.gsub!(regexp, '') + end + keyword + end + + def target_project + # TODO: other ways to specify project: + # * parse the email To field + # * specific project (eg. Setting.mail_handler_target_project) + target = Project.find_by_identifier(get_keyword(:project)) + raise MissingInformation.new('Unable to determine target project') if target.nil? + target + end + + # Returns a Hash of issue attributes extracted from keywords in the email body + def issue_attributes_from_keywords(issue) + assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue) + + attrs = { + 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id), + 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id), + 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id), + 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id), + 'assigned_to_id' => assigned_to.try(:id), + 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && + issue.project.shared_versions.named(k).first.try(:id), + 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'), + 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'), + 'estimated_hours' => get_keyword(:estimated_hours, :override => true), + 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0') + }.delete_if {|k, v| v.blank? } + + if issue.new_record? && attrs['tracker_id'].nil? + attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id) + end + + attrs + end + + # Returns a Hash of issue custom field values extracted from keywords in the email body + def custom_field_values_from_keywords(customized) + customized.custom_field_values.inject({}) do |h, v| + if keyword = get_keyword(v.custom_field.name, :override => true) + h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized) + end + h + end + end + + # Returns the text/plain part of the email + # If not found (eg. HTML-only email), returns the body with tags removed + def plain_text_body + return @plain_text_body unless @plain_text_body.nil? + + part = email.text_part || email.html_part || email + @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset) + + # strip html tags and remove doctype directive + @plain_text_body = strip_tags(@plain_text_body.strip) + @plain_text_body.sub! %r{^$/) + addr, name = m[2], m[1] + end + if addr.present? + user = self.class.new_user_from_attributes(addr, name) + if user.save + user + else + logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger + nil + end + else + logger.error "MailHandler: failed to create User: no FROM address found" if logger + nil + end + end + + # Removes the email body of text after the truncation configurations. + def cleanup_body(body) + delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)} + unless delimiters.empty? + regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE) + body = body.gsub(regex, '') + end + body.strip + end + + def find_assignee_from_keyword(keyword, issue) + keyword = keyword.to_s.downcase + assignable = issue.assignable_users + assignee = nil + assignee ||= assignable.detect {|a| + a.mail.to_s.downcase == keyword || + a.login.to_s.downcase == keyword + } + if assignee.nil? && keyword.match(/ /) + firstname, lastname = *(keyword.split) # "First Last Throwaway" + assignee ||= assignable.detect {|a| + a.is_a?(User) && a.firstname.to_s.downcase == firstname && + a.lastname.to_s.downcase == lastname + } + end + if assignee.nil? + assignee ||= assignable.detect {|a| a.name.downcase == keyword} + end + assignee + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/65/65b74e35a9a66d426c65b1f9b8de763ac90e4894.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/65/65b74e35a9a66d426c65b1f9b8de763ac90e4894.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,10 @@ +api.array :groups do + @groups.each do |group| + api.group do + api.id group.id + api.name group.lastname + + render_api_custom_values group.visible_custom_field_values, api + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/65/65d6be6883079fe415bfb4c73e1cd79a79360e50.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/65/65d6be6883079fe415bfb4c73e1cd79a79360e50.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,67 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../../../test_helper', __FILE__) + +class Redmine::Views::Builders::XmlTest < ActiveSupport::TestCase + + def test_hash + assert_xml_output('Ryan32') do |b| + b.person do + b.name 'Ryan' + b.age 32 + end + end + end + + def test_array + assert_xml_output('') do |b| + b.array :books do |b| + b.book :title => 'Book 1' + b.book :title => 'Book 2' + end + end + end + + def test_array_with_content_tags + assert_xml_output('Book 1Book 2') do |b| + b.array :books do |b| + b.book 'Book 1', :author => 'B. Smith' + b.book 'Book 2', :author => 'G. Cooper' + end + end + end + + def test_nested_arrays + assert_xml_output('B. SmithG. Cooper') do |b| + b.array :books do |books| + books.book do |book| + book.array :authors do |authors| + authors.author 'B. Smith' + authors.author 'G. Cooper' + end + end + end + end + end + + def assert_xml_output(expected, &block) + builder = Redmine::Views::Builders::Xml.new(ActionDispatch::TestRequest.new, ActionDispatch::TestResponse.new) + block.call(builder) + assert_equal('' + expected, builder.output) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/65/65e02704959a4eede1859ab38db94f171c07d983.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/65/65e02704959a4eede1859ab38db94f171c07d983.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,46 @@ +
    +<%= link_to(l(:label_attachment_new), new_project_file_path(@project), :class => 'icon icon-add') if User.current.allowed_to?(:manage_files, @project) %> +
    + +

    <%=l(:label_attachment_plural)%>

    + +<% delete_allowed = User.current.allowed_to?(:manage_files, @project) %> + + + + <%= sort_header_tag('filename', :caption => l(:field_filename)) %> + <%= sort_header_tag('created_on', :caption => l(:label_date), :default_order => 'desc') %> + <%= sort_header_tag('size', :caption => l(:field_filesize), :default_order => 'desc') %> + <%= sort_header_tag('downloads', :caption => l(:label_downloads_abbr), :default_order => 'desc') %> + + + + +<% @containers.each do |container| %> + <% next if container.attachments.empty? -%> + <% if container.is_a?(Version) -%> + + + + <% end -%> + <% container.attachments.each do |file| %> + "> + + + + + + + + <% end + reset_cycle %> +<% end %> + +
    MD5
    + <%= link_to(h(container), {:controller => 'versions', :action => 'show', :id => container}, :class => "icon icon-package") %> +
    <%= link_to_attachment file, :download => true, :title => file.description %><%= format_time(file.created_on) %><%= number_to_human_size(file.filesize) %><%= file.downloads %><%= file.digest %> + <%= link_to(image_tag('delete.png'), attachment_path(file), + :data => {:confirm => l(:text_are_you_sure)}, :method => :delete) if delete_allowed %> +
    + +<% html_title(l(:label_attachment_plural)) -%> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/66/6624461ad2ec134b393374cac55a9167385a095a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/66/6624461ad2ec134b393374cac55a9167385a095a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,239 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) +require 'versions_controller' + +# Re-raise errors caught by the controller. +class VersionsController; def rescue_action(e) raise e end; end + +class VersionsControllerTest < ActionController::TestCase + fixtures :projects, :versions, :issues, :users, :roles, :members, :member_roles, :enabled_modules, :issue_statuses, :issue_categories + + def setup + @controller = VersionsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_index + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:versions) + # Version with no date set appears + assert assigns(:versions).include?(Version.find(3)) + # Completed version doesn't appear + assert !assigns(:versions).include?(Version.find(1)) + # Context menu on issues + assert_select "script", :text => Regexp.new(Regexp.escape("contextMenuInit('/issues/context_menu')")) + # Links to versions anchors + assert_tag 'a', :attributes => {:href => '#2.0'}, + :ancestor => {:tag => 'div', :attributes => {:id => 'sidebar'}} + # Links to completed versions in the sidebar + assert_tag 'a', :attributes => {:href => '/versions/1'}, + :ancestor => {:tag => 'div', :attributes => {:id => 'sidebar'}} + end + + def test_index_with_completed_versions + get :index, :project_id => 1, :completed => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:versions) + # Version with no date set appears + assert assigns(:versions).include?(Version.find(3)) + # Completed version appears + assert assigns(:versions).include?(Version.find(1)) + end + + def test_index_with_tracker_ids + get :index, :project_id => 1, :tracker_ids => [1, 3] + assert_response :success + assert_template 'index' + assert_not_nil assigns(:issues_by_version) + assert_nil assigns(:issues_by_version).values.flatten.detect {|issue| issue.tracker_id == 2} + end + + def test_index_showing_subprojects_versions + @subproject_version = Version.create!(:project => Project.find(3), :name => "Subproject version") + get :index, :project_id => 1, :with_subprojects => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:versions) + + assert assigns(:versions).include?(Version.find(4)), "Shared version not found" + assert assigns(:versions).include?(@subproject_version), "Subproject version not found" + end + + def test_index_should_prepend_shared_versions + get :index, :project_id => 1 + assert_response :success + + assert_select '#sidebar' do + assert_select 'a[href=?]', '#2.0', :text => '2.0' + assert_select 'a[href=?]', '#subproject1-2.0', :text => 'eCookbook Subproject 1 - 2.0' + end + assert_select '#content' do + assert_select 'a[name=?]', '2.0', :text => '2.0' + assert_select 'a[name=?]', 'subproject1-2.0', :text => 'eCookbook Subproject 1 - 2.0' + end + end + + def test_show + get :show, :id => 2 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:version) + + assert_tag :tag => 'h2', :content => /1.0/ + end + + def test_show_should_display_nil_counts + with_settings :default_language => 'en' do + get :show, :id => 2, :status_by => 'category' + assert_response :success + assert_select 'div#status_by' do + assert_select 'select[name=status_by]' do + assert_select 'option[value=category][selected=selected]' + end + assert_select 'a', :text => 'none' + end + end + end + + def test_new + @request.session[:user_id] = 2 + get :new, :project_id => '1' + assert_response :success + assert_template 'new' + end + + def test_new_from_issue_form + @request.session[:user_id] = 2 + xhr :get, :new, :project_id => '1' + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + end + + def test_create + @request.session[:user_id] = 2 # manager + assert_difference 'Version.count' do + post :create, :project_id => '1', :version => {:name => 'test_add_version'} + end + assert_redirected_to '/projects/ecookbook/settings/versions' + version = Version.find_by_name('test_add_version') + assert_not_nil version + assert_equal 1, version.project_id + end + + def test_create_from_issue_form + @request.session[:user_id] = 2 + assert_difference 'Version.count' do + xhr :post, :create, :project_id => '1', :version => {:name => 'test_add_version_from_issue_form'} + end + version = Version.find_by_name('test_add_version_from_issue_form') + assert_not_nil version + assert_equal 1, version.project_id + + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + assert_include 'test_add_version_from_issue_form', response.body + end + + def test_create_from_issue_form_with_failure + @request.session[:user_id] = 2 + assert_no_difference 'Version.count' do + xhr :post, :create, :project_id => '1', :version => {:name => ''} + end + assert_response :success + assert_template 'new' + assert_equal 'text/javascript', response.content_type + end + + def test_get_edit + @request.session[:user_id] = 2 + get :edit, :id => 2 + assert_response :success + assert_template 'edit' + end + + def test_close_completed + Version.update_all("status = 'open'") + @request.session[:user_id] = 2 + put :close_completed, :project_id => 'ecookbook' + assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook' + assert_not_nil Version.find_by_status('closed') + end + + def test_post_update + @request.session[:user_id] = 2 + put :update, :id => 2, + :version => { :name => 'New version name', + :effective_date => Date.today.strftime("%Y-%m-%d")} + assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook' + version = Version.find(2) + assert_equal 'New version name', version.name + assert_equal Date.today, version.effective_date + end + + def test_post_update_with_validation_failure + @request.session[:user_id] = 2 + put :update, :id => 2, + :version => { :name => '', + :effective_date => Date.today.strftime("%Y-%m-%d")} + assert_response :success + assert_template 'edit' + end + + def test_destroy + @request.session[:user_id] = 2 + assert_difference 'Version.count', -1 do + delete :destroy, :id => 3 + end + assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook' + assert_nil Version.find_by_id(3) + end + + def test_destroy_version_in_use_should_fail + @request.session[:user_id] = 2 + assert_no_difference 'Version.count' do + delete :destroy, :id => 2 + end + assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook' + assert flash[:error].match(/Unable to delete version/) + assert Version.find_by_id(2) + end + + def test_issue_status_by + xhr :get, :status_by, :id => 2 + assert_response :success + assert_template 'status_by' + assert_template '_issue_counts' + end + + def test_issue_status_by_status + xhr :get, :status_by, :id => 2, :status_by => 'status' + assert_response :success + assert_template 'status_by' + assert_template '_issue_counts' + assert_include 'Assigned', response.body + assert_include 'Closed', response.body + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/66/66913dbb20af8be563c71466e0df0765d76b644f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/66/66913dbb20af8be563c71466e0df0765d76b644f.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,36 @@ +--- +journals_001: + created_on: <%= 2.days.ago.to_date.to_s(:db) %> + notes: "Journal notes" + id: 1 + journalized_type: Issue + user_id: 1 + journalized_id: 1 +journals_002: + created_on: <%= 1.days.ago.to_date.to_s(:db) %> + notes: "Some notes with Redmine links: #2, r2." + id: 2 + journalized_type: Issue + user_id: 2 + journalized_id: 1 +journals_003: + created_on: <%= 1.days.ago.to_date.to_s(:db) %> + notes: "A comment with inline image: !picture.jpg! and a reference to #1 and r2." + id: 3 + journalized_type: Issue + user_id: 2 + journalized_id: 2 +journals_004: + created_on: <%= 1.days.ago.to_date.to_s(:db) %> + notes: "A comment with a private version." + id: 4 + journalized_type: Issue + user_id: 1 + journalized_id: 6 +journals_005: + id: 5 + created_on: <%= 1.days.ago.to_date.to_s(:db) %> + notes: "A comment on a private issue." + user_id: 2 + journalized_type: Issue + journalized_id: 14 diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/67/670602b1a0a590019c6607a3c26246d319412236.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/67/670602b1a0a590019c6607a3c26246d319412236.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,66 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingMessagesTest < ActionController::IntegrationTest + def test_messages + assert_routing( + { :method => 'get', :path => "/boards/22/topics/2" }, + { :controller => 'messages', :action => 'show', :id => '2', + :board_id => '22' } + ) + assert_routing( + { :method => 'get', :path => "/boards/lala/topics/new" }, + { :controller => 'messages', :action => 'new', :board_id => 'lala' } + ) + assert_routing( + { :method => 'get', :path => "/boards/lala/topics/22/edit" }, + { :controller => 'messages', :action => 'edit', :id => '22', + :board_id => 'lala' } + ) + assert_routing( + { :method => 'post', :path => "/boards/lala/topics/quote/22" }, + { :controller => 'messages', :action => 'quote', :id => '22', + :board_id => 'lala' } + ) + assert_routing( + { :method => 'post', :path => "/boards/lala/topics/new" }, + { :controller => 'messages', :action => 'new', :board_id => 'lala' } + ) + assert_routing( + { :method => 'post', :path => "/boards/lala/topics/preview" }, + { :controller => 'messages', :action => 'preview', + :board_id => 'lala' } + ) + assert_routing( + { :method => 'post', :path => "/boards/lala/topics/22/edit" }, + { :controller => 'messages', :action => 'edit', :id => '22', + :board_id => 'lala' } + ) + assert_routing( + { :method => 'post', :path => "/boards/22/topics/555/replies" }, + { :controller => 'messages', :action => 'reply', :id => '555', + :board_id => '22' } + ) + assert_routing( + { :method => 'post', :path => "/boards/22/topics/555/destroy" }, + { :controller => 'messages', :action => 'destroy', :id => '555', + :board_id => '22' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/67/671278aa9eb41ffc1745cf0aca4827c983ebe8c5.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/67/671278aa9eb41ffc1745cf0aca4827c983ebe8c5.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,107 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class ApiTest::IssueRelationsTest < ActionController::IntegrationTest + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :workflows, + :issue_relations + + def setup + Setting.rest_api_enabled = '1' + end + + context "/issues/:issue_id/relations" do + context "GET" do + should "return issue relations" do + get '/issues/9/relations.xml', {}, credentials('jsmith') + + assert_response :success + assert_equal 'application/xml', @response.content_type + + assert_tag :tag => 'relations', + :attributes => { :type => 'array' }, + :child => { + :tag => 'relation', + :child => { + :tag => 'id', + :content => '1' + } + } + end + end + + context "POST" do + should "create a relation" do + assert_difference('IssueRelation.count') do + post '/issues/2/relations.xml', {:relation => {:issue_to_id => 7, :relation_type => 'relates'}}, credentials('jsmith') + end + + relation = IssueRelation.first(:order => 'id DESC') + assert_equal 2, relation.issue_from_id + assert_equal 7, relation.issue_to_id + assert_equal 'relates', relation.relation_type + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_tag 'relation', :child => {:tag => 'id', :content => relation.id.to_s} + end + + context "with failure" do + should "return the errors" do + assert_no_difference('IssueRelation.count') do + post '/issues/2/relations.xml', {:relation => {:issue_to_id => 7, :relation_type => 'foo'}}, credentials('jsmith') + end + + assert_response :unprocessable_entity + assert_tag :errors, :child => {:tag => 'error', :content => /relation_type is not included in the list/} + end + end + end + end + + context "/relations/:id" do + context "GET" do + should "return the relation" do + get '/relations/2.xml', {}, credentials('jsmith') + + assert_response :success + assert_equal 'application/xml', @response.content_type + assert_tag 'relation', :child => {:tag => 'id', :content => '2'} + end + end + + context "DELETE" do + should "delete the relation" do + assert_difference('IssueRelation.count', -1) do + delete '/relations/2.xml', {}, credentials('jsmith') + end + + assert_response :ok + assert_equal '', @response.body + assert_nil IssueRelation.find_by_id(2) + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/67/673796e159cf85c017eed0e9ba7ade50c519b16a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/67/673796e159cf85c017eed0e9ba7ade50c519b16a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,201 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class BoardsControllerTest < ActionController::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules + + def setup + User.current = nil + end + + def test_index + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:boards) + assert_not_nil assigns(:project) + end + + def test_index_not_found + get :index, :project_id => 97 + assert_response 404 + end + + def test_index_should_show_messages_if_only_one_board + Project.find(1).boards.slice(1..-1).each(&:destroy) + + get :index, :project_id => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:topics) + end + + def test_show + get :show, :project_id => 1, :id => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:board) + assert_not_nil assigns(:project) + assert_not_nil assigns(:topics) + end + + def test_show_should_display_sticky_messages_first + Message.update_all(:sticky => 0) + Message.update_all({:sticky => 1}, {:id => 1}) + + get :show, :project_id => 1, :id => 1 + assert_response :success + + topics = assigns(:topics) + assert_not_nil topics + assert topics.size > 1, "topics size was #{topics.size}" + assert topics.first.sticky? + assert topics.first.updated_on < topics.second.updated_on + end + + def test_show_with_permission_should_display_the_new_message_form + @request.session[:user_id] = 2 + get :show, :project_id => 1, :id => 1 + assert_response :success + assert_template 'show' + + assert_tag 'form', :attributes => {:id => 'message-form'} + assert_tag 'input', :attributes => {:name => 'message[subject]'} + end + + def test_show_atom + get :show, :project_id => 1, :id => 1, :format => 'atom' + assert_response :success + assert_template 'common/feed' + assert_not_nil assigns(:board) + assert_not_nil assigns(:project) + assert_not_nil assigns(:messages) + end + + def test_show_not_found + get :index, :project_id => 1, :id => 97 + assert_response 404 + end + + def test_new + @request.session[:user_id] = 2 + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'select[name=?]', 'board[parent_id]' do + assert_select 'option', (Project.find(1).boards.size + 1) + assert_select 'option[value=]', :text => '' + assert_select 'option[value=1]', :text => 'Help' + end + end + + def test_new_without_project_boards + Project.find(1).boards.delete_all + @request.session[:user_id] = 2 + + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + assert_select 'select[name=?]', 'board[parent_id]', 0 + end + + def test_create + @request.session[:user_id] = 2 + assert_difference 'Board.count' do + post :create, :project_id => 1, :board => { :name => 'Testing', :description => 'Testing board creation'} + end + assert_redirected_to '/projects/ecookbook/settings/boards' + board = Board.first(:order => 'id DESC') + assert_equal 'Testing', board.name + assert_equal 'Testing board creation', board.description + end + + def test_create_with_parent + @request.session[:user_id] = 2 + assert_difference 'Board.count' do + post :create, :project_id => 1, :board => { :name => 'Testing', :description => 'Testing', :parent_id => 2} + end + assert_redirected_to '/projects/ecookbook/settings/boards' + board = Board.first(:order => 'id DESC') + assert_equal Board.find(2), board.parent + end + + def test_create_with_failure + @request.session[:user_id] = 2 + assert_no_difference 'Board.count' do + post :create, :project_id => 1, :board => { :name => '', :description => 'Testing board creation'} + end + assert_response :success + assert_template 'new' + end + + def test_edit + @request.session[:user_id] = 2 + get :edit, :project_id => 1, :id => 2 + assert_response :success + assert_template 'edit' + end + + def test_edit_with_parent + board = Board.generate!(:project_id => 1, :parent_id => 2) + @request.session[:user_id] = 2 + get :edit, :project_id => 1, :id => board.id + assert_response :success + assert_template 'edit' + + assert_select 'select[name=?]', 'board[parent_id]' do + assert_select 'option[value=2][selected=selected]' + end + end + + def test_update + @request.session[:user_id] = 2 + assert_no_difference 'Board.count' do + put :update, :project_id => 1, :id => 2, :board => { :name => 'Testing', :description => 'Testing board update'} + end + assert_redirected_to '/projects/ecookbook/settings/boards' + assert_equal 'Testing', Board.find(2).name + end + + def test_update_position + @request.session[:user_id] = 2 + put :update, :project_id => 1, :id => 2, :board => { :move_to => 'highest'} + assert_redirected_to '/projects/ecookbook/settings/boards' + board = Board.find(2) + assert_equal 1, board.position + end + + def test_update_with_failure + @request.session[:user_id] = 2 + put :update, :project_id => 1, :id => 2, :board => { :name => '', :description => 'Testing board update'} + assert_response :success + assert_template 'edit' + end + + def test_destroy + @request.session[:user_id] = 2 + assert_difference 'Board.count', -1 do + delete :destroy, :project_id => 1, :id => 2 + end + assert_redirected_to '/projects/ecookbook/settings/boards' + assert_nil Board.find_by_id(2) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/67/67564d7eda2e47c3ebd8a07a650fb0bd7bbca60d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/67/67564d7eda2e47c3ebd8a07a650fb0bd7bbca60d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,13 @@ +
    +
    +<% i = 0 %> +<% split_on = (@issue.custom_field_values.size / 2.0).ceil - 1 %> +<% @issue.editable_custom_field_values.each do |value| %> +

    <%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %>

    +<% if i == split_on -%> +
    +<% end -%> +<% i += 1 -%> +<% end -%> +
    +
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/67/676b4cb5bfe9b002838d027234fb7519b7bba5b1.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/67/676b4cb5bfe9b002838d027234fb7519b7bba5b1.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,140 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class GroupsController < ApplicationController + layout 'admin' + + before_filter :require_admin + before_filter :find_group, :except => [:index, :new, :create] + accept_api_auth :index, :show, :create, :update, :destroy, :add_users, :remove_user + + helper :custom_fields + + def index + @groups = Group.sorted.all + + respond_to do |format| + format.html + format.api + end + end + + def show + respond_to do |format| + format.html + format.api + end + end + + def new + @group = Group.new + end + + def create + @group = Group.new + @group.safe_attributes = params[:group] + + respond_to do |format| + if @group.save + format.html { + flash[:notice] = l(:notice_successful_create) + redirect_to(params[:continue] ? new_group_path : groups_path) + } + format.api { render :action => 'show', :status => :created, :location => group_url(@group) } + else + format.html { render :action => "new" } + format.api { render_validation_errors(@group) } + end + end + end + + def edit + end + + def update + @group.safe_attributes = params[:group] + + respond_to do |format| + if @group.save + flash[:notice] = l(:notice_successful_update) + format.html { redirect_to(groups_path) } + format.api { render_api_ok } + else + format.html { render :action => "edit" } + format.api { render_validation_errors(@group) } + end + end + end + + def destroy + @group.destroy + + respond_to do |format| + format.html { redirect_to(groups_url) } + format.api { render_api_ok } + end + end + + def add_users + @users = User.find_all_by_id(params[:user_id] || params[:user_ids]) + @group.users << @users if request.post? + respond_to do |format| + format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' } + format.js + format.api { render_api_ok } + end + end + + def remove_user + @group.users.delete(User.find(params[:user_id])) if request.delete? + respond_to do |format| + format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' } + format.js + format.api { render_api_ok } + end + end + + def autocomplete_for_user + @users = User.active.not_in_group(@group).like(params[:q]).all(:limit => 100) + render :layout => false + end + + def edit_membership + @membership = Member.edit_membership(params[:membership_id], params[:membership], @group) + @membership.save if request.post? + respond_to do |format| + format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' } + format.js + end + end + + def destroy_membership + Member.find(params[:membership_id]).destroy if request.post? + respond_to do |format| + format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' } + format.js + end + end + + private + + def find_group + @group = Group.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/67/676be5a662e77fd1edb667b7618e0c6a2f07f82e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/67/676be5a662e77fd1edb667b7618e0c6a2f07f82e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,60 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingBoardsTest < ActionController::IntegrationTest + def test_boards + assert_routing( + { :method => 'get', :path => "/projects/world_domination/boards" }, + { :controller => 'boards', :action => 'index', :project_id => 'world_domination' } + ) + assert_routing( + { :method => 'get', :path => "/projects/world_domination/boards/new" }, + { :controller => 'boards', :action => 'new', :project_id => 'world_domination' } + ) + assert_routing( + { :method => 'get', :path => "/projects/world_domination/boards/44" }, + { :controller => 'boards', :action => 'show', :project_id => 'world_domination', + :id => '44' } + ) + assert_routing( + { :method => 'get', :path => "/projects/world_domination/boards/44.atom" }, + { :controller => 'boards', :action => 'show', :project_id => 'world_domination', + :id => '44', :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/projects/world_domination/boards/44/edit" }, + { :controller => 'boards', :action => 'edit', :project_id => 'world_domination', + :id => '44' } + ) + assert_routing( + { :method => 'post', :path => "/projects/world_domination/boards" }, + { :controller => 'boards', :action => 'create', :project_id => 'world_domination' } + ) + assert_routing( + { :method => 'put', :path => "/projects/world_domination/boards/44" }, + { :controller => 'boards', :action => 'update', :project_id => 'world_domination', + :id => '44' } + ) + assert_routing( + { :method => 'delete', :path => "/projects/world_domination/boards/44" }, + { :controller => 'boards', :action => 'destroy', :project_id => 'world_domination', + :id => '44' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/67/676f5dde2b525663017469a924ccb9594114d3c2.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/67/676f5dde2b525663017469a924ccb9594114d3c2.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,14 @@ +class SetDefaultRepositories < ActiveRecord::Migration + def self.up + Repository.update_all(["is_default = ?", false]) + # Sets the last repository as default in case multiple repositories exist for the same project + Repository.connection.select_values("SELECT r.id FROM #{Repository.table_name} r" + + " WHERE r.id = (SELECT max(r1.id) FROM #{Repository.table_name} r1 WHERE r1.project_id = r.project_id)").each do |i| + Repository.update_all(["is_default = ?", true], ["id = ?", i]) + end + end + + def self.down + Repository.update_all(["is_default = ?", false]) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/67/67cbbb936bbd8a3a195700edbb70375a0e5e221d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/67/67cbbb936bbd8a3a195700edbb70375a0e5e221d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,438 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module MenuManager + class MenuError < StandardError #:nodoc: + end + + module MenuController + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + @@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}} + mattr_accessor :menu_items + + # Set the menu item name for a controller or specific actions + # Examples: + # * menu_item :tickets # => sets the menu name to :tickets for the whole controller + # * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only + # * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only + # + # The default menu item name for a controller is controller_name by default + # Eg. the default menu item name for ProjectsController is :projects + def menu_item(id, options = {}) + if actions = options[:only] + actions = [] << actions unless actions.is_a?(Array) + actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id} + else + menu_items[controller_name.to_sym][:default] = id + end + end + end + + def menu_items + self.class.menu_items + end + + # Returns the menu item name according to the current action + def current_menu_item + @current_menu_item ||= menu_items[controller_name.to_sym][:actions][action_name.to_sym] || + menu_items[controller_name.to_sym][:default] + end + + # Redirects user to the menu item of the given project + # Returns false if user is not authorized + def redirect_to_project_menu_item(project, name) + item = Redmine::MenuManager.items(:project_menu).detect {|i| i.name.to_s == name.to_s} + if item && User.current.allowed_to?(item.url, project) && (item.condition.nil? || item.condition.call(project)) + redirect_to({item.param => project}.merge(item.url)) + return true + end + false + end + end + + module MenuHelper + # Returns the current menu item name + def current_menu_item + controller.current_menu_item + end + + # Renders the application main menu + def render_main_menu(project) + render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project) + end + + def display_main_menu?(project) + menu_name = project && !project.new_record? ? :project_menu : :application_menu + Redmine::MenuManager.items(menu_name).children.present? + end + + def render_menu(menu, project=nil) + links = [] + menu_items_for(menu, project) do |node| + links << render_menu_node(node, project) + end + links.empty? ? nil : content_tag('ul', links.join("\n").html_safe) + end + + def render_menu_node(node, project=nil) + if node.children.present? || !node.child_menus.nil? + return render_menu_node_with_children(node, project) + else + caption, url, selected = extract_node_details(node, project) + return content_tag('li', + render_single_menu_node(node, caption, url, selected)) + end + end + + def render_menu_node_with_children(node, project=nil) + caption, url, selected = extract_node_details(node, project) + + html = [].tap do |html| + html << '
  • ' + # Parent + html << render_single_menu_node(node, caption, url, selected) + + # Standard children + standard_children_list = "".html_safe.tap do |child_html| + node.children.each do |child| + child_html << render_menu_node(child, project) + end + end + + html << content_tag(:ul, standard_children_list, :class => 'menu-children') unless standard_children_list.empty? + + # Unattached children + unattached_children_list = render_unattached_children_menu(node, project) + html << content_tag(:ul, unattached_children_list, :class => 'menu-children unattached') unless unattached_children_list.blank? + + html << '
  • ' + end + return html.join("\n").html_safe + end + + # Returns a list of unattached children menu items + def render_unattached_children_menu(node, project) + return nil unless node.child_menus + + "".html_safe.tap do |child_html| + unattached_children = node.child_menus.call(project) + # Tree nodes support #each so we need to do object detection + if unattached_children.is_a? Array + unattached_children.each do |child| + child_html << content_tag(:li, render_unattached_menu_item(child, project)) + end + else + raise MenuError, ":child_menus must be an array of MenuItems" + end + end + end + + def render_single_menu_node(item, caption, url, selected) + link_to(h(caption), url, item.html_options(:selected => selected)) + end + + def render_unattached_menu_item(menu_item, project) + raise MenuError, ":child_menus must be an array of MenuItems" unless menu_item.is_a? MenuItem + + if User.current.allowed_to?(menu_item.url, project) + link_to(h(menu_item.caption), + menu_item.url, + menu_item.html_options) + end + end + + def menu_items_for(menu, project=nil) + items = [] + Redmine::MenuManager.items(menu).root.children.each do |node| + if allowed_node?(node, User.current, project) + if block_given? + yield node + else + items << node # TODO: not used? + end + end + end + return block_given? ? nil : items + end + + def extract_node_details(node, project=nil) + item = node + url = case item.url + when Hash + project.nil? ? item.url : {item.param => project}.merge(item.url) + when Symbol + send(item.url) + else + item.url + end + caption = item.caption(project) + return [caption, url, (current_menu_item == item.name)] + end + + # Checks if a user is allowed to access the menu item by: + # + # * Checking the conditions of the item + # * Checking the url target (project only) + def allowed_node?(node, user, project) + if node.condition && !node.condition.call(project) + # Condition that doesn't pass + return false + end + + if project + return user && user.allowed_to?(node.url, project) + else + # outside a project, all menu items allowed + return true + end + end + end + + class << self + def map(menu_name) + @items ||= {} + mapper = Mapper.new(menu_name.to_sym, @items) + if block_given? + yield mapper + else + mapper + end + end + + def items(menu_name) + @items[menu_name.to_sym] || MenuNode.new(:root, {}) + end + end + + class Mapper + def initialize(menu, items) + items[menu] ||= MenuNode.new(:root, {}) + @menu = menu + @menu_items = items[menu] + end + + # Adds an item at the end of the menu. Available options: + # * param: the parameter name that is used for the project id (default is :id) + # * if: a Proc that is called before rendering the item, the item is displayed only if it returns true + # * caption that can be: + # * a localized string Symbol + # * a String + # * a Proc that can take the project as argument + # * before, after: specify where the menu item should be inserted (eg. :after => :activity) + # * parent: menu item will be added as a child of another named menu (eg. :parent => :issues) + # * children: a Proc that is called before rendering the item. The Proc should return an array of MenuItems, which will be added as children to this item. + # eg. :children => Proc.new {|project| [Redmine::MenuManager::MenuItem.new(...)] } + # * last: menu item will stay at the end (eg. :last => true) + # * html_options: a hash of html options that are passed to link_to + def push(name, url, options={}) + options = options.dup + + if options[:parent] + subtree = self.find(options[:parent]) + if subtree + target_root = subtree + else + target_root = @menu_items.root + end + + else + target_root = @menu_items.root + end + + # menu item position + if first = options.delete(:first) + target_root.prepend(MenuItem.new(name, url, options)) + elsif before = options.delete(:before) + + if exists?(before) + target_root.add_at(MenuItem.new(name, url, options), position_of(before)) + else + target_root.add(MenuItem.new(name, url, options)) + end + + elsif after = options.delete(:after) + + if exists?(after) + target_root.add_at(MenuItem.new(name, url, options), position_of(after) + 1) + else + target_root.add(MenuItem.new(name, url, options)) + end + + elsif options[:last] # don't delete, needs to be stored + target_root.add_last(MenuItem.new(name, url, options)) + else + target_root.add(MenuItem.new(name, url, options)) + end + end + + # Removes a menu item + def delete(name) + if found = self.find(name) + @menu_items.remove!(found) + end + end + + # Checks if a menu item exists + def exists?(name) + @menu_items.any? {|node| node.name == name} + end + + def find(name) + @menu_items.find {|node| node.name == name} + end + + def position_of(name) + @menu_items.each do |node| + if node.name == name + return node.position + end + end + end + end + + class MenuNode + include Enumerable + attr_accessor :parent + attr_reader :last_items_count, :name + + def initialize(name, content = nil) + @name = name + @children = [] + @last_items_count = 0 + end + + def children + if block_given? + @children.each {|child| yield child} + else + @children + end + end + + # Returns the number of descendants + 1 + def size + @children.inject(1) {|sum, node| sum + node.size} + end + + def each &block + yield self + children { |child| child.each(&block) } + end + + # Adds a child at first position + def prepend(child) + add_at(child, 0) + end + + # Adds a child at given position + def add_at(child, position) + raise "Child already added" if find {|node| node.name == child.name} + + @children = @children.insert(position, child) + child.parent = self + child + end + + # Adds a child as last child + def add_last(child) + add_at(child, -1) + @last_items_count += 1 + child + end + + # Adds a child + def add(child) + position = @children.size - @last_items_count + add_at(child, position) + end + alias :<< :add + + # Removes a child + def remove!(child) + @children.delete(child) + @last_items_count -= +1 if child && child.last + child.parent = nil + child + end + + # Returns the position for this node in it's parent + def position + self.parent.children.index(self) + end + + # Returns the root for this node + def root + root = self + root = root.parent while root.parent + root + end + end + + class MenuItem < MenuNode + include Redmine::I18n + attr_reader :name, :url, :param, :condition, :parent, :child_menus, :last + + def initialize(name, url, options) + raise ArgumentError, "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call) + raise ArgumentError, "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash) + raise ArgumentError, "Cannot set the :parent to be the same as this item" if options[:parent] == name.to_sym + raise ArgumentError, "Invalid option :children for menu item '#{name}'" if options[:children] && !options[:children].respond_to?(:call) + @name = name + @url = url + @condition = options[:if] + @param = options[:param] || :id + @caption = options[:caption] + @html_options = options[:html] || {} + # Adds a unique class to each menu item based on its name + @html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ') + @parent = options[:parent] + @child_menus = options[:children] + @last = options[:last] || false + super @name.to_sym + end + + def caption(project=nil) + if @caption.is_a?(Proc) + c = @caption.call(project).to_s + c = @name.to_s.humanize if c.blank? + c + else + if @caption.nil? + l_or_humanize(name, :prefix => 'label_') + else + @caption.is_a?(Symbol) ? l(@caption) : @caption + end + end + end + + def html_options(options={}) + if options[:selected] + o = @html_options.dup + o[:class] += ' selected' + o + else + @html_options + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/67/67cf5cccf9960052155966022aeefcfe4c1324e2.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/67/67cf5cccf9960052155966022aeefcfe4c1324e2.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,30 @@ +xml.instruct! +xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do + xml.title @title + xml.link "rel" => "self", "href" => url_for(:format => 'atom', :key => User.current.rss_key, :only_path => false) + xml.link "rel" => "alternate", "href" => home_url(:only_path => false) + xml.id url_for(:controller => 'welcome', :only_path => false) + xml.updated((@journals.first ? @journals.first.event_datetime : Time.now).xmlschema) + xml.author { xml.name "#{Setting.app_title}" } + @journals.each do |change| + issue = change.issue + xml.entry do + xml.title "#{issue.project.name} - #{issue.tracker.name} ##{issue.id}: #{issue.subject}" + xml.link "rel" => "alternate", "href" => url_for(:controller => 'issues' , :action => 'show', :id => issue, :only_path => false) + xml.id url_for(:controller => 'issues' , :action => 'show', :id => issue, :journal_id => change, :only_path => false) + xml.updated change.created_on.xmlschema + xml.author do + xml.name change.user.name + xml.email(change.user.mail) if change.user.is_a?(User) && !change.user.mail.blank? && !change.user.pref.hide_mail + end + xml.content "type" => "html" do + xml.text! '
      ' + details_to_strings(change.details, false).each do |string| + xml.text! '
    • ' + string + '
    • ' + end + xml.text! '
    ' + xml.text! textilizable(change, :notes, :only_path => false) unless change.notes.blank? + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/67/67d42ac4101639d32c2a0940b1f3ea1663d8884f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/67/67d42ac4101639d32c2a0940b1f3ea1663d8884f.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1087 @@ +# Chinese (China) translations for Ruby on Rails +# by tsechingho (http://github.com/tsechingho) +zh: + # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) + direction: ltr + jquery: + locale: "zh-CN" + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%Y-%m-%d" + short: "%b%dæ—¥" + long: "%Yå¹´%b%dæ—¥" + + day_names: [星期天, 星期一, 星期二, 星期三, 星期四, 星期五, 星期六] + abbr_day_names: [æ—¥, 一, 二, 三, å››, 五, å…­] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, 一月, 二月, 三月, 四月, 五月, 六月, 七月, 八月, 乿œˆ, åæœˆ, å一月, å二月] + abbr_month_names: [~, 1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月, 11月, 12月] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%Yå¹´%b%dæ—¥ %A %H:%M:%S" + time: "%H:%M" + short: "%b%dæ—¥ %H:%M" + long: "%Yå¹´%b%dæ—¥ %H:%M" + am: "上åˆ" + pm: "下åˆ" + + datetime: + distance_in_words: + half_a_minute: "åŠåˆ†é’Ÿ" + less_than_x_seconds: + one: "一秒内" + other: "少于 %{count} ç§’" + x_seconds: + one: "一秒" + other: "%{count} ç§’" + less_than_x_minutes: + one: "一分钟内" + other: "少于 %{count} 分钟" + x_minutes: + one: "一分钟" + other: "%{count} 分钟" + about_x_hours: + one: "å¤§çº¦ä¸€å°æ—¶" + other: "大约 %{count} å°æ—¶" + x_hours: + one: "1 å°æ—¶" + other: "%{count} å°æ—¶" + x_days: + one: "一天" + other: "%{count} 天" + about_x_months: + one: "大约一个月" + other: "大约 %{count} 个月" + x_months: + one: "一个月" + other: "%{count} 个月" + about_x_years: + one: "大约一年" + other: "大约 %{count} å¹´" + over_x_years: + one: "超过一年" + other: "超过 %{count} å¹´" + almost_x_years: + one: "将近 1 å¹´" + other: "将近 %{count} å¹´" + + number: + # Default format for numbers + format: + separator: "." + delimiter: "" + precision: 3 + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + +# Used in array.to_sentence. + support: + array: + sentence_connector: "å’Œ" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "由于å‘生了一个错误 %{model} 无法ä¿å­˜" + other: "%{count} 个错误使得 %{model} 无法ä¿å­˜" + messages: + inclusion: "ä¸åŒ…å«äºŽåˆ—表中" + exclusion: "是ä¿ç•™å…³é”®å­—" + invalid: "是无效的" + confirmation: "与确认值ä¸åŒ¹é…" + accepted: "必须是å¯è¢«æŽ¥å—çš„" + empty: "ä¸èƒ½ç•™ç©º" + blank: "ä¸èƒ½ä¸ºç©ºå­—符" + too_long: "过长(最长为 %{count} 个字符)" + too_short: "过短(最短为 %{count} 个字符)" + wrong_length: "é•¿åº¦éžæ³•(必须为 %{count} 个字符)" + taken: "å·²ç»è¢«ä½¿ç”¨" + not_a_number: "䏿˜¯æ•°å­—" + not_a_date: "䏿˜¯åˆæ³•日期" + greater_than: "必须大于 %{count}" + greater_than_or_equal_to: "必须大于或等于 %{count}" + equal_to: "必须等于 %{count}" + less_than: "å¿…é¡»å°äºŽ %{count}" + less_than_or_equal_to: "å¿…é¡»å°äºŽæˆ–等于 %{count}" + odd: "å¿…é¡»ä¸ºå•æ•°" + even: "å¿…é¡»ä¸ºåŒæ•°" + greater_than_start_date: "必须在起始日期之åŽ" + not_same_project: "ä¸å±žäºŽåŒä¸€ä¸ªé¡¹ç›®" + circular_dependency: "此关è”将导致循环ä¾èµ–" + cant_link_an_issue_with_a_descendant: "问题ä¸èƒ½å…³è”到它的å­ä»»åŠ¡" + + actionview_instancetag_blank_option: 请选择 + + general_text_No: 'å¦' + general_text_Yes: '是' + general_text_no: 'å¦' + general_text_yes: '是' + general_lang_name: 'Simplified Chinese (简体中文)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: gb18030 + general_pdf_encoding: gb18030 + general_first_day_of_week: '7' + + notice_account_updated: å¸å·æ›´æ–°æˆåŠŸ + notice_account_invalid_creditentials: æ— æ•ˆçš„ç”¨æˆ·åæˆ–å¯†ç  + notice_account_password_updated: å¯†ç æ›´æ–°æˆåŠŸ + notice_account_wrong_password: 密ç é”™è¯¯ + notice_account_register_done: å¸å·åˆ›å»ºæˆåŠŸï¼Œè¯·ä½¿ç”¨æ³¨å†Œç¡®è®¤é‚®ä»¶ä¸­çš„é“¾æŽ¥æ¥æ¿€æ´»æ‚¨çš„å¸å·ã€‚ + notice_account_unknown_email: 未知用户 + notice_can_t_change_password: 该å¸å·ä½¿ç”¨äº†å¤–部认è¯ï¼Œå› æ­¤æ— æ³•更改密ç ã€‚ + notice_account_lost_email_sent: 系统已将引导您设置新密ç çš„邮件å‘é€ç»™æ‚¨ã€‚ + notice_account_activated: 您的å¸å·å·²è¢«æ¿€æ´»ã€‚您现在å¯ä»¥ç™»å½•了。 + notice_successful_create: 创建æˆåŠŸ + notice_successful_update: æ›´æ–°æˆåŠŸ + notice_successful_delete: 删除æˆåŠŸ + notice_successful_connection: 连接æˆåŠŸ + notice_file_not_found: 您访问的页é¢ä¸å­˜åœ¨æˆ–已被删除。 + notice_locking_conflict: æ•°æ®å·²è¢«å¦ä¸€ä½ç”¨æˆ·æ›´æ–° + notice_not_authorized: 对ä¸èµ·ï¼Œæ‚¨æ— æƒè®¿é—®æ­¤é¡µé¢ã€‚ + notice_not_authorized_archived_project: è¦è®¿é—®çš„项目已ç»å½’档。 + notice_email_sent: "邮件已å‘é€è‡³ %{value}" + notice_email_error: "å‘é€é‚®ä»¶æ—¶å‘生错误 (%{value})" + notice_feeds_access_key_reseted: 您的RSSå­˜å–键已被é‡ç½®ã€‚ + notice_api_access_key_reseted: 您的API访问键已被é‡ç½®ã€‚ + notice_failed_to_save_issues: "%{count} 个问题ä¿å­˜å¤±è´¥ï¼ˆå…±é€‰æ‹© %{total} 个问题):%{ids}." + notice_failed_to_save_members: "æˆå‘˜ä¿å­˜å¤±è´¥: %{errors}." + notice_no_issue_selected: "未选择任何问题ï¼è¯·é€‰æ‹©æ‚¨è¦ç¼–辑的问题。" + notice_account_pending: "您的å¸å·å·²è¢«æˆåŠŸåˆ›å»ºï¼Œæ­£åœ¨ç­‰å¾…ç®¡ç†å‘˜çš„审核。" + notice_default_data_loaded: æˆåŠŸè½½å…¥é»˜è®¤è®¾ç½®ã€‚ + notice_unable_delete_version: 无法删除版本 + notice_unable_delete_time_entry: 无法删除工时 + notice_issue_done_ratios_updated: 问题完æˆåº¦å·²æ›´æ–°ã€‚ + notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})" + + error_can_t_load_default_data: "无法载入默认设置:%{value}" + error_scm_not_found: "版本库中ä¸å­˜åœ¨è¯¥æ¡ç›®å’Œï¼ˆæˆ–)其修订版本。" + error_scm_command_failed: "访问版本库时å‘生错误:%{value}" + error_scm_annotate: "该æ¡ç›®ä¸å­˜åœ¨æˆ–无法追溯。" + error_issue_not_found_in_project: '问题ä¸å­˜åœ¨æˆ–ä¸å±žäºŽæ­¤é¡¹ç›®' + error_no_tracker_in_project: 该项目未设定跟踪标签,请检查项目é…置。 + error_no_default_issue_status: 未设置默认的问题状æ€ã€‚请检查系统设置("管ç†" -> "问题状æ€")。 + error_can_not_delete_custom_field: 无法删除自定义属性 + error_can_not_delete_tracker: "该跟踪标签已包å«é—®é¢˜,无法删除" + error_can_not_remove_role: "该角色正在使用中,无法删除" + error_can_not_reopen_issue_on_closed_version: 该问题被关è”到一个已ç»å…³é—­çš„ç‰ˆæœ¬ï¼Œå› æ­¤æ— æ³•é‡æ–°æ‰“开。 + error_can_not_archive_project: 该项目无法被存档 + error_issue_done_ratios_not_updated: 问题完æˆåº¦æœªèƒ½è¢«æ›´æ–°ã€‚ + error_workflow_copy_source: 请选择一个æºè·Ÿè¸ªæ ‡ç­¾æˆ–者角色 + error_workflow_copy_target: 请选择目标跟踪标签和角色 + error_unable_delete_issue_status: '无法删除问题状æ€' + error_unable_to_connect: "无法连接 (%{value})" + warning_attachments_not_saved: "%{count} 个文件ä¿å­˜å¤±è´¥" + + mail_subject_lost_password: "您的 %{value} 密ç " + mail_body_lost_password: '请点击以下链接æ¥ä¿®æ”¹æ‚¨çš„密ç ï¼š' + mail_subject_register: "%{value}å¸å·æ¿€æ´»" + mail_body_register: 'è¯·ç‚¹å‡»ä»¥ä¸‹é“¾æŽ¥æ¥æ¿€æ´»æ‚¨çš„å¸å·ï¼š' + mail_body_account_information_external: "您å¯ä»¥ä½¿ç”¨æ‚¨çš„ %{value} å¸å·æ¥ç™»å½•。" + mail_body_account_information: 您的å¸å·ä¿¡æ¯ + mail_subject_account_activation_request: "%{value}å¸å·æ¿€æ´»è¯·æ±‚" + mail_body_account_activation_request: "新用户(%{value}ï¼‰å·²å®Œæˆæ³¨å†Œï¼Œæ­£åœ¨ç­‰å€™æ‚¨çš„审核:" + mail_subject_reminder: "%{count} 个问题需è¦å°½å¿«è§£å†³ (%{days})" + mail_body_reminder: "指派给您的 %{count} 个问题需è¦åœ¨ %{days} 天内完æˆï¼š" + mail_subject_wiki_content_added: "'%{id}' wiki页é¢å·²æ·»åŠ " + mail_body_wiki_content_added: "'%{id}' wiki页é¢å·²ç”± %{author} 添加。" + mail_subject_wiki_content_updated: "'%{id}' wiki页é¢å·²æ›´æ–°ã€‚" + mail_body_wiki_content_updated: "'%{id}' wiki页é¢å·²ç”± %{author} 更新。" + + gui_validation_error: 1 个错误 + gui_validation_error_plural: "%{count} 个错误" + + field_name: åç§° + field_description: æè¿° + field_summary: æ‘˜è¦ + field_is_required: å¿…å¡« + field_firstname: åå­— + field_lastname: å§“æ° + field_mail: é‚®ä»¶åœ°å€ + field_filename: 文件 + field_filesize: å¤§å° + field_downloads: 下载次数 + field_author: 作者 + field_created_on: 创建于 + field_updated_on: 更新于 + field_field_format: æ ¼å¼ + field_is_for_all: 用于所有项目 + field_possible_values: å¯èƒ½çš„值 + field_regexp: æ­£åˆ™è¡¨è¾¾å¼ + field_min_length: 最å°é•¿åº¦ + field_max_length: 最大长度 + field_value: 值 + field_category: 类别 + field_title: 标题 + field_project: 项目 + field_issue: 问题 + field_status: çŠ¶æ€ + field_notes: 说明 + field_is_closed: 已关闭的问题 + field_is_default: 默认值 + field_tracker: 跟踪 + field_subject: 主题 + field_due_date: è®¡åˆ’å®Œæˆæ—¥æœŸ + field_assigned_to: 指派给 + field_priority: 优先级 + field_fixed_version: 目标版本 + field_user: 用户 + field_principal: 用户/用户组 + field_role: 角色 + field_homepage: 主页 + field_is_public: 公开 + field_parent: 上级项目 + field_is_in_roadmap: 在路线图中显示 + field_login: 登录å + field_mail_notification: 邮件通知 + field_admin: 管ç†å‘˜ + field_last_login_on: 最åŽç™»å½• + field_language: 语言 + field_effective_date: 日期 + field_password: å¯†ç  + field_new_password: æ–°å¯†ç  + field_password_confirmation: 确认 + field_version: 版本 + field_type: 类型 + field_host: 主机 + field_port: ç«¯å£ + field_account: å¸å· + field_base_dn: Base DN + field_attr_login: 登录å属性 + field_attr_firstname: å字属性 + field_attr_lastname: å§“æ°å±žæ€§ + field_attr_mail: 邮件属性 + field_onthefly: 峿—¶ç”¨æˆ·ç”Ÿæˆ + field_start_date: 开始日期 + field_done_ratio: "% 完æˆ" + field_auth_source: è®¤è¯æ¨¡å¼ + field_hide_mail: éšè—æˆ‘çš„é‚®ä»¶åœ°å€ + field_comments: 注释 + field_url: URL + field_start_page: 起始页 + field_subproject: å­é¡¹ç›® + field_hours: å°æ—¶ + field_activity: 活动 + field_spent_on: 日期 + field_identifier: 标识 + field_is_filter: 作为过滤æ¡ä»¶ + field_issue_to: 相关问题 + field_delay: 延期 + field_assignable: é—®é¢˜å¯æŒ‡æ´¾ç»™æ­¤è§’色 + field_redirect_existing_links: é‡å®šå‘到现有链接 + field_estimated_hours: 预期时间 + field_column_names: 列 + field_time_entries: 工时 + field_time_zone: 时区 + field_searchable: å¯ç”¨ä½œæœç´¢æ¡ä»¶ + field_default_value: 默认值 + field_comments_sorting: 显示注释 + field_parent_title: ä¸Šçº§é¡µé¢ + field_editable: å¯ç¼–辑 + field_watcher: 跟踪者 + field_identity_url: OpenID URL + field_content: 内容 + field_group_by: æ ¹æ®æ­¤æ¡ä»¶åˆ†ç»„ + field_sharing: 共享 + field_parent_issue: 父任务 + field_member_of_group: 用户组的æˆå‘˜ + field_assigned_to_role: 角色的æˆå‘˜ + field_text: 文本字段 + field_visible: å¯è§çš„ + + setting_app_title: åº”ç”¨ç¨‹åºæ ‡é¢˜ + setting_app_subtitle: 应用程åºå­æ ‡é¢˜ + setting_welcome_text: 欢迎文字 + setting_default_language: 默认语言 + setting_login_required: è¦æ±‚è®¤è¯ + setting_self_registration: å…许自注册 + setting_attachment_max_size: 附件大å°é™åˆ¶ + setting_issues_export_limit: 问题导出æ¡ç›®çš„é™åˆ¶ + setting_mail_from: 邮件å‘ä»¶äººåœ°å€ + setting_bcc_recipients: ä½¿ç”¨å¯†ä»¶æŠ„é€ (bcc) + setting_plain_text_mail: 纯文本(无HTML) + setting_host_name: 主机åç§° + setting_text_formatting: æ–‡æœ¬æ ¼å¼ + setting_wiki_compression: 压缩WikiåŽ†å²æ–‡æ¡£ + setting_feeds_limit: RSS Feedå†…å®¹æ¡æ•°é™åˆ¶ + setting_default_projects_public: 新建项目默认为公开项目 + setting_autofetch_changesets: 自动获å–程åºå˜æ›´ + setting_sys_api_enabled: å¯ç”¨ç”¨äºŽç‰ˆæœ¬åº“管ç†çš„Web Service + setting_commit_ref_keywords: 用于引用问题的关键字 + setting_commit_fix_keywords: 用于解决问题的关键字 + setting_autologin: 自动登录 + setting_date_format: æ—¥æœŸæ ¼å¼ + setting_time_format: æ—¶é—´æ ¼å¼ + setting_cross_project_issue_relations: å…许ä¸åŒé¡¹ç›®ä¹‹é—´çš„é—®é¢˜å…³è” + setting_issue_list_default_columns: 问题列表中显示的默认列 + setting_emails_header: 邮件头 + setting_emails_footer: 邮件签å + setting_protocol: åè®® + setting_per_page_options: æ¯é¡µæ˜¾ç¤ºæ¡ç›®ä¸ªæ•°çš„设置 + setting_user_format: ç”¨æˆ·æ˜¾ç¤ºæ ¼å¼ + setting_activity_days_default: 在项目活动中显示的天数 + setting_display_subprojects_issues: 在项目页é¢ä¸Šé»˜è®¤æ˜¾ç¤ºå­é¡¹ç›®çš„问题 + setting_enabled_scm: å¯ç”¨ SCM + setting_mail_handler_body_delimiters: åœ¨è¿™äº›è¡Œä¹‹åŽæˆªæ–­é‚®ä»¶ + setting_mail_handler_api_enabled: å¯ç”¨ç”¨äºŽæŽ¥æ”¶é‚®ä»¶çš„æœåŠ¡ + setting_mail_handler_api_key: API key + setting_sequential_project_identifiers: 顺åºäº§ç”Ÿé¡¹ç›®æ ‡è¯† + setting_gravatar_enabled: 使用Gravatarç”¨æˆ·å¤´åƒ + setting_gravatar_default: 默认的Gravatarå¤´åƒ + setting_diff_max_lines_displayed: 查看差别页é¢ä¸Šæ˜¾ç¤ºçš„æœ€å¤§è¡Œæ•° + setting_file_max_size_displayed: å…许直接显示的最大文本文件 + setting_repository_log_display_limit: åœ¨æ–‡ä»¶å˜æ›´è®°å½•页é¢ä¸Šæ˜¾ç¤ºçš„æœ€å¤§ä¿®è®¢ç‰ˆæœ¬æ•°é‡ + setting_openid: å…许使用OpenID登录和注册 + setting_password_min_length: 最短密ç é•¿åº¦ + setting_new_project_user_role_id: éžç®¡ç†å‘˜ç”¨æˆ·æ–°å»ºé¡¹ç›®æ—¶å°†è¢«èµ‹äºˆçš„(在该项目中的)角色 + setting_default_projects_modules: 新建项目默认å¯ç”¨çš„æ¨¡å— + setting_issue_done_ratio: 计算问题完æˆåº¦ï¼š + setting_issue_done_ratio_issue_field: 使用问题(的完æˆåº¦ï¼‰å±žæ€§ + setting_issue_done_ratio_issue_status: ä½¿ç”¨é—®é¢˜çŠ¶æ€ + setting_start_of_week: 日历开始于 + setting_rest_api_enabled: å¯ç”¨REST web service + setting_cache_formatted_text: 缓存格å¼åŒ–文字 + setting_default_notification_option: 默认æé†’选项 + setting_commit_logtime_enabled: 激活时间日志 + setting_commit_logtime_activity_id: 记录的活动 + setting_gantt_items_limit: 在甘特图上显示的最大记录数 + + permission_add_project: 新建项目 + permission_add_subprojects: 新建å­é¡¹ç›® + permission_edit_project: 编辑项目 + permission_select_project_modules: é€‰æ‹©é¡¹ç›®æ¨¡å— + permission_manage_members: ç®¡ç†æˆå‘˜ + permission_manage_project_activities: 管ç†é¡¹ç›®æ´»åЍ + permission_manage_versions: 管ç†ç‰ˆæœ¬ + permission_manage_categories: 管ç†é—®é¢˜ç±»åˆ« + permission_view_issues: 查看问题 + permission_add_issues: 新建问题 + permission_edit_issues: 更新问题 + permission_manage_issue_relations: 管ç†é—®é¢˜å…³è” + permission_add_issue_notes: 添加说明 + permission_edit_issue_notes: 编辑说明 + permission_edit_own_issue_notes: 编辑自己的说明 + permission_move_issues: 移动问题 + permission_delete_issues: 删除问题 + permission_manage_public_queries: 管ç†å…¬å¼€çš„æŸ¥è¯¢ + permission_save_queries: ä¿å­˜æŸ¥è¯¢ + permission_view_gantt: 查看甘特图 + permission_view_calendar: 查看日历 + permission_view_issue_watchers: 查看跟踪者列表 + permission_add_issue_watchers: 添加跟踪者 + permission_delete_issue_watchers: 删除跟踪者 + permission_log_time: 登记工时 + permission_view_time_entries: 查看耗时 + permission_edit_time_entries: 编辑耗时 + permission_edit_own_time_entries: 编辑自己的耗时 + permission_manage_news: ç®¡ç†æ–°é—» + permission_comment_news: 为新闻添加评论 + permission_manage_documents: ç®¡ç†æ–‡æ¡£ + permission_view_documents: 查看文档 + permission_manage_files: ç®¡ç†æ–‡ä»¶ + permission_view_files: 查看文件 + permission_manage_wiki: 管ç†Wiki + permission_rename_wiki_pages: é‡å®šå‘/é‡å‘½åWikié¡µé¢ + permission_delete_wiki_pages: 删除Wikié¡µé¢ + permission_view_wiki_pages: 查看Wiki + permission_view_wiki_edits: 查看Wiki历å²è®°å½• + permission_edit_wiki_pages: 编辑Wikié¡µé¢ + permission_delete_wiki_pages_attachments: 删除附件 + permission_protect_wiki_pages: ä¿æŠ¤Wikié¡µé¢ + permission_manage_repository: 管ç†ç‰ˆæœ¬åº“ + permission_browse_repository: æµè§ˆç‰ˆæœ¬åº“ + permission_view_changesets: æŸ¥çœ‹å˜æ›´ + permission_commit_access: 访问æäº¤ä¿¡æ¯ + permission_manage_boards: 管ç†è®¨è®ºåŒº + permission_view_messages: æŸ¥çœ‹å¸–å­ + permission_add_messages: å‘è¡¨å¸–å­ + permission_edit_messages: ç¼–è¾‘å¸–å­ + permission_edit_own_messages: ç¼–è¾‘è‡ªå·±çš„å¸–å­ + permission_delete_messages: åˆ é™¤å¸–å­ + permission_delete_own_messages: åˆ é™¤è‡ªå·±çš„å¸–å­ + permission_export_wiki_pages: 导出 wiki é¡µé¢ + permission_manage_subtasks: 管ç†å­ä»»åŠ¡ + + project_module_issue_tracking: 问题跟踪 + project_module_time_tracking: 时间跟踪 + project_module_news: æ–°é—» + project_module_documents: 文档 + project_module_files: 文件 + project_module_wiki: Wiki + project_module_repository: 版本库 + project_module_boards: 讨论区 + project_module_calendar: 日历 + project_module_gantt: 甘特图 + + label_user: 用户 + label_user_plural: 用户 + label_user_new: 新建用户 + label_user_anonymous: 匿å用户 + label_project: 项目 + label_project_new: 新建项目 + label_project_plural: 项目 + label_x_projects: + zero: 无项目 + one: 1 个项目 + other: "%{count} 个项目" + label_project_all: 所有的项目 + label_project_latest: 最近的项目 + label_issue: 问题 + label_issue_new: 新建问题 + label_issue_plural: 问题 + label_issue_view_all: 查看所有问题 + label_issues_by: "按 %{value} 分组显示问题" + label_issue_added: 问题已添加 + label_issue_updated: 问题已更新 + label_document: 文档 + label_document_new: 新建文档 + label_document_plural: 文档 + label_document_added: 文档已添加 + label_role: 角色 + label_role_plural: 角色 + label_role_new: 新建角色 + label_role_and_permissions: 角色和æƒé™ + label_member: æˆå‘˜ + label_member_new: 新建æˆå‘˜ + label_member_plural: æˆå‘˜ + label_tracker: 跟踪标签 + label_tracker_plural: 跟踪标签 + label_tracker_new: 新建跟踪标签 + label_workflow: 工作æµç¨‹ + label_issue_status: é—®é¢˜çŠ¶æ€ + label_issue_status_plural: é—®é¢˜çŠ¶æ€ + label_issue_status_new: æ–°å»ºé—®é¢˜çŠ¶æ€ + label_issue_category: 问题类别 + label_issue_category_plural: 问题类别 + label_issue_category_new: 新建问题类别 + label_custom_field: 自定义属性 + label_custom_field_plural: 自定义属性 + label_custom_field_new: 新建自定义属性 + label_enumerations: 枚举值 + label_enumeration_new: 新建枚举值 + label_information: ä¿¡æ¯ + label_information_plural: ä¿¡æ¯ + label_please_login: 请登录 + label_register: 注册 + label_login_with_open_id_option: 或使用OpenID登录 + label_password_lost: å¿˜è®°å¯†ç  + label_home: 主页 + label_my_page: æˆ‘çš„å·¥ä½œå° + label_my_account: 我的å¸å· + label_my_projects: 我的项目 + label_my_page_block: æˆ‘çš„å·¥ä½œå°æ¨¡å— + label_administration: ç®¡ç† + label_login: 登录 + label_logout: 退出 + label_help: 帮助 + label_reported_issues: 已报告的问题 + label_assigned_to_me_issues: 指派给我的问题 + label_last_login: 最åŽç™»å½• + label_registered_on: 注册于 + label_activity: 活动 + label_overall_activity: 活动概览 + label_user_activity: "%{value} 的活动" + label_new: 新建 + label_logged_as: 登录为 + label_environment: 环境 + label_authentication: è®¤è¯ + label_auth_source: è®¤è¯æ¨¡å¼ + label_auth_source_new: æ–°å»ºè®¤è¯æ¨¡å¼ + label_auth_source_plural: è®¤è¯æ¨¡å¼ + label_subproject_plural: å­é¡¹ç›® + label_subproject_new: 新建å­é¡¹ç›® + label_and_its_subprojects: "%{value} åŠå…¶å­é¡¹ç›®" + label_min_max_length: æœ€å° - 最大 长度 + label_list: 列表 + label_date: 日期 + label_integer: æ•´æ•° + label_float: 浮点数 + label_boolean: 布尔值 + label_string: 字符串 + label_text: 文本 + label_attribute: 属性 + label_attribute_plural: 属性 + label_download: "%{count} 次下载" + label_download_plural: "%{count} 次下载" + label_no_data: 没有任何数æ®å¯ä¾›æ˜¾ç¤º + label_change_status: å˜æ›´çŠ¶æ€ + label_history: 历å²è®°å½• + label_attachment: 文件 + label_attachment_new: 新建文件 + label_attachment_delete: 删除文件 + label_attachment_plural: 文件 + label_file_added: 文件已添加 + label_report: 报表 + label_report_plural: 报表 + label_news: æ–°é—» + label_news_new: 添加新闻 + label_news_plural: æ–°é—» + label_news_latest: 最近的新闻 + label_news_view_all: 查看所有新闻 + label_news_added: 新闻已添加 + label_settings: é…ç½® + label_overview: 概述 + label_version: 版本 + label_version_new: 新建版本 + label_version_plural: 版本 + label_close_versions: 关闭已完æˆçš„版本 + label_confirmation: 确认 + label_export_to: 导出 + label_read: 读å–... + label_public_projects: 公开的项目 + label_open_issues: 打开 + label_open_issues_plural: 打开 + label_closed_issues: 已关闭 + label_closed_issues_plural: 已关闭 + label_x_open_issues_abbr_on_total: + zero: 0 打开 / %{total} + one: 1 打开 / %{total} + other: "%{count} 打开 / %{total}" + label_x_open_issues_abbr: + zero: 0 打开 + one: 1 打开 + other: "%{count} 打开" + label_x_closed_issues_abbr: + zero: 0 已关闭 + one: 1 已关闭 + other: "%{count} 已关闭" + label_total: åˆè®¡ + label_permissions: æƒé™ + label_current_status: 当å‰çŠ¶æ€ + label_new_statuses_allowed: å…è®¸çš„æ–°çŠ¶æ€ + label_all: 全部 + label_none: æ—  + label_nobody: 无人 + label_next: 下一页 + label_previous: 上一页 + label_used_by: 使用中 + label_details: 详情 + label_add_note: 添加说明 + label_per_page: æ¯é¡µ + label_calendar: 日历 + label_months_from: ä¸ªæœˆä»¥æ¥ + label_gantt: 甘特图 + label_internal: 内部 + label_last_changes: "最近的 %{count} æ¬¡å˜æ›´" + label_change_view_all: æŸ¥çœ‹æ‰€æœ‰å˜æ›´ + label_personalize_page: 个性化定制本页 + label_comment: 评论 + label_comment_plural: 评论 + label_x_comments: + zero: 无评论 + one: 1 æ¡è¯„论 + other: "%{count} æ¡è¯„论" + label_comment_add: 添加评论 + label_comment_added: 评论已添加 + label_comment_delete: 删除评论 + label_query: 自定义查询 + label_query_plural: 自定义查询 + label_query_new: 新建查询 + label_filter_add: 增加过滤器 + label_filter_plural: 过滤器 + label_equals: 等于 + label_not_equals: ä¸ç­‰äºŽ + label_in_less_than: 剩余天数å°äºŽ + label_in_more_than: 剩余天数大于 + label_greater_or_equal: '>=' + label_less_or_equal: '<=' + label_in: 剩余天数 + label_today: 今天 + label_all_time: 全部时间 + label_yesterday: 昨天 + label_this_week: 本周 + label_last_week: 上周 + label_last_n_days: "æœ€åŽ %{count} 天" + label_this_month: 本月 + label_last_month: 上月 + label_this_year: 今年 + label_date_range: 日期范围 + label_less_than_ago: 之å‰å¤©æ•°å°‘于 + label_more_than_ago: 之å‰å¤©æ•°å¤§äºŽ + label_ago: 之å‰å¤©æ•° + label_contains: åŒ…å« + label_not_contains: ä¸åŒ…å« + label_day_plural: 天 + label_repository: 版本库 + label_repository_plural: 版本库 + label_browse: æµè§ˆ + label_modification: "%{count} 个更新" + label_modification_plural: "%{count} 个更新" + label_branch: 分支 + label_tag: 标签 + label_revision: 修订 + label_revision_plural: 修订 + label_revision_id: 修订 %{value} + label_associated_revisions: 相关修订版本 + label_added: 已添加 + label_modified: 已修改 + label_copied: å·²å¤åˆ¶ + label_renamed: å·²é‡å‘½å + label_deleted: 已删除 + label_latest_revision: 最近的修订版本 + label_latest_revision_plural: 最近的修订版本 + label_view_revisions: 查看修订 + label_view_all_revisions: 查看所有修订 + label_max_size: 最大尺寸 + label_sort_highest: 置顶 + label_sort_higher: 上移 + label_sort_lower: 下移 + label_sort_lowest: 置底 + label_roadmap: 路线图 + label_roadmap_due_in: "截止日期到 %{value}" + label_roadmap_overdue: "%{value} 延期" + label_roadmap_no_issues: 该版本没有问题 + label_search: æœç´¢ + label_result_plural: 结果 + label_all_words: 所有å•è¯ + label_wiki: Wiki + label_wiki_edit: Wiki 编辑 + label_wiki_edit_plural: Wiki 编辑记录 + label_wiki_page: Wiki é¡µé¢ + label_wiki_page_plural: Wiki é¡µé¢ + label_index_by_title: 按标题索引 + label_index_by_date: 按日期索引 + label_current_version: 当å‰ç‰ˆæœ¬ + label_preview: 预览 + label_feed_plural: Feeds + label_changes_details: æ‰€æœ‰å˜æ›´çš„详情 + label_issue_tracking: 问题跟踪 + label_spent_time: 耗时 + label_overall_spent_time: 总体耗时 + label_f_hour: "%{value} å°æ—¶" + label_f_hour_plural: "%{value} å°æ—¶" + label_time_tracking: 时间跟踪 + label_change_plural: å˜æ›´ + label_statistics: 统计 + label_commits_per_month: æ¯æœˆæäº¤æ¬¡æ•° + label_commits_per_author: æ¯ç”¨æˆ·æäº¤æ¬¡æ•° + label_view_diff: 查看差别 + label_diff_inline: 直列 + label_diff_side_by_side: 并排 + label_options: 选项 + label_copy_workflow_from: 从以下选项å¤åˆ¶å·¥ä½œæµç¨‹ + label_permissions_report: æƒé™æŠ¥è¡¨ + label_watched_issues: 跟踪的问题 + label_related_issues: 相关的问题 + label_applied_status: 应用åŽçš„çŠ¶æ€ + label_loading: 载入中... + label_relation_new: æ–°å»ºå…³è” + label_relation_delete: åˆ é™¤å…³è” + label_relates_to: å…³è”到 + label_duplicates: é‡å¤ + label_duplicated_by: 与其é‡å¤ + label_blocks: 阻挡 + label_blocked_by: 被阻挡 + label_precedes: 优先于 + label_follows: è·ŸéšäºŽ + label_end_to_start: 结æŸ-开始 + label_end_to_end: 结æŸ-ç»“æŸ + label_start_to_start: 开始-开始 + label_start_to_end: 开始-ç»“æŸ + label_stay_logged_in: ä¿æŒç™»å½•çŠ¶æ€ + label_disabled: ç¦ç”¨ + label_show_completed_versions: 显示已完æˆçš„版本 + label_me: 我 + label_board: 讨论区 + label_board_new: 新建讨论区 + label_board_plural: 讨论区 + label_board_locked: é”定 + label_board_sticky: 置顶 + label_topic_plural: 主题 + label_message_plural: å¸–å­ + label_message_last: æœ€æ–°çš„å¸–å­ + label_message_new: æ–°è´´ + label_message_posted: å‘帖æˆåŠŸ + label_reply_plural: å›žå¤ + label_send_information: 给用户å‘é€å¸å·ä¿¡æ¯ + label_year: å¹´ + label_month: 月 + label_week: 周 + label_date_from: 从 + label_date_to: 到 + label_language_based: æ ¹æ®ç”¨æˆ·çš„语言 + label_sort_by: "æ ¹æ® %{value} 排åº" + label_send_test_email: å‘逿µ‹è¯•邮件 + label_feeds_access_key: RSSå­˜å–é”® + label_missing_feeds_access_key: 缺少RSSå­˜å–é”® + label_feeds_access_key_created_on: "RSSå­˜å–键是在 %{value} 之å‰å»ºç«‹çš„" + label_module_plural: æ¨¡å— + label_added_time_by: "ç”± %{author} 在 %{age} 之剿·»åŠ " + label_updated_time: " 更新于 %{value} 之å‰" + label_updated_time_by: "ç”± %{author} 更新于 %{age} 之å‰" + label_jump_to_a_project: 选择一个项目... + label_file_plural: 文件 + label_changeset_plural: å˜æ›´ + label_default_columns: 默认列 + label_no_change_option: (ä¸å˜) + label_bulk_edit_selected_issues: 批é‡ä¿®æ”¹é€‰ä¸­çš„问题 + label_theme: 主题 + label_default: 默认 + label_search_titles_only: 仅在标题中æœç´¢ + label_user_mail_option_all: "æ”¶å–æˆ‘的项目的所有通知" + label_user_mail_option_selected: "æ”¶å–选中项目的所有通知..." + label_user_mail_option_none: "䏿”¶å–任何通知" + label_user_mail_option_only_my_events: "åªæ”¶å–我跟踪或å‚与的项目的通知" + label_user_mail_option_only_assigned: "åªæ”¶å–分é…给我的" + label_user_mail_option_only_owner: åªæ”¶å–由我创建的 + label_user_mail_no_self_notified: "ä¸è¦å‘é€å¯¹æˆ‘自己æäº¤çš„修改的通知" + label_registration_activation_by_email: é€šè¿‡é‚®ä»¶è®¤è¯æ¿€æ´»å¸å· + label_registration_manual_activation: 手动激活å¸å· + label_registration_automatic_activation: 自动激活å¸å· + label_display_per_page: "æ¯é¡µæ˜¾ç¤ºï¼š%{value}" + label_age: æäº¤æ—¶é—´ + label_change_properties: 修改属性 + label_general: 一般 + label_more: 更多 + label_scm: SCM + label_plugins: æ’ä»¶ + label_ldap_authentication: LDAP è®¤è¯ + label_downloads_abbr: D/L + label_optional_description: å¯é€‰çš„æè¿° + label_add_another_file: 添加其它文件 + label_preferences: 首选项 + label_chronological_order: æŒ‰æ—¶é—´é¡ºåº + label_reverse_chronological_order: 按时间顺åºï¼ˆå€’åºï¼‰ + label_planning: 计划 + label_incoming_emails: 接收邮件 + label_generate_key: 生æˆä¸€ä¸ªkey + label_issue_watchers: 跟踪者 + label_example: 示例 + label_display: 显示 + label_sort: æŽ’åº + label_ascending: å‡åº + label_descending: é™åº + label_date_from_to: 从 %{start} 到 %{end} + label_wiki_content_added: Wiki 页é¢å·²æ·»åŠ  + label_wiki_content_updated: Wiki 页é¢å·²æ›´æ–° + label_group: 组 + label_group_plural: 组 + label_group_new: 新建组 + label_time_entry_plural: 耗时 + label_version_sharing_none: ä¸å…±äº« + label_version_sharing_descendants: 与å­é¡¹ç›®å…±äº« + label_version_sharing_hierarchy: 与项目继承层次共享 + label_version_sharing_tree: 与项目树共享 + label_version_sharing_system: 与所有项目共享 + label_update_issue_done_ratios: 更新问题的完æˆåº¦ + label_copy_source: æº + label_copy_target: 目标 + label_copy_same_as_target: 与目标一致 + label_display_used_statuses_only: åªæ˜¾ç¤ºè¢«æ­¤è·Ÿè¸ªæ ‡ç­¾ä½¿ç”¨çš„çŠ¶æ€ + label_api_access_key: API访问键 + label_missing_api_access_key: 缺少API访问键 + label_api_access_key_created_on: API访问键是在 %{value} 之å‰å»ºç«‹çš„ + label_profile: 简介 + label_subtask_plural: å­ä»»åŠ¡ + label_project_copy_notifications: å¤åˆ¶é¡¹ç›®æ—¶å‘é€é‚®ä»¶é€šçŸ¥ + label_principal_search: "æœç´¢ç”¨æˆ·æˆ–组:" + label_user_search: "æœç´¢ç”¨æˆ·ï¼š" + + button_login: 登录 + button_submit: æäº¤ + button_save: ä¿å­˜ + button_check_all: 全选 + button_uncheck_all: 清除 + button_delete: 删除 + button_create: 创建 + button_create_and_continue: 创建并继续 + button_test: 测试 + button_edit: 编辑 + button_edit_associated_wikipage: "编辑相关wiki页é¢: %{page_title}" + button_add: 新增 + button_change: 修改 + button_apply: 应用 + button_clear: 清除 + button_lock: é”定 + button_unlock: è§£é” + button_download: 下载 + button_list: 列表 + button_view: 查看 + button_move: 移动 + button_move_and_follow: 移动并转到新问题 + button_back: 返回 + button_cancel: å–æ¶ˆ + button_activate: 激活 + button_sort: æŽ’åº + button_log_time: 登记工时 + button_rollback: æ¢å¤åˆ°è¿™ä¸ªç‰ˆæœ¬ + button_watch: 跟踪 + button_unwatch: å–æ¶ˆè·Ÿè¸ª + button_reply: å›žå¤ + button_archive: 存档 + button_unarchive: å–æ¶ˆå­˜æ¡£ + button_reset: é‡ç½® + button_rename: é‡å‘½å/é‡å®šå‘ + button_change_password: ä¿®æ”¹å¯†ç  + button_copy: å¤åˆ¶ + button_copy_and_follow: å¤åˆ¶å¹¶è½¬åˆ°æ–°é—®é¢˜ + button_annotate: 追溯 + button_update: æ›´æ–° + button_configure: é…ç½® + button_quote: 引用 + button_duplicate: 副本 + button_show: 显示 + + status_active: 活动的 + status_registered: 已注册 + status_locked: å·²é”定 + + version_status_open: 打开 + version_status_locked: é”定 + version_status_closed: 关闭 + + field_active: 活动 + + text_select_mail_notifications: 选择需è¦å‘é€é‚®ä»¶é€šçŸ¥çš„动作 + text_regexp_info: 例如:^[A-Z0-9]+$ + text_min_max_length_info: 0 表示没有é™åˆ¶ + text_project_destroy_confirmation: 您确信è¦åˆ é™¤è¿™ä¸ªé¡¹ç›®ä»¥åŠæ‰€æœ‰ç›¸å…³çš„æ•°æ®å—? + text_subprojects_destroy_warning: "以下å­é¡¹ç›®ä¹Ÿå°†è¢«åŒæ—¶åˆ é™¤ï¼š%{value}" + text_workflow_edit: 选择角色和跟踪标签æ¥ç¼–辑工作æµç¨‹ + text_are_you_sure: 您确定? + text_journal_changed: "%{label} 从 %{old} å˜æ›´ä¸º %{new}" + text_journal_set_to: "%{label} 被设置为 %{value}" + text_journal_deleted: "%{label} 已删除 (%{old})" + text_journal_added: "%{label} %{value} 已添加" + text_tip_issue_begin_day: 今天开始的任务 + text_tip_issue_end_day: 今天结æŸçš„任务 + text_tip_issue_begin_end_day: 今天开始并结æŸçš„任务 + text_caracters_maximum: "最多 %{count} 个字符。" + text_caracters_minimum: "è‡³å°‘éœ€è¦ %{count} 个字符。" + text_length_between: "长度必须在 %{min} 到 %{max} 个字符之间。" + text_tracker_no_workflow: 此跟踪标签未定义工作æµç¨‹ + text_unallowed_characters: éžæ³•字符 + text_comma_separated: å¯ä»¥ä½¿ç”¨å¤šä¸ªå€¼ï¼ˆç”¨é€—å·,分开)。 + text_line_separated: å¯ä»¥ä½¿ç”¨å¤šä¸ªå€¼ï¼ˆæ¯è¡Œä¸€ä¸ªå€¼ï¼‰ã€‚ + text_issues_ref_in_commit_messages: 在æäº¤ä¿¡æ¯ä¸­å¼•用和解决问题 + text_issue_added: "问题 %{id} 已由 %{author} æäº¤ã€‚" + text_issue_updated: "问题 %{id} 已由 %{author} 更新。" + text_wiki_destroy_confirmation: 您确定è¦åˆ é™¤è¿™ä¸ª wiki åŠå…¶æ‰€æœ‰å†…容å—? + text_issue_category_destroy_question: "有一些问题(%{count} ä¸ªï¼‰å±žäºŽæ­¤ç±»åˆ«ã€‚æ‚¨æƒ³è¿›è¡Œå“ªç§æ“作?" + text_issue_category_destroy_assignments: 删除问题的所属类别(问题å˜ä¸ºæ— ç±»åˆ«ï¼‰ + text_issue_category_reassign_to: 为问题选择其它类别 + text_user_mail_option: "对于没有选中的项目,您将åªä¼šæ”¶åˆ°æ‚¨è·Ÿè¸ªæˆ–å‚与的项目的通知(比如说,您是问题的报告者, 或被指派解决此问题)。" + text_no_configuration_data: "角色ã€è·Ÿè¸ªæ ‡ç­¾ã€é—®é¢˜çжæ€å’Œå·¥ä½œæµç¨‹è¿˜æ²¡æœ‰è®¾ç½®ã€‚\n强烈建议您先载入默认设置,然åŽåœ¨æ­¤åŸºç¡€ä¸Šè¿›è¡Œä¿®æ”¹ã€‚" + text_load_default_configuration: 载入默认设置 + text_status_changed_by_changeset: "å·²åº”ç”¨åˆ°å˜æ›´åˆ—表 %{value}." + text_time_logged_by_changeset: "已应用到修订版本 %{value}." + text_issues_destroy_confirmation: '您确定è¦åˆ é™¤é€‰ä¸­çš„问题å—?' + text_select_project_modules: '请选择此项目å¯ä»¥ä½¿ç”¨çš„æ¨¡å—:' + text_default_administrator_account_changed: 默认的管ç†å‘˜å¸å·å·²æ”¹å˜ + text_file_repository_writable: 附件路径å¯å†™ + text_plugin_assets_writable: æ’件的附件路径å¯å†™ + text_rmagick_available: RMagick å¯ç”¨ï¼ˆå¯é€‰çš„) + text_destroy_time_entries_question: 您è¦åˆ é™¤çš„问题已ç»ä¸ŠæŠ¥äº† %{hours} å°æ—¶çš„工作é‡ã€‚æ‚¨æƒ³è¿›è¡Œé‚£ç§æ“作? + text_destroy_time_entries: åˆ é™¤ä¸ŠæŠ¥çš„å·¥ä½œé‡ + text_assign_time_entries_to_project: å°†å·²ä¸ŠæŠ¥çš„å·¥ä½œé‡æäº¤åˆ°é¡¹ç›®ä¸­ + text_reassign_time_entries: 'å°†å·²ä¸ŠæŠ¥çš„å·¥ä½œé‡æŒ‡å®šåˆ°æ­¤é—®é¢˜ï¼š' + text_user_wrote: "%{value} 写到:" + text_enumeration_destroy_question: "%{count} 个对象被关è”到了这个枚举值。" + text_enumeration_category_reassign_to: '将它们关è”到新的枚举值:' + text_email_delivery_not_configured: "邮件傿•°å°šæœªé…置,因此邮件通知功能已被ç¦ç”¨ã€‚\n请在config/configuration.yml中é…置您的SMTPæœåŠ¡å™¨ä¿¡æ¯å¹¶é‡æ–°å¯åŠ¨ä»¥ä½¿å…¶ç”Ÿæ•ˆã€‚" + text_repository_usernames_mapping: "选择或更新与版本库中的用户å对应的Redmine用户。\n版本库中与Redmine中的åŒå用户将被自动对应。" + text_diff_truncated: '... å·®åˆ«å†…å®¹è¶…è¿‡äº†å¯æ˜¾ç¤ºçš„æœ€å¤§è¡Œæ•°å¹¶å·²è¢«æˆªæ–­' + text_custom_field_possible_values_info: 'æ¯é¡¹æ•°å€¼ä¸€è¡Œ' + text_wiki_page_destroy_question: æ­¤é¡µé¢æœ‰ %{descendants} 个å­é¡µé¢å’Œä¸‹çº§é¡µé¢ã€‚æ‚¨æƒ³è¿›è¡Œé‚£ç§æ“作? + text_wiki_page_nullify_children: å°†å­é¡µé¢ä¿ç•™ä¸ºæ ¹é¡µé¢ + text_wiki_page_destroy_children: 删除å­é¡µé¢åŠå…¶æ‰€æœ‰ä¸‹çº§é¡µé¢ + text_wiki_page_reassign_children: å°†å­é¡µé¢çš„上级页é¢è®¾ç½®ä¸º + text_own_membership_delete_confirmation: 你正在删除你现有的æŸäº›æˆ–全部æƒé™ï¼Œå¦‚果这样åšäº†ä½ å¯èƒ½å°†ä¼šå†ä¹Ÿæ— æ³•编辑该项目了。你确定è¦ç»§ç»­å—? + text_zoom_in: 放大 + text_zoom_out: ç¼©å° + + default_role_manager: 管ç†äººå‘˜ + default_role_developer: å¼€å‘人员 + default_role_reporter: 报告人员 + default_tracker_bug: 错误 + default_tracker_feature: 功能 + default_tracker_support: æ”¯æŒ + default_issue_status_new: 新建 + default_issue_status_in_progress: 进行中 + default_issue_status_resolved: 已解决 + default_issue_status_feedback: å馈 + default_issue_status_closed: 已关闭 + default_issue_status_rejected: å·²æ‹’ç» + default_doc_category_user: 用户文档 + default_doc_category_tech: 技术文档 + default_priority_low: 低 + default_priority_normal: 普通 + default_priority_high: 高 + default_priority_urgent: 紧急 + default_priority_immediate: 立刻 + default_activity_design: 设计 + default_activity_development: å¼€å‘ + + enumeration_issue_priorities: 问题优先级 + enumeration_doc_categories: 文档类别 + enumeration_activities: 活动(时间跟踪) + enumeration_system_activity: 系统活动 + + field_warn_on_leaving_unsaved: 当离开未ä¿å­˜å†…å®¹çš„é¡µé¢æ—¶ï¼Œæç¤ºæˆ‘ + text_warn_on_leaving_unsaved: 若离开当å‰é¡µé¢ï¼Œåˆ™è¯¥é¡µé¢å†…未ä¿å­˜çš„内容将丢失。 + label_my_queries: 我的自定义查询 + text_journal_changed_no_detail: "%{label} 已更新。" + label_news_comment_added: 添加到新闻的评论 + button_expand_all: 展开所有 + button_collapse_all: åˆæ‹¢æ‰€æœ‰ + label_additional_workflow_transitions_for_assignee: 当用户是问题的分é…对象时所å…许的问题状æ€è½¬æ¢ + label_additional_workflow_transitions_for_author: 当用户是问题作者时所å…许的问题状æ€è½¬æ¢ + label_bulk_edit_selected_time_entries: 批é‡ä¿®æ”¹é€‰å®šçš„æ—¶é—´æ¡ç›® + text_time_entries_destroy_confirmation: 是å¦ç¡®å®šè¦åˆ é™¤é€‰å®šçš„æ—¶é—´æ¡ç›®ï¼Ÿ + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: 问题备注已添加 + label_issue_status_updated: é—®é¢˜çŠ¶æ€æ›´æ–° + label_issue_priority_updated: 问题优先级更新 + label_issues_visibility_own: 创建或分é…给用户的问题 + field_issues_visibility: 问题å¯è§ + label_issues_visibility_all: 全部问题 + permission_set_own_issues_private: è®¾ç½®è‡ªå·±çš„é—®é¢˜ä¸ºå…¬å¼€æˆ–ç§æœ‰ + field_is_private: ç§æœ‰ + permission_set_issues_private: è®¾ç½®é—®é¢˜ä¸ºå…¬å¼€æˆ–ç§æœ‰ + label_issues_visibility_public: 全部éžç§æœ‰é—®é¢˜ + text_issues_destroy_descendants_confirmation: æ­¤æ“ä½œåŒæ—¶ä¼šåˆ é™¤ %{count} 个å­ä»»åŠ¡ã€‚ + + field_commit_logs_encoding: æäº¤æ³¨é‡Šçš„ç¼–ç  + field_scm_path_encoding: è·¯å¾„ç¼–ç  + text_scm_path_encoding_note: "默认: UTF-8" + field_path_to_repository: 库路径 + field_root_directory: 根目录 + field_cvs_module: CVS Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: 本地库 (e.g. /hgrepo, c:\hgrepo) + text_scm_command: 命令 + text_scm_command_version: 版本 + label_git_report_last_commit: 报告最åŽä¸€æ¬¡æ–‡ä»¶/目录æäº¤ + text_scm_config: 您å¯ä»¥åœ¨config/configuration.yml中é…置您的SCM命令。 请在编辑åŽï¼Œé‡å¯Redmine应用。 + text_scm_command_not_available: Scm命令ä¸å¯ç”¨ã€‚ 请检查管ç†é¢æ¿çš„é…置。 + text_git_repository_note: 库中无内容。(e.g. /gitrepo, c:\gitrepo) + notice_issue_successful_create: 问题 %{id} 已创建。 + label_between: 介于 + setting_issue_group_assignment: å…许问题被分é…给组 + label_diff: diff + description_query_sort_criteria_direction: æŽ’åºæ–¹å¼ + description_project_scope: æœç´¢èŒƒå›´ + description_filter: 过滤器 + description_user_mail_notification: 邮件通知设置 + description_date_from: 输入开始日期 + description_message_content: ä¿¡æ¯å†…容 + description_available_columns: 备选列 + description_date_range_interval: æŒ‰å¼€å§‹æ—¥æœŸå’Œç»“æŸæ—¥æœŸé€‰æ‹©èŒƒå›´ + description_issue_category_reassign: 选择问题类别 + description_search: æœç´¢å­—段 + description_notes: 批注 + description_date_range_list: 从列表中选择范围 + description_choose_project: 项目 + description_date_to: è¾“å…¥ç»“æŸæ—¥æœŸ + description_query_sort_criteria_attribute: æŽ’åºæ–¹å¼ + description_wiki_subpages_reassign: é€‰æ‹©çˆ¶é¡µé¢ + description_selected_columns: 已选列 + label_parent_revision: 父修订 + label_child_revision: å­ä¿®è®¢ + error_scm_annotate_big_text_file: 输入文本内容超长,无法输入。 + setting_default_issue_start_date_to_creation_date: ä½¿ç”¨å½“å‰æ—¥æœŸä½œä¸ºæ–°é—®é¢˜çš„开始日期 + button_edit_section: 编辑此区域 + setting_repositories_encodings: é™„ä»¶å’Œç‰ˆæœ¬åº“ç¼–ç  + description_all_columns: 所有列 + button_export: 导出 + label_export_options: "%{export_format} 导出选项" + error_attachment_too_big: 该文件无法上传。超过文件大å°é™åˆ¶ (%{max_size}) + notice_failed_to_save_time_entries: "无法ä¿å­˜ä¸‹åˆ—所选å–çš„ %{total} 个项目中的 %{count} 工时: %{ids}。" + label_x_issues: + zero: 0 问题 + one: 1 问题 + other: "%{count} 问题" + label_repository_new: 新建版本库 + field_repository_is_default: 主版本库 + label_copy_attachments: å¤åˆ¶é™„ä»¶ + label_item_position: "%{position}/%{count}" + label_completed_versions: 已完æˆçš„版本 + text_project_identifier_info: ä»…å°å†™å­—æ¯ï¼ˆa-zï¼‰ã€æ•°å­—ã€ç ´æŠ˜å·ï¼ˆ-)和下划线(_)å¯ä»¥ä½¿ç”¨ã€‚
    一旦ä¿å­˜ï¼Œæ ‡è¯†æ— æ³•修改。 + field_multiple: 多é‡å–值 + setting_commit_cross_project_ref: å…许引用/ä¿®å¤æ‰€æœ‰å…¶ä»–项目的问题 + text_issue_conflict_resolution_add_notes: æ·»åŠ è¯´æ˜Žå¹¶å–æ¶ˆæˆ‘çš„å…¶ä»–å˜æ›´å¤„ç†ã€‚ + text_issue_conflict_resolution_overwrite: ç›´æŽ¥å¥—ç”¨æˆ‘çš„å˜æ›´ (先å‰çš„说明将被ä¿ç•™ï¼Œä½†æ˜¯æŸäº›å˜æ›´å†…容å¯èƒ½ä¼šè¢«è¦†ç›–) + notice_issue_update_conflict: 当您正在编辑这个问题的时候,它已ç»è¢«å…¶ä»–人抢先一步更新过了。 + text_issue_conflict_resolution_cancel: å–æ¶ˆæˆ‘æ‰€æœ‰çš„å˜æ›´å¹¶é‡æ–°åˆ·æ–°æ˜¾ç¤º %{link} 。 + permission_manage_related_issues: ç›¸å…³é—®é¢˜ç®¡ç† + field_auth_source_ldap_filter: LDAP 过滤器 + label_search_for_watchers: é€šè¿‡æŸ¥æ‰¾æ–¹å¼æ·»åŠ è·Ÿè¸ªè€… + notice_account_deleted: 您的账å·å·²è¢«æ°¸ä¹…删除(账å·å·²æ— æ³•æ¢å¤ï¼‰ã€‚ + setting_unsubscribe: å…许用户退订 + button_delete_my_account: åˆ é™¤æˆ‘çš„è´¦å· + text_account_destroy_confirmation: |- + 确定继续处ç†ï¼Ÿ + 您的账å·ä¸€æ—¦åˆ é™¤ï¼Œå°†æ— æ³•冿¬¡æ¿€æ´»ä½¿ç”¨ã€‚ + error_session_expired: 您的会è¯å·²è¿‡æœŸã€‚è¯·é‡æ–°ç™»é™†ã€‚ + text_session_expiration_settings: "警告: 更改这些设置将会使包括你在内的当å‰ä¼šè¯å¤±æ•ˆã€‚" + setting_session_lifetime: ä¼šè¯æœ€å¤§æœ‰æ•ˆæ—¶é—´ + setting_session_timeout: 会è¯é—²ç½®è¶…æ—¶ + label_session_expiration: 会è¯è¿‡æœŸ + permission_close_project: 关闭/é‡å¼€é¡¹ç›® + label_show_closed_projects: 查看已关闭的项目 + button_close: 关闭 + button_reopen: é‡å¼€ + project_status_active: 已激活 + project_status_closed: 已关闭 + project_status_archived: 已存档 + text_project_closed: 当å‰é¡¹ç›®å·²è¢«å…³é—­ã€‚当å‰é¡¹ç›®åªè¯»ã€‚ + notice_user_successful_create: 用户 %{id} 已创建。 + field_core_fields: 标准字段 + field_timeout: è¶…æ—¶ (ç§’) + setting_thumbnails_enabled: 显示附件略缩图 + setting_thumbnails_size: 略缩图尺寸 (åƒç´ ) + label_status_transitions: 状æ€è½¬æ¢ + label_fields_permissions: 字段æƒé™ + label_readonly: åªè¯» + label_required: å¿…å¡« + text_repository_identifier_info: ä»…å°å†™å­—æ¯ï¼ˆa-zï¼‰ã€æ•°å­—ã€ç ´æŠ˜å·ï¼ˆ-)和下划线(_)å¯ä»¥ä½¿ç”¨ã€‚
    一旦ä¿å­˜ï¼Œæ ‡è¯†æ— æ³•修改。 + field_board_parent: çˆ¶è®ºå› + label_attribute_of_project: 项目 %{name} + label_attribute_of_author: 作者 %{name} + label_attribute_of_assigned_to: 分é…ç»™ %{name} + label_attribute_of_fixed_version: 目标版本 %{name} + label_copy_subtasks: å¤åˆ¶å­ä»»åŠ¡ + label_copied_to: å¤åˆ¶åˆ° + label_copied_from: å¤åˆ¶äºŽ + label_any_issues_in_project: 项目内任æ„问题 + label_any_issues_not_in_project: 项目外任æ„问题 + field_private_notes: ç§æœ‰æ³¨è§£ + permission_view_private_notes: æŸ¥çœ‹ç§æœ‰æ³¨è§£ + permission_set_notes_private: è®¾ç½®ä¸ºç§æœ‰æ³¨è§£ + label_no_issues_in_project: 项目内无相关问题 + label_any: 全部 + label_last_n_weeks: 上 %{count} å‘¨å‰ + setting_cross_project_subtasks: 支æŒè·¨é¡¹ç›®å­ä»»åŠ¡ + label_cross_project_descendants: 与å­é¡¹ç›®å…±äº« + label_cross_project_tree: 与项目树共享 + label_cross_project_hierarchy: 与项目继承层次共享 + label_cross_project_system: 与所有项目共享 + button_hide: éšè— + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/68/68105afd35bd8d0018fe2385a45c1f9d6a9df7cd.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/68/68105afd35bd8d0018fe2385a45c1f9d6a9df7cd.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,6 @@ +

    <%= l(:label_board_new) %>

    + +<%= labelled_form_for @board, :url => project_boards_path(@project) do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_create) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/69/6904e863535549693b29c5f01931c820124ef164.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/69/6904e863535549693b29c5f01931c820124ef164.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1 @@ +$('#tab-content-wiki').html('<%= escape_javascript(render :partial => 'projects/settings/wiki') %>'); diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/69/69263d2dc0d479e58420859c9a2d96157091cbd5.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/69/69263d2dc0d479e58420859c9a2d96157091cbd5.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,19 @@ +api.array :versions, api_meta(:total_count => @versions.size) do + @versions.each do |version| + api.version do + api.id version.id + api.project(:id => version.project_id, :name => version.project.name) unless version.project.nil? + + api.name version.name + api.description version.description + api.status version.status + api.due_date version.effective_date + api.sharing version.sharing + + render_api_custom_values version.custom_field_values, api + + api.created_on version.created_on + api.updated_on version.updated_on + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/69/6928f5ba5fc17fb66a0a7ba070cce99ed775980c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/69/6928f5ba5fc17fb66a0a7ba070cce99ed775980c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,240 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingTimelogsTest < ActionController::IntegrationTest + def test_timelogs_global + assert_routing( + { :method => 'get', :path => "/time_entries" }, + { :controller => 'timelog', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/time_entries.csv" }, + { :controller => 'timelog', :action => 'index', :format => 'csv' } + ) + assert_routing( + { :method => 'get', :path => "/time_entries.atom" }, + { :controller => 'timelog', :action => 'index', :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/time_entries/new" }, + { :controller => 'timelog', :action => 'new' } + ) + assert_routing( + { :method => 'get', :path => "/time_entries/22/edit" }, + { :controller => 'timelog', :action => 'edit', :id => '22' } + ) + assert_routing( + { :method => 'post', :path => "/time_entries" }, + { :controller => 'timelog', :action => 'create' } + ) + assert_routing( + { :method => 'put', :path => "/time_entries/22" }, + { :controller => 'timelog', :action => 'update', :id => '22' } + ) + assert_routing( + { :method => 'delete', :path => "/time_entries/55" }, + { :controller => 'timelog', :action => 'destroy', :id => '55' } + ) + end + + def test_timelogs_scoped_under_project + assert_routing( + { :method => 'get', :path => "/projects/567/time_entries" }, + { :controller => 'timelog', :action => 'index', :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/time_entries.csv" }, + { :controller => 'timelog', :action => 'index', :project_id => '567', + :format => 'csv' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/time_entries.atom" }, + { :controller => 'timelog', :action => 'index', :project_id => '567', + :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/time_entries/new" }, + { :controller => 'timelog', :action => 'new', :project_id => '567' } + ) + assert_routing( + { :method => 'get', :path => "/projects/567/time_entries/22/edit" }, + { :controller => 'timelog', :action => 'edit', + :id => '22', :project_id => '567' } + ) + assert_routing( + { :method => 'post', :path => "/projects/567/time_entries" }, + { :controller => 'timelog', :action => 'create', + :project_id => '567' } + ) + assert_routing( + { :method => 'put', :path => "/projects/567/time_entries/22" }, + { :controller => 'timelog', :action => 'update', + :id => '22', :project_id => '567' } + ) + assert_routing( + { :method => 'delete', :path => "/projects/567/time_entries/55" }, + { :controller => 'timelog', :action => 'destroy', + :id => '55', :project_id => '567' } + ) + end + + def test_timelogs_scoped_under_issues + assert_routing( + { :method => 'get', :path => "/issues/234/time_entries" }, + { :controller => 'timelog', :action => 'index', :issue_id => '234' } + ) + assert_routing( + { :method => 'get', :path => "/issues/234/time_entries.csv" }, + { :controller => 'timelog', :action => 'index', :issue_id => '234', + :format => 'csv' } + ) + assert_routing( + { :method => 'get', :path => "/issues/234/time_entries.atom" }, + { :controller => 'timelog', :action => 'index', :issue_id => '234', + :format => 'atom' } + ) + assert_routing( + { :method => 'get', :path => "/issues/234/time_entries/new" }, + { :controller => 'timelog', :action => 'new', :issue_id => '234' } + ) + assert_routing( + { :method => 'get', :path => "/issues/234/time_entries/22/edit" }, + { :controller => 'timelog', :action => 'edit', :id => '22', + :issue_id => '234' } + ) + assert_routing( + { :method => 'post', :path => "/issues/234/time_entries" }, + { :controller => 'timelog', :action => 'create', :issue_id => '234' } + ) + assert_routing( + { :method => 'put', :path => "/issues/234/time_entries/22" }, + { :controller => 'timelog', :action => 'update', :id => '22', + :issue_id => '234' } + ) + assert_routing( + { :method => 'delete', :path => "/issues/234/time_entries/55" }, + { :controller => 'timelog', :action => 'destroy', :id => '55', + :issue_id => '234' } + ) + end + + def test_timelogs_scoped_under_project_and_issues + assert_routing( + { :method => 'get', + :path => "/projects/ecookbook/issues/234/time_entries" }, + { :controller => 'timelog', :action => 'index', + :issue_id => '234', :project_id => 'ecookbook' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/ecookbook/issues/234/time_entries.csv" }, + { :controller => 'timelog', :action => 'index', + :issue_id => '234', :project_id => 'ecookbook', :format => 'csv' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/ecookbook/issues/234/time_entries.atom" }, + { :controller => 'timelog', :action => 'index', + :issue_id => '234', :project_id => 'ecookbook', :format => 'atom' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/ecookbook/issues/234/time_entries/new" }, + { :controller => 'timelog', :action => 'new', + :issue_id => '234', :project_id => 'ecookbook' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/ecookbook/issues/234/time_entries/22/edit" }, + { :controller => 'timelog', :action => 'edit', :id => '22', + :issue_id => '234', :project_id => 'ecookbook' } + ) + assert_routing( + { :method => 'post', + :path => "/projects/ecookbook/issues/234/time_entries" }, + { :controller => 'timelog', :action => 'create', + :issue_id => '234', :project_id => 'ecookbook' } + ) + assert_routing( + { :method => 'put', + :path => "/projects/ecookbook/issues/234/time_entries/22" }, + { :controller => 'timelog', :action => 'update', :id => '22', + :issue_id => '234', :project_id => 'ecookbook' } + ) + assert_routing( + { :method => 'delete', + :path => "/projects/ecookbook/issues/234/time_entries/55" }, + { :controller => 'timelog', :action => 'destroy', :id => '55', + :issue_id => '234', :project_id => 'ecookbook' } + ) + end + + def test_timelogs_report + assert_routing( + { :method => 'get', + :path => "/time_entries/report" }, + { :controller => 'timelog', :action => 'report' } + ) + assert_routing( + { :method => 'get', + :path => "/time_entries/report.csv" }, + { :controller => 'timelog', :action => 'report', :format => 'csv' } + ) + assert_routing( + { :method => 'get', + :path => "/issues/234/time_entries/report" }, + { :controller => 'timelog', :action => 'report', :issue_id => '234' } + ) + assert_routing( + { :method => 'get', + :path => "/issues/234/time_entries/report.csv" }, + { :controller => 'timelog', :action => 'report', :issue_id => '234', + :format => 'csv' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/567/time_entries/report" }, + { :controller => 'timelog', :action => 'report', :project_id => '567' } + ) + assert_routing( + { :method => 'get', + :path => "/projects/567/time_entries/report.csv" }, + { :controller => 'timelog', :action => 'report', :project_id => '567', + :format => 'csv' } + ) + end + + def test_timelogs_bulk_edit + assert_routing( + { :method => 'delete', + :path => "/time_entries/destroy" }, + { :controller => 'timelog', :action => 'destroy' } + ) + assert_routing( + { :method => 'post', + :path => "/time_entries/bulk_update" }, + { :controller => 'timelog', :action => 'bulk_update' } + ) + assert_routing( + { :method => 'get', + :path => "/time_entries/bulk_edit" }, + { :controller => 'timelog', :action => 'bulk_edit' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/69/693231f9e49b340c1c563f401aa7c54ec7a66549.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/69/693231f9e49b340c1c563f401aa7c54ec7a66549.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,22 @@ +# -*- encoding: utf-8 -*- +lib = File.expand_path('../lib/', __FILE__) +$:.unshift lib unless $:.include?(lib) +require 'awesome_nested_set/version' + +Gem::Specification.new do |s| + s.name = %q{awesome_nested_set} + s.version = ::AwesomeNestedSet::VERSION + s.authors = ["Brandon Keepers", "Daniel Morrison", "Philip Arndt"] + s.description = %q{An awesome nested set implementation for Active Record} + s.email = %q{info@collectiveidea.com} + s.extra_rdoc_files = [ + "README.rdoc" + ] + s.files = Dir.glob("lib/**/*") + %w(MIT-LICENSE README.rdoc CHANGELOG) + s.homepage = %q{http://github.com/collectiveidea/awesome_nested_set} + s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"] + s.require_paths = ["lib"] + s.rubygems_version = %q{1.3.6} + s.summary = %q{An awesome nested set implementation for Active Record} + s.add_runtime_dependency 'activerecord', '>= 3.0.0' +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/69/694cd2c99623d577c90a26a298a76c97fefa807e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/69/694cd2c99623d577c90a26a298a76c97fefa807e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,927 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) +require 'wiki_controller' + +# Re-raise errors caught by the controller. +class WikiController; def rescue_action(e) raise e end; end + +class WikiControllerTest < ActionController::TestCase + fixtures :projects, :users, :roles, :members, :member_roles, + :enabled_modules, :wikis, :wiki_pages, :wiki_contents, + :wiki_content_versions, :attachments + + def setup + @controller = WikiController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_show_start_page + get :show, :project_id => 'ecookbook' + assert_response :success + assert_template 'show' + assert_tag :tag => 'h1', :content => /CookBook documentation/ + + # child_pages macro + assert_tag :ul, :attributes => { :class => 'pages-hierarchy' }, + :child => { :tag => 'li', + :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' }, + :content => 'Page with an inline image' } } + end + + def test_export_link + Role.anonymous.add_permission! :export_wiki_pages + get :show, :project_id => 'ecookbook' + assert_response :success + assert_tag 'a', :attributes => {:href => '/projects/ecookbook/wiki/CookBook_documentation.txt'} + end + + def test_show_page_with_name + get :show, :project_id => 1, :id => 'Another_page' + assert_response :success + assert_template 'show' + assert_tag :tag => 'h1', :content => /Another page/ + # Included page with an inline image + assert_tag :tag => 'p', :content => /This is an inline image/ + assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3', + :alt => 'This is a logo' } + end + + def test_show_old_version + get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '2' + assert_response :success + assert_template 'show' + + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/1', :text => /Previous/ + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/diff', :text => /diff/ + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/3', :text => /Next/ + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/ + end + + def test_show_old_version_without_permission_should_be_denied + Role.anonymous.remove_permission! :view_wiki_edits + + get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '2' + assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fprojects%2Fecookbook%2Fwiki%2FCookBook_documentation%2F2' + end + + def test_show_first_version + get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '1' + assert_response :success + assert_template 'show' + + assert_select 'a', :text => /Previous/, :count => 0 + assert_select 'a', :text => /diff/, :count => 0 + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => /Next/ + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/ + end + + def test_show_redirected_page + WikiRedirect.create!(:wiki_id => 1, :title => 'Old_title', :redirects_to => 'Another_page') + + get :show, :project_id => 'ecookbook', :id => 'Old_title' + assert_redirected_to '/projects/ecookbook/wiki/Another_page' + end + + def test_show_with_sidebar + page = Project.find(1).wiki.pages.new(:title => 'Sidebar') + page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar') + page.save! + + get :show, :project_id => 1, :id => 'Another_page' + assert_response :success + assert_tag :tag => 'div', :attributes => {:id => 'sidebar'}, + :content => /Side bar content for test_show_with_sidebar/ + end + + def test_show_should_display_section_edit_links + @request.session[:user_id] = 2 + get :show, :project_id => 1, :id => 'Page with sections' + assert_no_tag 'a', :attributes => { + :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=1' + } + assert_tag 'a', :attributes => { + :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2' + } + assert_tag 'a', :attributes => { + :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=3' + } + end + + def test_show_current_version_should_display_section_edit_links + @request.session[:user_id] = 2 + get :show, :project_id => 1, :id => 'Page with sections', :version => 3 + + assert_tag 'a', :attributes => { + :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2' + } + end + + def test_show_old_version_should_not_display_section_edit_links + @request.session[:user_id] = 2 + get :show, :project_id => 1, :id => 'Page with sections', :version => 2 + + assert_no_tag 'a', :attributes => { + :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2' + } + end + + def test_show_unexistent_page_without_edit_right + get :show, :project_id => 1, :id => 'Unexistent page' + assert_response 404 + end + + def test_show_unexistent_page_with_edit_right + @request.session[:user_id] = 2 + get :show, :project_id => 1, :id => 'Unexistent page' + assert_response :success + assert_template 'edit' + end + + def test_show_unexistent_page_with_parent_should_preselect_parent + @request.session[:user_id] = 2 + get :show, :project_id => 1, :id => 'Unexistent page', :parent => 'Another_page' + assert_response :success + assert_template 'edit' + assert_tag 'select', :attributes => {:name => 'wiki_page[parent_id]'}, + :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}} + end + + def test_show_should_not_show_history_without_permission + Role.anonymous.remove_permission! :view_wiki_edits + get :show, :project_id => 1, :id => 'Page with sections', :version => 2 + + assert_response 302 + end + + def test_create_page + @request.session[:user_id] = 2 + assert_difference 'WikiPage.count' do + assert_difference 'WikiContent.count' do + put :update, :project_id => 1, + :id => 'New page', + :content => {:comments => 'Created the page', + :text => "h1. New page\n\nThis is a new page", + :version => 0} + end + end + assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'New_page' + page = Project.find(1).wiki.find_page('New page') + assert !page.new_record? + assert_not_nil page.content + assert_nil page.parent + assert_equal 'Created the page', page.content.comments + end + + def test_create_page_with_attachments + @request.session[:user_id] = 2 + assert_difference 'WikiPage.count' do + assert_difference 'Attachment.count' do + put :update, :project_id => 1, + :id => 'New page', + :content => {:comments => 'Created the page', + :text => "h1. New page\n\nThis is a new page", + :version => 0}, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}} + end + end + page = Project.find(1).wiki.find_page('New page') + assert_equal 1, page.attachments.count + assert_equal 'testfile.txt', page.attachments.first.filename + end + + def test_create_page_with_parent + @request.session[:user_id] = 2 + assert_difference 'WikiPage.count' do + put :update, :project_id => 1, :id => 'New page', + :content => {:text => "h1. New page\n\nThis is a new page", :version => 0}, + :wiki_page => {:parent_id => 2} + end + page = Project.find(1).wiki.find_page('New page') + assert_equal WikiPage.find(2), page.parent + end + + def test_edit_page + @request.session[:user_id] = 2 + get :edit, :project_id => 'ecookbook', :id => 'Another_page' + + assert_response :success + assert_template 'edit' + + assert_tag 'textarea', + :attributes => { :name => 'content[text]' }, + :content => "\n"+WikiPage.find_by_title('Another_page').content.text + end + + def test_edit_section + @request.session[:user_id] = 2 + get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 2 + + assert_response :success + assert_template 'edit' + + page = WikiPage.find_by_title('Page_with_sections') + section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2) + + assert_tag 'textarea', + :attributes => { :name => 'content[text]' }, + :content => "\n"+section + assert_tag 'input', + :attributes => { :name => 'section', :type => 'hidden', :value => '2' } + assert_tag 'input', + :attributes => { :name => 'section_hash', :type => 'hidden', :value => hash } + end + + def test_edit_invalid_section_should_respond_with_404 + @request.session[:user_id] = 2 + get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 10 + + assert_response 404 + end + + def test_update_page + @request.session[:user_id] = 2 + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_difference 'WikiContent::Version.count' do + put :update, :project_id => 1, + :id => 'Another_page', + :content => { + :comments => "my comments", + :text => "edited", + :version => 1 + } + end + end + end + assert_redirected_to '/projects/ecookbook/wiki/Another_page' + + page = Wiki.find(1).pages.find_by_title('Another_page') + assert_equal "edited", page.content.text + assert_equal 2, page.content.version + assert_equal "my comments", page.content.comments + end + + def test_update_page_with_parent + @request.session[:user_id] = 2 + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_difference 'WikiContent::Version.count' do + put :update, :project_id => 1, + :id => 'Another_page', + :content => { + :comments => "my comments", + :text => "edited", + :version => 1 + }, + :wiki_page => {:parent_id => '1'} + end + end + end + assert_redirected_to '/projects/ecookbook/wiki/Another_page' + + page = Wiki.find(1).pages.find_by_title('Another_page') + assert_equal "edited", page.content.text + assert_equal 2, page.content.version + assert_equal "my comments", page.content.comments + assert_equal WikiPage.find(1), page.parent + end + + def test_update_page_with_failure + @request.session[:user_id] = 2 + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_no_difference 'WikiContent::Version.count' do + put :update, :project_id => 1, + :id => 'Another_page', + :content => { + :comments => 'a' * 300, # failure here, comment is too long + :text => 'edited', + :version => 1 + } + end + end + end + assert_response :success + assert_template 'edit' + + assert_error_tag :descendant => {:content => /Comment is too long/} + assert_tag :tag => 'textarea', :attributes => {:id => 'content_text'}, :content => "\nedited" + assert_tag :tag => 'input', :attributes => {:id => 'content_version', :value => '1'} + end + + def test_update_page_with_parent_change_only_should_not_create_content_version + @request.session[:user_id] = 2 + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_no_difference 'WikiContent::Version.count' do + put :update, :project_id => 1, + :id => 'Another_page', + :content => { + :comments => '', + :text => Wiki.find(1).find_page('Another_page').content.text, + :version => 1 + }, + :wiki_page => {:parent_id => '1'} + end + end + end + page = Wiki.find(1).pages.find_by_title('Another_page') + assert_equal 1, page.content.version + assert_equal WikiPage.find(1), page.parent + end + + def test_update_page_with_attachments_only_should_not_create_content_version + @request.session[:user_id] = 2 + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_no_difference 'WikiContent::Version.count' do + assert_difference 'Attachment.count' do + put :update, :project_id => 1, + :id => 'Another_page', + :content => { + :comments => '', + :text => Wiki.find(1).find_page('Another_page').content.text, + :version => 1 + }, + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}} + end + end + end + end + page = Wiki.find(1).pages.find_by_title('Another_page') + assert_equal 1, page.content.version + end + + def test_update_stale_page_should_not_raise_an_error + @request.session[:user_id] = 2 + c = Wiki.find(1).find_page('Another_page').content + c.text = 'Previous text' + c.save! + assert_equal 2, c.version + + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_no_difference 'WikiContent::Version.count' do + put :update, :project_id => 1, + :id => 'Another_page', + :content => { + :comments => 'My comments', + :text => 'Text should not be lost', + :version => 1 + } + end + end + end + assert_response :success + assert_template 'edit' + assert_tag :div, + :attributes => { :class => /error/ }, + :content => /Data has been updated by another user/ + assert_tag 'textarea', + :attributes => { :name => 'content[text]' }, + :content => /Text should not be lost/ + assert_tag 'input', + :attributes => { :name => 'content[comments]', :value => 'My comments' } + + c.reload + assert_equal 'Previous text', c.text + assert_equal 2, c.version + end + + def test_update_section + @request.session[:user_id] = 2 + page = WikiPage.find_by_title('Page_with_sections') + section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2) + text = page.content.text + + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_difference 'WikiContent::Version.count' do + put :update, :project_id => 1, :id => 'Page_with_sections', + :content => { + :text => "New section content", + :version => 3 + }, + :section => 2, + :section_hash => hash + end + end + end + assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections' + assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.reload.content.text + end + + def test_update_section_should_allow_stale_page_update + @request.session[:user_id] = 2 + page = WikiPage.find_by_title('Page_with_sections') + section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2) + text = page.content.text + + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_difference 'WikiContent::Version.count' do + put :update, :project_id => 1, :id => 'Page_with_sections', + :content => { + :text => "New section content", + :version => 2 # Current version is 3 + }, + :section => 2, + :section_hash => hash + end + end + end + assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections' + page.reload + assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.content.text + assert_equal 4, page.content.version + end + + def test_update_section_should_not_allow_stale_section_update + @request.session[:user_id] = 2 + + assert_no_difference 'WikiPage.count' do + assert_no_difference 'WikiContent.count' do + assert_no_difference 'WikiContent::Version.count' do + put :update, :project_id => 1, :id => 'Page_with_sections', + :content => { + :comments => 'My comments', + :text => "Text should not be lost", + :version => 3 + }, + :section => 2, + :section_hash => Digest::MD5.hexdigest("wrong hash") + end + end + end + assert_response :success + assert_template 'edit' + assert_tag :div, + :attributes => { :class => /error/ }, + :content => /Data has been updated by another user/ + assert_tag 'textarea', + :attributes => { :name => 'content[text]' }, + :content => /Text should not be lost/ + assert_tag 'input', + :attributes => { :name => 'content[comments]', :value => 'My comments' } + end + + def test_preview + @request.session[:user_id] = 2 + xhr :post, :preview, :project_id => 1, :id => 'CookBook_documentation', + :content => { :comments => '', + :text => 'this is a *previewed text*', + :version => 3 } + assert_response :success + assert_template 'common/_preview' + assert_tag :tag => 'strong', :content => /previewed text/ + end + + def test_preview_new_page + @request.session[:user_id] = 2 + xhr :post, :preview, :project_id => 1, :id => 'New page', + :content => { :text => 'h1. New page', + :comments => '', + :version => 0 } + assert_response :success + assert_template 'common/_preview' + assert_tag :tag => 'h1', :content => /New page/ + end + + def test_history + @request.session[:user_id] = 2 + get :history, :project_id => 'ecookbook', :id => 'CookBook_documentation' + assert_response :success + assert_template 'history' + assert_not_nil assigns(:versions) + assert_equal 3, assigns(:versions).size + + assert_select "input[type=submit][name=commit]" + assert_select 'td' do + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => '2' + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/annotate', :text => 'Annotate' + assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => 'Delete' + end + end + + def test_history_with_one_version + @request.session[:user_id] = 2 + get :history, :project_id => 'ecookbook', :id => 'Another_page' + assert_response :success + assert_template 'history' + assert_not_nil assigns(:versions) + assert_equal 1, assigns(:versions).size + assert_select "input[type=submit][name=commit]", false + assert_select 'td' do + assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1', :text => '1' + assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1/annotate', :text => 'Annotate' + assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1', :text => 'Delete', :count => 0 + end + end + + def test_diff + content = WikiPage.find(1).content + assert_difference 'WikiContent::Version.count', 2 do + content.text = "Line removed\nThis is a sample text for testing diffs" + content.save! + content.text = "This is a sample text for testing diffs\nLine added" + content.save! + end + + get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => content.version, :version_from => (content.version - 1) + assert_response :success + assert_template 'diff' + assert_select 'span.diff_out', :text => 'Line removed' + assert_select 'span.diff_in', :text => 'Line added' + end + + def test_diff_with_invalid_version_should_respond_with_404 + get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => '99' + assert_response 404 + end + + def test_diff_with_invalid_version_from_should_respond_with_404 + get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => '99', :version_from => '98' + assert_response 404 + end + + def test_annotate + get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => 2 + assert_response :success + assert_template 'annotate' + + # Line 1 + assert_tag :tag => 'tr', :child => { + :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1', :sibling => { + :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/, :sibling => { + :tag => 'td', :content => /h1\. CookBook documentation/ + } + } + } + + # Line 5 + assert_tag :tag => 'tr', :child => { + :tag => 'th', :attributes => {:class => 'line-num'}, :content => '5', :sibling => { + :tag => 'td', :attributes => {:class => 'author'}, :content => /redMine Admin/, :sibling => { + :tag => 'td', :content => /Some updated \[\[documentation\]\] here/ + } + } + } + end + + def test_annotate_with_invalid_version_should_respond_with_404 + get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => '99' + assert_response 404 + end + + def test_get_rename + @request.session[:user_id] = 2 + get :rename, :project_id => 1, :id => 'Another_page' + assert_response :success + assert_template 'rename' + assert_tag 'option', + :attributes => {:value => ''}, + :content => '', + :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}} + assert_no_tag 'option', + :attributes => {:selected => 'selected'}, + :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}} + end + + def test_get_rename_child_page + @request.session[:user_id] = 2 + get :rename, :project_id => 1, :id => 'Child_1' + assert_response :success + assert_template 'rename' + assert_tag 'option', + :attributes => {:value => ''}, + :content => '', + :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}} + assert_tag 'option', + :attributes => {:value => '2', :selected => 'selected'}, + :content => /Another page/, + :parent => { + :tag => 'select', + :attributes => {:name => 'wiki_page[parent_id]'} + } + end + + def test_rename_with_redirect + @request.session[:user_id] = 2 + post :rename, :project_id => 1, :id => 'Another_page', + :wiki_page => { :title => 'Another renamed page', + :redirect_existing_links => 1 } + assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page' + wiki = Project.find(1).wiki + # Check redirects + assert_not_nil wiki.find_page('Another page') + assert_nil wiki.find_page('Another page', :with_redirect => false) + end + + def test_rename_without_redirect + @request.session[:user_id] = 2 + post :rename, :project_id => 1, :id => 'Another_page', + :wiki_page => { :title => 'Another renamed page', + :redirect_existing_links => "0" } + assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page' + wiki = Project.find(1).wiki + # Check that there's no redirects + assert_nil wiki.find_page('Another page') + end + + def test_rename_with_parent_assignment + @request.session[:user_id] = 2 + post :rename, :project_id => 1, :id => 'Another_page', + :wiki_page => { :title => 'Another page', :redirect_existing_links => "0", :parent_id => '4' } + assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page' + assert_equal WikiPage.find(4), WikiPage.find_by_title('Another_page').parent + end + + def test_rename_with_parent_unassignment + @request.session[:user_id] = 2 + post :rename, :project_id => 1, :id => 'Child_1', + :wiki_page => { :title => 'Child 1', :redirect_existing_links => "0", :parent_id => '' } + assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Child_1' + assert_nil WikiPage.find_by_title('Child_1').parent + end + + def test_destroy_a_page_without_children_should_not_ask_confirmation + @request.session[:user_id] = 2 + delete :destroy, :project_id => 1, :id => 'Child_2' + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + end + + def test_destroy_parent_should_ask_confirmation + @request.session[:user_id] = 2 + assert_no_difference('WikiPage.count') do + delete :destroy, :project_id => 1, :id => 'Another_page' + end + assert_response :success + assert_template 'destroy' + assert_select 'form' do + assert_select 'input[name=todo][value=nullify]' + assert_select 'input[name=todo][value=destroy]' + assert_select 'input[name=todo][value=reassign]' + end + end + + def test_destroy_parent_with_nullify_should_delete_parent_only + @request.session[:user_id] = 2 + assert_difference('WikiPage.count', -1) do + delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'nullify' + end + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert_nil WikiPage.find_by_id(2) + end + + def test_destroy_parent_with_cascade_should_delete_descendants + @request.session[:user_id] = 2 + assert_difference('WikiPage.count', -4) do + delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'destroy' + end + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert_nil WikiPage.find_by_id(2) + assert_nil WikiPage.find_by_id(5) + end + + def test_destroy_parent_with_reassign + @request.session[:user_id] = 2 + assert_difference('WikiPage.count', -1) do + delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'reassign', :reassign_to_id => 1 + end + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert_nil WikiPage.find_by_id(2) + assert_equal WikiPage.find(1), WikiPage.find_by_id(5).parent + end + + def test_destroy_version + @request.session[:user_id] = 2 + assert_difference 'WikiContent::Version.count', -1 do + assert_no_difference 'WikiContent.count' do + assert_no_difference 'WikiPage.count' do + delete :destroy_version, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => 2 + assert_redirected_to '/projects/ecookbook/wiki/CookBook_documentation/history' + end + end + end + end + + def test_index + get :index, :project_id => 'ecookbook' + assert_response :success + assert_template 'index' + pages = assigns(:pages) + assert_not_nil pages + assert_equal Project.find(1).wiki.pages.size, pages.size + assert_equal pages.first.content.updated_on, pages.first.updated_on + + assert_tag :ul, :attributes => { :class => 'pages-hierarchy' }, + :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/CookBook_documentation' }, + :content => 'CookBook documentation' }, + :child => { :tag => 'ul', + :child => { :tag => 'li', + :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' }, + :content => 'Page with an inline image' } } } }, + :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Another_page' }, + :content => 'Another page' } } + end + + def test_index_should_include_atom_link + get :index, :project_id => 'ecookbook' + assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'} + end + + def test_export_to_html + @request.session[:user_id] = 2 + get :export, :project_id => 'ecookbook' + + assert_response :success + assert_not_nil assigns(:pages) + assert assigns(:pages).any? + assert_equal "text/html", @response.content_type + + assert_select "a[name=?]", "CookBook_documentation" + assert_select "a[name=?]", "Another_page" + assert_select "a[name=?]", "Page_with_an_inline_image" + end + + def test_export_to_pdf + @request.session[:user_id] = 2 + get :export, :project_id => 'ecookbook', :format => 'pdf' + + assert_response :success + assert_not_nil assigns(:pages) + assert assigns(:pages).any? + assert_equal 'application/pdf', @response.content_type + assert_equal 'attachment; filename="ecookbook.pdf"', @response.headers['Content-Disposition'] + assert @response.body.starts_with?('%PDF') + end + + def test_export_without_permission_should_be_denied + @request.session[:user_id] = 2 + Role.find_by_name('Manager').remove_permission! :export_wiki_pages + get :export, :project_id => 'ecookbook' + + assert_response 403 + end + + def test_date_index + get :date_index, :project_id => 'ecookbook' + + assert_response :success + assert_template 'date_index' + assert_not_nil assigns(:pages) + assert_not_nil assigns(:pages_by_date) + + assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'} + end + + def test_not_found + get :show, :project_id => 999 + assert_response 404 + end + + def test_protect_page + page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page') + assert !page.protected? + @request.session[:user_id] = 2 + post :protect, :project_id => 1, :id => page.title, :protected => '1' + assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page' + assert page.reload.protected? + end + + def test_unprotect_page + page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation') + assert page.protected? + @request.session[:user_id] = 2 + post :protect, :project_id => 1, :id => page.title, :protected => '0' + assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'CookBook_documentation' + assert !page.reload.protected? + end + + def test_show_page_with_edit_link + @request.session[:user_id] = 2 + get :show, :project_id => 1 + assert_response :success + assert_template 'show' + assert_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' } + end + + def test_show_page_without_edit_link + @request.session[:user_id] = 4 + get :show, :project_id => 1 + assert_response :success + assert_template 'show' + assert_no_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' } + end + + def test_show_pdf + @request.session[:user_id] = 2 + get :show, :project_id => 1, :format => 'pdf' + assert_response :success + assert_not_nil assigns(:page) + assert_equal 'application/pdf', @response.content_type + assert_equal 'attachment; filename="CookBook_documentation.pdf"', + @response.headers['Content-Disposition'] + end + + def test_show_html + @request.session[:user_id] = 2 + get :show, :project_id => 1, :format => 'html' + assert_response :success + assert_not_nil assigns(:page) + assert_equal 'text/html', @response.content_type + assert_equal 'attachment; filename="CookBook_documentation.html"', + @response.headers['Content-Disposition'] + assert_tag 'h1', :content => 'CookBook documentation' + end + + def test_show_versioned_html + @request.session[:user_id] = 2 + get :show, :project_id => 1, :format => 'html', :version => 2 + assert_response :success + assert_not_nil assigns(:content) + assert_equal 2, assigns(:content).version + assert_equal 'text/html', @response.content_type + assert_equal 'attachment; filename="CookBook_documentation.html"', + @response.headers['Content-Disposition'] + assert_tag 'h1', :content => 'CookBook documentation' + end + + def test_show_txt + @request.session[:user_id] = 2 + get :show, :project_id => 1, :format => 'txt' + assert_response :success + assert_not_nil assigns(:page) + assert_equal 'text/plain', @response.content_type + assert_equal 'attachment; filename="CookBook_documentation.txt"', + @response.headers['Content-Disposition'] + assert_include 'h1. CookBook documentation', @response.body + end + + def test_show_versioned_txt + @request.session[:user_id] = 2 + get :show, :project_id => 1, :format => 'txt', :version => 2 + assert_response :success + assert_not_nil assigns(:content) + assert_equal 2, assigns(:content).version + assert_equal 'text/plain', @response.content_type + assert_equal 'attachment; filename="CookBook_documentation.txt"', + @response.headers['Content-Disposition'] + assert_include 'h1. CookBook documentation', @response.body + end + + def test_edit_unprotected_page + # Non members can edit unprotected wiki pages + @request.session[:user_id] = 4 + get :edit, :project_id => 1, :id => 'Another_page' + assert_response :success + assert_template 'edit' + end + + def test_edit_protected_page_by_nonmember + # Non members can't edit protected wiki pages + @request.session[:user_id] = 4 + get :edit, :project_id => 1, :id => 'CookBook_documentation' + assert_response 403 + end + + def test_edit_protected_page_by_member + @request.session[:user_id] = 2 + get :edit, :project_id => 1, :id => 'CookBook_documentation' + assert_response :success + assert_template 'edit' + end + + def test_history_of_non_existing_page_should_return_404 + get :history, :project_id => 1, :id => 'Unknown_page' + assert_response 404 + end + + def test_add_attachment + @request.session[:user_id] = 2 + assert_difference 'Attachment.count' do + post :add_attachment, :project_id => 1, :id => 'CookBook_documentation', + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}} + end + attachment = Attachment.first(:order => 'id DESC') + assert_equal Wiki.find(1).find_page('CookBook_documentation'), attachment.container + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/69/69880768f41d60c3194963e6b90500b6be6affe7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/69/69880768f41d60c3194963e6b90500b6be6affe7.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,48 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class GanttsController < ApplicationController + menu_item :gantt + before_filter :find_optional_project + + rescue_from Query::StatementInvalid, :with => :query_statement_invalid + + helper :gantt + helper :issues + helper :projects + helper :queries + include QueriesHelper + helper :sort + include SortHelper + include Redmine::Export::PDF + + def show + @gantt = Redmine::Helpers::Gantt.new(params) + @gantt.project = @project + retrieve_query + @query.group_by = nil + @gantt.query = @query if @query.valid? + + basename = (@project ? "#{@project.identifier}-" : '') + 'gantt' + + respond_to do |format| + format.html { render :action => "show", :layout => !request.xhr? } + format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image') + format.pdf { send_data(@gantt.to_pdf, :type => 'application/pdf', :filename => "#{basename}.pdf") } + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/69/69920dcbdbcd687ac4b06555af482a8e6a1caf3a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/69/69920dcbdbcd687ac4b06555af482a8e6a1caf3a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,15 @@ +<%= error_messages_for @document %> + +
    +

    <%= f.select :category_id, DocumentCategory.active.collect {|c| [c.name, c.id]} %>

    +

    <%= f.text_field :title, :required => true, :size => 60 %>

    +

    <%= f.text_area :description, :cols => 60, :rows => 15, :class => 'wiki-edit' %>

    +
    + +<%= wikitoolbar_for 'document_description' %> + +<% if @document.new_record? %> +
    +

    <%= render :partial => 'attachments/form', :locals => {:container => @document} %>

    +
    +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/69/69f952b81bb3ec0bda0f3e3b36510faab9da8bc9.svn-base --- a/.svn/pristine/69/69f952b81bb3ec0bda0f3e3b36510faab9da8bc9.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class MembersController < ApplicationController - model_object Member - before_filter :find_model_object, :except => [:new, :autocomplete_for_member] - before_filter :find_project_from_association, :except => [:new, :autocomplete_for_member] - before_filter :find_project, :only => [:new, :autocomplete_for_member] - before_filter :authorize - - def new - members = [] - if params[:member] && request.post? - attrs = params[:member].dup - if (user_ids = attrs.delete(:user_ids)) - user_ids.each do |user_id| - members << Member.new(attrs.merge(:user_id => user_id)) - end - else - members << Member.new(attrs) - end - @project.members << members - end - respond_to do |format| - if members.present? && members.all? {|m| m.valid? } - - format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project } - - format.js { - render(:update) {|page| - page.replace_html "tab-content-members", :partial => 'projects/settings/members' - page << 'hideOnLoad()' - members.each {|member| page.visual_effect(:highlight, "member-#{member.id}") } - } - } - else - - format.js { - render(:update) {|page| - errors = members.collect {|m| - m.errors.full_messages - }.flatten.uniq - - page.alert(l(:notice_failed_to_save_members, :errors => errors.join(', '))) - } - } - - end - end - end - - def edit - if request.post? and @member.update_attributes(params[:member]) - respond_to do |format| - format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project } - format.js { - render(:update) {|page| - page.replace_html "tab-content-members", :partial => 'projects/settings/members' - page << 'hideOnLoad()' - page.visual_effect(:highlight, "member-#{@member.id}") - } - } - end - end - end - - def destroy - if request.post? && @member.deletable? - @member.destroy - end - respond_to do |format| - format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project } - format.js { render(:update) {|page| - page.replace_html "tab-content-members", :partial => 'projects/settings/members' - page << 'hideOnLoad()' - } - } - end - end - - def autocomplete_for_member - @principals = Principal.active.like(params[:q]).find(:all, :limit => 100) - @project.principals - render :layout => false - end - -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6a/6a2bc828086d461018a7e29583471dd1e9855991.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6a/6a2bc828086d461018a7e29583471dd1e9855991.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,12 @@ + +require File.expand_path(File.dirname(__FILE__) + '../../../../../test/test_helper') + +class SamplePluginRoutingTest < ActionController::IntegrationTest + def test_example + assert_routing( + { :method => 'get', :path => "/projects/1234/hello" }, + { :controller => 'example', :action => 'say_hello', + :id => '1234' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6a/6a446c0fabce93b01244d29a3a538026070a8b28.svn-base --- a/.svn/pristine/6a/6a446c0fabce93b01244d29a3a538026070a8b28.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,125 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class IssueCategoriesController < ApplicationController - menu_item :settings - model_object IssueCategory - before_filter :find_model_object, :except => [:index, :new, :create] - before_filter :find_project_from_association, :except => [:index, :new, :create] - before_filter :find_project, :only => [:index, :new, :create] - before_filter :authorize - accept_api_auth :index, :show, :create, :update, :destroy - - def index - respond_to do |format| - format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project } - format.api { @categories = @project.issue_categories.all } - end - end - - def show - respond_to do |format| - format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project } - format.api - end - end - - def new - @category = @project.issue_categories.build(params[:issue_category]) - end - - verify :method => :post, :only => :create - def create - @category = @project.issue_categories.build(params[:issue_category]) - if @category.save - respond_to do |format| - format.html do - flash[:notice] = l(:notice_successful_create) - redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project - end - format.js do - # IE doesn't support the replace_html rjs method for select box options - render(:update) {|page| page.replace "issue_category_id", - content_tag('select', '' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]') - } - end - format.api { render :action => 'show', :status => :created, :location => issue_category_path(@category) } - end - else - respond_to do |format| - format.html { render :action => 'new'} - format.js do - render(:update) {|page| page.alert(@category.errors.full_messages.join('\n')) } - end - format.api { render_validation_errors(@category) } - end - end - end - - def edit - end - - verify :method => :put, :only => :update - def update - if @category.update_attributes(params[:issue_category]) - respond_to do |format| - format.html { - flash[:notice] = l(:notice_successful_update) - redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project - } - format.api { head :ok } - end - else - respond_to do |format| - format.html { render :action => 'edit' } - format.api { render_validation_errors(@category) } - end - end - end - - verify :method => :delete, :only => :destroy - def destroy - @issue_count = @category.issues.size - if @issue_count == 0 || params[:todo] || api_request? - reassign_to = nil - if params[:reassign_to_id] && (params[:todo] == 'reassign' || params[:todo].blank?) - reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id]) - end - @category.destroy(reassign_to) - respond_to do |format| - format.html { redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' } - format.api { head :ok } - end - return - end - @categories = @project.issue_categories - [@category] - end - -private - # Wrap ApplicationController's find_model_object method to set - # @category instead of just @issue_category - def find_model_object - super - @category = @object - end - - def find_project - @project = Project.find(params[:project_id]) - rescue ActiveRecord::RecordNotFound - render_404 - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6a/6aadf36d36bcfc9aacf50276d45e8a71de094fa9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6a/6aadf36d36bcfc9aacf50276d45e8a71de094fa9.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,74 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class WikiRedirectTest < ActiveSupport::TestCase + fixtures :projects, :wikis, :wiki_pages + + def setup + @wiki = Wiki.find(1) + @original = WikiPage.create(:wiki => @wiki, :title => 'Original title') + end + + def test_create_redirect + @original.title = 'New title' + assert @original.save + @original.reload + + assert_equal 'New_title', @original.title + assert @wiki.redirects.find_by_title('Original_title') + assert @wiki.find_page('Original title') + assert @wiki.find_page('ORIGINAL title') + end + + def test_update_redirect + # create a redirect that point to this page + assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Original_title') + + @original.title = 'New title' + @original.save + # make sure the old page now points to the new page + assert_equal 'New_title', @wiki.find_page('An old page').title + end + + def test_reverse_rename + # create a redirect that point to this page + assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Original_title') + + @original.title = 'An old page' + @original.save + assert !@wiki.redirects.find_by_title_and_redirects_to('An_old_page', 'An_old_page') + assert @wiki.redirects.find_by_title_and_redirects_to('Original_title', 'An_old_page') + end + + def test_rename_to_already_redirected + assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Other_page') + + @original.title = 'An old page' + @original.save + # this redirect have to be removed since 'An old page' page now exists + assert !@wiki.redirects.find_by_title_and_redirects_to('An_old_page', 'Other_page') + end + + def test_redirects_removed_when_deleting_page + assert WikiRedirect.create(:wiki => @wiki, :title => 'An_old_page', :redirects_to => 'Original_title') + + @original.destroy + assert !@wiki.redirects.find(:first) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6b/6b0540330b916387a1d60cb84e4a37702409f61c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6b/6b0540330b916387a1d60cb84e4a37702409f61c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,56 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module SyntaxHighlighting + + class << self + attr_reader :highlighter + delegate :highlight_by_filename, :highlight_by_language, :to => :highlighter + + def highlighter=(name) + if name.is_a?(Module) + @highlighter = name + else + @highlighter = const_get(name) + end + end + end + + module CodeRay + require 'coderay' + require 'coderay/helpers/file_type' + + class << self + # Highlights +text+ as the content of +filename+ + # Should not return line numbers nor outer pre tag + def highlight_by_filename(text, filename) + language = ::CodeRay::FileType[filename] + language ? ::CodeRay.scan(text, language).html(:break_lines => true) : ERB::Util.h(text) + end + + # Highlights +text+ using +language+ syntax + # Should not return outer pre tag + def highlight_by_language(text, language) + ::CodeRay.scan(text, language).html(:wrap => :span) + end + end + end + end + + SyntaxHighlighting.highlighter = 'CodeRay' +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6b/6b05bd90222f8c22a322a8c66db8a262d861c973.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6b/6b05bd90222f8c22a322a8c66db8a262d861c973.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,23 @@ +/* Greek (el) initialisation for the jQuery UI date picker plugin. */ +/* Written by Alex Cicovic (http://www.alexcicovic.com) */ +jQuery(function($){ + $.datepicker.regional['el'] = { + closeText: 'Κλείσιμο', + prevText: 'ΠÏοηγοÏμενος', + nextText: 'Επόμενος', + currentText: 'ΤÏέχων Μήνας', + monthNames: ['ΙανουάÏιος','ΦεβÏουάÏιος','ΜάÏτιος','ΑπÏίλιος','Μάιος','ΙοÏνιος', + 'ΙοÏλιος','ΑÏγουστος','ΣεπτέμβÏιος','ΟκτώβÏιος','ÎοέμβÏιος','ΔεκέμβÏιος'], + monthNamesShort: ['Ιαν','Φεβ','ΜαÏ','ΑπÏ','Μαι','Ιουν', + 'Ιουλ','Αυγ','Σεπ','Οκτ','Îοε','Δεκ'], + dayNames: ['ΚυÏιακή','ΔευτέÏα','ΤÏίτη','ΤετάÏτη','Πέμπτη','ΠαÏασκευή','Σάββατο'], + dayNamesShort: ['ΚυÏ','Δευ','ΤÏι','Τετ','Πεμ','ΠαÏ','Σαβ'], + dayNamesMin: ['Κυ','Δε','ΤÏ','Τε','Πε','Πα','Σα'], + weekHeader: 'Εβδ', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['el']); +}); \ No newline at end of file diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6b/6b1106e1c6126179b03f085b5f53f6f2a31a52ef.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6b/6b1106e1c6126179b03f085b5f53f6f2a31a52ef.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,22 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class NewsObserver < ActiveRecord::Observer + def after_create(news) + Mailer.news_added(news).deliver if Setting.notified_events.include?('news_added') + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6b/6b45dce23ff2dc83015e691c7398ac2bde7c8b86.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6b/6b45dce23ff2dc83015e691c7398ac2bde7c8b86.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,19 @@ +--- +repositories_001: + project_id: 1 + url: file:///<%= Rails.root %>/tmp/test/subversion_repository + id: 10 + root_url: file:///<%= Rails.root %>/tmp/test/subversion_repository + password: "" + login: "" + type: Repository::Subversion + is_default: true +repositories_002: + project_id: 2 + url: svn://localhost/test + id: 11 + root_url: svn://localhost + password: "" + login: "" + type: Repository::Subversion + is_default: true diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6b/6bbc4467d82e17475aa84af06e177a795c79d571.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6b/6bbc4467d82e17475aa84af06e177a795c79d571.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,33 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingWikisTest < ActionController::IntegrationTest + def test_wikis_plural_admin_setup + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/projects/ladida/wiki/destroy" }, + { :controller => 'wikis', :action => 'destroy', :id => 'ladida' } + ) + end + assert_routing( + { :method => 'post', :path => "/projects/ladida/wiki" }, + { :controller => 'wikis', :action => 'edit', :id => 'ladida' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6b/6be367ffa5b0225556429e9b82a2377cddc858d6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6b/6be367ffa5b0225556429e9b82a2377cddc858d6.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,239 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) +require 'my_controller' + +# Re-raise errors caught by the controller. +class MyController; def rescue_action(e) raise e end; end + +class MyControllerTest < ActionController::TestCase + fixtures :users, :user_preferences, :roles, :projects, :members, :member_roles, + :issues, :issue_statuses, :trackers, :enumerations, :custom_fields, :auth_sources + + def setup + @controller = MyController.new + @request = ActionController::TestRequest.new + @request.session[:user_id] = 2 + @response = ActionController::TestResponse.new + end + + def test_index + get :index + assert_response :success + assert_template 'page' + end + + def test_page + get :page + assert_response :success + assert_template 'page' + end + + def test_page_with_timelog_block + preferences = User.find(2).pref + preferences[:my_page_layout] = {'top' => ['timelog']} + preferences.save! + TimeEntry.create!(:user => User.find(2), :spent_on => Date.yesterday, :issue_id => 1, :hours => 2.5, :activity_id => 10) + + get :page + assert_response :success + assert_select 'tr.time-entry' do + assert_select 'td.subject a[href=/issues/1]' + assert_select 'td.hours', :text => '2.50' + end + end + + def test_my_account_should_show_editable_custom_fields + get :account + assert_response :success + assert_template 'account' + assert_equal User.find(2), assigns(:user) + + assert_tag :input, :attributes => { :name => 'user[custom_field_values][4]'} + end + + def test_my_account_should_not_show_non_editable_custom_fields + UserCustomField.find(4).update_attribute :editable, false + + get :account + assert_response :success + assert_template 'account' + assert_equal User.find(2), assigns(:user) + + assert_no_tag :input, :attributes => { :name => 'user[custom_field_values][4]'} + end + + def test_update_account + post :account, + :user => { + :firstname => "Joe", + :login => "root", + :admin => 1, + :group_ids => ['10'], + :custom_field_values => {"4" => "0100562500"} + } + + assert_redirected_to '/my/account' + user = User.find(2) + assert_equal user, assigns(:user) + assert_equal "Joe", user.firstname + assert_equal "jsmith", user.login + assert_equal "0100562500", user.custom_value_for(4).value + # ignored + assert !user.admin? + assert user.groups.empty? + end + + def test_my_account_should_show_destroy_link + get :account + assert_select 'a[href=/my/account/destroy]' + end + + def test_get_destroy_should_display_the_destroy_confirmation + get :destroy + assert_response :success + assert_template 'destroy' + assert_select 'form[action=/my/account/destroy]' do + assert_select 'input[name=confirm]' + end + end + + def test_post_destroy_without_confirmation_should_not_destroy_account + assert_no_difference 'User.count' do + post :destroy + end + assert_response :success + assert_template 'destroy' + end + + def test_post_destroy_without_confirmation_should_destroy_account + assert_difference 'User.count', -1 do + post :destroy, :confirm => '1' + end + assert_redirected_to '/' + assert_match /deleted/i, flash[:notice] + end + + def test_post_destroy_with_unsubscribe_not_allowed_should_not_destroy_account + User.any_instance.stubs(:own_account_deletable?).returns(false) + + assert_no_difference 'User.count' do + post :destroy, :confirm => '1' + end + assert_redirected_to '/my/account' + end + + def test_change_password + get :password + assert_response :success + assert_template 'password' + + # non matching password confirmation + post :password, :password => 'jsmith', + :new_password => 'secret123', + :new_password_confirmation => 'secret1234' + assert_response :success + assert_template 'password' + assert_error_tag :content => /Password doesn't match confirmation/ + + # wrong password + post :password, :password => 'wrongpassword', + :new_password => 'secret123', + :new_password_confirmation => 'secret123' + assert_response :success + assert_template 'password' + assert_equal 'Wrong password', flash[:error] + + # good password + post :password, :password => 'jsmith', + :new_password => 'secret123', + :new_password_confirmation => 'secret123' + assert_redirected_to '/my/account' + assert User.try_to_login('jsmith', 'secret123') + end + + def test_change_password_should_redirect_if_user_cannot_change_its_password + User.find(2).update_attribute(:auth_source_id, 1) + + get :password + assert_not_nil flash[:error] + assert_redirected_to '/my/account' + end + + def test_page_layout + get :page_layout + assert_response :success + assert_template 'page_layout' + end + + def test_add_block + post :add_block, :block => 'issuesreportedbyme' + assert_redirected_to '/my/page_layout' + assert User.find(2).pref[:my_page_layout]['top'].include?('issuesreportedbyme') + end + + def test_remove_block + post :remove_block, :block => 'issuesassignedtome' + assert_redirected_to '/my/page_layout' + assert !User.find(2).pref[:my_page_layout].values.flatten.include?('issuesassignedtome') + end + + def test_order_blocks + xhr :post, :order_blocks, :group => 'left', 'blocks' => ['documents', 'calendar', 'latestnews'] + assert_response :success + assert_equal ['documents', 'calendar', 'latestnews'], User.find(2).pref[:my_page_layout]['left'] + end + + def test_reset_rss_key_with_existing_key + @previous_token_value = User.find(2).rss_key # Will generate one if it's missing + post :reset_rss_key + + assert_not_equal @previous_token_value, User.find(2).rss_key + assert User.find(2).rss_token + assert_match /reset/, flash[:notice] + assert_redirected_to '/my/account' + end + + def test_reset_rss_key_without_existing_key + assert_nil User.find(2).rss_token + post :reset_rss_key + + assert User.find(2).rss_token + assert_match /reset/, flash[:notice] + assert_redirected_to '/my/account' + end + + def test_reset_api_key_with_existing_key + @previous_token_value = User.find(2).api_key # Will generate one if it's missing + post :reset_api_key + + assert_not_equal @previous_token_value, User.find(2).api_key + assert User.find(2).api_token + assert_match /reset/, flash[:notice] + assert_redirected_to '/my/account' + end + + def test_reset_api_key_without_existing_key + assert_nil User.find(2).api_token + post :reset_api_key + + assert User.find(2).api_token + assert_match /reset/, flash[:notice] + assert_redirected_to '/my/account' + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6b/6be65077afa803bdec605ecec12428554273b481.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6b/6be65077afa803bdec605ecec12428554273b481.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,31 @@ +
    +<%= link_to l(:label_tracker_new), new_tracker_path, :class => 'icon icon-add' %> +<%= link_to l(:field_summary), {:action => 'fields'}, :class => 'icon icon-summary' %> +
    + +

    <%=l(:label_tracker_plural)%>

    + + + + + + + + + +<% for tracker in @trackers %> + "> + + + + + +<% end %> + +
    <%=l(:label_tracker)%><%=l(:button_sort)%>
    <%= link_to h(tracker.name), edit_tracker_path(tracker) %><% unless tracker.workflow_rules.count > 0 %><%= l(:text_tracker_no_workflow) %> (<%= link_to l(:button_edit), {:controller => 'workflows', :action => 'edit', :tracker_id => tracker} %>)<% end %><%= reorder_links('tracker', {:action => 'update', :id => tracker}, :put) %> + <%= delete_link tracker_path(tracker) %> +
    + +

    <%= pagination_links_full @tracker_pages %>

    + +<% html_title(l(:label_tracker_plural)) -%> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6b/6bff5377f5dc44e4315dc69c0c28864f2c23b5b0.svn-base Binary file .svn/pristine/6b/6bff5377f5dc44e4315dc69c0c28864f2c23b5b0.svn-base has changed diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6c/6c1e26b54fdd47501d3a107c6cb94b31dc942cde.svn-base --- a/.svn/pristine/6c/6c1e26b54fdd47501d3a107c6cb94b31dc942cde.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module RolesHelper -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6c/6c4dd8ce0226a8bc5d18fafda9466bf4106576da.svn-base --- a/.svn/pristine/6c/6c4dd8ce0226a8bc5d18fafda9466bf4106576da.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1062 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -require 'forwardable' -require 'cgi' - -module ApplicationHelper - include Redmine::WikiFormatting::Macros::Definitions - include Redmine::I18n - include GravatarHelper::PublicMethods - - extend Forwardable - def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter - - # Return true if user is authorized for controller/action, otherwise false - def authorize_for(controller, action) - User.current.allowed_to?({:controller => controller, :action => action}, @project) - end - - # Display a link if user is authorized - # - # @param [String] name Anchor text (passed to link_to) - # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized - # @param [optional, Hash] html_options Options passed to link_to - # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to - def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) - link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) - end - - # Display a link to remote if user is authorized - def link_to_remote_if_authorized(name, options = {}, html_options = nil) - url = options[:url] || {} - link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action]) - end - - # Displays a link to user's account page if active - def link_to_user(user, options={}) - if user.is_a?(User) - name = h(user.name(options[:format])) - if user.active? - link_to name, :controller => 'users', :action => 'show', :id => user - else - name - end - else - h(user.to_s) - end - end - - # Displays a link to +issue+ with its subject. - # Examples: - # - # link_to_issue(issue) # => Defect #6: This is the subject - # link_to_issue(issue, :truncate => 6) # => Defect #6: This i... - # link_to_issue(issue, :subject => false) # => Defect #6 - # link_to_issue(issue, :project => true) # => Foo - Defect #6 - # - def link_to_issue(issue, options={}) - title = nil - subject = nil - if options[:subject] == false - title = truncate(issue.subject, :length => 60) - else - subject = issue.subject - if options[:truncate] - subject = truncate(subject, :length => options[:truncate]) - end - end - s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, - :class => issue.css_classes, - :title => title - s << ": #{h subject}" if subject - s = "#{h issue.project} - " + s if options[:project] - s - end - - # Generates a link to an attachment. - # Options: - # * :text - Link text (default to attachment filename) - # * :download - Force download (default: false) - def link_to_attachment(attachment, options={}) - text = options.delete(:text) || attachment.filename - action = options.delete(:download) ? 'download' : 'show' - link_to(h(text), - {:controller => 'attachments', :action => action, - :id => attachment, :filename => attachment.filename }, - options) - end - - # Generates a link to a SCM revision - # Options: - # * :text - Link text (default to the formatted revision) - def link_to_revision(revision, project, options={}) - text = options.delete(:text) || format_revision(revision) - rev = revision.respond_to?(:identifier) ? revision.identifier : revision - - link_to(h(text), {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev}, - :title => l(:label_revision_id, format_revision(revision))) - end - - # Generates a link to a message - def link_to_message(message, options={}, html_options = nil) - link_to( - h(truncate(message.subject, :length => 60)), - { :controller => 'messages', :action => 'show', - :board_id => message.board_id, - :id => message.root, - :r => (message.parent_id && message.id), - :anchor => (message.parent_id ? "message-#{message.id}" : nil) - }.merge(options), - html_options - ) - end - - # Generates a link to a project if active - # Examples: - # - # link_to_project(project) # => link to the specified project overview - # link_to_project(project, :action=>'settings') # => link to project settings - # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options - # link_to_project(project, {}, :class => "project") # => html options with default url (project overview) - # - def link_to_project(project, options={}, html_options = nil) - if project.active? - url = {:controller => 'projects', :action => 'show', :id => project}.merge(options) - link_to(h(project), url, html_options) - else - h(project) - end - end - - def toggle_link(name, id, options={}) - onclick = "Element.toggle('#{id}'); " - onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ") - onclick << "return false;" - link_to(name, "#", :onclick => onclick) - end - - def image_to_function(name, function, html_options = {}) - html_options.symbolize_keys! - tag(:input, html_options.merge({ - :type => "image", :src => image_path(name), - :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" - })) - end - - def prompt_to_remote(name, text, param, url, html_options = {}) - html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;" - link_to name, {}, html_options - end - - def format_activity_title(text) - h(truncate_single_line(text, :length => 100)) - end - - def format_activity_day(date) - date == Date.today ? l(:label_today).titleize : format_date(date) - end - - def format_activity_description(text) - h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "
    ") - end - - def format_version_name(version) - if version.project == @project - h(version) - else - h("#{version.project} - #{version}") - end - end - - def due_date_distance_in_words(date) - if date - l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date)) - end - end - - def render_page_hierarchy(pages, node=nil, options={}) - content = '' - if pages[node] - content << "
      \n" - pages[node].each do |page| - content << "
    • " - content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}, - :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil)) - content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id] - content << "
    • \n" - end - content << "
    \n" - end - content.html_safe - end - - # Renders flash messages - def render_flash_messages - s = '' - flash.each do |k,v| - s << content_tag('div', v, :class => "flash #{k}") - end - s.html_safe - end - - # Renders tabs and their content - def render_tabs(tabs) - if tabs.any? - render :partial => 'common/tabs', :locals => {:tabs => tabs} - else - content_tag 'p', l(:label_no_data), :class => "nodata" - end - end - - # Renders the project quick-jump box - def render_project_jump_box - return unless User.current.logged? - projects = User.current.memberships.collect(&:project).compact.uniq - if projects.any? - s = '' - s.html_safe - end - end - - def project_tree_options_for_select(projects, options = {}) - s = '' - project_tree(projects) do |project, level| - name_prefix = (level > 0 ? (' ' * 2 * level + '» ') : '') - tag_options = {:value => project.id} - if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project)) - tag_options[:selected] = 'selected' - else - tag_options[:selected] = nil - end - tag_options.merge!(yield(project)) if block_given? - s << content_tag('option', name_prefix + h(project), tag_options) - end - s.html_safe - end - - # Yields the given block for each project with its level in the tree - # - # Wrapper for Project#project_tree - def project_tree(projects, &block) - Project.project_tree(projects, &block) - end - - def project_nested_ul(projects, &block) - s = '' - if projects.any? - ancestors = [] - projects.sort_by(&:lft).each do |project| - if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) - s << "
      \n" - else - ancestors.pop - s << "" - while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) - ancestors.pop - s << "
    \n" - end - end - s << "
  • " - s << yield(project).to_s - ancestors << project - end - s << ("
  • \n" * ancestors.size) - end - s.html_safe - end - - def principals_check_box_tags(name, principals) - s = '' - principals.sort.each do |principal| - s << "\n" - end - s.html_safe - end - - # Returns a string for users/groups option tags - def principals_options_for_select(collection, selected=nil) - s = '' - groups = '' - collection.sort.each do |element| - selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) - (element.is_a?(Group) ? groups : s) << %() - end - unless groups.empty? - s << %(#{groups}) - end - s - end - - # Truncates and returns the string as a single line - def truncate_single_line(string, *args) - truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ') - end - - # Truncates at line break after 250 characters or options[:length] - def truncate_lines(string, options={}) - length = options[:length] || 250 - if string.to_s =~ /\A(.{#{length}}.*?)$/m - "#{$1}..." - else - string - end - end - - def html_hours(text) - text.gsub(%r{(\d+)\.(\d+)}, '\1.\2').html_safe - end - - def authoring(created, author, options={}) - l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe - end - - def time_tag(time) - text = distance_of_time_in_words(Time.now, time) - if @project - link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time)) - else - content_tag('acronym', text, :title => format_time(time)) - end - end - - def syntax_highlight(name, content) - Redmine::SyntaxHighlighting.highlight_by_filename(content, name) - end - - def to_path_param(path) - path.to_s.split(%r{[/\\]}).select {|p| !p.blank?} - end - - def pagination_links_full(paginator, count=nil, options={}) - page_param = options.delete(:page_param) || :page - per_page_links = options.delete(:per_page_links) - url_param = params.dup - - html = '' - if paginator.current.previous - # \xc2\xab(utf-8) = « - html << link_to_content_update( - "\xc2\xab " + l(:label_previous), - url_param.merge(page_param => paginator.current.previous)) + ' ' - end - - html << (pagination_links_each(paginator, options) do |n| - link_to_content_update(n.to_s, url_param.merge(page_param => n)) - end || '') - - if paginator.current.next - # \xc2\xbb(utf-8) = » - html << ' ' + link_to_content_update( - (l(:label_next) + " \xc2\xbb"), - url_param.merge(page_param => paginator.current.next)) - end - - unless count.nil? - html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})" - if per_page_links != false && links = per_page_links(paginator.items_per_page) - html << " | #{links}" - end - end - - html.html_safe - end - - def per_page_links(selected=nil) - links = Setting.per_page_options_array.collect do |n| - n == selected ? n : link_to_content_update(n, params.merge(:per_page => n)) - end - links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil - end - - def reorder_links(name, url, method = :post) - link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), - url.merge({"#{name}[move_to]" => 'highest'}), - :method => method, :title => l(:label_sort_highest)) + - link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), - url.merge({"#{name}[move_to]" => 'higher'}), - :method => method, :title => l(:label_sort_higher)) + - link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), - url.merge({"#{name}[move_to]" => 'lower'}), - :method => method, :title => l(:label_sort_lower)) + - link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), - url.merge({"#{name}[move_to]" => 'lowest'}), - :method => method, :title => l(:label_sort_lowest)) - end - - def breadcrumb(*args) - elements = args.flatten - elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil - end - - def other_formats_links(&block) - concat('

    '.html_safe + l(:label_export_to)) - yield Redmine::Views::OtherFormatsBuilder.new(self) - concat('

    '.html_safe) - end - - def page_header_title - if @project.nil? || @project.new_record? - h(Setting.app_title) - else - b = [] - ancestors = (@project.root? ? [] : @project.ancestors.visible.all) - if ancestors.any? - root = ancestors.shift - b << link_to_project(root, {:jump => current_menu_item}, :class => 'root') - if ancestors.size > 2 - b << "\xe2\x80\xa6" - ancestors = ancestors[-2, 2] - end - b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') } - end - b << h(@project) - b.join(" \xc2\xbb ").html_safe - end - end - - def html_title(*args) - if args.empty? - title = @html_title || [] - title << @project.name if @project - title << Setting.app_title unless Setting.app_title == title.last - title.select {|t| !t.blank? }.join(' - ') - else - @html_title ||= [] - @html_title += args - end - end - - # Returns the theme, controller name, and action as css classes for the - # HTML body. - def body_css_classes - css = [] - if theme = Redmine::Themes.theme(Setting.ui_theme) - css << 'theme-' + theme.name - end - - css << 'controller-' + params[:controller] - css << 'action-' + params[:action] - css.join(' ') - end - - def accesskey(s) - Redmine::AccessKeys.key_for s - end - - # Formats text according to system settings. - # 2 ways to call this method: - # * with a String: textilizable(text, options) - # * with an object and one of its attribute: textilizable(issue, :description, options) - def textilizable(*args) - options = args.last.is_a?(Hash) ? args.pop : {} - case args.size - when 1 - obj = options[:object] - text = args.shift - when 2 - obj = args.shift - attr = args.shift - text = obj.send(attr).to_s - else - raise ArgumentError, 'invalid arguments to textilizable' - end - return '' if text.blank? - project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) - only_path = options.delete(:only_path) == false ? false : true - - text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) - - @parsed_headings = [] - @current_section = 0 if options[:edit_section_links] - text = parse_non_pre_blocks(text) do |text| - [:parse_sections, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros, :parse_headings].each do |method_name| - send method_name, text, project, obj, attr, only_path, options - end - end - - if @parsed_headings.any? - replace_toc(text, @parsed_headings) - end - - text - end - - def parse_non_pre_blocks(text) - s = StringScanner.new(text) - tags = [] - parsed = '' - while !s.eos? - s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im) - text, full_tag, closing, tag = s[1], s[2], s[3], s[4] - if tags.empty? - yield text - end - parsed << text - if tag - if closing - if tags.last == tag.downcase - tags.pop - end - else - tags << tag.downcase - end - parsed << full_tag - end - end - # Close any non closing tags - while tag = tags.pop - parsed << "" - end - parsed.html_safe - end - - def parse_inline_attachments(text, project, obj, attr, only_path, options) - # when using an image link, try to use an attachment, if possible - if options[:attachments] || (obj && obj.respond_to?(:attachments)) - attachments = options[:attachments] || obj.attachments - text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| - filename, ext, alt, alttext = $1.downcase, $2, $3, $4 - # search for the picture in attachments - if found = Attachment.latest_attach(attachments, filename) - image_url = url_for :only_path => only_path, :controller => 'attachments', - :action => 'download', :id => found - desc = found.description.to_s.gsub('"', '') - if !desc.blank? && alttext.blank? - alt = " title=\"#{desc}\" alt=\"#{desc}\"" - end - "src=\"#{image_url}\"#{alt}".html_safe - else - m.html_safe - end - end - end - end - - # Wiki links - # - # Examples: - # [[mypage]] - # [[mypage|mytext]] - # wiki links can refer other project wikis, using project name or identifier: - # [[project:]] -> wiki starting page - # [[project:|mytext]] - # [[project:mypage]] - # [[project:mypage|mytext]] - def parse_wiki_links(text, project, obj, attr, only_path, options) - text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m| - link_project = project - esc, all, page, title = $1, $2, $3, $5 - if esc.nil? - if page =~ /^([^\:]+)\:(.*)$/ - link_project = Project.find_by_identifier($1) || Project.find_by_name($1) - page = $2 - title ||= $1 if page.blank? - end - - if link_project && link_project.wiki - # extract anchor - anchor = nil - if page =~ /^(.+?)\#(.+)$/ - page, anchor = $1, $2 - end - anchor = sanitize_anchor_name(anchor) if anchor.present? - # check if page exists - wiki_page = link_project.wiki.find_page(page) - url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page - "##{anchor}" - else - case options[:wiki_links] - when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '') - when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export - else - wiki_page_id = page.present? ? Wiki.titleize(page) : nil - url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor) - end - end - link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new'))) - else - # project or wiki doesn't exist - all.html_safe - end - else - all.html_safe - end - end - end - - # Redmine links - # - # Examples: - # Issues: - # #52 -> Link to issue #52 - # Changesets: - # r52 -> Link to revision 52 - # commit:a85130f -> Link to scmid starting with a85130f - # Documents: - # document#17 -> Link to document with id 17 - # document:Greetings -> Link to the document with title "Greetings" - # document:"Some document" -> Link to the document with title "Some document" - # Versions: - # version#3 -> Link to version with id 3 - # version:1.0.0 -> Link to version named "1.0.0" - # version:"1.0 beta 2" -> Link to version named "1.0 beta 2" - # Attachments: - # attachment:file.zip -> Link to the attachment of the current object named file.zip - # Source files: - # source:some/file -> Link to the file located at /some/file in the project's repository - # source:some/file@52 -> Link to the file's revision 52 - # source:some/file#L120 -> Link to line 120 of the file - # source:some/file@52#L120 -> Link to line 120 of the file's revision 52 - # export:some/file -> Force the download of the file - # Forum messages: - # message#1218 -> Link to message with id 1218 - # - # Links can refer other objects from other projects, using project identifier: - # identifier:r52 - # identifier:document:"Some document" - # identifier:version:1.0.0 - # identifier:source:some/file - def parse_redmine_links(text, project, obj, attr, only_path, options) - text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|forum|news|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m| - leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10 - link = nil - if project_identifier - project = Project.visible.find_by_identifier(project_identifier) - end - if esc.nil? - if prefix.nil? && sep == 'r' - # project.changesets.visible raises an SQL error because of a double join on repositories - if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier)) - link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision}, - :class => 'changeset', - :title => truncate_single_line(changeset.comments, :length => 100)) - end - elsif sep == '#' - oid = identifier.to_i - case prefix - when nil - if issue = Issue.visible.find_by_id(oid, :include => :status) - link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid}, - :class => issue.css_classes, - :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})") - end - when 'document' - if document = Document.visible.find_by_id(oid) - link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, - :class => 'document' - end - when 'version' - if version = Version.visible.find_by_id(oid) - link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, - :class => 'version' - end - when 'message' - if message = Message.visible.find_by_id(oid, :include => :parent) - link = link_to_message(message, {:only_path => only_path}, :class => 'message') - end - when 'forum' - if board = Board.visible.find_by_id(oid) - link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, - :class => 'board' - end - when 'news' - if news = News.visible.find_by_id(oid) - link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, - :class => 'news' - end - when 'project' - if p = Project.visible.find_by_id(oid) - link = link_to_project(p, {:only_path => only_path}, :class => 'project') - end - end - elsif sep == ':' - # removes the double quotes if any - name = identifier.gsub(%r{^"(.*)"$}, "\\1") - case prefix - when 'document' - if project && document = project.documents.visible.find_by_title(name) - link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, - :class => 'document' - end - when 'version' - if project && version = project.versions.visible.find_by_name(name) - link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, - :class => 'version' - end - when 'forum' - if project && board = project.boards.visible.find_by_name(name) - link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, - :class => 'board' - end - when 'news' - if project && news = project.news.visible.find_by_title(name) - link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, - :class => 'news' - end - when 'commit' - if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"])) - link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier}, - :class => 'changeset', - :title => truncate_single_line(h(changeset.comments), :length => 100) - end - when 'source', 'export' - if project && project.repository && User.current.allowed_to?(:browse_repository, project) - name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$} - path, rev, anchor = $1, $3, $5 - link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, - :path => to_path_param(path), - :rev => rev, - :anchor => anchor, - :format => (prefix == 'export' ? 'raw' : nil)}, - :class => (prefix == 'export' ? 'source download' : 'source') - end - when 'attachment' - attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) - if attachments && attachment = attachments.detect {|a| a.filename == name } - link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment}, - :class => 'attachment' - end - when 'project' - if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}]) - link = link_to_project(p, {:only_path => only_path}, :class => 'project') - end - end - end - end - (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe - end - end - - HEADING_RE = /(]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE) - - def parse_sections(text, project, obj, attr, only_path, options) - return unless options[:edit_section_links] - text.gsub!(HEADING_RE) do - @current_section += 1 - if @current_section > 1 - content_tag('div', - link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)), - :class => 'contextual', - :title => l(:button_edit_section)) + $1 - else - $1 - end - end - end - - # Headings and TOC - # Adds ids and links to headings unless options[:headings] is set to false - def parse_headings(text, project, obj, attr, only_path, options) - return if options[:headings] == false - - text.gsub!(HEADING_RE) do - level, attrs, content = $2.to_i, $3, $4 - item = strip_tags(content).strip - anchor = sanitize_anchor_name(item) - # used for single-file wiki export - anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) - @parsed_headings << [level, anchor, item] - "\n#{content}" - end - end - - MACROS_RE = / - (!)? # escaping - ( - \{\{ # opening tag - ([\w]+) # macro name - (\(([^\}]*)\))? # optional arguments - \}\} # closing tag - ) - /x unless const_defined?(:MACROS_RE) - - # Macros substitution - def parse_macros(text, project, obj, attr, only_path, options) - text.gsub!(MACROS_RE) do - esc, all, macro = $1, $2, $3.downcase - args = ($5 || '').split(',').each(&:strip) - if esc.nil? - begin - exec_macro(macro, obj, args) - rescue => e - "
    Error executing the #{macro} macro (#{e})
    " - end || all - else - all - end - end - end - - TOC_RE = /

    \{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE) - - # Renders the TOC with given headings - def replace_toc(text, headings) - text.gsub!(TOC_RE) do - if headings.empty? - '' - else - div_class = 'toc' - div_class << ' right' if $1 == '>' - div_class << ' left' if $1 == '<' - out = "

    • " - root = headings.map(&:first).min - current = root - started = false - headings.each do |level, anchor, item| - if level > current - out << '
      • ' * (level - current) - elsif level < current - out << "
      \n" * (current - level) + "
    • " - elsif started - out << '
    • ' - end - out << "#{item}" - current = level - started = true - end - out << '
    ' * (current - root) - out << '' - end - end - end - - # Same as Rails' simple_format helper without using paragraphs - def simple_format_without_paragraph(text) - text.to_s. - gsub(/\r\n?/, "\n"). # \r\n and \r -> \n - gsub(/\n\n+/, "

    "). # 2+ newline -> 2 br - gsub(/([^\n]\n)(?=[^\n])/, '\1
    '). # 1 newline -> br - html_safe - end - - def lang_options_for_select(blank=true) - (blank ? [["(auto)", ""]] : []) + - valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last } - end - - def label_tag_for(name, option_tags = nil, options = {}) - label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") - content_tag("label", label_text) - end - - def labelled_tabular_form_for(*args, &proc) - args << {} unless args.last.is_a?(Hash) - options = args.last - options[:html] ||= {} - options[:html][:class] = 'tabular' unless options[:html].has_key?(:class) - options.merge!({:builder => TabularFormBuilder}) - form_for(*args, &proc) - end - - def labelled_form_for(*args, &proc) - args << {} unless args.last.is_a?(Hash) - options = args.last - options.merge!({:builder => TabularFormBuilder}) - form_for(*args, &proc) - end - - def back_url_hidden_field_tag - back_url = params[:back_url] || request.env['HTTP_REFERER'] - back_url = CGI.unescape(back_url.to_s) - hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank? - end - - def check_all_links(form_name) - link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + - " | ".html_safe + - link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") - end - - def progress_bar(pcts, options={}) - pcts = [pcts, pcts] unless pcts.is_a?(Array) - pcts = pcts.collect(&:round) - pcts[1] = pcts[1] - pcts[0] - pcts << (100 - pcts[1] - pcts[0]) - width = options[:width] || '100px;' - legend = options[:legend] || '' - content_tag('table', - content_tag('tr', - (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) + - (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) + - (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe) - ), :class => 'progress', :style => "width: #{width};").html_safe + - content_tag('p', legend, :class => 'pourcent').html_safe - end - - def checked_image(checked=true) - if checked - image_tag 'toggle_check.png' - end - end - - def context_menu(url) - unless @context_menu_included - content_for :header_tags do - javascript_include_tag('context_menu') + - stylesheet_link_tag('context_menu') - end - if l(:direction) == 'rtl' - content_for :header_tags do - stylesheet_link_tag('context_menu_rtl') - end - end - @context_menu_included = true - end - javascript_tag "new ContextMenu('#{ url_for(url) }')" - end - - def context_menu_link(name, url, options={}) - options[:class] ||= '' - if options.delete(:selected) - options[:class] << ' icon-checked disabled' - options[:disabled] = true - end - if options.delete(:disabled) - options.delete(:method) - options.delete(:confirm) - options.delete(:onclick) - options[:class] << ' disabled' - url = '#' - end - link_to h(name), url, options - end - - def calendar_for(field_id) - include_calendar_headers_tags - image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) + - javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });") - end - - def include_calendar_headers_tags - unless @calendar_headers_tags_included - @calendar_headers_tags_included = true - content_for :header_tags do - start_of_week = case Setting.start_of_week.to_i - when 1 - 'Calendar._FD = 1;' # Monday - when 7 - 'Calendar._FD = 0;' # Sunday - when 6 - 'Calendar._FD = 6;' # Saturday - else - '' # use language - end - - javascript_include_tag('calendar/calendar') + - javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") + - javascript_tag(start_of_week) + - javascript_include_tag('calendar/calendar-setup') + - stylesheet_link_tag('calendar') - end - end - end - - def content_for(name, content = nil, &block) - @has_content ||= {} - @has_content[name] = true - super(name, content, &block) - end - - def has_content?(name) - (@has_content && @has_content[name]) || false - end - - def email_delivery_enabled? - !!ActionMailer::Base.perform_deliveries - end - - # Returns the avatar image tag for the given +user+ if avatars are enabled - # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe ') - def avatar(user, options = { }) - if Setting.gravatar_enabled? - options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default}) - email = nil - if user.respond_to?(:mail) - email = user.mail - elsif user.to_s =~ %r{<(.+?)>} - email = $1 - end - return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil - else - '' - end - end - - def sanitize_anchor_name(anchor) - anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') - end - - # Returns the javascript tags that are included in the html layout head - def javascript_heads - tags = javascript_include_tag(:defaults) - unless User.current.pref.warn_on_leaving_unsaved == '0' - tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });") - end - tags - end - - def favicon - "".html_safe - end - - def robot_exclusion_tag - ''.html_safe - end - - # Returns true if arg is expected in the API response - def include_in_api_response?(arg) - unless @included_in_api_response - param = params[:include] - @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',') - @included_in_api_response.collect!(&:strip) - end - @included_in_api_response.include?(arg.to_s) - end - - # Returns options or nil if nometa param or X-Redmine-Nometa header - # was set in the request - def api_meta(options) - if params[:nometa].present? || request.headers['X-Redmine-Nometa'] - # compatibility mode for activeresource clients that raise - # an error when unserializing an array with attributes - nil - else - options - end - end - - private - - def wiki_helper - helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) - extend helper - return self - end - - def link_to_content_update(text, url_params = {}, html_options = {}) - link_to(text, url_params, html_options) - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6c/6c63de300d0e80bc0b6e3de12c96ec81bd4c86b4.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6c/6c63de300d0e80bc0b6e3de12c96ec81bd4c86b4.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,103 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Ciphering + def self.included(base) + base.extend ClassMethods + end + + class << self + def encrypt_text(text) + if cipher_key.blank? || text.blank? + text + else + c = OpenSSL::Cipher::Cipher.new("aes-256-cbc") + iv = c.random_iv + c.encrypt + c.key = cipher_key + c.iv = iv + e = c.update(text.to_s) + e << c.final + "aes-256-cbc:" + [e, iv].map {|v| Base64.encode64(v).strip}.join('--') + end + end + + def decrypt_text(text) + if text && match = text.match(/\Aaes-256-cbc:(.+)\Z/) + if cipher_key.blank? + logger.error "Attempt to decrypt a ciphered text with no cipher key configured in config/configuration.yml" if logger + return text + end + text = match[1] + c = OpenSSL::Cipher::Cipher.new("aes-256-cbc") + e, iv = text.split("--").map {|s| Base64.decode64(s)} + c.decrypt + c.key = cipher_key + c.iv = iv + d = c.update(e) + d << c.final + else + text + end + end + + def cipher_key + key = Redmine::Configuration['database_cipher_key'].to_s + key.blank? ? nil : Digest::SHA256.hexdigest(key) + end + + def logger + Rails.logger + end + end + + module ClassMethods + def encrypt_all(attribute) + transaction do + all.each do |object| + clear = object.send(attribute) + object.send "#{attribute}=", clear + raise(ActiveRecord::Rollback) unless object.save(:validation => false) + end + end ? true : false + end + + def decrypt_all(attribute) + transaction do + all.each do |object| + clear = object.send(attribute) + object.send :write_attribute, attribute, clear + raise(ActiveRecord::Rollback) unless object.save(:validation => false) + end + end + end ? true : false + end + + private + + # Returns the value of the given ciphered attribute + def read_ciphered_attribute(attribute) + Redmine::Ciphering.decrypt_text(read_attribute(attribute)) + end + + # Sets the value of the given ciphered attribute + def write_ciphered_attribute(attribute, value) + write_attribute(attribute, Redmine::Ciphering.encrypt_text(value)) + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6c/6c693aad7bd181db73cd90834dd1050e5cf2a77a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6c/6c693aad7bd181db73cd90834dd1050e5cf2a77a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,165 @@ +--- +custom_fields_001: + name: Database + min_length: 0 + regexp: "" + is_for_all: true + is_filter: true + type: IssueCustomField + max_length: 0 + possible_values: + - MySQL + - PostgreSQL + - Oracle + id: 1 + is_required: false + field_format: list + default_value: "" + editable: true + position: 2 +custom_fields_002: + name: Searchable field + min_length: 1 + regexp: "" + is_for_all: true + is_filter: true + type: IssueCustomField + max_length: 100 + possible_values: "" + id: 2 + is_required: false + field_format: string + searchable: true + default_value: "Default string" + editable: true + position: 1 +custom_fields_003: + name: Development status + min_length: 0 + regexp: "" + is_for_all: false + is_filter: true + type: ProjectCustomField + max_length: 0 + possible_values: + - Stable + - Beta + - Alpha + - Planning + id: 3 + is_required: false + field_format: list + default_value: "" + editable: true + position: 1 +custom_fields_004: + name: Phone number + min_length: 0 + regexp: "" + is_for_all: false + type: UserCustomField + max_length: 0 + possible_values: "" + id: 4 + is_required: false + field_format: string + default_value: "" + editable: true + position: 1 +custom_fields_005: + name: Money + min_length: 0 + regexp: "" + is_for_all: false + type: UserCustomField + max_length: 0 + possible_values: "" + id: 5 + is_required: false + field_format: float + default_value: "" + editable: true + position: 2 +custom_fields_006: + name: Float field + min_length: 0 + regexp: "" + is_for_all: true + type: IssueCustomField + max_length: 0 + possible_values: "" + id: 6 + is_required: false + field_format: float + default_value: "" + editable: true + position: 3 +custom_fields_007: + name: Billable + min_length: 0 + regexp: "" + is_for_all: false + is_filter: true + type: TimeEntryActivityCustomField + max_length: 0 + possible_values: "" + id: 7 + is_required: false + field_format: bool + default_value: "" + editable: true + position: 1 +custom_fields_008: + name: Custom date + min_length: 0 + regexp: "" + is_for_all: true + is_filter: false + type: IssueCustomField + max_length: 0 + possible_values: "" + id: 8 + is_required: false + field_format: date + default_value: "" + editable: true + position: 4 +custom_fields_009: + name: Project 1 cf + min_length: 0 + regexp: "" + is_for_all: false + is_filter: true + type: IssueCustomField + max_length: 0 + possible_values: "" + id: 9 + is_required: false + field_format: date + default_value: "" + editable: true + position: 5 +custom_fields_010: + name: Overtime + min_length: 0 + regexp: "" + is_for_all: false + is_filter: false + type: TimeEntryCustomField + max_length: 0 + possible_values: "" + id: 10 + is_required: false + field_format: bool + default_value: 0 + editable: true + position: 1 +custom_fields_011: + id: 11 + name: Binary + type: CustomField + possible_values: + - !binary | + SGXDqWzDp2prc2Tigqw2NTTDuQ== + - Other value + field_format: list diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6c/6c96a40bb02084f0ca89601fc6cb8ef86565a45a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6c/6c96a40bb02084f0ca89601fc6cb8ef86565a45a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,2 @@ +

    <%= l(:mail_body_register) %>
    +<%= link_to h(@url), @url %>

    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6c/6cc533b0e56df4187bfb76b52dd325e1a95364a0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6c/6cc533b0e56df4187bfb76b52dd325e1a95364a0.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1 @@ +function jsToolBar(e){if(!document.createElement){return}if(!e){return}if(typeof document["selection"]=="undefined"&&typeof e["setSelectionRange"]=="undefined"){return}this.textarea=e;this.editor=document.createElement("div");this.editor.className="jstEditor";this.textarea.parentNode.insertBefore(this.editor,this.textarea);this.editor.appendChild(this.textarea);this.toolbar=document.createElement("div");this.toolbar.className="jstElements";this.editor.parentNode.insertBefore(this.toolbar,this.editor);if(this.editor.addEventListener&&navigator.appVersion.match(/\bMSIE\b/)){this.handle=document.createElement("div");this.handle.className="jstHandle";var t=this.resizeDragStart;var n=this;this.handle.addEventListener("mousedown",function(e){t.call(n,e)},false);window.addEventListener("unload",function(){var e=n.handle.parentNode.removeChild(n.handle);delete n.handle},false);this.editor.parentNode.insertBefore(this.handle,this.editor.nextSibling)}this.context=null;this.toolNodes={}}function jsButton(e,t,n,r){if(typeof jsToolBar.strings=="undefined"){this.title=e||null}else{this.title=jsToolBar.strings[e]||e||null}this.fn=t||function(){};this.scope=n||null;this.className=r||null}function jsSpace(e){this.id=e||null;this.width=null}function jsCombo(e,t,n,r,i){this.title=e||null;this.options=t||null;this.scope=n||null;this.fn=r||function(){};this.className=i||null}jsButton.prototype.draw=function(){if(!this.scope)return null;var e=document.createElement("button");e.setAttribute("type","button");e.tabIndex=200;if(this.className)e.className=this.className;e.title=this.title;var t=document.createElement("span");t.appendChild(document.createTextNode(this.title));e.appendChild(t);if(this.icon!=undefined){e.style.backgroundImage="url("+this.icon+")"}if(typeof this.fn=="function"){var n=this;e.onclick=function(){try{n.fn.apply(n.scope,arguments)}catch(e){}return false}}return e};jsSpace.prototype.draw=function(){var e=document.createElement("span");if(this.id)e.id=this.id;e.appendChild(document.createTextNode(String.fromCharCode(160)));e.className="jstSpacer";if(this.width)e.style.marginRight=this.width+"px";return e};jsCombo.prototype.draw=function(){if(!this.scope||!this.options)return null;var e=document.createElement("select");if(this.className)e.className=className;e.title=this.title;for(var t in this.options){var n=document.createElement("option");n.value=t;n.appendChild(document.createTextNode(this.options[t]));e.appendChild(n)}var r=this;e.onchange=function(){try{r.fn.call(r.scope,this.value)}catch(e){alert(e)}return false};return e};jsToolBar.prototype={base_url:"",mode:"wiki",elements:{},help_link:"",getMode:function(){return this.mode},setMode:function(e){this.mode=e||"wiki"},switchMode:function(e){e=e||"wiki";this.draw(e)},setHelpLink:function(e){this.help_link=e},button:function(e){var t=this.elements[e];if(typeof t.fn[this.mode]!="function")return null;var n=new jsButton(t.title,t.fn[this.mode],this,"jstb_"+e);if(t.icon!=undefined)n.icon=t.icon;return n},space:function(e){var t=new jsSpace(e);if(this.elements[e].width!==undefined)t.width=this.elements[e].width;return t},combo:function(e){var t=this.elements[e];var n=t[this.mode].list.length;if(typeof t[this.mode].fn!="function"||n==0){return null}else{var r={};for(var i=0;iAide";this.toolbar.appendChild(t);var n,r,i;for(var s in this.elements){n=this.elements[s];var o=n.type==undefined||n.type==""||n.disabled!=undefined&&n.disabled||n.context!=undefined&&n.context!=null&&n.context!=this.context;if(!o&&typeof this[n.type]=="function"){r=this[n.type](s);if(r)i=r.draw();if(i){this.toolNodes[s]=i;this.toolbar.appendChild(i)}}}},singleTag:function(e,t){e=e||null;t=t||e;if(!e||!t){return}this.encloseSelection(e,t)},encloseLineSelection:function(e,t,n){this.textarea.focus();e=e||"";t=t||"";var r,i,s,o,u,a;if(typeof document["selection"]!="undefined"){s=document.selection.createRange().text}else if(typeof this.textarea["setSelectionRange"]!="undefined"){r=this.textarea.selectionStart;i=this.textarea.selectionEnd;o=this.textarea.scrollTop;r=this.textarea.value.substring(0,r).replace(/[^\r\n]*$/g,"").length;i=this.textarea.value.length-this.textarea.value.substring(i,this.textarea.value.length).replace(/^[^\r\n]*/,"").length;s=this.textarea.value.substring(r,i)}if(s.match(/ $/)){s=s.substring(0,s.length-1);t=t+" "}if(typeof n=="function"){a=s?n.call(this,s):n("")}else{a=s?s:""}u=e+a+t;if(typeof document["selection"]!="undefined"){document.selection.createRange().text=u;var f=this.textarea.createTextRange();f.collapse(false);f.move("character",-t.length);f.select()}else if(typeof this.textarea["setSelectionRange"]!="undefined"){this.textarea.value=this.textarea.value.substring(0,r)+u+this.textarea.value.substring(i);if(s){this.textarea.setSelectionRange(r+u.length,r+u.length)}else{this.textarea.setSelectionRange(r+e.length,r+e.length)}this.textarea.scrollTop=o}},encloseSelection:function(e,t,n){this.textarea.focus();e=e||"";t=t||"";var r,i,s,o,u,a;if(typeof document["selection"]!="undefined"){s=document.selection.createRange().text}else if(typeof this.textarea["setSelectionRange"]!="undefined"){r=this.textarea.selectionStart;i=this.textarea.selectionEnd;o=this.textarea.scrollTop;s=this.textarea.value.substring(r,i)}if(s.match(/ $/)){s=s.substring(0,s.length-1);t=t+" "}if(typeof n=="function"){a=s?n.call(this,s):n("")}else{a=s?s:""}u=e+a+t;if(typeof document["selection"]!="undefined"){document.selection.createRange().text=u;var f=this.textarea.createTextRange();f.collapse(false);f.move("character",-t.length);f.select()}else if(typeof this.textarea["setSelectionRange"]!="undefined"){this.textarea.value=this.textarea.value.substring(0,r)+u+this.textarea.value.substring(i);if(s){this.textarea.setSelectionRange(r+u.length,r+u.length)}else{this.textarea.setSelectionRange(r+e.length,r+e.length)}this.textarea.scrollTop=o}},stripBaseURL:function(e){if(this.base_url!=""){var t=e.indexOf(this.base_url);if(t==0){e=e.substr(this.base_url.length)}}return e}};jsToolBar.prototype.resizeSetStartH=function(){this.dragStartH=this.textarea.offsetHeight+0};jsToolBar.prototype.resizeDragStart=function(e){var t=this;this.dragStartY=e.clientY;this.resizeSetStartH();document.addEventListener("mousemove",this.dragMoveHdlr=function(e){t.resizeDragMove(e)},false);document.addEventListener("mouseup",this.dragStopHdlr=function(e){t.resizeDragStop(e)},false)};jsToolBar.prototype.resizeDragMove=function(e){this.textarea.style.height=this.dragStartH+e.clientY-this.dragStartY+"px"};jsToolBar.prototype.resizeDragStop=function(e){document.removeEventListener("mousemove",this.dragMoveHdlr,false);document.removeEventListener("mouseup",this.dragStopHdlr,false)};jsToolBar.prototype.elements.strong={type:"button",title:"Strong",fn:{wiki:function(){this.singleTag("*")}}};jsToolBar.prototype.elements.em={type:"button",title:"Italic",fn:{wiki:function(){this.singleTag("_")}}};jsToolBar.prototype.elements.ins={type:"button",title:"Underline",fn:{wiki:function(){this.singleTag("+")}}};jsToolBar.prototype.elements.del={type:"button",title:"Deleted",fn:{wiki:function(){this.singleTag("-")}}};jsToolBar.prototype.elements.code={type:"button",title:"Code",fn:{wiki:function(){this.singleTag("@")}}};jsToolBar.prototype.elements.space1={type:"space"};jsToolBar.prototype.elements.h1={type:"button",title:"Heading 1",fn:{wiki:function(){this.encloseLineSelection("h1. ","",function(e){e=e.replace(/^h\d+\.\s+/,"");return e})}}};jsToolBar.prototype.elements.h2={type:"button",title:"Heading 2",fn:{wiki:function(){this.encloseLineSelection("h2. ","",function(e){e=e.replace(/^h\d+\.\s+/,"");return e})}}};jsToolBar.prototype.elements.h3={type:"button",title:"Heading 3",fn:{wiki:function(){this.encloseLineSelection("h3. ","",function(e){e=e.replace(/^h\d+\.\s+/,"");return e})}}};jsToolBar.prototype.elements.space2={type:"space"};jsToolBar.prototype.elements.ul={type:"button",title:"Unordered list",fn:{wiki:function(){this.encloseLineSelection("","",function(e){e=e.replace(/\r/g,"");return e.replace(/(\n|^)[#-]?\s*/g,"$1* ")})}}};jsToolBar.prototype.elements.ol={type:"button",title:"Ordered list",fn:{wiki:function(){this.encloseLineSelection("","",function(e){e=e.replace(/\r/g,"");return e.replace(/(\n|^)[*-]?\s*/g,"$1# ")})}}};jsToolBar.prototype.elements.space3={type:"space"};jsToolBar.prototype.elements.bq={type:"button",title:"Quote",fn:{wiki:function(){this.encloseLineSelection("","",function(e){e=e.replace(/\r/g,"");return e.replace(/(\n|^) *([^\n]*)/g,"$1> $2")})}}};jsToolBar.prototype.elements.unbq={type:"button",title:"Unquote",fn:{wiki:function(){this.encloseLineSelection("","",function(e){e=e.replace(/\r/g,"");return e.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2")})}}};jsToolBar.prototype.elements.pre={type:"button",title:"Preformatted text",fn:{wiki:function(){this.encloseLineSelection("
    \n","\n
    ")}}};jsToolBar.prototype.elements.space4={type:"space"};jsToolBar.prototype.elements.link={type:"button",title:"Wiki link",fn:{wiki:function(){this.encloseSelection("[[","]]")}}};jsToolBar.prototype.elements.img={type:"button",title:"Image",fn:{wiki:function(){this.encloseSelection("!","!")}}} \ No newline at end of file diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6c/6cd75d05f4deac0559250c87c2ffccdd03b546c2.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6c/6cd75d05f4deac0559250c87c2ffccdd03b546c2.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,129 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class EnumerationsControllerTest < ActionController::TestCase + fixtures :enumerations, :issues, :users + + def setup + @request.session[:user_id] = 1 # admin + end + + def test_index + get :index + assert_response :success + assert_template 'index' + end + + def test_index_should_require_admin + @request.session[:user_id] = nil + get :index + assert_response 302 + end + + def test_new + get :new, :type => 'IssuePriority' + assert_response :success + assert_template 'new' + assert_kind_of IssuePriority, assigns(:enumeration) + assert_tag 'input', :attributes => {:name => 'enumeration[type]', :value => 'IssuePriority'} + assert_tag 'input', :attributes => {:name => 'enumeration[name]'} + end + + def test_new_with_invalid_type_should_respond_with_404 + get :new, :type => 'UnknownType' + assert_response 404 + end + + def test_create + assert_difference 'IssuePriority.count' do + post :create, :enumeration => {:type => 'IssuePriority', :name => 'Lowest'} + end + assert_redirected_to '/enumerations' + e = IssuePriority.find_by_name('Lowest') + assert_not_nil e + end + + def test_create_with_failure + assert_no_difference 'IssuePriority.count' do + post :create, :enumeration => {:type => 'IssuePriority', :name => ''} + end + assert_response :success + assert_template 'new' + end + + def test_edit + get :edit, :id => 6 + assert_response :success + assert_template 'edit' + assert_tag 'input', :attributes => {:name => 'enumeration[name]', :value => 'High'} + end + + def test_edit_invalid_should_respond_with_404 + get :edit, :id => 999 + assert_response 404 + end + + def test_update + assert_no_difference 'IssuePriority.count' do + put :update, :id => 6, :enumeration => {:type => 'IssuePriority', :name => 'New name'} + end + assert_redirected_to '/enumerations' + e = IssuePriority.find(6) + assert_equal 'New name', e.name + end + + def test_update_with_failure + assert_no_difference 'IssuePriority.count' do + put :update, :id => 6, :enumeration => {:type => 'IssuePriority', :name => ''} + end + assert_response :success + assert_template 'edit' + end + + def test_destroy_enumeration_not_in_use + assert_difference 'IssuePriority.count', -1 do + delete :destroy, :id => 7 + end + assert_redirected_to :controller => 'enumerations', :action => 'index' + assert_nil Enumeration.find_by_id(7) + end + + def test_destroy_enumeration_in_use + assert_no_difference 'IssuePriority.count' do + delete :destroy, :id => 4 + end + assert_response :success + assert_template 'destroy' + assert_not_nil Enumeration.find_by_id(4) + assert_select 'select[name=reassign_to_id]' do + assert_select 'option[value=6]', :text => 'High' + end + end + + def test_destroy_enumeration_in_use_with_reassignment + issue = Issue.find(:first, :conditions => {:priority_id => 4}) + assert_difference 'IssuePriority.count', -1 do + delete :destroy, :id => 4, :reassign_to_id => 6 + end + assert_redirected_to :controller => 'enumerations', :action => 'index' + assert_nil Enumeration.find_by_id(4) + # check that the issue was reassign + assert_equal 6, issue.reload.priority_id + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6c/6cea03c76318e826b03e4d334daa05b149e4780d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6c/6cea03c76318e826b03e4d334daa05b149e4780d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,88 @@ +# ActsAsWatchable +module Redmine + module Acts + module Watchable + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + def acts_as_watchable(options = {}) + return if self.included_modules.include?(Redmine::Acts::Watchable::InstanceMethods) + class_eval do + has_many :watchers, :as => :watchable, :dependent => :delete_all + has_many :watcher_users, :through => :watchers, :source => :user, :validate => false + + scope :watched_by, lambda { |user_id| + { :include => :watchers, + :conditions => ["#{Watcher.table_name}.user_id = ?", user_id] } + } + attr_protected :watcher_ids, :watcher_user_ids + end + send :include, Redmine::Acts::Watchable::InstanceMethods + alias_method_chain :watcher_user_ids=, :uniq_ids + end + end + + module InstanceMethods + def self.included(base) + base.extend ClassMethods + end + + # Returns an array of users that are proposed as watchers + def addable_watcher_users + users = self.project.users.sort - self.watcher_users + if respond_to?(:visible?) + users.reject! {|user| !visible?(user)} + end + users + end + + # Adds user as a watcher + def add_watcher(user) + self.watchers << Watcher.new(:user => user) + end + + # Removes user from the watchers list + def remove_watcher(user) + return nil unless user && user.is_a?(User) + Watcher.delete_all "watchable_type = '#{self.class}' AND watchable_id = #{self.id} AND user_id = #{user.id}" + end + + # Adds/removes watcher + def set_watcher(user, watching=true) + watching ? add_watcher(user) : remove_watcher(user) + end + + # Overrides watcher_user_ids= to make user_ids uniq + def watcher_user_ids_with_uniq_ids=(user_ids) + if user_ids.is_a?(Array) + user_ids = user_ids.uniq + end + send :watcher_user_ids_without_uniq_ids=, user_ids + end + + # Returns true if object is watched by +user+ + def watched_by?(user) + !!(user && self.watcher_user_ids.detect {|uid| uid == user.id }) + end + + def notified_watchers + notified = watcher_users.active + notified.reject! {|user| user.mail.blank? || user.mail_notification == 'none'} + if respond_to?(:visible?) + notified.reject! {|user| !visible?(user)} + end + notified + end + + # Returns an array of watchers' email addresses + def watcher_recipients + notified_watchers.collect(&:mail) + end + + module ClassMethods; end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6d/6d0551244176bd8c1f71da8d031910d4b7e0c899.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6d/6d0551244176bd8c1f71da8d031910d4b7e0c899.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,37 @@ +require 'rexml/document' + +module Redmine + module VERSION #:nodoc: + MAJOR = 2 + MINOR = 2 + TINY = 0 + + # Branch values: + # * official release: nil + # * stable branch: stable + # * trunk: devel + BRANCH = 'stable' + + # Retrieves the revision from the working copy + def self.revision + if File.directory?(File.join(Rails.root, '.svn')) + begin + path = Redmine::Scm::Adapters::AbstractAdapter.shell_quote(Rails.root.to_s) + if `svn info --xml #{path}` =~ /revision="(\d+)"/ + return $1.to_i + end + rescue + # Could not find the current revision + end + end + nil + end + + REVISION = self.revision + ARRAY = [MAJOR, MINOR, TINY, BRANCH, REVISION].compact + STRING = ARRAY.join('.') + + def self.to_a; ARRAY end + def self.to_s; STRING end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6d/6d89cd2cacde4d59156afd5de16aaba31367e0ed.svn-base Binary file .svn/pristine/6d/6d89cd2cacde4d59156afd5de16aaba31367e0ed.svn-base has changed diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6d/6d8b57d3f9af4fafe2fd6cde3312b234c4375e61.svn-base --- a/.svn/pristine/6d/6d8b57d3f9af4fafe2fd6cde3312b234c4375e61.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +0,0 @@ -class Message < ActiveRecord::Base - generator_for :subject, :start => 'A Message' - generator_for :content, :start => 'Some content here' - generator_for :board, :method => :generate_board - - def self.generate_board - Board.generate! - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6d/6daddbb134251308e32b4b262caaa9e987cee1e0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6d/6daddbb134251308e32b4b262caaa9e987cee1e0.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,47 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingAdminTest < ActionController::IntegrationTest + def test_administration_panel + assert_routing( + { :method => 'get', :path => "/admin" }, + { :controller => 'admin', :action => 'index' } + ) + assert_routing( + { :method => 'get', :path => "/admin/projects" }, + { :controller => 'admin', :action => 'projects' } + ) + assert_routing( + { :method => 'get', :path => "/admin/plugins" }, + { :controller => 'admin', :action => 'plugins' } + ) + assert_routing( + { :method => 'get', :path => "/admin/info" }, + { :controller => 'admin', :action => 'info' } + ) + assert_routing( + { :method => 'get', :path => "/admin/test_email" }, + { :controller => 'admin', :action => 'test_email' } + ) + assert_routing( + { :method => 'post', :path => "/admin/default_configuration" }, + { :controller => 'admin', :action => 'default_configuration' } + ) + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6e/6e19e1e338055e9065d37596fdaf97de3c4d84c3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6e/6e19e1e338055e9065d37596fdaf97de3c4d84c3.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,149 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module CustomFieldsHelper + + def custom_fields_tabs + CustomField::CUSTOM_FIELDS_TABS + end + + # Return custom field html tag corresponding to its format + def custom_field_tag(name, custom_value) + custom_field = custom_value.custom_field + field_name = "#{name}[custom_field_values][#{custom_field.id}]" + field_name << "[]" if custom_field.multiple? + field_id = "#{name}_custom_field_values_#{custom_field.id}" + + tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"} + + field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format) + case field_format.try(:edit_as) + when "date" + text_field_tag(field_name, custom_value.value, tag_options.merge(:size => 10)) + + calendar_for(field_id) + when "text" + text_area_tag(field_name, custom_value.value, tag_options.merge(:rows => 3)) + when "bool" + hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, tag_options) + when "list" + blank_option = ''.html_safe + unless custom_field.multiple? + if custom_field.is_required? + unless custom_field.default_value.present? + blank_option = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '') + end + else + blank_option = content_tag('option') + end + end + s = select_tag(field_name, blank_option + options_for_select(custom_field.possible_values_options(custom_value.customized), custom_value.value), + tag_options.merge(:multiple => custom_field.multiple?)) + if custom_field.multiple? + s << hidden_field_tag(field_name, '') + end + s + else + text_field_tag(field_name, custom_value.value, tag_options) + end + end + + # Return custom field label tag + def custom_field_label_tag(name, custom_value, options={}) + required = options[:required] || custom_value.custom_field.is_required? + + content_tag "label", h(custom_value.custom_field.name) + + (required ? " *".html_safe : ""), + :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}" + end + + # Return custom field tag with its label tag + def custom_field_tag_with_label(name, custom_value, options={}) + custom_field_label_tag(name, custom_value, options) + custom_field_tag(name, custom_value) + end + + def custom_field_tag_for_bulk_edit(name, custom_field, projects=nil) + field_name = "#{name}[custom_field_values][#{custom_field.id}]" + field_name << "[]" if custom_field.multiple? + field_id = "#{name}_custom_field_values_#{custom_field.id}" + + tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"} + + field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format) + case field_format.try(:edit_as) + when "date" + text_field_tag(field_name, '', tag_options.merge(:size => 10)) + + calendar_for(field_id) + when "text" + text_area_tag(field_name, '', tag_options.merge(:rows => 3)) + when "bool" + select_tag(field_name, options_for_select([[l(:label_no_change_option), ''], + [l(:general_text_yes), '1'], + [l(:general_text_no), '0']]), tag_options) + when "list" + options = [] + options << [l(:label_no_change_option), ''] unless custom_field.multiple? + options << [l(:label_none), '__none__'] unless custom_field.is_required? + options += custom_field.possible_values_options(projects) + select_tag(field_name, options_for_select(options), tag_options.merge(:multiple => custom_field.multiple?)) + else + text_field_tag(field_name, '', tag_options) + end + end + + # Return a string used to display a custom value + def show_value(custom_value) + return "" unless custom_value + format_value(custom_value.value, custom_value.custom_field.field_format) + end + + # Return a string used to display a custom value + def format_value(value, field_format) + if value.is_a?(Array) + value.collect {|v| format_value(v, field_format)}.compact.sort.join(', ') + else + Redmine::CustomFieldFormat.format_value(value, field_format) + end + end + + # Return an array of custom field formats which can be used in select_tag + def custom_field_formats_for_select(custom_field) + Redmine::CustomFieldFormat.as_select(custom_field.class.customized_class.name) + end + + # Renders the custom_values in api views + def render_api_custom_values(custom_values, api) + api.array :custom_fields do + custom_values.each do |custom_value| + attrs = {:id => custom_value.custom_field_id, :name => custom_value.custom_field.name} + attrs.merge!(:multiple => true) if custom_value.custom_field.multiple? + api.custom_field attrs do + if custom_value.value.is_a?(Array) + api.array :value do + custom_value.value.each do |value| + api.value value unless value.blank? + end + end + else + api.value custom_value.value + end + end + end + end unless custom_values.empty? + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6e/6e9e1243ef58d1f7ab5c26c4b1c4078ac0562ef5.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6e/6e9e1243ef58d1f7ab5c26c4b1c4078ac0562ef5.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,21 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module MailHandlerHelper +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/6e/6ec02141f430c80f57516715f8456cabdaf278c2.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/6e/6ec02141f430c80f57516715f8456cabdaf278c2.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,442 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../../test_helper', __FILE__) +require 'digest/md5' + +class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase + + def setup + @formatter = Redmine::WikiFormatting::Textile::Formatter + end + + MODIFIERS = { + "*" => 'strong', # bold + "_" => 'em', # italic + "+" => 'ins', # underline + "-" => 'del', # deleted + "^" => 'sup', # superscript + "~" => 'sub' # subscript + } + + def test_modifiers + assert_html_output( + '*bold*' => 'bold', + 'before *bold*' => 'before bold', + '*bold* after' => 'bold after', + '*two words*' => 'two words', + '*two*words*' => 'two*words', + '*two * words*' => 'two * words', + '*two* *words*' => 'two words', + '*(two)* *(words)*' => '(two) (words)', + # with class + '*(foo)two words*' => 'two words' + ) + end + + def test_modifiers_combination + MODIFIERS.each do |m1, tag1| + MODIFIERS.each do |m2, tag2| + next if m1 == m2 + text = "#{m2}#{m1}Phrase modifiers#{m1}#{m2}" + html = "<#{tag2}><#{tag1}>Phrase modifiers" + assert_html_output text => html + end + end + end + + def test_styles + # single style + assert_html_output({ + 'p{color:red}. text' => '

    text

    ', + 'p{color:red;}. text' => '

    text

    ', + 'p{color: red}. text' => '

    text

    ', + 'p{color:#f00}. text' => '

    text

    ', + 'p{color:#ff0000}. text' => '

    text

    ', + 'p{border:10px}. text' => '

    text

    ', + 'p{border:10}. text' => '

    text

    ', + 'p{border:10%}. text' => '

    text

    ', + 'p{border:10em}. text' => '

    text

    ', + 'p{border:1.5em}. text' => '

    text

    ', + 'p{border-left:1px}. text' => '

    text

    ', + 'p{border-right:1px}. text' => '

    text

    ', + 'p{border-top:1px}. text' => '

    text

    ', + 'p{border-bottom:1px}. text' => '

    text

    ', + }, false) + + # multiple styles + assert_html_output({ + 'p{color:red; border-top:1px}. text' => '

    text

    ', + 'p{color:red ; border-top:1px}. text' => '

    text

    ', + 'p{color:red;border-top:1px}. text' => '

    text

    ', + }, false) + + # styles with multiple values + assert_html_output({ + 'p{border:1px solid red;}. text' => '

    text

    ', + 'p{border-top-left-radius: 10px 5px;}. text' => '

    text

    ', + }, false) + end + + def test_invalid_styles_should_be_filtered + assert_html_output({ + 'p{invalid}. text' => '

    text

    ', + 'p{invalid:red}. text' => '

    text

    ', + 'p{color:(red)}. text' => '

    text

    ', + 'p{color:red;invalid:blue}. text' => '

    text

    ', + 'p{invalid:blue;color:red}. text' => '

    text

    ', + 'p{color:"}. text' => '

    p{color:"}. text

    ', + }, false) + end + + def test_inline_code + assert_html_output( + 'this is @some code@' => 'this is some code', + '@@' => '<Location /redmine>' + ) + end + + def test_nested_lists + raw = <<-RAW +# Item 1 +# Item 2 +** Item 2a +** Item 2b +# Item 3 +** Item 3a +RAW + + expected = <<-EXPECTED +
      +
    1. Item 1
    2. +
    3. Item 2 +
        +
      • Item 2a
      • +
      • Item 2b
      • +
      +
    4. +
    5. Item 3 +
        +
      • Item 3a
      • +
      +
    6. +
    +EXPECTED + + assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '') + end + + def test_escaping + assert_html_output( + 'this is a " => "

    <script>some script;</script>

    ", - # do not escape pre/code tags - "
    \nline 1\nline2
    " => "
    \nline 1\nline2
    ", - "
    \nline 1\nline2
    " => "
    \nline 1\nline2
    ", - "
    content
    " => "
    <div>content</div>
    ", - "HTML comment: " => "

    HTML comment: <!-- no comments -->

    ", - " +

    <%= f.text_field :name, :required => true, :size => 60 %>

    + +<% unless @project.allowed_parents.compact.empty? %> +

    <%= label(:project, :parent_id, l(:field_parent)) %><%= parent_project_select_tag(@project) %>

    +<% end %> + +

    <%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %>

    +

    <%= f.text_field :identifier, :required => true, :size => 60, :disabled => @project.identifier_frozen? %> +<% unless @project.identifier_frozen? %> + <%= l(:text_length_between, :min => 1, :max => Project::IDENTIFIER_MAX_LENGTH) %> <%= l(:text_project_identifier_info).html_safe %> +<% end %>

    +

    <%= f.text_field :homepage, :size => 60 %>

    +

    <%= f.check_box :is_public %>

    +<%= wikitoolbar_for 'project_description' %> + +<% @project.custom_field_values.each do |value| %> +

    <%= custom_field_tag_with_label :project, value %>

    +<% end %> +<%= call_hook(:view_projects_form, :project => @project, :form => f) %> + + +<% if @project.new_record? %> +
    <%= l(:label_module_plural) %> +<% Redmine::AccessControl.available_project_modules.each do |m| %> + +<% end %> +<%= hidden_field_tag 'project[enabled_module_names][]', '' %> +<%= javascript_tag 'observeProjectModules()' %> +
    +<% end %> + +<% if @project.new_record? || @project.module_enabled?('issue_tracking') %> +<% unless @trackers.empty? %> +
    <%=l(:label_tracker_plural)%> +<% @trackers.each do |tracker| %> + +<% end %> +<%= hidden_field_tag 'project[tracker_ids][]', '' %> +
    +<% end %> + +<% unless @issue_custom_fields.empty? %> +
    <%=l(:label_custom_field_plural)%> +<% @issue_custom_fields.each do |custom_field| %> + +<% end %> +<%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %> +
    +<% end %> +<% end %> + diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d3/d3d2f31a14fe74bbd0e9f83346a5f38a3ee8b9ad.svn-base --- a/.svn/pristine/d3/d3d2f31a14fe74bbd0e9f83346a5f38a3ee8b9ad.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module WelcomeHelper -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d3/d3d65d25f759791112afec8e9891bb6fa5d40f56.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d3/d3d65d25f759791112afec8e9891bb6fa5d40f56.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,7 @@ +

    Good Bye

    + +

    <%= l(:text_say_goodbye) %>

    + +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'example', :plugin => 'sample_plugin', :media => "screen" %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d3/d3d9bf30f88fdc7250bd3d2626cf179c354940a3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d3/d3d9bf30f88fdc7250bd3d2626cf179c354940a3.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,22 @@ +--- +changes_001: + id: 1 + changeset_id: 100 + action: A + path: /test/some/path/in/the/repo + from_path: + from_revision: +changes_002: + id: 2 + changeset_id: 100 + action: A + path: /test/some/path/elsewhere/in/the/repo + from_path: + from_revision: +changes_003: + id: 3 + changeset_id: 101 + action: M + path: /test/some/path/in/the/repo + from_path: + from_revision: diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d3/d3f415dece34a1117d042ed2dfb14252d8d7eeec.svn-base --- a/.svn/pristine/d3/d3f415dece34a1117d042ed2dfb14252d8d7eeec.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,25 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module AdminHelper - def project_status_options_for_select(selected) - options_for_select([[l(:label_all), ''], - [l(:status_active), 1]], selected) - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d4/d41f15d9b37a3eb6c6bcd52ec522bdb47635d440.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d4/d41f15d9b37a3eb6c6bcd52ec522bdb47635d440.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,45 @@ +ActiveRecord::Schema.define(:version => 0) do + + create_table :categories, :force => true do |t| + t.column :name, :string + t.column :parent_id, :integer + t.column :lft, :integer + t.column :rgt, :integer + t.column :organization_id, :integer + end + + create_table :departments, :force => true do |t| + t.column :name, :string + end + + create_table :notes, :force => true do |t| + t.column :body, :text + t.column :parent_id, :integer + t.column :lft, :integer + t.column :rgt, :integer + t.column :notable_id, :integer + t.column :notable_type, :string + end + + create_table :renamed_columns, :force => true do |t| + t.column :name, :string + t.column :mother_id, :integer + t.column :red, :integer + t.column :black, :integer + end + + create_table :things, :force => true do |t| + t.column :body, :text + t.column :parent_id, :integer + t.column :lft, :integer + t.column :rgt, :integer + t.column :children_count, :integer + end + + create_table :brokens, :force => true do |t| + t.column :name, :string + t.column :parent_id, :integer + t.column :lft, :integer + t.column :rgt, :integer + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d4/d433c2a67bf6894f8afced62c37440b82b2851a7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d4/d433c2a67bf6894f8afced62c37440b82b2851a7.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,54 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module UsersHelper + def users_status_options_for_select(selected) + user_count_by_status = User.count(:group => 'status').to_hash + options_for_select([[l(:label_all), ''], + ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", '1'], + ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", '2'], + ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", '3']], selected.to_s) + end + + def user_mail_notification_options(user) + user.valid_notification_options.collect {|o| [l(o.last), o.first]} + end + + def change_status_link(user) + url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil} + + if user.locked? + link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock' + elsif user.registered? + link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock' + elsif user != User.current + link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :put, :class => 'icon icon-lock' + end + end + + def user_settings_tabs + tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general}, + {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural} + ] + if Group.all.any? + tabs.insert 1, {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural} + end + tabs + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d4/d479fb55686ebc16bc355a2929fe952b6df8d668.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d4/d479fb55686ebc16bc355a2929fe952b6df8d668.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1138 @@ +html {overflow-y:scroll;} +body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; } + +h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;} +#content h1, h2, h3, h4 {color: #555;} +h2, .wiki h1 {font-size: 20px;} +h3, .wiki h2 {font-size: 16px;} +h4, .wiki h3 {font-size: 13px;} +h4 {border-bottom: 1px dotted #bbb;} + +/***** Layout *****/ +#wrapper {background: white;} + +#top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;} +#top-menu ul {margin: 0; padding: 0;} +#top-menu li { + float:left; + list-style-type:none; + margin: 0px 0px 0px 0px; + padding: 0px 0px 0px 0px; + white-space:nowrap; +} +#top-menu a {color: #fff; margin-right: 8px; font-weight: bold;} +#top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; } + +#account {float:right;} + +#header {height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;} +#header a {color:#f8f8f8;} +#header h1 a.ancestor { font-size: 80%; } +#quick-search {float:right;} + +#main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;} +#main-menu ul {margin: 0; padding: 0;} +#main-menu li { + float:left; + list-style-type:none; + margin: 0px 2px 0px 0px; + padding: 0px 0px 0px 0px; + white-space:nowrap; +} +#main-menu li a { + display: block; + color: #fff; + text-decoration: none; + font-weight: bold; + margin: 0; + padding: 4px 10px 4px 10px; +} +#main-menu li a:hover {background:#759FCF; color:#fff;} +#main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;} + +#admin-menu ul {margin: 0; padding: 0;} +#admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;} + +#admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;} +#admin-menu a.projects { background-image: url(../images/projects.png); } +#admin-menu a.users { background-image: url(../images/user.png); } +#admin-menu a.groups { background-image: url(../images/group.png); } +#admin-menu a.roles { background-image: url(../images/database_key.png); } +#admin-menu a.trackers { background-image: url(../images/ticket.png); } +#admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); } +#admin-menu a.workflows { background-image: url(../images/ticket_go.png); } +#admin-menu a.custom_fields { background-image: url(../images/textfield.png); } +#admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); } +#admin-menu a.settings { background-image: url(../images/changeset.png); } +#admin-menu a.plugins { background-image: url(../images/plugin.png); } +#admin-menu a.info { background-image: url(../images/help.png); } +#admin-menu a.server_authentication { background-image: url(../images/server_key.png); } + +#main {background-color:#EEEEEE;} + +#sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;} +* html #sidebar{ width: 22%; } +#sidebar h3{ font-size: 14px; margin-top:14px; color: #666; } +#sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; } +* html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; } +#sidebar .contextual { margin-right: 1em; } + +#content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; } +* html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;} +html>body #content { min-height: 600px; } +* html body #content { height: 600px; } /* IE */ + +#main.nosidebar #sidebar{ display: none; } +#main.nosidebar #content{ width: auto; border-right: 0; } + +#footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;} + +#login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; } +#login-form table td {padding: 6px;} +#login-form label {font-weight: bold;} +#login-form input#username, #login-form input#password { width: 300px; } + +div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;} +div.modal h3.title {display:none;} +div.modal p.buttons {text-align:right; margin-bottom:0;} + +input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; } + +.clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; } + +/***** Links *****/ +a, a:link, a:visited{ color: #169; text-decoration: none; } +a:hover, a:active{ color: #c61a1a; text-decoration: underline;} +a img{ border: 0; } + +a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; } +a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; } +a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;} + +#sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;} +#sidebar a.selected:hover {text-decoration:none;} +#admin-menu a {line-height:1.7em;} +#admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;} + +a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;} +a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;} + +a#toggle-completed-versions {color:#999;} +/***** Tables *****/ +table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; } +table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; } +table.list td { vertical-align: top; padding-right:10px; } +table.list td.id { width: 2%; text-align: center;} +table.list td.checkbox { width: 15px; padding: 2px 0 0 0; } +table.list td.checkbox input {padding:0px;} +table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; } +table.list td.buttons a { padding-right: 0.6em; } +table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; } + +tr.project td.name a { white-space:nowrap; } +tr.project.closed, tr.project.archived { color: #aaa; } +tr.project.closed a, tr.project.archived a { color: #aaa; } + +tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;} +tr.project.idnt-1 td.name {padding-left: 0.5em;} +tr.project.idnt-2 td.name {padding-left: 2em;} +tr.project.idnt-3 td.name {padding-left: 3.5em;} +tr.project.idnt-4 td.name {padding-left: 5em;} +tr.project.idnt-5 td.name {padding-left: 6.5em;} +tr.project.idnt-6 td.name {padding-left: 8em;} +tr.project.idnt-7 td.name {padding-left: 9.5em;} +tr.project.idnt-8 td.name {padding-left: 11em;} +tr.project.idnt-9 td.name {padding-left: 12.5em;} + +tr.issue { text-align: center; white-space: nowrap; } +tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations { white-space: normal; } +tr.issue td.subject, tr.issue td.relations { text-align: left; } +tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;} +tr.issue td.relations span {white-space: nowrap;} +table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;} +table.issues td.description pre {white-space:normal;} + +tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;} +tr.issue.idnt-1 td.subject {padding-left: 0.5em;} +tr.issue.idnt-2 td.subject {padding-left: 2em;} +tr.issue.idnt-3 td.subject {padding-left: 3.5em;} +tr.issue.idnt-4 td.subject {padding-left: 5em;} +tr.issue.idnt-5 td.subject {padding-left: 6.5em;} +tr.issue.idnt-6 td.subject {padding-left: 8em;} +tr.issue.idnt-7 td.subject {padding-left: 9.5em;} +tr.issue.idnt-8 td.subject {padding-left: 11em;} +tr.issue.idnt-9 td.subject {padding-left: 12.5em;} + +tr.entry { border: 1px solid #f8f8f8; } +tr.entry td { white-space: nowrap; } +tr.entry td.filename { width: 30%; } +tr.entry td.filename_no_report { width: 70%; } +tr.entry td.size { text-align: right; font-size: 90%; } +tr.entry td.revision, tr.entry td.author { text-align: center; } +tr.entry td.age { text-align: right; } +tr.entry.file td.filename a { margin-left: 16px; } +tr.entry.file td.filename_no_report a { margin-left: 16px; } + +tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;} +tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);} + +tr.changeset { height: 20px } +tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; } +tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; } +tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;} +tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;} + +table.files tr.file td { text-align: center; } +table.files tr.file td.filename { text-align: left; padding-left: 24px; } +table.files tr.file td.digest { font-size: 80%; } + +table.members td.roles, table.memberships td.roles { width: 45%; } + +tr.message { height: 2.6em; } +tr.message td.subject { padding-left: 20px; } +tr.message td.created_on { white-space: nowrap; } +tr.message td.last_message { font-size: 80%; white-space: nowrap; } +tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; } +tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; } + +tr.version.closed, tr.version.closed a { color: #999; } +tr.version td.name { padding-left: 20px; } +tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; } +tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; } + +tr.user td { width:13%; } +tr.user td.email { width:18%; } +tr.user td { white-space: nowrap; } +tr.user.locked, tr.user.registered { color: #aaa; } +tr.user.locked a, tr.user.registered a { color: #aaa; } + +table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;} + +tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;} + +tr.time-entry { text-align: center; white-space: nowrap; } +tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; } +td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; } +td.hours .hours-dec { font-size: 0.9em; } + +table.plugins td { vertical-align: middle; } +table.plugins td.configure { text-align: right; padding-right: 1em; } +table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; } +table.plugins span.description { display: block; font-size: 0.9em; } +table.plugins span.url { display: block; font-size: 0.9em; } + +table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; } +table.list tbody tr.group span.count {position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;} +tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;} +tr.group:hover a.toggle-all { display:inline;} +a.toggle-all:hover {text-decoration:none;} + +table.list tbody tr:hover { background-color:#ffffdd; } +table.list tbody tr.group:hover { background-color:inherit; } +table td {padding:2px;} +table p {margin:0;} +.odd {background-color:#f6f7f8;} +.even {background-color: #fff;} + +a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; } +a.sort.asc { background-image: url(../images/sort_asc.png); } +a.sort.desc { background-image: url(../images/sort_desc.png); } + +table.attributes { width: 100% } +table.attributes th { vertical-align: top; text-align: left; } +table.attributes td { vertical-align: top; } + +table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; } +table.boards td.topic-count, table.boards td.message-count {text-align:center;} +table.boards td.last-message {font-size:80%;} + +table.messages td.author, table.messages td.created_on, table.messages td.reply-count {text-align:center;} + +table.query-columns { + border-collapse: collapse; + border: 0; +} + +table.query-columns td.buttons { + vertical-align: middle; + text-align: center; +} + +td.center {text-align:center;} + +h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; } + +div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; } +div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; } +div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; } +div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; } + +#watchers ul {margin: 0; padding: 0;} +#watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;} +#watchers select {width: 95%; display: block;} +#watchers a.delete {opacity: 0.4;} +#watchers a.delete:hover {opacity: 1;} +#watchers img.gravatar {margin: 0 4px 2px 0;} + +span#watchers_inputs {overflow:auto; display:block;} +span.search_for_watchers {display:block;} +span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;} +span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; } + + +.highlight { background-color: #FCFD8D;} +.highlight.token-1 { background-color: #faa;} +.highlight.token-2 { background-color: #afa;} +.highlight.token-3 { background-color: #aaf;} + +.box{ + padding:6px; + margin-bottom: 10px; + background-color:#f6f6f6; + color:#505050; + line-height:1.5em; + border: 1px solid #e4e4e4; +} + +div.square { + border: 1px solid #999; + float: left; + margin: .3em .4em 0 .4em; + overflow: hidden; + width: .6em; height: .6em; +} +.contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;} +.contextual input, .contextual select {font-size:0.9em;} +.message .contextual { margin-top: 0; } + +.splitcontent {overflow:auto;} +.splitcontentleft{float:left; width:49%;} +.splitcontentright{float:right; width:49%;} +form {display: inline;} +input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;} +fieldset {border: 1px solid #e4e4e4; margin:0;} +legend {color: #484848;} +hr { width: 100%; height: 1px; background: #ccc; border: 0;} +blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;} +blockquote blockquote { margin-left: 0;} +acronym { border-bottom: 1px dotted; cursor: help; } +textarea.wiki-edit {width:99%; resize:vertical;} +li p {margin-top: 0;} +div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;} +p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;} +p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; } +p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; } + +div.issue div.subject div div { padding-left: 16px; } +div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;} +div.issue div.subject>div>p { margin-top: 0.5em; } +div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;} +div.issue span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;} +div.issue .next-prev-links {color:#999;} +div.issue table.attributes th {width:22%;} +div.issue table.attributes td {width:28%;} + +#issue_tree table.issues, #relations table.issues { border: 0; } +#issue_tree td.checkbox, #relations td.checkbox {display:none;} +#relations td.buttons {padding:0;} + +fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; } +fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; } +fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); } + +fieldset#date-range p { margin: 2px 0 2px 0; } +fieldset#filters table { border-collapse: collapse; } +fieldset#filters table td { padding: 0; vertical-align: middle; } +fieldset#filters tr.filter { height: 2.1em; } +fieldset#filters td.field { width:230px; } +fieldset#filters td.operator { width:180px; } +fieldset#filters td.operator select {max-width:170px;} +fieldset#filters td.values { white-space:nowrap; } +fieldset#filters td.values select {min-width:130px;} +fieldset#filters td.values input {height:1em;} +fieldset#filters td.add-filter { text-align: right; vertical-align: top; } + +.toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;} +.buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; } + +div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;} +div#issue-changesets div.changeset { padding: 4px;} +div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; } +div#issue-changesets p { margin-top: 0; margin-bottom: 1em;} + +.journal ul.details img {margin:0 0 -3px 4px;} +div.journal {overflow:auto;} +div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;} + +div#activity dl, #search-results { margin-left: 2em; } +div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; } +div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } +div#activity dt.me .time { border-bottom: 1px solid #999; } +div#activity dt .time { color: #777; font-size: 80%; } +div#activity dd .description, #search-results dd .description { font-style: italic; } +div#activity span.project:after, #search-results span.project:after { content: " -"; } +div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; } + +#search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; } + +div#search-results-counts {float:right;} +div#search-results-counts ul { margin-top: 0.5em; } +div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; } + +dt.issue { background-image: url(../images/ticket.png); } +dt.issue-edit { background-image: url(../images/ticket_edit.png); } +dt.issue-closed { background-image: url(../images/ticket_checked.png); } +dt.issue-note { background-image: url(../images/ticket_note.png); } +dt.changeset { background-image: url(../images/changeset.png); } +dt.news { background-image: url(../images/news.png); } +dt.message { background-image: url(../images/message.png); } +dt.reply { background-image: url(../images/comments.png); } +dt.wiki-page { background-image: url(../images/wiki_edit.png); } +dt.attachment { background-image: url(../images/attachment.png); } +dt.document { background-image: url(../images/document.png); } +dt.project { background-image: url(../images/projects.png); } +dt.time-entry { background-image: url(../images/time.png); } + +#search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); } + +div#roadmap .related-issues { margin-bottom: 1em; } +div#roadmap .related-issues td.checkbox { display: none; } +div#roadmap .wiki h1:first-child { display: none; } +div#roadmap .wiki h1 { font-size: 120%; } +div#roadmap .wiki h2 { font-size: 110%; } +body.controller-versions.action-show div#roadmap .related-issues {width:70%;} + +div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; } +div#version-summary fieldset { margin-bottom: 1em; } +div#version-summary fieldset.time-tracking table { width:100%; } +div#version-summary th, div#version-summary td.total-hours { text-align: right; } + +table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; } +table#time-report tbody tr.subtotal { font-style: italic; color:#777;} +table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; } +table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;} +table#time-report .hours-dec { font-size: 0.9em; } + +div.wiki-page .contextual a {opacity: 0.4} +div.wiki-page .contextual a:hover {opacity: 1} + +form .attributes select { width: 60%; } +input#issue_subject { width: 99%; } +select#issue_done_ratio { width: 95px; } + +ul.projects {margin:0; padding-left:1em;} +ul.projects ul {padding-left:1.6em;} +ul.projects.root {margin:0; padding:0;} +ul.projects li {list-style-type:none;} + +#projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;} +#projects-index ul.projects li.root {margin-bottom: 1em;} +#projects-index ul.projects li.child {margin-top: 1em;} +#projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; } +.my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; } + +#notified-projects ul, #tracker_project_ids ul {max-height:250px; overflow-y:auto;} + +#related-issues li img {vertical-align:middle;} + +ul.properties {padding:0; font-size: 0.9em; color: #777;} +ul.properties li {list-style-type:none;} +ul.properties li span {font-style:italic;} + +.total-hours { font-size: 110%; font-weight: bold; } +.total-hours span.hours-int { font-size: 120%; } + +.autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;} +#user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; } + +#workflow_copy_form select { width: 200px; } +table.transitions td.enabled {background: #bfb;} +table.fields_permissions select {font-size:90%} +table.fields_permissions td.readonly {background:#ddd;} +table.fields_permissions td.required {background:#d88;} + +textarea#custom_field_possible_values {width: 99%} +input#content_comments {width: 99%} + +.pagination {font-size: 90%} +p.pagination {margin-top:8px;} + +/***** Tabular forms ******/ +.tabular p{ + margin: 0; + padding: 3px 0 3px 0; + padding-left: 180px; /* width of left column containing the label elements */ + min-height: 1.8em; + clear:left; +} + +html>body .tabular p {overflow:hidden;} + +.tabular label{ + font-weight: bold; + float: left; + text-align: right; + /* width of left column */ + margin-left: -180px; + /* width of labels. Should be smaller than left column to create some right margin */ + width: 175px; +} + +.tabular label.floating{ + font-weight: normal; + margin-left: 0px; + text-align: left; + width: 270px; +} + +.tabular label.block{ + font-weight: normal; + margin-left: 0px !important; + text-align: left; + float: none; + display: block; + width: auto; +} + +.tabular label.inline{ + font-weight: normal; + float:none; + margin-left: 5px !important; + width: auto; +} + +label.no-css { + font-weight: inherit; + float:none; + text-align:left; + margin-left:0px; + width:auto; +} +input#time_entry_comments { width: 90%;} + +#preview fieldset {margin-top: 1em; background: url(../images/draft.png)} + +.tabular.settings p{ padding-left: 300px; } +.tabular.settings label{ margin-left: -300px; width: 295px; } +.tabular.settings textarea { width: 99%; } + +.settings.enabled_scm table {width:100%} +.settings.enabled_scm td.scm_name{ font-weight: bold; } + +fieldset.settings label { display: block; } +fieldset#notified_events .parent { padding-left: 20px; } + +span.required {color: #bb0000;} +.summary {font-style: italic;} + +#attachments_fields input.description {margin-left: 8px; width:340px;} +#attachments_fields span {display:block; white-space:nowrap;} +#attachments_fields img {vertical-align: middle;} + +div.attachments { margin-top: 12px; } +div.attachments p { margin:4px 0 2px 0; } +div.attachments img { vertical-align: middle; } +div.attachments span.author { font-size: 0.9em; color: #888; } + +div.thumbnails {margin-top:0.6em;} +div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;} +div.thumbnails img {margin: 3px;} + +p.other-formats { text-align: right; font-size:0.9em; color: #666; } +.other-formats span + span:before { content: "| "; } + +a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; } + +em.info {font-style:normal;font-size:90%;color:#888;display:block;} +em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;} + +textarea.text_cf {width:90%;} + +/* Project members tab */ +div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% } +div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% } +div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; } +div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; } +div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; } +div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; } + +#users_for_watcher {height: 200px; overflow:auto;} +#users_for_watcher label {display: block;} + +table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; } + +input#principal_search, input#user_search {width:100%} +input#principal_search, input#user_search { + background: url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px; + border:1px solid #9EB1C2; border-radius:3px; height:1.5em; width:95%; +} +input#principal_search.ajax-loading, input#user_search.ajax-loading { + background-image: url(../images/loading.gif); +} + +* html div#tab-content-members fieldset div { height: 450px; } + +/***** Flash & error messages ****/ +#errorExplanation, div.flash, .nodata, .warning, .conflict { + padding: 4px 4px 4px 30px; + margin-bottom: 12px; + font-size: 1.1em; + border: 2px solid; +} + +div.flash {margin-top: 8px;} + +div.flash.error, #errorExplanation { + background: url(../images/exclamation.png) 8px 50% no-repeat; + background-color: #ffe3e3; + border-color: #dd0000; + color: #880000; +} + +div.flash.notice { + background: url(../images/true.png) 8px 5px no-repeat; + background-color: #dfffdf; + border-color: #9fcf9f; + color: #005f00; +} + +div.flash.warning, .conflict { + background: url(../images/warning.png) 8px 5px no-repeat; + background-color: #FFEBC1; + border-color: #FDBF3B; + color: #A6750C; + text-align: left; +} + +.nodata, .warning { + text-align: center; + background-color: #FFEBC1; + border-color: #FDBF3B; + color: #A6750C; +} + +#errorExplanation ul { font-size: 0.9em;} +#errorExplanation h2, #errorExplanation p { display: none; } + +.conflict-details {font-size:80%;} + +/***** Ajax indicator ******/ +#ajax-indicator { +position: absolute; /* fixed not supported by IE */ +background-color:#eee; +border: 1px solid #bbb; +top:35%; +left:40%; +width:20%; +font-weight:bold; +text-align:center; +padding:0.6em; +z-index:100; +opacity: 0.5; +} + +html>body #ajax-indicator { position: fixed; } + +#ajax-indicator span { +background-position: 0% 40%; +background-repeat: no-repeat; +background-image: url(../images/loading.gif); +padding-left: 26px; +vertical-align: bottom; +} + +/***** Calendar *****/ +table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;} +table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; } +table.cal thead th.week-number {width: auto;} +table.cal tbody tr {height: 100px;} +table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;} +table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;} +table.cal td p.day-num {font-size: 1.1em; text-align:right;} +table.cal td.odd p.day-num {color: #bbb;} +table.cal td.today {background:#ffffdd;} +table.cal td.today p.day-num {font-weight: bold;} +table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;} +table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;} +table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;} +p.cal.legend span {display:block;} + +/***** Tooltips ******/ +.tooltip{position:relative;z-index:24;} +.tooltip:hover{z-index:25;color:#000;} +.tooltip span.tip{display: none; text-align:left;} + +div.tooltip:hover span.tip{ +display:block; +position:absolute; +top:12px; left:24px; width:270px; +border:1px solid #555; +background-color:#fff; +padding: 4px; +font-size: 0.8em; +color:#505050; +} + +img.ui-datepicker-trigger { + cursor: pointer; + vertical-align: middle; + margin-left: 4px; +} + +/***** Progress bar *****/ +table.progress { + border-collapse: collapse; + border-spacing: 0pt; + empty-cells: show; + text-align: center; + float:left; + margin: 1px 6px 1px 0px; +} + +table.progress td { height: 1em; } +table.progress td.closed { background: #BAE0BA none repeat scroll 0%; } +table.progress td.done { background: #D3EDD3 none repeat scroll 0%; } +table.progress td.todo { background: #eee none repeat scroll 0%; } +p.pourcent {font-size: 80%;} +p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;} + +#roadmap table.progress td { height: 1.2em; } +/***** Tabs *****/ +#content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;} +#content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;} +#content .tabs ul li { + float:left; + list-style-type:none; + white-space:nowrap; + margin-right:4px; + background:#fff; + position:relative; + margin-bottom:-1px; +} +#content .tabs ul li a{ + display:block; + font-size: 0.9em; + text-decoration:none; + line-height:1.3em; + padding:4px 6px 4px 6px; + border: 1px solid #ccc; + border-bottom: 1px solid #bbbbbb; + background-color: #f6f6f6; + color:#999; + font-weight:bold; + border-top-left-radius:3px; + border-top-right-radius:3px; +} + +#content .tabs ul li a:hover { + background-color: #ffffdd; + text-decoration:none; +} + +#content .tabs ul li a.selected { + background-color: #fff; + border: 1px solid #bbbbbb; + border-bottom: 1px solid #fff; + color:#444; +} + +#content .tabs ul li a.selected:hover {background-color: #fff;} + +div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; } + +button.tab-left, button.tab-right { + font-size: 0.9em; + cursor: pointer; + height:24px; + border: 1px solid #ccc; + border-bottom: 1px solid #bbbbbb; + position:absolute; + padding:4px; + width: 20px; + bottom: -1px; +} + +button.tab-left { + right: 20px; + background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%; + border-top-left-radius:3px; +} + +button.tab-right { + right: 0; + background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%; + border-top-right-radius:3px; +} + +/***** Diff *****/ +.diff_out { background: #fcc; } +.diff_out span { background: #faa; } +.diff_in { background: #cfc; } +.diff_in span { background: #afa; } + +.text-diff { + padding: 1em; + background-color:#f6f6f6; + color:#505050; + border: 1px solid #e4e4e4; +} + +/***** Wiki *****/ +div.wiki table { + border-collapse: collapse; + margin-bottom: 1em; +} + +div.wiki table, div.wiki td, div.wiki th { + border: 1px solid #bbb; + padding: 4px; +} + +div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;} + +div.wiki .external { + background-position: 0% 60%; + background-repeat: no-repeat; + padding-left: 12px; + background-image: url(../images/external.png); +} + +div.wiki a.new {color: #b73535;} + +div.wiki ul, div.wiki ol {margin-bottom:1em;} + +div.wiki pre { + margin: 1em 1em 1em 1.6em; + padding: 8px; + background-color: #fafafa; + border: 1px solid #e2e2e2; + width:auto; + overflow-x: auto; + overflow-y: hidden; +} + +div.wiki ul.toc { + background-color: #ffffdd; + border: 1px solid #e4e4e4; + padding: 4px; + line-height: 1.2em; + margin-bottom: 12px; + margin-right: 12px; + margin-left: 0; + display: table +} +* html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */ + +div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; } +div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; } +div.wiki ul.toc ul { margin: 0; padding: 0; } +div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;} +div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;} +div.wiki ul.toc a { + font-size: 0.9em; + font-weight: normal; + text-decoration: none; + color: #606060; +} +div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;} + +a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; } +a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; } +h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; } + +div.wiki img { vertical-align: middle; } + +/***** My page layout *****/ +.block-receiver { + border:1px dashed #c0c0c0; + margin-bottom: 20px; + padding: 15px 0 15px 0; +} + +.mypage-box { + margin:0 0 20px 0; + color:#505050; + line-height:1.5em; +} + +.handle {cursor: move;} + +a.close-icon { + display:block; + margin-top:3px; + overflow:hidden; + width:12px; + height:12px; + background-repeat: no-repeat; + cursor:pointer; + background-image:url('../images/close.png'); +} +a.close-icon:hover {background-image:url('../images/close_hl.png');} + +/***** Gantt chart *****/ +.gantt_hdr { + position:absolute; + top:0; + height:16px; + border-top: 1px solid #c0c0c0; + border-bottom: 1px solid #c0c0c0; + border-right: 1px solid #c0c0c0; + text-align: center; + overflow: hidden; +} + +.gantt_hdr.nwday {background-color:#f1f1f1;} + +.gantt_subjects { font-size: 0.8em; } +.gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; } + +.task { + position: absolute; + height:8px; + font-size:0.8em; + color:#888; + padding:0; + margin:0; + line-height:16px; + white-space:nowrap; +} + +.task.label {width:100%;} +.task.label.project, .task.label.version { font-weight: bold; } + +.task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; } +.task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; } +.task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; } + +.task_todo.parent { background: #888; border: 1px solid #888; height: 3px;} +.task_late.parent, .task_done.parent { height: 3px;} +.task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;} +.task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;} + +.version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} +.version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} +.version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} +.version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; } + +.project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} +.project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} +.project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} +.project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; } + +.version-behind-schedule a, .issue-behind-schedule a {color: #f66914;} +.version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;} + +/***** Icons *****/ +.icon { + background-position: 0% 50%; + background-repeat: no-repeat; + padding-left: 20px; + padding-top: 2px; + padding-bottom: 3px; +} + +.icon-add { background-image: url(../images/add.png); } +.icon-edit { background-image: url(../images/edit.png); } +.icon-copy { background-image: url(../images/copy.png); } +.icon-duplicate { background-image: url(../images/duplicate.png); } +.icon-del { background-image: url(../images/delete.png); } +.icon-move { background-image: url(../images/move.png); } +.icon-save { background-image: url(../images/save.png); } +.icon-cancel { background-image: url(../images/cancel.png); } +.icon-multiple { background-image: url(../images/table_multiple.png); } +.icon-folder { background-image: url(../images/folder.png); } +.open .icon-folder { background-image: url(../images/folder_open.png); } +.icon-package { background-image: url(../images/package.png); } +.icon-user { background-image: url(../images/user.png); } +.icon-projects { background-image: url(../images/projects.png); } +.icon-help { background-image: url(../images/help.png); } +.icon-attachment { background-image: url(../images/attachment.png); } +.icon-history { background-image: url(../images/history.png); } +.icon-time { background-image: url(../images/time.png); } +.icon-time-add { background-image: url(../images/time_add.png); } +.icon-stats { background-image: url(../images/stats.png); } +.icon-warning { background-image: url(../images/warning.png); } +.icon-fav { background-image: url(../images/fav.png); } +.icon-fav-off { background-image: url(../images/fav_off.png); } +.icon-reload { background-image: url(../images/reload.png); } +.icon-lock { background-image: url(../images/locked.png); } +.icon-unlock { background-image: url(../images/unlock.png); } +.icon-checked { background-image: url(../images/true.png); } +.icon-details { background-image: url(../images/zoom_in.png); } +.icon-report { background-image: url(../images/report.png); } +.icon-comment { background-image: url(../images/comment.png); } +.icon-summary { background-image: url(../images/lightning.png); } +.icon-server-authentication { background-image: url(../images/server_key.png); } +.icon-issue { background-image: url(../images/ticket.png); } +.icon-zoom-in { background-image: url(../images/zoom_in.png); } +.icon-zoom-out { background-image: url(../images/zoom_out.png); } +.icon-passwd { background-image: url(../images/textfield_key.png); } +.icon-test { background-image: url(../images/bullet_go.png); } + +.icon-file { background-image: url(../images/files/default.png); } +.icon-file.text-plain { background-image: url(../images/files/text.png); } +.icon-file.text-x-c { background-image: url(../images/files/c.png); } +.icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); } +.icon-file.text-x-java { background-image: url(../images/files/java.png); } +.icon-file.text-x-javascript { background-image: url(../images/files/js.png); } +.icon-file.text-x-php { background-image: url(../images/files/php.png); } +.icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); } +.icon-file.text-xml { background-image: url(../images/files/xml.png); } +.icon-file.text-css { background-image: url(../images/files/css.png); } +.icon-file.text-html { background-image: url(../images/files/html.png); } +.icon-file.image-gif { background-image: url(../images/files/image.png); } +.icon-file.image-jpeg { background-image: url(../images/files/image.png); } +.icon-file.image-png { background-image: url(../images/files/image.png); } +.icon-file.image-tiff { background-image: url(../images/files/image.png); } +.icon-file.application-pdf { background-image: url(../images/files/pdf.png); } +.icon-file.application-zip { background-image: url(../images/files/zip.png); } +.icon-file.application-x-gzip { background-image: url(../images/files/zip.png); } + +img.gravatar { + padding: 2px; + border: solid 1px #d5d5d5; + background: #fff; + vertical-align: middle; +} + +div.issue img.gravatar { + float: left; + margin: 0 6px 0 0; + padding: 5px; +} + +div.issue table img.gravatar { + height: 14px; + width: 14px; + padding: 2px; + float: left; + margin: 0 0.5em 0 0; +} + +h2 img.gravatar {margin: -2px 4px -4px 0;} +h3 img.gravatar {margin: -4px 4px -4px 0;} +h4 img.gravatar {margin: -6px 4px -4px 0;} +td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;} +#activity dt img.gravatar {float: left; margin: 0 1em 1em 0;} +/* Used on 12px Gravatar img tags without the icon background */ +.icon-gravatar {float: left; margin-right: 4px;} + +#activity dt, .journal {clear: left;} + +.journal-link {float: right;} + +h2 img { vertical-align:middle; } + +.hascontextmenu { cursor: context-menu; } + +/************* CodeRay styles *************/ +.syntaxhl div {display: inline;} +.syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;} +.syntaxhl .code pre { overflow: auto } +.syntaxhl .debug { color: white !important; background: blue !important; } + +.syntaxhl .annotation { color:#007 } +.syntaxhl .attribute-name { color:#b48 } +.syntaxhl .attribute-value { color:#700 } +.syntaxhl .binary { color:#509 } +.syntaxhl .char .content { color:#D20 } +.syntaxhl .char .delimiter { color:#710 } +.syntaxhl .char { color:#D20 } +.syntaxhl .class { color:#258; font-weight:bold } +.syntaxhl .class-variable { color:#369 } +.syntaxhl .color { color:#0A0 } +.syntaxhl .comment { color:#385 } +.syntaxhl .comment .char { color:#385 } +.syntaxhl .comment .delimiter { color:#385 } +.syntaxhl .complex { color:#A08 } +.syntaxhl .constant { color:#258; font-weight:bold } +.syntaxhl .decorator { color:#B0B } +.syntaxhl .definition { color:#099; font-weight:bold } +.syntaxhl .delimiter { color:black } +.syntaxhl .directive { color:#088; font-weight:bold } +.syntaxhl .doc { color:#970 } +.syntaxhl .doc-string { color:#D42; font-weight:bold } +.syntaxhl .doctype { color:#34b } +.syntaxhl .entity { color:#800; font-weight:bold } +.syntaxhl .error { color:#F00; background-color:#FAA } +.syntaxhl .escape { color:#666 } +.syntaxhl .exception { color:#C00; font-weight:bold } +.syntaxhl .float { color:#06D } +.syntaxhl .function { color:#06B; font-weight:bold } +.syntaxhl .global-variable { color:#d70 } +.syntaxhl .hex { color:#02b } +.syntaxhl .imaginary { color:#f00 } +.syntaxhl .include { color:#B44; font-weight:bold } +.syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black } +.syntaxhl .inline-delimiter { font-weight: bold; color: #666 } +.syntaxhl .instance-variable { color:#33B } +.syntaxhl .integer { color:#06D } +.syntaxhl .key .char { color: #60f } +.syntaxhl .key .delimiter { color: #404 } +.syntaxhl .key { color: #606 } +.syntaxhl .keyword { color:#939; font-weight:bold } +.syntaxhl .label { color:#970; font-weight:bold } +.syntaxhl .local-variable { color:#963 } +.syntaxhl .namespace { color:#707; font-weight:bold } +.syntaxhl .octal { color:#40E } +.syntaxhl .operator { } +.syntaxhl .predefined { color:#369; font-weight:bold } +.syntaxhl .predefined-constant { color:#069 } +.syntaxhl .predefined-type { color:#0a5; font-weight:bold } +.syntaxhl .preprocessor { color:#579 } +.syntaxhl .pseudo-class { color:#00C; font-weight:bold } +.syntaxhl .regexp .content { color:#808 } +.syntaxhl .regexp .delimiter { color:#404 } +.syntaxhl .regexp .modifier { color:#C2C } +.syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); } +.syntaxhl .reserved { color:#080; font-weight:bold } +.syntaxhl .shell .content { color:#2B2 } +.syntaxhl .shell .delimiter { color:#161 } +.syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); } +.syntaxhl .string .char { color: #46a } +.syntaxhl .string .content { color: #46a } +.syntaxhl .string .delimiter { color: #46a } +.syntaxhl .string .modifier { color: #46a } +.syntaxhl .symbol .content { color:#d33 } +.syntaxhl .symbol .delimiter { color:#d33 } +.syntaxhl .symbol { color:#d33 } +.syntaxhl .tag { color:#070 } +.syntaxhl .type { color:#339; font-weight:bold } +.syntaxhl .value { color: #088; } +.syntaxhl .variable { color:#037 } + +.syntaxhl .insert { background: hsla(120,100%,50%,0.12) } +.syntaxhl .delete { background: hsla(0,100%,50%,0.12) } +.syntaxhl .change { color: #bbf; background: #007; } +.syntaxhl .head { color: #f8f; background: #505 } +.syntaxhl .head .filename { color: white; } + +.syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; } +.syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; } + +.syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold } +.syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold } +.syntaxhl .change .change { color: #88f } +.syntaxhl .head .head { color: #f4f } + +/***** Media print specific styles *****/ +@media print { + #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; } + #main { background: #fff; } + #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;} + #wiki_add_attachment { display:none; } + .hide-when-print { display: none; } + .autoscroll {overflow-x: visible;} + table.list {margin-top:0.5em;} + table.list th, table.list td {border: 1px solid #aaa;} +} + +/* Accessibility specific styles */ +.hidden-for-sighted { + position:absolute; + left:-10000px; + top:auto; + width:1px; + height:1px; + overflow:hidden; +} diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d5/d50e8c11a3579a8c45b25d637c47f3b304f9eb14.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d5/d50e8c11a3579a8c45b25d637c47f3b304f9eb14.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,32 @@ +<%= error_messages_for 'role' %> + +<% unless @role.anonymous? %> +
    +<% unless @role.builtin? %> +

    <%= f.text_field :name, :required => true %>

    +

    <%= f.check_box :assignable %>

    +<% end %> +

    <%= f.select :issues_visibility, Role::ISSUES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %>

    +<% if @role.new_record? && @roles.any? %> +

    +<%= select_tag(:copy_workflow_from, content_tag("option") + options_from_collection_for_select(@roles, :id, :name, params[:copy_workflow_from] || @copy_from.try(:id))) %>

    +<% end %> +
    +<% end %> + +

    <%= l(:label_permissions) %>

    +
    +<% perms_by_module = @role.setable_permissions.group_by {|p| p.project_module.to_s} %> +<% perms_by_module.keys.sort.each do |mod| %> +
    <%= mod.blank? ? l(:label_project) : l_or_humanize(mod, :prefix => 'project_module_') %> + <% perms_by_module[mod].each do |permission| %> + + <% end %> +
    +<% end %> +
    <%= check_all_links 'permissions' %> +<%= hidden_field_tag 'role[permissions][]', '' %> +
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d5/d5af5255aeef7f16693678f7afc164c0ff6fdbec.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d5/d5af5255aeef7f16693678f7afc164c0ff6fdbec.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,9 @@ +class AddCustomFieldsMultiple < ActiveRecord::Migration + def self.up + add_column :custom_fields, :multiple, :boolean, :default => false + end + + def self.down + remove_column :custom_fields, :multiple + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d5/d5c42a8cf284c9057072604f567dd5624a3806d3.svn-base --- a/.svn/pristine/d5/d5c42a8cf284c9057072604f567dd5624a3806d3.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,313 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -require 'iconv' -require 'redmine/codeset_util' - -module RepositoriesHelper - def format_revision(revision) - if revision.respond_to? :format_identifier - revision.format_identifier - else - revision.to_s - end - end - - def truncate_at_line_break(text, length = 255) - if text - text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...') - end - end - - def render_properties(properties) - unless properties.nil? || properties.empty? - content = '' - properties.keys.sort.each do |property| - content << content_tag('li', "#{h property}: #{h properties[property]}".html_safe) - end - content_tag('ul', content.html_safe, :class => 'properties') - end - end - - def render_changeset_changes - changes = @changeset.changes.find(:all, :limit => 1000, :order => 'path').collect do |change| - case change.action - when 'A' - # Detects moved/copied files - if !change.from_path.blank? - change.action = - @changeset.changes.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C' - end - change - when 'D' - @changeset.changes.detect {|c| c.from_path == change.path} ? nil : change - else - change - end - end.compact - - tree = { } - changes.each do |change| - p = tree - dirs = change.path.to_s.split('/').select {|d| !d.blank?} - path = '' - dirs.each do |dir| - path += '/' + dir - p[:s] ||= {} - p = p[:s] - p[path] ||= {} - p = p[path] - end - p[:c] = change - end - render_changes_tree(tree[:s]) - end - - def render_changes_tree(tree) - return '' if tree.nil? - output = '' - output << '
      ' - tree.keys.sort.each do |file| - style = 'change' - text = File.basename(h(file)) - if s = tree[file][:s] - style << ' folder' - path_param = to_path_param(@repository.relative_path(file)) - text = link_to(h(text), :controller => 'repositories', - :action => 'show', - :id => @project, - :path => path_param, - :rev => @changeset.identifier) - output << "
    • #{text}
    • " - output << render_changes_tree(s) - elsif c = tree[file][:c] - style << " change-#{c.action}" - path_param = to_path_param(@repository.relative_path(c.path)) - text = link_to(h(text), :controller => 'repositories', - :action => 'entry', - :id => @project, - :path => path_param, - :rev => @changeset.identifier) unless c.action == 'D' - text << " - #{h(c.revision)}" unless c.revision.blank? - text << ' ('.html_safe + link_to(l(:label_diff), :controller => 'repositories', - :action => 'diff', - :id => @project, - :path => path_param, - :rev => @changeset.identifier) + ') '.html_safe if c.action == 'M' - text << ' '.html_safe + content_tag('span', h(c.from_path), :class => 'copied-from') unless c.from_path.blank? - output << "
    • #{text}
    • " - end - end - output << '
    ' - output.html_safe - end - - def repository_field_tags(form, repository) - method = repository.class.name.demodulize.underscore + "_field_tags" - if repository.is_a?(Repository) && - respond_to?(method) && method != 'repository_field_tags' - send(method, form, repository) - end - end - - def scm_select_tag(repository) - scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']] - Redmine::Scm::Base.all.each do |scm| - if Setting.enabled_scm.include?(scm) || - (repository && repository.class.name.demodulize == scm) - scm_options << ["Repository::#{scm}".constantize.scm_name, scm] - end - end - select_tag('repository_scm', - options_for_select(scm_options, repository.class.name.demodulize), - :disabled => (repository && !repository.new_record?), - :onchange => remote_function( - :url => { - :controller => 'repositories', - :action => 'edit', - :id => @project - }, - :method => :get, - :with => "Form.serialize(this.form)") - ) - end - - def with_leading_slash(path) - path.to_s.starts_with?('/') ? path : "/#{path}" - end - - def without_leading_slash(path) - path.gsub(%r{^/+}, '') - end - - def subversion_field_tags(form, repository) - content_tag('p', form.text_field(:url, :size => 60, :required => true, - :disabled => (repository && !repository.root_url.blank?)) + - '
    '.html_safe + - '(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') + - content_tag('p', form.text_field(:login, :size => 30)) + - content_tag('p', form.password_field( - :password, :size => 30, :name => 'ignore', - :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)), - :onfocus => "this.value=''; this.name='repository[password]';", - :onchange => "this.name='repository[password]';")) - end - - def darcs_field_tags(form, repository) - content_tag('p', form.text_field( - :url, :label => l(:field_path_to_repository), - :size => 60, :required => true, - :disabled => (repository && !repository.new_record?))) + - content_tag('p', form.select( - :log_encoding, [nil] + Setting::ENCODINGS, - :label => l(:field_commit_logs_encoding), :required => true)) - end - - def mercurial_field_tags(form, repository) - content_tag('p', form.text_field( - :url, :label => l(:field_path_to_repository), - :size => 60, :required => true, - :disabled => (repository && !repository.root_url.blank?) - ) + - '
    '.html_safe + l(:text_mercurial_repository_note)) + - content_tag('p', form.select( - :path_encoding, [nil] + Setting::ENCODINGS, - :label => l(:field_scm_path_encoding) - ) + - '
    '.html_safe + l(:text_scm_path_encoding_note)) - end - - def git_field_tags(form, repository) - content_tag('p', form.text_field( - :url, :label => l(:field_path_to_repository), - :size => 60, :required => true, - :disabled => (repository && !repository.root_url.blank?) - ) + - '
    '.html_safe + - l(:text_git_repository_note)) + - content_tag('p', form.select( - :path_encoding, [nil] + Setting::ENCODINGS, - :label => l(:field_scm_path_encoding) - ) + - '
    '.html_safe + l(:text_scm_path_encoding_note)) + - content_tag('p', form.check_box( - :extra_report_last_commit, - :label => l(:label_git_report_last_commit) - )) - end - - def cvs_field_tags(form, repository) - content_tag('p', form.text_field( - :root_url, - :label => l(:field_cvsroot), - :size => 60, :required => true, - :disabled => !repository.new_record?)) + - content_tag('p', form.text_field( - :url, - :label => l(:field_cvs_module), - :size => 30, :required => true, - :disabled => !repository.new_record?)) + - content_tag('p', form.select( - :log_encoding, [nil] + Setting::ENCODINGS, - :label => l(:field_commit_logs_encoding), :required => true)) + - content_tag('p', form.select( - :path_encoding, [nil] + Setting::ENCODINGS, - :label => l(:field_scm_path_encoding) - ) + - '
    '.html_safe + l(:text_scm_path_encoding_note)) - end - - def bazaar_field_tags(form, repository) - content_tag('p', form.text_field( - :url, :label => l(:field_path_to_repository), - :size => 60, :required => true, - :disabled => (repository && !repository.new_record?))) + - content_tag('p', form.select( - :log_encoding, [nil] + Setting::ENCODINGS, - :label => l(:field_commit_logs_encoding), :required => true)) - end - - def filesystem_field_tags(form, repository) - content_tag('p', form.text_field( - :url, :label => l(:field_root_directory), - :size => 60, :required => true, - :disabled => (repository && !repository.root_url.blank?))) + - content_tag('p', form.select( - :path_encoding, [nil] + Setting::ENCODINGS, - :label => l(:field_scm_path_encoding) - ) + - '
    '.html_safe + l(:text_scm_path_encoding_note)) - end - - def index_commits(commits, heads, href_proc = nil) - return nil if commits.nil? or commits.first.parents.nil? - map = {} - commit_hashes = [] - refs_map = {} - href_proc ||= Proc.new {|x|x} - heads.each{|r| refs_map[r.scmid] ||= []; refs_map[r.scmid] << r} - commits.reverse.each_with_index do |c, i| - h = {} - h[:parents] = c.parents.collect do |p| - [p.scmid, 0, 0] - end - h[:rdmid] = i - h[:space] = 0 - h[:refs] = refs_map[c.scmid].join(" ") if refs_map.include? c.scmid - h[:scmid] = c.scmid - h[:href] = href_proc.call(c.scmid) - commit_hashes << h - map[c.scmid] = h - end - heads.sort! do |a,b| - a.to_s <=> b.to_s - end - j = 0 - heads.each do |h| - if map.include? h.scmid then - j = mark_chain(j += 1, map[h.scmid], map) - end - end - # when no head matched anything use first commit - if j == 0 then - mark_chain(j += 1, map.values.first, map) - end - map - end - - def mark_chain(mark, commit, map) - stack = [[mark, commit]] - markmax = mark - until stack.empty? - current = stack.pop - m, commit = current - commit[:space] = m if commit[:space] == 0 - m1 = m - 1 - commit[:parents].each_with_index do |p, i| - psha = p[0] - if map.include? psha and map[psha][:space] == 0 then - stack << [m1 += 1, map[psha]] if i == 0 - stack = [[m1 += 1, map[psha]]] + stack if i > 0 - end - end - markmax = m1 if markmax < m1 - end - markmax - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d5/d5fb8b749aef7b90edab4c97451f4b95781fa82b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d5/d5fb8b749aef7b90edab4c97451f4b95781fa82b.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,23 @@ +/* Estonian initialisation for the jQuery UI date picker plugin. */ +/* Written by Mart Sõmermaa (mrts.pydev at gmail com). */ +jQuery(function($){ + $.datepicker.regional['et'] = { + closeText: 'Sulge', + prevText: 'Eelnev', + nextText: 'Järgnev', + currentText: 'Täna', + monthNames: ['Jaanuar','Veebruar','Märts','Aprill','Mai','Juuni', + 'Juuli','August','September','Oktoober','November','Detsember'], + monthNamesShort: ['Jaan', 'Veebr', 'Märts', 'Apr', 'Mai', 'Juuni', + 'Juuli', 'Aug', 'Sept', 'Okt', 'Nov', 'Dets'], + dayNames: ['Pühapäev', 'Esmaspäev', 'Teisipäev', 'Kolmapäev', 'Neljapäev', 'Reede', 'Laupäev'], + dayNamesShort: ['Pühap', 'Esmasp', 'Teisip', 'Kolmap', 'Neljap', 'Reede', 'Laup'], + dayNamesMin: ['P','E','T','K','N','R','L'], + weekHeader: 'näd', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['et']); +}); \ No newline at end of file diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d6/d66065a9dcfc99aec6ff539c6e6b8b39441b8dc8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d6/d66065a9dcfc99aec6ff539c6e6b8b39441b8dc8.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,42 @@ +<%= form_tag(project_enumerations_path(@project), :method => :put, :class => "tabular") do %> + + + + + + <% TimeEntryActivity.new.available_custom_fields.each do |value| %> + + <% end %> + + + + <% @project.activities(true).each do |enumeration| %> + <%= fields_for "enumerations[#{enumeration.id}]", enumeration do |ff| %> + + + + <% enumeration.custom_field_values.each do |value| %> + + <% end %> + + + <% end %> + <% end %> +
    <%= l(:field_name) %><%= l(:enumeration_system_activity) %><%= h value.name %><%= l(:field_active) %>
    + <%= ff.hidden_field :parent_id, :value => enumeration.id unless enumeration.project %> + <%= h(enumeration) %> + <%= checked_image !enumeration.project %> + <%= custom_field_tag "enumerations[#{enumeration.id}]", value %> + + <%= ff.check_box :active %> +
    + +
    +<%= link_to(l(:button_reset), project_enumerations_path(@project), + :method => :delete, + :data => {:confirm => l(:text_are_you_sure)}, + :class => 'icon icon-del') %> +
    + +<%= submit_tag l(:button_save) %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d6/d67c3b233c2de5e381b50b04ecf28037f5dff73a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d6/d67c3b233c2de5e381b50b04ecf28037f5dff73a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,133 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) +require 'sys_controller' +require 'mocha' + +# Re-raise errors caught by the controller. +class SysController; def rescue_action(e) raise e end; end + +class SysControllerTest < ActionController::TestCase + fixtures :projects, :repositories, :enabled_modules + + def setup + @controller = SysController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + Setting.sys_api_enabled = '1' + Setting.enabled_scm = %w(Subversion Git) + end + + def teardown + Setting.clear_cache + end + + def test_projects_with_repository_enabled + get :projects + assert_response :success + assert_equal 'application/xml', @response.content_type + with_options :tag => 'projects' do |test| + test.assert_tag :children => { :count => Project.active.has_module(:repository).count } + test.assert_tag 'project', :child => {:tag => 'identifier', :sibling => {:tag => 'is-public'}} + end + assert_no_tag 'extra-info' + assert_no_tag 'extra_info' + end + + def test_create_project_repository + assert_nil Project.find(4).repository + + post :create_project_repository, :id => 4, + :vendor => 'Subversion', + :repository => { :url => 'file:///create/project/repository/subproject2'} + assert_response :created + assert_equal 'application/xml', @response.content_type + + r = Project.find(4).repository + assert r.is_a?(Repository::Subversion) + assert_equal 'file:///create/project/repository/subproject2', r.url + + assert_tag 'repository-subversion', + :child => { + :tag => 'id', :content => r.id.to_s, + :sibling => {:tag => 'url', :content => r.url} + } + assert_no_tag 'extra-info' + assert_no_tag 'extra_info' + end + + def test_create_already_existing + post :create_project_repository, :id => 1, + :vendor => 'Subversion', + :repository => { :url => 'file:///create/project/repository/subproject2'} + + assert_response :conflict + end + + def test_create_with_failure + post :create_project_repository, :id => 4, + :vendor => 'Subversion', + :repository => { :url => 'invalid url'} + + assert_response :unprocessable_entity + end + + def test_fetch_changesets + Repository::Subversion.any_instance.expects(:fetch_changesets).twice.returns(true) + get :fetch_changesets + assert_response :success + end + + def test_fetch_changesets_one_project_by_identifier + Repository::Subversion.any_instance.expects(:fetch_changesets).once.returns(true) + get :fetch_changesets, :id => 'ecookbook' + assert_response :success + end + + def test_fetch_changesets_one_project_by_id + Repository::Subversion.any_instance.expects(:fetch_changesets).once.returns(true) + get :fetch_changesets, :id => '1' + assert_response :success + end + + def test_fetch_changesets_unknown_project + get :fetch_changesets, :id => 'unknown' + assert_response 404 + end + + def test_disabled_ws_should_respond_with_403_error + with_settings :sys_api_enabled => '0' do + get :projects + assert_response 403 + end + end + + def test_api_key + with_settings :sys_api_key => 'my_secret_key' do + get :projects, :key => 'my_secret_key' + assert_response :success + end + end + + def test_wrong_key_should_respond_with_403_error + with_settings :sys_api_enabled => 'my_secret_key' do + get :projects, :key => 'wrong_key' + assert_response 403 + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d6/d6dee0eaf76929c4e06b2f2bc3058bdad1d0a1fc.svn-base Binary file .svn/pristine/d6/d6dee0eaf76929c4e06b2f2bc3058bdad1d0a1fc.svn-base has changed diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d6/d6fa2ab030f2e7ebc86e650e4acc6d8328176458.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d6/d6fa2ab030f2e7ebc86e650e4acc6d8328176458.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,121 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +namespace :redmine do + namespace :attachments do + desc 'Removes uploaded files left unattached after one day.' + task :prune => :environment do + Attachment.prune + end + end + + namespace :tokens do + desc 'Removes expired tokens.' + task :prune => :environment do + Token.destroy_expired + end + end + + namespace :watchers do + desc 'Removes watchers from what they can no longer view.' + task :prune => :environment do + Watcher.prune + end + end + + desc 'Fetch changesets from the repositories' + task :fetch_changesets => :environment do + Repository.fetch_changesets + end + + desc 'Migrates and copies plugins assets.' + task :plugins do + Rake::Task["redmine:plugins:migrate"].invoke + Rake::Task["redmine:plugins:assets"].invoke + end + + namespace :plugins do + desc 'Migrates installed plugins.' + task :migrate => :environment do + name = ENV['NAME'] + version = nil + version_string = ENV['VERSION'] + if version_string + if version_string =~ /^\d+$/ + version = version_string.to_i + if name.nil? + abort "The VERSION argument requires a plugin NAME." + end + else + abort "Invalid VERSION #{version_string} given." + end + end + + begin + Redmine::Plugin.migrate(name, version) + rescue Redmine::PluginNotFound + abort "Plugin #{name} was not found." + end + + Rake::Task["db:schema:dump"].invoke + end + + desc 'Copies plugins assets into the public directory.' + task :assets => :environment do + name = ENV['NAME'] + + begin + Redmine::Plugin.mirror_assets(name) + rescue Redmine::PluginNotFound + abort "Plugin #{name} was not found." + end + end + + desc 'Runs the plugins tests.' + task :test do + Rake::Task["redmine:plugins:test:units"].invoke + Rake::Task["redmine:plugins:test:functionals"].invoke + Rake::Task["redmine:plugins:test:integration"].invoke + end + + namespace :test do + desc 'Runs the plugins unit tests.' + Rake::TestTask.new :units => "db:test:prepare" do |t| + t.libs << "test" + t.verbose = true + t.pattern = "plugins/#{ENV['NAME'] || '*'}/test/unit/**/*_test.rb" + end + + desc 'Runs the plugins functional tests.' + Rake::TestTask.new :functionals => "db:test:prepare" do |t| + t.libs << "test" + t.verbose = true + t.pattern = "plugins/#{ENV['NAME'] || '*'}/test/functional/**/*_test.rb" + end + + desc 'Runs the plugins integration tests.' + Rake::TestTask.new :integration => "db:test:prepare" do |t| + t.libs << "test" + t.verbose = true + t.pattern = "plugins/#{ENV['NAME'] || '*'}/test/integration/**/*_test.rb" + end + end + end +end + +# Load plugins' rake tasks +Dir[File.join(Rails.root, "plugins/*/lib/tasks/**/*.rake")].sort.each { |ext| load ext } diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d7/d71cf18bdc19861ad8ec64f239467d87c9f0a844.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d7/d71cf18bdc19861ad8ec64f239467d87c9f0a844.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1088 @@ +# Hebrew translation for Redmine +# Initiated by Dotan Nahum (dipidi@gmail.com) +# Jul 2010 - Updated by Orgad Shaneh (orgads@gmail.com) + +he: + direction: rtl + date: + formats: + default: "%d/%m/%Y" + short: "%d/%m" + long: "%d/%m/%Y" + only_day: "%e" + + day_names: [ר×שון, שני, שלישי, רביעי, חמישי, שישי, שבת] + abbr_day_names: ["×'", "ב'", "×’'", "ד'", "×”'", "ו'", "ש'"] + month_names: [~, ינו×ר, פברו×ר, מרץ, ×פריל, מ××™, יוני, יולי, ×וגוסט, ספטמבר, ×וקטובר, נובמבר, דצמבר] + abbr_month_names: [~, ×™×× , פבר, מרץ, ×פר, מ××™, יונ, יול, ×וג, ספט, ×וק, נוב, דצמ] + order: + - :day + - :month + - :year + + time: + formats: + default: "%a %d/%m/%Y %H:%M:%S" + time: "%H:%M" + short: "%d %b %H:%M" + long: "%B %d, %Y %H:%M" + only_second: "%S" + + datetime: + formats: + default: "%d-%m-%YT%H:%M:%S%Z" + + am: 'am' + pm: 'pm' + + datetime: + distance_in_words: + half_a_minute: 'חצי דקה' + less_than_x_seconds: + zero: 'פחות משניה' + one: 'פחות משניה' + other: 'פחות מ־%{count} שניות' + x_seconds: + one: 'שניה ×חת' + other: '%{count} שניות' + less_than_x_minutes: + zero: 'פחות מדקה ×חת' + one: 'פחות מדקה ×חת' + other: 'פחות מ־%{count} דקות' + x_minutes: + one: 'דקה ×חת' + other: '%{count} דקות' + about_x_hours: + one: 'בערך שעה ×חת' + other: 'בערך %{count} שעות' + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: '×™×•× ×חד' + other: '%{count} ימי×' + about_x_months: + one: 'בערך חודש ×חד' + other: 'בערך %{count} חודשי×' + x_months: + one: 'חודש ×חד' + other: '%{count} חודשי×' + about_x_years: + one: 'בערך שנה ×חת' + other: 'בערך %{count} שני×' + over_x_years: + one: 'מעל שנה ×חת' + other: 'מעל %{count} שני×' + almost_x_years: + one: "כמעט שנה" + other: "כמעט %{count} שני×" + + number: + format: + precision: 3 + separator: '.' + delimiter: ',' + currency: + format: + unit: 'ש"×—' + precision: 2 + format: '%u %n' + human: + storage_units: + format: "%n %u" + units: + byte: + one: "בייט" + other: "בתי×" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + + support: + array: + sentence_connector: "וג×" + skip_last_comma: true + + activerecord: + errors: + template: + header: + one: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + messages: + inclusion: "×œ× × ×›×œ×œ ברשימה" + exclusion: "×œ× ×–×ž×™×Ÿ" + invalid: "×œ× ×•×œ×™×“×™" + confirmation: "×œ× ×ª×•×× ×œ×ישור" + accepted: "חייב ב×ישור" + empty: "חייב להכלל" + blank: "חייב להכלל" + too_long: "×רוך מדי (×œ× ×™×•×ª×¨ מ־%{count} תוי×)" + too_short: "קצר מדי (×œ× ×™×•×ª×¨ מ־%{count} תוי×)" + wrong_length: "×œ× ×‘×ורך הנכון (חייב להיות %{count} תוי×)" + taken: "×œ× ×–×ž×™×Ÿ" + not_a_number: "×”×•× ×œ× ×ž×¡×¤×¨" + greater_than: "חייב להיות גדול מ־%{count}" + greater_than_or_equal_to: "חייב להיות גדול ×ו שווה ל־%{count}" + equal_to: "חייב להיות שווה ל־%{count}" + less_than: "חייב להיות קטן מ־%{count}" + less_than_or_equal_to: "חייב להיות קטן ×ו שווה ל־%{count}" + odd: "חייב להיות ××™ זוגי" + even: "חייב להיות זוגי" + greater_than_start_date: "חייב להיות מ×וחר יותר מת×ריך ההתחלה" + not_same_project: "×œ× ×©×™×™×š ל×ותו הפרויקט" + circular_dependency: "קשר ×–×” יצור תלות מעגלית" + cant_link_an_issue_with_a_descendant: "×œ× × ×™×ª×Ÿ לקשר × ×•×©× ×œ×ª×ªÖ¾×ž×©×™×ž×” שלו" + + actionview_instancetag_blank_option: בחר בבקשה + + general_text_No: 'ל×' + general_text_Yes: 'כן' + general_text_no: 'ל×' + general_text_yes: 'כן' + general_lang_name: 'Hebrew (עברית)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: ISO-8859-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '7' + + notice_account_updated: החשבון עודכן בהצלחה! + notice_account_invalid_creditentials: ×©× ×ž×©×ª×ž×© ×ו סיסמה ×©×’×•×™×™× + notice_account_password_updated: הסיסמה עודכנה בהצלחה! + notice_account_wrong_password: סיסמה שגויה + notice_account_register_done: החשבון נוצר בהצלחה. להפעלת החשבון לחץ על הקישור שנשלח לדו×"ל שלך. + notice_account_unknown_email: משתמש ×œ× ×ž×•×›×¨. + notice_can_t_change_password: החשבון ×”×–×” משתמש במקור הזדהות חיצוני. שינוי סיסמה הינו בילתי ×פשר + notice_account_lost_email_sent: דו×"ל ×¢× ×”×•×¨×ות לבחירת סיסמה חדשה נשלח ×ליך. + notice_account_activated: חשבונך הופעל. ×תה יכול להתחבר כעת. + notice_successful_create: יצירה מוצלחת. + notice_successful_update: עידכון מוצלח. + notice_successful_delete: מחיקה מוצלחת. + notice_successful_connection: חיבור מוצלח. + notice_file_not_found: הדף ש×תה מנסה לגשת ×ליו ×ינו ×§×™×™× ×ו שהוסר. + notice_locking_conflict: המידע עודכן על ידי משתמש ×חר. + notice_not_authorized: ×ינך מורשה לר×ות דף ×–×”. + notice_not_authorized_archived_project: הפרויקט ש×תה מנסה לגשת ×ליו × ×ž×¦× ×‘×רכיון. + notice_email_sent: "דו×ל נשלח לכתובת %{value}" + notice_email_error: "×רעה שגי××” בעת שליחת הדו×ל (%{value})" + notice_feeds_access_key_reseted: מפתח ×”Ö¾RSS שלך ×ופס. + notice_api_access_key_reseted: מפתח הגישה שלך ל־API ×ופס. + notice_failed_to_save_issues: "נכשרת בשמירת %{count} נוש××™× ×‘ %{total} נבחרו: %{ids}." + notice_failed_to_save_members: "כשלון בשמירת חבר(×™×): %{errors}." + notice_no_issue_selected: "×œ× × ×‘×—×¨ ××£ נוש×! בחר בבקשה ×ת הנוש××™× ×©×‘×¨×¦×•× ×š לערוך." + notice_account_pending: "החשבון שלך נוצר ועתה מחכה ל×ישור מנהל המערכת." + notice_default_data_loaded: ×פשרויות ברירת מחדל מופעלות. + notice_unable_delete_version: ×œ× × ×™×ª×Ÿ למחוק גירסה + notice_unable_delete_time_entry: ×œ× × ×™×ª×Ÿ למחוק רשומת זמן. + notice_issue_done_ratios_updated: ×חוזי התקדמות ×œ× ×•×©× ×¢×•×“×›× ×•. + + error_can_t_load_default_data: "×פשרויות ברירת המחדל ×œ× ×”×¦×œ×™×—×• להיטען: %{value}" + error_scm_not_found: כניסה ו\×ו מהדורה ××™× × ×§×™×™×ž×™× ×‘×ž×גר. + error_scm_command_failed: "×רעה שגי××” בעת ניסון גישה למ×גר: %{value}" + error_scm_annotate: "הכניסה ×œ× ×§×™×™×ž×ª ×ו ×©×œ× × ×™×ª×Ÿ לת×ר ×ותה." + error_issue_not_found_in_project: 'הנוש××™× ×œ× × ×ž×¦×ו ×ו ××™× × ×©×™×›×™× ×œ×¤×¨×•×™×§×˜' + error_no_tracker_in_project: ×œ× ×”×•×’×“×¨ סיווג לפרויקט ×–×”. × × ×‘×“×•×§ ×ת הגדרות הפרויקט. + error_no_default_issue_status: ×œ× ×ž×•×’×“×¨ מצב ברירת מחדל לנוש××™×. × × ×‘×“×•×§ ×ת התצורה ("ניהול -> מצבי נוש×"). + error_can_not_delete_custom_field: ×œ× × ×™×ª×Ÿ למחוק שדה מות×× ×ישית + error_can_not_delete_tracker: ×§×™×™×ž×™× × ×•×©××™× ×‘×¡×™×•×•×’ ×–×”, ×•×œ× × ×™×ª×Ÿ למחוק ×ותו. + error_can_not_remove_role: תפקיד ×–×” × ×ž×¦× ×‘×©×™×ž×•×©, ×•×œ× × ×™×ª×Ÿ למחוק ×ותו. + error_can_not_reopen_issue_on_closed_version: ×œ× × ×™×ª×Ÿ לפתוח מחדש × ×•×©× ×©×ž×©×•×™×š לגירסה סגורה + error_can_not_archive_project: ×œ× × ×™×ª×Ÿ ל×רכב פרויקט ×–×” + error_issue_done_ratios_not_updated: ×חוז התקדמות ×œ× ×•×©× ×œ× ×¢×•×“×›×Ÿ. + error_workflow_copy_source: × × ×‘×—×¨ סיווג ×ו תפקיד מקור + error_workflow_copy_target: × × ×‘×—×¨ תפקיד(×™×) וסיווג(×™×) + error_unable_delete_issue_status: ×œ× × ×™×ª×Ÿ למחוק מצב × ×•×©× + error_unable_to_connect: ×œ× × ×™×ª×Ÿ להתחבר (%{value}) + warning_attachments_not_saved: "כשלון בשמירת %{count} קבצי×." + + mail_subject_lost_password: "סיסמת ×”Ö¾%{value} שלך" + mail_body_lost_password: 'לשינו סיסמת ×”Ö¾Redmine שלך, לחץ על הקישור הב×:' + mail_subject_register: "הפעלת חשבון %{value}" + mail_body_register: 'להפעלת חשבון ×”Ö¾Redmine שלך, לחץ על הקישור הב×:' + mail_body_account_information_external: "×תה יכול להשתמש בחשבון %{value} כדי להתחבר" + mail_body_account_information: פרטי החשבון שלך + mail_subject_account_activation_request: "בקשת הפעלה לחשבון %{value}" + mail_body_account_activation_request: "משתמש חדש (%{value}) נרש×. החשבון שלו מחכה ל×ישור שלך:" + mail_subject_reminder: "%{count} נוש××™× ×ž×™×•×¢×“×™× ×œ×”×’×©×” ×‘×™×ž×™× ×”×§×¨×•×‘×™× (%{days})" + mail_body_reminder: "%{count} נוש××™× ×©×ž×™×•×¢×“×™× ×ליך ×ž×™×•×¢×“×™× ×œ×”×’×©×” בתוך %{days} ימי×:" + mail_subject_wiki_content_added: "דף ×”Ö¾wiki â€'%{id}' נוסף" + mail_body_wiki_content_added: דף ×”Ö¾wiki â€'%{id}' נוסף ×¢"×™ %{author}. + mail_subject_wiki_content_updated: "דף ×”Ö¾wiki â€'%{id}' עודכן" + mail_body_wiki_content_updated: דף ×”Ö¾wiki â€'%{id}' עודכן ×¢"×™ %{author}. + + gui_validation_error: שגי××” 1 + gui_validation_error_plural: "%{count} שגי×ות" + + field_name: ×©× + field_description: תי×ור + field_summary: תקציר + field_is_required: נדרש + field_firstname: ×©× ×¤×¨×˜×™ + field_lastname: ×©× ×ž×©×¤×—×” + field_mail: דו×"ל + field_filename: קובץ + field_filesize: גודל + field_downloads: הורדות + field_author: כותב + field_created_on: נוצר + field_updated_on: עודכן + field_field_format: פורמט + field_is_for_all: לכל ×”×¤×¨×•×™×§×˜×™× + field_possible_values: ×¢×¨×›×™× ××¤×©×¨×™×™× + field_regexp: ביטוי רגיל + field_min_length: ×ורך מינימ×לי + field_max_length: ×ורך מקסימ×לי + field_value: ערך + field_category: קטגוריה + field_title: כותרת + field_project: פרויקט + field_issue: × ×•×©× + field_status: מצב + field_notes: הערות + field_is_closed: × ×•×©× ×¡×’×•×¨ + field_is_default: ערך ברירת מחדל + field_tracker: סיווג + field_subject: ×©× × ×•×©× + field_due_date: ת×ריך ×¡×™×•× + field_assigned_to: ×חר××™ + field_priority: עדיפות + field_fixed_version: גירסת יעד + field_user: מתשמש + field_principal: מנהל + field_role: תפקיד + field_homepage: דף הבית + field_is_public: פומבי + field_parent: תת פרויקט של + field_is_in_roadmap: נוש××™× ×”×ž×•×¦×’×™× ×‘×ž×¤×ª ×”×“×¨×›×™× + field_login: ×©× ×ž×©×ª×ž×© + field_mail_notification: הודעות דו×"ל + field_admin: ניהול + field_last_login_on: התחברות ×חרונה + field_language: שפה + field_effective_date: ת×ריך + field_password: סיסמה + field_new_password: סיסמה חדשה + field_password_confirmation: ×ישור + field_version: גירסה + field_type: סוג + field_host: שרת + field_port: פורט + field_account: חשבון + field_base_dn: בסיס DN + field_attr_login: תכונת התחברות + field_attr_firstname: תכונת ×©× ×¤×¨×˜×™× + field_attr_lastname: תכונת ×©× ×ž×©×¤×—×” + field_attr_mail: תכונת דו×"ל + field_onthefly: יצירת ×ž×©×ª×ž×©×™× ×–×¨×™×–×” + field_start_date: ת×ריך התחלה + field_done_ratio: "% גמור" + field_auth_source: מקור הזדהות + field_hide_mail: ×”×—×‘× ×ת כתובת הדו×"ל שלי + field_comments: הערות + field_url: URL + field_start_page: דף התחלתי + field_subproject: תת־פרויקט + field_hours: שעות + field_activity: פעילות + field_spent_on: ת×ריך + field_identifier: מזהה + field_is_filter: משמש כמסנן + field_issue_to: נוש××™× ×§×©×•×¨×™× + field_delay: עיקוב + field_assignable: ניתן להקצות נוש××™× ×œ×ª×¤×§×™×“ ×–×” + field_redirect_existing_links: העבר ×§×™×©×•×¨×™× ×§×™×™×ž×™× + field_estimated_hours: זמן משוער + field_column_names: עמודות + field_time_entries: ×¨×™×©×•× ×–×ž× ×™× + field_time_zone: ×יזור זמן + field_searchable: ניתן לחיפוש + field_default_value: ערך ברירת מחדל + field_comments_sorting: הצג הערות + field_parent_title: דף ×ב + field_editable: ניתן לעריכה + field_watcher: צופה + field_identity_url: כתובת OpenID + field_content: תוכן + field_group_by: קבץ ×ת התוצ×ות לפי + field_sharing: שיתוף + field_parent_issue: משימת ×ב + field_text: שדה טקסט + + setting_app_title: כותרת ×™×©×•× + setting_app_subtitle: תת־כותרת ×™×©×•× + setting_welcome_text: טקסט "ברוך הב×" + setting_default_language: שפת ברירת מחדל + setting_login_required: דרושה הזדהות + setting_self_registration: ×פשר הרשמה עצמית + setting_attachment_max_size: גודל דבוקה מקסימ×לי + setting_issues_export_limit: גבול ×™×¦×•× × ×•×©××™× + setting_mail_from: כתובת שליחת דו×"ל + setting_bcc_recipients: מוסתר (bcc) + setting_plain_text_mail: טקסט פשוט בלבד (×œ×œ× HTML) + setting_host_name: ×©× ×©×¨×ª + setting_text_formatting: עיצוב טקסט + setting_wiki_compression: כיווץ היסטורית wiki + setting_feeds_limit: גבול תוכן הזנות + setting_default_projects_public: ×¤×¨×•×™×§×˜×™× ×—×“×©×™× ×”×™× × ×¤×•×ž×‘×™×™× ×›×‘×¨×™×¨×ª מחדל + setting_autofetch_changesets: משיכה ×וטומטית של ×©×™× ×•×™×™× + setting_sys_api_enabled: ×פשר שירות רשת לניהול המ×גר + setting_commit_ref_keywords: מילות מפתח מקשרות + setting_commit_fix_keywords: מילות מפתח מתקנות + setting_autologin: התחברות ×וטומטית + setting_date_format: פורמט ת×ריך + setting_time_format: פורמט זמן + setting_cross_project_issue_relations: הרשה קישור נוש××™× ×‘×™×Ÿ ×¤×¨×•×™×§×˜×™× + setting_issue_list_default_columns: עמודות ברירת מחדל המוצגות ברשימת הנוש××™× + setting_emails_footer: תחתית דו×"ל + setting_protocol: פרוטוקול + setting_per_page_options: ×פשרויות ××•×‘×™×§×˜×™× ×œ×¤×™ דף + setting_user_format: פורמט הצגת ×ž×©×ª×ž×©×™× + setting_activity_days_default: ×™×ž×™× ×”×ž×•×¦×’×™× ×¢×œ פעילות הפרויקט + setting_display_subprojects_issues: הצג נוש××™× ×©×œ ×ª×ª×™Ö¾×¤×¨×•×™×§×˜×™× ×›×‘×¨×™×¨×ª מחדל + setting_enabled_scm: ×פשר ניהול תצורה + setting_mail_handler_body_delimiters: חתוך כתובות דו×ר ×חרי ×חת משורות ×לה + setting_mail_handler_api_enabled: ×פשר שירות רשת לדו×ר נכנס + setting_mail_handler_api_key: מפתח API + setting_sequential_project_identifiers: השתמש ×‘×ž×¡×¤×¨×™× ×¢×•×§×‘×™× ×œ×ž×–×”×™ פרויקט + setting_gravatar_enabled: שימוש בצלמיות ×ž×©×ª×ž×©×™× ×žÖ¾Gravatar + setting_gravatar_default: תמונת Gravatar ברירת מחדל + setting_diff_max_lines_displayed: מספר מירבי של שורות בתצוגת ×©×™× ×•×™×™× + setting_file_max_size_displayed: גודל מירבי של מלל המוצג בתוך השורה + setting_repository_log_display_limit: מספר מירבי של מהדורות המוצגות ביומן קובץ + setting_openid: ×פשר התחברות ×•×¨×™×©×•× ×‘×מצעות OpenID + setting_password_min_length: ×ורך סיסמה מינימ×לי + setting_new_project_user_role_id: התפקיד שמוגדר למשתמש פשוט ×שר יוצר פרויקט + setting_default_projects_modules: ×ž×•×“×•×œ×™× ×ž××•×¤×©×¨×™× ×‘×‘×¨×™×¨×ª מחדל עבור ×¤×¨×•×™×§×˜×™× ×—×“×©×™× + setting_issue_done_ratio: חשב ×חוז התקדמות ×‘× ×•×©× ×¢× + setting_issue_done_ratio_issue_field: השתמש בשדה ×”× ×•×©× + setting_issue_done_ratio_issue_status: השתמש במצב ×”× ×•×©× + setting_start_of_week: השבוע מתחיל ×‘×™×•× + setting_rest_api_enabled: ×פשר שירות רשת REST + setting_cache_formatted_text: שמור טקסט מעוצב במטמון + setting_default_notification_option: ×פשרות התר××” ברירת־מחדל + + permission_add_project: יצירת פרויקט + permission_add_subprojects: יצירת תתי־פרויקט + permission_edit_project: עריכת פרויקט + permission_select_project_modules: בחירת מודולי פרויקט + permission_manage_members: ניהול ×—×‘×¨×™× + permission_manage_project_activities: נהל פעילויות פרויקט + permission_manage_versions: ניהול גירס×ות + permission_manage_categories: ניהול קטגוריות נוש××™× + permission_view_issues: צפיה בנוש××™× + permission_add_issues: הוספת × ×•×©× + permission_edit_issues: עריכת נוש××™× + permission_manage_issue_relations: ניהול ×§×©×¨×™× ×‘×™×Ÿ נוש××™× + permission_add_issue_notes: הוספת הערות לנוש××™× + permission_edit_issue_notes: עריכת רשימות + permission_edit_own_issue_notes: עריכת הערות של עצמו + permission_move_issues: הזזת נוש××™× + permission_delete_issues: מחיקת נוש××™× + permission_manage_public_queries: ניהול ש×ילתות פומביות + permission_save_queries: שמירת ש×ילתות + permission_view_gantt: צפיה בג×נט + permission_view_calendar: צפיה בלוח השנה + permission_view_issue_watchers: צפיה ברשימת ×¦×•×¤×™× + permission_add_issue_watchers: הוספת ×¦×•×¤×™× + permission_delete_issue_watchers: הסרת ×¦×•×¤×™× + permission_log_time: תיעוד זמן שהושקע + permission_view_time_entries: צפיה ×‘×¨×™×©×•× ×–×ž× ×™× + permission_edit_time_entries: עריכת ×¨×™×©×•× ×–×ž× ×™× + permission_edit_own_time_entries: עריכת ×¨×™×©×•× ×”×–×ž× ×™× ×©×œ עצמו + permission_manage_news: ניהול חדשות + permission_comment_news: תגובה לחדשות + permission_manage_documents: ניהול ×ž×¡×ž×›×™× + permission_view_documents: צפיה ×‘×ž×¡×ž×›×™× + permission_manage_files: ניהול ×§×‘×¦×™× + permission_view_files: צפיה ×‘×§×‘×¦×™× + permission_manage_wiki: ניהול wiki + permission_rename_wiki_pages: שינוי ×©× ×©×œ דפי wiki + permission_delete_wiki_pages: מחיקת דפי wiki + permission_view_wiki_pages: צפיה ב־wiki + permission_view_wiki_edits: צפיה בהיסטורית wiki + permission_edit_wiki_pages: עריכת דפי wiki + permission_delete_wiki_pages_attachments: מחיקת דבוקות + permission_protect_wiki_pages: ×”×’× ×” על כל דפי wiki + permission_manage_repository: ניהול מ×גר + permission_browse_repository: סיור במ×גר + permission_view_changesets: צפיה בסדרות ×©×™× ×•×™×™× + permission_commit_access: ×ישור הפקדות + permission_manage_boards: ניהול לוחות + permission_view_messages: צפיה בהודעות + permission_add_messages: הצבת הודעות + permission_edit_messages: עריכת הודעות + permission_edit_own_messages: עריכת הודעות של עצמו + permission_delete_messages: מחיקת הודעות + permission_delete_own_messages: מחיקת הודעות של עצמו + permission_export_wiki_pages: ×™×¦× ×“×¤×™ wiki + permission_manage_subtasks: נהל תתי־משימות + + project_module_issue_tracking: מעקב נוש××™× + project_module_time_tracking: מעקב ×חר ×–×ž× ×™× + project_module_news: חדשות + project_module_documents: ×ž×¡×ž×›×™× + project_module_files: ×§×‘×¦×™× + project_module_wiki: Wiki + project_module_repository: מ×גר + project_module_boards: לוחות + project_module_calendar: לוח שנה + project_module_gantt: ×’×נט + + label_user: משתמש + label_user_plural: ×ž×©×ª×ž×©×™× + label_user_new: משתמש חדש + label_user_anonymous: ×למוני + label_project: פרויקט + label_project_new: פרויקט חדש + label_project_plural: ×¤×¨×•×™×§×˜×™× + label_x_projects: + zero: ×œ×œ× ×¤×¨×•×™×§×˜×™× + one: פרויקט ×חד + other: "%{count} פרויקטי×" + label_project_all: כל ×”×¤×¨×•×™×§×˜×™× + label_project_latest: ×”×¤×¨×•×™×§×˜×™× ×”×—×“×©×™× ×‘×™×•×ª×¨ + label_issue: × ×•×©× + label_issue_new: × ×•×©× ×—×“×© + label_issue_plural: נוש××™× + label_issue_view_all: צפה בכל הנוש××™× + label_issues_by: "נוש××™× ×œ×¤×™ %{value}" + label_issue_added: × ×•×©× × ×•×¡×£ + label_issue_updated: × ×•×©× ×¢×•×“×›×Ÿ + label_document: מסמך + label_document_new: מסמך חדש + label_document_plural: ×ž×¡×ž×›×™× + label_document_added: מסמך נוסף + label_role: תפקיד + label_role_plural: ×ª×¤×§×™×“×™× + label_role_new: תפקיד חדש + label_role_and_permissions: ×ª×¤×§×™×“×™× ×•×”×¨×©×ות + label_member: חבר + label_member_new: חבר חדש + label_member_plural: ×—×‘×¨×™× + label_tracker: סיווג + label_tracker_plural: ×¡×™×•×•×’×™× + label_tracker_new: סיווג חדש + label_workflow: זרימת עבודה + label_issue_status: מצב × ×•×©× + label_issue_status_plural: מצבי × ×•×©× + label_issue_status_new: מצב חדש + label_issue_category: קטגורית × ×•×©× + label_issue_category_plural: קטגוריות × ×•×©× + label_issue_category_new: קטגוריה חדשה + label_custom_field: שדה ×ישי + label_custom_field_plural: שדות ××™×©×™×™× + label_custom_field_new: שדה ×ישי חדש + label_enumerations: ×ינומרציות + label_enumeration_new: ערך חדש + label_information: מידע + label_information_plural: מידע + label_please_login: × × ×”×ª×—×‘×¨ + label_register: הרשמה + label_login_with_open_id_option: ×ו התחבר ב×מצעות OpenID + label_password_lost: ×בדה הסיסמה? + label_home: דף הבית + label_my_page: הדף שלי + label_my_account: החשבון שלי + label_my_projects: ×”×¤×¨×•×™×§×˜×™× ×©×œ×™ + label_my_page_block: בלוק הדף שלי + label_administration: ניהול + label_login: התחבר + label_logout: התנתק + label_help: עזרה + label_reported_issues: נוש××™× ×©×“×•×•×—×• + label_assigned_to_me_issues: נוש××™× ×©×”×•×¦×‘×• לי + label_last_login: התחברות ×חרונה + label_registered_on: × ×¨×©× ×‘×ª×ריך + label_activity: פעילות + label_overall_activity: פעילות כוללת + label_user_activity: "הפעילות של %{value}" + label_new: חדש + label_logged_as: מחובר ×› + label_environment: סביבה + label_authentication: הזדהות + label_auth_source: מקור הזדהות + label_auth_source_new: מקור הזדהות חדש + label_auth_source_plural: מקורות הזדהות + label_subproject_plural: ×ª×ªÖ¾×¤×¨×•×™×§×˜×™× + label_subproject_new: תת־פרויקט חדש + label_and_its_subprojects: "%{value} וכל ×ª×ª×™Ö¾×”×¤×¨×•×™×§×˜×™× ×©×œ×•" + label_min_max_length: ×ורך מינימ×לי - מקסימ×לי + label_list: רשימה + label_date: ת×ריך + label_integer: מספר ×©×œ× + label_float: צף + label_boolean: ערך בולי×× ×™ + label_string: טקסט + label_text: טקסט ×רוך + label_attribute: תכונה + label_attribute_plural: תכונות + label_download: "הורדה %{count}" + label_download_plural: "%{count} הורדות" + label_no_data: ×ין מידע להציג + label_change_status: שנה מצב + label_history: היסטוריה + label_attachment: קובץ + label_attachment_new: קובץ חדש + label_attachment_delete: מחק קובץ + label_attachment_plural: ×§×‘×¦×™× + label_file_added: קובץ נוסף + label_report: דו"×— + label_report_plural: דו"חות + label_news: חדשות + label_news_new: הוסף חדשות + label_news_plural: חדשות + label_news_latest: חדשות ×חרונות + label_news_view_all: צפה בכל החדשות + label_news_added: חדשות נוספו + label_settings: הגדרות + label_overview: מבט רחב + label_version: גירסה + label_version_new: גירסה חדשה + label_version_plural: גירס×ות + label_close_versions: סגור גירס×ות שהושלמו + label_confirmation: ×ישור + label_export_to: ×™×¦× ×œ + label_read: קר×... + label_public_projects: ×¤×¨×•×™×§×˜×™× ×¤×•×ž×‘×™×™× + label_open_issues: פתוח + label_open_issues_plural: ×¤×ª×•×—×™× + label_closed_issues: סגור + label_closed_issues_plural: ×¡×’×•×¨×™× + label_x_open_issues_abbr_on_total: + zero: 0 ×¤×ª×•×—×™× / %{total} + one: 1 פתוח / %{total} + other: "%{count} ×¤×ª×•×—×™× / %{total}" + label_x_open_issues_abbr: + zero: 0 ×¤×ª×•×—×™× + one: 1 פתוח + other: "%{count} פתוחי×" + label_x_closed_issues_abbr: + zero: 0 ×¡×’×•×¨×™× + one: 1 סגור + other: "%{count} סגורי×" + label_total: סה"×› + label_permissions: הרש×ות + label_current_status: מצב נוכחי + label_new_statuses_allowed: ×ž×¦×‘×™× ×—×“×©×™× ××¤×©×¨×™×™× + label_all: הכל + label_none: ×›×œ×•× + label_nobody: ××£ ×חד + label_next: ×”×‘× + label_previous: ×”×§×•×“× + label_used_by: בשימוש ×¢"×™ + label_details: ×¤×¨×˜×™× + label_add_note: הוסף הערה + label_per_page: לכל דף + label_calendar: לוח שנה + label_months_from: ×—×•×“×©×™× ×ž + label_gantt: ×’×נט + label_internal: פנימי + label_last_changes: "%{count} ×©×™× ×•×™× ×חרוני×" + label_change_view_all: צפה בכל ×”×©×™× ×•×™× + label_personalize_page: הת×× ×ישית דף ×–×” + label_comment: תגובה + label_comment_plural: תגובות + label_x_comments: + zero: ×ין הערות + one: הערה ×חת + other: "%{count} הערות" + label_comment_add: הוסף תגובה + label_comment_added: תגובה נוספה + label_comment_delete: מחק תגובות + label_query: ש×ילתה ×ישית + label_query_plural: ש×ילתות ×ישיות + label_query_new: ש×ילתה חדשה + label_filter_add: הוסף מסנן + label_filter_plural: ×ž×¡× × ×™× + label_equals: ×”×•× + label_not_equals: ×”×•× ×œ× + label_in_less_than: בפחות מ + label_in_more_than: ביותר מ + label_greater_or_equal: ">=" + label_less_or_equal: <= + label_in: ב + label_today: ×”×™×•× + label_all_time: תמיד + label_yesterday: ×תמול + label_this_week: השבוע + label_last_week: השבוע שעבר + label_last_n_days: "ב־%{count} ×™×ž×™× ×חרוני×" + label_this_month: החודש + label_last_month: חודש שעבר + label_this_year: השנה + label_date_range: טווח ת××¨×™×›×™× + label_less_than_ago: פחות מ + label_more_than_ago: יותר מ + label_ago: לפני + label_contains: מכיל + label_not_contains: ×œ× ×ž×›×™×œ + label_day_plural: ×™×ž×™× + label_repository: מ×גר + label_repository_plural: מ××’×¨×™× + label_browse: סייר + label_modification: "שינוי %{count}" + label_modification_plural: "%{count} שינויי×" + label_branch: ×¢× ×£ + label_tag: סימון + label_revision: מהדורה + label_revision_plural: מהדורות + label_revision_id: מהדורה %{value} + label_associated_revisions: מהדורות קשורות + label_added: נוסף + label_modified: שונה + label_copied: הועתק + label_renamed: ×”×©× ×©×•× ×” + label_deleted: נמחק + label_latest_revision: מהדורה ×חרונה + label_latest_revision_plural: מהדורות ×חרונות + label_view_revisions: צפה במהדורות + label_view_all_revisions: צפה בכל המהדורות + label_max_size: גודל מקסימ×לי + label_sort_highest: ×”×–×– לר×שית + label_sort_higher: ×”×–×– למעלה + label_sort_lower: ×”×–×– למטה + label_sort_lowest: ×”×–×– לתחתית + label_roadmap: מפת ×”×“×¨×›×™× + label_roadmap_due_in: "נגמר בעוד %{value}" + label_roadmap_overdue: "%{value} מ×חר" + label_roadmap_no_issues: ×ין נוש××™× ×œ×’×™×¨×¡×” זו + label_search: חפש + label_result_plural: תוצ×ות + label_all_words: כל ×”×ž×™×œ×™× + label_wiki: Wiki + label_wiki_edit: ערוך wiki + label_wiki_edit_plural: עריכות wiki + label_wiki_page: דף Wiki + label_wiki_page_plural: דפי wiki + label_index_by_title: סדר על פי כותרת + label_index_by_date: סדר על פי ת×ריך + label_current_version: גירסה נוכחית + label_preview: תצוגה מקדימה + label_feed_plural: הזנות + label_changes_details: פירוט כל ×”×©×™× ×•×™×™× + label_issue_tracking: מעקב ×חר נוש××™× + label_spent_time: זמן שהושקע + label_overall_spent_time: זמן שהושקע סה"×› + label_f_hour: "%{value} שעה" + label_f_hour_plural: "%{value} שעות" + label_time_tracking: מעקב ×–×ž× ×™× + label_change_plural: ×©×™× ×•×™×™× + label_statistics: סטטיסטיקות + label_commits_per_month: הפקדות לפי חודש + label_commits_per_author: הפקדות לפי כותב + label_view_diff: צפה ×‘×©×™× ×•×™×™× + label_diff_inline: בתוך השורה + label_diff_side_by_side: צד לצד + label_options: ×פשרויות + label_copy_workflow_from: העתק זירמת עבודה מ + label_permissions_report: דו"×— הרש×ות + label_watched_issues: נוש××™× ×©× ×¦×¤×• + label_related_issues: נוש××™× ×§×©×•×¨×™× + label_applied_status: מצב מוחל + label_loading: טוען... + label_relation_new: קשר חדש + label_relation_delete: מחק קשר + label_relates_to: קשור ל + label_duplicates: מכפיל ×ת + label_duplicated_by: שוכפל ×¢"×™ + label_blocks: ×—×•×¡× ×ת + label_blocked_by: ×—×¡×•× ×¢"×™ + label_precedes: ×ž×§×“×™× ×ת + label_follows: עוקב ×חרי + label_end_to_start: מהתחלה לסוף + label_end_to_end: מהסוף לסוף + label_start_to_start: מהתחלה להתחלה + label_start_to_end: מהתחלה לסוף + label_stay_logged_in: הש×ר מחובר + label_disabled: מבוטל + label_show_completed_versions: הצג גירס×ות גמורות + label_me: ×× ×™ + label_board: ×¤×•×¨×•× + label_board_new: ×¤×•×¨×•× ×—×“×© + label_board_plural: ×¤×•×¨×•×ž×™× + label_board_locked: נעול + label_board_sticky: דביק + label_topic_plural: נוש××™× + label_message_plural: הודעות + label_message_last: הודעה ×חרונה + label_message_new: הודעה חדשה + label_message_posted: הודעה נוספה + label_reply_plural: השבות + label_send_information: שלח מידע על חשבון למשתמש + label_year: שנה + label_month: חודש + label_week: שבוע + label_date_from: מת×ריך + label_date_to: עד + label_language_based: מבוסס שפה + label_sort_by: "מיין לפי %{value}" + label_send_test_email: שלח דו×"ל בדיקה + label_feeds_access_key: מפתח גישה ל־RSS + label_missing_feeds_access_key: חסר מפתח גישה ל־RSS + label_feeds_access_key_created_on: "מפתח הזנת RSS נוצר לפני%{value}" + label_module_plural: ×ž×•×“×•×œ×™× + label_added_time_by: 'נוסף ×¢"×™ %{author} לפני %{age}' + label_updated_time_by: 'עודכן ×¢"×™ %{author} לפני %{age}' + label_updated_time: "עודכן לפני %{value} " + label_jump_to_a_project: קפוץ לפרויקט... + label_file_plural: ×§×‘×¦×™× + label_changeset_plural: סדרות ×©×™× ×•×™×™× + label_default_columns: עמודת ברירת מחדל + label_no_change_option: (×ין שינוי×) + label_bulk_edit_selected_issues: ערוך ×ת הנוש××™× ×”×ž×¡×•×ž× ×™× + label_theme: ערכת × ×•×©× + label_default: ברירת מחדל + label_search_titles_only: חפש בכותרות בלבד + label_user_mail_option_all: "לכל ×ירוע בכל ×”×¤×¨×•×™×§×˜×™× ×©×œ×™" + label_user_mail_option_selected: "לכל ×ירוע ×‘×¤×¨×•×™×§×˜×™× ×©×‘×—×¨×ª×™ בלבד..." + label_user_mail_option_only_my_events: עבור ×“×‘×¨×™× ×©×× ×™ צופה ×ו מעורב ×‘×”× ×‘×œ×‘×“ + label_user_mail_option_only_assigned: עבור ×“×‘×¨×™× ×©×× ×™ ×חר××™ ×¢×œ×™×”× ×‘×œ×‘×“ + label_user_mail_option_only_owner: עבור ×“×‘×¨×™× ×©×× ×™ ×”×‘×¢×œ×™× ×©×œ×”× ×‘×œ×‘×“ + label_user_mail_no_self_notified: "×× ×™ ×œ× ×¨×•×¦×” שיודיעו לי על ×©×™× ×•×™×™× ×©×× ×™ מבצע" + label_registration_activation_by_email: הפעל חשבון ב×מצעות דו×"ל + label_registration_manual_activation: הפעלת חשבון ידנית + label_registration_automatic_activation: הפעלת חשבון ×וטומטית + label_display_per_page: "בכל דף: %{value} תוצ×ות" + label_age: גיל + label_change_properties: שנה מ××¤×™×™× ×™× + label_general: כללי + label_more: עוד + label_scm: מערכת ניהול תצורה + label_plugins: ×ª×•×¡×¤×™× + label_ldap_authentication: הזדהות LDAP + label_downloads_abbr: D/L + label_optional_description: תי×ור רשות + label_add_another_file: הוסף עוד קובץ + label_preferences: העדפות + label_chronological_order: בסדר כרונולוגי + label_reverse_chronological_order: בסדר כרונולוגי הפוך + label_planning: תכנון + label_incoming_emails: דו×"ל נכנס + label_generate_key: צור מפתח + label_issue_watchers: ×¦×•×¤×™× + label_example: ×“×•×’×ž× + label_display: תצוגה + label_sort: מיון + label_ascending: בסדר עולה + label_descending: בסדר יורד + label_date_from_to: 'מת×ריך %{start} ועד ת×ריך %{end}' + label_wiki_content_added: נוסף דף ל־wiki + label_wiki_content_updated: דף wiki עודכן + label_group: קבוצה + label_group_plural: קבוצות + label_group_new: קבוצה חדשה + label_time_entry_plural: זמן שהושקע + label_version_sharing_none: ×œ× ×ž×©×•×ª×£ + label_version_sharing_descendants: ×¢× ×¤×¨×•×™×§×˜×™× ×‘× ×™× + label_version_sharing_hierarchy: ×¢× ×”×™×¨×¨×›×™×ª ×”×¤×¨×•×™×§×˜×™× + label_version_sharing_tree: ×¢× ×¢×¥ הפרויקט + label_version_sharing_system: ×¢× ×›×œ ×”×¤×¨×•×™×§×˜×™× + label_update_issue_done_ratios: עדכן ×חוז התקדמות ×œ× ×•×©× + label_copy_source: מקור + label_copy_target: יעד + label_copy_same_as_target: ×–×”×” ליעד + label_display_used_statuses_only: הצג רק ×ת ×”×ž×¦×‘×™× ×‘×©×™×ž×•×© לסיווג ×–×” + label_api_access_key: מפתח גישה ל־API + label_missing_api_access_key: חסר מפתח גישה ל־API + label_api_access_key_created_on: 'מפתח גישה ל־API נוצר לפני %{value}' + label_profile: פרופיל + label_subtask_plural: תתי־משימות + label_project_copy_notifications: שלח התר×ות דו×ר במהלך העתקת הפרויקט + + button_login: התחבר + button_submit: ×שר + button_save: שמור + button_check_all: בחר הכל + button_uncheck_all: בחר ×›×œ×•× + button_delete: מחק + button_create: צור + button_create_and_continue: צור ופתח חדש + button_test: בדוק + button_edit: ערוך + button_edit_associated_wikipage: "ערוך דף wiki מקושר: %{page_title}" + button_add: הוסף + button_change: שנה + button_apply: החל + button_clear: × ×§×” + button_lock: נעל + button_unlock: בטל נעילה + button_download: הורד + button_list: רשימה + button_view: צפה + button_move: ×”×–×– + button_move_and_follow: העבר ועקוב + button_back: ×”×§×•×“× + button_cancel: בטל + button_activate: הפעל + button_sort: מיין + button_log_time: ×¨×™×©×•× ×–×ž× ×™× + button_rollback: חזור למהדורה זו + button_watch: צפה + button_unwatch: בטל צפיה + button_reply: השב + button_archive: ×רכיון + button_unarchive: ×”×•×¦× ×ž×”×רכיון + button_reset: ×פס + button_rename: שנה ×©× + button_change_password: שנה סיסמה + button_copy: העתק + button_copy_and_follow: העתק ועקוב + button_annotate: הוסף תי×ור מסגרת + button_update: עדכן + button_configure: ×פשרויות + button_quote: צטט + button_duplicate: שכפל + button_show: הצג + + status_active: פעיל + status_registered: ×¨×©×•× + status_locked: נעול + + version_status_open: פתוח + version_status_locked: נעול + version_status_closed: סגור + + field_active: פעיל + + text_select_mail_notifications: בחר פעולת שבגללן ישלח דו×"ל. + text_regexp_info: כגון. ^[A-Z0-9]+$ + text_min_max_length_info: 0 משמעו ×œ×œ× ×”×’×‘×œ×•×ª + text_project_destroy_confirmation: ×”×× ×תה בטוח שברצונך למחוק ×ת הפרויקט ו×ת כל המידע הקשור ×ליו? + text_subprojects_destroy_warning: "תת־הפרויקטי×: %{value} ימחקו ×’× ×›×Ÿ." + text_workflow_edit: בחר תפקיד וסיווג כדי לערוך ×ת זרימת העבודה + text_are_you_sure: ×”×× ×תה בטוח? + text_journal_changed: "%{label} השתנה מ%{old} ל%{new}" + text_journal_set_to: "%{label} נקבע ל%{value}" + text_journal_deleted: "%{label} נמחק (%{old})" + text_journal_added: "%{label} %{value} נוסף" + text_tip_issue_begin_day: מטלה המתחילה ×”×™×•× + text_tip_issue_end_day: מטלה המסתיימת ×”×™×•× + text_tip_issue_begin_end_day: מטלה המתחילה ומסתיימת ×”×™×•× + text_caracters_maximum: "×ž×§×¡×™×ž×•× %{count} תווי×." + text_caracters_minimum: "חייב להיות לפחות ב×ורך של %{count} תווי×." + text_length_between: "×ורך בין %{min} ל %{max} תווי×." + text_tracker_no_workflow: זרימת עבודה ×œ× ×”×•×’×“×¨×” עבור סיווג ×–×” + text_unallowed_characters: ×ª×•×•×™× ×œ× ×ž×•×¨×©×™× + text_comma_separated: הכנסת ×¢×¨×›×™× ×ž×¨×•×‘×™× ×ž×•×ª×¨×ª (×ž×•×¤×¨×“×™× ×‘×¤×¡×™×§×™×). + text_line_separated: ניתן להזין מספר ×¢×¨×›×™× (שורה ×חת לכל ערך). + text_issues_ref_in_commit_messages: קישור ×•×ª×™×§×•× × ×•×©××™× ×‘×”×•×“×¢×•×ª הפקדה + text_issue_added: "×”× ×•×©× %{id} דווח (בידי %{author})." + text_issue_updated: "×”× ×•×©× %{id} עודכן (בידי %{author})." + text_wiki_destroy_confirmation: ×”×× ×תה בטוח שברצונך למחוק ×ת ×”WIKI ×”×–×” ו×ת כל תוכנו? + text_issue_category_destroy_question: "כמה נוש××™× (%{count}) ×ž×•×¦×‘×™× ×œ×§×˜×’×•×¨×™×” הזו. מה ברצונך לעשות?" + text_issue_category_destroy_assignments: הסר הצבת קטגוריה + text_issue_category_reassign_to: הצב מחדש ×ת הקטגוריה לנוש××™× + text_user_mail_option: "×‘×¤×¨×•×™×§×˜×™× ×©×œ× ×‘×—×¨×ª, ×תה רק תקבל התרעות על ש×תה צופה ×ו קשור ××œ×™×”× (לדוגמ×:נוש××™× ×©×תה היוצר ×©×œ×”× ×ו ×חר××™ עליה×)." + text_no_configuration_data: "×œ× ×”×•×’×“×¨×” תצורה עבור תפקידי×, סיווגי×, מצבי × ×•×©× ×•×–×¨×™×ž×ª עבודה.\nמומלץ מ×ד לטעון ×ת תצורת ברירת המחדל. תוכל לשנותה מ×וחר יותר." + text_load_default_configuration: טען ×ת ×פשרויות ברירת המחדל + text_status_changed_by_changeset: "הוחל בסדרת ×”×©×™× ×•×™×™× %{value}." + text_issues_destroy_confirmation: '×”×× ×תה בטוח שברצונך למחוק ×ת הנוש××™×?' + text_select_project_modules: 'בחר ×ž×•×“×•×œ×™× ×œ×”×—×™×œ על פרויקט ×–×”:' + text_default_administrator_account_changed: מנהל המערכת ברירת המחדל שונה + text_file_repository_writable: מ×גר ×”×§×‘×¦×™× × ×™×ª×Ÿ לכתיבה + text_plugin_assets_writable: ספרית נכסי ×ª×•×¡×¤×™× × ×™×ª× ×ª לכתיבה + text_rmagick_available: RMagick זמין (רשות) + text_destroy_time_entries_question: "%{hours} שעות דווחו על הנוש××™× ×©×תה עומד למחוק. מה ברצונך לעשות?" + text_destroy_time_entries: מחק שעות שדווחו + text_assign_time_entries_to_project: הצב שעות שדווחו לפרויקט ×”×–×” + text_reassign_time_entries: 'הצב מחדש שעות שדווחו לפרויקט ×”×–×”:' + text_user_wrote: "%{value} כתב:" + text_enumeration_destroy_question: "%{count} ××•×‘×™×§×˜×™× ×ž×•×¦×‘×™× ×œ×¢×¨×š ×–×”." + text_enumeration_category_reassign_to: 'הצב מחדש לערך ×”×–×”:' + text_email_delivery_not_configured: '×œ× × ×§×‘×¢×” תצורה לשליחת דו×ר, וההתר×ות כבויות.\nקבע ×ת תצורת שרת ×”Ö¾SMTP בקובץ /etc/redmine/<instance>/configuration.yml והתחל ×ת ×”×פליקציה מחדש ×¢"מ ל×פשר ×ות×.' + text_repository_usernames_mapping: "בחר ×ו עדכן ×ת משתמש Redmine הממופה לכל ×©× ×ž×©×ª×ž×© ביומן המ×גר.\n×ž×©×ª×ž×©×™× ×‘×¢×œ×™ ×©× ×ו כתובת דו×ר ×–×”×” ב־Redmine ובמ×גר ×ž×ž×•×¤×™× ×‘×ופן ×וטומטי." + text_diff_truncated: '... ×”×©×™× ×•×™×™× ×¢×•×‘×¨×™× ×ת מספר השורות המירבי לתצוגה, ולכן ×”× ×§×•×¦×¦×•.' + text_custom_field_possible_values_info: שורה ×חת לכל ערך + text_wiki_page_destroy_question: לדף ×–×” יש %{descendants} ×“×¤×™× ×‘× ×™× ×•×ª×œ×•×™×™×. מה ברצונך לעשות? + text_wiki_page_nullify_children: הש×ר ×“×¤×™× ×‘× ×™× ×›×“×¤×™× ×¨××©×™×™× + text_wiki_page_destroy_children: מחק ×ת ×”×“×¤×™× ×”×‘× ×™× ×•×ת כל ×”×ª×œ×•×™×™× ×‘×”× + text_wiki_page_reassign_children: הצב מחדש ×“×¤×™× ×‘× ×™× ×œ×“×£ ×”×ב הנוכחי + text_own_membership_delete_confirmation: |- + בכוונתך למחוק חלק ×ו ×ת כל ההרש×ות שלך. ל×חר מכן ×œ× ×ª×•×›×œ יותר לערוך פרויקט ×–×”. + ×”×× ×תה בטוח שברצונך להמשיך? + text_zoom_in: התקרב + text_zoom_out: התרחק + + default_role_manager: מנהל + default_role_developer: מפתח + default_role_reporter: מדווח + default_tracker_bug: תקלה + default_tracker_feature: יכולת + default_tracker_support: תמיכה + default_issue_status_new: חדש + default_issue_status_in_progress: בעבודה + default_issue_status_resolved: נפתר + default_issue_status_feedback: משוב + default_issue_status_closed: סגור + default_issue_status_rejected: נדחה + default_doc_category_user: תיעוד משתמש + default_doc_category_tech: תיעוד טכני + default_priority_low: נמוכה + default_priority_normal: רגילה + default_priority_high: גבוהה + default_priority_urgent: דחופה + default_priority_immediate: מידית + default_activity_design: עיצוב + default_activity_development: פיתוח + + enumeration_issue_priorities: עדיפות נוש××™× + enumeration_doc_categories: קטגוריות ×ž×¡×ž×›×™× + enumeration_activities: פעילויות (מעקב ×חר זמני×) + enumeration_system_activity: פעילות מערכת + label_user_mail_option_none: No events + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + label_principal_search: "Search for user or group:" + label_user_search: "Search for user:" + field_visible: Visible + setting_emails_header: Emails header + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Enable time logging + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maximum number of items displayed on the gantt chart + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: Priority updated + label_issues_visibility_own: Issues created by or assigned to the user + field_issues_visibility: Issues visibility + label_issues_visibility_all: All issues + permission_set_own_issues_private: Set own issues public or private + field_is_private: Private + permission_set_issues_private: Set issues public or private + label_issues_visibility_public: All non private issues + text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). + field_commit_logs_encoding: קידוד הודעות הפקדה + field_scm_path_encoding: Path encoding + text_scm_path_encoding_note: "Default: UTF-8" + field_path_to_repository: Path to repository + field_root_directory: Root directory + field_cvs_module: Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Command + text_scm_command_version: Version + label_git_report_last_commit: Report last commit for files and directories + text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 × ×•×©× + one: 1 × ×•×©× + other: "%{count} נוש××™×" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: הכל + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: ×¢× ×¤×¨×•×™×§×˜×™× ×‘× ×™× + label_cross_project_tree: ×¢× ×¢×¥ הפרויקט + label_cross_project_hierarchy: ×¢× ×”×™×¨×¨×›×™×ª ×”×¤×¨×•×™×§×˜×™× + label_cross_project_system: ×¢× ×›×œ ×”×¤×¨×•×™×§×˜×™× + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d7/d7491fe30d43b3102b4c91e4fb373cefd28da79d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d7/d7491fe30d43b3102b4c91e4fb373cefd28da79d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,21 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module TrackersHelper +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d7/d76383ae6cf596872815e185d754067b3a322dd7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d7/d76383ae6cf596872815e185d754067b3a322dd7.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,45 @@ +
    +<%= link_to l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add' %> +
    + +

    <%=l(:label_project_plural)%>

    + +<%= form_tag({}, :method => :get) do %> +
    <%= l(:label_filter_plural) %> + +<%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> + +<%= text_field_tag 'name', params[:name], :size => 30 %> +<%= submit_tag l(:button_apply), :class => "small", :name => nil %> +<%= link_to l(:button_clear), {:controller => 'admin', :action => 'projects'}, :class => 'icon icon-reload' %> +
    +<% end %> +  + +
    + + + + + + + + +<% project_tree(@projects) do |project, level| %> + <%= project.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>"> + + + + + +<% end %> + +
    <%=l(:label_project)%><%=l(:field_is_public)%><%=l(:field_created_on)%>
    <%= link_to_project(project, {:action => (project.active? ? 'settings' : 'show')}, :title => project.short_description) %><%= checked_image project.is_public? %><%= format_date(project.created_on) %> + <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project, :status => params[:status] }, :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-lock') unless project.archived? %> + <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project, :status => params[:status] }, :method => :post, :class => 'icon icon-unlock') if project.archived? && (project.parent.nil? || !project.parent.archived?) %> + <%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %> + <%= link_to(l(:button_delete), project_path(project), :method => :delete, :class => 'icon icon-del') %> +
    +
    + +<% html_title(l(:label_project_plural)) -%> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d7/d7dec33f0cffbd1bfe34a5a4d9b65de67d349509.svn-base --- a/.svn/pristine/d7/d7dec33f0cffbd1bfe34a5a4d9b65de67d349509.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ -class Enumeration < ActiveRecord::Base - generator_for :name, :start => 'Enumeration0' - generator_for :type => 'TimeEntryActivity' - -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d8/d820b362da40aaa2b6531056312ceeddee3d097b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d8/d820b362da40aaa2b6531056312ceeddee3d097b.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,274 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class RepositoriesCvsControllerTest < ActionController::TestCase + tests RepositoriesController + + fixtures :projects, :users, :roles, :members, :member_roles, + :repositories, :enabled_modules + + REPOSITORY_PATH = Rails.root.join('tmp/test/cvs_repository').to_s + REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin? + # CVS module + MODULE_NAME = 'test' + PRJ_ID = 3 + NUM_REV = 7 + + def setup + Setting.default_language = 'en' + User.current = nil + + @project = Project.find(PRJ_ID) + @repository = Repository::Cvs.create(:project => Project.find(PRJ_ID), + :root_url => REPOSITORY_PATH, + :url => MODULE_NAME, + :log_encoding => 'UTF-8') + assert @repository + end + + if File.directory?(REPOSITORY_PATH) + def test_get_new + @request.session[:user_id] = 1 + @project.repository.destroy + get :new, :project_id => 'subproject1', :repository_scm => 'Cvs' + assert_response :success + assert_template 'new' + assert_kind_of Repository::Cvs, assigns(:repository) + assert assigns(:repository).new_record? + end + + def test_browse_root + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal 3, assigns(:entries).size + + entry = assigns(:entries).detect {|e| e.name == 'images'} + assert_equal 'dir', entry.kind + + entry = assigns(:entries).detect {|e| e.name == 'README'} + assert_equal 'file', entry.kind + + assert_not_nil assigns(:changesets) + assert assigns(:changesets).size > 0 + end + + def test_browse_directory + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param] + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['add.png', 'delete.png', 'edit.png'], assigns(:entries).collect(&:name) + entry = assigns(:entries).detect {|e| e.name == 'edit.png'} + assert_not_nil entry + assert_equal 'file', entry.kind + assert_equal 'images/edit.png', entry.path + end + + def test_browse_at_given_revision + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :show, :id => PRJ_ID, :path => repository_path_hash(['images'])[:param], + :rev => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name) + end + + def test_entry + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param] + assert_response :success + assert_template 'entry' + assert_no_tag :tag => 'td', + :attributes => { :class => /line-code/}, + :content => /before_filter/ + end + + def test_entry_at_given_revision + # changesets must be loaded + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param], + :rev => 2 + assert_response :success + assert_template 'entry' + # this line was removed in r3 + assert_tag :tag => 'td', + :attributes => { :class => /line-code/}, + :content => /before_filter/ + end + + def test_entry_not_found + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'zzz.c'])[:param] + assert_tag :tag => 'p', + :attributes => { :id => /errorExplanation/ }, + :content => /The entry or revision was not found in the repository/ + end + + def test_entry_download + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param], + :format => 'raw' + assert_response :success + end + + def test_directory_entry + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :entry, :id => PRJ_ID, + :path => repository_path_hash(['sources'])[:param] + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entry) + assert_equal 'sources', assigns(:entry).name + end + + def test_diff + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + ['inline', 'sbs'].each do |dt| + get :diff, :id => PRJ_ID, :rev => 3, :type => dt + assert_response :success + assert_template 'diff' + assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_out' }, + :content => /before_filter :require_login/ + assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_in' }, + :content => /with one change/ + end + end + + def test_diff_new_files + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + ['inline', 'sbs'].each do |dt| + get :diff, :id => PRJ_ID, :rev => 1, :type => dt + assert_response :success + assert_template 'diff' + assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_in' }, + :content => /watched.remove_watcher/ + assert_tag :tag => 'th', :attributes => { :class => 'filename' }, + :content => /test\/README/ + assert_tag :tag => 'th', :attributes => { :class => 'filename' }, + :content => /test\/images\/delete.png / + assert_tag :tag => 'th', :attributes => { :class => 'filename' }, + :content => /test\/images\/edit.png/ + assert_tag :tag => 'th', :attributes => { :class => 'filename' }, + :content => /test\/sources\/watchers_controller.rb/ + end + end + + def test_annotate + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + get :annotate, :id => PRJ_ID, + :path => repository_path_hash(['sources', 'watchers_controller.rb'])[:param] + assert_response :success + assert_template 'annotate' + + # 1.1 line + assert_select 'tr' do + assert_select 'th.line-num', :text => '21' + assert_select 'td.revision', :text => /1.1/ + assert_select 'td.author', :text => /LANG/ + end + # 1.2 line + assert_select 'tr' do + assert_select 'th.line-num', :text => '32' + assert_select 'td.revision', :text => /1.2/ + assert_select 'td.author', :text => /LANG/ + end + end + + def test_destroy_valid_repository + @request.session[:user_id] = 1 # admin + assert_equal 0, @repository.changesets.count + @repository.fetch_changesets + @project.reload + assert_equal NUM_REV, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + + def test_destroy_invalid_repository + @request.session[:user_id] = 1 # admin + @project.repository.destroy + @repository = Repository::Cvs.create!( + :project => Project.find(PRJ_ID), + :root_url => "/invalid", + :url => MODULE_NAME, + :log_encoding => 'UTF-8' + ) + @repository.fetch_changesets + @project.reload + assert_equal 0, @repository.changesets.count + + assert_difference 'Repository.count', -1 do + delete :destroy, :id => @repository.id + end + assert_response 302 + @project.reload + assert_nil @project.repository + end + else + puts "CVS test repository NOT FOUND. Skipping functional tests !!!" + def test_fake; assert true end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d8/d8240e0ef2bf417ed03d747eb93bd62ad19a2085.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d8/d8240e0ef2bf417ed03d747eb93bd62ad19a2085.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1087 @@ +# Update to 2.2 by Karel Picman +# Update to 1.1 by Michal Gebauer +# Updated by Josef LiÅ¡ka +# CZ translation by Maxim KruÅ¡ina | Massimo Filippi, s.r.o. | maxim@mxm.cz +# Based on original CZ translation by Jan KadleÄek +cs: + # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) + direction: ltr + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%Y-%m-%d" + short: "%b %d" + long: "%B %d, %Y" + + day_names: [NedÄ›le, PondÄ›lí, Úterý, StÅ™eda, ÄŒtvrtek, Pátek, Sobota] + abbr_day_names: [Ne, Po, Út, St, ÄŒt, Pá, So] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, Leden, Únor, BÅ™ezen, Duben, KvÄ›ten, ÄŒerven, ÄŒervenec, Srpen, Září, Říjen, Listopad, Prosinec] + abbr_month_names: [~, Led, Úno, BÅ™e, Dub, KvÄ›, ÄŒer, ÄŒec, Srp, Zář, Říj, Lis, Pro] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%a, %d %b %Y %H:%M:%S %z" + time: "%H:%M" + short: "%d %b %H:%M" + long: "%B %d, %Y %H:%M" + am: "dop." + pm: "odp." + + datetime: + distance_in_words: + half_a_minute: "půl minuty" + less_than_x_seconds: + one: "ménÄ› než sekunda" + other: "ménÄ› než %{count} sekund" + x_seconds: + one: "1 sekunda" + other: "%{count} sekund" + less_than_x_minutes: + one: "ménÄ› než minuta" + other: "ménÄ› než %{count} minut" + x_minutes: + one: "1 minuta" + other: "%{count} minut" + about_x_hours: + one: "asi 1 hodina" + other: "asi %{count} hodin" + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "1 den" + other: "%{count} dnů" + about_x_months: + one: "asi 1 mÄ›síc" + other: "asi %{count} mÄ›síců" + x_months: + one: "1 mÄ›síc" + other: "%{count} mÄ›síců" + about_x_years: + one: "asi 1 rok" + other: "asi %{count} let" + over_x_years: + one: "více než 1 rok" + other: "více než %{count} roky" + almost_x_years: + one: "témeÅ™ 1 rok" + other: "téměř %{count} roky" + + number: + # Výchozí formát pro Äísla + format: + separator: "." + delimiter: "" + precision: 3 + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Bajt" + other: "Bajtů" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + +# Used in array.to_sentence. + support: + array: + sentence_connector: "a" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 chyba zabránila uložení %{model}" + other: "%{count} chyb zabránilo uložení %{model}" + messages: + inclusion: "není zahrnuto v seznamu" + exclusion: "je rezervováno" + invalid: "je neplatné" + confirmation: "se neshoduje s potvrzením" + accepted: "musí být akceptováno" + empty: "nemůže být prázdný" + blank: "nemůže být prázdný" + too_long: "je příliÅ¡ dlouhý" + too_short: "je příliÅ¡ krátký" + wrong_length: "má chybnou délku" + taken: "je již použito" + not_a_number: "není Äíslo" + not_a_date: "není platné datum" + greater_than: "musí být vÄ›tší než %{count}" + greater_than_or_equal_to: "musí být vÄ›tší nebo rovno %{count}" + equal_to: "musí být pÅ™esnÄ› %{count}" + less_than: "musí být ménÄ› než %{count}" + less_than_or_equal_to: "musí být ménÄ› nebo rovno %{count}" + odd: "musí být liché" + even: "musí být sudé" + greater_than_start_date: "musí být vÄ›tší než poÄáteÄní datum" + not_same_project: "nepatří stejnému projektu" + circular_dependency: "Tento vztah by vytvoÅ™il cyklickou závislost" + cant_link_an_issue_with_a_descendant: "Úkol nemůže být spojen s jedním z jeho dílÄích úkolů" + + actionview_instancetag_blank_option: Prosím vyberte + + general_text_No: 'Ne' + general_text_Yes: 'Ano' + general_text_no: 'ne' + general_text_yes: 'ano' + general_lang_name: 'Czech (ÄŒeÅ¡tina)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: ÚÄet byl úspěšnÄ› zmÄ›nÄ›n. + notice_account_invalid_creditentials: Chybné jméno nebo heslo + notice_account_password_updated: Heslo bylo úspěšnÄ› zmÄ›nÄ›no. + notice_account_wrong_password: Chybné heslo + notice_account_register_done: ÚÄet byl úspěšnÄ› vytvoÅ™en. Pro aktivaci úÄtu kliknÄ›te na odkaz v emailu, který vám byl zaslán. + notice_account_unknown_email: Neznámý uživatel. + notice_can_t_change_password: Tento úÄet používá externí autentifikaci. Zde heslo zmÄ›nit nemůžete. + notice_account_lost_email_sent: Byl vám zaslán email s intrukcemi jak si nastavíte nové heslo. + notice_account_activated: Váš úÄet byl aktivován. Nyní se můžete pÅ™ihlásit. + notice_successful_create: ÚspěšnÄ› vytvoÅ™eno. + notice_successful_update: ÚspěšnÄ› aktualizováno. + notice_successful_delete: ÚspěšnÄ› odstranÄ›no. + notice_successful_connection: Úspěšné pÅ™ipojení. + notice_file_not_found: Stránka na kterou se snažíte zobrazit neexistuje nebo byla smazána. + notice_locking_conflict: Údaje byly zmÄ›nÄ›ny jiným uživatelem. + notice_not_authorized: Nemáte dostateÄná práva pro zobrazení této stránky. + notice_not_authorized_archived_project: Projekt ke kterému se snažíte pÅ™istupovat byl archivován. + notice_email_sent: "Na adresu %{value} byl odeslán email" + notice_email_error: "PÅ™i odesílání emailu nastala chyba (%{value})" + notice_feeds_access_key_reseted: Váš klÃ­Ä pro přístup k RSS byl resetován. + notice_api_access_key_reseted: Váš API přístupový klÃ­Ä byl resetován. + notice_failed_to_save_issues: "Chyba pÅ™i uložení %{count} úkolu(ů) z %{total} vybraných: %{ids}." + notice_failed_to_save_members: "NepodaÅ™ilo se uložit Älena(y): %{errors}." + notice_no_issue_selected: "Nebyl zvolen žádný úkol. Prosím, zvolte úkoly, které chcete editovat" + notice_account_pending: "Váš úÄet byl vytvoÅ™en, nyní Äeká na schválení administrátorem." + notice_default_data_loaded: Výchozí konfigurace úspěšnÄ› nahrána. + notice_unable_delete_version: Nemohu odstanit verzi + notice_unable_delete_time_entry: Nelze smazat Äas ze záznamu. + notice_issue_done_ratios_updated: Koeficienty dokonÄení úkolu byly aktualizovány. + notice_gantt_chart_truncated: Graf byl oříznut, poÄet položek pÅ™esáhl limit pro zobrazení (%{max}) + + error_can_t_load_default_data: "Výchozí konfigurace nebyla nahrána: %{value}" + error_scm_not_found: "Položka a/nebo revize neexistují v repozitáři." + error_scm_command_failed: "PÅ™i pokusu o přístup k repozitáři doÅ¡lo k chybÄ›: %{value}" + error_scm_annotate: "Položka neexistuje nebo nemůže být komentována." + error_issue_not_found_in_project: 'Úkol nebyl nalezen nebo nepatří k tomuto projektu' + error_no_tracker_in_project: Žádná fronta nebyla pÅ™iÅ™azena tomuto projektu. Prosím zkontroluje nastavení projektu. + error_no_default_issue_status: Není nastaven výchozí stav úkolu. Prosím zkontrolujte nastavení ("Administrace -> Stavy úkolů"). + error_can_not_delete_custom_field: Nelze smazat volitelné pole + error_can_not_delete_tracker: Tato fronta obsahuje úkoly a nemůže být smazán. + error_can_not_remove_role: Tato role je právÄ› používaná a nelze ji smazat. + error_can_not_reopen_issue_on_closed_version: Úkol pÅ™iÅ™azený k uzavÅ™ené verzi nemůže být znovu otevÅ™en + error_can_not_archive_project: Tento projekt nemůže být archivován + error_issue_done_ratios_not_updated: Koeficient dokonÄení úkolu nebyl aktualizován. + error_workflow_copy_source: Prosím vyberte zdrojovou frontu nebo roly + error_workflow_copy_target: Prosím vyberte cílovou frontu(y) a roly(e) + error_unable_delete_issue_status: Nelze smazat stavy úkolů + error_unable_to_connect: Nelze se pÅ™ipojit (%{value}) + warning_attachments_not_saved: "%{count} soubor(ů) nebylo možné uložit." + + mail_subject_lost_password: "VaÅ¡e heslo (%{value})" + mail_body_lost_password: 'Pro zmÄ›nu vaÅ¡eho hesla kliknÄ›te na následující odkaz:' + mail_subject_register: "Aktivace úÄtu (%{value})" + mail_body_register: 'Pro aktivaci vaÅ¡eho úÄtu kliknÄ›te na následující odkaz:' + mail_body_account_information_external: "Pomocí vaÅ¡eho úÄtu %{value} se můžete pÅ™ihlásit." + mail_body_account_information: Informace o vaÅ¡em úÄtu + mail_subject_account_activation_request: "Aktivace %{value} úÄtu" + mail_body_account_activation_request: "Byl zaregistrován nový uživatel %{value}. Aktivace jeho úÄtu závisí na vaÅ¡em potvrzení." + mail_subject_reminder: "%{count} úkol(ů) má termín bÄ›hem nÄ›kolik dní (%{days})" + mail_body_reminder: "%{count} úkol(ů), které máte pÅ™iÅ™azeny má termín bÄ›hem nÄ›kolik dní (%{days}):" + mail_subject_wiki_content_added: "'%{id}' Wiki stránka byla pÅ™idána" + mail_body_wiki_content_added: "'%{id}' Wiki stránka byla pÅ™idána od %{author}." + mail_subject_wiki_content_updated: "'%{id}' Wiki stránka byla aktualizována" + mail_body_wiki_content_updated: "'%{id}' Wiki stránka byla aktualizována od %{author}." + + gui_validation_error: 1 chyba + gui_validation_error_plural: "%{count} chyb(y)" + + field_name: Název + field_description: Popis + field_summary: PÅ™ehled + field_is_required: Povinné pole + field_firstname: Jméno + field_lastname: Příjmení + field_mail: Email + field_filename: Soubor + field_filesize: Velikost + field_downloads: Staženo + field_author: Autor + field_created_on: VytvoÅ™eno + field_updated_on: Aktualizováno + field_field_format: Formát + field_is_for_all: Pro vÅ¡echny projekty + field_possible_values: Možné hodnoty + field_regexp: Regulární výraz + field_min_length: Minimální délka + field_max_length: Maximální délka + field_value: Hodnota + field_category: Kategorie + field_title: Název + field_project: Projekt + field_issue: Úkol + field_status: Stav + field_notes: Poznámka + field_is_closed: Úkol uzavÅ™en + field_is_default: Výchozí stav + field_tracker: Fronta + field_subject: PÅ™edmÄ›t + field_due_date: Uzavřít do + field_assigned_to: PÅ™iÅ™azeno + field_priority: Priorita + field_fixed_version: Cílová verze + field_user: Uživatel + field_principal: Hlavní + field_role: Role + field_homepage: Domovská stránka + field_is_public: VeÅ™ejný + field_parent: NadÅ™azený projekt + field_is_in_roadmap: Úkoly zobrazené v plánu + field_login: PÅ™ihlášení + field_mail_notification: Emailová oznámení + field_admin: Administrátor + field_last_login_on: Poslední pÅ™ihlášení + field_language: Jazyk + field_effective_date: Datum + field_password: Heslo + field_new_password: Nové heslo + field_password_confirmation: Potvrzení + field_version: Verze + field_type: Typ + field_host: Host + field_port: Port + field_account: ÚÄet + field_base_dn: Base DN + field_attr_login: PÅ™ihlášení (atribut) + field_attr_firstname: Jméno (atribut) + field_attr_lastname: Příjemní (atribut) + field_attr_mail: Email (atribut) + field_onthefly: Automatické vytváření uživatelů + field_start_date: ZaÄátek + field_done_ratio: "% Hotovo" + field_auth_source: AutentifikaÄní mód + field_hide_mail: Nezobrazovat můj email + field_comments: Komentář + field_url: URL + field_start_page: Výchozí stránka + field_subproject: Podprojekt + field_hours: Hodiny + field_activity: Aktivita + field_spent_on: Datum + field_identifier: Identifikátor + field_is_filter: Použít jako filtr + field_issue_to: Související úkol + field_delay: ZpoždÄ›ní + field_assignable: Úkoly mohou být pÅ™iÅ™azeny této roli + field_redirect_existing_links: PÅ™esmÄ›rovat stvávající odkazy + field_estimated_hours: Odhadovaná doba + field_column_names: Sloupce + field_time_entries: Zaznamenaný Äas + field_time_zone: ÄŒasové pásmo + field_searchable: Umožnit vyhledávání + field_default_value: Výchozí hodnota + field_comments_sorting: Zobrazit komentáře + field_parent_title: RodiÄovská stránka + field_editable: Editovatelný + field_watcher: Sleduje + field_identity_url: OpenID URL + field_content: Obsah + field_group_by: Seskupovat výsledky podle + field_sharing: Sdílení + field_parent_issue: RodiÄovský úkol + field_member_of_group: Skupina pÅ™iÅ™aditele + field_assigned_to_role: Role pÅ™iÅ™aditele + field_text: Textové pole + field_visible: Viditelný + + setting_app_title: Název aplikace + setting_app_subtitle: Podtitulek aplikace + setting_welcome_text: Uvítací text + setting_default_language: Výchozí jazyk + setting_login_required: Autentifikace vyžadována + setting_self_registration: Povolena automatická registrace + setting_attachment_max_size: Maximální velikost přílohy + setting_issues_export_limit: Limit pro export úkolů + setting_mail_from: Odesílat emaily z adresy + setting_bcc_recipients: Příjemci skryté kopie (bcc) + setting_plain_text_mail: pouze prostý text (ne HTML) + setting_host_name: Jméno serveru + setting_text_formatting: Formátování textu + setting_wiki_compression: Komprese historie Wiki + setting_feeds_limit: Limit obsahu příspÄ›vků + setting_default_projects_public: Nové projekty nastavovat jako veÅ™ejné + setting_autofetch_changesets: Automaticky stahovat commity + setting_sys_api_enabled: Povolit WS pro správu repozitory + setting_commit_ref_keywords: KlíÄová slova pro odkazy + setting_commit_fix_keywords: KlíÄová slova pro uzavÅ™ení + setting_autologin: Automatické pÅ™ihlaÅ¡ování + setting_date_format: Formát data + setting_time_format: Formát Äasu + setting_cross_project_issue_relations: Povolit vazby úkolů napÅ™Ã­Ä projekty + setting_issue_list_default_columns: Výchozí sloupce zobrazené v seznamu úkolů + setting_emails_header: HlaviÄka emailů + setting_emails_footer: PatiÄka emailů + setting_protocol: Protokol + setting_per_page_options: Povolené poÄty řádků na stránce + setting_user_format: Formát zobrazení uživatele + setting_activity_days_default: Dny zobrazené v Äinnosti projektu + setting_display_subprojects_issues: Automaticky zobrazit úkoly podprojektu v hlavním projektu + setting_enabled_scm: Povolené SCM + setting_mail_handler_body_delimiters: Zkrátit e-maily po jednom z tÄ›chto řádků + setting_mail_handler_api_enabled: Povolit WS pro příchozí e-maily + setting_mail_handler_api_key: API klÃ­Ä + setting_sequential_project_identifiers: Generovat sekvenÄní identifikátory projektů + setting_gravatar_enabled: Použít uživatelské ikony Gravatar + setting_gravatar_default: Výchozí Gravatar + setting_diff_max_lines_displayed: Maximální poÄet zobrazených řádků rozdílů + setting_file_max_size_displayed: Maximální velikost textových souborů zobrazených přímo na stránce + setting_repository_log_display_limit: Maximální poÄet revizí zobrazených v logu souboru + setting_openid: Umožnit pÅ™ihlaÅ¡ování a registrace s OpenID + setting_password_min_length: Minimální délka hesla + setting_new_project_user_role_id: Role pÅ™iÅ™azená uživateli bez práv administrátora, který projekt vytvoÅ™il + setting_default_projects_modules: Výchozí zapnutné moduly pro nový projekt + setting_issue_done_ratio: SpoÄítat koeficient dokonÄení úkolu s + setting_issue_done_ratio_issue_field: Použít pole úkolu + setting_issue_done_ratio_issue_status: Použít stav úkolu + setting_start_of_week: ZaÄínat kalendáře + setting_rest_api_enabled: Zapnout službu REST + setting_cache_formatted_text: Ukládat formátovaný text do vyrovnávací pamÄ›ti + setting_default_notification_option: Výchozí nastavení oznámení + setting_commit_logtime_enabled: Povolit zapisování Äasu + setting_commit_logtime_activity_id: Aktivita pro zapsaný Äas + setting_gantt_items_limit: Maximální poÄet položek zobrazený na ganttovÄ› grafu + + permission_add_project: VytvoÅ™it projekt + permission_add_subprojects: VytvoÅ™it podprojekty + permission_edit_project: Úprava projektů + permission_select_project_modules: VýbÄ›r modulů projektu + permission_manage_members: Spravování Älenství + permission_manage_project_activities: Spravovat aktivity projektu + permission_manage_versions: Spravování verzí + permission_manage_categories: Spravování kategorií úkolů + permission_view_issues: Zobrazit úkoly + permission_add_issues: PÅ™idávání úkolů + permission_edit_issues: Upravování úkolů + permission_manage_issue_relations: Spravování vztahů mezi úkoly + permission_add_issue_notes: PÅ™idávání poznámek + permission_edit_issue_notes: Upravování poznámek + permission_edit_own_issue_notes: Upravování vlastních poznámek + permission_move_issues: PÅ™esouvání úkolů + permission_delete_issues: Mazání úkolů + permission_manage_public_queries: Správa veÅ™ejných dotazů + permission_save_queries: Ukládání dotazů + permission_view_gantt: Zobrazené Ganttova diagramu + permission_view_calendar: Prohlížení kalendáře + permission_view_issue_watchers: Zobrazení seznamu sledujícíh uživatelů + permission_add_issue_watchers: PÅ™idání sledujících uživatelů + permission_delete_issue_watchers: Smazat pÅ™ihlížející + permission_log_time: Zaznamenávání stráveného Äasu + permission_view_time_entries: Zobrazení stráveného Äasu + permission_edit_time_entries: Upravování záznamů o stráveném Äasu + permission_edit_own_time_entries: Upravování vlastních zázamů o stráveném Äase + permission_manage_news: Spravování novinek + permission_comment_news: Komentování novinek + permission_manage_documents: Správa dokumentů + permission_view_documents: Prohlížení dokumentů + permission_manage_files: Spravování souborů + permission_view_files: Prohlížení souborů + permission_manage_wiki: Spravování Wiki + permission_rename_wiki_pages: PÅ™ejmenovávání Wiki stránek + permission_delete_wiki_pages: Mazání stránek na Wiki + permission_view_wiki_pages: Prohlížení Wiki + permission_view_wiki_edits: Prohlížení historie Wiki + permission_edit_wiki_pages: Upravování stránek Wiki + permission_delete_wiki_pages_attachments: Mazání příloh + permission_protect_wiki_pages: ZabezpeÄení Wiki stránek + permission_manage_repository: Spravování repozitáře + permission_browse_repository: Procházení repozitáře + permission_view_changesets: Zobrazování sady zmÄ›n + permission_commit_access: Commit přístup + permission_manage_boards: Správa diskusních fór + permission_view_messages: Prohlížení zpráv + permission_add_messages: Posílání zpráv + permission_edit_messages: Upravování zpráv + permission_edit_own_messages: Upravit vlastní zprávy + permission_delete_messages: Mazání zpráv + permission_delete_own_messages: Smazat vlastní zprávy + permission_export_wiki_pages: Exportovat Wiki stránky + permission_manage_subtasks: Spravovat podúkoly + + project_module_issue_tracking: Sledování úkolů + project_module_time_tracking: Sledování Äasu + project_module_news: Novinky + project_module_documents: Dokumenty + project_module_files: Soubory + project_module_wiki: Wiki + project_module_repository: Repozitář + project_module_boards: Diskuse + project_module_calendar: Kalendář + project_module_gantt: Gantt + + label_user: Uživatel + label_user_plural: Uživatelé + label_user_new: Nový uživatel + label_user_anonymous: Anonymní + label_project: Projekt + label_project_new: Nový projekt + label_project_plural: Projekty + label_x_projects: + zero: žádné projekty + one: 1 projekt + other: "%{count} projekty(ů)" + label_project_all: VÅ¡echny projekty + label_project_latest: Poslední projekty + label_issue: Úkol + label_issue_new: Nový úkol + label_issue_plural: Úkoly + label_issue_view_all: VÅ¡echny úkoly + label_issues_by: "Úkoly podle %{value}" + label_issue_added: Úkol pÅ™idán + label_issue_updated: Úkol aktualizován + label_document: Dokument + label_document_new: Nový dokument + label_document_plural: Dokumenty + label_document_added: Dokument pÅ™idán + label_role: Role + label_role_plural: Role + label_role_new: Nová role + label_role_and_permissions: Role a práva + label_member: ÄŒlen + label_member_new: Nový Älen + label_member_plural: ÄŒlenové + label_tracker: Fronta + label_tracker_plural: Fronty + label_tracker_new: Nová fronta + label_workflow: PrůbÄ›h práce + label_issue_status: Stav úkolu + label_issue_status_plural: Stavy úkolů + label_issue_status_new: Nový stav + label_issue_category: Kategorie úkolu + label_issue_category_plural: Kategorie úkolů + label_issue_category_new: Nová kategorie + label_custom_field: Uživatelské pole + label_custom_field_plural: Uživatelská pole + label_custom_field_new: Nové uživatelské pole + label_enumerations: Seznamy + label_enumeration_new: Nová hodnota + label_information: Informace + label_information_plural: Informace + label_please_login: Prosím pÅ™ihlaÅ¡te se + label_register: Registrovat + label_login_with_open_id_option: nebo se pÅ™ihlaÅ¡te s OpenID + label_password_lost: Zapomenuté heslo + label_home: Úvodní + label_my_page: Moje stránka + label_my_account: Můj úÄet + label_my_projects: Moje projekty + label_my_page_block: Bloky na mé stránce + label_administration: Administrace + label_login: PÅ™ihlášení + label_logout: Odhlášení + label_help: NápovÄ›da + label_reported_issues: Nahlášené úkoly + label_assigned_to_me_issues: Mé úkoly + label_last_login: Poslední pÅ™ihlášení + label_registered_on: Registrován + label_activity: Aktivita + label_overall_activity: Celková aktivita + label_user_activity: "Aktivita uživatele: %{value}" + label_new: Nový + label_logged_as: PÅ™ihlášen jako + label_environment: ProstÅ™edí + label_authentication: Autentifikace + label_auth_source: Mód autentifikace + label_auth_source_new: Nový mód autentifikace + label_auth_source_plural: Módy autentifikace + label_subproject_plural: Podprojekty + label_subproject_new: Nový podprojekt + label_and_its_subprojects: "%{value} a jeho podprojekty" + label_min_max_length: Min - Max délka + label_list: Seznam + label_date: Datum + label_integer: Celé Äíslo + label_float: Desetinné Äíslo + label_boolean: Ano/Ne + label_string: Text + label_text: Dlouhý text + label_attribute: Atribut + label_attribute_plural: Atributy + label_download: "%{count} stažení" + label_download_plural: "%{count} stažení" + label_no_data: Žádné položky + label_change_status: ZmÄ›nit stav + label_history: Historie + label_attachment: Soubor + label_attachment_new: Nový soubor + label_attachment_delete: Odstranit soubor + label_attachment_plural: Soubory + label_file_added: Soubor pÅ™idán + label_report: PÅ™ehled + label_report_plural: PÅ™ehledy + label_news: Novinky + label_news_new: PÅ™idat novinku + label_news_plural: Novinky + label_news_latest: Poslední novinky + label_news_view_all: Zobrazit vÅ¡echny novinky + label_news_added: Novinka pÅ™idána + label_settings: Nastavení + label_overview: PÅ™ehled + label_version: Verze + label_version_new: Nová verze + label_version_plural: Verze + label_close_versions: Zavřít dokonÄené verze + label_confirmation: Potvrzení + label_export_to: 'Také k dispozici:' + label_read: NaÄítá se... + label_public_projects: VeÅ™ejné projekty + label_open_issues: otevÅ™ený + label_open_issues_plural: otevÅ™ené + label_closed_issues: uzavÅ™ený + label_closed_issues_plural: uzavÅ™ené + label_x_open_issues_abbr_on_total: + zero: 0 otevÅ™ených / %{total} + one: 1 otevÅ™ený / %{total} + other: "%{count} otevÅ™ených / %{total}" + label_x_open_issues_abbr: + zero: 0 otevÅ™ených + one: 1 otevÅ™ený + other: "%{count} otevÅ™ených" + label_x_closed_issues_abbr: + zero: 0 uzavÅ™ených + one: 1 uzavÅ™ený + other: "%{count} uzavÅ™ených" + label_total: Celkem + label_permissions: Práva + label_current_status: Aktuální stav + label_new_statuses_allowed: Nové povolené stavy + label_all: vÅ¡e + label_none: nic + label_nobody: nikdo + label_next: Další + label_previous: PÅ™edchozí + label_used_by: Použito + label_details: Detaily + label_add_note: PÅ™idat poznámku + label_per_page: Na stránku + label_calendar: Kalendář + label_months_from: mÄ›síců od + label_gantt: Ganttův graf + label_internal: Interní + label_last_changes: "posledních %{count} zmÄ›n" + label_change_view_all: Zobrazit vÅ¡echny zmÄ›ny + label_personalize_page: PÅ™izpůsobit tuto stránku + label_comment: Komentář + label_comment_plural: Komentáře + label_x_comments: + zero: žádné komentáře + one: 1 komentář + other: "%{count} komentářů" + label_comment_add: PÅ™idat komentáře + label_comment_added: Komentář pÅ™idán + label_comment_delete: Odstranit komentář + label_query: Uživatelský dotaz + label_query_plural: Uživatelské dotazy + label_query_new: Nový dotaz + label_filter_add: PÅ™idat filtr + label_filter_plural: Filtry + label_equals: je + label_not_equals: není + label_in_less_than: je měší než + label_in_more_than: je vÄ›tší než + label_greater_or_equal: '>=' + label_less_or_equal: '<=' + label_in: v + label_today: dnes + label_all_time: vÅ¡e + label_yesterday: vÄera + label_this_week: tento týden + label_last_week: minulý týden + label_last_n_days: "posledních %{count} dnů" + label_this_month: tento mÄ›síc + label_last_month: minulý mÄ›síc + label_this_year: tento rok + label_date_range: ÄŒasový rozsah + label_less_than_ago: pÅ™ed ménÄ› jak (dny) + label_more_than_ago: pÅ™ed více jak (dny) + label_ago: pÅ™ed (dny) + label_contains: obsahuje + label_not_contains: neobsahuje + label_day_plural: dny + label_repository: Repozitář + label_repository_plural: Repozitáře + label_browse: Procházet + label_modification: "%{count} zmÄ›na" + label_modification_plural: "%{count} zmÄ›n" + label_branch: VÄ›tev + label_tag: Tag + label_revision: Revize + label_revision_plural: Revizí + label_revision_id: "Revize %{value}" + label_associated_revisions: Související verze + label_added: pÅ™idáno + label_modified: zmÄ›nÄ›no + label_copied: zkopírováno + label_renamed: pÅ™ejmenováno + label_deleted: odstranÄ›no + label_latest_revision: Poslední revize + label_latest_revision_plural: Poslední revize + label_view_revisions: Zobrazit revize + label_view_all_revisions: Zobrazit vÅ¡echny revize + label_max_size: Maximální velikost + label_sort_highest: PÅ™esunout na zaÄátek + label_sort_higher: PÅ™esunout nahoru + label_sort_lower: PÅ™esunout dolů + label_sort_lowest: PÅ™esunout na konec + label_roadmap: Plán + label_roadmap_due_in: "Zbývá %{value}" + label_roadmap_overdue: "%{value} pozdÄ›" + label_roadmap_no_issues: Pro tuto verzi nejsou žádné úkoly + label_search: Hledat + label_result_plural: Výsledky + label_all_words: VÅ¡echna slova + label_wiki: Wiki + label_wiki_edit: Wiki úprava + label_wiki_edit_plural: Wiki úpravy + label_wiki_page: Wiki stránka + label_wiki_page_plural: Wiki stránky + label_index_by_title: Index dle názvu + label_index_by_date: Index dle data + label_current_version: Aktuální verze + label_preview: Náhled + label_feed_plural: PříspÄ›vky + label_changes_details: Detail vÅ¡ech zmÄ›n + label_issue_tracking: Sledování úkolů + label_spent_time: Strávený Äas + label_overall_spent_time: Celkem strávený Äas + label_f_hour: "%{value} hodina" + label_f_hour_plural: "%{value} hodin" + label_time_tracking: Sledování Äasu + label_change_plural: ZmÄ›ny + label_statistics: Statistiky + label_commits_per_month: Commitů za mÄ›síc + label_commits_per_author: Commitů za autora + label_view_diff: Zobrazit rozdíly + label_diff_inline: uvnitÅ™ + label_diff_side_by_side: vedle sebe + label_options: Nastavení + label_copy_workflow_from: Kopírovat průbÄ›h práce z + label_permissions_report: PÅ™ehled práv + label_watched_issues: Sledované úkoly + label_related_issues: Související úkoly + label_applied_status: Použitý stav + label_loading: Nahrávám... + label_relation_new: Nová souvislost + label_relation_delete: Odstranit souvislost + label_relates_to: související s + label_duplicates: duplikuje + label_duplicated_by: zduplikován + label_blocks: blokuje + label_blocked_by: zablokován + label_precedes: pÅ™edchází + label_follows: následuje + label_end_to_start: od konce do zaÄátku + label_end_to_end: od konce do konce + label_start_to_start: od zaÄátku do zaÄátku + label_start_to_end: od zaÄátku do konce + label_stay_logged_in: Zůstat pÅ™ihlášený + label_disabled: zakázán + label_show_completed_versions: Ukázat dokonÄené verze + label_me: já + label_board: Fórum + label_board_new: Nové fórum + label_board_plural: Fóra + label_board_locked: UzamÄeno + label_board_sticky: Nálepka + label_topic_plural: Témata + label_message_plural: Zprávy + label_message_last: Poslední zpráva + label_message_new: Nová zpráva + label_message_posted: Zpráva pÅ™idána + label_reply_plural: OdpovÄ›di + label_send_information: Zaslat informace o úÄtu uživateli + label_year: Rok + label_month: MÄ›síc + label_week: Týden + label_date_from: Od + label_date_to: Do + label_language_based: Podle výchozího jazyku + label_sort_by: "SeÅ™adit podle %{value}" + label_send_test_email: Poslat testovací email + label_feeds_access_key: Přístupový klÃ­Ä pro RSS + label_missing_feeds_access_key: Postrádá přístupový klÃ­Ä pro RSS + label_feeds_access_key_created_on: "Přístupový klÃ­Ä pro RSS byl vytvoÅ™en pÅ™ed %{value}" + label_module_plural: Moduly + label_added_time_by: "PÅ™idáno uživatelem %{author} pÅ™ed %{age}" + label_updated_time_by: "Aktualizováno uživatelem %{author} pÅ™ed %{age}" + label_updated_time: "Aktualizováno pÅ™ed %{value}" + label_jump_to_a_project: Vyberte projekt... + label_file_plural: Soubory + label_changeset_plural: Changesety + label_default_columns: Výchozí sloupce + label_no_change_option: (beze zmÄ›ny) + label_bulk_edit_selected_issues: Hromadná úprava vybraných úkolů + label_theme: Téma + label_default: Výchozí + label_search_titles_only: Vyhledávat pouze v názvech + label_user_mail_option_all: "Pro vÅ¡echny události vÅ¡ech mých projektů" + label_user_mail_option_selected: "Pro vÅ¡echny události vybraných projektů..." + label_user_mail_option_none: "Žádné události" + label_user_mail_option_only_my_events: "Jen pro vÄ›ci co sleduji nebo jsem v nich zapojen" + label_user_mail_option_only_assigned: "Jen pro vÅ¡eci kterým sem pÅ™iÅ™azen" + label_user_mail_option_only_owner: "Jen pro vÄ›ci které vlastním" + label_user_mail_no_self_notified: "Nezasílat informace o mnou vytvoÅ™ených zmÄ›nách" + label_registration_activation_by_email: aktivace úÄtu emailem + label_registration_manual_activation: manuální aktivace úÄtu + label_registration_automatic_activation: automatická aktivace úÄtu + label_display_per_page: "%{value} na stránku" + label_age: VÄ›k + label_change_properties: ZmÄ›nit vlastnosti + label_general: Obecné + label_more: Více + label_scm: SCM + label_plugins: Doplňky + label_ldap_authentication: Autentifikace LDAP + label_downloads_abbr: Staž. + label_optional_description: Volitelný popis + label_add_another_file: PÅ™idat další soubor + label_preferences: Nastavení + label_chronological_order: V chronologickém poÅ™adí + label_reverse_chronological_order: V obrácaném chronologickém poÅ™adí + label_planning: Plánování + label_incoming_emails: Příchozí e-maily + label_generate_key: Generovat klÃ­Ä + label_issue_watchers: Sledování + label_example: Příklad + label_display: Zobrazit + label_sort: Řazení + label_ascending: VzestupnÄ› + label_descending: SestupnÄ› + label_date_from_to: Od %{start} do %{end} + label_wiki_content_added: Wiki stránka pÅ™idána + label_wiki_content_updated: Wiki stránka aktualizována + label_group: Skupina + label_group_plural: Skupiny + label_group_new: Nová skupina + label_time_entry_plural: Strávený Äas + label_version_sharing_none: Nesdíleno + label_version_sharing_descendants: S podprojekty + label_version_sharing_hierarchy: S hierarchií projektu + label_version_sharing_tree: Se stromem projektu + label_version_sharing_system: Se vÅ¡emi projekty + label_update_issue_done_ratios: Aktualizovat koeficienty dokonÄení úkolů + label_copy_source: Zdroj + label_copy_target: Cíl + label_copy_same_as_target: Stejný jako cíl + label_display_used_statuses_only: Zobrazit pouze stavy které jsou použité touto frontou + label_api_access_key: API přístupový klÃ­Ä + label_missing_api_access_key: ChybÄ›jící přístupový klÃ­Ä API + label_api_access_key_created_on: API přístupový klÃ­Ä vytvoÅ™en %{value} + label_profile: Profil + label_subtask_plural: Podúkol + label_project_copy_notifications: Odeslat email oznámení v průbÄ›hu kopie projektu + label_principal_search: "Hledat uživatele nebo skupinu:" + label_user_search: "Hledat uživatele:" + + button_login: PÅ™ihlásit + button_submit: Potvrdit + button_save: Uložit + button_check_all: ZaÅ¡rtnout vÅ¡e + button_uncheck_all: OdÅ¡rtnout vÅ¡e + button_delete: Odstranit + button_create: VytvoÅ™it + button_create_and_continue: VytvoÅ™it a pokraÄovat + button_test: Testovat + button_edit: Upravit + button_edit_associated_wikipage: "Upravit pÅ™iÅ™azenou Wiki stránku: %{page_title}" + button_add: PÅ™idat + button_change: ZmÄ›nit + button_apply: Použít + button_clear: Smazat + button_lock: Zamknout + button_unlock: Odemknout + button_download: Stáhnout + button_list: Vypsat + button_view: Zobrazit + button_move: PÅ™esunout + button_move_and_follow: PÅ™esunout a následovat + button_back: ZpÄ›t + button_cancel: Storno + button_activate: Aktivovat + button_sort: SeÅ™adit + button_log_time: PÅ™idat Äas + button_rollback: ZpÄ›t k této verzi + button_watch: Sledovat + button_unwatch: Nesledovat + button_reply: OdpovÄ›dÄ›t + button_archive: Archivovat + button_unarchive: Odarchivovat + button_reset: Resetovat + button_rename: PÅ™ejmenovat + button_change_password: ZmÄ›nit heslo + button_copy: Kopírovat + button_copy_and_follow: Kopírovat a následovat + button_annotate: Komentovat + button_update: Aktualizovat + button_configure: Konfigurovat + button_quote: Citovat + button_duplicate: Duplikovat + button_show: Zobrazit + + status_active: aktivní + status_registered: registrovaný + status_locked: uzamÄený + + version_status_open: otevÅ™ený + version_status_locked: uzamÄený + version_status_closed: zavÅ™ený + + field_active: Aktivní + + text_select_mail_notifications: Vyberte akci pÅ™i které bude zasláno upozornÄ›ní emailem. + text_regexp_info: napÅ™. ^[A-Z0-9]+$ + text_min_max_length_info: 0 znamená bez limitu + text_project_destroy_confirmation: Jste si jisti, že chcete odstranit tento projekt a vÅ¡echna související data ? + text_subprojects_destroy_warning: "Jeho podprojek(y): %{value} budou také smazány." + text_workflow_edit: Vyberte roli a frontu k editaci průbÄ›hu práce + text_are_you_sure: Jste si jisti? + text_journal_changed: "%{label} zmÄ›nÄ›n z %{old} na %{new}" + text_journal_set_to: "%{label} nastaven na %{value}" + text_journal_deleted: "%{label} smazán (%{old})" + text_journal_added: "%{label} %{value} pÅ™idán" + text_tip_issue_begin_day: úkol zaÄíná v tento den + text_tip_issue_end_day: úkol konÄí v tento den + text_tip_issue_begin_end_day: úkol zaÄíná a konÄí v tento den + text_caracters_maximum: "%{count} znaků maximálnÄ›." + text_caracters_minimum: "Musí být alespoň %{count} znaků dlouhé." + text_length_between: "Délka mezi %{min} a %{max} znaky." + text_tracker_no_workflow: Pro tuto frontu není definován žádný průbÄ›h práce + text_unallowed_characters: Nepovolené znaky + text_comma_separated: Povoleno více hodnot (oddÄ›lÄ›né Äárkou). + text_line_separated: Více hodnot povoleno (jeden řádek pro každou hodnotu). + text_issues_ref_in_commit_messages: Odkazování a opravování úkolů ve zprávách commitů + text_issue_added: "Úkol %{id} byl vytvoÅ™en uživatelem %{author}." + text_issue_updated: "Úkol %{id} byl aktualizován uživatelem %{author}." + text_wiki_destroy_confirmation: Opravdu si pÅ™ejete odstranit tuto Wiki a celý její obsah? + text_issue_category_destroy_question: "NÄ›které úkoly (%{count}) jsou pÅ™iÅ™azeny k této kategorii. Co s nimi chtete udÄ›lat?" + text_issue_category_destroy_assignments: ZruÅ¡it pÅ™iÅ™azení ke kategorii + text_issue_category_reassign_to: PÅ™iÅ™adit úkoly do této kategorie + text_user_mail_option: "U projektů, které nebyly vybrány, budete dostávat oznámení pouze o vaÅ¡ich Äi o sledovaných položkách (napÅ™. o položkách jejichž jste autor nebo ke kterým jste pÅ™iÅ™azen(a))." + text_no_configuration_data: "Role, fronty, stavy úkolů ani průbÄ›h práce nebyly zatím nakonfigurovány.\nVelice doporuÄujeme nahrát výchozí konfiguraci. Po té si můžete vÅ¡e upravit" + text_load_default_configuration: Nahrát výchozí konfiguraci + text_status_changed_by_changeset: "Použito v changesetu %{value}." + text_time_logged_by_changeset: Aplikováno v changesetu %{value}. + text_issues_destroy_confirmation: 'Opravdu si pÅ™ejete odstranit vÅ¡echny zvolené úkoly?' + text_select_project_modules: 'Aktivní moduly v tomto projektu:' + text_default_administrator_account_changed: Výchozí nastavení administrátorského úÄtu zmÄ›nÄ›no + text_file_repository_writable: Povolen zápis do adresáře ukládání souborů + text_plugin_assets_writable: Možnost zápisu do adresáře plugin assets + text_rmagick_available: RMagick k dispozici (volitelné) + text_destroy_time_entries_question: "U úkolů, které chcete odstranit je evidováno %{hours} práce. Co chete udÄ›lat?" + text_destroy_time_entries: Odstranit evidované hodiny. + text_assign_time_entries_to_project: PÅ™iÅ™adit evidované hodiny projektu + text_reassign_time_entries: 'PÅ™eÅ™adit evidované hodiny k tomuto úkolu:' + text_user_wrote: "%{value} napsal:" + text_enumeration_destroy_question: "NÄ›kolik (%{count}) objektů je pÅ™iÅ™azeno k této hodnotÄ›." + text_enumeration_category_reassign_to: 'PÅ™eÅ™adit je do této:' + text_email_delivery_not_configured: "DoruÄování e-mailů není nastaveno a odesílání notifikací je zakázáno.\nNastavte Váš SMTP server v souboru config/configuration.yml a restartujte aplikaci." + text_repository_usernames_mapping: "Vybrat nebo upravit mapování mezi Redmine uživateli a uživatelskými jmény nalezenými v logu repozitáře.\nUživatelé se shodným Redmine uživatelským jménem a uživatelským jménem v repozitáři jsou mapovaní automaticky." + text_diff_truncated: '... Rozdílový soubor je zkrácen, protože jeho délka pÅ™esahuje max. limit.' + text_custom_field_possible_values_info: 'Každá hodnota na novém řádku' + text_wiki_page_destroy_question: Tato stránka má %{descendants} podstránek a potomků. Co chcete udÄ›lat? + text_wiki_page_nullify_children: Ponechat podstránky jako koÅ™enové stránky + text_wiki_page_destroy_children: Smazat podstránky a vÅ¡echny jejich potomky + text_wiki_page_reassign_children: PÅ™iÅ™adit podstránky k tomuto rodiÄi + text_own_membership_delete_confirmation: "Chystáte se odebrat si nÄ›která nebo vÅ¡echny svá oprávnÄ›ní a potom již nemusíte být schopni upravit tento projekt.\nOpravdu chcete pokraÄovat?" + text_zoom_in: PÅ™iblížit + text_zoom_out: Oddálit + + default_role_manager: Manažer + default_role_developer: Vývojář + default_role_reporter: Reportér + default_tracker_bug: Chyba + default_tracker_feature: Požadavek + default_tracker_support: Podpora + default_issue_status_new: Nový + default_issue_status_in_progress: Ve vývoji + default_issue_status_resolved: VyÅ™eÅ¡ený + default_issue_status_feedback: ÄŒeká se + default_issue_status_closed: UzavÅ™ený + default_issue_status_rejected: Odmítnutý + default_doc_category_user: Uživatelská dokumentace + default_doc_category_tech: Technická dokumentace + default_priority_low: Nízká + default_priority_normal: Normální + default_priority_high: Vysoká + default_priority_urgent: Urgentní + default_priority_immediate: Okamžitá + default_activity_design: Návhr + default_activity_development: Vývoj + + enumeration_issue_priorities: Priority úkolů + enumeration_doc_categories: Kategorie dokumentů + enumeration_activities: Aktivity (sledování Äasu) + enumeration_system_activity: Systémová aktivita + + field_warn_on_leaving_unsaved: Varuj mÄ› pÅ™ed opuÅ¡tÄ›ním stránky s neuloženým textem + text_warn_on_leaving_unsaved: Aktuální stránka obsahuje neuložený text, který bude ztracen, když opustíte stránku. + label_my_queries: Moje vlastní dotazy + text_journal_changed_no_detail: "%{label} aktualizován" + label_news_comment_added: K novince byl pÅ™idán komentář + button_expand_all: Rozbal vÅ¡e + button_collapse_all: Sbal vÅ¡e + label_additional_workflow_transitions_for_assignee: Další zmÄ›na stavu povolena, jestliže je uživatel pÅ™iÅ™azen + label_additional_workflow_transitions_for_author: Další zmÄ›na stavu povolena, jestliže je uživatel autorem + label_bulk_edit_selected_time_entries: Hromadná zmÄ›na záznamů Äasu + text_time_entries_destroy_confirmation: Jste si jistí, že chcete smazat vybraný záznam(y) Äasu? + label_role_anonymous: Anonymní + label_role_non_member: Není Älenem + label_issue_note_added: PÅ™idána poznámka + label_issue_status_updated: Aktualizován stav + label_issue_priority_updated: Aktualizována priorita + label_issues_visibility_own: Úkol vytvoÅ™en nebo pÅ™iÅ™azen uživatel(i/em) + field_issues_visibility: Viditelnost úkolů + label_issues_visibility_all: VÅ¡echny úkoly + permission_set_own_issues_private: Nastavit vlastní úkoly jako veÅ™ejné nebo soukromé + field_is_private: Soukromý + permission_set_issues_private: Nastavit úkoly jako veÅ™ejné nebo soukromé + label_issues_visibility_public: VÅ¡echny úkoly, které nejsou soukromé + text_issues_destroy_descendants_confirmation: "%{count} podúkol(ů) bude rovněž smazán(o)." + field_commit_logs_encoding: Kódování zpráv pÅ™i commitu + field_scm_path_encoding: Kódování cesty SCM + text_scm_path_encoding_note: "Výchozí: UTF-8" + field_path_to_repository: Cesta k repositáři + field_root_directory: KoÅ™enový adresář + field_cvs_module: Modul + field_cvsroot: CVSROOT + text_mercurial_repository_note: Lokální repositář (napÅ™. /hgrepo, c:\hgrepo) + text_scm_command: Příkaz + text_scm_command_version: Verze + label_git_report_last_commit: Reportovat poslední commit pro soubory a adresáře + text_scm_config: Můžete si nastavit vaÅ¡e SCM příkazy v config/configuration.yml. Restartujte, prosím, aplikaci po jejich úpravÄ›. + text_scm_command_not_available: SCM příkaz není k dispozici. Zkontrolujte, prosím, nastavení v panelu Administrace. + notice_issue_successful_create: Úkol %{id} vytvoÅ™en. + label_between: mezi + setting_issue_group_assignment: Povolit pÅ™iÅ™azení úkolu skupinÄ› + label_diff: rozdíl + text_git_repository_note: Repositář je "bare and local" (napÅ™. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: SmÄ›r třídÄ›ní + description_project_scope: Rozsah vyhledávání + description_filter: Filtr + description_user_mail_notification: Nastavení emailových notifikací + description_date_from: Zadejte poÄáteÄní datum + description_message_content: Obsah zprávy + description_available_columns: Dostupné sloupce + description_date_range_interval: Zvolte rozsah výbÄ›rem poÄáteÄního a koncového data + description_issue_category_reassign: Zvolte kategorii úkolu + description_search: Vyhledávací pole + description_notes: Poznámky + description_date_range_list: Zvolte rozsah ze seznamu + description_choose_project: Projekty + description_date_to: Zadejte datum + description_query_sort_criteria_attribute: Třídící atribut + description_wiki_subpages_reassign: Zvolte novou rodiÄovskou stránku + description_selected_columns: Vybraný sloupec + label_parent_revision: RodiÄ + label_child_revision: Potomek + error_scm_annotate_big_text_file: Vstup nemůže být anotován, protože pÅ™ekraÄuje povolenou velikost textového souboru + setting_default_issue_start_date_to_creation_date: Použij aktuální datum jako poÄáteÄní datum pro nové úkoly + button_edit_section: Uprav tuto sekci + setting_repositories_encodings: Kódování příloh a repositářů + description_all_columns: VÅ¡echny sloupce + button_export: Export + label_export_options: "nastavení exportu %{export_format}" + error_attachment_too_big: Soubor nemůže být nahrán, protože jeho velikost je vÄ›tší než maximum (%{max_size}) + notice_failed_to_save_time_entries: "Chyba pÅ™i ukládání %{count} Äasov(ých/ého) záznam(ů) z %{total} vybraného: %{ids}." + label_x_issues: + zero: 0 Úkol + one: 1 Úkol + other: "%{count} Úkoly" + label_repository_new: Nový repositář + field_repository_is_default: Hlavní repositář + label_copy_attachments: Kopírovat přílohy + label_item_position: "%{position}/%{count}" + label_completed_versions: DokonÄené verze + text_project_identifier_info: Jsou povolena pouze malá písmena (a-z), Äíslice, pomlÄky a podtržítka.
    Po uložení již nelze identifikátor mÄ›nit. + field_multiple: Více hodnot + setting_commit_cross_project_ref: Povolit reference a opravy úklů ze vÅ¡ech ostatních projektů + text_issue_conflict_resolution_add_notes: PÅ™idat moje poznámky a zahodit ostatní zmÄ›ny + text_issue_conflict_resolution_overwrite: PÅ™esto pÅ™ijmout moje úpravy (pÅ™edchozí poznámky budou zachovány, ale nÄ›které zmÄ›ny mohou být pÅ™epsány) + notice_issue_update_conflict: BÄ›hem vaÅ¡ich úprav byl úkol aktualizován jiným uživatelem. + text_issue_conflict_resolution_cancel: ZahoÄ vÅ¡echny moje zmÄ›ny a znovu zobraz %{link} + permission_manage_related_issues: Spravuj související úkoly + field_auth_source_ldap_filter: LDAP filtr + label_search_for_watchers: Hledej sledující pro pÅ™idání + notice_account_deleted: Váš úÄet byl trvale smazán. + setting_unsubscribe: Povolit uživatelům smazání jejich vlastního úÄtu + button_delete_my_account: Smazat můj úÄet + text_account_destroy_confirmation: |- + SkuteÄnÄ› chcete pokraÄovat? + Váš úÄet bude nenávratnÄ› smazán. + error_session_expired: VaÅ¡e sezení vyprÅ¡elo. Znovu se pÅ™ihlaste, prosím. + text_session_expiration_settings: "Varování: zmÄ›nou tohoto nastavení mohou vyprÅ¡et aktuální sezení vÄetnÄ› toho vaÅ¡eho." + setting_session_lifetime: Maximální Äas sezení + setting_session_timeout: VyprÅ¡ení sezení bez aktivity + label_session_expiration: VyprÅ¡ení sezení + permission_close_project: Zavřít / Otevřít projekt + label_show_closed_projects: Zobrazit zavÅ™ené projekty + button_close: Zavřít + button_reopen: Znovu otevřít + project_status_active: aktivní + project_status_closed: zavÅ™ený + project_status_archived: archivovaný + text_project_closed: Tento projekt je uzevÅ™ený a je pouze pro Ätení. + notice_user_successful_create: Uživatel %{id} vytvoÅ™en. + field_core_fields: Standardní pole + field_timeout: VyprÅ¡ení (v sekundách) + setting_thumbnails_enabled: Zobrazit náhled přílohy + setting_thumbnails_size: Velikost náhledu (v pixelech) + label_status_transitions: ZmÄ›na stavu + label_fields_permissions: Práva k polím + label_readonly: Pouze pro Ätení + label_required: Vyžadováno + text_repository_identifier_info: Jou povoleny pouze malá písmena (a-z), Äíslice, pomlÄky a podtržítka.
    Po uložení již nelze identifikátor zmÄ›nit. + field_board_parent: RodiÄovské fórum + label_attribute_of_project: Projektové %{name} + label_attribute_of_author: Autorovo %{name} + label_attribute_of_assigned_to: "%{name} pÅ™iÅ™azené(ho)" + label_attribute_of_fixed_version: Cílová verze %{name} + label_copy_subtasks: Kopírovat podúkoly + label_copied_to: zkopírováno do + label_copied_from: zkopírováno z + label_any_issues_in_project: jakékoli úkoly v projektu + label_any_issues_not_in_project: jakékoli úkoly mimo projektu + field_private_notes: Soukromé poznámky + permission_view_private_notes: Zobrazit soukromé poznámky + permission_set_notes_private: Nastavit poznámky jako soukromé + label_no_issues_in_project: žádné úkoly v projektu + label_any: vÅ¡e + label_last_n_weeks: poslední %{count} týdny + setting_cross_project_subtasks: Povolit podúkoly napÅ™Ã­Ä projekty + label_cross_project_descendants: S podprojekty + label_cross_project_tree: Se stromem projektu + label_cross_project_hierarchy: S hierarchií projektu + label_cross_project_system: Se vÅ¡emi projekty + button_hide: Skrýt + setting_non_working_week_days: Dny pracovního volna/klidu + label_in_the_next_days: v přístích + label_in_the_past_days: v minulých diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d8/d84028e9377754df4951a789fc085f8e2eb7bb46.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d8/d84028e9377754df4951a789fc085f8e2eb7bb46.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,287 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require "digest/md5" + +class Attachment < ActiveRecord::Base + belongs_to :container, :polymorphic => true + belongs_to :author, :class_name => "User", :foreign_key => "author_id" + + validates_presence_of :filename, :author + validates_length_of :filename, :maximum => 255 + validates_length_of :disk_filename, :maximum => 255 + validates_length_of :description, :maximum => 255 + validate :validate_max_file_size + + acts_as_event :title => :filename, + :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}} + + acts_as_activity_provider :type => 'files', + :permission => :view_files, + :author_key => :author_id, + :find_options => {:select => "#{Attachment.table_name}.*", + :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " + + "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"} + + acts_as_activity_provider :type => 'documents', + :permission => :view_documents, + :author_key => :author_id, + :find_options => {:select => "#{Attachment.table_name}.*", + :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " + + "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"} + + cattr_accessor :storage_path + @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files") + + cattr_accessor :thumbnails_storage_path + @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails") + + before_save :files_to_final_location + after_destroy :delete_from_disk + + # Returns an unsaved copy of the attachment + def copy(attributes=nil) + copy = self.class.new + copy.attributes = self.attributes.dup.except("id", "downloads") + copy.attributes = attributes if attributes + copy + end + + def validate_max_file_size + if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes + errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes)) + end + end + + def file=(incoming_file) + unless incoming_file.nil? + @temp_file = incoming_file + if @temp_file.size > 0 + if @temp_file.respond_to?(:original_filename) + self.filename = @temp_file.original_filename + self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding) + end + if @temp_file.respond_to?(:content_type) + self.content_type = @temp_file.content_type.to_s.chomp + end + if content_type.blank? && filename.present? + self.content_type = Redmine::MimeType.of(filename) + end + self.filesize = @temp_file.size + end + end + end + + def file + nil + end + + def filename=(arg) + write_attribute :filename, sanitize_filename(arg.to_s) + if new_record? && disk_filename.blank? + self.disk_filename = Attachment.disk_filename(filename) + end + filename + end + + # Copies the temporary file to its final location + # and computes its MD5 hash + def files_to_final_location + if @temp_file && (@temp_file.size > 0) + logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") + md5 = Digest::MD5.new + File.open(diskfile, "wb") do |f| + if @temp_file.respond_to?(:read) + buffer = "" + while (buffer = @temp_file.read(8192)) + f.write(buffer) + md5.update(buffer) + end + else + f.write(@temp_file) + md5.update(@temp_file) + end + end + self.digest = md5.hexdigest + end + @temp_file = nil + # Don't save the content type if it's longer than the authorized length + if self.content_type && self.content_type.length > 255 + self.content_type = nil + end + end + + # Deletes the file from the file system if it's not referenced by other attachments + def delete_from_disk + if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty? + delete_from_disk! + end + end + + # Returns file's location on disk + def diskfile + File.join(self.class.storage_path, disk_filename.to_s) + end + + def title + title = filename.to_s + if description.present? + title << " (#{description})" + end + title + end + + def increment_download + increment!(:downloads) + end + + def project + container.try(:project) + end + + def visible?(user=User.current) + container && container.attachments_visible?(user) + end + + def deletable?(user=User.current) + container && container.attachments_deletable?(user) + end + + def image? + !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i) + end + + def thumbnailable? + image? + end + + # Returns the full path the attachment thumbnail, or nil + # if the thumbnail cannot be generated. + def thumbnail(options={}) + if thumbnailable? && readable? + size = options[:size].to_i + if size > 0 + # Limit the number of thumbnails per image + size = (size / 50) * 50 + # Maximum thumbnail size + size = 800 if size > 800 + else + size = Setting.thumbnails_size.to_i + end + size = 100 unless size > 0 + target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb") + + begin + Redmine::Thumbnail.generate(self.diskfile, target, size) + rescue => e + logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger + return nil + end + end + end + + # Deletes all thumbnails + def self.clear_thumbnails + Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file| + File.delete file + end + end + + def is_text? + Redmine::MimeType.is_type?('text', filename) + end + + def is_diff? + self.filename =~ /\.(patch|diff)$/i + end + + # Returns true if the file is readable + def readable? + File.readable?(diskfile) + end + + # Returns the attachment token + def token + "#{id}.#{digest}" + end + + # Finds an attachment that matches the given token and that has no container + def self.find_by_token(token) + if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/ + attachment_id, attachment_digest = $1, $2 + attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first + if attachment && attachment.container.nil? + attachment + end + end + end + + # Bulk attaches a set of files to an object + # + # Returns a Hash of the results: + # :files => array of the attached files + # :unsaved => array of the files that could not be attached + def self.attach_files(obj, attachments) + result = obj.save_attachments(attachments, User.current) + obj.attach_saved_attachments + result + end + + def self.latest_attach(attachments, filename) + attachments.sort_by(&:created_on).reverse.detect { + |att| att.filename.downcase == filename.downcase + } + end + + def self.prune(age=1.day) + Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all + end + + private + + # Physically deletes the file from the file system + def delete_from_disk! + if disk_filename.present? && File.exist?(diskfile) + File.delete(diskfile) + end + end + + def sanitize_filename(value) + # get only the filename, not the whole path + just_filename = value.gsub(/^.*(\\|\/)/, '') + + # Finally, replace invalid characters with underscore + @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_') + end + + # Returns an ASCII or hashed filename + def self.disk_filename(filename) + timestamp = DateTime.now.strftime("%y%m%d%H%M%S") + ascii = '' + if filename =~ %r{^[a-zA-Z0-9_\.\-]*$} + ascii = filename + else + ascii = Digest::MD5.hexdigest(filename) + # keep the extension if any + ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$} + end + while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}")) + timestamp.succ! + end + "#{timestamp}_#{ascii}" + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d8/d8639e1305d3a5ce3a25132171c278b1ff63a8b1.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d8/d8639e1305d3a5ce3a25132171c278b1ff63a8b1.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,26 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Comment < ActiveRecord::Base + include Redmine::SafeAttributes + belongs_to :commented, :polymorphic => true, :counter_cache => true + belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + + validates_presence_of :commented, :author, :comments + + safe_attributes 'comments' +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d8/d872c59c53fb6c77d47a3fa010a7ea331d0c6dee.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d8/d872c59c53fb6c77d47a3fa010a7ea331d0c6dee.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,55 @@ +

    <%= link_to l(:label_role_plural), roles_path %> » <%=l(:label_permissions_report)%>

    + +<%= form_tag(permissions_roles_path, :id => 'permissions_form') do %> +<%= hidden_field_tag 'permissions[0]', '', :id => nil %> +
    + + + + + <% @roles.each do |role| %> + + <% end %> + + + +<% perms_by_module = @permissions.group_by {|p| p.project_module.to_s} %> +<% perms_by_module.keys.sort.each do |mod| %> + <% unless mod.blank? %> + + + <% @roles.each do |role| %> + + <% end %> + + <% end %> + <% perms_by_module[mod].each do |permission| %> + + + <% @roles.each do |role| %> + + <% end %> + + <% end %> +<% end %> + +
    <%=l(:label_permissions)%> + <%= content_tag(role.builtin? ? 'em' : 'span', h(role.name)) %> + <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.role-#{role.id}')", + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %> +
    +   + <%= l_or_humanize(mod, :prefix => 'project_module_') %> + <%= h(role.name) %>
    + <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('.permission-#{permission.name} input')", + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %> + <%= l_or_humanize(permission.name, :prefix => 'permission_') %> + + <% if role.setable_permissions.include? permission %> + <%= check_box_tag "permissions[#{role.id}][]", permission.name, (role.permissions.include? permission.name), :id => nil, :class => "role-#{role.id}" %> + <% end %> +
    +
    +

    <%= check_all_links 'permissions_form' %>

    +

    <%= submit_tag l(:button_save) %>

    +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d8/d87bb15034cb1df578fd9d349f69712ac9695482.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d8/d87bb15034cb1df578fd9d349f69712ac9695482.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,749 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../../test_helper', __FILE__) + +class Redmine::Helpers::GanttHelperTest < ActionView::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :journals, :journal_details, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :workflows, + :versions, + :groups_users + + include ApplicationHelper + include ProjectsHelper + include IssuesHelper + include ERB::Util + + def setup + setup_with_controller + User.current = User.find(1) + end + + def today + @today ||= Date.today + end + + # Creates a Gantt chart for a 4 week span + def create_gantt(project=Project.generate!, options={}) + @project = project + @gantt = Redmine::Helpers::Gantt.new(options) + @gantt.project = @project + @gantt.query = Query.create!(:project => @project, :name => 'Gantt') + @gantt.view = self + @gantt.instance_variable_set('@date_from', options[:date_from] || (today - 14)) + @gantt.instance_variable_set('@date_to', options[:date_to] || (today + 14)) + end + + context "#number_of_rows" do + context "with one project" do + should "return the number of rows just for that project" + end + + context "with no project" do + should "return the total number of rows for all the projects, resursively" + end + + should "not exceed max_rows option" do + p = Project.generate! + 5.times do + Issue.generate!(:project => p) + end + create_gantt(p) + @gantt.render + assert_equal 6, @gantt.number_of_rows + assert !@gantt.truncated + create_gantt(p, :max_rows => 3) + @gantt.render + assert_equal 3, @gantt.number_of_rows + assert @gantt.truncated + end + end + + context "#number_of_rows_on_project" do + setup do + create_gantt + end + + should "count 0 for an empty the project" do + assert_equal 0, @gantt.number_of_rows_on_project(@project) + end + + should "count the number of issues without a version" do + @project.issues << Issue.generate!(:project => @project, :fixed_version => nil) + assert_equal 2, @gantt.number_of_rows_on_project(@project) + end + + should "count the number of issues on versions, including cross-project" do + version = Version.generate! + @project.versions << version + @project.issues << Issue.generate!(:project => @project, :fixed_version => version) + assert_equal 3, @gantt.number_of_rows_on_project(@project) + end + end + + # TODO: more of an integration test + context "#subjects" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => (today + 7), :sharing => 'none') + @project.versions << @version + @issue = Issue.generate!(:fixed_version => @version, + :subject => "gantt#line_for_project", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => (today - 1), + :due_date => (today + 7)) + @project.issues << @issue + end + + context "project" do + should "be rendered" do + @output_buffer = @gantt.subjects + assert_select "div.project-name a", /#{@project.name}/ + end + + should "have an indent of 4" do + @output_buffer = @gantt.subjects + assert_select "div.project-name[style*=left:4px]" + end + end + + context "version" do + should "be rendered" do + @output_buffer = @gantt.subjects + assert_select "div.version-name a", /#{@version.name}/ + end + + should "be indented 24 (one level)" do + @output_buffer = @gantt.subjects + assert_select "div.version-name[style*=left:24px]" + end + + context "without assigned issues" do + setup do + @version = Version.generate!(:effective_date => (today + 14), + :sharing => 'none', + :name => 'empty_version') + @project.versions << @version + end + + should "not be rendered" do + @output_buffer = @gantt.subjects + assert_select "div.version-name a", :text => /#{@version.name}/, :count => 0 + end + end + end + + context "issue" do + should "be rendered" do + @output_buffer = @gantt.subjects + assert_select "div.issue-subject", /#{@issue.subject}/ + end + + should "be indented 44 (two levels)" do + @output_buffer = @gantt.subjects + assert_select "div.issue-subject[style*=left:44px]" + end + + context "assigned to a shared version of another project" do + setup do + p = Project.generate! + p.enabled_module_names = [:issue_tracking] + @shared_version = Version.generate!(:sharing => 'system') + p.versions << @shared_version + # Reassign the issue to a shared version of another project + @issue = Issue.generate!(:fixed_version => @shared_version, + :subject => "gantt#assigned_to_shared_version", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => (today - 1), + :due_date => (today + 7)) + @project.issues << @issue + end + + should "be rendered" do + @output_buffer = @gantt.subjects + assert_select "div.issue-subject", /#{@issue.subject}/ + end + end + + context "with subtasks" do + setup do + attrs = {:project => @project, :tracker => @tracker, :fixed_version => @version} + @child1 = Issue.generate!( + attrs.merge(:subject => 'child1', + :parent_issue_id => @issue.id, + :start_date => (today - 1), + :due_date => (today + 2)) + ) + @child2 = Issue.generate!( + attrs.merge(:subject => 'child2', + :parent_issue_id => @issue.id, + :start_date => today, + :due_date => (today + 7)) + ) + @grandchild = Issue.generate!( + attrs.merge(:subject => 'grandchild', + :parent_issue_id => @child1.id, + :start_date => (today - 1), + :due_date => (today + 2)) + ) + end + + should "indent subtasks" do + @output_buffer = @gantt.subjects + # parent task 44px + assert_select "div.issue-subject[style*=left:44px]", /#{@issue.subject}/ + # children 64px + assert_select "div.issue-subject[style*=left:64px]", /child1/ + assert_select "div.issue-subject[style*=left:64px]", /child2/ + # grandchild 84px + assert_select "div.issue-subject[style*=left:84px]", /grandchild/, @output_buffer + end + end + end + end + + context "#lines" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => (today + 7)) + @project.versions << @version + @issue = Issue.generate!(:fixed_version => @version, + :subject => "gantt#line_for_project", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => (today - 1), + :due_date => (today + 7)) + @project.issues << @issue + @output_buffer = @gantt.lines + end + + context "project" do + should "be rendered" do + assert_select "div.project.task_todo" + assert_select "div.project.starting" + assert_select "div.project.ending" + assert_select "div.label.project", /#{@project.name}/ + end + end + + context "version" do + should "be rendered" do + assert_select "div.version.task_todo" + assert_select "div.version.starting" + assert_select "div.version.ending" + assert_select "div.label.version", /#{@version.name}/ + end + end + + context "issue" do + should "be rendered" do + assert_select "div.task_todo" + assert_select "div.task.label", /#{@issue.done_ratio}/ + assert_select "div.tooltip", /#{@issue.subject}/ + end + end + end + + context "#render_project" do + should "be tested" + end + + context "#render_issues" do + should "be tested" + end + + context "#render_version" do + should "be tested" + end + + context "#subject_for_project" do + setup do + create_gantt + end + + context ":html format" do + should "add an absolute positioned div" do + @output_buffer = @gantt.subject_for_project(@project, {:format => :html}) + assert_select "div[style*=absolute]" + end + + should "use the indent option to move the div to the right" do + @output_buffer = @gantt.subject_for_project(@project, {:format => :html, :indent => 40}) + assert_select "div[style*=left:40]" + end + + should "include the project name" do + @output_buffer = @gantt.subject_for_project(@project, {:format => :html}) + assert_select 'div', :text => /#{@project.name}/ + end + + should "include a link to the project" do + @output_buffer = @gantt.subject_for_project(@project, {:format => :html}) + assert_select 'a[href=?]', "/projects/#{@project.identifier}", :text => /#{@project.name}/ + end + + should "style overdue projects" do + @project.enabled_module_names = [:issue_tracking] + @project.versions << Version.generate!(:effective_date => (today - 1)) + assert @project.reload.overdue?, "Need an overdue project for this test" + @output_buffer = @gantt.subject_for_project(@project, {:format => :html}) + assert_select 'div span.project-overdue' + end + end + should "test the PNG format" + should "test the PDF format" + end + + context "#line_for_project" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => (today - 1)) + @project.versions << @version + @project.issues << Issue.generate!(:fixed_version => @version, + :subject => "gantt#line_for_project", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => (today - 7), + :due_date => (today + 7)) + end + + context ":html format" do + context "todo line" do + should "start from the starting point on the left" do + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.task_todo[style*=left:28px]", true, @output_buffer + end + + should "be the total width of the project" do + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.task_todo[style*=width:58px]", true, @output_buffer + end + end + + context "late line" do + should_eventually "start from the starting point on the left" do + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.task_late[style*=left:28px]", true, @output_buffer + end + + should_eventually "be the total delayed width of the project" do + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.task_late[style*=width:30px]", true, @output_buffer + end + end + + context "done line" do + should_eventually "start from the starting point on the left" do + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.task_done[style*=left:28px]", true, @output_buffer + end + + should_eventually "Be the total done width of the project" do + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.task_done[style*=width:18px]", true, @output_buffer + end + end + + context "starting marker" do + should "not appear if the starting point is off the gantt chart" do + # Shift the date range of the chart + @gantt.instance_variable_set('@date_from', today) + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.starting", false, @output_buffer + end + + should "appear at the starting point" do + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.starting[style*=left:28px]", true, @output_buffer + end + end + + context "ending marker" do + should "not appear if the starting point is off the gantt chart" do + # Shift the date range of the chart + @gantt.instance_variable_set('@date_to', (today - 14)) + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.ending", false, @output_buffer + end + + should "appear at the end of the date range" do + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.ending[style*=left:88px]", true, @output_buffer + end + end + + context "status content" do + should "appear at the far left, even if it's far in the past" do + @gantt.instance_variable_set('@date_to', (today - 14)) + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.label", /#{@project.name}/ + end + + should "show the project name" do + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.label", /#{@project.name}/ + end + + should_eventually "show the percent complete" do + @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project.label", /0%/ + end + end + end + should "test the PNG format" + should "test the PDF format" + end + + context "#subject_for_version" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => (today - 1)) + @project.versions << @version + @project.issues << Issue.generate!(:fixed_version => @version, + :subject => "gantt#subject_for_version", + :tracker => @tracker, + :project => @project, + :start_date => today) + + end + + context ":html format" do + should "add an absolute positioned div" do + @output_buffer = @gantt.subject_for_version(@version, {:format => :html}) + assert_select "div[style*=absolute]" + end + + should "use the indent option to move the div to the right" do + @output_buffer = @gantt.subject_for_version(@version, {:format => :html, :indent => 40}) + assert_select "div[style*=left:40]" + end + + should "include the version name" do + @output_buffer = @gantt.subject_for_version(@version, {:format => :html}) + assert_select 'div', :text => /#{@version.name}/ + end + + should "include a link to the version" do + @output_buffer = @gantt.subject_for_version(@version, {:format => :html}) + assert_select 'a[href=?]', Regexp.escape("/versions/#{@version.to_param}"), :text => /#{@version.name}/ + end + + should "style late versions" do + assert @version.overdue?, "Need an overdue version for this test" + @output_buffer = @gantt.subject_for_version(@version, {:format => :html}) + assert_select 'div span.version-behind-schedule' + end + + should "style behind schedule versions" do + assert @version.behind_schedule?, "Need a behind schedule version for this test" + @output_buffer = @gantt.subject_for_version(@version, {:format => :html}) + assert_select 'div span.version-behind-schedule' + end + end + should "test the PNG format" + should "test the PDF format" + end + + context "#line_for_version" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => (today + 7)) + @project.versions << @version + @project.issues << Issue.generate!(:fixed_version => @version, + :subject => "gantt#line_for_project", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => (today - 7), + :due_date => (today + 7)) + end + + context ":html format" do + context "todo line" do + should "start from the starting point on the left" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.task_todo[style*=left:28px]", true, @output_buffer + end + + should "be the total width of the version" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.task_todo[style*=width:58px]", true, @output_buffer + end + end + + context "late line" do + should "start from the starting point on the left" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.task_late[style*=left:28px]", true, @output_buffer + end + + should "be the total delayed width of the version" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.task_late[style*=width:30px]", true, @output_buffer + end + end + + context "done line" do + should "start from the starting point on the left" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.task_done[style*=left:28px]", true, @output_buffer + end + + should "be the total done width of the version" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.task_done[style*=width:16px]", true, @output_buffer + end + end + + context "starting marker" do + should "not appear if the starting point is off the gantt chart" do + # Shift the date range of the chart + @gantt.instance_variable_set('@date_from', today) + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.starting", false + end + + should "appear at the starting point" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.starting[style*=left:28px]", true, @output_buffer + end + end + + context "ending marker" do + should "not appear if the starting point is off the gantt chart" do + # Shift the date range of the chart + @gantt.instance_variable_set('@date_to', (today - 14)) + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.ending", false + end + + should "appear at the end of the date range" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.ending[style*=left:88px]", true, @output_buffer + end + end + + context "status content" do + should "appear at the far left, even if it's far in the past" do + @gantt.instance_variable_set('@date_to', (today - 14)) + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.label", /#{@version.name}/ + end + + should "show the version name" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.label", /#{@version.name}/ + end + + should "show the percent complete" do + @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version.label", /30%/ + end + end + end + should "test the PNG format" + should "test the PDF format" + end + + context "#subject_for_issue" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @issue = Issue.generate!(:subject => "gantt#subject_for_issue", + :tracker => @tracker, + :project => @project, + :start_date => (today - 3), + :due_date => (today - 1)) + @project.issues << @issue + end + + context ":html format" do + should "add an absolute positioned div" do + @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html}) + assert_select "div[style*=absolute]" + end + + should "use the indent option to move the div to the right" do + @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html, :indent => 40}) + assert_select "div[style*=left:40]" + end + + should "include the issue subject" do + @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html}) + assert_select 'div', :text => /#{@issue.subject}/ + end + + should "include a link to the issue" do + @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html}) + assert_select 'a[href=?]', Regexp.escape("/issues/#{@issue.to_param}"), :text => /#{@tracker.name} ##{@issue.id}/ + end + + should "style overdue issues" do + assert @issue.overdue?, "Need an overdue issue for this test" + @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html}) + assert_select 'div span.issue-overdue' + end + end + should "test the PNG format" + should "test the PDF format" + end + + context "#line_for_issue" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => (today + 7)) + @project.versions << @version + @issue = Issue.generate!(:fixed_version => @version, + :subject => "gantt#line_for_project", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => (today - 7), + :due_date => (today + 7)) + @project.issues << @issue + end + + context ":html format" do + context "todo line" do + should "start from the starting point on the left" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_todo[style*=left:28px]", true, @output_buffer + end + + should "be the total width of the issue" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_todo[style*=width:58px]", true, @output_buffer + end + end + + context "late line" do + should "start from the starting point on the left" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_late[style*=left:28px]", true, @output_buffer + end + + should "be the total delayed width of the issue" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_late[style*=width:30px]", true, @output_buffer + end + end + + context "done line" do + should "start from the starting point on the left" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_done[style*=left:28px]", true, @output_buffer + end + + should "be the total done width of the issue" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + # 15 days * 4 px * 30% - 2 px for borders = 16 px + assert_select "div.task_done[style*=width:16px]", true, @output_buffer + end + + should "not be the total done width if the chart starts after issue start date" do + create_gantt(@project, :date_from => (today - 5)) + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_done[style*=left:0px]", true, @output_buffer + assert_select "div.task_done[style*=width:8px]", true, @output_buffer + end + + context "for completed issue" do + setup do + @issue.done_ratio = 100 + end + + should "be the total width of the issue" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_done[style*=width:58px]", true, @output_buffer + end + + should "be the total width of the issue with due_date=start_date" do + @issue.due_date = @issue.start_date + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_done[style*=width:2px]", true, @output_buffer + end + end + end + + context "status content" do + should "appear at the far left, even if it's far in the past" do + @gantt.instance_variable_set('@date_to', (today - 14)) + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task.label", true, @output_buffer + end + + should "show the issue status" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task.label", /#{@issue.status.name}/ + end + + should "show the percent complete" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task.label", /30%/ + end + end + end + + should "have an issue tooltip" do + @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.tooltip", /#{@issue.subject}/ + end + should "test the PNG format" + should "test the PDF format" + end + + context "#to_image" do + should "be tested" + end + + context "#to_pdf" do + should "be tested" + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d8/d886d8a19901a4b56d0aaccc00956e0c5770dc2f.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d8/d886d8a19901a4b56d0aaccc00956e0c5770dc2f.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,197 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class MyController < ApplicationController + before_filter :require_login + + helper :issues + helper :users + helper :custom_fields + + BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues, + 'issuesreportedbyme' => :label_reported_issues, + 'issueswatched' => :label_watched_issues, + 'news' => :label_news_latest, + 'calendar' => :label_calendar, + 'documents' => :label_document_plural, + 'timelog' => :label_spent_time + }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze + + DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'], + 'right' => ['issuesreportedbyme'] + }.freeze + + def index + page + render :action => 'page' + end + + # Show user's page + def page + @user = User.current + @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT + end + + # Edit user's account + def account + @user = User.current + @pref = @user.pref + if request.post? + @user.safe_attributes = params[:user] + @user.pref.attributes = params[:pref] + @user.pref[:no_self_notified] = (params[:no_self_notified] == '1') + if @user.save + @user.pref.save + @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : []) + set_language_if_valid @user.language + flash[:notice] = l(:notice_account_updated) + redirect_to :action => 'account' + return + end + end + end + + # Destroys user's account + def destroy + @user = User.current + unless @user.own_account_deletable? + redirect_to :action => 'account' + return + end + + if request.post? && params[:confirm] + @user.destroy + if @user.destroyed? + logout_user + flash[:notice] = l(:notice_account_deleted) + end + redirect_to home_path + end + end + + # Manage user's password + def password + @user = User.current + unless @user.change_password_allowed? + flash[:error] = l(:notice_can_t_change_password) + redirect_to :action => 'account' + return + end + if request.post? + if @user.check_password?(params[:password]) + @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation] + if @user.save + flash[:notice] = l(:notice_account_password_updated) + redirect_to :action => 'account' + end + else + flash[:error] = l(:notice_account_wrong_password) + end + end + end + + # Create a new feeds key + def reset_rss_key + if request.post? + if User.current.rss_token + User.current.rss_token.destroy + User.current.reload + end + User.current.rss_key + flash[:notice] = l(:notice_feeds_access_key_reseted) + end + redirect_to :action => 'account' + end + + # Create a new API key + def reset_api_key + if request.post? + if User.current.api_token + User.current.api_token.destroy + User.current.reload + end + User.current.api_key + flash[:notice] = l(:notice_api_access_key_reseted) + end + redirect_to :action => 'account' + end + + # User's page layout configuration + def page_layout + @user = User.current + @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup + @block_options = [] + BLOCKS.each do |k, v| + unless %w(top left right).detect {|f| (@blocks[f] ||= []).include?(k)} + @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize] + end + end + end + + # Add a block to user's page + # The block is added on top of the page + # params[:block] : id of the block to add + def add_block + block = params[:block].to_s.underscore + (render :nothing => true; return) unless block && (BLOCKS.keys.include? block) + @user = User.current + layout = @user.pref[:my_page_layout] || {} + # remove if already present in a group + %w(top left right).each {|f| (layout[f] ||= []).delete block } + # add it on top + layout['top'].unshift block + @user.pref[:my_page_layout] = layout + @user.pref.save + redirect_to :action => 'page_layout' + end + + # Remove a block to user's page + # params[:block] : id of the block to remove + def remove_block + block = params[:block].to_s.underscore + @user = User.current + # remove block in all groups + layout = @user.pref[:my_page_layout] || {} + %w(top left right).each {|f| (layout[f] ||= []).delete block } + @user.pref[:my_page_layout] = layout + @user.pref.save + redirect_to :action => 'page_layout' + end + + # Change blocks order on user's page + # params[:group] : group to order (top, left or right) + # params[:list-(top|left|right)] : array of block ids of the group + def order_blocks + group = params[:group] + @user = User.current + if group.is_a?(String) + group_items = (params["blocks"] || []).collect(&:underscore) + group_items.each {|s| s.sub!(/^block_/, '')} + if group_items and group_items.is_a? Array + layout = @user.pref[:my_page_layout] || {} + # remove group blocks if they are presents in other groups + %w(top left right).each {|f| + layout[f] = (layout[f] || []) - group_items + } + layout[group] = group_items + @user.pref[:my_page_layout] = layout + @user.pref.save + end + end + render :nothing => true + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d8/d8ed23d57cbf25c5697f0046250deb47d4d986f0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d8/d8ed23d57cbf25c5697f0046250deb47d4d986f0.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,52 @@ +<%= error_messages_for 'tracker' %> + +
    +
    + +

    <%= f.text_field :name, :required => true %>

    +

    <%= f.check_box :is_in_roadmap %>

    + +

    + + <% Tracker::CORE_FIELDS.each do |field| %> + + <% end %> +

    +<%= hidden_field_tag 'tracker[core_fields][]', '' %> + +<% if IssueCustomField.all.any? %> +

    + + <% IssueCustomField.all.each do |field| %> + + <% end %> +

    +<%= hidden_field_tag 'tracker[custom_field_ids][]', '' %> +<% end %> + +<% if @tracker.new_record? && @trackers.any? %> +

    +<%= select_tag(:copy_workflow_from, content_tag("option") + options_from_collection_for_select(@trackers, :id, :name)) %>

    +<% end %> + +
    +<%= submit_tag l(@tracker.new_record? ? :button_create : :button_save) %> +
    + +
    +<% if @projects.any? %> +
    <%= l(:label_project_plural) %> +<%= render_project_nested_lists(@projects) do |p| + content_tag('label', check_box_tag('tracker[project_ids][]', p.id, @tracker.projects.include?(p), :id => nil) + ' ' + h(p)) +end %> +<%= hidden_field_tag('tracker[project_ids][]', '', :id => nil) %> +

    <%= check_all_links 'tracker_project_ids' %>

    +
    +<% end %> +
    diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d9/d913b426f0ac47a8c67a615cc644671eedcdad09.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d9/d913b426f0ac47a8c67a615cc644671eedcdad09.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,26 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Views + class ApiTemplateHandler + def self.call(template) + "Redmine::Views::Builders.for(params[:format], request, response) do |api|; #{template.source}; self.output_buffer = api.output; end" + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d9/d980ec891829eaff6db830fbecf2991936a709a8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d9/d980ec891829eaff6db830fbecf2991936a709a8.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,792 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'iconv' +require 'tcpdf' +require 'fpdf/chinese' +require 'fpdf/japanese' +require 'fpdf/korean' + +module Redmine + module Export + module PDF + include ActionView::Helpers::TextHelper + include ActionView::Helpers::NumberHelper + include IssuesHelper + + class ITCPDF < TCPDF + include Redmine::I18n + attr_accessor :footer_date + + def initialize(lang, orientation='P') + @@k_path_cache = Rails.root.join('tmp', 'pdf') + FileUtils.mkdir_p @@k_path_cache unless File::exist?(@@k_path_cache) + set_language_if_valid lang + pdf_encoding = l(:general_pdf_encoding).upcase + super(orientation, 'mm', 'A4', (pdf_encoding == 'UTF-8'), pdf_encoding) + case current_language.to_s.downcase + when 'vi' + @font_for_content = 'DejaVuSans' + @font_for_footer = 'DejaVuSans' + else + case pdf_encoding + when 'UTF-8' + @font_for_content = 'FreeSans' + @font_for_footer = 'FreeSans' + when 'CP949' + extend(PDF_Korean) + AddUHCFont() + @font_for_content = 'UHC' + @font_for_footer = 'UHC' + when 'CP932', 'SJIS', 'SHIFT_JIS' + extend(PDF_Japanese) + AddSJISFont() + @font_for_content = 'SJIS' + @font_for_footer = 'SJIS' + when 'GB18030' + extend(PDF_Chinese) + AddGBFont() + @font_for_content = 'GB' + @font_for_footer = 'GB' + when 'BIG5' + extend(PDF_Chinese) + AddBig5Font() + @font_for_content = 'Big5' + @font_for_footer = 'Big5' + else + @font_for_content = 'Arial' + @font_for_footer = 'Helvetica' + end + end + SetCreator(Redmine::Info.app_name) + SetFont(@font_for_content) + @outlines = [] + @outlineRoot = nil + end + + def SetFontStyle(style, size) + SetFont(@font_for_content, style, size) + end + + def SetTitle(txt) + txt = begin + utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt) + hextxt = "" + rescue + txt + end || '' + super(txt) + end + + def textstring(s) + # Format a text string + if s =~ /^ txt, :l => level, :p => PageNo(), :y => (@h - y)*@k} + end + + def bookmark_title(txt) + txt = begin + utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt) + hextxt = "" + rescue + txt + end || '' + end + + def putbookmarks + nb=@outlines.size + return if (nb==0) + lru=[] + level=0 + @outlines.each_with_index do |o, i| + if(o[:l]>0) + parent=lru[o[:l]-1] + #Set parent and last pointers + @outlines[i][:parent]=parent + @outlines[parent][:last]=i + if (o[:l]>level) + #Level increasing: set first pointer + @outlines[parent][:first]=i + end + else + @outlines[i][:parent]=nb + end + if (o[:l]<=level && i>0) + #Set prev and next pointers + prev=lru[o[:l]] + @outlines[prev][:next]=i + @outlines[i][:prev]=prev + end + lru[o[:l]]=i + level=o[:l] + end + #Outline items + n=self.n+1 + @outlines.each_with_index do |o, i| + newobj() + out('<>') + out('endobj') + end + #Outline root + newobj() + @outlineRoot=self.n + out("<>"); + out('endobj'); + end + + def putresources() + super + putbookmarks() + end + + def putcatalog() + super + if(@outlines.size > 0) + out("/Outlines #{@outlineRoot} 0 R"); + out('/PageMode /UseOutlines'); + end + end + end + + # fetch row values + def fetch_row_values(issue, query, level) + query.inline_columns.collect do |column| + s = if column.is_a?(QueryCustomFieldColumn) + cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id} + show_value(cv) + else + value = issue.send(column.name) + if column.name == :subject + value = " " * level + value + end + if value.is_a?(Date) + format_date(value) + elsif value.is_a?(Time) + format_time(value) + else + value + end + end + s.to_s + end + end + + # calculate columns width + def calc_col_width(issues, query, table_width, pdf) + # calculate statistics + # by captions + pdf.SetFontStyle('B',8) + col_padding = pdf.GetStringWidth('OO') + col_width_min = query.inline_columns.map {|v| pdf.GetStringWidth(v.caption) + col_padding} + col_width_max = Array.new(col_width_min) + col_width_avg = Array.new(col_width_min) + word_width_max = query.inline_columns.map {|c| + n = 10 + c.caption.split.each {|w| + x = pdf.GetStringWidth(w) + col_padding + n = x if n < x + } + n + } + + # by properties of issues + pdf.SetFontStyle('',8) + col_padding = pdf.GetStringWidth('OO') + k = 1 + issue_list(issues) {|issue, level| + k += 1 + values = fetch_row_values(issue, query, level) + values.each_with_index {|v,i| + n = pdf.GetStringWidth(v) + col_padding + col_width_max[i] = n if col_width_max[i] < n + col_width_min[i] = n if col_width_min[i] > n + col_width_avg[i] += n + v.split.each {|w| + x = pdf.GetStringWidth(w) + col_padding + word_width_max[i] = x if word_width_max[i] < x + } + } + } + col_width_avg.map! {|x| x / k} + + # calculate columns width + ratio = table_width / col_width_avg.inject(0) {|s,w| s += w} + col_width = col_width_avg.map {|w| w * ratio} + + # correct max word width if too many columns + ratio = table_width / word_width_max.inject(0) {|s,w| s += w} + word_width_max.map! {|v| v * ratio} if ratio < 1 + + # correct and lock width of some columns + done = 1 + col_fix = [] + col_width.each_with_index do |w,i| + if w > col_width_max[i] + col_width[i] = col_width_max[i] + col_fix[i] = 1 + done = 0 + elsif w < word_width_max[i] + col_width[i] = word_width_max[i] + col_fix[i] = 1 + done = 0 + else + col_fix[i] = 0 + end + end + + # iterate while need to correct and lock coluns width + while done == 0 + # calculate free & locked columns width + done = 1 + fix_col_width = 0 + free_col_width = 0 + col_width.each_with_index do |w,i| + if col_fix[i] == 1 + fix_col_width += w + else + free_col_width += w + end + end + + # calculate column normalizing ratio + if free_col_width == 0 + ratio = table_width / col_width.inject(0) {|s,w| s += w} + else + ratio = (table_width - fix_col_width) / free_col_width + end + + # correct columns width + col_width.each_with_index do |w,i| + if col_fix[i] == 0 + col_width[i] = w * ratio + + # check if column width less then max word width + if col_width[i] < word_width_max[i] + col_width[i] = word_width_max[i] + col_fix[i] = 1 + done = 0 + elsif col_width[i] > col_width_max[i] + col_width[i] = col_width_max[i] + col_fix[i] = 1 + done = 0 + end + end + end + end + col_width + end + + def render_table_header(pdf, query, col_width, row_height, col_id_width, table_width) + # headers + pdf.SetFontStyle('B',8) + pdf.SetFillColor(230, 230, 230) + + # render it background to find the max height used + base_x = pdf.GetX + base_y = pdf.GetY + max_height = issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, row_height, true) + pdf.Rect(base_x, base_y, table_width + col_id_width, max_height, 'FD'); + pdf.SetXY(base_x, base_y); + + # write the cells on page + pdf.RDMCell(col_id_width, row_height, "#", "T", 0, 'C', 1) + issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, row_height, true) + issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, col_id_width, col_width) + pdf.SetY(base_y + max_height); + + # rows + pdf.SetFontStyle('',8) + pdf.SetFillColor(255, 255, 255) + end + + # Returns a PDF string of a list of issues + def issues_to_pdf(issues, project, query) + pdf = ITCPDF.new(current_language, "L") + title = query.new_record? ? l(:label_issue_plural) : query.name + title = "#{project} - #{title}" if project + pdf.SetTitle(title) + pdf.alias_nb_pages + pdf.footer_date = format_date(Date.today) + pdf.SetAutoPageBreak(false) + pdf.AddPage("L") + + # Landscape A4 = 210 x 297 mm + page_height = 210 + page_width = 297 + right_margin = 10 + bottom_margin = 20 + col_id_width = 10 + row_height = 4 + + # column widths + table_width = page_width - right_margin - 10 # fixed left margin + col_width = [] + unless query.inline_columns.empty? + col_width = calc_col_width(issues, query, table_width - col_id_width, pdf) + table_width = col_width.inject(0) {|s,v| s += v} + end + + # use full width if the description is displayed + if table_width > 0 && query.has_column?(:description) + col_width = col_width.map {|w| w = w * (page_width - right_margin - 10 - col_id_width) / table_width} + table_width = col_width.inject(0) {|s,v| s += v} + end + + # title + pdf.SetFontStyle('B',11) + pdf.RDMCell(190,10, title) + pdf.Ln + render_table_header(pdf, query, col_width, row_height, col_id_width, table_width) + previous_group = false + issue_list(issues) do |issue, level| + if query.grouped? && + (group = query.group_by_column.value(issue)) != previous_group + pdf.SetFontStyle('B',10) + group_label = group.blank? ? 'None' : group.to_s.dup + group_label << " (#{query.issue_count_by_group[group]})" + pdf.Bookmark group_label, 0, -1 + pdf.RDMCell(table_width + col_id_width, row_height * 2, group_label, 1, 1, 'L') + pdf.SetFontStyle('',8) + previous_group = group + end + + # fetch row values + col_values = fetch_row_values(issue, query, level) + + # render it off-page to find the max height used + base_x = pdf.GetX + base_y = pdf.GetY + pdf.SetY(2 * page_height) + max_height = issues_to_pdf_write_cells(pdf, col_values, col_width, row_height) + pdf.SetXY(base_x, base_y) + + # make new page if it doesn't fit on the current one + space_left = page_height - base_y - bottom_margin + if max_height > space_left + pdf.AddPage("L") + render_table_header(pdf, query, col_width, row_height, col_id_width, table_width) + base_x = pdf.GetX + base_y = pdf.GetY + end + + # write the cells on page + pdf.RDMCell(col_id_width, row_height, issue.id.to_s, "T", 0, 'C', 1) + issues_to_pdf_write_cells(pdf, col_values, col_width, row_height) + issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, col_id_width, col_width) + pdf.SetY(base_y + max_height); + + if query.has_column?(:description) && issue.description? + pdf.SetX(10) + pdf.SetAutoPageBreak(true, 20) + pdf.RDMwriteHTMLCell(0, 5, 10, 0, issue.description.to_s, issue.attachments, "LRBT") + pdf.SetAutoPageBreak(false) + end + end + + if issues.size == Setting.issues_export_limit.to_i + pdf.SetFontStyle('B',10) + pdf.RDMCell(0, row_height, '...') + end + pdf.Output + end + + # Renders MultiCells and returns the maximum height used + def issues_to_pdf_write_cells(pdf, col_values, col_widths, + row_height, head=false) + base_y = pdf.GetY + max_height = row_height + col_values.each_with_index do |column, i| + col_x = pdf.GetX + if head == true + pdf.RDMMultiCell(col_widths[i], row_height, column.caption, "T", 'L', 1) + else + pdf.RDMMultiCell(col_widths[i], row_height, column, "T", 'L', 1) + end + max_height = (pdf.GetY - base_y) if (pdf.GetY - base_y) > max_height + pdf.SetXY(col_x + col_widths[i], base_y); + end + return max_height + end + + # Draw lines to close the row (MultiCell border drawing in not uniform) + def issues_to_pdf_draw_borders(pdf, top_x, top_y, lower_y, + id_width, col_widths) + col_x = top_x + id_width + pdf.Line(col_x, top_y, col_x, lower_y) # id right border + col_widths.each do |width| + col_x += width + pdf.Line(col_x, top_y, col_x, lower_y) # columns right border + end + pdf.Line(top_x, top_y, top_x, lower_y) # left border + pdf.Line(top_x, lower_y, col_x, lower_y) # bottom border + end + + # Returns a PDF string of a single issue + def issue_to_pdf(issue, assoc={}) + pdf = ITCPDF.new(current_language) + pdf.SetTitle("#{issue.project} - #{issue.tracker} ##{issue.id}") + pdf.alias_nb_pages + pdf.footer_date = format_date(Date.today) + pdf.AddPage + pdf.SetFontStyle('B',11) + buf = "#{issue.project} - #{issue.tracker} ##{issue.id}" + pdf.RDMMultiCell(190, 5, buf) + pdf.SetFontStyle('',8) + base_x = pdf.GetX + i = 1 + issue.ancestors.visible.each do |ancestor| + pdf.SetX(base_x + i) + buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}" + pdf.RDMMultiCell(190 - i, 5, buf) + i += 1 if i < 35 + end + pdf.SetFontStyle('B',11) + pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s) + pdf.SetFontStyle('',8) + pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}") + pdf.Ln + + left = [] + left << [l(:field_status), issue.status] + left << [l(:field_priority), issue.priority] + left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id') + left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id') + left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id') + + right = [] + right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date') + right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date') + right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio') + right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours') + right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project) + + rows = left.size > right.size ? left.size : right.size + while left.size < rows + left << nil + end + while right.size < rows + right << nil + end + + half = (issue.custom_field_values.size / 2.0).ceil + issue.custom_field_values.each_with_index do |custom_value, i| + (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value)] + end + + rows = left.size > right.size ? left.size : right.size + rows.times do |i| + item = left[i] + pdf.SetFontStyle('B',9) + pdf.RDMCell(35,5, item ? "#{item.first}:" : "", i == 0 ? "LT" : "L") + pdf.SetFontStyle('',9) + pdf.RDMCell(60,5, item ? item.last.to_s : "", i == 0 ? "RT" : "R") + + item = right[i] + pdf.SetFontStyle('B',9) + pdf.RDMCell(35,5, item ? "#{item.first}:" : "", i == 0 ? "LT" : "L") + pdf.SetFontStyle('',9) + pdf.RDMCell(60,5, item ? item.last.to_s : "", i == 0 ? "RT" : "R") + pdf.Ln + end + + pdf.SetFontStyle('B',9) + pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1) + pdf.SetFontStyle('',9) + + # Set resize image scale + pdf.SetImageScale(1.6) + pdf.RDMwriteHTMLCell(35+155, 5, 0, 0, + issue.description.to_s, issue.attachments, "LRB") + + unless issue.leaf? + # for CJK + truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 90 : 65 ) + + pdf.SetFontStyle('B',9) + pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR") + pdf.Ln + issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level| + buf = truncate("#{child.tracker} # #{child.id}: #{child.subject}", + :length => truncate_length) + level = 10 if level >= 10 + pdf.SetFontStyle('',8) + pdf.RDMCell(35+135,5, (level >=1 ? " " * level : "") + buf, "L") + pdf.SetFontStyle('B',8) + pdf.RDMCell(20,5, child.status.to_s, "R") + pdf.Ln + end + end + + relations = issue.relations.select { |r| r.other_issue(issue).visible? } + unless relations.empty? + # for CJK + truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 80 : 60 ) + + pdf.SetFontStyle('B',9) + pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR") + pdf.Ln + relations.each do |relation| + buf = "" + buf += "#{l(relation.label_for(issue))} " + if relation.delay && relation.delay != 0 + buf += "(#{l('datetime.distance_in_words.x_days', :count => relation.delay)}) " + end + if Setting.cross_project_issue_relations? + buf += "#{relation.other_issue(issue).project} - " + end + buf += "#{relation.other_issue(issue).tracker}" + + " # #{relation.other_issue(issue).id}: #{relation.other_issue(issue).subject}" + buf = truncate(buf, :length => truncate_length) + pdf.SetFontStyle('', 8) + pdf.RDMCell(35+155-60, 5, buf, "L") + pdf.SetFontStyle('B',8) + pdf.RDMCell(20,5, relation.other_issue(issue).status.to_s, "") + pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "") + pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), "R") + pdf.Ln + end + end + pdf.RDMCell(190,5, "", "T") + pdf.Ln + + if issue.changesets.any? && + User.current.allowed_to?(:view_changesets, issue.project) + pdf.SetFontStyle('B',9) + pdf.RDMCell(190,5, l(:label_associated_revisions), "B") + pdf.Ln + for changeset in issue.changesets + pdf.SetFontStyle('B',8) + csstr = "#{l(:label_revision)} #{changeset.format_identifier} - " + csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s + pdf.RDMCell(190, 5, csstr) + pdf.Ln + unless changeset.comments.blank? + pdf.SetFontStyle('',8) + pdf.RDMwriteHTMLCell(190,5,0,0, + changeset.comments.to_s, issue.attachments, "") + end + pdf.Ln + end + end + + if assoc[:journals].present? + pdf.SetFontStyle('B',9) + pdf.RDMCell(190,5, l(:label_history), "B") + pdf.Ln + assoc[:journals].each do |journal| + pdf.SetFontStyle('B',8) + title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}" + title << " (#{l(:field_private_notes)})" if journal.private_notes? + pdf.RDMCell(190,5, title) + pdf.Ln + pdf.SetFontStyle('I',8) + details_to_strings(journal.details, true).each do |string| + pdf.RDMMultiCell(190,5, "- " + string) + end + if journal.notes? + pdf.Ln unless journal.details.empty? + pdf.SetFontStyle('',8) + pdf.RDMwriteHTMLCell(190,5,0,0, + journal.notes.to_s, issue.attachments, "") + end + pdf.Ln + end + end + + if issue.attachments.any? + pdf.SetFontStyle('B',9) + pdf.RDMCell(190,5, l(:label_attachment_plural), "B") + pdf.Ln + for attachment in issue.attachments + pdf.SetFontStyle('',8) + pdf.RDMCell(80,5, attachment.filename) + pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R") + pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R") + pdf.RDMCell(65,5, attachment.author.name,0,0,"R") + pdf.Ln + end + end + pdf.Output + end + + # Returns a PDF string of a set of wiki pages + def wiki_pages_to_pdf(pages, project) + pdf = ITCPDF.new(current_language) + pdf.SetTitle(project.name) + pdf.alias_nb_pages + pdf.footer_date = format_date(Date.today) + pdf.AddPage + pdf.SetFontStyle('B',11) + pdf.RDMMultiCell(190,5, project.name) + pdf.Ln + # Set resize image scale + pdf.SetImageScale(1.6) + pdf.SetFontStyle('',9) + write_page_hierarchy(pdf, pages.group_by(&:parent_id)) + pdf.Output + end + + # Returns a PDF string of a single wiki page + def wiki_page_to_pdf(page, project) + pdf = ITCPDF.new(current_language) + pdf.SetTitle("#{project} - #{page.title}") + pdf.alias_nb_pages + pdf.footer_date = format_date(Date.today) + pdf.AddPage + pdf.SetFontStyle('B',11) + pdf.RDMMultiCell(190,5, + "#{project} - #{page.title} - # #{page.content.version}") + pdf.Ln + # Set resize image scale + pdf.SetImageScale(1.6) + pdf.SetFontStyle('',9) + write_wiki_page(pdf, page) + pdf.Output + end + + def write_page_hierarchy(pdf, pages, node=nil, level=0) + if pages[node] + pages[node].each do |page| + if @new_page + pdf.AddPage + else + @new_page = true + end + pdf.Bookmark page.title, level + write_wiki_page(pdf, page) + write_page_hierarchy(pdf, pages, page.id, level + 1) if pages[page.id] + end + end + end + + def write_wiki_page(pdf, page) + pdf.RDMwriteHTMLCell(190,5,0,0, + page.content.text.to_s, page.attachments, 0) + if page.attachments.any? + pdf.Ln + pdf.SetFontStyle('B',9) + pdf.RDMCell(190,5, l(:label_attachment_plural), "B") + pdf.Ln + for attachment in page.attachments + pdf.SetFontStyle('',8) + pdf.RDMCell(80,5, attachment.filename) + pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R") + pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R") + pdf.RDMCell(65,5, attachment.author.name,0,0,"R") + pdf.Ln + end + end + end + + class RDMPdfEncoding + def self.rdm_from_utf8(txt, encoding) + txt ||= '' + txt = Redmine::CodesetUtil.from_utf8(txt, encoding) + if txt.respond_to?(:force_encoding) + txt.force_encoding('ASCII-8BIT') + end + txt + end + + def self.attach(attachments, filename, encoding) + filename_utf8 = Redmine::CodesetUtil.to_utf8(filename, encoding) + atta = nil + if filename_utf8 =~ /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i + atta = Attachment.latest_attach(attachments, filename_utf8) + end + if atta && atta.readable? && atta.visible? + return atta + else + return nil + end + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/d9/d98580268108411b4971461a40dedaf1dca1cebb.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/d9/d98580268108411b4971461a40dedaf1dca1cebb.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,71 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class ReportsControllerTest < ActionController::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :workflows, + :versions + + def setup + end + + def test_get_issue_report + get :issue_report, :id => 1 + + assert_response :success + assert_template 'issue_report' + + [:issues_by_tracker, :issues_by_version, :issues_by_category, :issues_by_assigned_to, + :issues_by_author, :issues_by_subproject, :issues_by_priority].each do |ivar| + assert_not_nil assigns(ivar) + end + + assert_equal IssuePriority.all.reverse, assigns(:priorities) + end + + def test_get_issue_report_details + %w(tracker version priority category assigned_to author subproject).each do |detail| + get :issue_report_details, :id => 1, :detail => detail + + assert_response :success + assert_template 'issue_report_details' + assert_not_nil assigns(:field) + assert_not_nil assigns(:rows) + assert_not_nil assigns(:data) + assert_not_nil assigns(:report_title) + end + end + + def test_get_issue_report_details_by_priority + get :issue_report_details, :id => 1, :detail => 'priority' + assert_equal IssuePriority.all.reverse, assigns(:rows) + end + + def test_get_issue_report_details_with_an_invalid_detail + get :issue_report_details, :id => 1, :detail => 'invalid' + + assert_redirected_to '/projects/ecookbook/issues/report' + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/da/da90c32e29d880fb86d4a92a29e832113b99a3f6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/da/da90c32e29d880fb86d4a92a29e832113b99a3f6.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,45 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class RoutingWorkflowsTest < ActionController::IntegrationTest + def test_workflows + assert_routing( + { :method => 'get', :path => "/workflows" }, + { :controller => 'workflows', :action => 'index' } + ) + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/workflows/edit" }, + { :controller => 'workflows', :action => 'edit' } + ) + end + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/workflows/permissions" }, + { :controller => 'workflows', :action => 'permissions' } + ) + end + ["get", "post"].each do |method| + assert_routing( + { :method => method, :path => "/workflows/copy" }, + { :controller => 'workflows', :action => 'copy' } + ) + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/db/db1913fc4efda154284af35ccff3e42e0708bad8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/db/db1913fc4efda154284af35ccff3e42e0708bad8.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,166 @@ +require File.expand_path('../../test_helper', __FILE__) + +class ActivitiesControllerTest < ActionController::TestCase + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :groups_users, + :enabled_modules, + :workflows, + :journals, :journal_details + + + def test_project_index + get :index, :id => 1, :with_subprojects => 0 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:events_by_day) + + assert_tag :tag => "h3", + :content => /#{2.days.ago.to_date.day}/, + :sibling => { :tag => "dl", + :child => { :tag => "dt", + :attributes => { :class => /issue-edit/ }, + :child => { :tag => "a", + :content => /(#{IssueStatus.find(2).name})/, + } + } + } + end + + def test_project_index_with_invalid_project_id_should_respond_404 + get :index, :id => 299 + assert_response 404 + end + + def test_previous_project_index + get :index, :id => 1, :from => 2.days.ago.to_date + assert_response :success + assert_template 'index' + assert_not_nil assigns(:events_by_day) + + assert_tag :tag => "h3", + :content => /#{3.day.ago.to_date.day}/, + :sibling => { :tag => "dl", + :child => { :tag => "dt", + :attributes => { :class => /issue/ }, + :child => { :tag => "a", + :content => /Can't print recipes/, + } + } + } + end + + def test_global_index + @request.session[:user_id] = 1 + get :index + assert_response :success + assert_template 'index' + assert_not_nil assigns(:events_by_day) + + i5 = Issue.find(5) + d5 = User.find(1).time_to_date(i5.created_on) + assert_tag :tag => "h3", + :content => /#{d5.day}/, + :sibling => { :tag => "dl", + :child => { :tag => "dt", + :attributes => { :class => /issue/ }, + :child => { :tag => "a", + :content => /Subproject issue/, + } + } + } + end + + def test_user_index + @request.session[:user_id] = 1 + get :index, :user_id => 2 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:events_by_day) + + assert_select 'h2 a[href=/users/2]', :text => 'John Smith' + + i1 = Issue.find(1) + d1 = User.find(1).time_to_date(i1.created_on) + + assert_tag :tag => "h3", + :content => /#{d1.day}/, + :sibling => { :tag => "dl", + :child => { :tag => "dt", + :attributes => { :class => /issue/ }, + :child => { :tag => "a", + :content => /Can't print recipes/, + } + } + } + end + + def test_user_index_with_invalid_user_id_should_respond_404 + get :index, :user_id => 299 + assert_response 404 + end + + def test_index_atom_feed + get :index, :format => 'atom', :with_subprojects => 0 + assert_response :success + assert_template 'common/feed' + + assert_tag :tag => 'link', :parent => {:tag => 'feed', :parent => nil }, + :attributes => {:rel => 'self', :href => 'http://test.host/activity.atom?with_subprojects=0'} + assert_tag :tag => 'link', :parent => {:tag => 'feed', :parent => nil }, + :attributes => {:rel => 'alternate', :href => 'http://test.host/activity?with_subprojects=0'} + + assert_tag :tag => 'entry', :child => { + :tag => 'link', + :attributes => {:href => 'http://test.host/issues/11'}} + end + + def test_index_atom_feed_with_explicit_selection + get :index, :format => 'atom', :with_subprojects => 0, + :show_changesets => 1, + :show_documents => 1, + :show_files => 1, + :show_issues => 1, + :show_messages => 1, + :show_news => 1, + :show_time_entries => 1, + :show_wiki_edits => 1 + + assert_response :success + assert_template 'common/feed' + + assert_tag :tag => 'link', :parent => {:tag => 'feed', :parent => nil }, + :attributes => {:rel => 'self', :href => 'http://test.host/activity.atom?show_changesets=1&show_documents=1&show_files=1&show_issues=1&show_messages=1&show_news=1&show_time_entries=1&show_wiki_edits=1&with_subprojects=0'} + assert_tag :tag => 'link', :parent => {:tag => 'feed', :parent => nil }, + :attributes => {:rel => 'alternate', :href => 'http://test.host/activity?show_changesets=1&show_documents=1&show_files=1&show_issues=1&show_messages=1&show_news=1&show_time_entries=1&show_wiki_edits=1&with_subprojects=0'} + + assert_tag :tag => 'entry', :child => { + :tag => 'link', + :attributes => {:href => 'http://test.host/issues/11'}} + end + + def test_index_atom_feed_with_one_item_type + get :index, :format => 'atom', :show_issues => '1' + assert_response :success + assert_template 'common/feed' + assert_tag :tag => 'title', :content => /Issues/ + end + + def test_index_should_show_private_notes_with_permission_only + journal = Journal.create!(:journalized => Issue.find(2), :notes => 'Private notes with searchkeyword', :private_notes => true) + @request.session[:user_id] = 2 + + get :index + assert_response :success + assert_include journal, assigns(:events_by_day).values.flatten + + Role.find(1).remove_permission! :view_private_notes + get :index + assert_response :success + assert_not_include journal, assigns(:events_by_day).values.flatten + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/db/db78dc41b63948b8d9e9d114615e04997bccd4b3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/db/db78dc41b63948b8d9e9d114615e04997bccd4b3.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,3 @@ +class <%= @model_class %> < ActiveRecord::Base + unloadable +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/db/db8f4e77d30a8490da79511166f71ee7a646520b.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/db/db8f4e77d30a8490da79511166f71ee7a646520b.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,86 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class ContextMenusController < ApplicationController + helper :watchers + helper :issues + + def issues + @issues = Issue.visible.all(:conditions => {:id => params[:ids]}, :include => :project) + if (@issues.size == 1) + @issue = @issues.first + end + @issue_ids = @issues.map(&:id).sort + + @allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&) + @projects = @issues.collect(&:project).compact.uniq + @project = @projects.first if @projects.size == 1 + + @can = {:edit => User.current.allowed_to?(:edit_issues, @projects), + :log_time => (@project && User.current.allowed_to?(:log_time, @project)), + :update => (User.current.allowed_to?(:edit_issues, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)), + :move => (@project && User.current.allowed_to?(:move_issues, @project)), + :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), + :delete => User.current.allowed_to?(:delete_issues, @projects) + } + if @project + if @issue + @assignables = @issue.assignable_users + else + @assignables = @project.assignable_users + end + @trackers = @project.trackers + else + #when multiple projects, we only keep the intersection of each set + @assignables = @projects.map(&:assignable_users).reduce(:&) + @trackers = @projects.map(&:trackers).reduce(:&) + end + @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&) + + @priorities = IssuePriority.active.reverse + @back = back_url + + @options_by_custom_field = {} + if @can[:edit] + custom_fields = @issues.map(&:available_custom_fields).reduce(:&).select do |f| + %w(bool list user version).include?(f.field_format) && !f.multiple? + end + custom_fields.each do |field| + values = field.possible_values_options(@projects) + if values.any? + @options_by_custom_field[field] = values + end + end + end + + @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&) + render :layout => false + end + + def time_entries + @time_entries = TimeEntry.all( + :conditions => {:id => params[:ids]}, :include => :project) + @projects = @time_entries.collect(&:project).compact.uniq + @project = @projects.first if @projects.size == 1 + @activities = TimeEntryActivity.shared.active + @can = {:edit => User.current.allowed_to?(:edit_time_entries, @projects), + :delete => User.current.allowed_to?(:edit_time_entries, @projects) + } + @back = back_url + render :layout => false + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/db/dbe1bda8432254ab1c9b544c6336106917863bf8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/db/dbe1bda8432254ab1c9b544c6336106917863bf8.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,139 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class MessagesController < ApplicationController + menu_item :boards + default_search_scope :messages + before_filter :find_board, :only => [:new, :preview] + before_filter :find_message, :except => [:new, :preview] + before_filter :authorize, :except => [:preview, :edit, :destroy] + + helper :boards + helper :watchers + helper :attachments + include AttachmentsHelper + + REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE) + + # Show a topic and its replies + def show + page = params[:page] + # Find the page of the requested reply + if params[:r] && page.nil? + offset = @topic.children.count(:conditions => ["#{Message.table_name}.id < ?", params[:r].to_i]) + page = 1 + offset / REPLIES_PER_PAGE + end + + @reply_count = @topic.children.count + @reply_pages = Paginator.new self, @reply_count, REPLIES_PER_PAGE, page + @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}], + :order => "#{Message.table_name}.created_on ASC", + :limit => @reply_pages.items_per_page, + :offset => @reply_pages.current.offset) + + @reply = Message.new(:subject => "RE: #{@message.subject}") + render :action => "show", :layout => false if request.xhr? + end + + # Create a new topic + def new + @message = Message.new + @message.author = User.current + @message.board = @board + @message.safe_attributes = params[:message] + if request.post? + @message.save_attachments(params[:attachments]) + if @message.save + call_hook(:controller_messages_new_after_save, { :params => params, :message => @message}) + render_attachment_warning_if_needed(@message) + redirect_to board_message_path(@board, @message) + end + end + end + + # Reply to a topic + def reply + @reply = Message.new + @reply.author = User.current + @reply.board = @board + @reply.safe_attributes = params[:reply] + @topic.children << @reply + if !@reply.new_record? + call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply}) + attachments = Attachment.attach_files(@reply, params[:attachments]) + render_attachment_warning_if_needed(@reply) + end + redirect_to board_message_path(@board, @topic, :r => @reply) + end + + # Edit a message + def edit + (render_403; return false) unless @message.editable_by?(User.current) + @message.safe_attributes = params[:message] + if request.post? && @message.save + attachments = Attachment.attach_files(@message, params[:attachments]) + render_attachment_warning_if_needed(@message) + flash[:notice] = l(:notice_successful_update) + @message.reload + redirect_to board_message_path(@message.board, @message.root, :r => (@message.parent_id && @message.id)) + end + end + + # Delete a messages + def destroy + (render_403; return false) unless @message.destroyable_by?(User.current) + r = @message.to_param + @message.destroy + if @message.parent + redirect_to board_message_path(@board, @message.parent, :r => r) + else + redirect_to project_board_path(@project, @board) + end + end + + def quote + @subject = @message.subject + @subject = "RE: #{@subject}" unless @subject.starts_with?('RE:') + + @content = "#{ll(Setting.default_language, :text_user_wrote, @message.author)}\n> " + @content << @message.content.to_s.strip.gsub(%r{
    ((.|\s)*?)
    }m, '[...]').gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n" + end + + def preview + message = @board.messages.find_by_id(params[:id]) + @attachements = message.attachments if message + @text = (params[:message] || params[:reply])[:content] + @previewed = message + render :partial => 'common/preview' + end + +private + def find_message + find_board + @message = @board.messages.find(params[:id], :include => :parent) + @topic = @message.root + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_board + @board = Board.find(params[:board_id], :include => :project) + @project = @board.project + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/dc/dc6b5b76f3744d49697cec4f0525f325d4065393.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/dc/dc6b5b76f3744d49697cec4f0525f325d4065393.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,39 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class CustomValueTest < ActiveSupport::TestCase + fixtures :custom_fields, :custom_values, :users + + def test_default_value + field = CustomField.find_by_default_value('Default string') + assert_not_nil field + + v = CustomValue.new(:custom_field => field) + assert_equal 'Default string', v.value + + v = CustomValue.new(:custom_field => field, :value => 'Not empty') + assert_equal 'Not empty', v.value + end + + def test_sti_polymorphic_association + # Rails uses top level sti class for polymorphic association. See #3978. + assert !User.find(4).custom_values.empty? + assert !CustomValue.find(2).customized.nil? + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/dc/dc72dc360fcebca8e2972a84e7d6d09313cabbe7.svn-base --- a/.svn/pristine/dc/dc72dc360fcebca8e2972a84e7d6d09313cabbe7.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -class EnabledModule < ActiveRecord::Base - generator_for :name, :start => 'module_001' - -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/dc/dc9b1cbad00145256a0e92e2088ee0a8b20dc51e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/dc/dc9b1cbad00145256a0e92e2088ee0a8b20dc51e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1181 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class ApplicationHelperTest < ActionView::TestCase + include ERB::Util + + fixtures :projects, :roles, :enabled_modules, :users, + :repositories, :changesets, + :trackers, :issue_statuses, :issues, :versions, :documents, + :wikis, :wiki_pages, :wiki_contents, + :boards, :messages, :news, + :attachments, :enumerations + + def setup + super + set_tmp_attachments_directory + end + + context "#link_to_if_authorized" do + context "authorized user" do + should "be tested" + end + + context "unauthorized user" do + should "be tested" + end + + should "allow using the :controller and :action for the target link" do + User.current = User.find_by_login('admin') + + @project = Issue.first.project # Used by helper + response = link_to_if_authorized("By controller/action", + {:controller => 'issues', :action => 'edit', :id => Issue.first.id}) + assert_match /href/, response + end + + end + + def test_auto_links + to_test = { + 'http://foo.bar' => 'http://foo.bar', + 'http://foo.bar/~user' => 'http://foo.bar/~user', + 'http://foo.bar.' => 'http://foo.bar.', + 'https://foo.bar.' => 'https://foo.bar.', + 'This is a link: http://foo.bar.' => 'This is a link: http://foo.bar.', + 'A link (eg. http://foo.bar).' => 'A link (eg. http://foo.bar).', + 'http://foo.bar/foo.bar#foo.bar.' => 'http://foo.bar/foo.bar#foo.bar.', + 'http://www.foo.bar/Test_(foobar)' => 'http://www.foo.bar/Test_(foobar)', + '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : http://www.foo.bar/Test_(foobar))', + '(see inline link : http://www.foo.bar/Test)' => '(see inline link : http://www.foo.bar/Test)', + '(see inline link : http://www.foo.bar/Test).' => '(see inline link : http://www.foo.bar/Test).', + '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see inline link)', + '(see "inline link":http://www.foo.bar/Test)' => '(see inline link)', + '(see "inline link":http://www.foo.bar/Test).' => '(see inline link).', + 'www.foo.bar' => 'www.foo.bar', + 'http://foo.bar/page?p=1&t=z&s=' => 'http://foo.bar/page?p=1&t=z&s=', + 'http://foo.bar/page#125' => 'http://foo.bar/page#125', + 'http://foo@www.bar.com' => 'http://foo@www.bar.com', + 'http://foo:bar@www.bar.com' => 'http://foo:bar@www.bar.com', + 'ftp://foo.bar' => 'ftp://foo.bar', + 'ftps://foo.bar' => 'ftps://foo.bar', + 'sftp://foo.bar' => 'sftp://foo.bar', + # two exclamation marks + 'http://example.net/path!602815048C7B5C20!302.html' => 'http://example.net/path!602815048C7B5C20!302.html', + # escaping + 'http://foo"bar' => 'http://foo"bar', + # wrap in angle brackets + '' => '<http://foo.bar>' + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + if 'ruby'.respond_to?(:encoding) + def test_auto_links_with_non_ascii_characters + to_test = { + 'http://foo.bar/теÑÑ‚' => 'http://foo.bar/теÑÑ‚' + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + else + puts 'Skipping test_auto_links_with_non_ascii_characters, unsupported ruby version' + end + + def test_auto_mailto + assert_equal '

    ', + textilizable('test@foo.bar') + end + + def test_inline_images + to_test = { + '!http://foo.bar/image.jpg!' => '', + 'floating !>http://foo.bar/image.jpg!' => 'floating
    ', + 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class ', + 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style ', + 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title This is a title', + 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title This is a double-quoted "title"', + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_inline_images_inside_tags + raw = <<-RAW +h1. !foo.png! Heading + +Centered image: + +p=. !bar.gif! +RAW + + assert textilizable(raw).include?('') + assert textilizable(raw).include?('') + end + + def test_attached_images + to_test = { + 'Inline image: !logo.gif!' => 'Inline image: This is a logo', + 'Inline image: !logo.GIF!' => 'Inline image: This is a logo', + 'No match: !ogo.gif!' => 'No match: ', + 'No match: !ogo.GIF!' => 'No match: ', + # link image + '!logo.gif!:http://foo.bar/' => 'This is a logo', + } + attachments = Attachment.find(:all) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => attachments) } + end + + def test_attached_images_filename_extension + set_tmp_attachments_directory + a1 = Attachment.new( + :container => Issue.find(1), + :file => mock_file_with_options({:original_filename => "testtest.JPG"}), + :author => User.find(1)) + assert a1.save + assert_equal "testtest.JPG", a1.filename + assert_equal "image/jpeg", a1.content_type + assert a1.image? + + a2 = Attachment.new( + :container => Issue.find(1), + :file => mock_file_with_options({:original_filename => "testtest.jpeg"}), + :author => User.find(1)) + assert a2.save + assert_equal "testtest.jpeg", a2.filename + assert_equal "image/jpeg", a2.content_type + assert a2.image? + + a3 = Attachment.new( + :container => Issue.find(1), + :file => mock_file_with_options({:original_filename => "testtest.JPE"}), + :author => User.find(1)) + assert a3.save + assert_equal "testtest.JPE", a3.filename + assert_equal "image/jpeg", a3.content_type + assert a3.image? + + a4 = Attachment.new( + :container => Issue.find(1), + :file => mock_file_with_options({:original_filename => "Testtest.BMP"}), + :author => User.find(1)) + assert a4.save + assert_equal "Testtest.BMP", a4.filename + assert_equal "image/x-ms-bmp", a4.content_type + assert a4.image? + + to_test = { + 'Inline image: !testtest.jpg!' => + 'Inline image: ', + 'Inline image: !testtest.jpeg!' => + 'Inline image: ', + 'Inline image: !testtest.jpe!' => + 'Inline image: ', + 'Inline image: !testtest.bmp!' => + 'Inline image: ', + } + + attachments = [a1, a2, a3, a4] + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => attachments) } + end + + def test_attached_images_should_read_later + set_fixtures_attachments_directory + a1 = Attachment.find(16) + assert_equal "testfile.png", a1.filename + assert a1.readable? + assert (! a1.visible?(User.anonymous)) + assert a1.visible?(User.find(2)) + a2 = Attachment.find(17) + assert_equal "testfile.PNG", a2.filename + assert a2.readable? + assert (! a2.visible?(User.anonymous)) + assert a2.visible?(User.find(2)) + assert a1.created_on < a2.created_on + + to_test = { + 'Inline image: !testfile.png!' => + 'Inline image: ', + 'Inline image: !Testfile.PNG!' => + 'Inline image: ', + } + attachments = [a1, a2] + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => attachments) } + set_tmp_attachments_directory + end + + def test_textile_external_links + to_test = { + 'This is a "link":http://foo.bar' => 'This is a link', + 'This is an intern "link":/foo/bar' => 'This is an intern link', + '"link (Link title)":http://foo.bar' => 'link', + '"link (Link title with "double-quotes")":http://foo.bar' => 'link', + "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":

    \n\n\n\t

    Another paragraph", + # no multiline link text + "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line
    and another on a second line\":test", + # mailto link + "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "system administrator", + # two exclamation marks + '"a link":http://example.net/path!602815048C7B5C20!302.html' => 'a link', + # escaping + '"test":http://foo"bar' => 'test', + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + if 'ruby'.respond_to?(:encoding) + def test_textile_external_links_with_non_ascii_characters + to_test = { + 'This is a "link":http://foo.bar/теÑÑ‚' => 'This is a link' + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + else + puts 'Skipping test_textile_external_links_with_non_ascii_characters, unsupported ruby version' + end + + def test_redmine_links + issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3}, + :class => 'issue status-1 priority-4 priority-lowest overdue', :title => 'Error 281 when updating a recipe (New)') + note_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'}, + :class => 'issue status-1 priority-4 priority-lowest overdue', :title => 'Error 281 when updating a recipe (New)') + + changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1}, + :class => 'changeset', :title => 'My very first commit') + changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, + :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') + + document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1}, + :class => 'document') + + version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2}, + :class => 'version') + + board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'} + + message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4} + + news_url = {:controller => 'news', :action => 'show', :id => 1} + + project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'} + + source_url = '/projects/ecookbook/repository/entry/some/file' + source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file' + source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext' + source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext' + + export_url = '/projects/ecookbook/repository/raw/some/file' + export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file' + export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext' + export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext' + + to_test = { + # tickets + '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.", + # ticket notes + '#3-14' => note_link, + '#3#note-14' => note_link, + # should not ignore leading zero + '#03' => '#03', + # changesets + 'r1' => changeset_link, + 'r1.' => "#{changeset_link}.", + 'r1, r2' => "#{changeset_link}, #{changeset_link2}", + 'r1,r2' => "#{changeset_link},#{changeset_link2}", + # documents + 'document#1' => document_link, + 'document:"Test document"' => document_link, + # versions + 'version#2' => version_link, + 'version:1.0' => version_link, + 'version:"1.0"' => version_link, + # source + 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'), + 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'), + 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".", + 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".", + 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".", + 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".", + 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",", + 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'), + 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'), + 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'), + 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'), + 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'), + # export + 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'), + 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'), + 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'), + 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'), + # forum + 'forum#2' => link_to('Discussion', board_url, :class => 'board'), + 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'), + # message + 'message#4' => link_to('Post 2', message_url, :class => 'message'), + 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'), + # news + 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'), + 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'), + # project + 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'), + 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'), + 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'), + # not found + '#0123456789' => '#0123456789', + # invalid expressions + 'source:' => 'source:', + # url hash + "http://foo.bar/FAQ#3" => 'http://foo.bar/FAQ#3', + } + @project = Project.find(1) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text), "#{text} failed" } + end + + def test_redmine_links_with_a_different_project_before_current_project + vp1 = Version.generate!(:project_id => 1, :name => '1.4.4') + vp3 = Version.generate!(:project_id => 3, :name => '1.4.4') + + @project = Project.find(3) + assert_equal %(

    1.4.4 1.4.4

    ), + textilizable("ecookbook:version:1.4.4 version:1.4.4") + end + + def test_escaped_redmine_links_should_not_be_parsed + to_test = [ + '#3.', + '#3-14.', + '#3#-note14.', + 'r1', + 'document#1', + 'document:"Test document"', + 'version#2', + 'version:1.0', + 'version:"1.0"', + 'source:/some/file' + ] + @project = Project.find(1) + to_test.each { |text| assert_equal "

    #{text}

    ", textilizable("!" + text), "#{text} failed" } + end + + def test_cross_project_redmine_links + source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, + :class => 'source') + + changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, + :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') + + to_test = { + # documents + 'document:"Test document"' => 'document:"Test document"', + 'ecookbook:document:"Test document"' => 'Test document', + 'invalid:document:"Test document"' => 'invalid:document:"Test document"', + # versions + 'version:"1.0"' => 'version:"1.0"', + 'ecookbook:version:"1.0"' => '1.0', + 'invalid:version:"1.0"' => 'invalid:version:"1.0"', + # changeset + 'r2' => 'r2', + 'ecookbook:r2' => changeset_link, + 'invalid:r2' => 'invalid:r2', + # source + 'source:/some/file' => 'source:/some/file', + 'ecookbook:source:/some/file' => source_link, + 'invalid:source:/some/file' => 'invalid:source:/some/file', + } + @project = Project.find(3) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text), "#{text} failed" } + end + + def test_multiple_repositories_redmine_links + svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg') + Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123') + hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg') + Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd') + + changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, + :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') + svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123}, + :class => 'changeset', :title => '') + hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'}, + :class => 'changeset', :title => '') + + source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source') + hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source') + + to_test = { + 'r2' => changeset_link, + 'svn_repo-1|r123' => svn_changeset_link, + 'invalid|r123' => 'invalid|r123', + 'commit:hg1|abcd' => hg_changeset_link, + 'commit:invalid|abcd' => 'commit:invalid|abcd', + # source + 'source:some/file' => source_link, + 'source:hg1|some/file' => hg_source_link, + 'source:invalid|some/file' => 'source:invalid|some/file', + } + + @project = Project.find(1) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text), "#{text} failed" } + end + + def test_cross_project_multiple_repositories_redmine_links + svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg') + Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123') + hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg') + Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd') + + changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, + :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') + svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123}, + :class => 'changeset', :title => '') + hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'}, + :class => 'changeset', :title => '') + + source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source') + hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source') + + to_test = { + 'ecookbook:r2' => changeset_link, + 'ecookbook:svn1|r123' => svn_changeset_link, + 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123', + 'ecookbook:commit:hg1|abcd' => hg_changeset_link, + 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd', + 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd', + # source + 'ecookbook:source:some/file' => source_link, + 'ecookbook:source:hg1|some/file' => hg_source_link, + 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file', + 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file', + } + + @project = Project.find(3) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text), "#{text} failed" } + end + + def test_redmine_links_git_commit + changeset_link = link_to('abcd', + { + :controller => 'repositories', + :action => 'revision', + :id => 'subproject1', + :rev => 'abcd', + }, + :class => 'changeset', :title => 'test commit') + to_test = { + 'commit:abcd' => changeset_link, + } + @project = Project.find(3) + r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git') + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => 'abcd', + :scmid => 'abcd', + :comments => 'test commit') + assert( c.save ) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'. + def test_redmine_links_darcs_commit + changeset_link = link_to('20080308225258-98289-abcd456efg.gz', + { + :controller => 'repositories', + :action => 'revision', + :id => 'subproject1', + :rev => '123', + }, + :class => 'changeset', :title => 'test commit') + to_test = { + 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link, + } + @project = Project.find(3) + r = Repository::Darcs.create!( + :project => @project, :url => '/tmp/test/darcs', + :log_encoding => 'UTF-8') + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => '20080308225258-98289-abcd456efg.gz', + :comments => 'test commit') + assert( c.save ) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_redmine_links_mercurial_commit + changeset_link_rev = link_to('r123', + { + :controller => 'repositories', + :action => 'revision', + :id => 'subproject1', + :rev => '123' , + }, + :class => 'changeset', :title => 'test commit') + changeset_link_commit = link_to('abcd', + { + :controller => 'repositories', + :action => 'revision', + :id => 'subproject1', + :rev => 'abcd' , + }, + :class => 'changeset', :title => 'test commit') + to_test = { + 'r123' => changeset_link_rev, + 'commit:abcd' => changeset_link_commit, + } + @project = Project.find(3) + r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test') + assert r + c = Changeset.new(:repository => r, + :committed_on => Time.now, + :revision => '123', + :scmid => 'abcd', + :comments => 'test commit') + assert( c.save ) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_attachment_links + attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment') + to_test = { + 'attachment:error281.txt' => attachment_link + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" } + end + + def test_attachment_link_should_link_to_latest_attachment + set_tmp_attachments_directory + a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago) + a2 = Attachment.generate!(:filename => "test.txt") + + assert_equal %(

    test.txt

    ), + textilizable('attachment:test.txt', :attachments => [a1, a2]) + end + + def test_wiki_links + to_test = { + '[[CookBook documentation]]' => 'CookBook documentation', + '[[Another page|Page]]' => 'Page', + # title content should be formatted + '[[Another page|With _styled_ *title*]]' => 'With styled title', + '[[Another page|With title containing HTML entities & markups]]' => 'With title containing <strong>HTML entities & markups</strong>', + # link with anchor + '[[CookBook documentation#One-section]]' => 'CookBook documentation', + '[[Another page#anchor|Page]]' => 'Page', + # UTF8 anchor + '[[Another_page#ТеÑÑ‚|ТеÑÑ‚]]' => %|ТеÑÑ‚|, + # page that doesn't exist + '[[Unknown page]]' => 'Unknown page', + '[[Unknown page|404]]' => '404', + # link to another project wiki + '[[onlinestore:]]' => 'onlinestore', + '[[onlinestore:|Wiki]]' => 'Wiki', + '[[onlinestore:Start page]]' => 'Start page', + '[[onlinestore:Start page|Text]]' => 'Text', + '[[onlinestore:Unknown page]]' => 'Unknown page', + # striked through link + '-[[Another page|Page]]-' => 'Page', + '-[[Another page|Page]] link-' => 'Page link', + # escaping + '![[Another page|Page]]' => '[[Another page|Page]]', + # project does not exist + '[[unknowproject:Start]]' => '[[unknowproject:Start]]', + '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]', + } + + @project = Project.find(1) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_wiki_links_within_local_file_generation_context + + to_test = { + # link to a page + '[[CookBook documentation]]' => 'CookBook documentation', + '[[CookBook documentation|documentation]]' => 'documentation', + '[[CookBook documentation#One-section]]' => 'CookBook documentation', + '[[CookBook documentation#One-section|documentation]]' => 'documentation', + # page that doesn't exist + '[[Unknown page]]' => 'Unknown page', + '[[Unknown page|404]]' => '404', + '[[Unknown page#anchor]]' => 'Unknown page', + '[[Unknown page#anchor|404]]' => '404', + } + + @project = Project.find(1) + + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :wiki_links => :local) } + end + + def test_wiki_links_within_wiki_page_context + + page = WikiPage.find_by_title('Another_page' ) + + to_test = { + # link to another page + '[[CookBook documentation]]' => 'CookBook documentation', + '[[CookBook documentation|documentation]]' => 'documentation', + '[[CookBook documentation#One-section]]' => 'CookBook documentation', + '[[CookBook documentation#One-section|documentation]]' => 'documentation', + # link to the current page + '[[Another page]]' => 'Another page', + '[[Another page|Page]]' => 'Page', + '[[Another page#anchor]]' => 'Another page', + '[[Another page#anchor|Page]]' => 'Page', + # page that doesn't exist + '[[Unknown page]]' => 'Unknown page', + '[[Unknown page|404]]' => '404', + '[[Unknown page#anchor]]' => 'Unknown page', + '[[Unknown page#anchor|404]]' => '404', + } + + @project = Project.find(1) + + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(WikiContent.new( :text => text, :page => page ), :text) } + end + + def test_wiki_links_anchor_option_should_prepend_page_title_to_href + + to_test = { + # link to a page + '[[CookBook documentation]]' => 'CookBook documentation', + '[[CookBook documentation|documentation]]' => 'documentation', + '[[CookBook documentation#One-section]]' => 'CookBook documentation', + '[[CookBook documentation#One-section|documentation]]' => 'documentation', + # page that doesn't exist + '[[Unknown page]]' => 'Unknown page', + '[[Unknown page|404]]' => '404', + '[[Unknown page#anchor]]' => 'Unknown page', + '[[Unknown page#anchor|404]]' => '404', + } + + @project = Project.find(1) + + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text, :wiki_links => :anchor) } + end + + def test_html_tags + to_test = { + "
    content
    " => "

    <div>content</div>

    ", + "
    content
    " => "

    <div class=\"bold\">content</div>

    ", + "" => "

    <script>some script;</script>

    ", + # do not escape pre/code tags + "
    \nline 1\nline2
    " => "
    \nline 1\nline2
    ", + "
    \nline 1\nline2
    " => "
    \nline 1\nline2
    ", + "
    content
    " => "
    <div>content</div>
    ", + "HTML comment: " => "

    HTML comment: <!-- no comments -->

    ", + " Status masalah").' + error_can_not_reopen_issue_on_closed_version: 'Masalah yang ditujukan pada versi tertutup tidak bisa dibuka kembali' + error_can_not_archive_project: Proyek ini tidak bisa diarsipkan + + warning_attachments_not_saved: "%{count} berkas tidak bisa disimpan." + + mail_subject_lost_password: "Kata sandi %{value} anda" + mail_body_lost_password: 'Untuk mengubah kata sandi anda, klik tautan berikut::' + mail_subject_register: "Aktivasi akun %{value} anda" + mail_body_register: 'Untuk mengaktifkan akun anda, klik tautan berikut:' + mail_body_account_information_external: "Anda dapat menggunakan akun %{value} anda untuk login." + mail_body_account_information: Informasi akun anda + mail_subject_account_activation_request: "Permintaan aktivasi akun %{value} " + mail_body_account_activation_request: "Pengguna baru (%{value}) sudan didaftarkan. Akun tersebut menunggu persetujuan anda:" + mail_subject_reminder: "%{count} masalah harus selesai pada hari berikutnya (%{days})" + mail_body_reminder: "%{count} masalah yang ditugaskan pada anda harus selesai dalam %{days} hari kedepan:" + mail_subject_wiki_content_added: "'%{id}' halaman wiki sudah ditambahkan" + mail_body_wiki_content_added: "The '%{id}' halaman wiki sudah ditambahkan oleh %{author}." + mail_subject_wiki_content_updated: "'%{id}' halaman wiki sudah diperbarui" + mail_body_wiki_content_updated: "The '%{id}' halaman wiki sudah diperbarui oleh %{author}." + + gui_validation_error: 1 kesalahan + gui_validation_error_plural: "%{count} kesalahan" + + field_name: Nama + field_description: Deskripsi + field_summary: Ringkasan + field_is_required: Dibutuhkan + field_firstname: Nama depan + field_lastname: Nama belakang + field_mail: Email + field_filename: Berkas + field_filesize: Ukuran + field_downloads: Unduhan + field_author: Pengarang + field_created_on: Dibuat + field_updated_on: Diperbarui + field_field_format: Format + field_is_for_all: Untuk semua proyek + field_possible_values: Nilai yang mungkin + field_regexp: Regular expression + field_min_length: Panjang minimum + field_max_length: Panjang maksimum + field_value: Nilai + field_category: Kategori + field_title: Judul + field_project: Proyek + field_issue: Masalah + field_status: Status + field_notes: Catatan + field_is_closed: Masalah ditutup + field_is_default: Nilai default + field_tracker: Pelacak + field_subject: Perihal + field_due_date: Harus selesai + field_assigned_to: Ditugaskan ke + field_priority: Prioritas + field_fixed_version: Versi target + field_user: Pengguna + field_role: Peran + field_homepage: Halaman web + field_is_public: Publik + field_parent: Subproyek dari + field_is_in_roadmap: Masalah ditampilkan di rencana kerja + field_login: Login + field_mail_notification: Notifikasi email + field_admin: Administrator + field_last_login_on: Terakhir login + field_language: Bahasa + field_effective_date: Tanggal + field_password: Kata sandi + field_new_password: Kata sandi baru + field_password_confirmation: Konfirmasi + field_version: Versi + field_type: Tipe + field_host: Host + field_port: Port + field_account: Akun + field_base_dn: Base DN + field_attr_login: Atribut login + field_attr_firstname: Atribut nama depan + field_attr_lastname: Atribut nama belakang + field_attr_mail: Atribut email + field_onthefly: Pembuatan pengguna seketika + field_start_date: Mulai + field_done_ratio: "% Selesai" + field_auth_source: Mode otentikasi + field_hide_mail: Sembunyikan email saya + field_comments: Komentar + field_url: URL + field_start_page: Halaman awal + field_subproject: Subproyek + field_hours: Jam + field_activity: Kegiatan + field_spent_on: Tanggal + field_identifier: Pengenal + field_is_filter: Digunakan sebagai penyaring + field_issue_to: Masalah terkait + field_delay: Tertunday + field_assignable: Masalah dapat ditugaskan pada peran ini + field_redirect_existing_links: Alihkan tautan yang ada + field_estimated_hours: Perkiraan waktu + field_column_names: Kolom + field_time_zone: Zona waktu + field_searchable: Dapat dicari + field_default_value: Nilai default + field_comments_sorting: Tampilkan komentar + field_parent_title: Halaman induk + field_editable: Dapat disunting + field_watcher: Pemantau + field_identity_url: OpenID URL + field_content: Isi + field_group_by: Dikelompokkan berdasar + field_sharing: Berbagi + + setting_app_title: Judul aplikasi + setting_app_subtitle: Subjudul aplikasi + setting_welcome_text: Teks sambutan + setting_default_language: Bahasa Default + setting_login_required: Butuhkan otentikasi + setting_self_registration: Swa-pendaftaran + setting_attachment_max_size: Ukuran maksimum untuk lampiran + setting_issues_export_limit: Batasan ukuran export masalah + setting_mail_from: Emisi alamat email + setting_bcc_recipients: Blind carbon copy recipients (bcc) + setting_plain_text_mail: Plain text mail (no HTML) + setting_host_name: Nama host dan path + setting_text_formatting: Format teks + setting_wiki_compression: Kompresi untuk riwayat wiki + setting_feeds_limit: Batasan isi feed + setting_default_projects_public: Proyek baru defaultnya adalah publik + setting_autofetch_changesets: Autofetch commits + setting_sys_api_enabled: Aktifkan WS untuk pengaturan repositori + setting_commit_ref_keywords: Referensi kaca kunci + setting_commit_fix_keywords: Pembetulan kaca kunci + setting_autologin: Autologin + setting_date_format: Format tanggal + setting_time_format: Format waktu + setting_cross_project_issue_relations: Perbolehkan kaitan masalah proyek berbeda + setting_issue_list_default_columns: Kolom default ditampilkan di daftar masalah + setting_emails_footer: Footer untuk email + setting_protocol: Protokol + setting_per_page_options: Pilihan obyek per halaman + setting_user_format: Format tampilan untuk pengguna + setting_activity_days_default: Hari tertampil pada kegiatan proyek + setting_display_subprojects_issues: Secara default, tampilkan masalah subproyek pada proyek utama + setting_enabled_scm: Enabled SCM + setting_mail_handler_api_enabled: Enable WS for incoming emails + setting_mail_handler_api_key: API key + setting_sequential_project_identifiers: Buat pengenal proyek terurut + setting_gravatar_enabled: Gunakan icon pengguna dari Gravatar + setting_gravatar_default: Gambar default untuk Gravatar + setting_diff_max_lines_displayed: Maksimum perbedaan baris tertampil + setting_file_max_size_displayed: Maksimum berkas tertampil secara inline + setting_repository_log_display_limit: Nilai maksimum dari revisi ditampilkan di log berkas + setting_openid: Perbolehkan Login dan pendaftaran melalui OpenID + setting_password_min_length: Panjang minimum untuk kata sandi + setting_new_project_user_role_id: Peran diberikan pada pengguna non-admin yang membuat proyek + setting_default_projects_modules: Modul yang diaktifkan pada proyek baru + + permission_add_project: Tambahkan proyek + permission_edit_project: Sunting proyek + permission_select_project_modules: Pilih modul proyek + permission_manage_members: Atur anggota + permission_manage_versions: Atur versi + permission_manage_categories: Atur kategori masalah + permission_add_issues: Tambahkan masalah + permission_edit_issues: Sunting masalah + permission_manage_issue_relations: Atur kaitan masalah + permission_add_issue_notes: Tambahkan catatan + permission_edit_issue_notes: Sunting catatan + permission_edit_own_issue_notes: Sunting catatan saya + permission_move_issues: Pindahkan masalah + permission_delete_issues: Hapus masalah + permission_manage_public_queries: Atur query publik + permission_save_queries: Simpan query + permission_view_gantt: Tampilkan gantt chart + permission_view_calendar: Tampilkan kalender + permission_view_issue_watchers: Tampilkan daftar pemantau + permission_add_issue_watchers: Tambahkan pemantau + permission_delete_issue_watchers: Hapus pemantau + permission_log_time: Log waktu terpakai + permission_view_time_entries: Tampilkan waktu terpakai + permission_edit_time_entries: Sunting catatan waktu + permission_edit_own_time_entries: Sunting catatan waktu saya + permission_manage_news: Atur berita + permission_comment_news: Komentari berita + permission_manage_documents: Atur dokumen + permission_view_documents: Tampilkan dokumen + permission_manage_files: Atur berkas + permission_view_files: Tampilkan berkas + permission_manage_wiki: Atur wiki + permission_rename_wiki_pages: Ganti nama halaman wiki + permission_delete_wiki_pages: Hapus halaman wiki + permission_view_wiki_pages: Tampilkan wiki + permission_view_wiki_edits: Tampilkan riwayat wiki + permission_edit_wiki_pages: Sunting halaman wiki + permission_delete_wiki_pages_attachments: Hapus lampiran + permission_protect_wiki_pages: Proteksi halaman wiki + permission_manage_repository: Atur repositori + permission_browse_repository: Jelajah repositori + permission_view_changesets: Tampilkan set perubahan + permission_commit_access: Commit akses + permission_manage_boards: Atur forum + permission_view_messages: Tampilkan pesan + permission_add_messages: Tambahkan pesan + permission_edit_messages: Sunting pesan + permission_edit_own_messages: Sunting pesan saya + permission_delete_messages: Hapus pesan + permission_delete_own_messages: Hapus pesan saya + + project_module_issue_tracking: Pelacak masalah + project_module_time_tracking: Pelacak waktu + project_module_news: Berita + project_module_documents: Dokumen + project_module_files: Berkas + project_module_wiki: Wiki + project_module_repository: Repositori + project_module_boards: Forum + + label_user: Pengguna + label_user_plural: Pengguna + label_user_new: Pengguna baru + label_user_anonymous: Anonymous + label_project: Proyek + label_project_new: Proyek baru + label_project_plural: Proyek + label_x_projects: + zero: tidak ada proyek + one: 1 proyek + other: "%{count} proyek" + label_project_all: Semua Proyek + label_project_latest: Proyek terakhir + label_issue: Masalah + label_issue_new: Masalah baru + label_issue_plural: Masalah + label_issue_view_all: tampilkan semua masalah + label_issues_by: "Masalah ditambahkan oleh %{value}" + label_issue_added: Masalah ditambahan + label_issue_updated: Masalah diperbarui + label_document: Dokumen + label_document_new: Dokumen baru + label_document_plural: Dokumen + label_document_added: Dokumen ditambahkan + label_role: Peran + label_role_plural: Peran + label_role_new: Peran baru + label_role_and_permissions: Peran dan perijinan + label_member: Anggota + label_member_new: Anggota baru + label_member_plural: Anggota + label_tracker: Pelacak + label_tracker_plural: Pelacak + label_tracker_new: Pelacak baru + label_workflow: Alur kerja + label_issue_status: Status masalah + label_issue_status_plural: Status masalah + label_issue_status_new: Status baru + label_issue_category: Kategori masalah + label_issue_category_plural: Kategori masalah + label_issue_category_new: Kategori baru + label_custom_field: Field kustom + label_custom_field_plural: Field kustom + label_custom_field_new: Field kustom + label_enumerations: Enumerasi + label_enumeration_new: Buat baru + label_information: Informasi + label_information_plural: Informasi + label_please_login: Silakan login + label_register: mendaftar + label_login_with_open_id_option: atau login menggunakan OpenID + label_password_lost: Lupa password + label_home: Halaman depan + label_my_page: Beranda + label_my_account: Akun saya + label_my_projects: Proyek saya + label_administration: Administrasi + label_login: Login + label_logout: Keluar + label_help: Bantuan + label_reported_issues: Masalah terlapor + label_assigned_to_me_issues: Masalah yang ditugaskan pada saya + label_last_login: Terakhir login + label_registered_on: Terdaftar pada + label_activity: Kegiatan + label_overall_activity: Kegiatan umum + label_user_activity: "kegiatan %{value}" + label_new: Baru + label_logged_as: Login sebagai + label_environment: Lingkungan + label_authentication: Otentikasi + label_auth_source: Mode Otentikasi + label_auth_source_new: Mode otentikasi baru + label_auth_source_plural: Mode Otentikasi + label_subproject_plural: Subproyek + label_and_its_subprojects: "%{value} dan subproyeknya" + label_min_max_length: Panjang Min - Maks + label_list: Daftar + label_date: Tanggal + label_integer: Integer + label_float: Float + label_boolean: Boolean + label_string: Text + label_text: Long text + label_attribute: Atribut + label_attribute_plural: Atribut + label_download: "%{count} Unduhan" + label_download_plural: "%{count} Unduhan" + label_no_data: Tidak ada data untuk ditampilkan + label_change_status: Status perubahan + label_history: Riwayat + label_attachment: Berkas + label_attachment_new: Berkas baru + label_attachment_delete: Hapus Berkas + label_attachment_plural: Berkas + label_file_added: Berkas ditambahkan + label_report: Laporan + label_report_plural: Laporan + label_news: Berita + label_news_new: Tambahkan berita + label_news_plural: Berita + label_news_latest: Berita terakhir + label_news_view_all: Tampilkan semua berita + label_news_added: Berita ditambahkan + label_settings: Pengaturan + label_overview: Umum + label_version: Versi + label_version_new: Versi baru + label_version_plural: Versi + label_confirmation: Konfirmasi + label_export_to: 'Juga tersedia dalam:' + label_read: Baca... + label_public_projects: Proyek publik + label_open_issues: belum selesai + label_open_issues_plural: belum selesai + label_closed_issues: selesai + label_closed_issues_plural: selesai + label_x_open_issues_abbr_on_total: + zero: 0 belum selesai / %{total} + one: 1 belum selesai / %{total} + other: "%{count} terbuka / %{total}" + label_x_open_issues_abbr: + zero: 0 belum selesai + one: 1 belum selesai + other: "%{count} belum selesai" + label_x_closed_issues_abbr: + zero: 0 selesai + one: 1 selesai + other: "%{count} selesai" + label_total: Total + label_permissions: Perijinan + label_current_status: Status sekarang + label_new_statuses_allowed: Status baru yang diijinkan + label_all: semua + label_none: tidak ada + label_nobody: tidak ada + label_next: Berikut + label_previous: Sebelum + label_used_by: Digunakan oleh + label_details: Rincian + label_add_note: Tambahkan catatan + label_per_page: Per halaman + label_calendar: Kalender + label_months_from: dari bulan + label_gantt: Gantt + label_internal: Internal + label_last_changes: "%{count} perubahan terakhir" + label_change_view_all: Tampilkan semua perubahan + label_personalize_page: Personalkan halaman ini + label_comment: Komentar + label_comment_plural: Komentar + label_x_comments: + zero: tak ada komentar + one: 1 komentar + other: "%{count} komentar" + label_comment_add: Tambahkan komentar + label_comment_added: Komentar ditambahkan + label_comment_delete: Hapus komentar + label_query: Custom query + label_query_plural: Custom queries + label_query_new: Query baru + label_filter_add: Tambahkan filter + label_filter_plural: Filter + label_equals: sama dengan + label_not_equals: tidak sama dengan + label_in_less_than: kurang dari + label_in_more_than: lebih dari + label_greater_or_equal: '>=' + label_less_or_equal: '<=' + label_in: pada + label_today: hari ini + label_all_time: semua waktu + label_yesterday: kemarin + label_this_week: minggu ini + label_last_week: minggu lalu + label_last_n_days: "%{count} hari terakhir" + label_this_month: bulan ini + label_last_month: bulan lalu + label_this_year: this year + label_date_range: Jangkauan tanggal + label_less_than_ago: kurang dari hari yang lalu + label_more_than_ago: lebih dari hari yang lalu + label_ago: hari yang lalu + label_contains: berisi + label_not_contains: tidak berisi + label_day_plural: hari + label_repository: Repositori + label_repository_plural: Repositori + label_browse: Jelajah + label_modification: "%{count} perubahan" + label_modification_plural: "%{count} perubahan" + label_branch: Cabang + label_tag: Tag + label_revision: Revisi + label_revision_plural: Revisi + label_associated_revisions: Revisi terkait + label_added: ditambahkan + label_modified: diubah + label_copied: disalin + label_renamed: diganti nama + label_deleted: dihapus + label_latest_revision: Revisi terakhir + label_latest_revision_plural: Revisi terakhir + label_view_revisions: Tampilkan revisi + label_view_all_revisions: Tampilkan semua revisi + label_max_size: Ukuran maksimum + label_sort_highest: Ke paling atas + label_sort_higher: Ke atas + label_sort_lower: Ke bawah + label_sort_lowest: Ke paling bawah + label_roadmap: Rencana kerja + label_roadmap_due_in: "Harus selesai dalam %{value}" + label_roadmap_overdue: "%{value} terlambat" + label_roadmap_no_issues: Tak ada masalah pada versi ini + label_search: Cari + label_result_plural: Hasil + label_all_words: Semua kata + label_wiki: Wiki + label_wiki_edit: Sunting wiki + label_wiki_edit_plural: Sunting wiki + label_wiki_page: Halaman wiki + label_wiki_page_plural: Halaman wiki + label_index_by_title: Indeks menurut judul + label_index_by_date: Indeks menurut tanggal + label_current_version: Versi sekarang + label_preview: Tinjauan + label_feed_plural: Feeds + label_changes_details: Rincian semua perubahan + label_issue_tracking: Pelacak masalah + label_spent_time: Waktu terpakai + label_f_hour: "%{value} jam" + label_f_hour_plural: "%{value} jam" + label_time_tracking: Pelacak waktu + label_change_plural: Perubahan + label_statistics: Statistik + label_commits_per_month: Komit per bulan + label_commits_per_author: Komit per pengarang + label_view_diff: Tampilkan perbedaan + label_diff_inline: inline + label_diff_side_by_side: berdampingan + label_options: Pilihan + label_copy_workflow_from: Salin alur kerja dari + label_permissions_report: Laporan perijinan + label_watched_issues: Masalah terpantau + label_related_issues: Masalah terkait + label_applied_status: Status teraplikasi + label_loading: Memuat... + label_relation_new: Kaitan baru + label_relation_delete: Hapus kaitan + label_relates_to: terkait pada + label_duplicates: salinan + label_duplicated_by: disalin oleh + label_blocks: blok + label_blocked_by: diblok oleh + label_precedes: mendahului + label_follows: mengikuti + label_end_to_start: akhir ke awal + label_end_to_end: akhir ke akhir + label_start_to_start: awal ke awal + label_start_to_end: awal ke akhir + label_stay_logged_in: Tetap login + label_disabled: tidak diaktifkan + label_show_completed_versions: Tampilkan versi lengkap + label_me: saya + label_board: Forum + label_board_new: Forum baru + label_board_plural: Forum + label_topic_plural: Topik + label_message_plural: Pesan + label_message_last: Pesan terakhir + label_message_new: Pesan baru + label_message_posted: Pesan ditambahkan + label_reply_plural: Balasan + label_send_information: Kirim informasi akun ke pengguna + label_year: Tahun + label_month: Bulan + label_week: Minggu + label_date_from: Dari + label_date_to: Sampai + label_language_based: Berdasarkan bahasa pengguna + label_sort_by: "Urut berdasarkan %{value}" + label_send_test_email: Kirim email percobaan + label_feeds_access_key_created_on: "RSS access key dibuat %{value} yang lalu" + label_module_plural: Modul + label_added_time_by: "Ditambahkan oleh %{author} %{age} yang lalu" + label_updated_time_by: "Diperbarui oleh %{author} %{age} yang lalu" + label_updated_time: "Diperbarui oleh %{value} yang lalu" + label_jump_to_a_project: Pilih proyek... + label_file_plural: Berkas + label_changeset_plural: Set perubahan + label_default_columns: Kolom default + label_no_change_option: (Tak ada perubahan) + label_bulk_edit_selected_issues: Ubah masalah terpilih secara masal + label_theme: Tema + label_default: Default + label_search_titles_only: Cari judul saja + label_user_mail_option_all: "Untuk semua kejadian pada semua proyek saya" + label_user_mail_option_selected: "Hanya untuk semua kejadian pada proyek yang saya pilih ..." + label_user_mail_no_self_notified: "Saya tak ingin diberitahu untuk perubahan yang saya buat sendiri" + label_user_mail_assigned_only_mail_notification: "Kirim email hanya bila saya ditugaskan untuk masalah terkait" + label_user_mail_block_mail_notification: "Saya tidak ingin menerima email. Terima kasih." + label_registration_activation_by_email: aktivasi akun melalui email + label_registration_manual_activation: aktivasi akun secara manual + label_registration_automatic_activation: aktivasi akun secara otomatis + label_display_per_page: "Per halaman: %{value}" + label_age: Umur + label_change_properties: Rincian perubahan + label_general: Umum + label_more: Lanjut + label_scm: SCM + label_plugins: Plugin + label_ldap_authentication: Otentikasi LDAP + label_downloads_abbr: Unduh + label_optional_description: Deskripsi optional + label_add_another_file: Tambahkan berkas lain + label_preferences: Preferensi + label_chronological_order: Urut sesuai kronologis + label_reverse_chronological_order: Urut dari yang terbaru + label_planning: Perencanaan + label_incoming_emails: Email masuk + label_generate_key: Buat kunci + label_issue_watchers: Pemantau + label_example: Contoh + label_display: Tampilan + label_sort: Urut + label_ascending: Menaik + label_descending: Menurun + label_date_from_to: Dari %{start} sampai %{end} + label_wiki_content_added: Halaman wiki ditambahkan + label_wiki_content_updated: Halaman wiki diperbarui + label_group: Kelompok + label_group_plural: Kelompok + label_group_new: Kelompok baru + label_time_entry_plural: Waktu terpakai + label_version_sharing_none: Tidak dibagi + label_version_sharing_descendants: Dengan subproyek + label_version_sharing_hierarchy: Dengan hirarki proyek + label_version_sharing_tree: Dengan pohon proyek + label_version_sharing_system: Dengan semua proyek + + + button_login: Login + button_submit: Kirim + button_save: Simpan + button_check_all: Contreng semua + button_uncheck_all: Hilangkan semua contreng + button_delete: Hapus + button_create: Buat + button_create_and_continue: Buat dan lanjutkan + button_test: Test + button_edit: Sunting + button_add: Tambahkan + button_change: Ubah + button_apply: Terapkan + button_clear: Bersihkan + button_lock: Kunci + button_unlock: Buka kunci + button_download: Unduh + button_list: Daftar + button_view: Tampilkan + button_move: Pindah + button_move_and_follow: Pindah dan ikuti + button_back: Kembali + button_cancel: Batal + button_activate: Aktifkan + button_sort: Urut + button_log_time: Rekam waktu + button_rollback: Kembali ke versi ini + button_watch: Pantau + button_unwatch: Tidak Memantau + button_reply: Balas + button_archive: Arsip + button_unarchive: Batalkan arsip + button_reset: Reset + button_rename: Ganti nama + button_change_password: Ubah kata sandi + button_copy: Salin + button_copy_and_follow: Salin dan ikuti + button_annotate: Anotasi + button_update: Perbarui + button_configure: Konfigur + button_quote: Kutip + button_duplicate: Duplikat + + status_active: aktif + status_registered: terdaftar + status_locked: terkunci + + version_status_open: terbuka + version_status_locked: terkunci + version_status_closed: tertutup + + field_active: Aktif + + text_select_mail_notifications: Pilih aksi dimana email notifikasi akan dikirimkan. + text_regexp_info: mis. ^[A-Z0-9]+$ + text_min_max_length_info: 0 berarti tidak ada pembatasan + text_project_destroy_confirmation: Apakah anda benar-benar akan menghapus proyek ini beserta data terkait ? + text_subprojects_destroy_warning: "Subproyek: %{value} juga akan dihapus." + text_workflow_edit: Pilih peran dan pelacak untuk menyunting alur kerja + text_are_you_sure: Anda yakin ? + text_journal_changed: "%{label} berubah dari %{old} menjadi %{new}" + text_journal_set_to: "%{label} di set ke %{value}" + text_journal_deleted: "%{label} dihapus (%{old})" + text_journal_added: "%{label} %{value} ditambahkan" + text_tip_issue_begin_day: tugas dimulai hari itu + text_tip_issue_end_day: tugas berakhir hari itu + text_tip_issue_begin_end_day: tugas dimulai dan berakhir hari itu + text_caracters_maximum: "maximum %{count} karakter." + text_caracters_minimum: "Setidaknya harus sepanjang %{count} karakter." + text_length_between: "Panjang diantara %{min} dan %{max} karakter." + text_tracker_no_workflow: Tidak ada alur kerja untuk pelacak ini + text_unallowed_characters: Karakter tidak diperbolehkan + text_comma_separated: Beberapa nilai diperbolehkan (dipisahkan koma). + text_issues_ref_in_commit_messages: Mereferensikan dan membetulkan masalah pada pesan komit + text_issue_added: "Masalah %{id} sudah dilaporkan oleh %{author}." + text_issue_updated: "Masalah %{id} sudah diperbarui oleh %{author}." + text_wiki_destroy_confirmation: Apakah anda benar-benar akan menghapus wiki ini beserta semua isinya ? + text_issue_category_destroy_question: "Beberapa masalah (%{count}) ditugaskan pada kategori ini. Apa yang anda lakukan ?" + text_issue_category_destroy_assignments: Hapus kategori penugasan + text_issue_category_reassign_to: Tugaskan kembali masalah untuk kategori ini + text_user_mail_option: "Untuk proyek yang tidak dipilih, anda hanya akan menerima notifikasi hal-hal yang anda pantau atau anda terlibat di dalamnya (misalnya masalah yang anda tulis atau ditugaskan pada anda)." + text_no_configuration_data: "Peran, pelacak, status masalah dan alur kerja belum dikonfigur.\nSangat disarankan untuk memuat konfigurasi default. Anda akan bisa mengubahnya setelah konfigurasi dimuat." + text_load_default_configuration: Muat konfigurasi default + text_status_changed_by_changeset: "Diterapkan di set perubahan %{value}." + text_issues_destroy_confirmation: 'Apakah anda yakin untuk menghapus masalah terpilih ?' + text_select_project_modules: 'Pilih modul untuk diaktifkan pada proyek ini:' + text_default_administrator_account_changed: Akun administrator default sudah berubah + text_file_repository_writable: Direktori yang bisa ditulisi untuk lampiran + text_plugin_assets_writable: Direktori yang bisa ditulisi untuk plugin asset + text_rmagick_available: RMagick tersedia (optional) + text_destroy_time_entries_question: "%{hours} jam sudah dilaporkan pada masalah yang akan anda hapus. Apa yang akan anda lakukan ?" + text_destroy_time_entries: Hapus jam yang terlapor + text_assign_time_entries_to_project: Tugaskan jam terlapor pada proyek + text_reassign_time_entries: 'Tugaskan kembali jam terlapor pada masalah ini:' + text_user_wrote: "%{value} menulis:" + text_enumeration_destroy_question: "%{count} obyek ditugaskan untuk nilai ini." + text_enumeration_category_reassign_to: 'Tugaskan kembali untuk nilai ini:' + text_email_delivery_not_configured: "Pengiriman email belum dikonfigurasi, notifikasi tidak diaktifkan.\nAnda harus mengkonfigur SMTP server anda pada config/configuration.yml dan restart kembali aplikasi untuk mengaktifkan." + text_repository_usernames_mapping: "Pilih atau perbarui pengguna Redmine yang terpetakan ke setiap nama pengguna yang ditemukan di log repositori.\nPengguna dengan nama pengguna dan repositori atau email yang sama secara otomasit akan dipetakan." + text_diff_truncated: '... Perbedaan terpotong karena melebihi batas maksimum yang bisa ditampilkan.' + text_custom_field_possible_values_info: 'Satu baris untuk setiap nilai' + text_wiki_page_destroy_question: "Halaman ini mempunyai %{descendants} halaman anak dan turunannya. Apa yang akan anda lakukan ?" + text_wiki_page_nullify_children: "Biarkan halaman anak sebagai halaman teratas (root)" + text_wiki_page_destroy_children: "Hapus halaman anak dan semua turunannya" + text_wiki_page_reassign_children: "Tujukan halaman anak ke halaman induk yang ini" + + default_role_manager: Manager + default_role_developer: Pengembang + default_role_reporter: Pelapor + default_tracker_bug: Bug + default_tracker_feature: Fitur + default_tracker_support: Dukungan + default_issue_status_new: Baru + default_issue_status_in_progress: Dalam proses + default_issue_status_resolved: Resolved + default_issue_status_feedback: Umpan balik + default_issue_status_closed: Ditutup + default_issue_status_rejected: Ditolak + default_doc_category_user: Dokumentasi pengguna + default_doc_category_tech: Dokumentasi teknis + default_priority_low: Rendah + default_priority_normal: Normal + default_priority_high: Tinggi + default_priority_urgent: Penting + default_priority_immediate: Segera + default_activity_design: Rancangan + default_activity_development: Pengembangan + + enumeration_issue_priorities: Prioritas masalah + enumeration_doc_categories: Kategori dokumen + enumeration_activities: Kegiatan + enumeration_system_activity: Kegiatan Sistem + label_copy_source: Source + label_update_issue_done_ratios: Update issue done ratios + setting_issue_done_ratio: Calculate the issue done ratio with + label_api_access_key: API access key + text_line_separated: Multiple values allowed (one line for each value). + label_revision_id: Revision %{value} + permission_view_issues: View Issues + setting_issue_done_ratio_issue_status: Use the issue status + error_issue_done_ratios_not_updated: Issue done ratios not updated. + label_display_used_statuses_only: Only display statuses that are used by this tracker + error_workflow_copy_target: Please select target tracker(s) and role(s) + label_api_access_key_created_on: API access key created %{value} ago + label_feeds_access_key: RSS access key + notice_api_access_key_reseted: Your API access key was reset. + setting_rest_api_enabled: Enable REST web service + label_copy_same_as_target: Same as target + button_show: Show + setting_issue_done_ratio_issue_field: Use the issue field + label_missing_api_access_key: Missing an API access key + label_copy_target: Target + label_missing_feeds_access_key: Missing a RSS access key + notice_issue_done_ratios_updated: Issue done ratios updated. + error_workflow_copy_source: Please select a source tracker or role + setting_start_of_week: Start calendars on + setting_mail_handler_body_delimiters: Truncate emails after one of these lines + permission_add_subprojects: Create subprojects + label_subproject_new: New subproject + text_own_membership_delete_confirmation: |- + You are about to remove some or all of your permissions and may no longer be able to edit this project after that. + Are you sure you want to continue? + label_close_versions: Close completed versions + label_board_sticky: Sticky + label_board_locked: Locked + permission_export_wiki_pages: Export wiki pages + setting_cache_formatted_text: Cache formatted text + permission_manage_project_activities: Manage project activities + error_unable_delete_issue_status: Unable to delete issue status + label_profile: Profile + permission_manage_subtasks: Manage subtasks + field_parent_issue: Parent task + label_subtask_plural: Subtasks + label_project_copy_notifications: Send email notifications during the project copy + error_can_not_delete_custom_field: Unable to delete custom field + error_unable_to_connect: Unable to connect (%{value}) + error_can_not_remove_role: This role is in use and can not be deleted. + error_can_not_delete_tracker: This tracker contains issues and can't be deleted. + field_principal: Principal + label_my_page_block: My page block + notice_failed_to_save_members: "Failed to save member(s): %{errors}." + text_zoom_out: Zoom out + text_zoom_in: Zoom in + notice_unable_delete_time_entry: Unable to delete time log entry. + label_overall_spent_time: Overall spent time + field_time_entries: Log time + project_module_gantt: Gantt + project_module_calendar: Calendar + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Text field + label_user_mail_option_only_owner: Only for things I am the owner of + setting_default_notification_option: Default notification option + label_user_mail_option_only_my_events: Only for things I watch or I'm involved in + label_user_mail_option_only_assigned: Only for things I am assigned to + label_user_mail_option_none: No events + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + notice_not_authorized_archived_project: The project you're trying to access has been archived. + label_principal_search: "Search for user or group:" + label_user_search: "Search for user:" + field_visible: Visible + setting_emails_header: Emails header + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Enable time logging + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maximum number of items displayed on the gantt chart + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: Priority updated + label_issues_visibility_own: Issues created by or assigned to the user + field_issues_visibility: Issues visibility + label_issues_visibility_all: All issues + permission_set_own_issues_private: Set own issues public or private + field_is_private: Private + permission_set_issues_private: Set issues public or private + label_issues_visibility_public: All non private issues + text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). + field_commit_logs_encoding: Commit messages encoding + field_scm_path_encoding: Path encoding + text_scm_path_encoding_note: "Default: UTF-8" + field_path_to_repository: Path to repository + field_root_directory: Root directory + field_cvs_module: Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Command + text_scm_command_version: Version + label_git_report_last_commit: Report last commit for files and directories + text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 masalah + one: 1 masalah + other: "%{count} masalah" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: semua + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: Dengan subproyek + label_cross_project_tree: Dengan pohon proyek + label_cross_project_hierarchy: Dengan hirarki proyek + label_cross_project_system: Dengan semua proyek + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/f9/f922d7c0c922dddd808045035ad0f654dfb46fda.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/f9/f922d7c0c922dddd808045035ad0f654dfb46fda.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,56 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Document < ActiveRecord::Base + include Redmine::SafeAttributes + belongs_to :project + belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id" + acts_as_attachable :delete_permission => :manage_documents + + acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project + acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"}, + :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil }, + :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}} + acts_as_activity_provider :find_options => {:include => :project} + + validates_presence_of :project, :title, :category + validates_length_of :title, :maximum => 60 + + scope :visible, lambda {|*args| { :include => :project, + :conditions => Project.allowed_to_condition(args.shift || User.current, :view_documents, *args) } } + + safe_attributes 'category_id', 'title', 'description' + + def visible?(user=User.current) + !user.nil? && user.allowed_to?(:view_documents, project) + end + + def initialize(attributes=nil, *args) + super + if new_record? + self.category ||= DocumentCategory.default + end + end + + def updated_on + unless @updated_on + a = attachments.last + @updated_on = (a && a.created_on) || created_on + end + @updated_on + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/f9/f924ef380c43b9bc6870c2880f294487b8ad3aa8.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/f9/f924ef380c43b9bc6870c2880f294487b8ad3aa8.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,113 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Configuration + + # Configuration default values + @defaults = { + 'email_delivery' => nil + } + + @config = nil + + class << self + # Loads the Redmine configuration file + # Valid options: + # * :file: the configuration file to load (default: config/configuration.yml) + # * :env: the environment to load the configuration for (default: Rails.env) + def load(options={}) + filename = options[:file] || File.join(Rails.root, 'config', 'configuration.yml') + env = options[:env] || Rails.env + + @config = @defaults.dup + + load_deprecated_email_configuration(env) + if File.file?(filename) + @config.merge!(load_from_yaml(filename, env)) + end + + # Compatibility mode for those who copy email.yml over configuration.yml + %w(delivery_method smtp_settings sendmail_settings).each do |key| + if value = @config.delete(key) + @config['email_delivery'] ||= {} + @config['email_delivery'][key] = value + end + end + + if @config['email_delivery'] + ActionMailer::Base.perform_deliveries = true + @config['email_delivery'].each do |k, v| + v.symbolize_keys! if v.respond_to?(:symbolize_keys!) + ActionMailer::Base.send("#{k}=", v) + end + end + + @config + end + + # Returns a configuration setting + def [](name) + load unless @config + @config[name] + end + + # Yields a block with the specified hash configuration settings + def with(settings) + settings.stringify_keys! + load unless @config + was = settings.keys.inject({}) {|h,v| h[v] = @config[v]; h} + @config.merge! settings + yield if block_given? + @config.merge! was + end + + private + + def load_from_yaml(filename, env) + yaml = nil + begin + yaml = YAML::load_file(filename) + rescue ArgumentError + $stderr.puts "Your Redmine configuration file located at #{filename} is not a valid YAML file and could not be loaded." + exit 1 + end + conf = {} + if yaml.is_a?(Hash) + if yaml['default'] + conf.merge!(yaml['default']) + end + if yaml[env] + conf.merge!(yaml[env]) + end + else + $stderr.puts "Your Redmine configuration file located at #{filename} is not a valid Redmine configuration file." + exit 1 + end + conf + end + + def load_deprecated_email_configuration(env) + deprecated_email_conf = File.join(Rails.root, 'config', 'email.yml') + if File.file?(deprecated_email_conf) + warn "Storing outgoing emails configuration in config/email.yml is deprecated. You should now store it in config/configuration.yml using the email_delivery setting." + @config.merge!({'email_delivery' => load_from_yaml(deprecated_email_conf, env)}) + end + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/f9/f997cdb8d5aa036fade9b17162bf4429d398a59e.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/f9/f997cdb8d5aa036fade9b17162bf4429d398a59e.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,31 @@ +<%= link_to(@repository.identifier.present? ? h(@repository.identifier) : 'root', + :action => 'show', :id => @project, + :repository_id => @repository.identifier_param, + :path => nil, :rev => @rev) %> +<% +dirs = path.split('/') +if 'file' == kind + filename = dirs.pop +end +link_path = '' +dirs.each do |dir| + next if dir.blank? + link_path << '/' unless link_path.empty? + link_path << "#{dir}" + %> + / <%= link_to h(dir), :action => 'show', :id => @project, :repository_id => @repository.identifier_param, + :path => to_path_param(link_path), :rev => @rev %> +<% end %> +<% if filename %> + / <%= link_to h(filename), + :action => 'changes', :id => @project, :repository_id => @repository.identifier_param, + :path => to_path_param("#{link_path}/#{filename}"), :rev => @rev %> +<% end %> +<% + # @rev is revsion or Git and Mercurial branch or tag. + # For Mercurial *tip*, @rev and @changeset are nil. + rev_text = @changeset.nil? ? @rev : format_revision(@changeset) +%> +<%= "@ #{h rev_text}" unless rev_text.blank? %> + +<% html_title(with_leading_slash(path)) -%> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/f9/f9cf3cc50cbd2f4a2fd47db432cd42fb7139d0f3.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/f9/f9cf3cc50cbd2f4a2fd47db432cd42fb7139d0f3.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,13 @@ +--- +watchers_001: + watchable_type: Issue + watchable_id: 2 + user_id: 3 +watchers_002: + watchable_type: Message + watchable_id: 1 + user_id: 1 +watchers_003: + watchable_type: Issue + watchable_id: 2 + user_id: 1 diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/f9/f9f416bbc304c7dec0133188327c4e29b538c67d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/f9/f9f416bbc304c7dec0133188327c4e29b538c67d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1086 @@ +# Update to 1.1 by Michal Gebauer +# Updated by Josef LiÅ¡ka +# CZ translation by Maxim KruÅ¡ina | Massimo Filippi, s.r.o. | maxim@mxm.cz +# Based on original CZ translation by Jan KadleÄek +cs: + # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) + direction: ltr + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%Y-%m-%d" + short: "%b %d" + long: "%B %d, %Y" + + day_names: [NedÄ›le, PondÄ›lí, Úterý, StÅ™eda, ÄŒtvrtek, Pátek, Sobota] + abbr_day_names: [Ne, Po, Út, St, ÄŒt, Pá, So] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, Leden, Únor, BÅ™ezen, Duben, KvÄ›ten, ÄŒerven, ÄŒervenec, Srpen, Září, Říjen, Listopad, Prosinec] + abbr_month_names: [~, Led, Úno, BÅ™e, Dub, KvÄ›, ÄŒer, ÄŒec, Srp, Zář, Říj, Lis, Pro] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%a, %d %b %Y %H:%M:%S %z" + time: "%H:%M" + short: "%d %b %H:%M" + long: "%B %d, %Y %H:%M" + am: "dop." + pm: "odp." + + datetime: + distance_in_words: + half_a_minute: "půl minuty" + less_than_x_seconds: + one: "ménÄ› než sekunda" + other: "ménÄ› než %{count} sekund" + x_seconds: + one: "1 sekunda" + other: "%{count} sekund" + less_than_x_minutes: + one: "ménÄ› než minuta" + other: "ménÄ› než %{count} minut" + x_minutes: + one: "1 minuta" + other: "%{count} minut" + about_x_hours: + one: "asi 1 hodina" + other: "asi %{count} hodin" + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "1 den" + other: "%{count} dnů" + about_x_months: + one: "asi 1 mÄ›síc" + other: "asi %{count} mÄ›síců" + x_months: + one: "1 mÄ›síc" + other: "%{count} mÄ›síců" + about_x_years: + one: "asi 1 rok" + other: "asi %{count} let" + over_x_years: + one: "více než 1 rok" + other: "více než %{count} roky" + almost_x_years: + one: "témeÅ™ 1 rok" + other: "téměř %{count} roky" + + number: + # Výchozí formát pro Äísla + format: + separator: "." + delimiter: "" + precision: 3 + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: "Bajt" + other: "Bajtů" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + +# Used in array.to_sentence. + support: + array: + sentence_connector: "a" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 chyba zabránila uložení %{model}" + other: "%{count} chyb zabránilo uložení %{model}" + messages: + inclusion: "není zahrnuto v seznamu" + exclusion: "je rezervováno" + invalid: "je neplatné" + confirmation: "se neshoduje s potvrzením" + accepted: "musí být akceptováno" + empty: "nemůže být prázdný" + blank: "nemůže být prázdný" + too_long: "je příliÅ¡ dlouhý" + too_short: "je příliÅ¡ krátký" + wrong_length: "má chybnou délku" + taken: "je již použito" + not_a_number: "není Äíslo" + not_a_date: "není platné datum" + greater_than: "musí být vÄ›tší než %{count}" + greater_than_or_equal_to: "musí být vÄ›tší nebo rovno %{count}" + equal_to: "musí být pÅ™esnÄ› %{count}" + less_than: "musí být ménÄ› než %{count}" + less_than_or_equal_to: "musí být ménÄ› nebo rovno %{count}" + odd: "musí být liché" + even: "musí být sudé" + greater_than_start_date: "musí být vÄ›tší než poÄáteÄní datum" + not_same_project: "nepatří stejnému projektu" + circular_dependency: "Tento vztah by vytvoÅ™il cyklickou závislost" + cant_link_an_issue_with_a_descendant: "Úkol nemůže být spojen s jedním z jeho dílÄích úkolů" + + actionview_instancetag_blank_option: Prosím vyberte + + general_text_No: 'Ne' + general_text_Yes: 'Ano' + general_text_no: 'ne' + general_text_yes: 'ano' + general_lang_name: 'Czech (ÄŒeÅ¡tina)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: ÚÄet byl úspěšnÄ› zmÄ›nÄ›n. + notice_account_invalid_creditentials: Chybné jméno nebo heslo + notice_account_password_updated: Heslo bylo úspěšnÄ› zmÄ›nÄ›no. + notice_account_wrong_password: Chybné heslo + notice_account_register_done: ÚÄet byl úspěšnÄ› vytvoÅ™en. Pro aktivaci úÄtu kliknÄ›te na odkaz v emailu, který vám byl zaslán. + notice_account_unknown_email: Neznámý uživatel. + notice_can_t_change_password: Tento úÄet používá externí autentifikaci. Zde heslo zmÄ›nit nemůžete. + notice_account_lost_email_sent: Byl vám zaslán email s intrukcemi jak si nastavíte nové heslo. + notice_account_activated: Váš úÄet byl aktivován. Nyní se můžete pÅ™ihlásit. + notice_successful_create: ÚspěšnÄ› vytvoÅ™eno. + notice_successful_update: ÚspěšnÄ› aktualizováno. + notice_successful_delete: ÚspěšnÄ› odstranÄ›no. + notice_successful_connection: Úspěšné pÅ™ipojení. + notice_file_not_found: Stránka na kterou se snažíte zobrazit neexistuje nebo byla smazána. + notice_locking_conflict: Údaje byly zmÄ›nÄ›ny jiným uživatelem. + notice_not_authorized: Nemáte dostateÄná práva pro zobrazení této stránky. + notice_not_authorized_archived_project: Projekt ke kterému se snažíte pÅ™istupovat byl archivován. + notice_email_sent: "Na adresu %{value} byl odeslán email" + notice_email_error: "PÅ™i odesílání emailu nastala chyba (%{value})" + notice_feeds_access_key_reseted: Váš klÃ­Ä pro přístup k RSS byl resetován. + notice_api_access_key_reseted: Váš API přístupový klÃ­Ä byl resetován. + notice_failed_to_save_issues: "Chyba pÅ™i uložení %{count} úkolu(ů) z %{total} vybraných: %{ids}." + notice_failed_to_save_members: "NepodaÅ™ilo se uložit Älena(y): %{errors}." + notice_no_issue_selected: "Nebyl zvolen žádný úkol. Prosím, zvolte úkoly, které chcete editovat" + notice_account_pending: "Váš úÄet byl vytvoÅ™en, nyní Äeká na schválení administrátorem." + notice_default_data_loaded: Výchozí konfigurace úspěšnÄ› nahrána. + notice_unable_delete_version: Nemohu odstanit verzi + notice_unable_delete_time_entry: Nelze smazat Äas ze záznamu. + notice_issue_done_ratios_updated: Koeficienty dokonÄení úkolu byly aktualizovány. + notice_gantt_chart_truncated: Graf byl oříznut, poÄet položek pÅ™esáhl limit pro zobrazení (%{max}) + + error_can_t_load_default_data: "Výchozí konfigurace nebyla nahrána: %{value}" + error_scm_not_found: "Položka a/nebo revize neexistují v repozitáři." + error_scm_command_failed: "PÅ™i pokusu o přístup k repozitáři doÅ¡lo k chybÄ›: %{value}" + error_scm_annotate: "Položka neexistuje nebo nemůže být komentována." + error_issue_not_found_in_project: 'Úkol nebyl nalezen nebo nepatří k tomuto projektu' + error_no_tracker_in_project: Žádná fronta nebyla pÅ™iÅ™azena tomuto projektu. Prosím zkontroluje nastavení projektu. + error_no_default_issue_status: Není nastaven výchozí stav úkolu. Prosím zkontrolujte nastavení ("Administrace -> Stavy úkolů"). + error_can_not_delete_custom_field: Nelze smazat volitelné pole + error_can_not_delete_tracker: Tato fronta obsahuje úkoly a nemůže být smazán. + error_can_not_remove_role: Tato role je právÄ› používaná a nelze ji smazat. + error_can_not_reopen_issue_on_closed_version: Úkol pÅ™iÅ™azený k uzavÅ™ené verzi nemůže být znovu otevÅ™en + error_can_not_archive_project: Tento projekt nemůže být archivován + error_issue_done_ratios_not_updated: Koeficient dokonÄení úkolu nebyl aktualizován. + error_workflow_copy_source: Prosím vyberte zdrojovou frontu nebo roly + error_workflow_copy_target: Prosím vyberte cílovou frontu(y) a roly(e) + error_unable_delete_issue_status: Nelze smazat stavy úkolů + error_unable_to_connect: Nelze se pÅ™ipojit (%{value}) + warning_attachments_not_saved: "%{count} soubor(ů) nebylo možné uložit." + + mail_subject_lost_password: "VaÅ¡e heslo (%{value})" + mail_body_lost_password: 'Pro zmÄ›nu vaÅ¡eho hesla kliknÄ›te na následující odkaz:' + mail_subject_register: "Aktivace úÄtu (%{value})" + mail_body_register: 'Pro aktivaci vaÅ¡eho úÄtu kliknÄ›te na následující odkaz:' + mail_body_account_information_external: "Pomocí vaÅ¡eho úÄtu %{value} se můžete pÅ™ihlásit." + mail_body_account_information: Informace o vaÅ¡em úÄtu + mail_subject_account_activation_request: "Aktivace %{value} úÄtu" + mail_body_account_activation_request: "Byl zaregistrován nový uživatel %{value}. Aktivace jeho úÄtu závisí na vaÅ¡em potvrzení." + mail_subject_reminder: "%{count} úkol(ů) má termín bÄ›hem nÄ›kolik dní (%{days})" + mail_body_reminder: "%{count} úkol(ů), které máte pÅ™iÅ™azeny má termín bÄ›hem nÄ›kolik dní (%{days}):" + mail_subject_wiki_content_added: "'%{id}' Wiki stránka byla pÅ™idána" + mail_body_wiki_content_added: "'%{id}' Wiki stránka byla pÅ™idána od %{author}." + mail_subject_wiki_content_updated: "'%{id}' Wiki stránka byla aktualizována" + mail_body_wiki_content_updated: "'%{id}' Wiki stránka byla aktualizována od %{author}." + + gui_validation_error: 1 chyba + gui_validation_error_plural: "%{count} chyb(y)" + + field_name: Název + field_description: Popis + field_summary: PÅ™ehled + field_is_required: Povinné pole + field_firstname: Jméno + field_lastname: Příjmení + field_mail: Email + field_filename: Soubor + field_filesize: Velikost + field_downloads: Staženo + field_author: Autor + field_created_on: VytvoÅ™eno + field_updated_on: Aktualizováno + field_field_format: Formát + field_is_for_all: Pro vÅ¡echny projekty + field_possible_values: Možné hodnoty + field_regexp: Regulární výraz + field_min_length: Minimální délka + field_max_length: Maximální délka + field_value: Hodnota + field_category: Kategorie + field_title: Název + field_project: Projekt + field_issue: Úkol + field_status: Stav + field_notes: Poznámka + field_is_closed: Úkol uzavÅ™en + field_is_default: Výchozí stav + field_tracker: Fronta + field_subject: PÅ™edmÄ›t + field_due_date: Uzavřít do + field_assigned_to: PÅ™iÅ™azeno + field_priority: Priorita + field_fixed_version: Cílová verze + field_user: Uživatel + field_principal: Hlavní + field_role: Role + field_homepage: Domovská stránka + field_is_public: VeÅ™ejný + field_parent: NadÅ™azený projekt + field_is_in_roadmap: Úkoly zobrazené v plánu + field_login: PÅ™ihlášení + field_mail_notification: Emailová oznámení + field_admin: Administrátor + field_last_login_on: Poslední pÅ™ihlášení + field_language: Jazyk + field_effective_date: Datum + field_password: Heslo + field_new_password: Nové heslo + field_password_confirmation: Potvrzení + field_version: Verze + field_type: Typ + field_host: Host + field_port: Port + field_account: ÚÄet + field_base_dn: Base DN + field_attr_login: PÅ™ihlášení (atribut) + field_attr_firstname: Jméno (atribut) + field_attr_lastname: Příjemní (atribut) + field_attr_mail: Email (atribut) + field_onthefly: Automatické vytváření uživatelů + field_start_date: ZaÄátek + field_done_ratio: "% Hotovo" + field_auth_source: AutentifikaÄní mód + field_hide_mail: Nezobrazovat můj email + field_comments: Komentář + field_url: URL + field_start_page: Výchozí stránka + field_subproject: Podprojekt + field_hours: Hodiny + field_activity: Aktivita + field_spent_on: Datum + field_identifier: Identifikátor + field_is_filter: Použít jako filtr + field_issue_to: Související úkol + field_delay: ZpoždÄ›ní + field_assignable: Úkoly mohou být pÅ™iÅ™azeny této roli + field_redirect_existing_links: PÅ™esmÄ›rovat stvávající odkazy + field_estimated_hours: Odhadovaná doba + field_column_names: Sloupce + field_time_entries: Zaznamenaný Äas + field_time_zone: ÄŒasové pásmo + field_searchable: Umožnit vyhledávání + field_default_value: Výchozí hodnota + field_comments_sorting: Zobrazit komentáře + field_parent_title: RodiÄovská stránka + field_editable: Editovatelný + field_watcher: Sleduje + field_identity_url: OpenID URL + field_content: Obsah + field_group_by: Seskupovat výsledky podle + field_sharing: Sdílení + field_parent_issue: RodiÄovský úkol + field_member_of_group: Skupina pÅ™iÅ™aditele + field_assigned_to_role: Role pÅ™iÅ™aditele + field_text: Textové pole + field_visible: Viditelný + + setting_app_title: Název aplikace + setting_app_subtitle: Podtitulek aplikace + setting_welcome_text: Uvítací text + setting_default_language: Výchozí jazyk + setting_login_required: Autentifikace vyžadována + setting_self_registration: Povolena automatická registrace + setting_attachment_max_size: Maximální velikost přílohy + setting_issues_export_limit: Limit pro export úkolů + setting_mail_from: Odesílat emaily z adresy + setting_bcc_recipients: Příjemci skryté kopie (bcc) + setting_plain_text_mail: pouze prostý text (ne HTML) + setting_host_name: Jméno serveru + setting_text_formatting: Formátování textu + setting_wiki_compression: Komprese historie Wiki + setting_feeds_limit: Limit obsahu příspÄ›vků + setting_default_projects_public: Nové projekty nastavovat jako veÅ™ejné + setting_autofetch_changesets: Automaticky stahovat commity + setting_sys_api_enabled: Povolit WS pro správu repozitory + setting_commit_ref_keywords: KlíÄová slova pro odkazy + setting_commit_fix_keywords: KlíÄová slova pro uzavÅ™ení + setting_autologin: Automatické pÅ™ihlaÅ¡ování + setting_date_format: Formát data + setting_time_format: Formát Äasu + setting_cross_project_issue_relations: Povolit vazby úkolů napÅ™Ã­Ä projekty + setting_issue_list_default_columns: Výchozí sloupce zobrazené v seznamu úkolů + setting_emails_header: HlaviÄka emailů + setting_emails_footer: PatiÄka emailů + setting_protocol: Protokol + setting_per_page_options: Povolené poÄty řádků na stránce + setting_user_format: Formát zobrazení uživatele + setting_activity_days_default: Dny zobrazené v Äinnosti projektu + setting_display_subprojects_issues: Automaticky zobrazit úkoly podprojektu v hlavním projektu + setting_enabled_scm: Povolené SCM + setting_mail_handler_body_delimiters: Zkrátit e-maily po jednom z tÄ›chto řádků + setting_mail_handler_api_enabled: Povolit WS pro příchozí e-maily + setting_mail_handler_api_key: API klÃ­Ä + setting_sequential_project_identifiers: Generovat sekvenÄní identifikátory projektů + setting_gravatar_enabled: Použít uživatelské ikony Gravatar + setting_gravatar_default: Výchozí Gravatar + setting_diff_max_lines_displayed: Maximální poÄet zobrazených řádků rozdílů + setting_file_max_size_displayed: Maximální velikost textových souborů zobrazených přímo na stránce + setting_repository_log_display_limit: Maximální poÄet revizí zobrazených v logu souboru + setting_openid: Umožnit pÅ™ihlaÅ¡ování a registrace s OpenID + setting_password_min_length: Minimální délka hesla + setting_new_project_user_role_id: Role pÅ™iÅ™azená uživateli bez práv administrátora, který projekt vytvoÅ™il + setting_default_projects_modules: Výchozí zapnutné moduly pro nový projekt + setting_issue_done_ratio: SpoÄítat koeficient dokonÄení úkolu s + setting_issue_done_ratio_issue_field: Použít pole úkolu + setting_issue_done_ratio_issue_status: Použít stav úkolu + setting_start_of_week: ZaÄínat kalendáře + setting_rest_api_enabled: Zapnout službu REST + setting_cache_formatted_text: Ukládat formátovaný text do vyrovnávací pamÄ›ti + setting_default_notification_option: Výchozí nastavení oznámení + setting_commit_logtime_enabled: Povolit zapisování Äasu + setting_commit_logtime_activity_id: Aktivita pro zapsaný Äas + setting_gantt_items_limit: Maximální poÄet položek zobrazený na ganttovÄ› grafu + + permission_add_project: VytvoÅ™it projekt + permission_add_subprojects: VytvoÅ™it podprojekty + permission_edit_project: Úprava projektů + permission_select_project_modules: VýbÄ›r modulů projektu + permission_manage_members: Spravování Älenství + permission_manage_project_activities: Spravovat aktivity projektu + permission_manage_versions: Spravování verzí + permission_manage_categories: Spravování kategorií úkolů + permission_view_issues: Zobrazit úkoly + permission_add_issues: PÅ™idávání úkolů + permission_edit_issues: Upravování úkolů + permission_manage_issue_relations: Spravování vztahů mezi úkoly + permission_add_issue_notes: PÅ™idávání poznámek + permission_edit_issue_notes: Upravování poznámek + permission_edit_own_issue_notes: Upravování vlastních poznámek + permission_move_issues: PÅ™esouvání úkolů + permission_delete_issues: Mazání úkolů + permission_manage_public_queries: Správa veÅ™ejných dotazů + permission_save_queries: Ukládání dotazů + permission_view_gantt: Zobrazené Ganttova diagramu + permission_view_calendar: Prohlížení kalendáře + permission_view_issue_watchers: Zobrazení seznamu sledujícíh uživatelů + permission_add_issue_watchers: PÅ™idání sledujících uživatelů + permission_delete_issue_watchers: Smazat pÅ™ihlížející + permission_log_time: Zaznamenávání stráveného Äasu + permission_view_time_entries: Zobrazení stráveného Äasu + permission_edit_time_entries: Upravování záznamů o stráveném Äasu + permission_edit_own_time_entries: Upravování vlastních zázamů o stráveném Äase + permission_manage_news: Spravování novinek + permission_comment_news: Komentování novinek + permission_manage_documents: Správa dokumentů + permission_view_documents: Prohlížení dokumentů + permission_manage_files: Spravování souborů + permission_view_files: Prohlížení souborů + permission_manage_wiki: Spravování Wiki + permission_rename_wiki_pages: PÅ™ejmenovávání Wiki stránek + permission_delete_wiki_pages: Mazání stránek na Wiki + permission_view_wiki_pages: Prohlížení Wiki + permission_view_wiki_edits: Prohlížení historie Wiki + permission_edit_wiki_pages: Upravování stránek Wiki + permission_delete_wiki_pages_attachments: Mazání příloh + permission_protect_wiki_pages: ZabezpeÄení Wiki stránek + permission_manage_repository: Spravování repozitáře + permission_browse_repository: Procházení repozitáře + permission_view_changesets: Zobrazování sady zmÄ›n + permission_commit_access: Commit přístup + permission_manage_boards: Správa diskusních fór + permission_view_messages: Prohlížení zpráv + permission_add_messages: Posílání zpráv + permission_edit_messages: Upravování zpráv + permission_edit_own_messages: Upravit vlastní zprávy + permission_delete_messages: Mazání zpráv + permission_delete_own_messages: Smazat vlastní zprávy + permission_export_wiki_pages: Exportovat Wiki stránky + permission_manage_subtasks: Spravovat podúkoly + + project_module_issue_tracking: Sledování úkolů + project_module_time_tracking: Sledování Äasu + project_module_news: Novinky + project_module_documents: Dokumenty + project_module_files: Soubory + project_module_wiki: Wiki + project_module_repository: Repozitář + project_module_boards: Diskuse + project_module_calendar: Kalendář + project_module_gantt: Gantt + + label_user: Uživatel + label_user_plural: Uživatelé + label_user_new: Nový uživatel + label_user_anonymous: Anonymní + label_project: Projekt + label_project_new: Nový projekt + label_project_plural: Projekty + label_x_projects: + zero: žádné projekty + one: 1 projekt + other: "%{count} projekty(ů)" + label_project_all: VÅ¡echny projekty + label_project_latest: Poslední projekty + label_issue: Úkol + label_issue_new: Nový úkol + label_issue_plural: Úkoly + label_issue_view_all: VÅ¡echny úkoly + label_issues_by: "Úkoly podle %{value}" + label_issue_added: Úkol pÅ™idán + label_issue_updated: Úkol aktualizován + label_document: Dokument + label_document_new: Nový dokument + label_document_plural: Dokumenty + label_document_added: Dokument pÅ™idán + label_role: Role + label_role_plural: Role + label_role_new: Nová role + label_role_and_permissions: Role a práva + label_member: ÄŒlen + label_member_new: Nový Älen + label_member_plural: ÄŒlenové + label_tracker: Fronta + label_tracker_plural: Fronty + label_tracker_new: Nová fronta + label_workflow: PrůbÄ›h práce + label_issue_status: Stav úkolu + label_issue_status_plural: Stavy úkolů + label_issue_status_new: Nový stav + label_issue_category: Kategorie úkolu + label_issue_category_plural: Kategorie úkolů + label_issue_category_new: Nová kategorie + label_custom_field: Uživatelské pole + label_custom_field_plural: Uživatelská pole + label_custom_field_new: Nové uživatelské pole + label_enumerations: Seznamy + label_enumeration_new: Nová hodnota + label_information: Informace + label_information_plural: Informace + label_please_login: Prosím pÅ™ihlaÅ¡te se + label_register: Registrovat + label_login_with_open_id_option: nebo se pÅ™ihlaÅ¡te s OpenID + label_password_lost: Zapomenuté heslo + label_home: Úvodní + label_my_page: Moje stránka + label_my_account: Můj úÄet + label_my_projects: Moje projekty + label_my_page_block: Bloky na mé stránce + label_administration: Administrace + label_login: PÅ™ihlášení + label_logout: Odhlášení + label_help: NápovÄ›da + label_reported_issues: Nahlášené úkoly + label_assigned_to_me_issues: Mé úkoly + label_last_login: Poslední pÅ™ihlášení + label_registered_on: Registrován + label_activity: Aktivita + label_overall_activity: Celková aktivita + label_user_activity: "Aktivita uživatele: %{value}" + label_new: Nový + label_logged_as: PÅ™ihlášen jako + label_environment: ProstÅ™edí + label_authentication: Autentifikace + label_auth_source: Mód autentifikace + label_auth_source_new: Nový mód autentifikace + label_auth_source_plural: Módy autentifikace + label_subproject_plural: Podprojekty + label_subproject_new: Nový podprojekt + label_and_its_subprojects: "%{value} a jeho podprojekty" + label_min_max_length: Min - Max délka + label_list: Seznam + label_date: Datum + label_integer: Celé Äíslo + label_float: Desetinné Äíslo + label_boolean: Ano/Ne + label_string: Text + label_text: Dlouhý text + label_attribute: Atribut + label_attribute_plural: Atributy + label_download: "%{count} stažení" + label_download_plural: "%{count} stažení" + label_no_data: Žádné položky + label_change_status: ZmÄ›nit stav + label_history: Historie + label_attachment: Soubor + label_attachment_new: Nový soubor + label_attachment_delete: Odstranit soubor + label_attachment_plural: Soubory + label_file_added: Soubor pÅ™idán + label_report: PÅ™ehled + label_report_plural: PÅ™ehledy + label_news: Novinky + label_news_new: PÅ™idat novinku + label_news_plural: Novinky + label_news_latest: Poslední novinky + label_news_view_all: Zobrazit vÅ¡echny novinky + label_news_added: Novinka pÅ™idána + label_settings: Nastavení + label_overview: PÅ™ehled + label_version: Verze + label_version_new: Nová verze + label_version_plural: Verze + label_close_versions: Zavřít dokonÄené verze + label_confirmation: Potvrzení + label_export_to: 'Také k dispozici:' + label_read: NaÄítá se... + label_public_projects: VeÅ™ejné projekty + label_open_issues: otevÅ™ený + label_open_issues_plural: otevÅ™ené + label_closed_issues: uzavÅ™ený + label_closed_issues_plural: uzavÅ™ené + label_x_open_issues_abbr_on_total: + zero: 0 otevÅ™ených / %{total} + one: 1 otevÅ™ený / %{total} + other: "%{count} otevÅ™ených / %{total}" + label_x_open_issues_abbr: + zero: 0 otevÅ™ených + one: 1 otevÅ™ený + other: "%{count} otevÅ™ených" + label_x_closed_issues_abbr: + zero: 0 uzavÅ™ených + one: 1 uzavÅ™ený + other: "%{count} uzavÅ™ených" + label_total: Celkem + label_permissions: Práva + label_current_status: Aktuální stav + label_new_statuses_allowed: Nové povolené stavy + label_all: vÅ¡e + label_none: nic + label_nobody: nikdo + label_next: Další + label_previous: PÅ™edchozí + label_used_by: Použito + label_details: Detaily + label_add_note: PÅ™idat poznámku + label_per_page: Na stránku + label_calendar: Kalendář + label_months_from: mÄ›síců od + label_gantt: Ganttův graf + label_internal: Interní + label_last_changes: "posledních %{count} zmÄ›n" + label_change_view_all: Zobrazit vÅ¡echny zmÄ›ny + label_personalize_page: PÅ™izpůsobit tuto stránku + label_comment: Komentář + label_comment_plural: Komentáře + label_x_comments: + zero: žádné komentáře + one: 1 komentář + other: "%{count} komentářů" + label_comment_add: PÅ™idat komentáře + label_comment_added: Komentář pÅ™idán + label_comment_delete: Odstranit komentář + label_query: Uživatelský dotaz + label_query_plural: Uživatelské dotazy + label_query_new: Nový dotaz + label_filter_add: PÅ™idat filtr + label_filter_plural: Filtry + label_equals: je + label_not_equals: není + label_in_less_than: je měší než + label_in_more_than: je vÄ›tší než + label_greater_or_equal: '>=' + label_less_or_equal: '<=' + label_in: v + label_today: dnes + label_all_time: vÅ¡e + label_yesterday: vÄera + label_this_week: tento týden + label_last_week: minulý týden + label_last_n_days: "posledních %{count} dnů" + label_this_month: tento mÄ›síc + label_last_month: minulý mÄ›síc + label_this_year: tento rok + label_date_range: ÄŒasový rozsah + label_less_than_ago: pÅ™ed ménÄ› jak (dny) + label_more_than_ago: pÅ™ed více jak (dny) + label_ago: pÅ™ed (dny) + label_contains: obsahuje + label_not_contains: neobsahuje + label_day_plural: dny + label_repository: Repozitář + label_repository_plural: Repozitáře + label_browse: Procházet + label_modification: "%{count} zmÄ›na" + label_modification_plural: "%{count} zmÄ›n" + label_branch: VÄ›tev + label_tag: Tag + label_revision: Revize + label_revision_plural: Revizí + label_revision_id: "Revize %{value}" + label_associated_revisions: Související verze + label_added: pÅ™idáno + label_modified: zmÄ›nÄ›no + label_copied: zkopírováno + label_renamed: pÅ™ejmenováno + label_deleted: odstranÄ›no + label_latest_revision: Poslední revize + label_latest_revision_plural: Poslední revize + label_view_revisions: Zobrazit revize + label_view_all_revisions: Zobrazit vÅ¡echny revize + label_max_size: Maximální velikost + label_sort_highest: PÅ™esunout na zaÄátek + label_sort_higher: PÅ™esunout nahoru + label_sort_lower: PÅ™esunout dolů + label_sort_lowest: PÅ™esunout na konec + label_roadmap: Plán + label_roadmap_due_in: "Zbývá %{value}" + label_roadmap_overdue: "%{value} pozdÄ›" + label_roadmap_no_issues: Pro tuto verzi nejsou žádné úkoly + label_search: Hledat + label_result_plural: Výsledky + label_all_words: VÅ¡echna slova + label_wiki: Wiki + label_wiki_edit: Wiki úprava + label_wiki_edit_plural: Wiki úpravy + label_wiki_page: Wiki stránka + label_wiki_page_plural: Wiki stránky + label_index_by_title: Index dle názvu + label_index_by_date: Index dle data + label_current_version: Aktuální verze + label_preview: Náhled + label_feed_plural: PříspÄ›vky + label_changes_details: Detail vÅ¡ech zmÄ›n + label_issue_tracking: Sledování úkolů + label_spent_time: Strávený Äas + label_overall_spent_time: Celkem strávený Äas + label_f_hour: "%{value} hodina" + label_f_hour_plural: "%{value} hodin" + label_time_tracking: Sledování Äasu + label_change_plural: ZmÄ›ny + label_statistics: Statistiky + label_commits_per_month: Commitů za mÄ›síc + label_commits_per_author: Commitů za autora + label_view_diff: Zobrazit rozdíly + label_diff_inline: uvnitÅ™ + label_diff_side_by_side: vedle sebe + label_options: Nastavení + label_copy_workflow_from: Kopírovat průbÄ›h práce z + label_permissions_report: PÅ™ehled práv + label_watched_issues: Sledované úkoly + label_related_issues: Související úkoly + label_applied_status: Použitý stav + label_loading: Nahrávám... + label_relation_new: Nová souvislost + label_relation_delete: Odstranit souvislost + label_relates_to: související s + label_duplicates: duplikuje + label_duplicated_by: zduplikován + label_blocks: blokuje + label_blocked_by: zablokován + label_precedes: pÅ™edchází + label_follows: následuje + label_end_to_start: od konce do zaÄátku + label_end_to_end: od konce do konce + label_start_to_start: od zaÄátku do zaÄátku + label_start_to_end: od zaÄátku do konce + label_stay_logged_in: Zůstat pÅ™ihlášený + label_disabled: zakázán + label_show_completed_versions: Ukázat dokonÄené verze + label_me: já + label_board: Fórum + label_board_new: Nové fórum + label_board_plural: Fóra + label_board_locked: UzamÄeno + label_board_sticky: Nálepka + label_topic_plural: Témata + label_message_plural: Zprávy + label_message_last: Poslední zpráva + label_message_new: Nová zpráva + label_message_posted: Zpráva pÅ™idána + label_reply_plural: OdpovÄ›di + label_send_information: Zaslat informace o úÄtu uživateli + label_year: Rok + label_month: MÄ›síc + label_week: Týden + label_date_from: Od + label_date_to: Do + label_language_based: Podle výchozího jazyku + label_sort_by: "SeÅ™adit podle %{value}" + label_send_test_email: Poslat testovací email + label_feeds_access_key: Přístupový klÃ­Ä pro RSS + label_missing_feeds_access_key: Postrádá přístupový klÃ­Ä pro RSS + label_feeds_access_key_created_on: "Přístupový klÃ­Ä pro RSS byl vytvoÅ™en pÅ™ed %{value}" + label_module_plural: Moduly + label_added_time_by: "PÅ™idáno uživatelem %{author} pÅ™ed %{age}" + label_updated_time_by: "Aktualizováno uživatelem %{author} pÅ™ed %{age}" + label_updated_time: "Aktualizováno pÅ™ed %{value}" + label_jump_to_a_project: Vyberte projekt... + label_file_plural: Soubory + label_changeset_plural: Changesety + label_default_columns: Výchozí sloupce + label_no_change_option: (beze zmÄ›ny) + label_bulk_edit_selected_issues: Hromadná úprava vybraných úkolů + label_theme: Téma + label_default: Výchozí + label_search_titles_only: Vyhledávat pouze v názvech + label_user_mail_option_all: "Pro vÅ¡echny události vÅ¡ech mých projektů" + label_user_mail_option_selected: "Pro vÅ¡echny události vybraných projektů..." + label_user_mail_option_none: "Žádné události" + label_user_mail_option_only_my_events: "Jen pro vÄ›ci co sleduji nebo jsem v nich zapojen" + label_user_mail_option_only_assigned: "Jen pro vÅ¡eci kterým sem pÅ™iÅ™azen" + label_user_mail_option_only_owner: "Jen pro vÄ›ci které vlastním" + label_user_mail_no_self_notified: "Nezasílat informace o mnou vytvoÅ™ených zmÄ›nách" + label_registration_activation_by_email: aktivace úÄtu emailem + label_registration_manual_activation: manuální aktivace úÄtu + label_registration_automatic_activation: automatická aktivace úÄtu + label_display_per_page: "%{value} na stránku" + label_age: VÄ›k + label_change_properties: ZmÄ›nit vlastnosti + label_general: Obecné + label_more: Více + label_scm: SCM + label_plugins: Doplňky + label_ldap_authentication: Autentifikace LDAP + label_downloads_abbr: Staž. + label_optional_description: Volitelný popis + label_add_another_file: PÅ™idat další soubor + label_preferences: Nastavení + label_chronological_order: V chronologickém poÅ™adí + label_reverse_chronological_order: V obrácaném chronologickém poÅ™adí + label_planning: Plánování + label_incoming_emails: Příchozí e-maily + label_generate_key: Generovat klÃ­Ä + label_issue_watchers: Sledování + label_example: Příklad + label_display: Zobrazit + label_sort: Řazení + label_ascending: VzestupnÄ› + label_descending: SestupnÄ› + label_date_from_to: Od %{start} do %{end} + label_wiki_content_added: Wiki stránka pÅ™idána + label_wiki_content_updated: Wiki stránka aktualizována + label_group: Skupina + label_group_plural: Skupiny + label_group_new: Nová skupina + label_time_entry_plural: Strávený Äas + label_version_sharing_none: Nesdíleno + label_version_sharing_descendants: S podprojekty + label_version_sharing_hierarchy: S hierarchií projektu + label_version_sharing_tree: Se stromem projektu + label_version_sharing_system: Se vÅ¡emi projekty + label_update_issue_done_ratios: Aktualizovat koeficienty dokonÄení úkolů + label_copy_source: Zdroj + label_copy_target: Cíl + label_copy_same_as_target: Stejný jako cíl + label_display_used_statuses_only: Zobrazit pouze stavy které jsou použité touto frontou + label_api_access_key: API přístupový klÃ­Ä + label_missing_api_access_key: ChybÄ›jící přístupový klÃ­Ä API + label_api_access_key_created_on: API přístupový klÃ­Ä vytvoÅ™en %{value} + label_profile: Profil + label_subtask_plural: Podúkol + label_project_copy_notifications: Odeslat email oznámení v průbÄ›hu kopie projektu + label_principal_search: "Hledat uživatele nebo skupinu:" + label_user_search: "Hledat uživatele:" + + button_login: PÅ™ihlásit + button_submit: Potvrdit + button_save: Uložit + button_check_all: ZaÅ¡rtnout vÅ¡e + button_uncheck_all: OdÅ¡rtnout vÅ¡e + button_delete: Odstranit + button_create: VytvoÅ™it + button_create_and_continue: VytvoÅ™it a pokraÄovat + button_test: Testovat + button_edit: Upravit + button_edit_associated_wikipage: "Upravit pÅ™iÅ™azenou Wiki stránku: %{page_title}" + button_add: PÅ™idat + button_change: ZmÄ›nit + button_apply: Použít + button_clear: Smazat + button_lock: Zamknout + button_unlock: Odemknout + button_download: Stáhnout + button_list: Vypsat + button_view: Zobrazit + button_move: PÅ™esunout + button_move_and_follow: PÅ™esunout a následovat + button_back: ZpÄ›t + button_cancel: Storno + button_activate: Aktivovat + button_sort: SeÅ™adit + button_log_time: PÅ™idat Äas + button_rollback: ZpÄ›t k této verzi + button_watch: Sledovat + button_unwatch: Nesledovat + button_reply: OdpovÄ›dÄ›t + button_archive: Archivovat + button_unarchive: Odarchivovat + button_reset: Resetovat + button_rename: PÅ™ejmenovat + button_change_password: ZmÄ›nit heslo + button_copy: Kopírovat + button_copy_and_follow: Kopírovat a následovat + button_annotate: Komentovat + button_update: Aktualizovat + button_configure: Konfigurovat + button_quote: Citovat + button_duplicate: Duplikovat + button_show: Zobrazit + + status_active: aktivní + status_registered: registrovaný + status_locked: uzamÄený + + version_status_open: otevÅ™ený + version_status_locked: uzamÄený + version_status_closed: zavÅ™ený + + field_active: Aktivní + + text_select_mail_notifications: Vyberte akci pÅ™i které bude zasláno upozornÄ›ní emailem. + text_regexp_info: napÅ™. ^[A-Z0-9]+$ + text_min_max_length_info: 0 znamená bez limitu + text_project_destroy_confirmation: Jste si jisti, že chcete odstranit tento projekt a vÅ¡echna související data ? + text_subprojects_destroy_warning: "Jeho podprojek(y): %{value} budou také smazány." + text_workflow_edit: Vyberte roli a frontu k editaci průbÄ›hu práce + text_are_you_sure: Jste si jisti? + text_journal_changed: "%{label} zmÄ›nÄ›n z %{old} na %{new}" + text_journal_set_to: "%{label} nastaven na %{value}" + text_journal_deleted: "%{label} smazán (%{old})" + text_journal_added: "%{label} %{value} pÅ™idán" + text_tip_issue_begin_day: úkol zaÄíná v tento den + text_tip_issue_end_day: úkol konÄí v tento den + text_tip_issue_begin_end_day: úkol zaÄíná a konÄí v tento den + text_caracters_maximum: "%{count} znaků maximálnÄ›." + text_caracters_minimum: "Musí být alespoň %{count} znaků dlouhé." + text_length_between: "Délka mezi %{min} a %{max} znaky." + text_tracker_no_workflow: Pro tuto frontu není definován žádný průbÄ›h práce + text_unallowed_characters: Nepovolené znaky + text_comma_separated: Povoleno více hodnot (oddÄ›lÄ›né Äárkou). + text_line_separated: Více hodnot povoleno (jeden řádek pro každou hodnotu). + text_issues_ref_in_commit_messages: Odkazování a opravování úkolů ve zprávách commitů + text_issue_added: "Úkol %{id} byl vytvoÅ™en uživatelem %{author}." + text_issue_updated: "Úkol %{id} byl aktualizován uživatelem %{author}." + text_wiki_destroy_confirmation: Opravdu si pÅ™ejete odstranit tuto Wiki a celý její obsah? + text_issue_category_destroy_question: "NÄ›které úkoly (%{count}) jsou pÅ™iÅ™azeny k této kategorii. Co s nimi chtete udÄ›lat?" + text_issue_category_destroy_assignments: ZruÅ¡it pÅ™iÅ™azení ke kategorii + text_issue_category_reassign_to: PÅ™iÅ™adit úkoly do této kategorie + text_user_mail_option: "U projektů, které nebyly vybrány, budete dostávat oznámení pouze o vaÅ¡ich Äi o sledovaných položkách (napÅ™. o položkách jejichž jste autor nebo ke kterým jste pÅ™iÅ™azen(a))." + text_no_configuration_data: "Role, fronty, stavy úkolů ani průbÄ›h práce nebyly zatím nakonfigurovány.\nVelice doporuÄujeme nahrát výchozí konfiguraci. Po té si můžete vÅ¡e upravit" + text_load_default_configuration: Nahrát výchozí konfiguraci + text_status_changed_by_changeset: "Použito v changesetu %{value}." + text_time_logged_by_changeset: Aplikováno v changesetu %{value}. + text_issues_destroy_confirmation: 'Opravdu si pÅ™ejete odstranit vÅ¡echny zvolené úkoly?' + text_select_project_modules: 'Aktivní moduly v tomto projektu:' + text_default_administrator_account_changed: Výchozí nastavení administrátorského úÄtu zmÄ›nÄ›no + text_file_repository_writable: Povolen zápis do adresáře ukládání souborů + text_plugin_assets_writable: Možnost zápisu do adresáře plugin assets + text_rmagick_available: RMagick k dispozici (volitelné) + text_destroy_time_entries_question: "U úkolů, které chcete odstranit je evidováno %{hours} práce. Co chete udÄ›lat?" + text_destroy_time_entries: Odstranit evidované hodiny. + text_assign_time_entries_to_project: PÅ™iÅ™adit evidované hodiny projektu + text_reassign_time_entries: 'PÅ™eÅ™adit evidované hodiny k tomuto úkolu:' + text_user_wrote: "%{value} napsal:" + text_enumeration_destroy_question: "NÄ›kolik (%{count}) objektů je pÅ™iÅ™azeno k této hodnotÄ›." + text_enumeration_category_reassign_to: 'PÅ™eÅ™adit je do této:' + text_email_delivery_not_configured: "DoruÄování e-mailů není nastaveno a odesílání notifikací je zakázáno.\nNastavte Váš SMTP server v souboru config/configuration.yml a restartujte aplikaci." + text_repository_usernames_mapping: "Vybrat nebo upravit mapování mezi Redmine uživateli a uživatelskými jmény nalezenými v logu repozitáře.\nUživatelé se shodným Redmine uživatelským jménem a uživatelským jménem v repozitáři jsou mapovaní automaticky." + text_diff_truncated: '... Rozdílový soubor je zkrácen, protože jeho délka pÅ™esahuje max. limit.' + text_custom_field_possible_values_info: 'Každá hodnota na novém řádku' + text_wiki_page_destroy_question: Tato stránka má %{descendants} podstránek a potomků. Co chcete udÄ›lat? + text_wiki_page_nullify_children: Ponechat podstránky jako koÅ™enové stránky + text_wiki_page_destroy_children: Smazat podstránky a vÅ¡echny jejich potomky + text_wiki_page_reassign_children: PÅ™iÅ™adit podstránky k tomuto rodiÄi + text_own_membership_delete_confirmation: "Chystáte se odebrat si nÄ›která nebo vÅ¡echny svá oprávnÄ›ní a potom již nemusíte být schopni upravit tento projekt.\nOpravdu chcete pokraÄovat?" + text_zoom_in: PÅ™iblížit + text_zoom_out: Oddálit + + default_role_manager: Manažer + default_role_developer: Vývojář + default_role_reporter: Reportér + default_tracker_bug: Chyba + default_tracker_feature: Požadavek + default_tracker_support: Podpora + default_issue_status_new: Nový + default_issue_status_in_progress: Ve vývoji + default_issue_status_resolved: VyÅ™eÅ¡ený + default_issue_status_feedback: ÄŒeká se + default_issue_status_closed: UzavÅ™ený + default_issue_status_rejected: Odmítnutý + default_doc_category_user: Uživatelská dokumentace + default_doc_category_tech: Technická dokumentace + default_priority_low: Nízká + default_priority_normal: Normální + default_priority_high: Vysoká + default_priority_urgent: Urgentní + default_priority_immediate: Okamžitá + default_activity_design: Návhr + default_activity_development: Vývoj + + enumeration_issue_priorities: Priority úkolů + enumeration_doc_categories: Kategorie dokumentů + enumeration_activities: Aktivity (sledování Äasu) + enumeration_system_activity: Systémová aktivita + + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: Priority updated + label_issues_visibility_own: Issues created by or assigned to the user + field_issues_visibility: Issues visibility + label_issues_visibility_all: All issues + permission_set_own_issues_private: Set own issues public or private + field_is_private: Private + permission_set_issues_private: Set issues public or private + label_issues_visibility_public: All non private issues + text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). + field_commit_logs_encoding: Kódování zpráv pÅ™i commitu + field_scm_path_encoding: Path encoding + text_scm_path_encoding_note: "Default: UTF-8" + field_path_to_repository: Path to repository + field_root_directory: Root directory + field_cvs_module: Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Command + text_scm_command_version: Version + label_git_report_last_commit: Report last commit for files and directories + text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 Úkol + one: 1 Úkol + other: "%{count} Úkoly" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: vše + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: S podprojekty + label_cross_project_tree: Se stromem projektu + label_cross_project_hierarchy: S hierarchií projektu + label_cross_project_system: Se všemi projekty + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fa/fa07a6df53d2a50f2d1d7256c158389830184b31.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fa/fa07a6df53d2a50f2d1d7256c158389830184b31.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,410 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module IssuesHelper + include ApplicationHelper + + def issue_list(issues, &block) + ancestors = [] + issues.each do |issue| + while (ancestors.any? && !issue.is_descendant_of?(ancestors.last)) + ancestors.pop + end + yield issue, ancestors.size + ancestors << issue unless issue.leaf? + end + end + + # Renders a HTML/CSS tooltip + # + # To use, a trigger div is needed. This is a div with the class of "tooltip" + # that contains this method wrapped in a span with the class of "tip" + # + #
    <%= link_to_issue(issue) %> + # <%= render_issue_tooltip(issue) %> + #
    + # + def render_issue_tooltip(issue) + @cached_label_status ||= l(:field_status) + @cached_label_start_date ||= l(:field_start_date) + @cached_label_due_date ||= l(:field_due_date) + @cached_label_assigned_to ||= l(:field_assigned_to) + @cached_label_priority ||= l(:field_priority) + @cached_label_project ||= l(:field_project) + + link_to_issue(issue) + "

    ".html_safe + + "#{@cached_label_project}: #{link_to_project(issue.project)}
    ".html_safe + + "#{@cached_label_status}: #{h(issue.status.name)}
    ".html_safe + + "#{@cached_label_start_date}: #{format_date(issue.start_date)}
    ".html_safe + + "#{@cached_label_due_date}: #{format_date(issue.due_date)}
    ".html_safe + + "#{@cached_label_assigned_to}: #{h(issue.assigned_to)}
    ".html_safe + + "#{@cached_label_priority}: #{h(issue.priority.name)}".html_safe + end + + def issue_heading(issue) + h("#{issue.tracker} ##{issue.id}") + end + + def render_issue_subject_with_tree(issue) + s = '' + ancestors = issue.root? ? [] : issue.ancestors.visible.all + ancestors.each do |ancestor| + s << '
    ' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id))) + end + s << '
    ' + subject = h(issue.subject) + if issue.is_private? + subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject + end + s << content_tag('h3', subject) + s << '
    ' * (ancestors.size + 1) + s.html_safe + end + + def render_descendants_tree(issue) + s = '
    ' + issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level| + css = "issue issue-#{child.id} hascontextmenu" + css << " idnt idnt-#{level}" if level > 0 + s << content_tag('tr', + content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') + + content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') + + content_tag('td', h(child.status)) + + content_tag('td', link_to_user(child.assigned_to)) + + content_tag('td', progress_bar(child.done_ratio, :width => '80px')), + :class => css) + end + s << '
    ' + s.html_safe + end + + # Returns a link for adding a new subtask to the given issue + def link_to_new_subtask(issue) + attrs = { + :tracker_id => issue.tracker, + :parent_issue_id => issue + } + link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs)) + end + + class IssueFieldsRows + include ActionView::Helpers::TagHelper + + def initialize + @left = [] + @right = [] + end + + def left(*args) + args.any? ? @left << cells(*args) : @left + end + + def right(*args) + args.any? ? @right << cells(*args) : @right + end + + def size + @left.size > @right.size ? @left.size : @right.size + end + + def to_html + html = ''.html_safe + blank = content_tag('th', '') + content_tag('td', '') + size.times do |i| + left = @left[i] || blank + right = @right[i] || blank + html << content_tag('tr', left + right) + end + html + end + + def cells(label, text, options={}) + content_tag('th', "#{label}:", options) + content_tag('td', text, options) + end + end + + def issue_fields_rows + r = IssueFieldsRows.new + yield r + r.to_html + end + + def render_custom_fields_rows(issue) + return if issue.custom_field_values.empty? + ordered_values = [] + half = (issue.custom_field_values.size / 2.0).ceil + half.times do |i| + ordered_values << issue.custom_field_values[i] + ordered_values << issue.custom_field_values[i + half] + end + s = "\n" + n = 0 + ordered_values.compact.each do |value| + s << "\n\n" if n > 0 && (n % 2) == 0 + s << "\t#{ h(value.custom_field.name) }:#{ simple_format_without_paragraph(h(show_value(value))) }\n" + n += 1 + end + s << "\n" + s.html_safe + end + + def issues_destroy_confirmation_message(issues) + issues = [issues] unless issues.is_a?(Array) + message = l(:text_issues_destroy_confirmation) + descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2} + if descendant_count > 0 + issues.each do |issue| + next if issue.root? + issues.each do |other_issue| + descendant_count -= 1 if issue.is_descendant_of?(other_issue) + end + end + if descendant_count > 0 + message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count) + end + end + message + end + + def sidebar_queries + unless @sidebar_queries + @sidebar_queries = Query.visible.all( + :order => "#{Query.table_name}.name ASC", + # Project specific queries and global queries + :conditions => (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]) + ) + end + @sidebar_queries + end + + def query_links(title, queries) + # links to #index on issues/show + url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params + + content_tag('h3', h(title)) + + queries.collect {|query| + css = 'query' + css << ' selected' if query == @query + link_to(h(query.name), url_params.merge(:query_id => query), :class => css) + }.join('
    ').html_safe + end + + def render_sidebar_queries + out = ''.html_safe + queries = sidebar_queries.select {|q| !q.is_public?} + out << query_links(l(:label_my_queries), queries) if queries.any? + queries = sidebar_queries.select {|q| q.is_public?} + out << query_links(l(:label_query_plural), queries) if queries.any? + out + end + + # Returns the textual representation of a journal details + # as an array of strings + def details_to_strings(details, no_html=false, options={}) + options[:only_path] = (options[:only_path] == false ? false : true) + strings = [] + values_by_field = {} + details.each do |detail| + if detail.property == 'cf' + field_id = detail.prop_key + field = CustomField.find_by_id(field_id) + if field && field.multiple? + values_by_field[field_id] ||= {:added => [], :deleted => []} + if detail.old_value + values_by_field[field_id][:deleted] << detail.old_value + end + if detail.value + values_by_field[field_id][:added] << detail.value + end + next + end + end + strings << show_detail(detail, no_html, options) + end + values_by_field.each do |field_id, changes| + detail = JournalDetail.new(:property => 'cf', :prop_key => field_id) + if changes[:added].any? + detail.value = changes[:added] + strings << show_detail(detail, no_html, options) + elsif changes[:deleted].any? + detail.old_value = changes[:deleted] + strings << show_detail(detail, no_html, options) + end + end + strings + end + + # Returns the textual representation of a single journal detail + def show_detail(detail, no_html=false, options={}) + multiple = false + case detail.property + when 'attr' + field = detail.prop_key.to_s.gsub(/\_id$/, "") + label = l(("field_" + field).to_sym) + case detail.prop_key + when 'due_date', 'start_date' + value = format_date(detail.value.to_date) if detail.value + old_value = format_date(detail.old_value.to_date) if detail.old_value + + when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id', + 'priority_id', 'category_id', 'fixed_version_id' + value = find_name_by_reflection(field, detail.value) + old_value = find_name_by_reflection(field, detail.old_value) + + when 'estimated_hours' + value = "%0.02f" % detail.value.to_f unless detail.value.blank? + old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank? + + when 'parent_id' + label = l(:field_parent_issue) + value = "##{detail.value}" unless detail.value.blank? + old_value = "##{detail.old_value}" unless detail.old_value.blank? + + when 'is_private' + value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank? + old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank? + end + when 'cf' + custom_field = CustomField.find_by_id(detail.prop_key) + if custom_field + multiple = custom_field.multiple? + label = custom_field.name + value = format_value(detail.value, custom_field.field_format) if detail.value + old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value + end + when 'attachment' + label = l(:label_attachment) + end + call_hook(:helper_issues_show_detail_after_setting, + {:detail => detail, :label => label, :value => value, :old_value => old_value }) + + label ||= detail.prop_key + value ||= detail.value + old_value ||= detail.old_value + + unless no_html + label = content_tag('strong', label) + old_value = content_tag("i", h(old_value)) if detail.old_value + old_value = content_tag("del", old_value) if detail.old_value and detail.value.blank? + if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key) + # Link to the attachment if it has not been removed + value = link_to_attachment(atta, :download => true, :only_path => options[:only_path]) + if options[:only_path] != false && atta.is_text? + value += link_to( + image_tag('magnifier.png'), + :controller => 'attachments', :action => 'show', + :id => atta, :filename => atta.filename + ) + end + else + value = content_tag("i", h(value)) if value + end + end + + if detail.property == 'attr' && detail.prop_key == 'description' + s = l(:text_journal_changed_no_detail, :label => label) + unless no_html + diff_link = link_to 'diff', + {:controller => 'journals', :action => 'diff', :id => detail.journal_id, + :detail_id => detail.id, :only_path => options[:only_path]}, + :title => l(:label_view_diff) + s << " (#{ diff_link })" + end + s.html_safe + elsif detail.value.present? + case detail.property + when 'attr', 'cf' + if detail.old_value.present? + l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe + elsif multiple + l(:text_journal_added, :label => label, :value => value).html_safe + else + l(:text_journal_set_to, :label => label, :value => value).html_safe + end + when 'attachment' + l(:text_journal_added, :label => label, :value => value).html_safe + end + else + l(:text_journal_deleted, :label => label, :old => old_value).html_safe + end + end + + # Find the name of an associated record stored in the field attribute + def find_name_by_reflection(field, id) + association = Issue.reflect_on_association(field.to_sym) + if association + record = association.class_name.constantize.find_by_id(id) + return record.name if record + end + end + + # Renders issue children recursively + def render_api_issue_children(issue, api) + return if issue.leaf? + api.array :children do + issue.children.each do |child| + api.issue(:id => child.id) do + api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil? + api.subject child.subject + render_api_issue_children(child, api) + end + end + end + end + + def issues_to_csv(issues, project, query, options={}) + decimal_separator = l(:general_csv_decimal_separator) + encoding = l(:general_csv_encoding) + columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns) + if options[:description] + if description = query.available_columns.detect {|q| q.name == :description} + columns << description + end + end + + export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv| + # csv header fields + csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) } + + # csv lines + issues.each do |issue| + col_values = columns.collect do |column| + s = if column.is_a?(QueryCustomFieldColumn) + cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id} + show_value(cv) + else + value = column.value(issue) + if value.is_a?(Date) + format_date(value) + elsif value.is_a?(Time) + format_time(value) + elsif value.is_a?(Float) + ("%.2f" % value).gsub('.', decimal_separator) + else + value + end + end + s.to_s + end + csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } + end + end + export + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fa/fa16a1d3cc85f7d975c8b277b6a1fb967cd6fdd6.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fa/fa16a1d3cc85f7d975c8b277b6a1fb967cd6fdd6.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1208 @@ +# vim:ts=4:sw=4: +# = RedCloth - Textile and Markdown Hybrid for Ruby +# +# Homepage:: http://whytheluckystiff.net/ruby/redcloth/ +# Author:: why the lucky stiff (http://whytheluckystiff.net/) +# Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.) +# License:: BSD +# +# (see http://hobix.com/textile/ for a Textile Reference.) +# +# Based on (and also inspired by) both: +# +# PyTextile: http://diveintomark.org/projects/textile/textile.py.txt +# Textism for PHP: http://www.textism.com/tools/textile/ +# +# + +# = RedCloth +# +# RedCloth is a Ruby library for converting Textile and/or Markdown +# into HTML. You can use either format, intermingled or separately. +# You can also extend RedCloth to honor your own custom text stylings. +# +# RedCloth users are encouraged to use Textile if they are generating +# HTML and to use Markdown if others will be viewing the plain text. +# +# == What is Textile? +# +# Textile is a simple formatting style for text +# documents, loosely based on some HTML conventions. +# +# == Sample Textile Text +# +# h2. This is a title +# +# h3. This is a subhead +# +# This is a bit of paragraph. +# +# bq. This is a blockquote. +# +# = Writing Textile +# +# A Textile document consists of paragraphs. Paragraphs +# can be specially formatted by adding a small instruction +# to the beginning of the paragraph. +# +# h[n]. Header of size [n]. +# bq. Blockquote. +# # Numeric list. +# * Bulleted list. +# +# == Quick Phrase Modifiers +# +# Quick phrase modifiers are also included, to allow formatting +# of small portions of text within a paragraph. +# +# \_emphasis\_ +# \_\_italicized\_\_ +# \*strong\* +# \*\*bold\*\* +# ??citation?? +# -deleted text- +# +inserted text+ +# ^superscript^ +# ~subscript~ +# @code@ +# %(classname)span% +# +# ==notextile== (leave text alone) +# +# == Links +# +# To make a hypertext link, put the link text in "quotation +# marks" followed immediately by a colon and the URL of the link. +# +# Optional: text in (parentheses) following the link text, +# but before the closing quotation mark, will become a Title +# attribute for the link, visible as a tool tip when a cursor is above it. +# +# Example: +# +# "This is a link (This is a title) ":http://www.textism.com +# +# Will become: +# +# This is a link +# +# == Images +# +# To insert an image, put the URL for the image inside exclamation marks. +# +# Optional: text that immediately follows the URL in (parentheses) will +# be used as the Alt text for the image. Images on the web should always +# have descriptive Alt text for the benefit of readers using non-graphical +# browsers. +# +# Optional: place a colon followed by a URL immediately after the +# closing ! to make the image into a link. +# +# Example: +# +# !http://www.textism.com/common/textist.gif(Textist)! +# +# Will become: +# +# Textist +# +# With a link: +# +# !/common/textist.gif(Textist)!:http://textism.com +# +# Will become: +# +# Textist +# +# == Defining Acronyms +# +# HTML allows authors to define acronyms via the tag. The definition appears as a +# tool tip when a cursor hovers over the acronym. A crucial aid to clear writing, +# this should be used at least once for each acronym in documents where they appear. +# +# To quickly define an acronym in Textile, place the full text in (parentheses) +# immediately following the acronym. +# +# Example: +# +# ACLU(American Civil Liberties Union) +# +# Will become: +# +# ACLU +# +# == Adding Tables +# +# In Textile, simple tables can be added by seperating each column by +# a pipe. +# +# |a|simple|table|row| +# |And|Another|table|row| +# +# Attributes are defined by style definitions in parentheses. +# +# table(border:1px solid black). +# (background:#ddd;color:red). |{}| | | | +# +# == Using RedCloth +# +# RedCloth is simply an extension of the String class, which can handle +# Textile formatting. Use it like a String and output HTML with its +# RedCloth#to_html method. +# +# doc = RedCloth.new " +# +# h2. Test document +# +# Just a simple test." +# +# puts doc.to_html +# +# By default, RedCloth uses both Textile and Markdown formatting, with +# Textile formatting taking precedence. If you want to turn off Markdown +# formatting, to boost speed and limit the processor: +# +# class RedCloth::Textile.new( str ) + +class RedCloth3 < String + + VERSION = '3.0.4' + DEFAULT_RULES = [:textile, :markdown] + + # + # Two accessor for setting security restrictions. + # + # This is a nice thing if you're using RedCloth for + # formatting in public places (e.g. Wikis) where you + # don't want users to abuse HTML for bad things. + # + # If +:filter_html+ is set, HTML which wasn't + # created by the Textile processor will be escaped. + # + # If +:filter_styles+ is set, it will also disable + # the style markup specifier. ('{color: red}') + # + attr_accessor :filter_html, :filter_styles + + # + # Accessor for toggling hard breaks. + # + # If +:hard_breaks+ is set, single newlines will + # be converted to HTML break tags. This is the + # default behavior for traditional RedCloth. + # + attr_accessor :hard_breaks + + # Accessor for toggling lite mode. + # + # In lite mode, block-level rules are ignored. This means + # that tables, paragraphs, lists, and such aren't available. + # Only the inline markup for bold, italics, entities and so on. + # + # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] ) + # r.to_html + # #=> "And then? She fell!" + # + attr_accessor :lite_mode + + # + # Accessor for toggling span caps. + # + # Textile places `span' tags around capitalized + # words by default, but this wreaks havoc on Wikis. + # If +:no_span_caps+ is set, this will be + # suppressed. + # + attr_accessor :no_span_caps + + # + # Establishes the markup predence. Available rules include: + # + # == Textile Rules + # + # The following textile rules can be set individually. Or add the complete + # set of rules with the single :textile rule, which supplies the rule set in + # the following precedence: + # + # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/) + # block_textile_table:: Textile table block structures + # block_textile_lists:: Textile list structures + # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.) + # inline_textile_image:: Textile inline images + # inline_textile_link:: Textile inline links + # inline_textile_span:: Textile inline spans + # glyphs_textile:: Textile entities (such as em-dashes and smart quotes) + # + # == Markdown + # + # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/) + # block_markdown_setext:: Markdown setext headers + # block_markdown_atx:: Markdown atx headers + # block_markdown_rule:: Markdown horizontal rules + # block_markdown_bq:: Markdown blockquotes + # block_markdown_lists:: Markdown lists + # inline_markdown_link:: Markdown links + attr_accessor :rules + + # Returns a new RedCloth object, based on _string_ and + # enforcing all the included _restrictions_. + # + # r = RedCloth.new( "h1. A bold man", [:filter_html] ) + # r.to_html + # #=>"

    A <b>bold</b> man

    " + # + def initialize( string, restrictions = [] ) + restrictions.each { |r| method( "#{ r }=" ).call( true ) } + super( string ) + end + + # + # Generates HTML from the Textile contents. + # + # r = RedCloth.new( "And then? She *fell*!" ) + # r.to_html( true ) + # #=>"And then? She fell!" + # + def to_html( *rules ) + rules = DEFAULT_RULES if rules.empty? + # make our working copy + text = self.dup + + @urlrefs = {} + @shelf = [] + textile_rules = [:block_textile_table, :block_textile_lists, + :block_textile_prefix, :inline_textile_image, :inline_textile_link, + :inline_textile_code, :inline_textile_span, :glyphs_textile] + markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule, + :block_markdown_bq, :block_markdown_lists, + :inline_markdown_reflink, :inline_markdown_link] + @rules = rules.collect do |rule| + case rule + when :markdown + markdown_rules + when :textile + textile_rules + else + rule + end + end.flatten + + # standard clean up + incoming_entities text + clean_white_space text + + # start processor + @pre_list = [] + rip_offtags text + no_textile text + escape_html_tags text + # need to do this before #hard_break and #blocks + block_textile_quotes text unless @lite_mode + hard_break text + unless @lite_mode + refs text + blocks text + end + inline text + smooth_offtags text + + retrieve text + + text.gsub!( /<\/?notextile>/, '' ) + text.gsub!( /x%x%/, '&' ) + clean_html text if filter_html + text.strip! + text + + end + + ####### + private + ####### + # + # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents. + # (from PyTextile) + # + TEXTILE_TAGS = + + [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230], + [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249], + [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217], + [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732], + [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]]. + + collect! do |a, b| + [a.chr, ( b.zero? and "" or "&#{ b };" )] + end + + # + # Regular expressions to convert to HTML. + # + A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/ + A_VLGN = /[\-^~]/ + C_CLAS = '(?:\([^")]+\))' + C_LNGE = '(?:\[[^"\[\]]+\])' + C_STYL = '(?:\{[^"}]+\})' + S_CSPN = '(?:\\\\\d+)' + S_RSPN = '(?:/\d+)' + A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)" + S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)" + C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)" + # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' ) + PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' ) + PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' ) + PUNCT_Q = Regexp::quote( '*-_+^~%' ) + HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)' + + # Text markup tags, don't conflict with block tags + SIMPLE_HTML_TAGS = [ + 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code', + 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br', + 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo' + ] + + QTAGS = [ + ['**', 'b', :limit], + ['*', 'strong', :limit], + ['??', 'cite', :limit], + ['-', 'del', :limit], + ['__', 'i', :limit], + ['_', 'em', :limit], + ['%', 'span', :limit], + ['+', 'ins', :limit], + ['^', 'sup', :limit], + ['~', 'sub', :limit] + ] + QTAGS_JOIN = QTAGS.map {|rc, ht, rtype| Regexp::quote rc}.join('|') + + QTAGS.collect! do |rc, ht, rtype| + rcq = Regexp::quote rc + re = + case rtype + when :limit + /(^|[>\s\(]) # sta + (?!\-\-) + (#{QTAGS_JOIN}|) # oqs + (#{rcq}) # qtag + (\w|[^\s].*?[^\s]) # content + (?!\-\-) + #{rcq} + (#{QTAGS_JOIN}|) # oqa + (?=[[:punct:]]|<|\s|\)|$)/x + else + /(#{rcq}) + (#{C}) + (?::(\S+))? + (\w|[^\s\-].*?[^\s\-]) + #{rcq}/xm + end + [rc, ht, re, rtype] + end + + # Elements to handle + GLYPHS = [ + # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1’\2' ], # single closing + # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1’' ], # single closing + # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '’' ], # single closing + # [ /\'/, '‘' ], # single opening + # [ //, '>' ], # greater-than + # [ /([^\s\[{(])?"(\s|:|$)/, '\1”\2' ], # double closing + # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1”' ], # double closing + # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '”' ], # double closing + # [ /"/, '“' ], # double opening + # [ /\b( )?\.{3}/, '\1…' ], # ellipsis + # [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '\1' ], # 3+ uppercase acronym + # [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^\2\3', :no_span_caps ], # 3+ uppercase caps + # [ /(\.\s)?\s?--\s?/, '\1—' ], # em dash + # [ /\s->\s/, ' → ' ], # right arrow + # [ /\s-\s/, ' – ' ], # en dash + # [ /(\d+) ?x ?(\d+)/, '\1×\2' ], # dimension sign + # [ /\b ?[(\[]TM[\])]/i, '™' ], # trademark + # [ /\b ?[(\[]R[\])]/i, '®' ], # registered + # [ /\b ?[(\[]C[\])]/i, '©' ] # copyright + ] + + H_ALGN_VALS = { + '<' => 'left', + '=' => 'center', + '>' => 'right', + '<>' => 'justify' + } + + V_ALGN_VALS = { + '^' => 'top', + '-' => 'middle', + '~' => 'bottom' + } + + # + # Flexible HTML escaping + # + def htmlesc( str, mode=:Quotes ) + if str + str.gsub!( '&', '&' ) + str.gsub!( '"', '"' ) if mode != :NoQuotes + str.gsub!( "'", ''' ) if mode == :Quotes + str.gsub!( '<', '<') + str.gsub!( '>', '>') + end + str + end + + # Search and replace for Textile glyphs (quotes, dashes, other symbols) + def pgl( text ) + #GLYPHS.each do |re, resub, tog| + # next if tog and method( tog ).call + # text.gsub! re, resub + #end + text.gsub!(/\b([A-Z][A-Z0-9]{1,})\b(?:[(]([^)]*)[)])/) do |m| + "#{$1}" + end + end + + # Parses Textile attribute lists and builds an HTML attribute string + def pba( text_in, element = "" ) + + return '' unless text_in + + style = [] + text = text_in.dup + if element == 'td' + colspan = $1 if text =~ /\\(\d+)/ + rowspan = $1 if text =~ /\/(\d+)/ + style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN + end + + if text.sub!( /\{([^"}]*)\}/, '' ) && !filter_styles + sanitized = sanitize_styles($1) + style << "#{ sanitized };" unless sanitized.blank? + end + + lang = $1 if + text.sub!( /\[([^)]+?)\]/, '' ) + + cls = $1 if + text.sub!( /\(([^()]+?)\)/, '' ) + + style << "padding-left:#{ $1.length }em;" if + text.sub!( /([(]+)/, '' ) + + style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' ) + + style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN + + cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/ + + atts = '' + atts << " style=\"#{ style.join }\"" unless style.empty? + atts << " class=\"#{ cls }\"" unless cls.to_s.empty? + atts << " lang=\"#{ lang }\"" if lang + atts << " id=\"#{ id }\"" if id + atts << " colspan=\"#{ colspan }\"" if colspan + atts << " rowspan=\"#{ rowspan }\"" if rowspan + + atts + end + + STYLES_RE = /^(color|width|height|border|background|padding|margin|font|text|float)(-[a-z]+)*:\s*((\d+%?|\d+px|\d+(\.\d+)?em|#[0-9a-f]+|[a-z]+)\s*)+$/i + + def sanitize_styles(str) + styles = str.split(";").map(&:strip) + styles.reject! do |style| + !style.match(STYLES_RE) + end + styles.join(";") + end + + TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m + + # Parses a Textile table block, building HTML from the result. + def block_textile_table( text ) + text.gsub!( TABLE_RE ) do |matches| + + tatts, fullrow = $~[1..2] + tatts = pba( tatts, 'table' ) + tatts = shelve( tatts ) if tatts + rows = [] + + fullrow.each_line do |row| + ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m + cells = [] + row.split( /(\|)(?![^\[\|]*\]\])/ )[1..-2].each do |cell| + next if cell == '|' + ctyp = 'd' + ctyp = 'h' if cell =~ /^_/ + + catts = '' + catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/ + + catts = shelve( catts ) if catts + cells << "\t\t\t#{ cell }" + end + ratts = shelve( ratts ) if ratts + rows << "\t\t\n#{ cells.join( "\n" ) }\n\t\t" + end + "\t\n#{ rows.join( "\n" ) }\n\t\n\n" + end + end + + LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m + LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m + + # Parses Textile lists and generates HTML + def block_textile_lists( text ) + text.gsub!( LISTS_RE ) do |match| + lines = match.split( /\n/ ) + last_line = -1 + depth = [] + lines.each_with_index do |line, line_id| + if line =~ LISTS_CONTENT_RE + tl,atts,content = $~[1..3] + if depth.last + if depth.last.length > tl.length + (depth.length - 1).downto(0) do |i| + break if depth[i].length == tl.length + lines[line_id - 1] << "\n\t\n\t" + depth.pop + end + end + if depth.last and depth.last.length == tl.length + lines[line_id - 1] << '' + end + end + unless depth.last == tl + depth << tl + atts = pba( atts ) + atts = shelve( atts ) if atts + lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t
  • #{ content }" + else + lines[line_id] = "\t\t
  • #{ content }" + end + last_line = line_id + + else + last_line = line_id + end + if line_id - last_line > 1 or line_id == lines.length - 1 + while v = depth.pop + lines[last_line] << "
  • \n\t" + end + end + end + lines.join( "\n" ) + end + end + + QUOTES_RE = /(^>+([^\n]*?)(\n|$))+/m + QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m + + def block_textile_quotes( text ) + text.gsub!( QUOTES_RE ) do |match| + lines = match.split( /\n/ ) + quotes = '' + indent = 0 + lines.each do |line| + line =~ QUOTES_CONTENT_RE + bq,content = $1, $2 + l = bq.count('>') + if l != indent + quotes << ("\n\n" + (l>indent ? '
    ' * (l-indent) : '
    ' * (indent-l)) + "\n\n") + indent = l + end + quotes << (content + "\n") + end + quotes << ("\n" + '' * indent + "\n\n") + quotes + end + end + + CODE_RE = /(\W) + @ + (?:\|(\w+?)\|)? + (.+?) + @ + (?=\W)/x + + def inline_textile_code( text ) + text.gsub!( CODE_RE ) do |m| + before,lang,code,after = $~[1..4] + lang = " lang=\"#{ lang }\"" if lang + rip_offtags( "#{ before }#{ code }
    #{ after }", false ) + end + end + + def lT( text ) + text =~ /\#$/ ? 'o' : 'u' + end + + def hard_break( text ) + text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1
    " ) if hard_breaks + end + + BLOCKS_GROUP_RE = /\n{2,}(?! )/m + + def blocks( text, deep_code = false ) + text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk| + plain = blk !~ /\A[#*> ]/ + + # skip blocks that are complex HTML + if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1 + blk + else + # search for indentation levels + blk.strip! + if blk.empty? + blk + else + code_blk = nil + blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk| + flush_left iblk + blocks iblk, plain + iblk.gsub( /^(\S)/, "\t\\1" ) + if plain + code_blk = iblk; "" + else + iblk + end + end + + block_applied = 0 + @rules.each do |rule_name| + block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) ) + end + if block_applied.zero? + if deep_code + blk = "\t
    #{ blk }
    " + else + blk = "\t

    #{ blk }

    " + end + end + # hard_break blk + blk + "\n#{ code_blk }" + end + end + + end.join( "\n\n" ) ) + end + + def textile_bq( tag, atts, cite, content ) + cite, cite_title = check_refs( cite ) + cite = " cite=\"#{ cite }\"" if cite + atts = shelve( atts ) if atts + "\t\n\t\t#{ content }

    \n\t" + end + + def textile_p( tag, atts, cite, content ) + atts = shelve( atts ) if atts + "\t<#{ tag }#{ atts }>#{ content }" + end + + alias textile_h1 textile_p + alias textile_h2 textile_p + alias textile_h3 textile_p + alias textile_h4 textile_p + alias textile_h5 textile_p + alias textile_h6 textile_p + + def textile_fn_( tag, num, atts, cite, content ) + atts << " id=\"fn#{ num }\" class=\"footnote\"" + content = "#{ num } #{ content }" + atts = shelve( atts ) if atts + "\t#{ content }

    " + end + + BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m + + def block_textile_prefix( text ) + if text =~ BLOCK_RE + tag,tagpre,num,atts,cite,content = $~[1..6] + atts = pba( atts ) + + # pass to prefix handler + replacement = nil + if respond_to? "textile_#{ tag }", true + replacement = method( "textile_#{ tag }" ).call( tag, atts, cite, content ) + elsif respond_to? "textile_#{ tagpre }_", true + replacement = method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content ) + end + text.gsub!( $& ) { replacement } if replacement + end + end + + SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m + def block_markdown_setext( text ) + if text =~ SETEXT_RE + tag = if $2 == "="; "h1"; else; "h2"; end + blk, cont = "<#{ tag }>#{ $1 }", $' + blocks cont + text.replace( blk + cont ) + end + end + + ATX_RE = /\A(\#{1,6}) # $1 = string of #'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #'s (not counted) + $/x + def block_markdown_atx( text ) + if text =~ ATX_RE + tag = "h#{ $1.length }" + blk, cont = "<#{ tag }>#{ $2 }\n\n", $' + blocks cont + text.replace( blk + cont ) + end + end + + MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m + + def block_markdown_bq( text ) + text.gsub!( MARKDOWN_BQ_RE ) do |blk| + blk.gsub!( /^ *> ?/, '' ) + flush_left blk + blocks blk + blk.gsub!( /^(\S)/, "\t\\1" ) + "
    \n#{ blk }\n
    \n\n" + end + end + + MARKDOWN_RULE_RE = /^(#{ + ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' ) + })$/ + + def block_markdown_rule( text ) + text.gsub!( MARKDOWN_RULE_RE ) do |blk| + "
    " + end + end + + # XXX TODO XXX + def block_markdown_lists( text ) + end + + def inline_textile_span( text ) + QTAGS.each do |qtag_rc, ht, qtag_re, rtype| + text.gsub!( qtag_re ) do |m| + + case rtype + when :limit + sta,oqs,qtag,content,oqa = $~[1..6] + atts = nil + if content =~ /^(#{C})(.+)$/ + atts, content = $~[1..2] + end + else + qtag,atts,cite,content = $~[1..4] + sta = '' + end + atts = pba( atts ) + atts = shelve( atts ) if atts + + "#{ sta }#{ oqs }<#{ ht }#{ atts }>#{ content }#{ oqa }" + + end + end + end + + LINK_RE = / + ( + ([\s\[{(]|[#{PUNCT}])? # $pre + " # start + (#{C}) # $atts + ([^"\n]+?) # $text + \s? + (?:\(([^)]+?)\)(?="))? # $title + ": + ( # $url + (\/|[a-zA-Z]+:\/\/|www\.|mailto:) # $proto + [[:alnum:]_\/]\S+? + ) + (\/)? # $slash + ([^[:alnum:]_\=\/;\(\)]*?) # $post + ) + (?=<|\s|$) + /x +#" + def inline_textile_link( text ) + text.gsub!( LINK_RE ) do |m| + all,pre,atts,text,title,url,proto,slash,post = $~[1..9] + if text.include?('
    ') + all + else + url, url_title = check_refs( url ) + title ||= url_title + + # Idea below : an URL with unbalanced parethesis and + # ending by ')' is put into external parenthesis + if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) ) + url=url[0..-2] # discard closing parenth from url + post = ")"+post # add closing parenth to post + end + atts = pba( atts ) + atts = " href=\"#{ htmlesc url }#{ slash }\"#{ atts }" + atts << " title=\"#{ htmlesc title }\"" if title + atts = shelve( atts ) if atts + + external = (url =~ /^https?:\/\//) ? ' class="external"' : '' + + "#{ pre }#{ text }#{ post }" + end + end + end + + MARKDOWN_REFLINK_RE = / + \[([^\[\]]+)\] # $text + [ ]? # opt. space + (?:\n[ ]*)? # one optional newline followed by spaces + \[(.*?)\] # $id + /x + + def inline_markdown_reflink( text ) + text.gsub!( MARKDOWN_REFLINK_RE ) do |m| + text, id = $~[1..2] + + if id.empty? + url, title = check_refs( text ) + else + url, title = check_refs( id ) + end + + atts = " href=\"#{ url }\"" + atts << " title=\"#{ title }\"" if title + atts = shelve( atts ) + + "#{ text }" + end + end + + MARKDOWN_LINK_RE = / + \[([^\[\]]+)\] # $text + \( # open paren + [ \t]* # opt space + ? # $href + [ \t]* # opt space + (?: # whole title + (['"]) # $quote + (.*?) # $title + \3 # matching quote + )? # title is optional + \) + /x + + def inline_markdown_link( text ) + text.gsub!( MARKDOWN_LINK_RE ) do |m| + text, url, quote, title = $~[1..4] + + atts = " href=\"#{ url }\"" + atts << " title=\"#{ title }\"" if title + atts = shelve( atts ) + + "#{ text }" + end + end + + TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/ + MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m + + def refs( text ) + @rules.each do |rule_name| + method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/ + end + end + + def refs_textile( text ) + text.gsub!( TEXTILE_REFS_RE ) do |m| + flag, url = $~[2..3] + @urlrefs[flag.downcase] = [url, nil] + nil + end + end + + def refs_markdown( text ) + text.gsub!( MARKDOWN_REFS_RE ) do |m| + flag, url = $~[2..3] + title = $~[6] + @urlrefs[flag.downcase] = [url, title] + nil + end + end + + def check_refs( text ) + ret = @urlrefs[text.downcase] if text + ret || [text, nil] + end + + IMAGE_RE = / + (>|\s|^) # start of line? + \! # opening + (\<|\=|\>)? # optional alignment atts + (#{C}) # optional style,class atts + (?:\. )? # optional dot-space + ([^\s(!]+?) # presume this is the src + \s? # optional space + (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title + \! # closing + (?::#{ HYPERLINK })? # optional href + /x + + def inline_textile_image( text ) + text.gsub!( IMAGE_RE ) do |m| + stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8] + htmlesc title + atts = pba( atts ) + atts = " src=\"#{ htmlesc url.dup }\"#{ atts }" + atts << " title=\"#{ title }\"" if title + atts << " alt=\"#{ title }\"" + # size = @getimagesize($url); + # if($size) $atts.= " $size[3]"; + + href, alt_title = check_refs( href ) if href + url, url_title = check_refs( url ) + + out = '' + out << "" if href + out << "" + out << "#{ href_a1 }#{ href_a2 }" if href + + if algn + algn = h_align( algn ) + if stln == "

    " + out = "

    #{ out }" + else + out = "#{ stln }

    #{ out }
    " + end + else + out = stln + out + end + + out + end + end + + def shelve( val ) + @shelf << val + " :redsh##{ @shelf.length }:" + end + + def retrieve( text ) + @shelf.each_with_index do |r, i| + text.gsub!( " :redsh##{ i + 1 }:", r ) + end + end + + def incoming_entities( text ) + ## turn any incoming ampersands into a dummy character for now. + ## This uses a negative lookahead for alphanumerics followed by a semicolon, + ## implying an incoming html entity, to be skipped + + text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" ) + end + + def no_textile( text ) + text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/, + '\1\2\3' ) + text.gsub!( /^ *==([^=]+.*?)==/m, + '\1\2\3' ) + end + + def clean_white_space( text ) + # normalize line breaks + text.gsub!( /\r\n/, "\n" ) + text.gsub!( /\r/, "\n" ) + text.gsub!( /\t/, ' ' ) + text.gsub!( /^ +$/, '' ) + text.gsub!( /\n{3,}/, "\n\n" ) + text.gsub!( /"$/, "\" " ) + + # if entire document is indented, flush + # to the left side + flush_left text + end + + def flush_left( text ) + indt = 0 + if text =~ /^ / + while text !~ /^ {#{indt}}\S/ + indt += 1 + end unless text.empty? + if indt.nonzero? + text.gsub!( /^ {#{indt}}/, '' ) + end + end + end + + def footnote_ref( text ) + text.gsub!( /\b\[([0-9]+?)\](\s)?/, + '\1\2' ) + end + + OFFTAGS = /(code|pre|kbd|notextile)/ + OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }\W|\Z)/mi + OFFTAG_OPEN = /<#{ OFFTAGS }/ + OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/ + HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m + ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m + + def glyphs_textile( text, level = 0 ) + if text !~ HASTAG_MATCH + pgl text + footnote_ref text + else + codepre = 0 + text.gsub!( ALLTAG_MATCH ) do |line| + ## matches are off if we're between ,
     etc.
    +                if $1
    +                    if line =~ OFFTAG_OPEN
    +                        codepre += 1
    +                    elsif line =~ OFFTAG_CLOSE
    +                        codepre -= 1
    +                        codepre = 0 if codepre < 0
    +                    end 
    +                elsif codepre.zero?
    +                    glyphs_textile( line, level + 1 )
    +                else
    +                    htmlesc( line, :NoQuotes )
    +                end
    +                # p [level, codepre, line]
    +
    +                line
    +            end
    +        end
    +    end
    +
    +    def rip_offtags( text, escape_aftertag=true, escape_line=true )
    +        if text =~ /<.*>/
    +            ## strip and encode 
     content
    +            codepre, used_offtags = 0, {}
    +            text.gsub!( OFFTAG_MATCH ) do |line|
    +                if $3
    +                    first, offtag, aftertag = $3, $4, $5
    +                    codepre += 1
    +                    used_offtags[offtag] = true
    +                    if codepre - used_offtags.length > 0
    +                        htmlesc( line, :NoQuotes ) if escape_line
    +                        @pre_list.last << line
    +                        line = ""
    +                    else
    +                        ### htmlesc is disabled between CODE tags which will be parsed with highlighter
    +                        ### Regexp in formatter.rb is : /\s?(.+)/m
    +                        ### NB: some changes were made not to use $N variables, because we use "match"
    +                        ###   and it breaks following lines
    +                        htmlesc( aftertag, :NoQuotes ) if aftertag && escape_aftertag && !first.match(//)
    +                        line = ""
    +                        first.match(/<#{ OFFTAGS }([^>]*)>/)
    +                        tag = $1
    +                        $2.to_s.match(/(class\=("[^"]+"|'[^']+'))/i)
    +                        tag << " #{$1}" if $1
    +                        @pre_list << "<#{ tag }>#{ aftertag }"
    +                    end
    +                elsif $1 and codepre > 0
    +                    if codepre - used_offtags.length > 0
    +                        htmlesc( line, :NoQuotes ) if escape_line
    +                        @pre_list.last << line
    +                        line = ""
    +                    end
    +                    codepre -= 1 unless codepre.zero?
    +                    used_offtags = {} if codepre.zero?
    +                end 
    +                line
    +            end
    +        end
    +        text
    +    end
    +
    +    def smooth_offtags( text )
    +        unless @pre_list.empty?
    +            ## replace 
     content
    +            text.gsub!( // ) { @pre_list[$1.to_i] }
    +        end
    +    end
    +
    +    def inline( text ) 
    +        [/^inline_/, /^glyphs_/].each do |meth_re|
    +            @rules.each do |rule_name|
    +                method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
    +            end
    +        end
    +    end
    +
    +    def h_align( text ) 
    +        H_ALGN_VALS[text]
    +    end
    +
    +    def v_align( text ) 
    +        V_ALGN_VALS[text]
    +    end
    +
    +    def textile_popup_help( name, windowW, windowH )
    +        ' ' + name + '
    ' + end + + # HTML cleansing stuff + BASIC_TAGS = { + 'a' => ['href', 'title'], + 'img' => ['src', 'alt', 'title'], + 'br' => [], + 'i' => nil, + 'u' => nil, + 'b' => nil, + 'pre' => nil, + 'kbd' => nil, + 'code' => ['lang'], + 'cite' => nil, + 'strong' => nil, + 'em' => nil, + 'ins' => nil, + 'sup' => nil, + 'sub' => nil, + 'del' => nil, + 'table' => nil, + 'tr' => nil, + 'td' => ['colspan', 'rowspan'], + 'th' => nil, + 'ol' => nil, + 'ul' => nil, + 'li' => nil, + 'p' => nil, + 'h1' => nil, + 'h2' => nil, + 'h3' => nil, + 'h4' => nil, + 'h5' => nil, + 'h6' => nil, + 'blockquote' => ['cite'] + } + + def clean_html( text, tags = BASIC_TAGS ) + text.gsub!( /]*)>/ ) do + raw = $~ + tag = raw[2].downcase + if tags.has_key? tag + pcs = [tag] + tags[tag].each do |prop| + ['"', "'", ''].each do |q| + q2 = ( q != '' ? q : '\s' ) + if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i + attrv = $1 + next if prop == 'src' and attrv =~ %r{^(?!http)\w+:} + pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\"" + break + end + end + end if tags[tag] + "<#{raw[1]}#{pcs.join " "}>" + else + " " + end + end + end + + ALLOWED_TAGS = %w(redpre pre code notextile) + + def escape_html_tags(text) + text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "<#{$1}#{'>' unless $3.blank?}" } + end +end + diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fa/fa2c839ac66756a565560a357b756991bf2642f9.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fa/fa2c839ac66756a565560a357b756991bf2642f9.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,2 @@ +$('#ajax-modal').html('<%= escape_javascript(render :partial => 'versions/new_modal') %>'); +showModal('ajax-modal', '600px'); diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fa/fa4c6495ebff9e1214dc90dd2c21d6f78f2cf2e7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fa/fa4c6495ebff9e1214dc90dd2c21d6f78f2cf2e7.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,67 @@ +api.issue do + api.id @issue.id + api.project(:id => @issue.project_id, :name => @issue.project.name) unless @issue.project.nil? + api.tracker(:id => @issue.tracker_id, :name => @issue.tracker.name) unless @issue.tracker.nil? + api.status(:id => @issue.status_id, :name => @issue.status.name) unless @issue.status.nil? + api.priority(:id => @issue.priority_id, :name => @issue.priority.name) unless @issue.priority.nil? + api.author(:id => @issue.author_id, :name => @issue.author.name) unless @issue.author.nil? + api.assigned_to(:id => @issue.assigned_to_id, :name => @issue.assigned_to.name) unless @issue.assigned_to.nil? + api.category(:id => @issue.category_id, :name => @issue.category.name) unless @issue.category.nil? + api.fixed_version(:id => @issue.fixed_version_id, :name => @issue.fixed_version.name) unless @issue.fixed_version.nil? + api.parent(:id => @issue.parent_id) unless @issue.parent.nil? + + api.subject @issue.subject + api.description @issue.description + api.start_date @issue.start_date + api.due_date @issue.due_date + api.done_ratio @issue.done_ratio + api.estimated_hours @issue.estimated_hours + api.spent_hours(@issue.spent_hours) if User.current.allowed_to?(:view_time_entries, @project) + + render_api_custom_values @issue.custom_field_values, api + + api.created_on @issue.created_on + api.updated_on @issue.updated_on + + render_api_issue_children(@issue, api) if include_in_api_response?('children') + + api.array :attachments do + @issue.attachments.each do |attachment| + render_api_attachment(attachment, api) + end + end if include_in_api_response?('attachments') + + api.array :relations do + @relations.each do |relation| + api.relation(:id => relation.id, :issue_id => relation.issue_from_id, :issue_to_id => relation.issue_to_id, :relation_type => relation.relation_type, :delay => relation.delay) + end + end if include_in_api_response?('relations') && @relations.present? + + api.array :changesets do + @issue.changesets.each do |changeset| + api.changeset :revision => changeset.revision do + api.user(:id => changeset.user_id, :name => changeset.user.name) unless changeset.user.nil? + api.comments changeset.comments + api.committed_on changeset.committed_on + end + end + end if include_in_api_response?('changesets') && User.current.allowed_to?(:view_changesets, @project) + + api.array :journals do + @journals.each do |journal| + api.journal :id => journal.id do + api.user(:id => journal.user_id, :name => journal.user.name) unless journal.user.nil? + api.notes journal.notes + api.created_on journal.created_on + api.array :details do + journal.details.each do |detail| + api.detail :property => detail.property, :name => detail.prop_key do + api.old_value detail.old_value + api.new_value detail.value + end + end + end + end + end + end if include_in_api_response?('journals') +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fa/fab2fb0765f825d8bc1189c4084f5886082c476a.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fa/fab2fb0765f825d8bc1189c4084f5886082c476a.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,41 @@ +
    +<% if @block_options.present? %> + <%= form_tag({:action => "add_block"}, :id => "block-form") do %> + <%= label_tag('block-select', l(:label_my_page_block)) %>: + <%= select_tag 'block', + content_tag('option') + options_for_select(@block_options), + :id => "block-select" %> + <%= link_to l(:button_add), '#', :onclick => '$("#block-form").submit()', :class => 'icon icon-add' %> + <% end %> +<% end %> +<%= link_to l(:button_back), {:action => 'page'}, :class => 'icon icon-cancel' %> +
    + +

    <%=l(:label_my_page)%>

    + +
    + <% @blocks['top'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> + <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %> + <% end if @blocks['top'] %> +
    + +
    + <% @blocks['left'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> + <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %> + <% end if @blocks['left'] %> +
    + +
    + <% @blocks['right'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> + <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %> + <% end if @blocks['right'] %> +
    + +<%= javascript_tag "initMyPageSortable('top', '#{ escape_javascript url_for(:action => "order_blocks", :group => "top") }');" %> +<%= javascript_tag "initMyPageSortable('left', '#{ escape_javascript url_for(:action => "order_blocks", :group => "left") }');" %> +<%= javascript_tag "initMyPageSortable('right', '#{ escape_javascript url_for(:action => "order_blocks", :group => "right") }');" %> + +<% html_title(l(:label_my_page)) -%> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fb/fb08a7fc94c83309a3c9970df3838fd57ed7a246.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fb/fb08a7fc94c83309a3c9970df3838fd57ed7a246.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,213 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) +require 'messages_controller' + +# Re-raise errors caught by the controller. +class MessagesController; def rescue_action(e) raise e end; end + +class MessagesControllerTest < ActionController::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules + + def setup + @controller = MessagesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_show + get :show, :board_id => 1, :id => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:board) + assert_not_nil assigns(:project) + assert_not_nil assigns(:topic) + end + + def test_show_should_contain_reply_field_tags_for_quoting + @request.session[:user_id] = 2 + get :show, :board_id => 1, :id => 1 + assert_response :success + + # tags required by MessagesController#quote + assert_tag 'input', :attributes => {:id => 'message_subject'} + assert_tag 'textarea', :attributes => {:id => 'message_content'} + assert_tag 'div', :attributes => {:id => 'reply'} + end + + def test_show_with_pagination + message = Message.find(1) + assert_difference 'Message.count', 30 do + 30.times do + message.children << Message.new(:subject => 'Reply', :content => 'Reply body', :author_id => 2, :board_id => 1) + end + end + get :show, :board_id => 1, :id => 1, :r => message.children.last(:order => 'id').id + assert_response :success + assert_template 'show' + replies = assigns(:replies) + assert_not_nil replies + assert !replies.include?(message.children.first(:order => 'id')) + assert replies.include?(message.children.last(:order => 'id')) + end + + def test_show_with_reply_permission + @request.session[:user_id] = 2 + get :show, :board_id => 1, :id => 1 + assert_response :success + assert_template 'show' + assert_tag :div, :attributes => { :id => 'reply' }, + :descendant => { :tag => 'textarea', :attributes => { :id => 'message_content' } } + end + + def test_show_message_not_found + get :show, :board_id => 1, :id => 99999 + assert_response 404 + end + + def test_get_new + @request.session[:user_id] = 2 + get :new, :board_id => 1 + assert_response :success + assert_template 'new' + end + + def test_post_new + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + + with_settings :notified_events => %w(message_posted) do + post :new, :board_id => 1, + :message => { :subject => 'Test created message', + :content => 'Message body'} + end + message = Message.find_by_subject('Test created message') + assert_not_nil message + assert_redirected_to "/boards/1/topics/#{message.to_param}" + assert_equal 'Message body', message.content + assert_equal 2, message.author_id + assert_equal 1, message.board_id + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert_equal "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] Test created message", mail.subject + assert_mail_body_match 'Message body', mail + # author + assert mail.bcc.include?('jsmith@somenet.foo') + # project member + assert mail.bcc.include?('dlopper@somenet.foo') + end + + def test_get_edit + @request.session[:user_id] = 2 + get :edit, :board_id => 1, :id => 1 + assert_response :success + assert_template 'edit' + end + + def test_post_edit + @request.session[:user_id] = 2 + post :edit, :board_id => 1, :id => 1, + :message => { :subject => 'New subject', + :content => 'New body'} + assert_redirected_to '/boards/1/topics/1' + message = Message.find(1) + assert_equal 'New subject', message.subject + assert_equal 'New body', message.content + end + + def test_post_edit_sticky_and_locked + @request.session[:user_id] = 2 + post :edit, :board_id => 1, :id => 1, + :message => { :subject => 'New subject', + :content => 'New body', + :locked => '1', + :sticky => '1'} + assert_redirected_to '/boards/1/topics/1' + message = Message.find(1) + assert_equal true, message.sticky? + assert_equal true, message.locked? + end + + def test_post_edit_should_allow_to_change_board + @request.session[:user_id] = 2 + post :edit, :board_id => 1, :id => 1, + :message => { :subject => 'New subject', + :content => 'New body', + :board_id => 2} + assert_redirected_to '/boards/2/topics/1' + message = Message.find(1) + assert_equal Board.find(2), message.board + end + + def test_reply + @request.session[:user_id] = 2 + post :reply, :board_id => 1, :id => 1, :reply => { :content => 'This is a test reply', :subject => 'Test reply' } + reply = Message.find(:first, :order => 'id DESC') + assert_redirected_to "/boards/1/topics/1?r=#{reply.id}" + assert Message.find_by_subject('Test reply') + end + + def test_destroy_topic + @request.session[:user_id] = 2 + assert_difference 'Message.count', -3 do + post :destroy, :board_id => 1, :id => 1 + end + assert_redirected_to '/projects/ecookbook/boards/1' + assert_nil Message.find_by_id(1) + end + + def test_destroy_reply + @request.session[:user_id] = 2 + assert_difference 'Message.count', -1 do + post :destroy, :board_id => 1, :id => 2 + end + assert_redirected_to '/boards/1/topics/1?r=2' + assert_nil Message.find_by_id(2) + end + + def test_quote + @request.session[:user_id] = 2 + xhr :get, :quote, :board_id => 1, :id => 3 + assert_response :success + assert_equal 'text/javascript', response.content_type + assert_template 'quote' + assert_include 'RE: First post', response.body + assert_include '> An other reply', response.body + end + + def test_preview_new + @request.session[:user_id] = 2 + post :preview, + :board_id => 1, + :message => {:subject => "", :content => "Previewed text"} + assert_response :success + assert_template 'common/_preview' + end + + def test_preview_edit + @request.session[:user_id] = 2 + post :preview, + :id => 4, + :board_id => 1, + :message => {:subject => "", :content => "Previewed text"} + assert_response :success + assert_template 'common/_preview' + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fb/fb831ceb031f77a301df0cd90da94f30274b8153.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fb/fb831ceb031f77a301df0cd90da94f30274b8153.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,164 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class AdminControllerTest < ActionController::TestCase + fixtures :projects, :users, :roles + + def setup + User.current = nil + @request.session[:user_id] = 1 # admin + end + + def test_index + get :index + assert_no_tag :tag => 'div', + :attributes => { :class => /nodata/ } + end + + def test_index_with_no_configuration_data + delete_configuration_data + get :index + assert_tag :tag => 'div', + :attributes => { :class => /nodata/ } + end + + def test_projects + get :projects + assert_response :success + assert_template 'projects' + assert_not_nil assigns(:projects) + # active projects only + assert_nil assigns(:projects).detect {|u| !u.active?} + end + + def test_projects_with_status_filter + get :projects, :status => 1 + assert_response :success + assert_template 'projects' + assert_not_nil assigns(:projects) + # active projects only + assert_nil assigns(:projects).detect {|u| !u.active?} + end + + def test_projects_with_name_filter + get :projects, :name => 'store', :status => '' + assert_response :success + assert_template 'projects' + projects = assigns(:projects) + assert_not_nil projects + assert_equal 1, projects.size + assert_equal 'OnlineStore', projects.first.name + end + + def test_load_default_configuration_data + delete_configuration_data + post :default_configuration, :lang => 'fr' + assert_response :redirect + assert_nil flash[:error] + assert IssueStatus.find_by_name('Nouveau') + end + + def test_load_default_configuration_data_should_rescue_error + delete_configuration_data + Redmine::DefaultData::Loader.stubs(:load).raises(Exception.new("Something went wrong")) + post :default_configuration, :lang => 'fr' + assert_response :redirect + assert_not_nil flash[:error] + assert_match /Something went wrong/, flash[:error] + end + + def test_test_email + user = User.find(1) + user.pref[:no_self_notified] = '1' + user.pref.save! + ActionMailer::Base.deliveries.clear + + get :test_email + assert_redirected_to '/settings/edit?tab=notifications' + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + user = User.find(1) + assert_equal [user.mail], mail.bcc + end + + def test_test_email_failure_should_display_the_error + Mailer.stubs(:test_email).raises(Exception, 'Some error message') + get :test_email + assert_redirected_to '/settings/edit?tab=notifications' + assert_match /Some error message/, flash[:error] + end + + def test_no_plugins + Redmine::Plugin.clear + + get :plugins + assert_response :success + assert_template 'plugins' + end + + def test_plugins + # Register a few plugins + Redmine::Plugin.register :foo do + name 'Foo plugin' + author 'John Smith' + description 'This is a test plugin' + version '0.0.1' + settings :default => {'sample_setting' => 'value', 'foo'=>'bar'}, :partial => 'foo/settings' + end + Redmine::Plugin.register :bar do + end + + get :plugins + assert_response :success + assert_template 'plugins' + + assert_tag :td, :child => { :tag => 'span', :content => 'Foo plugin' } + assert_tag :td, :child => { :tag => 'span', :content => 'Bar' } + end + + def test_info + get :info + assert_response :success + assert_template 'info' + end + + def test_admin_menu_plugin_extension + Redmine::MenuManager.map :admin_menu do |menu| + menu.push :test_admin_menu_plugin_extension, '/foo/bar', :caption => 'Test' + end + + get :index + assert_response :success + assert_tag :a, :attributes => { :href => '/foo/bar' }, + :content => 'Test' + + Redmine::MenuManager.map :admin_menu do |menu| + menu.delete :test_admin_menu_plugin_extension + end + end + + private + + def delete_configuration_data + Role.delete_all('builtin = 0') + Tracker.delete_all + IssueStatus.delete_all + Enumeration.delete_all + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fb/fb9f7cf41b7a6b4d9de67a8872479cba6bfe93fe.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fb/fb9f7cf41b7a6b4d9de67a8872479cba6bfe93fe.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,46 @@ +--- +projects_trackers_001: + project_id: 4 + tracker_id: 3 +projects_trackers_002: + project_id: 1 + tracker_id: 1 +projects_trackers_003: + project_id: 5 + tracker_id: 1 +projects_trackers_004: + project_id: 1 + tracker_id: 2 +projects_trackers_005: + project_id: 5 + tracker_id: 2 +projects_trackers_006: + project_id: 5 + tracker_id: 3 +projects_trackers_007: + project_id: 2 + tracker_id: 1 +projects_trackers_008: + project_id: 2 + tracker_id: 2 +projects_trackers_009: + project_id: 2 + tracker_id: 3 +projects_trackers_010: + project_id: 3 + tracker_id: 2 +projects_trackers_011: + project_id: 3 + tracker_id: 3 +projects_trackers_012: + project_id: 4 + tracker_id: 1 +projects_trackers_013: + project_id: 4 + tracker_id: 2 +projects_trackers_014: + project_id: 1 + tracker_id: 3 +projects_trackers_015: + project_id: 6 + tracker_id: 1 diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fb/fbc68f3692f8e2cc3c5089591e7d79fb0fb3bac7.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fb/fbc68f3692f8e2cc3c5089591e7d79fb0fb3bac7.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,58 @@ +<%= render :partial => 'action_menu' %> + +

    <%=l(:label_workflow)%>

    + +
    +
      +
    • <%= link_to l(:label_status_transitions), {:action => 'edit', :role_id => @role, :tracker_id => @tracker}, :class => 'selected' %>
    • +
    • <%= link_to l(:label_fields_permissions), {:action => 'permissions', :role_id => @role, :tracker_id => @tracker} %>
    • +
    +
    + +

    <%=l(:text_workflow_edit)%>:

    + +<%= form_tag({}, :method => 'get') do %> +

    + + + + + <%= submit_tag l(:button_edit), :name => nil %> + + <%= hidden_field_tag 'used_statuses_only', '0' %> + + +

    +<% end %> + +<% if @tracker && @role && @statuses.any? %> + <%= form_tag({}, :id => 'workflow_form' ) do %> + <%= hidden_field_tag 'tracker_id', @tracker.id %> + <%= hidden_field_tag 'role_id', @role.id %> + <%= hidden_field_tag 'used_statuses_only', params[:used_statuses_only] %> +
    + <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %> + +
    + <%= l(:label_additional_workflow_transitions_for_author) %> +
    + <%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %> +
    +
    + <%= javascript_tag "hideFieldset($('#author_workflows'))" unless @workflows['author'].present? %> + +
    + <%= l(:label_additional_workflow_transitions_for_assignee) %> +
    + <%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %> +
    +
    + <%= javascript_tag "hideFieldset($('#assignee_workflows'))" unless @workflows['assignee'].present? %> +
    + <%= submit_tag l(:button_save) %> + <% end %> +<% end %> + +<% html_title(l(:label_workflow)) -%> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fb/fbd9312384b9108653ac87420225c6ac2b8787f0.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fb/fbd9312384b9108653ac87420225c6ac2b8787f0.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,77 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../test_helper', __FILE__) + +class MenuManagerTest < ActionController::IntegrationTest + include Redmine::I18n + + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :workflows + + def test_project_menu_with_specific_locale + get 'projects/ecookbook/issues', { }, 'HTTP_ACCEPT_LANGUAGE' => 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3' + + assert_tag :div, :attributes => { :id => 'main-menu' }, + :descendant => { :tag => 'li', :child => { :tag => 'a', :content => ll('fr', :label_activity), + :attributes => { :href => '/projects/ecookbook/activity', + :class => 'activity' } } } + assert_tag :div, :attributes => { :id => 'main-menu' }, + :descendant => { :tag => 'li', :child => { :tag => 'a', :content => ll('fr', :label_issue_plural), + :attributes => { :href => '/projects/ecookbook/issues', + :class => 'issues selected' } } } + end + + def test_project_menu_with_additional_menu_items + Setting.default_language = 'en' + assert_no_difference 'Redmine::MenuManager.items(:project_menu).size' do + Redmine::MenuManager.map :project_menu do |menu| + menu.push :foo, { :controller => 'projects', :action => 'show' }, :caption => 'Foo' + menu.push :bar, { :controller => 'projects', :action => 'show' }, :before => :activity + menu.push :hello, { :controller => 'projects', :action => 'show' }, :caption => Proc.new {|p| p.name.upcase }, :after => :bar + end + + get 'projects/ecookbook' + assert_tag :div, :attributes => { :id => 'main-menu' }, + :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Foo', + :attributes => { :class => 'foo' } } } + + assert_tag :div, :attributes => { :id => 'main-menu' }, + :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Bar', + :attributes => { :class => 'bar' } }, + :before => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' } } } + + assert_tag :div, :attributes => { :id => 'main-menu' }, + :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK', + :attributes => { :class => 'hello' } }, + :before => { :tag => 'li', :child => { :tag => 'a', :content => 'Activity' } } } + + # Remove the menu items + Redmine::MenuManager.map :project_menu do |menu| + menu.delete :foo + menu.delete :bar + menu.delete :hello + end + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fb/fbe58c1be039b03da7e1b24c3b52ccb21c06900d.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fb/fbe58c1be039b03da7e1b24c3b52ccb21c06900d.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,3 @@ +api.upload do + api.token @attachment.token +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fc/fc01cb7f3c8f3eb9966701ee1b49005a54a9c291.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fc/fc01cb7f3c8f3eb9966701ee1b49005a54a9c291.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1081 @@ +# Bulgarian translation by Nikolay Solakov and Ivan Cenov +bg: + # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) + direction: ltr + date: + formats: + # Use the strftime parameters for formats. + # When no format has been given, it uses default. + # You can provide other formats here if you like! + default: "%d-%m-%Y" + short: "%b %d" + long: "%B %d, %Y" + + day_names: [ÐеделÑ, Понеделник, Вторник, СрÑда, Четвъртък, Петък, Събота] + abbr_day_names: [Ðед, Пон, Вто, СрÑ, Чет, Пет, Съб] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, Януари, Февруари, Март, Ðприл, Май, Юни, Юли, ÐвгуÑÑ‚, Септември, Октомври, Ðоември, Декември] + abbr_month_names: [~, Яну, Фев, Мар, Ðпр, Май, Юни, Юли, Ðвг, Сеп, Окт, Ðое, Дек] + # Used in date_select and datime_select. + order: + - :year + - :month + - :day + + time: + formats: + default: "%a, %d %b %Y %H:%M:%S %z" + time: "%H:%M" + short: "%d %b %H:%M" + long: "%B %d, %Y %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "half a minute" + less_than_x_seconds: + one: "по-малко от 1 Ñекунда" + other: "по-малко от %{count} Ñекунди" + x_seconds: + one: "1 Ñекунда" + other: "%{count} Ñекунди" + less_than_x_minutes: + one: "по-малко от 1 минута" + other: "по-малко от %{count} минути" + x_minutes: + one: "1 минута" + other: "%{count} минути" + about_x_hours: + one: "около 1 чаÑ" + other: "около %{count} чаÑа" + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "1 ден" + other: "%{count} дена" + about_x_months: + one: "около 1 меÑец" + other: "около %{count} меÑеца" + x_months: + one: "1 меÑец" + other: "%{count} меÑеца" + about_x_years: + one: "около 1 година" + other: "около %{count} години" + over_x_years: + one: "над 1 година" + other: "над %{count} години" + almost_x_years: + one: "почти 1 година" + other: "почти %{count} години" + + number: + format: + separator: "." + delimiter: "" + precision: 3 + + human: + format: + delimiter: "" + precision: 3 + storage_units: + format: "%n %u" + units: + byte: + one: байт + other: байта + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + +# Used in array.to_sentence. + support: + array: + sentence_connector: "и" + skip_last_comma: false + + activerecord: + errors: + template: + header: + one: "1 грешка попречи този %{model} да бъде запиÑан" + other: "%{count} грешки попречиха този %{model} да бъде запиÑан" + messages: + inclusion: "не ÑъщеÑтвува в ÑпиÑъка" + exclusion: "е запазено" + invalid: "е невалидно" + confirmation: "липÑва одобрение" + accepted: "трÑбва да Ñе приеме" + empty: "не може да е празно" + blank: "не може да е празно" + too_long: "е прекалено дълго" + too_short: "е прекалено къÑо" + wrong_length: "е Ñ Ð³Ñ€ÐµÑˆÐ½Ð° дължина" + taken: "вече ÑъщеÑтвува" + not_a_number: "не е чиÑло" + not_a_date: "е невалидна дата" + greater_than: "трÑбва да бъде по-голÑм[a/о] от %{count}" + greater_than_or_equal_to: "трÑбва да бъде по-голÑм[a/о] от или равен[a/o] на %{count}" + equal_to: "трÑбва да бъде равен[a/o] на %{count}" + less_than: "трÑбва да бъде по-малък[a/o] от %{count}" + less_than_or_equal_to: "трÑбва да бъде по-малък[a/o] от или равен[a/o] на %{count}" + odd: "трÑбва да бъде нечетен[a/o]" + even: "трÑбва да бъде четен[a/o]" + greater_than_start_date: "трÑбва да е Ñлед началната дата" + not_same_project: "не е от ÑÑŠÑ‰Ð¸Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚" + circular_dependency: "Тази Ñ€ÐµÐ»Ð°Ñ†Ð¸Ñ Ñ‰Ðµ доведе до безкрайна завиÑимоÑÑ‚" + cant_link_an_issue_with_a_descendant: "Една задача не може да бъде Ñвързвана към ÑÐ²Ð¾Ñ Ð¿Ð¾Ð´Ð·Ð°Ð´Ð°Ñ‡Ð°" + + actionview_instancetag_blank_option: Изберете + + general_text_No: 'Ðе' + general_text_Yes: 'Да' + general_text_no: 'не' + general_text_yes: 'да' + general_lang_name: 'Bulgarian (БългарÑки)' + general_csv_separator: ',' + general_csv_decimal_separator: '.' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '1' + + notice_account_updated: Профилът е обновен уÑпешно. + notice_account_invalid_creditentials: Ðевалиден потребител или парола. + notice_account_password_updated: Паролата е уÑпешно променена. + notice_account_wrong_password: Грешна парола + notice_account_register_done: Профилът е Ñъздаден уÑпешно. + notice_account_unknown_email: Ðепознат e-mail. + notice_can_t_change_password: Този профил е Ñ Ð²ÑŠÐ½ÑˆÐµÐ½ метод за оторизациÑ. Ðевъзможна ÑмÑна на паролата. + notice_account_lost_email_sent: Изпратен ви е e-mail Ñ Ð¸Ð½Ñтрукции за избор на нова парола. + notice_account_activated: Профилът ви е активиран. Вече може да влезете в ÑиÑтемата. + notice_successful_create: УÑпешно Ñъздаване. + notice_successful_update: УÑпешно обновÑване. + notice_successful_delete: УÑпешно изтриване. + notice_successful_connection: УÑпешно Ñвързване. + notice_file_not_found: ÐеÑъщеÑтвуваща или премеÑтена Ñтраница. + notice_locking_conflict: Друг потребител Ð¿Ñ€Ð¾Ð¼ÐµÐ½Ñ Ñ‚ÐµÐ·Ð¸ данни в момента. + notice_not_authorized: ÐÑмате право на доÑтъп до тази Ñтраница. + notice_not_authorized_archived_project: Проектът, който Ñе опитвате да видите е архивиран. Ðко ÑмÑтате, че това не е правилно, обърнете Ñе към админиÑтратора за разархивиране. + notice_email_sent: "Изпратен e-mail на %{value}" + notice_email_error: "Грешка при изпращане на e-mail (%{value})" + notice_feeds_access_key_reseted: Ð’Ð°ÑˆÐ¸Ñ ÐºÐ»ÑŽÑ‡ за RSS доÑтъп беше променен. + notice_api_access_key_reseted: ВашиÑÑ‚ API ключ за доÑтъп беше изчиÑтен. + notice_failed_to_save_issues: "ÐеуÑпешен Ð·Ð°Ð¿Ð¸Ñ Ð½Ð° %{count} задачи от %{total} избрани: %{ids}." + notice_failed_to_save_time_entries: "ÐевъзможноÑÑ‚ за Ð·Ð°Ð¿Ð¸Ñ Ð½Ð° %{count} запиÑа за използвано време от %{total} избрани: %{ids}." + notice_failed_to_save_members: "ÐевъзможноÑÑ‚ за Ð·Ð°Ð¿Ð¸Ñ Ð½Ð° член(ове): %{errors}." + notice_no_issue_selected: "ÐÑма избрани задачи." + notice_account_pending: "Профилът Ви е Ñъздаден и очаква одобрение от админиÑтратор." + notice_default_data_loaded: Примерната Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ðµ заредена уÑпешно. + notice_unable_delete_version: ÐевъзможноÑÑ‚ за изтриване на верÑÐ¸Ñ + notice_unable_delete_time_entry: ÐевъзможноÑÑ‚ за изтриване на Ð·Ð°Ð¿Ð¸Ñ Ð·Ð° използвано време. + notice_issue_done_ratios_updated: Обновен процент на завършените задачи. + notice_gantt_chart_truncated: МрежовиÑÑ‚ график е Ñъкратен, понеже броÑÑ‚ на обектите, които могат да бъдат показани е твърде голÑм (%{max}) + notice_issue_successful_create: Задача %{id} е Ñъздадена. + notice_issue_update_conflict: Задачата е била променена от друг потребител, докато вие Ñте Ñ Ñ€ÐµÐ´Ð°ÐºÑ‚Ð¸Ñ€Ð°Ð»Ð¸. + notice_account_deleted: ВашиÑÑ‚ профил беше премахнат без възможноÑÑ‚ за възÑтановÑване. + notice_user_successful_create: Потребител %{id} е Ñъздаден. + + error_can_t_load_default_data: "Грешка при зареждане на примерната информациÑ: %{value}" + error_scm_not_found: ÐеÑъщеÑтвуващ обект в хранилището. + error_scm_command_failed: "Грешка при опит за ÐºÐ¾Ð¼ÑƒÐ½Ð¸ÐºÐ°Ñ†Ð¸Ñ Ñ Ñ…Ñ€Ð°Ð½Ð¸Ð»Ð¸Ñ‰Ðµ: %{value}" + error_scm_annotate: "Обектът не ÑъщеÑтвува или не може да бъде анотиран." + error_scm_annotate_big_text_file: "Файлът не може да бъде анотиран, понеже Ð½Ð°Ð´Ñ…Ð²ÑŠÑ€Ð»Ñ Ð¼Ð°ÐºÑÐ¸Ð¼Ð°Ð»Ð½Ð¸Ñ Ñ€Ð°Ð·Ð¼ÐµÑ€ за текÑтови файлове." + error_issue_not_found_in_project: 'Задачата не е намерена или не принадлежи на този проект' + error_no_tracker_in_project: ÐÑма аÑоциирани тракери Ñ Ñ‚Ð¾Ð·Ð¸ проект. Проверете наÑтройките на проекта. + error_no_default_issue_status: ÐÑма уÑтановено подразбиращо Ñе ÑÑŠÑтоÑние за задачите. ÐœÐ¾Ð»Ñ Ð¿Ñ€Ð¾Ð²ÐµÑ€ÐµÑ‚Ðµ вашата ÐºÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ (Вижте "ÐдминиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ -> СъÑтоÑÐ½Ð¸Ñ Ð½Ð° задачи"). + error_can_not_delete_custom_field: ÐевъзможноÑÑ‚ за изтриване на потребителÑко поле + error_can_not_delete_tracker: Този тракер Ñъдържа задачи и не може да бъде изтрит. + error_can_not_remove_role: Тази Ñ€Ð¾Ð»Ñ Ñе използва и не може да бъде изтрита. + error_can_not_reopen_issue_on_closed_version: Задача, аÑоциирана ÑÑŠÑ Ð·Ð°Ñ‚Ð²Ð¾Ñ€ÐµÐ½Ð° верÑÐ¸Ñ Ð½Ðµ може да бъде отворена отново + error_can_not_archive_project: Този проект не може да бъде архивиран + error_issue_done_ratios_not_updated: Процентът на завършените задачи не е обновен. + error_workflow_copy_source: ÐœÐ¾Ð»Ñ Ð¸Ð·Ð±ÐµÑ€ÐµÑ‚Ðµ source тракер или Ñ€Ð¾Ð»Ñ + error_workflow_copy_target: ÐœÐ¾Ð»Ñ Ð¸Ð·Ð±ÐµÑ€ÐµÑ‚Ðµ тракер(и) и Ñ€Ð¾Ð»Ñ (роли). + error_unable_delete_issue_status: ÐевъзможноÑÑ‚ за изтриване на ÑÑŠÑтоÑние на задача + error_unable_to_connect: ÐевъзможноÑÑ‚ за Ñвързване Ñ (%{value}) + error_attachment_too_big: Този файл не може да бъде качен, понеже Ð½Ð°Ð´Ñ…Ð²ÑŠÑ€Ð»Ñ Ð¼Ð°ÐºÑималната възможна големина (%{max_size}) + error_session_expired: Вашата ÑеÑÐ¸Ñ Ðµ изтекла. ÐœÐ¾Ð»Ñ Ð²Ð»ÐµÐ·ÐµÑ‚Ðµ в Redmine отново. + warning_attachments_not_saved: "%{count} файла не бÑха запиÑани." + + mail_subject_lost_password: "Вашата парола (%{value})" + mail_body_lost_password: 'За да Ñмените паролата Ñи, използвайте ÑÐ»ÐµÐ´Ð½Ð¸Ñ Ð»Ð¸Ð½Ðº:' + mail_subject_register: "ÐÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ Ð½Ð° профил (%{value})" + mail_body_register: 'За да активирате профила Ñи използвайте ÑÐ»ÐµÐ´Ð½Ð¸Ñ Ð»Ð¸Ð½Ðº:' + mail_body_account_information_external: "Можете да използвате Ð²Ð°ÑˆÐ¸Ñ %{value} профил за вход." + mail_body_account_information: ИнформациÑта за профила ви + mail_subject_account_activation_request: "ЗаÑвка за активиране на профил в %{value}" + mail_body_account_activation_request: "Има новорегиÑтриран потребител (%{value}), очакващ вашето одобрение:" + mail_subject_reminder: "%{count} задачи Ñ ÐºÑ€Ð°ÐµÐ½ Ñрок Ñ Ñледващите %{days} дни" + mail_body_reminder: "%{count} задачи, назначени на Ð²Ð°Ñ Ñа Ñ ÐºÑ€Ð°ÐµÐ½ Ñрок в Ñледващите %{days} дни:" + mail_subject_wiki_content_added: "Wiki Ñтраницата '%{id}' беше добавена" + mail_body_wiki_content_added: Wiki Ñтраницата '%{id}' беше добавена от %{author}. + mail_subject_wiki_content_updated: "Wiki Ñтраницата '%{id}' беше обновена" + mail_body_wiki_content_updated: Wiki Ñтраницата '%{id}' беше обновена от %{author}. + + gui_validation_error: 1 грешка + gui_validation_error_plural: "%{count} грешки" + + field_name: Име + field_description: ОпиÑание + field_summary: ÐÐ½Ð¾Ñ‚Ð°Ñ†Ð¸Ñ + field_is_required: Задължително + field_firstname: Име + field_lastname: Ð¤Ð°Ð¼Ð¸Ð»Ð¸Ñ + field_mail: Email + field_filename: Файл + field_filesize: Големина + field_downloads: Изтеглени файлове + field_author: Ðвтор + field_created_on: От дата + field_updated_on: Обновена + field_field_format: Тип + field_is_for_all: За вÑички проекти + field_possible_values: Възможни ÑтойноÑти + field_regexp: РегулÑрен израз + field_min_length: Мин. дължина + field_max_length: МакÑ. дължина + field_value: СтойноÑÑ‚ + field_category: ÐšÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ + field_title: Заглавие + field_project: Проект + field_issue: Задача + field_status: СъÑтоÑние + field_notes: Бележка + field_is_closed: Затворена задача + field_is_default: СъÑтоÑние по подразбиране + field_tracker: Тракер + field_subject: ОтноÑно + field_due_date: Крайна дата + field_assigned_to: Възложена на + field_priority: Приоритет + field_fixed_version: Планувана верÑÐ¸Ñ + field_user: Потребител + field_principal: Principal + field_role: Ð Ð¾Ð»Ñ + field_homepage: Ðачална Ñтраница + field_is_public: Публичен + field_parent: Подпроект на + field_is_in_roadmap: Да Ñе вижда ли в Пътна карта + field_login: Потребител + field_mail_notification: ИзвеÑÑ‚Ð¸Ñ Ð¿Ð¾ пощата + field_admin: ÐдминиÑтратор + field_last_login_on: ПоÑледно Ñвързване + field_language: Език + field_effective_date: Дата + field_password: Парола + field_new_password: Ðова парола + field_password_confirmation: Потвърждение + field_version: ВерÑÐ¸Ñ + field_type: Тип + field_host: ХоÑÑ‚ + field_port: Порт + field_account: Профил + field_base_dn: Base DN + field_attr_login: Ðтрибут Login + field_attr_firstname: Ðтрибут Първо име (Firstname) + field_attr_lastname: Ðтрибут Ð¤Ð°Ð¼Ð¸Ð»Ð¸Ñ (Lastname) + field_attr_mail: Ðтрибут Email + field_onthefly: Динамично Ñъздаване на потребител + field_start_date: Ðачална дата + field_done_ratio: "% ПрогреÑ" + field_auth_source: Ðачин на Ð¾Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ð¸Ñ + field_hide_mail: Скрий e-mail адреÑа ми + field_comments: Коментар + field_url: ÐÐ´Ñ€ÐµÑ + field_start_page: Ðачална Ñтраница + field_subproject: Подпроект + field_hours: ЧаÑове + field_activity: ДейноÑÑ‚ + field_spent_on: Дата + field_identifier: Идентификатор + field_is_filter: Използва Ñе за филтър + field_issue_to: Свързана задача + field_delay: ОтмеÑтване + field_assignable: Възможно е възлагане на задачи за тази Ñ€Ð¾Ð»Ñ + field_redirect_existing_links: ПренаÑочване на ÑъщеÑтвуващи линкове + field_estimated_hours: ИзчиÑлено време + field_column_names: Колони + field_time_entries: Log time + field_time_zone: ЧаÑова зона + field_searchable: С възможноÑÑ‚ за търÑене + field_default_value: СтойноÑÑ‚ по подразбиране + field_comments_sorting: Сортиране на коментарите + field_parent_title: РодителÑка Ñтраница + field_editable: Editable + field_watcher: Ðаблюдател + field_identity_url: OpenID URL + field_content: Съдържание + field_group_by: Групиране на резултатите по + field_sharing: Sharing + field_parent_issue: РодителÑка задача + field_member_of_group: Член на група + field_assigned_to_role: Assignee's role + field_text: ТекÑтово поле + field_visible: Видим + field_warn_on_leaving_unsaved: Предупреди ме, когато напуÑкам Ñтраница Ñ Ð½ÐµÐ·Ð°Ð¿Ð¸Ñано Ñъдържание + field_issues_visibility: ВидимоÑÑ‚ на задачите + field_is_private: Лична + field_commit_logs_encoding: Кодова таблица на ÑъобщениÑта при поверÑване + field_scm_path_encoding: Кодова таблица на пътищата (path) + field_path_to_repository: Път до хранилището + field_root_directory: Коренна Ð´Ð¸Ñ€ÐµÐºÑ‚Ð¾Ñ€Ð¸Ñ (папка) + field_cvsroot: CVSROOT + field_cvs_module: Модул + field_repository_is_default: Главно хранилище + field_multiple: Избор на повече от една ÑтойноÑÑ‚ + field_auth_source_ldap_filter: LDAP филтър + field_core_fields: Стандартни полета + field_timeout: Таймаут (в Ñекунди) + field_board_parent: РодителÑки форум + field_private_notes: Лични бележки + + setting_app_title: Заглавие + setting_app_subtitle: ОпиÑание + setting_welcome_text: Допълнителен текÑÑ‚ + setting_default_language: Език по подразбиране + setting_login_required: ИзиÑкване за вход в ÑиÑтемата + setting_self_registration: РегиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ Ð¾Ñ‚ потребители + setting_attachment_max_size: МакÑимална големина на прикачен файл + setting_issues_export_limit: МакÑимален брой задачи за екÑпорт + setting_mail_from: E-mail Ð°Ð´Ñ€ÐµÑ Ð·Ð° емиÑии + setting_bcc_recipients: Получатели на Ñкрито копие (bcc) + setting_plain_text_mail: Ñамо чиÑÑ‚ текÑÑ‚ (без HTML) + setting_host_name: ХоÑÑ‚ + setting_text_formatting: Форматиране на текÑта + setting_wiki_compression: КомпреÑиране на Wiki иÑториÑта + setting_feeds_limit: МакÑимален брой запиÑи в ATOM емиÑии + setting_default_projects_public: Ðовите проекти Ñа публични по подразбиране + setting_autofetch_changesets: Ðвтоматично извличане на ревизиите + setting_sys_api_enabled: Разрешаване на WS за управление + setting_commit_ref_keywords: ОтбелÑзващи ключови думи + setting_commit_fix_keywords: Приключващи ключови думи + setting_autologin: Ðвтоматичен вход + setting_date_format: Формат на датата + setting_time_format: Формат на чаÑа + setting_cross_project_issue_relations: Релации на задачи между проекти + setting_cross_project_subtasks: Подзадачи от други проекти + setting_issue_list_default_columns: Показвани колони по подразбиране + setting_repositories_encodings: Кодова таблица на прикачените файлове и хранилищата + setting_emails_header: Emails header + setting_emails_footer: ПодтекÑÑ‚ за e-mail + setting_protocol: Протокол + setting_per_page_options: Опции за Ñтраниране + setting_user_format: ПотребителÑки формат + setting_activity_days_default: Брой дни показвани на таб ДейноÑÑ‚ + setting_display_subprojects_issues: Задачите от подпроектите по подразбиране Ñе показват в главните проекти + setting_enabled_scm: Разрешена SCM + setting_mail_handler_body_delimiters: ОтрÑзване на e-mail-ите Ñлед един от тези редове + setting_mail_handler_api_enabled: Разрешаване на WS за входÑщи e-mail-и + setting_mail_handler_api_key: API ключ + setting_sequential_project_identifiers: Генериране на поÑледователни проектни идентификатори + setting_gravatar_enabled: Използване на портребителÑки икони от Gravatar + setting_gravatar_default: Подразбиращо Ñе изображение от Gravatar + setting_diff_max_lines_displayed: МакÑимален брой показвани diff редове + setting_file_max_size_displayed: МакÑимален размер на текÑтовите файлове, показвани inline + setting_repository_log_display_limit: МакÑимален брой на показванете ревизии в лог файла + setting_openid: Рарешаване на OpenID вход и региÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ + setting_password_min_length: Минимална дължина на парола + setting_new_project_user_role_id: РолÑ, давана на потребител, Ñъздаващ проекти, който не е админиÑтратор + setting_default_projects_modules: Ðктивирани модули по подразбиране за нов проект + setting_issue_done_ratio: ИзчиÑление на процента на готови задачи Ñ + setting_issue_done_ratio_issue_field: Използване на поле '% ПрогреÑ' + setting_issue_done_ratio_issue_status: Използване на ÑÑŠÑтоÑнието на задачите + setting_start_of_week: Първи ден на Ñедмицата + setting_rest_api_enabled: Разрешаване на REST web ÑÑŠÑ€Ð²Ð¸Ñ + setting_cache_formatted_text: Кеширане на форматираните текÑтове + setting_default_notification_option: Подразбиращ Ñе начин за извеÑÑ‚Ñване + setting_commit_logtime_enabled: Разрешаване на отчитането на работното време + setting_commit_logtime_activity_id: ДейноÑÑ‚ при отчитане на работното време + setting_gantt_items_limit: МакÑимален брой обекти, които да Ñе показват в мрежов график + setting_issue_group_assignment: Разрешено назначаването на задачи на групи + setting_default_issue_start_date_to_creation_date: Ðачална дата на новите задачи по подразбиране да бъде днешната дата + setting_commit_cross_project_ref: ОтбелÑзване и приключване на задачи от други проекти, неÑвързани Ñ ÐºÐ¾Ð½ÐºÑ€ÐµÑ‚Ð½Ð¾Ñ‚Ð¾ хранилище + setting_unsubscribe: Потребителите могат да премахват профилите Ñи + setting_session_lifetime: МакÑимален живот на ÑеÑиите + setting_session_timeout: Таймаут за неактивноÑÑ‚ преди прекратÑване на ÑеÑиите + setting_thumbnails_enabled: Показване на миниатюри на прикачените Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ + setting_thumbnails_size: Размер на миниатюрите (в пикÑели) + setting_non_working_week_days: Ðе работни дни + + permission_add_project: Създаване на проект + permission_add_subprojects: Създаване на подпроекти + permission_edit_project: Редактиране на проект + permission_close_project: ЗатварÑне / отварÑне на проект + permission_select_project_modules: Избор на проектни модули + permission_manage_members: Управление на членовете (на екип) + permission_manage_project_activities: Управление на дейноÑтите на проекта + permission_manage_versions: Управление на верÑиите + permission_manage_categories: Управление на категориите + permission_view_issues: Разглеждане на задачите + permission_add_issues: ДобавÑне на задачи + permission_edit_issues: Редактиране на задачи + permission_manage_issue_relations: Управление на връзките между задачите + permission_set_own_issues_private: УÑтановÑване на ÑобÑтвените задачи публични или лични + permission_set_issues_private: УÑтановÑване на задачите публични или лични + permission_add_issue_notes: ДобавÑне на бележки + permission_edit_issue_notes: Редактиране на бележки + permission_edit_own_issue_notes: Редактиране на ÑобÑтвени бележки + permission_view_private_notes: Разглеждане на лични бележки + permission_set_notes_private: УÑтановÑване на бележките лични + permission_move_issues: ПремеÑтване на задачи + permission_delete_issues: Изтриване на задачи + permission_manage_public_queries: Управление на публичните заÑвки + permission_save_queries: Ð—Ð°Ð¿Ð¸Ñ Ð½Ð° Ð·Ð°Ð¿Ð¸Ñ‚Ð²Ð°Ð½Ð¸Ñ (queries) + permission_view_gantt: Разглеждане на мрежов график + permission_view_calendar: Разглеждане на календари + permission_view_issue_watchers: Разглеждане на ÑпиÑък Ñ Ð½Ð°Ð±Ð»ÑŽÐ´Ð°Ñ‚ÐµÐ»Ð¸ + permission_add_issue_watchers: ДобавÑне на наблюдатели + permission_delete_issue_watchers: Изтриване на наблюдатели + permission_log_time: Log spent time + permission_view_time_entries: Разглеждане на изразходваното време + permission_edit_time_entries: Редактиране на time logs + permission_edit_own_time_entries: Редактиране на ÑобÑтвените time logs + permission_manage_news: Управление на новини + permission_comment_news: Коментиране на новини + permission_manage_documents: Управление на документи + permission_view_documents: Разглеждане на документи + permission_manage_files: Управление на файлове + permission_view_files: Разглеждане на файлове + permission_manage_wiki: Управление на wiki + permission_rename_wiki_pages: Преименуване на wiki Ñтраници + permission_delete_wiki_pages: Изтриване на wiki Ñтраници + permission_view_wiki_pages: Разглеждане на wiki + permission_view_wiki_edits: Разглеждане на wiki иÑÑ‚Ð¾Ñ€Ð¸Ñ + permission_edit_wiki_pages: Редактиране на wiki Ñтраници + permission_delete_wiki_pages_attachments: Изтриване на прикачени файлове към wiki Ñтраници + permission_protect_wiki_pages: Заключване на wiki Ñтраници + permission_manage_repository: Управление на хранилища + permission_browse_repository: Разглеждане на хранилища + permission_view_changesets: Разглеждане на changesets + permission_commit_access: ПоверÑване + permission_manage_boards: Управление на boards + permission_view_messages: Разглеждане на ÑÑŠÐ¾Ð±Ñ‰ÐµÐ½Ð¸Ñ + permission_add_messages: Публикуване на ÑÑŠÐ¾Ð±Ñ‰ÐµÐ½Ð¸Ñ + permission_edit_messages: Редактиране на ÑÑŠÐ¾Ð±Ñ‰ÐµÐ½Ð¸Ñ + permission_edit_own_messages: Редактиране на ÑобÑтвени ÑÑŠÐ¾Ð±Ñ‰ÐµÐ½Ð¸Ñ + permission_delete_messages: Изтриване на ÑÑŠÐ¾Ð±Ñ‰ÐµÐ½Ð¸Ñ + permission_delete_own_messages: Изтриване на ÑобÑтвени ÑÑŠÐ¾Ð±Ñ‰ÐµÐ½Ð¸Ñ + permission_export_wiki_pages: ЕкÑпорт на wiki Ñтраници + permission_manage_subtasks: Управление на подзадачите + permission_manage_related_issues: Управление на връзките между задачи и ревизии + + project_module_issue_tracking: Тракинг + project_module_time_tracking: ОтделÑне на време + project_module_news: Ðовини + project_module_documents: Документи + project_module_files: Файлове + project_module_wiki: Wiki + project_module_repository: Хранилище + project_module_boards: Форуми + project_module_calendar: Календар + project_module_gantt: Мрежов график + + label_user: Потребител + label_user_plural: Потребители + label_user_new: Ðов потребител + label_user_anonymous: Ðнонимен + label_project: Проект + label_project_new: Ðов проект + label_project_plural: Проекти + label_x_projects: + zero: 0 проекта + one: 1 проект + other: "%{count} проекта" + label_project_all: Ð’Ñички проекти + label_project_latest: ПоÑледни проекти + label_issue: Задача + label_issue_new: Ðова задача + label_issue_plural: Задачи + label_issue_view_all: Ð’Ñички задачи + label_issues_by: "Задачи по %{value}" + label_issue_added: Добавена задача + label_issue_updated: Обновена задача + label_issue_note_added: Добавена бележка + label_issue_status_updated: Обновено ÑÑŠÑтоÑние + label_issue_priority_updated: Обновен приоритет + label_document: Документ + label_document_new: Ðов документ + label_document_plural: Документи + label_document_added: Добавен документ + label_role: Ð Ð¾Ð»Ñ + label_role_plural: Роли + label_role_new: Ðова Ñ€Ð¾Ð»Ñ + label_role_and_permissions: Роли и права + label_role_anonymous: Ðнонимен + label_role_non_member: Ðе член + label_member: Член + label_member_new: Ðов член + label_member_plural: Членове + label_tracker: Тракер + label_tracker_plural: Тракери + label_tracker_new: Ðов тракер + label_workflow: Работен Ð¿Ñ€Ð¾Ñ†ÐµÑ + label_issue_status: СъÑтоÑние на задача + label_issue_status_plural: СъÑтоÑÐ½Ð¸Ñ Ð½Ð° задачи + label_issue_status_new: Ðово ÑÑŠÑтоÑние + label_issue_category: ÐšÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ Ð·Ð°Ð´Ð°Ñ‡Ð° + label_issue_category_plural: Категории задачи + label_issue_category_new: Ðова ÐºÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ + label_custom_field: ПотребителÑко поле + label_custom_field_plural: ПотребителÑки полета + label_custom_field_new: Ðово потребителÑко поле + label_enumerations: СпиÑъци + label_enumeration_new: Ðова ÑтойноÑÑ‚ + label_information: Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ + label_information_plural: Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ + label_please_login: Вход + label_register: РегиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ + label_login_with_open_id_option: или вход чрез OpenID + label_password_lost: Забравена парола + label_home: Ðачало + label_my_page: Лична Ñтраница + label_my_account: Профил + label_my_projects: Проекти, в които учаÑтвам + label_my_page_block: Блокове в личната Ñтраница + label_administration: ÐдминиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ + label_login: Вход + label_logout: Изход + label_help: Помощ + label_reported_issues: Публикувани задачи + label_assigned_to_me_issues: Възложени на мен + label_last_login: ПоÑледно Ñвързване + label_registered_on: РегиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ + label_activity: ДейноÑÑ‚ + label_overall_activity: ЦÑлоÑтна дейноÑÑ‚ + label_user_activity: "ÐктивноÑÑ‚ на %{value}" + label_new: Ðов + label_logged_as: Здравейте, + label_environment: Среда + label_authentication: ÐžÑ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ð¸Ñ + label_auth_source: Ðачин на Ð¾Ñ‚Ð¾Ñ€Ð¾Ð·Ð°Ñ†Ð¸Ñ + label_auth_source_new: Ðов начин на Ð¾Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ð¸Ñ + label_auth_source_plural: Ðачини на Ð¾Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ð¸Ñ + label_subproject_plural: Подпроекти + label_subproject_new: Ðов подпроект + label_and_its_subprojects: "%{value} и неговите подпроекти" + label_min_max_length: Минимална - макÑимална дължина + label_list: СпиÑък + label_date: Дата + label_integer: ЦелочиÑлен + label_float: Дробно + label_boolean: Ð§ÐµÐºÐ±Ð¾ÐºÑ + label_string: ТекÑÑ‚ + label_text: Дълъг текÑÑ‚ + label_attribute: Ðтрибут + label_attribute_plural: Ðтрибути + label_download: "%{count} изтеглÑне" + label_download_plural: "%{count} изтеглÑниÑ" + label_no_data: ÐÑма изходни данни + label_change_status: ПромÑна на ÑÑŠÑтоÑнието + label_history: ИÑÑ‚Ð¾Ñ€Ð¸Ñ + label_attachment: Файл + label_attachment_new: Ðов файл + label_attachment_delete: Изтриване + label_attachment_plural: Файлове + label_file_added: Добавен файл + label_report: Справка + label_report_plural: Справки + label_news: Ðовини + label_news_new: Добави + label_news_plural: Ðовини + label_news_latest: ПоÑледни новини + label_news_view_all: Виж вÑички + label_news_added: Добавена новина + label_news_comment_added: Добавен коментар към новина + label_settings: ÐаÑтройки + label_overview: Общ изглед + label_version: ВерÑÐ¸Ñ + label_version_new: Ðова верÑÐ¸Ñ + label_version_plural: ВерÑии + label_close_versions: ЗатварÑне на завършените верÑии + label_confirmation: Одобрение + label_export_to: ЕкÑпорт към + label_read: Read... + label_public_projects: Публични проекти + label_open_issues: отворена + label_open_issues_plural: отворени + label_closed_issues: затворена + label_closed_issues_plural: затворени + label_x_open_issues_abbr_on_total: + zero: 0 отворени / %{total} + one: 1 отворена / %{total} + other: "%{count} отворени / %{total}" + label_x_open_issues_abbr: + zero: 0 отворени + one: 1 отворена + other: "%{count} отворени" + label_x_closed_issues_abbr: + zero: 0 затворени + one: 1 затворена + other: "%{count} затворени" + label_x_issues: + zero: 0 задачи + one: 1 задача + other: "%{count} задачи" + label_total: Общо + label_permissions: Права + label_current_status: Текущо ÑÑŠÑтоÑние + label_new_statuses_allowed: Позволени ÑÑŠÑтоÑÐ½Ð¸Ñ + label_all: вÑички + label_any: коÑто и да е + label_none: никакви + label_nobody: никой + label_next: Следващ + label_previous: Предишен + label_used_by: Използва Ñе от + label_details: Детайли + label_add_note: ДобавÑне на бележка + label_per_page: Ðа Ñтраница + label_calendar: Календар + label_months_from: меÑеца от + label_gantt: Мрежов график + label_internal: Вътрешен + label_last_changes: "поÑледни %{count} промени" + label_change_view_all: Виж вÑички промени + label_personalize_page: ПерÑонализиране + label_comment: Коментар + label_comment_plural: Коментари + label_x_comments: + zero: 0 коментара + one: 1 коментар + other: "%{count} коментара" + label_comment_add: ДобавÑне на коментар + label_comment_added: Добавен коментар + label_comment_delete: Изтриване на коментари + label_query: ПотребителÑка Ñправка + label_query_plural: ПотребителÑки Ñправки + label_query_new: Ðова заÑвка + label_my_queries: Моите заÑвки + label_filter_add: Добави филтър + label_filter_plural: Филтри + label_equals: е + label_not_equals: не е + label_in_less_than: Ñлед по-малко от + label_in_more_than: Ñлед повече от + label_in_the_next_days: в Ñледващите + label_in_the_past_days: в предишните + label_greater_or_equal: ">=" + label_less_or_equal: <= + label_between: между + label_in: в Ñледващите + label_today: Ð´Ð½ÐµÑ + label_all_time: вÑички + label_yesterday: вчера + label_this_week: тази Ñедмица + label_last_week: поÑледната Ñедмица + label_last_n_weeks: поÑледните %{count} Ñедмици + label_last_n_days: "поÑледните %{count} дни" + label_this_month: Ñ‚ÐµÐºÑƒÑ‰Ð¸Ñ Ð¼ÐµÑец + label_last_month: поÑÐ»ÐµÐ´Ð½Ð¸Ñ Ð¼ÐµÑец + label_this_year: текущата година + label_date_range: Период + label_less_than_ago: преди по-малко от + label_more_than_ago: преди повече от + label_ago: преди + label_contains: Ñъдържа + label_not_contains: не Ñъдържа + label_any_issues_in_project: задачи от проект + label_any_issues_not_in_project: задачи, които не Ñа в проект + label_no_issues_in_project: никакви задачи в проект + label_day_plural: дни + label_repository: Хранилище + label_repository_new: Ðово хранилище + label_repository_plural: Хранилища + label_browse: Разглеждане + label_modification: "%{count} промÑна" + label_modification_plural: "%{count} промени" + label_branch: работен вариант + label_tag: ВерÑÐ¸Ñ + label_revision: Ð ÐµÐ²Ð¸Ð·Ð¸Ñ + label_revision_plural: Ревизии + label_revision_id: Ð ÐµÐ²Ð¸Ð·Ð¸Ñ %{value} + label_associated_revisions: ÐÑоциирани ревизии + label_added: добавено + label_modified: променено + label_copied: копирано + label_renamed: преименувано + label_deleted: изтрито + label_latest_revision: ПоÑледна Ñ€ÐµÐ²Ð¸Ð·Ð¸Ñ + label_latest_revision_plural: ПоÑледни ревизии + label_view_revisions: Виж ревизиите + label_view_all_revisions: Разглеждане на вÑички ревизии + label_max_size: МакÑимална големина + label_sort_highest: ПремеÑти най-горе + label_sort_higher: ПремеÑти по-горе + label_sort_lower: ПремеÑти по-долу + label_sort_lowest: ПремеÑти най-долу + label_roadmap: Пътна карта + label_roadmap_due_in: "Излиза Ñлед %{value}" + label_roadmap_overdue: "%{value} закъÑнение" + label_roadmap_no_issues: ÐÑма задачи за тази верÑÐ¸Ñ + label_search: ТърÑене + label_result_plural: Pезултати + label_all_words: Ð’Ñички думи + label_wiki: Wiki + label_wiki_edit: Wiki Ñ€ÐµÐ´Ð°ÐºÑ†Ð¸Ñ + label_wiki_edit_plural: Wiki редакции + label_wiki_page: Wiki Ñтраница + label_wiki_page_plural: Wiki Ñтраници + label_index_by_title: Ð˜Ð½Ð´ÐµÐºÑ + label_index_by_date: Ð˜Ð½Ð´ÐµÐºÑ Ð¿Ð¾ дата + label_current_version: Текуща верÑÐ¸Ñ + label_preview: Преглед + label_feed_plural: ЕмиÑии + label_changes_details: Подробни промени + label_issue_tracking: Тракинг + label_spent_time: Отделено време + label_overall_spent_time: Общо употребено време + label_f_hour: "%{value} чаÑ" + label_f_hour_plural: "%{value} чаÑа" + label_time_tracking: ОтделÑне на време + label_change_plural: Промени + label_statistics: СтатиÑтики + label_commits_per_month: Ревизии по меÑеци + label_commits_per_author: Ревизии по автор + label_diff: diff + label_view_diff: Виж разликите + label_diff_inline: хоризонтално + label_diff_side_by_side: вертикално + label_options: Опции + label_copy_workflow_from: Копирай Ñ€Ð°Ð±Ð¾Ñ‚Ð½Ð¸Ñ Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð¾Ñ‚ + label_permissions_report: Справка за права + label_watched_issues: Ðаблюдавани задачи + label_related_issues: Свързани задачи + label_applied_status: УÑтановено ÑÑŠÑтоÑние + label_loading: Зареждане... + label_relation_new: Ðова Ñ€ÐµÐ»Ð°Ñ†Ð¸Ñ + label_relation_delete: Изтриване на Ñ€ÐµÐ»Ð°Ñ†Ð¸Ñ + label_relates_to: Ñвързана ÑÑŠÑ + label_duplicates: дублира + label_duplicated_by: дублирана от + label_blocks: блокира + label_blocked_by: блокирана от + label_precedes: предшеÑтва + label_follows: изпълнÑва Ñе Ñлед + label_copied_to: копирана в + label_copied_from: копирана от + label_end_to_start: край към начало + label_end_to_end: край към край + label_start_to_start: начало към начало + label_start_to_end: начало към край + label_stay_logged_in: Запомни ме + label_disabled: забранено + label_show_completed_versions: Показване на реализирани верÑии + label_me: аз + label_board: Форум + label_board_new: Ðов форум + label_board_plural: Форуми + label_board_locked: Заключена + label_board_sticky: Sticky + label_topic_plural: Теми + label_message_plural: Ð¡ÑŠÐ¾Ð±Ñ‰ÐµÐ½Ð¸Ñ + label_message_last: ПоÑледно Ñъобщение + label_message_new: Ðова тема + label_message_posted: Добавено Ñъобщение + label_reply_plural: Отговори + label_send_information: Изпращане на информациÑта до Ð¿Ð¾Ñ‚Ñ€ÐµÐ±Ð¸Ñ‚ÐµÐ»Ñ + label_year: Година + label_month: МеÑец + label_week: Седмица + label_date_from: От + label_date_to: До + label_language_based: Ð’ завиÑимоÑÑ‚ от езика + label_sort_by: "Сортиране по %{value}" + label_send_test_email: Изпращане на теÑтов e-mail + label_feeds_access_key: RSS access ключ + label_missing_feeds_access_key: ЛипÑващ RSS ключ за доÑтъп + label_feeds_access_key_created_on: "%{value} от Ñъздаването на RSS ключа" + label_module_plural: Модули + label_added_time_by: "Публикувана от %{author} преди %{age}" + label_updated_time_by: "Обновена от %{author} преди %{age}" + label_updated_time: "Обновена преди %{value}" + label_jump_to_a_project: Проект... + label_file_plural: Файлове + label_changeset_plural: Ревизии + label_default_columns: По подразбиране + label_no_change_option: (Без промÑна) + label_bulk_edit_selected_issues: Групово редактиране на задачи + label_bulk_edit_selected_time_entries: Групово редактиране на запиÑи за използвано време + label_theme: Тема + label_default: По подразбиране + label_search_titles_only: Само в заглавиÑта + label_user_mail_option_all: "За вÑÑко Ñъбитие в проектите, в които учаÑтвам" + label_user_mail_option_selected: "За вÑички ÑÑŠÐ±Ð¸Ñ‚Ð¸Ñ Ñамо в избраните проекти..." + label_user_mail_option_none: "Само за наблюдавани или в които учаÑтвам (автор или назначени на мен)" + label_user_mail_option_only_my_events: Само за неща, в които Ñъм включен/а + label_user_mail_option_only_assigned: Само за неща, назначени на мен + label_user_mail_option_only_owner: Само за неща, на които аз Ñъм ÑобÑтвеник + label_user_mail_no_self_notified: "Ðе иÑкам извеÑÑ‚Ð¸Ñ Ð·Ð° извършени от мен промени" + label_registration_activation_by_email: активиране на профила по email + label_registration_manual_activation: ръчно активиране + label_registration_automatic_activation: автоматично активиране + label_display_per_page: "Ðа Ñтраница по: %{value}" + label_age: ВъзраÑÑ‚ + label_change_properties: ПромÑна на наÑтройки + label_general: ОÑновни + label_more: Още + label_scm: SCM (СиÑтема за контрол на верÑиите) + label_plugins: Плъгини + label_ldap_authentication: LDAP Ð¾Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ð¸Ñ + label_downloads_abbr: D/L + label_optional_description: Ðезадължително опиÑание + label_add_another_file: ДобавÑне на друг файл + label_preferences: ÐŸÑ€ÐµÐ´Ð¿Ð¾Ñ‡Ð¸Ñ‚Ð°Ð½Ð¸Ñ + label_chronological_order: Хронологичен ред + label_reverse_chronological_order: Обратен хронологичен ред + label_planning: Планиране + label_incoming_emails: ВходÑщи e-mail-и + label_generate_key: Генериране на ключ + label_issue_watchers: Ðаблюдатели + label_example: Пример + label_display: Показване + label_sort: Сортиране + label_ascending: ÐараÑтващ + label_descending: ÐамалÑващ + label_date_from_to: От %{start} до %{end} + label_wiki_content_added: Wiki Ñтраница беше добавена + label_wiki_content_updated: Wiki Ñтраница беше обновена + label_group: Група + label_group_plural: Групи + label_group_new: Ðова група + label_time_entry_plural: Използвано време + label_version_sharing_none: Ðе Ñподелен + label_version_sharing_descendants: С подпроекти + label_version_sharing_hierarchy: С проектна Ð¹ÐµÑ€Ð°Ñ€Ñ…Ð¸Ñ + label_version_sharing_tree: С дърво на проектите + label_version_sharing_system: С вÑички проекти + label_update_issue_done_ratios: ОбновÑване на процента на завършените задачи + label_copy_source: Източник + label_copy_target: Цел + label_copy_same_as_target: Също като целта + label_display_used_statuses_only: Показване Ñамо на ÑÑŠÑтоÑниÑта, използвани от този тракер + label_api_access_key: API ключ за доÑтъп + label_missing_api_access_key: ЛипÑващ API ключ + label_api_access_key_created_on: API ключ за доÑтъп е Ñъздаден преди %{value} + label_profile: Профил + label_subtask_plural: Подзадачи + label_project_copy_notifications: Изпращане на Send e-mail извеÑÑ‚Ð¸Ñ Ð¿Ð¾ време на копирането на проекта + label_principal_search: "ТърÑене на потребител или група:" + label_user_search: "ТърÑене на потребител:" + label_additional_workflow_transitions_for_author: Позволени Ñа допълнителни преходи, когато потребителÑÑ‚ е авторът + label_additional_workflow_transitions_for_assignee: Позволени Ñа допълнителни преходи, когато потребителÑÑ‚ е назначениÑÑ‚ към задачата + label_issues_visibility_all: Ð’Ñички задачи + label_issues_visibility_public: Ð’Ñички не-лични задачи + label_issues_visibility_own: Задачи, Ñъздадени от или назначени на Ð¿Ð¾Ñ‚Ñ€ÐµÐ±Ð¸Ñ‚ÐµÐ»Ñ + label_git_report_last_commit: Извеждане на поÑледното поверÑване за файлове и папки + label_parent_revision: Ð ÐµÐ²Ð¸Ð·Ð¸Ñ Ñ€Ð¾Ð´Ð¸Ñ‚ÐµÐ» + label_child_revision: Ð ÐµÐ²Ð¸Ð·Ð¸Ñ Ð½Ð°Ñледник + label_export_options: "%{export_format} опции за екÑпорт" + label_copy_attachments: Копиране на прикачените файлове + label_copy_subtasks: Копиране на подзадачите + label_item_position: "%{position}/%{count}" + label_completed_versions: Завършени верÑии + label_search_for_watchers: ТърÑене на потребители за наблюдатели + label_session_expiration: Изтичане на ÑеÑиите + label_show_closed_projects: Разглеждане на затворени проекти + label_status_transitions: Преходи между ÑÑŠÑтоÑниÑта + label_fields_permissions: ВидимоÑÑ‚ на полетата + label_readonly: Само за четене + label_required: Задължително + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_cross_project_descendants: С подпроекти + label_cross_project_tree: С дърво на проектите + label_cross_project_hierarchy: С проектна Ð¹ÐµÑ€Ð°Ñ€Ñ…Ð¸Ñ + label_cross_project_system: С вÑички проекти + + button_login: Вход + button_submit: Изпращане + button_save: Ð—Ð°Ð¿Ð¸Ñ + button_check_all: Избор на вÑички + button_uncheck_all: ИзчиÑтване на вÑички + button_collapse_all: Скриване вÑички + button_expand_all: Разгъване вÑички + button_delete: Изтриване + button_create: Създаване + button_create_and_continue: Създаване и продължаване + button_test: ТеÑÑ‚ + button_edit: Ð ÐµÐ´Ð°ÐºÑ†Ð¸Ñ + button_edit_associated_wikipage: "Редактиране на аÑоциираната Wiki Ñтраница: %{page_title}" + button_add: ДобавÑне + button_change: ПромÑна + button_apply: Приложи + button_clear: ИзчиÑти + button_lock: Заключване + button_unlock: Отключване + button_download: ИзтеглÑне + button_list: СпиÑък + button_view: Преглед + button_move: ПремеÑтване + button_move_and_follow: ПремеÑтване и продължаване + button_back: Ðазад + button_cancel: Отказ + button_activate: ÐÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ + button_sort: Сортиране + button_log_time: ОтделÑне на време + button_rollback: Върни Ñе към тази Ñ€ÐµÐ²Ð¸Ð·Ð¸Ñ + button_watch: Ðаблюдаване + button_unwatch: Край на наблюдението + button_reply: Отговор + button_archive: Ðрхивиране + button_unarchive: Разархивиране + button_reset: Генериране наново + button_rename: Преименуване + button_change_password: ПромÑна на парола + button_copy: Копиране + button_copy_and_follow: Копиране и продължаване + button_annotate: ÐÐ½Ð¾Ñ‚Ð°Ñ†Ð¸Ñ + button_update: ОбновÑване + button_configure: Конфигуриране + button_quote: Цитат + button_duplicate: Дублиране + button_show: Показване + button_hide: Скриване + button_edit_section: Редактиране на тази ÑÐµÐºÑ†Ð¸Ñ + button_export: ЕкÑпорт + button_delete_my_account: Премахване на Ð¼Ð¾Ñ Ð¿Ñ€Ð¾Ñ„Ð¸Ð» + button_close: ЗатварÑне + button_reopen: ОтварÑне + + status_active: активен + status_registered: региÑтриран + status_locked: заключен + + project_status_active: активен + project_status_closed: затворен + project_status_archived: архивиран + + version_status_open: отворена + version_status_locked: заключена + version_status_closed: затворена + + field_active: Ðктивен + + text_select_mail_notifications: Изберете ÑÑŠÐ±Ð¸Ñ‚Ð¸Ñ Ð·Ð° изпращане на e-mail. + text_regexp_info: пр. ^[A-Z0-9]+$ + text_min_max_length_info: 0 - без Ð¾Ð³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñ + text_project_destroy_confirmation: Сигурни ли Ñте, че иÑкате да изтриете проекта и данните в него? + text_subprojects_destroy_warning: "Ðеговите подпроекти: %{value} Ñъщо ще бъдат изтрити." + text_workflow_edit: Изберете Ñ€Ð¾Ð»Ñ Ð¸ тракер за да редактирате Ñ€Ð°Ð±Ð¾Ñ‚Ð½Ð¸Ñ Ð¿Ñ€Ð¾Ñ†ÐµÑ + text_are_you_sure: Сигурни ли Ñте? + text_journal_changed: "%{label} променен от %{old} на %{new}" + text_journal_changed_no_detail: "%{label} променен" + text_journal_set_to: "%{label} уÑтановен на %{value}" + text_journal_deleted: "%{label} изтрит (%{old})" + text_journal_added: "Добавено %{label} %{value}" + text_tip_issue_begin_day: задача, започваща този ден + text_tip_issue_end_day: задача, завършваща този ден + text_tip_issue_begin_end_day: задача, започваща и завършваща този ден + text_project_identifier_info: 'Позволени Ñа малки букви (a-z), цифри, тирета и _.
    ПромÑна Ñлед Ñъздаването му не е възможна.' + text_caracters_maximum: "До %{count} Ñимвола." + text_caracters_minimum: "Минимум %{count} Ñимвола." + text_length_between: "От %{min} до %{max} Ñимвола." + text_tracker_no_workflow: ÐÑма дефиниран работен Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð·Ð° този тракер + text_unallowed_characters: Ðепозволени Ñимволи + text_comma_separated: Позволено е изброÑване (Ñ Ñ€Ð°Ð·Ð´ÐµÐ»Ð¸Ñ‚ÐµÐ» запетаÑ). + text_line_separated: Позволени Ñа много ÑтойноÑти (по едно на ред). + text_issues_ref_in_commit_messages: ОтбелÑзване и приключване на задачи от ревизии + text_issue_added: "Публикувана е нова задача Ñ Ð½Ð¾Ð¼ÐµÑ€ %{id} (от %{author})." + text_issue_updated: "Задача %{id} е обновена (от %{author})." + text_wiki_destroy_confirmation: Сигурни ли Ñте, че иÑкате да изтриете това Wiki и цÑлото му Ñъдържание? + text_issue_category_destroy_question: "Има задачи (%{count}) обвързани Ñ Ñ‚Ð°Ð·Ð¸ категориÑ. Какво ще изберете?" + text_issue_category_destroy_assignments: Премахване на връзките Ñ ÐºÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñта + text_issue_category_reassign_to: Преобвързване Ñ ÐºÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ + text_user_mail_option: "За неизбраните проекти, ще получавате извеÑÑ‚Ð¸Ñ Ñамо за наблюдавани дейноÑти или в които учаÑтвате (Ñ‚.е. автор или назначени на мен)." + text_no_configuration_data: "Ð’Ñе още не Ñа конфигурирани Роли, тракери, ÑÑŠÑтоÑÐ½Ð¸Ñ Ð½Ð° задачи и работен процеÑ.\nСтрого Ñе препоръчва зареждането на примерната информациÑ. Веднъж заредена ще имате възможноÑÑ‚ да Ñ Ñ€ÐµÐ´Ð°ÐºÑ‚Ð¸Ñ€Ð°Ñ‚Ðµ." + text_load_default_configuration: Зареждане на примерна Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ + text_status_changed_by_changeset: "Приложено Ñ Ñ€ÐµÐ²Ð¸Ð·Ð¸Ñ %{value}." + text_time_logged_by_changeset: Приложено в Ñ€ÐµÐ²Ð¸Ð·Ð¸Ñ %{value}. + text_issues_destroy_confirmation: 'Сигурни ли Ñте, че иÑкате да изтриете избраните задачи?' + text_issues_destroy_descendants_confirmation: Тази Ð¾Ð¿ÐµÑ€Ð°Ñ†Ð¸Ñ Ñ‰Ðµ премахне и %{count} подзадача(и). + text_time_entries_destroy_confirmation: Сигурен ли Ñте, че изтриете избраните запиÑи за изразходвано време? + text_select_project_modules: 'Изберете активните модули за този проект:' + text_default_administrator_account_changed: Сменен Ñ„Ð°Ð±Ñ€Ð¸Ñ‡Ð½Ð¸Ñ Ð°Ð´Ð¼Ð¸Ð½Ð¸ÑтраторÑки профил + text_file_repository_writable: ВъзможноÑÑ‚ за пиÑане в хранилището Ñ Ñ„Ð°Ð¹Ð»Ð¾Ð²Ðµ + text_plugin_assets_writable: Папката на приÑтавките е разрешена за Ð·Ð°Ð¿Ð¸Ñ + text_rmagick_available: Ðаличен RMagick (по избор) + text_destroy_time_entries_question: "%{hours} чаÑа Ñа отделени на задачите, които иÑкате да изтриете. Какво избирате?" + text_destroy_time_entries: Изтриване на отделеното време + text_assign_time_entries_to_project: ПрехвърлÑне на отделеното време към проект + text_reassign_time_entries: 'ПрехвърлÑне на отделеното време към задача:' + text_user_wrote: "%{value} напиÑа:" + text_enumeration_destroy_question: "%{count} обекта Ñа Ñвързани Ñ Ñ‚Ð°Ð·Ð¸ ÑтойноÑÑ‚." + text_enumeration_category_reassign_to: 'ПреÑвържете ги към тази ÑтойноÑÑ‚:' + text_email_delivery_not_configured: "Изпращането на e-mail-и не е конфигурирано и извеÑтиÑта не Ñа разрешени.\nКонфигурирайте Ð²Ð°ÑˆÐ¸Ñ SMTP Ñървър в config/configuration.yml и реÑтартирайте Redmine, за да ги разрешите." + text_repository_usernames_mapping: "Изберете или променете потребителите в Redmine, ÑъответÑтващи на потребителите в дневника на хранилището (repository).\nПотребителите Ñ ÐµÐ´Ð½Ð°ÐºÐ²Ð¸ имена в Redmine и хранилищата Ñе ÑъвмеÑÑ‚Ñват автоматично." + text_diff_truncated: '... Този diff не е пълен, понеже е Ð½Ð°Ð´Ñ…Ð²ÑŠÑ€Ð»Ñ Ð¼Ð°ÐºÑÐ¸Ð¼Ð°Ð»Ð½Ð¸Ñ Ñ€Ð°Ð·Ð¼ÐµÑ€, който може да бъде показан.' + text_custom_field_possible_values_info: 'Една ÑтойноÑÑ‚ на ред' + text_wiki_page_destroy_question: Тази Ñтраница има %{descendants} Ñтраници деца и descendant(s). Какво желаете да правите? + text_wiki_page_nullify_children: Запазване на тези Ñтраници като коренни Ñтраници + text_wiki_page_destroy_children: Изтриване на Ñтраниците деца и вÑички техни descendants + text_wiki_page_reassign_children: Преназначаване на Ñтраниците деца на тази родителÑка Ñтраница + text_own_membership_delete_confirmation: "Вие Ñте на път да премахнете нÑкои или вÑички ваши Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð¸ е възможно Ñлед това да не можете да редактирате този проект.\nСигурен ли Ñте, че иÑкате да продължите?" + text_zoom_in: Увеличаване + text_zoom_out: ÐамалÑване + text_warn_on_leaving_unsaved: Страницата Ñъдържа незапиÑано Ñъдържание, което може да бъде загубено, ако Ñ Ð½Ð°Ð¿ÑƒÑнете. + text_scm_path_encoding_note: "По подразбиране: UTF-8" + text_git_repository_note: Празно и локално хранилище (например /gitrepo, c:\gitrepo) + text_mercurial_repository_note: Локално хранилище (например /hgrepo, c:\hgrepo) + text_scm_command: SCM команда + text_scm_command_version: ВерÑÐ¸Ñ + text_scm_config: Можете да конфигурирате SCM командите в config/configuration.yml. За да активирате промените, реÑтартирайте Redmine. + text_scm_command_not_available: SCM командата не е налична или доÑтъпна. Проверете конфигурациÑта в админиÑÑ‚Ñ€Ð°Ñ‚Ð¸Ð²Ð½Ð¸Ñ Ð¿Ð°Ð½ÐµÐ». + text_issue_conflict_resolution_overwrite: Прилагане на моите промени (предишните коментари ще бъдат запазени, но нÑкои други промени може да бъдат презапиÑани) + text_issue_conflict_resolution_add_notes: ДобавÑне на моите коментари и отхвърлÑне на другите мои промени + text_issue_conflict_resolution_cancel: ОтхвърлÑне на вÑички мои промени и презареждане на %{link} + text_account_destroy_confirmation: "Сигурен/на ли Ñте, че желаете да продължите?\nВашиÑÑ‚ профил ще бъде премахнат без възможноÑÑ‚ за възÑтановÑване." + text_session_expiration_settings: "Внимание: промÑната на тези уÑтановÑÐ²Ð°Ð½Ð¾Ñ Ð¼Ð¾Ð¶Ðµ да прекрати вÑички активни ÑеÑии, включително и вашата." + text_project_closed: Този проект е затворен и е Ñамо за четене. + + default_role_manager: Мениджър + default_role_developer: Разработчик + default_role_reporter: Публикуващ + default_tracker_bug: Грешка + default_tracker_feature: ФункционалноÑÑ‚ + default_tracker_support: Поддръжка + default_issue_status_new: Ðова + default_issue_status_in_progress: Изпълнение + default_issue_status_resolved: Приключена + default_issue_status_feedback: Обратна връзка + default_issue_status_closed: Затворена + default_issue_status_rejected: Отхвърлена + default_doc_category_user: Ð”Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ð¸Ñ Ð·Ð° Ð¿Ð¾Ñ‚Ñ€ÐµÐ±Ð¸Ñ‚ÐµÐ»Ñ + default_doc_category_tech: ТехничеÑка Ð´Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ð¸Ñ + default_priority_low: ÐиÑък + default_priority_normal: Ðормален + default_priority_high: ВиÑок + default_priority_urgent: Спешен + default_priority_immediate: Веднага + default_activity_design: Дизайн + default_activity_development: Разработка + + enumeration_issue_priorities: Приоритети на задачи + enumeration_doc_categories: Категории документи + enumeration_activities: ДейноÑти (time tracking) + enumeration_system_activity: СиÑтемна активноÑÑ‚ + description_filter: Филтър + description_search: ТърÑене + description_choose_project: Проекти + description_project_scope: Обхват на търÑенето + description_notes: Бележки + description_message_content: Съдържание на Ñъобщението + description_query_sort_criteria_attribute: Ðтрибут на Ñортиране + description_query_sort_criteria_direction: ПоÑока на Ñортиране + description_user_mail_notification: ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ð¸Ð·Ð²ÐµÑтиÑта по пощата + description_available_columns: Ðалични колони + description_selected_columns: Избрани колони + description_issue_category_reassign: Изберете ÐºÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ + description_wiki_subpages_reassign: Изберете нова родителÑка Ñтраница + description_all_columns: Ð’Ñички колони + description_date_range_list: Изберете диапазон от ÑпиÑъка + description_date_range_interval: Изберете диапазон чрез задаване на начална и крайна дати + description_date_from: Въведете начална дата + description_date_to: Въведете крайна дата + text_repository_identifier_info: 'Позволени Ñа малки букви (a-z), цифри, тирета и _.
    ПромÑна Ñлед Ñъздаването му не е възможна.' diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fc/fc704247258255beec93055af5d732e51089fa26.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fc/fc704247258255beec93055af5d732e51089fa26.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1080 @@ +ro: + direction: ltr + date: + formats: + default: "%d-%m-%Y" + short: "%d %b" + long: "%d %B %Y" + only_day: "%e" + + day_names: [Duminică, Luni, Marti, Miercuri, Joi, Vineri, Sâmbătă] + abbr_day_names: [Dum, Lun, Mar, Mie, Joi, Vin, Sâm] + + # Don't forget the nil at the beginning; there's no such thing as a 0th month + month_names: [~, Ianuarie, Februarie, Martie, Aprilie, Mai, Iunie, Iulie, August, Septembrie, Octombrie, Noiembrie, Decembrie] + abbr_month_names: [~, Ian, Feb, Mar, Apr, Mai, Iun, Iul, Aug, Sep, Oct, Noi, Dec] + # Used in date_select and datime_select. + order: + - :day + - :month + - :year + + time: + formats: + default: "%m/%d/%Y %I:%M %p" + time: "%I:%M %p" + short: "%d %b %H:%M" + long: "%B %d, %Y %H:%M" + am: "am" + pm: "pm" + + datetime: + distance_in_words: + half_a_minute: "jumătate de minut" + less_than_x_seconds: + one: "mai puÈ›in de o secundă" + other: "mai puÈ›in de %{count} secunde" + x_seconds: + one: "o secundă" + other: "%{count} secunde" + less_than_x_minutes: + one: "mai puÈ›in de un minut" + other: "mai puÈ›in de %{count} minute" + x_minutes: + one: "un minut" + other: "%{count} minute" + about_x_hours: + one: "aproximativ o oră" + other: "aproximativ %{count} ore" + x_hours: + one: "1 hour" + other: "%{count} hours" + x_days: + one: "o zi" + other: "%{count} zile" + about_x_months: + one: "aproximativ o lună" + other: "aproximativ %{count} luni" + x_months: + one: "o luna" + other: "%{count} luni" + about_x_years: + one: "aproximativ un an" + other: "aproximativ %{count} ani" + over_x_years: + one: "peste un an" + other: "peste %{count} ani" + almost_x_years: + one: "almost 1 year" + other: "almost %{count} years" + + number: + format: + separator: "." + delimiter: "" + precision: 3 + + human: + format: + precision: 3 + delimiter: "" + storage_units: + format: "%n %u" + units: + kb: KB + tb: TB + gb: GB + byte: + one: Byte + other: Bytes + mb: MB + +# Used in array.to_sentence. + support: + array: + sentence_connector: "È™i" + skip_last_comma: true + + activerecord: + errors: + template: + header: + one: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + messages: + inclusion: "nu este inclus în listă" + exclusion: "este rezervat" + invalid: "nu este valid" + confirmation: "nu este identică" + accepted: "trebuie acceptat" + empty: "trebuie completat" + blank: "nu poate fi gol" + too_long: "este prea lung" + too_short: "este prea scurt" + wrong_length: "nu are lungimea corectă" + taken: "a fost luat deja" + not_a_number: "nu este un număr" + not_a_date: "nu este o dată validă" + greater_than: "trebuie să fie mai mare de %{count}" + greater_than_or_equal_to: "trebuie să fie mai mare sau egal cu %{count}" + equal_to: "trebuie să fie egal cu {count}}" + less_than: "trebuie să fie mai mic decat %{count}" + less_than_or_equal_to: "trebuie să fie mai mic sau egal cu %{count}" + odd: "trebuie să fie impar" + even: "trebuie să fie par" + greater_than_start_date: "trebuie să fie după data de început" + not_same_project: "trebuie să aparÈ›ină aceluiaÈ™i proiect" + circular_dependency: "Această relaÈ›ie ar crea o dependență circulară" + cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks" + + actionview_instancetag_blank_option: SelectaÈ›i + + general_text_No: 'Nu' + general_text_Yes: 'Da' + general_text_no: 'nu' + general_text_yes: 'da' + general_lang_name: 'Română' + general_csv_separator: '.' + general_csv_decimal_separator: ',' + general_csv_encoding: UTF-8 + general_pdf_encoding: UTF-8 + general_first_day_of_week: '2' + + notice_account_updated: Cont actualizat. + notice_account_invalid_creditentials: Utilizator sau parola nevalidă + notice_account_password_updated: Parolă actualizată. + notice_account_wrong_password: Parolă greÈ™ită + notice_account_register_done: Contul a fost creat. Pentru activare, urmaÈ›i legătura trimisă prin email. + notice_account_unknown_email: Utilizator necunoscut. + notice_can_t_change_password: Acest cont foloseÈ™te o sursă externă de autentificare. Nu se poate schimba parola. + notice_account_lost_email_sent: S-a trimis un email cu instrucÈ›iuni de schimbare a parolei. + notice_account_activated: Contul a fost activat. Vă puteÈ›i autentifica acum. + notice_successful_create: Creat. + notice_successful_update: Actualizat. + notice_successful_delete: Șters. + notice_successful_connection: Conectat. + notice_file_not_found: Pagina pe care doriÈ›i să o accesaÈ›i nu există sau a fost È™tearsă. + notice_locking_conflict: Datele au fost actualizate de alt utilizator. + notice_not_authorized: Nu sunteÈ›i autorizat sa accesaÈ›i această pagină. + notice_email_sent: "S-a trimis un email către %{value}" + notice_email_error: "A intervenit o eroare la trimiterea de email (%{value})" + notice_feeds_access_key_reseted: Cheia de acces RSS a fost resetată. + notice_failed_to_save_issues: "Nu s-au putut salva %{count} tichete din cele %{total} selectate: %{ids}." + notice_no_issue_selected: "Niciun tichet selectat! Vă rugăm să selectaÈ›i tichetele pe care doriÈ›i să le editaÈ›i." + notice_account_pending: "Contul dumneavoastră a fost creat È™i aÈ™teaptă aprobarea administratorului." + notice_default_data_loaded: S-a încărcat configuraÈ›ia implicită. + notice_unable_delete_version: Nu se poate È™terge versiunea. + + error_can_t_load_default_data: "Nu s-a putut încărca configuraÈ›ia implicită: %{value}" + error_scm_not_found: "Nu s-a găsit articolul sau revizia în depozit." + error_scm_command_failed: "A intervenit o eroare la accesarea depozitului: %{value}" + error_scm_annotate: "Nu există sau nu poate fi adnotată." + error_issue_not_found_in_project: 'Tichetul nu a fost găsit sau nu aparÈ›ine acestui proiect' + + warning_attachments_not_saved: "Nu s-au putut salva %{count} fiÈ™iere." + + mail_subject_lost_password: "Parola dumneavoastră: %{value}" + mail_body_lost_password: 'Pentru a schimba parola, accesaÈ›i:' + mail_subject_register: "Activarea contului %{value}" + mail_body_register: 'Pentru activarea contului, accesaÈ›i:' + mail_body_account_information_external: "PuteÈ›i folosi contul „{value}}†pentru a vă autentifica." + mail_body_account_information: InformaÈ›ii despre contul dumneavoastră + mail_subject_account_activation_request: "Cerere de activare a contului %{value}" + mail_body_account_activation_request: "S-a înregistrat un utilizator nou (%{value}). Contul aÈ™teaptă aprobarea dumneavoastră:" + mail_subject_reminder: "%{count} tichete trebuie rezolvate în următoarele %{days} zile" + mail_body_reminder: "%{count} tichete atribuite dumneavoastră trebuie rezolvate în următoarele %{days} zile:" + + gui_validation_error: o eroare + gui_validation_error_plural: "%{count} erori" + + field_name: Nume + field_description: Descriere + field_summary: Rezumat + field_is_required: Obligatoriu + field_firstname: Prenume + field_lastname: Nume + field_mail: Email + field_filename: FiÈ™ier + field_filesize: Mărime + field_downloads: Descărcări + field_author: Autor + field_created_on: Creat la + field_updated_on: Actualizat la + field_field_format: Format + field_is_for_all: Pentru toate proiectele + field_possible_values: Valori posibile + field_regexp: Expresie regulară + field_min_length: lungime minimă + field_max_length: lungime maximă + field_value: Valoare + field_category: Categorie + field_title: Titlu + field_project: Proiect + field_issue: Tichet + field_status: Stare + field_notes: Note + field_is_closed: Rezolvat + field_is_default: Implicit + field_tracker: Tip de tichet + field_subject: Subiect + field_due_date: Data finalizării + field_assigned_to: Atribuit + field_priority: Prioritate + field_fixed_version: Versiune È›intă + field_user: Utilizator + field_role: Rol + field_homepage: Pagina principală + field_is_public: Public + field_parent: Sub-proiect al + field_is_in_roadmap: Tichete afiÈ™ate în plan + field_login: Autentificare + field_mail_notification: Notificări prin e-mail + field_admin: Administrator + field_last_login_on: Ultima autentificare în + field_language: Limba + field_effective_date: Data + field_password: Parola + field_new_password: Parola nouă + field_password_confirmation: Confirmare + field_version: Versiune + field_type: Tip + field_host: Gazdă + field_port: Port + field_account: Cont + field_base_dn: Base DN + field_attr_login: Atribut autentificare + field_attr_firstname: Atribut prenume + field_attr_lastname: Atribut nume + field_attr_mail: Atribut email + field_onthefly: Creare utilizator pe loc + field_start_date: Data începerii + field_done_ratio: Realizat (%) + field_auth_source: Mod autentificare + field_hide_mail: Nu se afiÈ™ează adresa de email + field_comments: Comentariu + field_url: URL + field_start_page: Pagina de start + field_subproject: Subproiect + field_hours: Ore + field_activity: Activitate + field_spent_on: Data + field_identifier: Identificator + field_is_filter: Filtru + field_issue_to: Tichet asociat + field_delay: ÃŽntârziere + field_assignable: Se pot atribui tichete acestui rol + field_redirect_existing_links: RedirecÈ›ionează legăturile existente + field_estimated_hours: Timp estimat + field_column_names: Coloane + field_time_zone: Fus orar + field_searchable: Căutare + field_default_value: Valoare implicita + field_comments_sorting: AfiÈ™ează comentarii + field_parent_title: Pagina superioara + field_editable: Modificabil + field_watcher: UrmăreÈ™te + field_identity_url: URL OpenID + field_content: ConÈ›inut + + setting_app_title: Titlu aplicaÈ›ie + setting_app_subtitle: Subtitlu aplicaÈ›ie + setting_welcome_text: Text de întâmpinare + setting_default_language: Limba implicita + setting_login_required: Necesita autentificare + setting_self_registration: ÃŽnregistrare automată + setting_attachment_max_size: Mărime maxima ataÈ™ament + setting_issues_export_limit: Limită de tichete exportate + setting_mail_from: Adresa de email a expeditorului + setting_bcc_recipients: AlÈ›i destinatari pentru email (BCC) + setting_plain_text_mail: Mesaje text (fără HTML) + setting_host_name: Numele gazdei È™i calea + setting_text_formatting: Formatare text + setting_wiki_compression: Comprimare istoric Wiki + setting_feeds_limit: Limita de actualizări din feed + setting_default_projects_public: Proiectele noi sunt implicit publice + setting_autofetch_changesets: Preluare automată a modificărilor din depozit + setting_sys_api_enabled: Activare WS pentru gestionat depozitul + setting_commit_ref_keywords: Cuvinte cheie pt. referire tichet + setting_commit_fix_keywords: Cuvinte cheie pt. rezolvare tichet + setting_autologin: Autentificare automată + setting_date_format: Format dată + setting_time_format: Format oră + setting_cross_project_issue_relations: Permite legături de tichete între proiecte + setting_issue_list_default_columns: Coloane implicite afiÈ™ate în lista de tichete + setting_emails_footer: Subsol email + setting_protocol: Protocol + setting_per_page_options: Număr de obiecte pe pagină + setting_user_format: Stil de afiÈ™are pentru utilizator + setting_activity_days_default: Se afiÈ™ează zile în jurnalul proiectului + setting_display_subprojects_issues: AfiÈ™ează implicit tichetele sub-proiectelor în proiectele principale + setting_enabled_scm: SCM activat + setting_mail_handler_api_enabled: Activare WS pentru email primit + setting_mail_handler_api_key: cheie API + setting_sequential_project_identifiers: Generează secvenÈ›ial identificatoarele de proiect + setting_gravatar_enabled: FoloseÈ™te poze Gravatar pentru utilizatori + setting_diff_max_lines_displayed: Număr maxim de linii de diferență afiÈ™ate + setting_file_max_size_displayed: Număr maxim de fiÈ™iere text afiÈ™ate în pagină (inline) + setting_repository_log_display_limit: Număr maxim de revizii afiÈ™ate în istoricul fiÈ™ierului + setting_openid: Permite înregistrare È™i autentificare cu OpenID + + permission_edit_project: Editează proiectul + permission_select_project_modules: Alege module pentru proiect + permission_manage_members: Editează membri + permission_manage_versions: Editează versiuni + permission_manage_categories: Editează categorii + permission_add_issues: Adaugă tichete + permission_edit_issues: Editează tichete + permission_manage_issue_relations: Editează relaÈ›ii tichete + permission_add_issue_notes: Adaugă note + permission_edit_issue_notes: Editează note + permission_edit_own_issue_notes: Editează notele proprii + permission_move_issues: Mută tichete + permission_delete_issues: Șterge tichete + permission_manage_public_queries: Editează căutările implicite + permission_save_queries: Salvează căutările + permission_view_gantt: AfiÈ™ează Gantt + permission_view_calendar: AfiÈ™ează calendarul + permission_view_issue_watchers: AfiÈ™ează lista de persoane interesate + permission_add_issue_watchers: Adaugă persoane interesate + permission_log_time: ÃŽnregistrează timpul de lucru + permission_view_time_entries: AfiÈ™ează timpul de lucru + permission_edit_time_entries: Editează jurnalele cu timp de lucru + permission_edit_own_time_entries: Editează jurnalele proprii cu timpul de lucru + permission_manage_news: Editează È™tiri + permission_comment_news: Comentează È™tirile + permission_manage_documents: Editează documente + permission_view_documents: AfiÈ™ează documente + permission_manage_files: Editează fiÈ™iere + permission_view_files: AfiÈ™ează fiÈ™iere + permission_manage_wiki: Editează wiki + permission_rename_wiki_pages: RedenumeÈ™te pagini wiki + permission_delete_wiki_pages: Șterge pagini wiki + permission_view_wiki_pages: AfiÈ™ează wiki + permission_view_wiki_edits: AfiÈ™ează istoricul wiki + permission_edit_wiki_pages: Editează pagini wiki + permission_delete_wiki_pages_attachments: Șterge ataÈ™amente + permission_protect_wiki_pages: Blochează pagini wiki + permission_manage_repository: Gestionează depozitul + permission_browse_repository: RăsfoieÈ™te depozitul + permission_view_changesets: AfiÈ™ează modificările din depozit + permission_commit_access: Acces commit + permission_manage_boards: Editează forum + permission_view_messages: AfiÈ™ează mesaje + permission_add_messages: Scrie mesaje + permission_edit_messages: Editează mesaje + permission_edit_own_messages: Editează mesajele proprii + permission_delete_messages: Șterge mesaje + permission_delete_own_messages: Șterge mesajele proprii + + project_module_issue_tracking: Tichete + project_module_time_tracking: Timp de lucru + project_module_news: Știri + project_module_documents: Documente + project_module_files: FiÈ™iere + project_module_wiki: Wiki + project_module_repository: Depozit + project_module_boards: Forum + + label_user: Utilizator + label_user_plural: Utilizatori + label_user_new: Utilizator nou + label_project: Proiect + label_project_new: Proiect nou + label_project_plural: Proiecte + label_x_projects: + zero: niciun proiect + one: un proiect + other: "%{count} proiecte" + label_project_all: Toate proiectele + label_project_latest: Proiecte noi + label_issue: Tichet + label_issue_new: Tichet nou + label_issue_plural: Tichete + label_issue_view_all: AfiÈ™ează toate tichetele + label_issues_by: "Sortează după %{value}" + label_issue_added: Adaugat + label_issue_updated: Actualizat + label_document: Document + label_document_new: Document nou + label_document_plural: Documente + label_document_added: Adăugat + label_role: Rol + label_role_plural: Roluri + label_role_new: Rol nou + label_role_and_permissions: Roluri È™i permisiuni + label_member: Membru + label_member_new: membru nou + label_member_plural: Membri + label_tracker: Tip de tichet + label_tracker_plural: Tipuri de tichete + label_tracker_new: Tip nou de tichet + label_workflow: Mod de lucru + label_issue_status: Stare tichet + label_issue_status_plural: Stare tichete + label_issue_status_new: Stare nouă + label_issue_category: Categorie de tichet + label_issue_category_plural: Categorii de tichete + label_issue_category_new: Categorie nouă + label_custom_field: Câmp personalizat + label_custom_field_plural: Câmpuri personalizate + label_custom_field_new: Câmp nou personalizat + label_enumerations: Enumerări + label_enumeration_new: Valoare nouă + label_information: InformaÈ›ie + label_information_plural: InformaÈ›ii + label_please_login: Vă rugăm să vă autentificaÈ›i + label_register: ÃŽnregistrare + label_login_with_open_id_option: sau autentificare cu OpenID + label_password_lost: Parolă uitată + label_home: Acasă + label_my_page: Pagina mea + label_my_account: Contul meu + label_my_projects: Proiectele mele + label_administration: Administrare + label_login: Autentificare + label_logout: IeÈ™ire din cont + label_help: Ajutor + label_reported_issues: Tichete + label_assigned_to_me_issues: Tichetele mele + label_last_login: Ultima conectare + label_registered_on: ÃŽnregistrat la + label_activity: Activitate + label_overall_activity: Activitate - vedere de ansamblu + label_user_activity: "Activitate %{value}" + label_new: Nou + label_logged_as: Autentificat ca + label_environment: Mediu + label_authentication: Autentificare + label_auth_source: Mod de autentificare + label_auth_source_new: Nou + label_auth_source_plural: Moduri de autentificare + label_subproject_plural: Sub-proiecte + label_and_its_subprojects: "%{value} È™i sub-proiecte" + label_min_max_length: lungime min - max + label_list: Listă + label_date: Dată + label_integer: ÃŽntreg + label_float: Zecimal + label_boolean: Valoare logică + label_string: Text + label_text: Text lung + label_attribute: Atribut + label_attribute_plural: Atribute + label_download: "%{count} descărcare" + label_download_plural: "%{count} descărcări" + label_no_data: Nu există date de afiÈ™at + label_change_status: Schimbă starea + label_history: Istoric + label_attachment: FiÈ™ier + label_attachment_new: FiÈ™ier nou + label_attachment_delete: Șterge fiÈ™ier + label_attachment_plural: FiÈ™iere + label_file_added: Adăugat + label_report: Raport + label_report_plural: Rapoarte + label_news: Știri + label_news_new: Adaugă È™tire + label_news_plural: Știri + label_news_latest: Ultimele È™tiri + label_news_view_all: AfiÈ™ează toate È™tirile + label_news_added: Adăugat + label_settings: Setări + label_overview: Pagină proiect + label_version: Versiune + label_version_new: Versiune nouă + label_version_plural: Versiuni + label_confirmation: Confirmare + label_export_to: 'Disponibil È™i în:' + label_read: CiteÈ™te... + label_public_projects: Proiecte publice + label_open_issues: deschis + label_open_issues_plural: deschise + label_closed_issues: închis + label_closed_issues_plural: închise + label_x_open_issues_abbr_on_total: + zero: 0 deschise / %{total} + one: 1 deschis / %{total} + other: "%{count} deschise / %{total}" + label_x_open_issues_abbr: + zero: 0 deschise + one: 1 deschis + other: "%{count} deschise" + label_x_closed_issues_abbr: + zero: 0 închise + one: 1 închis + other: "%{count} închise" + label_total: Total + label_permissions: Permisiuni + label_current_status: Stare curentă + label_new_statuses_allowed: Stări noi permise + label_all: toate + label_none: niciunul + label_nobody: nimeni + label_next: ÃŽnainte + label_previous: ÃŽnapoi + label_used_by: Folosit de + label_details: Detalii + label_add_note: Adaugă o notă + label_per_page: pe pagină + label_calendar: Calendar + label_months_from: luni de la + label_gantt: Gantt + label_internal: Intern + label_last_changes: "ultimele %{count} schimbări" + label_change_view_all: AfiÈ™ează toate schimbările + label_personalize_page: Personalizează aceasta pagina + label_comment: Comentariu + label_comment_plural: Comentarii + label_x_comments: + zero: fara comentarii + one: 1 comentariu + other: "%{count} comentarii" + label_comment_add: Adaugă un comentariu + label_comment_added: Adăugat + label_comment_delete: Șterge comentariul + label_query: Cautare personalizata + label_query_plural: Căutări personalizate + label_query_new: Căutare nouă + label_filter_add: Adaugă filtru + label_filter_plural: Filtre + label_equals: este + label_not_equals: nu este + label_in_less_than: în mai puÈ›in de + label_in_more_than: în mai mult de + label_in: în + label_today: astăzi + label_all_time: oricând + label_yesterday: ieri + label_this_week: săptămâna aceasta + label_last_week: săptămâna trecută + label_last_n_days: "ultimele %{count} zile" + label_this_month: luna aceasta + label_last_month: luna trecută + label_this_year: anul acesta + label_date_range: Perioada + label_less_than_ago: mai puÈ›in de ... zile + label_more_than_ago: mai mult de ... zile + label_ago: în urma + label_contains: conÈ›ine + label_not_contains: nu conÈ›ine + label_day_plural: zile + label_repository: Depozit + label_repository_plural: Depozite + label_browse: AfiÈ™ează + label_modification: "%{count} schimbare" + label_modification_plural: "%{count} schimbări" + label_revision: Revizie + label_revision_plural: Revizii + label_associated_revisions: Revizii asociate + label_added: adaugată + label_modified: modificată + label_copied: copiată + label_renamed: redenumită + label_deleted: È™tearsă + label_latest_revision: Ultima revizie + label_latest_revision_plural: Ultimele revizii + label_view_revisions: AfiÈ™ează revizii + label_max_size: Mărime maximă + label_sort_highest: Prima + label_sort_higher: ÃŽn sus + label_sort_lower: ÃŽn jos + label_sort_lowest: Ultima + label_roadmap: Planificare + label_roadmap_due_in: "De terminat în %{value}" + label_roadmap_overdue: "ÃŽntârziat cu %{value}" + label_roadmap_no_issues: Nu există tichete pentru această versiune + label_search: Caută + label_result_plural: Rezultate + label_all_words: toate cuvintele + label_wiki: Wiki + label_wiki_edit: Editare Wiki + label_wiki_edit_plural: Editări Wiki + label_wiki_page: Pagină Wiki + label_wiki_page_plural: Pagini Wiki + label_index_by_title: Sortează după titlu + label_index_by_date: Sortează după dată + label_current_version: Versiunea curentă + label_preview: Previzualizare + label_feed_plural: Feed-uri + label_changes_details: Detaliile tuturor schimbărilor + label_issue_tracking: Urmărire tichete + label_spent_time: Timp alocat + label_f_hour: "%{value} oră" + label_f_hour_plural: "%{value} ore" + label_time_tracking: Urmărire timp de lucru + label_change_plural: Schimbări + label_statistics: Statistici + label_commits_per_month: Commit pe luna + label_commits_per_author: Commit per autor + label_view_diff: AfiÈ™ează diferenÈ›ele + label_diff_inline: în linie + label_diff_side_by_side: una lângă alta + label_options: OpÈ›iuni + label_copy_workflow_from: Copiază modul de lucru de la + label_permissions_report: Permisiuni + label_watched_issues: Tichete urmărite + label_related_issues: Tichete asociate + label_applied_status: Stare aplicată + label_loading: ÃŽncarcă... + label_relation_new: Asociere nouă + label_relation_delete: Șterge asocierea + label_relates_to: asociat cu + label_duplicates: duplicate + label_duplicated_by: la fel ca + label_blocks: blocări + label_blocked_by: blocat de + label_precedes: precede + label_follows: urmează + label_end_to_start: de la sfârÈ™it la început + label_end_to_end: de la sfârÈ™it la sfârÈ™it + label_start_to_start: de la început la început + label_start_to_end: de la început la sfârÈ™it + label_stay_logged_in: Păstrează autentificarea + label_disabled: dezactivat + label_show_completed_versions: Arată versiunile terminate + label_me: eu + label_board: Forum + label_board_new: Forum nou + label_board_plural: Forumuri + label_topic_plural: Subiecte + label_message_plural: Mesaje + label_message_last: Ultimul mesaj + label_message_new: Mesaj nou + label_message_posted: Adăugat + label_reply_plural: Răspunsuri + label_send_information: Trimite utilizatorului informaÈ›iile despre cont + label_year: An + label_month: Lună + label_week: Săptămână + label_date_from: De la + label_date_to: La + label_language_based: Un funcÈ›ie de limba de afiÈ™are a utilizatorului + label_sort_by: "Sortează după %{value}" + label_send_test_email: Trimite email de test + label_feeds_access_key_created_on: "Cheie de acces creată acum %{value}" + label_module_plural: Module + label_added_time_by: "Adăugat de %{author} acum %{age}" + label_updated_time_by: "Actualizat de %{author} acum %{age}" + label_updated_time: "Actualizat acum %{value}" + label_jump_to_a_project: Alege proiectul... + label_file_plural: FiÈ™iere + label_changeset_plural: Schimbări + label_default_columns: Coloane implicite + label_no_change_option: (fără schimbări) + label_bulk_edit_selected_issues: Editează toate tichetele selectate + label_theme: Tema + label_default: Implicită + label_search_titles_only: Caută numai în titluri + label_user_mail_option_all: "Pentru orice eveniment, în toate proiectele mele" + label_user_mail_option_selected: " Pentru orice eveniment, în proiectele selectate..." + label_user_mail_no_self_notified: "Nu trimite notificări pentru modificările mele" + label_registration_activation_by_email: activare cont prin email + label_registration_manual_activation: activare manuală a contului + label_registration_automatic_activation: activare automată a contului + label_display_per_page: "pe pagină: %{value}" + label_age: vechime + label_change_properties: Schimbă proprietățile + label_general: General + label_more: Mai mult + label_scm: SCM + label_plugins: Plugin-uri + label_ldap_authentication: autentificare LDAP + label_downloads_abbr: D/L + label_optional_description: Descriere (opÈ›ională) + label_add_another_file: Adaugă alt fiÈ™ier + label_preferences: PreferinÈ›e + label_chronological_order: în ordine cronologică + label_reverse_chronological_order: ÃŽn ordine invers cronologică + label_planning: Planificare + label_incoming_emails: Mesaje primite + label_generate_key: Generează o cheie + label_issue_watchers: Cine urmăreÈ™te + label_example: Exemplu + label_display: AfiÈ™ează + + label_sort: Sortează + label_ascending: Crescător + label_descending: Descrescător + label_date_from_to: De la %{start} la %{end} + + button_login: Autentificare + button_submit: Trimite + button_save: Salvează + button_check_all: Bifează tot + button_uncheck_all: Debifează tot + button_delete: Șterge + button_create: Creează + button_create_and_continue: Creează È™i continua + button_test: Testează + button_edit: Editează + button_add: Adaugă + button_change: Modifică + button_apply: Aplică + button_clear: Șterge + button_lock: Blochează + button_unlock: Deblochează + button_download: Descarcă + button_list: Listează + button_view: AfiÈ™ează + button_move: Mută + button_back: ÃŽnapoi + button_cancel: Anulează + button_activate: Activează + button_sort: Sortează + button_log_time: ÃŽnregistrează timpul de lucru + button_rollback: Revenire la această versiune + button_watch: Urmăresc + button_unwatch: Nu urmăresc + button_reply: Răspunde + button_archive: Arhivează + button_unarchive: Dezarhivează + button_reset: Resetează + button_rename: RedenumeÈ™te + button_change_password: Schimbare parolă + button_copy: Copiază + button_annotate: Adnotează + button_update: Actualizează + button_configure: Configurează + button_quote: Citează + + status_active: activ + status_registered: înregistrat + status_locked: blocat + + text_select_mail_notifications: SelectaÈ›i acÈ›iunile notificate prin email. + text_regexp_info: ex. ^[A-Z0-9]+$ + text_min_max_length_info: 0 înseamnă fără restricÈ›ii + text_project_destroy_confirmation: Sigur doriÈ›i să È™tergeÈ›i proiectul È™i toate datele asociate? + text_subprojects_destroy_warning: "Se vor È™terge È™i sub-proiectele: %{value}." + text_workflow_edit: SelectaÈ›i un rol È™i un tip de tichet pentru a edita modul de lucru + text_are_you_sure: SunteÈ›i sigur(ă)? + text_tip_issue_begin_day: sarcină care începe în această zi + text_tip_issue_end_day: sarcină care se termină în această zi + text_tip_issue_begin_end_day: sarcină care începe È™i se termină în această zi + text_caracters_maximum: "maxim %{count} caractere." + text_caracters_minimum: "Trebuie să fie minim %{count} caractere." + text_length_between: "Lungime între %{min} È™i %{max} caractere." + text_tracker_no_workflow: Nu sunt moduri de lucru pentru acest tip de tichet + text_unallowed_characters: Caractere nepermise + text_comma_separated: Sunt permise mai multe valori (separate cu virgulă). + text_issues_ref_in_commit_messages: Referire la tichete È™i rezolvare în textul mesajului + text_issue_added: "Tichetul %{id} a fost adăugat de %{author}." + text_issue_updated: "Tichetul %{id} a fost actualizat de %{author}." + text_wiki_destroy_confirmation: Sigur doriÈ›i È™tergerea Wiki È™i a conÈ›inutului asociat? + text_issue_category_destroy_question: "Această categorie conÈ›ine (%{count}) tichete. Ce doriÈ›i să faceÈ›i?" + text_issue_category_destroy_assignments: Șterge apartenenÈ›a la categorie. + text_issue_category_reassign_to: Atribuie tichetele la această categorie + text_user_mail_option: "Pentru proiectele care nu sunt selectate, veÈ›i primi notificări doar pentru ceea ce urmăriÈ›i sau în ce sunteÈ›i implicat (ex: tichete create de dumneavoastră sau care vă sunt atribuite)." + text_no_configuration_data: "Nu s-au configurat încă rolurile, stările tichetelor È™i modurile de lucru.\nEste recomandat să încărcaÈ›i configuraÈ›ia implicită. O veÈ›i putea modifica ulterior." + text_load_default_configuration: ÃŽncarcă configuraÈ›ia implicită + text_status_changed_by_changeset: "Aplicat în setul %{value}." + text_issues_destroy_confirmation: 'Sigur doriÈ›i să È™tergeÈ›i tichetele selectate?' + text_select_project_modules: 'SelectaÈ›i modulele active pentru acest proiect:' + text_default_administrator_account_changed: S-a schimbat contul administratorului implicit + text_file_repository_writable: Se poate scrie în directorul de ataÈ™amente + text_plugin_assets_writable: Se poate scrie în directorul de plugin-uri + text_rmagick_available: Este disponibil RMagick (opÈ›ional) + text_destroy_time_entries_question: "%{hours} ore sunt înregistrate la tichetele pe care doriÈ›i să le È™tergeÈ›i. Ce doriÈ›i sa faceÈ›i?" + text_destroy_time_entries: Șterge orele înregistrate + text_assign_time_entries_to_project: Atribuie orele la proiect + text_reassign_time_entries: 'Atribuie orele înregistrate la tichetul:' + text_user_wrote: "%{value} a scris:" + text_enumeration_destroy_question: "Această valoare are %{count} obiecte." + text_enumeration_category_reassign_to: 'Atribuie la această valoare:' + text_email_delivery_not_configured: "Trimiterea de emailuri nu este configurată È™i ca urmare, notificările sunt dezactivate.\nConfiguraÈ›i serverul SMTP în config/configuration.yml È™i reporniÈ›i aplicaÈ›ia pentru a le activa." + text_repository_usernames_mapping: "SelectaÈ›i sau modificaÈ›i contul Redmine echivalent contului din istoricul depozitului.\nUtilizatorii cu un cont (sau e-mail) identic în Redmine È™i depozit sunt echivalate automat." + text_diff_truncated: '... ComparaÈ›ia a fost trunchiată pentru ca depășeÈ™te lungimea maximă de text care poate fi afiÈ™at.' + text_custom_field_possible_values_info: 'O linie pentru fiecare valoare' + + default_role_manager: Manager + default_role_developer: Dezvoltator + default_role_reporter: Creator de rapoarte + default_tracker_bug: Defect + default_tracker_feature: FuncÈ›ie + default_tracker_support: Suport + default_issue_status_new: Nou + default_issue_status_in_progress: In Progress + default_issue_status_resolved: Rezolvat + default_issue_status_feedback: AÈ™teaptă reacÈ›ii + default_issue_status_closed: ÃŽnchis + default_issue_status_rejected: Respins + default_doc_category_user: DocumentaÈ›ie + default_doc_category_tech: DocumentaÈ›ie tehnică + default_priority_low: mică + default_priority_normal: normală + default_priority_high: mare + default_priority_urgent: urgentă + default_priority_immediate: imediată + default_activity_design: Design + default_activity_development: Dezvoltare + + enumeration_issue_priorities: Priorități tichete + enumeration_doc_categories: Categorii documente + enumeration_activities: Activități (timp de lucru) + label_greater_or_equal: ">=" + label_less_or_equal: <= + text_wiki_page_destroy_question: Această pagină are %{descendants} pagini anterioare È™i descendenÈ›i. Ce doriÈ›i să faceÈ›i? + text_wiki_page_reassign_children: Atribuie paginile la această pagină + text_wiki_page_nullify_children: MenÈ›ine paginile ca È™i pagini iniÈ›iale (root) + text_wiki_page_destroy_children: Șterge paginile È™i descendenÈ›ii + setting_password_min_length: Lungime minimă parolă + field_group_by: Grupează după + mail_subject_wiki_content_updated: "Pagina wiki '%{id}' a fost actualizată" + label_wiki_content_added: Adăugat + mail_subject_wiki_content_added: "Pagina wiki '%{id}' a fost adăugată" + mail_body_wiki_content_added: Pagina wiki '%{id}' a fost adăugată de %{author}. + label_wiki_content_updated: Actualizat + mail_body_wiki_content_updated: Pagina wiki '%{id}' a fost actualizată de %{author}. + permission_add_project: Crează proiect + setting_new_project_user_role_id: Rol atribuit utilizatorului non-admin care crează un proiect. + label_view_all_revisions: Arată toate reviziile + label_tag: Tag + label_branch: Branch + error_no_tracker_in_project: Nu există un tracker asociat cu proiectul. VerificaÈ›i vă rog setările proiectului. + error_no_default_issue_status: Nu există un status implicit al tichetelor. VerificaÈ›i vă rog configuraÈ›ia (MergeÈ›i la "Administrare -> Stări tichete"). + text_journal_changed: "%{label} schimbat din %{old} în %{new}" + text_journal_set_to: "%{label} setat ca %{value}" + text_journal_deleted: "%{label} È™ters (%{old})" + label_group_plural: Grupuri + label_group: Grup + label_group_new: Grup nou + label_time_entry_plural: Timp alocat + text_journal_added: "%{label} %{value} added" + field_active: Active + enumeration_system_activity: System Activity + permission_delete_issue_watchers: Delete watchers + version_status_closed: closed + version_status_locked: locked + version_status_open: open + error_can_not_reopen_issue_on_closed_version: An issue assigned to a closed version can not be reopened + label_user_anonymous: Anonymous + button_move_and_follow: Move and follow + setting_default_projects_modules: Default enabled modules for new projects + setting_gravatar_default: Default Gravatar image + field_sharing: Sharing + label_version_sharing_hierarchy: With project hierarchy + label_version_sharing_system: With all projects + label_version_sharing_descendants: With subprojects + label_version_sharing_tree: With project tree + label_version_sharing_none: Not shared + error_can_not_archive_project: This project can not be archived + button_duplicate: Duplicate + button_copy_and_follow: Copy and follow + label_copy_source: Source + setting_issue_done_ratio: Calculate the issue done ratio with + setting_issue_done_ratio_issue_status: Use the issue status + error_issue_done_ratios_not_updated: Issue done ratios not updated. + error_workflow_copy_target: Please select target tracker(s) and role(s) + setting_issue_done_ratio_issue_field: Use the issue field + label_copy_same_as_target: Same as target + label_copy_target: Target + notice_issue_done_ratios_updated: Issue done ratios updated. + error_workflow_copy_source: Please select a source tracker or role + label_update_issue_done_ratios: Update issue done ratios + setting_start_of_week: Start calendars on + permission_view_issues: View Issues + label_display_used_statuses_only: Only display statuses that are used by this tracker + label_revision_id: Revision %{value} + label_api_access_key: API access key + label_api_access_key_created_on: API access key created %{value} ago + label_feeds_access_key: RSS access key + notice_api_access_key_reseted: Your API access key was reset. + setting_rest_api_enabled: Enable REST web service + label_missing_api_access_key: Missing an API access key + label_missing_feeds_access_key: Missing a RSS access key + button_show: Show + text_line_separated: Multiple values allowed (one line for each value). + setting_mail_handler_body_delimiters: Truncate emails after one of these lines + permission_add_subprojects: Create subprojects + label_subproject_new: New subproject + text_own_membership_delete_confirmation: |- + You are about to remove some or all of your permissions and may no longer be able to edit this project after that. + Are you sure you want to continue? + label_close_versions: Close completed versions + label_board_sticky: Sticky + label_board_locked: Locked + permission_export_wiki_pages: Export wiki pages + setting_cache_formatted_text: Cache formatted text + permission_manage_project_activities: Manage project activities + error_unable_delete_issue_status: Unable to delete issue status + label_profile: Profile + permission_manage_subtasks: Manage subtasks + field_parent_issue: Parent task + label_subtask_plural: Subtasks + label_project_copy_notifications: Send email notifications during the project copy + error_can_not_delete_custom_field: Unable to delete custom field + error_unable_to_connect: Unable to connect (%{value}) + error_can_not_remove_role: This role is in use and can not be deleted. + error_can_not_delete_tracker: This tracker contains issues and can't be deleted. + field_principal: Principal + label_my_page_block: My page block + notice_failed_to_save_members: "Failed to save member(s): %{errors}." + text_zoom_out: Zoom out + text_zoom_in: Zoom in + notice_unable_delete_time_entry: Unable to delete time log entry. + label_overall_spent_time: Overall spent time + field_time_entries: Log time + project_module_gantt: Gantt + project_module_calendar: Calendar + button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" + field_text: Text field + label_user_mail_option_only_owner: Only for things I am the owner of + setting_default_notification_option: Default notification option + label_user_mail_option_only_my_events: Only for things I watch or I'm involved in + label_user_mail_option_only_assigned: Only for things I am assigned to + label_user_mail_option_none: No events + field_member_of_group: Assignee's group + field_assigned_to_role: Assignee's role + notice_not_authorized_archived_project: The project you're trying to access has been archived. + label_principal_search: "Search for user or group:" + label_user_search: "Search for user:" + field_visible: Visible + setting_emails_header: Emails header + setting_commit_logtime_activity_id: Activity for logged time + text_time_logged_by_changeset: Applied in changeset %{value}. + setting_commit_logtime_enabled: Enable time logging + notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) + setting_gantt_items_limit: Maximum number of items displayed on the gantt chart + field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text + text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + label_my_queries: My custom queries + text_journal_changed_no_detail: "%{label} updated" + label_news_comment_added: Comment added to a news + button_expand_all: Expand all + button_collapse_all: Collapse all + label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee + label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author + label_bulk_edit_selected_time_entries: Bulk edit selected time entries + text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? + label_role_anonymous: Anonymous + label_role_non_member: Non member + label_issue_note_added: Note added + label_issue_status_updated: Status updated + label_issue_priority_updated: Priority updated + label_issues_visibility_own: Issues created by or assigned to the user + field_issues_visibility: Issues visibility + label_issues_visibility_all: All issues + permission_set_own_issues_private: Set own issues public or private + field_is_private: Private + permission_set_issues_private: Set issues public or private + label_issues_visibility_public: All non private issues + text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). + field_commit_logs_encoding: Codare pentru mesaje + field_scm_path_encoding: Path encoding + text_scm_path_encoding_note: "Default: UTF-8" + field_path_to_repository: Path to repository + field_root_directory: Root directory + field_cvs_module: Module + field_cvsroot: CVSROOT + text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) + text_scm_command: Command + text_scm_command_version: Version + label_git_report_last_commit: Report last commit for files and directories + text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. + text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. + notice_issue_successful_create: Issue %{id} created. + label_between: between + setting_issue_group_assignment: Allow issue assignment to groups + label_diff: diff + text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Sort direction + description_project_scope: Search scope + description_filter: Filter + description_user_mail_notification: Mail notification settings + description_date_from: Enter start date + description_message_content: Message content + description_available_columns: Available Columns + description_date_range_interval: Choose range by selecting start and end date + description_issue_category_reassign: Choose issue category + description_search: Searchfield + description_notes: Notes + description_date_range_list: Choose range from list + description_choose_project: Projects + description_date_to: Enter end date + description_query_sort_criteria_attribute: Sort attribute + description_wiki_subpages_reassign: Choose new parent page + description_selected_columns: Selected Columns + label_parent_revision: Parent + label_child_revision: Child + error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues + button_edit_section: Edit this section + setting_repositories_encodings: Attachments and repositories encodings + description_all_columns: All Columns + button_export: Export + label_export_options: "%{export_format} export options" + error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) + notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_x_issues: + zero: 0 tichet + one: 1 tichet + other: "%{count} tichete" + label_repository_new: New repository + field_repository_is_default: Main repository + label_copy_attachments: Copy attachments + label_item_position: "%{position}/%{count}" + label_completed_versions: Completed versions + text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_multiple: Multiple values + setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed + text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes + text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) + notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. + text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} + permission_manage_related_issues: Manage related issues + field_auth_source_ldap_filter: LDAP filter + label_search_for_watchers: Search for watchers to add + notice_account_deleted: Your account has been permanently deleted. + setting_unsubscribe: Allow users to delete their own account + button_delete_my_account: Delete my account + text_account_destroy_confirmation: |- + Are you sure you want to proceed? + Your account will be permanently deleted, with no way to reactivate it. + error_session_expired: Your session has expired. Please login again. + text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + setting_session_lifetime: Session maximum lifetime + setting_session_timeout: Session inactivity timeout + label_session_expiration: Session expiration + permission_close_project: Close / reopen the project + label_show_closed_projects: View closed projects + button_close: Close + button_reopen: Reopen + project_status_active: active + project_status_closed: closed + project_status_archived: archived + text_project_closed: This project is closed and read-only. + notice_user_successful_create: User %{id} created. + field_core_fields: Standard fields + field_timeout: Timeout (in seconds) + setting_thumbnails_enabled: Display attachment thumbnails + setting_thumbnails_size: Thumbnails size (in pixels) + label_status_transitions: Status transitions + label_fields_permissions: Fields permissions + label_readonly: Read-only + label_required: Required + text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.
    Once saved, the identifier cannot be changed. + field_board_parent: Parent forum + label_attribute_of_project: Project's %{name} + label_attribute_of_author: Author's %{name} + label_attribute_of_assigned_to: Assignee's %{name} + label_attribute_of_fixed_version: Target version's %{name} + label_copy_subtasks: Copy subtasks + label_copied_to: copied to + label_copied_from: copied from + label_any_issues_in_project: any issues in project + label_any_issues_not_in_project: any issues not in project + field_private_notes: Private notes + permission_view_private_notes: View private notes + permission_set_notes_private: Set notes as private + label_no_issues_in_project: no issues in project + label_any: toate + label_last_n_weeks: last %{count} weeks + setting_cross_project_subtasks: Allow cross-project subtasks + label_cross_project_descendants: With subprojects + label_cross_project_tree: With project tree + label_cross_project_hierarchy: With project hierarchy + label_cross_project_system: With all projects + button_hide: Hide + setting_non_working_week_days: Non-working days + label_in_the_next_days: in the next + label_in_the_past_days: in the past diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fc/fcda58bf1c184621fc268b0519ceb9aea66d8455.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fc/fcda58bf1c184621fc268b0519ceb9aea66d8455.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,12 @@ +issue_relation_001: + id: 1 + issue_from_id: 10 + issue_to_id: 9 + relation_type: blocks + delay: +issue_relation_002: + id: 2 + issue_from_id: 2 + issue_to_id: 3 + relation_type: relates + delay: diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fd/fd3ede0a5e12e3d96f4d011a2d4e4835e31d6383.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fd/fd3ede0a5e12e3d96f4d011a2d4e4835e31d6383.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,67 @@ +require File.expand_path('../../test_helper', __FILE__) + +class AutoCompletesControllerTest < ActionController::TestCase + fixtures :projects, :issues, :issue_statuses, + :enumerations, :users, :issue_categories, + :trackers, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :workflows, + :journals, :journal_details + + def test_issues_should_not_be_case_sensitive + get :issues, :project_id => 'ecookbook', :q => 'ReCiPe' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).detect {|issue| issue.subject.match /recipe/} + end + + def test_issues_should_accept_term_param + get :issues, :project_id => 'ecookbook', :term => 'ReCiPe' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).detect {|issue| issue.subject.match /recipe/} + end + + def test_issues_should_return_issue_with_given_id + get :issues, :project_id => 'subproject1', :q => '13' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + + def test_auto_complete_with_scope_all_should_search_other_projects + get :issues, :project_id => 'ecookbook', :q => '13', :scope => 'all' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + + def test_auto_complete_without_project_should_search_all_projects + get :issues, :q => '13' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + + def test_auto_complete_without_scope_all_should_not_search_other_projects + get :issues, :project_id => 'ecookbook', :q => '13' + assert_response :success + assert_equal [], assigns(:issues) + end + + def test_issues_should_return_json + get :issues, :project_id => 'subproject1', :q => '13' + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Array, json + issue = json.first + assert_kind_of Hash, issue + assert_equal 13, issue['id'] + assert_equal 13, issue['value'] + assert_equal 'Bug #13: Subproject issue two', issue['label'] + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fd/fd4c8c5c34ecdf4437056903541cdc2e7af5c1a9.svn-base --- a/.svn/pristine/fd/fd4c8c5c34ecdf4437056903541cdc2e7af5c1a9.svn-base Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,38 +0,0 @@ -# encoding: utf-8 -# -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -module ReportsHelper - - def aggregate(data, criteria) - a = 0 - data.each { |row| - match = 1 - criteria.each { |k, v| - match = 0 unless (row[k].to_s == v.to_s) || (k == 'closed' && row[k] == (v == 0 ? "f" : "t")) - } unless criteria.nil? - a = a + row["total"].to_i if match == 1 - } unless data.nil? - a - end - - def aggregate_link(data, criteria, *args) - a = aggregate data, criteria - a > 0 ? link_to(h(a), *args) : '-' - end -end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fd/fd6fcf7bddb6935d831becc708784f6e4a57d861.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fd/fd6fcf7bddb6935d831becc708784f6e4a57d861.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,23 @@ +/* Vietnamese initialisation for the jQuery UI date picker plugin. */ +/* Translated by Le Thanh Huy (lthanhhuy@cit.ctu.edu.vn). */ +jQuery(function($){ + $.datepicker.regional['vi'] = { + closeText: 'Äóng', + prevText: '<Trước', + nextText: 'Tiếp>', + currentText: 'Hôm nay', + monthNames: ['Tháng Má»™t', 'Tháng Hai', 'Tháng Ba', 'Tháng Tư', 'Tháng Năm', 'Tháng Sáu', + 'Tháng Bảy', 'Tháng Tám', 'Tháng Chín', 'Tháng Mưá»i', 'Tháng Mưá»i Má»™t', 'Tháng Mưá»i Hai'], + monthNamesShort: ['Tháng 1', 'Tháng 2', 'Tháng 3', 'Tháng 4', 'Tháng 5', 'Tháng 6', + 'Tháng 7', 'Tháng 8', 'Tháng 9', 'Tháng 10', 'Tháng 11', 'Tháng 12'], + dayNames: ['Chá»§ Nhật', 'Thứ Hai', 'Thứ Ba', 'Thứ Tư', 'Thứ Năm', 'Thứ Sáu', 'Thứ Bảy'], + dayNamesShort: ['CN', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7'], + dayNamesMin: ['CN', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7'], + weekHeader: 'Tu', + dateFormat: 'dd/mm/yy', + firstDay: 0, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['vi']); +}); diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fd/fdef3e8ceeb36b14bf914b909bebd1919154acce.svn-base Binary file .svn/pristine/fd/fdef3e8ceeb36b14bf914b909bebd1919154acce.svn-base has changed diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fe/fe0e2a7971f5615a462d500c0c1966b4ca458e24.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fe/fe0e2a7971f5615a462d500c0c1966b4ca458e24.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,66 @@ +<%= board_breadcrumb(@board) %> + +
    +<%= link_to_if_authorized l(:label_message_new), + {:controller => 'messages', :action => 'new', :board_id => @board}, + :class => 'icon icon-add', + :onclick => 'showAndScrollTo("add-message", "message_subject"); return false;' %> +<%= watcher_tag(@board, User.current) %> +
    + + + +

    <%=h @board.name %>

    +

    <%=h @board.description %>

    + +<% if @topics.any? %> + + + + + <%= sort_header_tag('created_on', :caption => l(:field_created_on)) %> + <%= sort_header_tag('replies', :caption => l(:label_reply_plural)) %> + <%= sort_header_tag('updated_on', :caption => l(:label_message_last)) %> + + + <% @topics.each do |topic| %> + + + + + + + + <% end %> + +
    <%= l(:field_subject) %><%= l(:field_author) %>
    <%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => topic } %><%= link_to_user(topic.author) %><%= format_time(topic.created_on) %><%= topic.replies_count %> + <% if topic.last_reply %> + <%= authoring topic.last_reply.created_on, topic.last_reply.author %>
    + <%= link_to_message topic.last_reply %> + <% end %> +
    +

    <%= pagination_links_full @topic_pages, @topic_count %>

    +<% else %> +

    <%= l(:label_no_data) %>

    +<% end %> + +<% other_formats_links do |f| %> + <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %> +<% end %> + +<% html_title @board.name %> + +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@project}: #{@board}") %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fe/febf10dabf84b8277c708939ec95b486b54f1899.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fe/febf10dabf84b8277c708939ec95b486b54f1899.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,3 @@ +one: + id: 1 + name: One \ No newline at end of file diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fe/fec7c0dc6aee39e1a0e0a5c3722658dec570795c.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fe/fec7c0dc6aee39e1a0e0a5c3722658dec570795c.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,23 @@ +/* Norwegian initialisation for the jQuery UI date picker plugin. */ +/* Written by Naimdjon Takhirov (naimdjon@gmail.com). */ + +jQuery(function($){ + $.datepicker.regional['no'] = { + closeText: 'Lukk', + prevText: '«Forrige', + nextText: 'Neste»', + currentText: 'I dag', + monthNames: ['januar','februar','mars','april','mai','juni','juli','august','september','oktober','november','desember'], + monthNamesShort: ['jan','feb','mar','apr','mai','jun','jul','aug','sep','okt','nov','des'], + dayNamesShort: ['søn','man','tir','ons','tor','fre','lør'], + dayNames: ['søndag','mandag','tirsdag','onsdag','torsdag','fredag','lørdag'], + dayNamesMin: ['sø','ma','ti','on','to','fr','lø'], + weekHeader: 'Uke', + dateFormat: 'dd.mm.yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: '' + }; + $.datepicker.setDefaults($.datepicker.regional['no']); +}); diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/fe/fefc79e88b16b070b422bb7f512dd505ebd8c604.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/fe/fefc79e88b16b070b422bb7f512dd505ebd8c604.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,116 @@ +--- +wiki_content_versions_001: + updated_on: 2007-03-07 00:08:07 +01:00 + page_id: 1 + id: 1 + version: 1 + author_id: 2 + comments: Page creation + wiki_content_id: 1 + compression: "" + data: |- + h1. CookBook documentation + + + + Some [[documentation]] here... +wiki_content_versions_002: + updated_on: 2007-03-07 00:08:34 +01:00 + page_id: 1 + id: 2 + version: 2 + author_id: 1 + comments: Small update + wiki_content_id: 1 + compression: "" + data: |- + h1. CookBook documentation + + + + Some updated [[documentation]] here... +wiki_content_versions_003: + updated_on: 2007-03-07 00:10:51 +01:00 + page_id: 1 + id: 3 + version: 3 + author_id: 1 + comments: "" + wiki_content_id: 1 + compression: "" + data: |- + h1. CookBook documentation + Some updated [[documentation]] here... +wiki_content_versions_004: + data: |- + h1. Another page + + This is a link to a ticket: #2 + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 2 + wiki_content_id: 2 + id: 4 + version: 1 + author_id: 1 + comments: +wiki_content_versions_005: + data: |- + h1. Title + + Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero. + + h2. Heading 1 + + @WHATEVER@ + + Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in. + + Cras ipsum felis, ultrices at porttitor vel, faucibus eu nunc. + + h2. Heading 2 + + Morbi facilisis accumsan orci non pharetra. + updated_on: 2007-03-08 00:16:07 +01:00 + page_id: 11 + wiki_content_id: 11 + id: 5 + version: 2 + author_id: 1 + comments: +wiki_content_versions_006: + data: |- + h1. Title + + Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero. + + h2. Heading 1 + + @WHATEVER@ + + Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in. + + h2. Heading 2 + + Morbi facilisis accumsan orci non pharetra. + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 11 + wiki_content_id: 11 + id: 6 + version: 3 + author_id: 1 + comments: +wiki_content_versions_007: + data: |- + h1. Page with an inline image + + This is an inline image: + + !logo.gif! + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 4 + wiki_content_id: 4 + id: 7 + version: 1 + author_id: 1 + comments: + diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/ff/ff4ade207165498e6822191c7d47b12871a75858.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/ff/ff4ade207165498e6822191c7d47b12871a75858.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,157 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../test_helper', __FILE__) + +class JournalTest < ActiveSupport::TestCase + fixtures :projects, :issues, :issue_statuses, :journals, :journal_details, + :users, :members, :member_roles, :roles, :enabled_modules, + :projects_trackers, :trackers + + def setup + @journal = Journal.find 1 + end + + def test_journalized_is_an_issue + issue = @journal.issue + assert_kind_of Issue, issue + assert_equal 1, issue.id + end + + def test_new_status + status = @journal.new_status + assert_not_nil status + assert_kind_of IssueStatus, status + assert_equal 2, status.id + end + + def test_create_should_send_email_notification + ActionMailer::Base.deliveries.clear + issue = Issue.find(:first) + user = User.find(:first) + journal = issue.init_journal(user, issue) + + assert journal.save + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_should_not_save_journal_with_blank_notes_and_no_details + journal = Journal.new(:journalized => Issue.first, :user => User.first) + + assert_no_difference 'Journal.count' do + assert_equal false, journal.save + end + end + + def test_create_should_not_split_non_private_notes + assert_difference 'Journal.count' do + assert_no_difference 'JournalDetail.count' do + journal = Journal.generate!(:notes => 'Notes') + end + end + + assert_difference 'Journal.count' do + assert_difference 'JournalDetail.count' do + journal = Journal.generate!(:notes => 'Notes', :details => [JournalDetail.new]) + end + end + + assert_difference 'Journal.count' do + assert_difference 'JournalDetail.count' do + journal = Journal.generate!(:notes => '', :details => [JournalDetail.new]) + end + end + end + + def test_create_should_split_private_notes + assert_difference 'Journal.count' do + assert_no_difference 'JournalDetail.count' do + journal = Journal.generate!(:notes => 'Notes', :private_notes => true) + journal.reload + assert_equal true, journal.private_notes + assert_equal 'Notes', journal.notes + end + end + + assert_difference 'Journal.count', 2 do + assert_difference 'JournalDetail.count' do + journal = Journal.generate!(:notes => 'Notes', :private_notes => true, :details => [JournalDetail.new]) + journal.reload + assert_equal true, journal.private_notes + assert_equal 'Notes', journal.notes + assert_equal 0, journal.details.size + + journal_with_changes = Journal.order('id DESC').offset(1).first + assert_equal false, journal_with_changes.private_notes + assert_nil journal_with_changes.notes + assert_equal 1, journal_with_changes.details.size + assert_equal journal.created_on, journal_with_changes.created_on + end + end + + assert_difference 'Journal.count' do + assert_difference 'JournalDetail.count' do + journal = Journal.generate!(:notes => '', :private_notes => true, :details => [JournalDetail.new]) + journal.reload + assert_equal false, journal.private_notes + assert_equal '', journal.notes + assert_equal 1, journal.details.size + end + end + end + + def test_visible_scope_for_anonymous + # Anonymous user should see issues of public projects only + journals = Journal.visible(User.anonymous).all + assert journals.any? + assert_nil journals.detect {|journal| !journal.issue.project.is_public?} + # Anonymous user should not see issues without permission + Role.anonymous.remove_permission!(:view_issues) + journals = Journal.visible(User.anonymous).all + assert journals.empty? + end + + def test_visible_scope_for_user + user = User.find(9) + assert user.projects.empty? + # Non member user should see issues of public projects only + journals = Journal.visible(user).all + assert journals.any? + assert_nil journals.detect {|journal| !journal.issue.project.is_public?} + # Non member user should not see issues without permission + Role.non_member.remove_permission!(:view_issues) + user.reload + journals = Journal.visible(user).all + assert journals.empty? + # User should see issues of projects for which he has view_issues permissions only + Member.create!(:principal => user, :project_id => 1, :role_ids => [1]) + user.reload + journals = Journal.visible(user).all + assert journals.any? + assert_nil journals.detect {|journal| journal.issue.project_id != 1} + end + + def test_visible_scope_for_admin + user = User.find(1) + user.members.each(&:destroy) + assert user.projects.empty? + journals = Journal.visible(user).all + assert journals.any? + # Admin should see issues on private projects that he does not belong to + assert journals.detect {|journal| !journal.issue.project.is_public?} + end +end diff -r ab89f95ef405 -r b2ea0641f798 .svn/pristine/ff/ffe18b9b736e029b1f321d0b23049d5f30475171.svn-base --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.svn/pristine/ff/ffe18b9b736e029b1f321d0b23049d5f30475171.svn-base Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,23 @@ +/* Inicialización en español para la extensión 'UI date picker' para jQuery. */ +/* Traducido por Vester (xvester@gmail.com). */ +jQuery(function($){ + $.datepicker.regional['es'] = { + closeText: 'Cerrar', + prevText: '<Ant', + nextText: 'Sig>', + currentText: 'Hoy', + monthNames: ['Enero','Febrero','Marzo','Abril','Mayo','Junio', + 'Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'], + monthNamesShort: ['Ene','Feb','Mar','Abr','May','Jun', + 'Jul','Ago','Sep','Oct','Nov','Dic'], + dayNames: ['Domingo','Lunes','Martes','Miércoles','Jueves','Viernes','Sábado'], + dayNamesShort: ['Dom','Lun','Mar','Mié','Juv','Vie','Sáb'], + dayNamesMin: ['Do','Lu','Ma','Mi','Ju','Vi','Sá'], + weekHeader: 'Sm', + dateFormat: 'dd/mm/yy', + firstDay: 1, + isRTL: false, + showMonthAfterYear: false, + yearSuffix: ''}; + $.datepicker.setDefaults($.datepicker.regional['es']); +}); \ No newline at end of file diff -r ab89f95ef405 -r b2ea0641f798 .svn/wc.db Binary file .svn/wc.db has changed diff -r ab89f95ef405 -r b2ea0641f798 Gemfile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Gemfile Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,95 @@ +source 'http://rubygems.org' + +gem "rails", "3.2.13" +gem "jquery-rails", "~> 2.0.2" +gem "i18n", "~> 0.6.0" +gem "coderay", "~> 1.0.6" +gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby] +gem "builder", "3.0.0" + +# Optional gem for LDAP authentication +group :ldap do + gem "net-ldap", "~> 0.3.1" +end + +# Optional gem for OpenID authentication +group :openid do + gem "ruby-openid", "~> 2.1.4", :require => "openid" + gem "rack-openid" +end + +# Optional gem for exporting the gantt to a PNG file, not supported with jruby +platforms :mri, :mingw do + group :rmagick do + # RMagick 2 supports ruby 1.9 + # RMagick 1 would be fine for ruby 1.8 but Bundler does not support + # different requirements for the same gem on different platforms + gem "rmagick", ">= 2.0.0" + end +end + +# Database gems +platforms :mri, :mingw do + group :postgresql do + gem "pg", ">= 0.11.0" + end + + group :sqlite do + gem "sqlite3" + end +end + +platforms :mri_18, :mingw_18 do + group :mysql do + gem "mysql", "~> 2.8.1" + end +end + +platforms :mri_19, :mingw_19 do + group :mysql do + gem "mysql2", "~> 0.3.11" + end +end + +platforms :jruby do + gem "jruby-openssl" + + group :mysql do + gem "activerecord-jdbcmysql-adapter" + end + + group :postgresql do + gem "activerecord-jdbcpostgresql-adapter" + end + + group :sqlite do + gem "activerecord-jdbcsqlite3-adapter" + end +end + +group :development do + gem "rdoc", ">= 2.4.2" + gem "yard" +end + +group :test do + gem "shoulda", "~> 2.11" + # Shoulda does not work nice on Ruby 1.9.3 and JRuby 1.7. + # It seems to need test-unit explicitely. + platforms = [:mri_19] + platforms << :jruby if defined?(JRUBY_VERSION) && JRUBY_VERSION >= "1.7" + gem "test-unit", :platforms => platforms + gem "mocha", "~> 0.13.3" +end + +local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local") +if File.exists?(local_gemfile) + puts "Loading Gemfile.local ..." if $DEBUG # `ruby -d` or `bundle -v` + instance_eval File.read(local_gemfile) +end + +# Load plugins' Gemfiles +Dir.glob File.expand_path("../plugins/*/Gemfile", __FILE__) do |file| + puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v` + instance_eval File.read(file) +end diff -r ab89f95ef405 -r b2ea0641f798 Rakefile --- a/Rakefile Thu Jun 20 08:46:39 2013 +0100 +++ b/Rakefile Thu Jun 20 08:47:50 2013 +0100 @@ -1,15 +1,7 @@ +#!/usr/bin/env rake # Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/switchtower.rake, and they will automatically be available to Rake. +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require(File.join(File.dirname(__FILE__), 'config', 'boot')) +require File.expand_path('../config/application', __FILE__) -require 'rake' -require 'rake/testtask' - -begin - require 'rdoc/task' -rescue LoadError - # RDoc is not available -end - -require 'tasks/rails' +RedmineApp::Application.load_tasks diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/account_controller.rb --- a/app/controllers/account_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/account_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -29,6 +29,9 @@ else authenticate_user end + rescue AuthSourceException => e + logger.error "An error occured when authenticating #{params[:username]}: #{e.message}" + render_error :message => e.message end # Log out current user and redirect to welcome page @@ -37,19 +40,26 @@ redirect_to home_url end - # Enable user to choose a new password + # Lets user choose a new password def lost_password redirect_to(home_url) && return unless Setting.lost_password? if params[:token] - @token = Token.find_by_action_and_value("recovery", params[:token]) - redirect_to(home_url) && return unless @token and !@token.expired? + @token = Token.find_by_action_and_value("recovery", params[:token].to_s) + if @token.nil? || @token.expired? + redirect_to home_url + return + end @user = @token.user + unless @user && @user.active? + redirect_to home_url + return + end if request.post? @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation] if @user.save @token.destroy flash[:notice] = l(:notice_account_password_updated) - redirect_to :action => 'login' + redirect_to signin_path return end end @@ -57,17 +67,23 @@ return else if request.post? - user = User.find_by_mail(params[:mail]) - # user not found in db - (flash.now[:error] = l(:notice_account_unknown_email); return) unless user - # user uses an external authentification - (flash.now[:error] = l(:notice_can_t_change_password); return) if user.auth_source_id + user = User.find_by_mail(params[:mail].to_s) + # user not found or not active + unless user && user.active? + flash.now[:error] = l(:notice_account_unknown_email) + return + end + # user cannot change its password + unless user.change_password_allowed? + flash.now[:error] = l(:notice_can_t_change_password) + return + end # create a new token for password recovery token = Token.new(:user => user, :action => "recovery") if token.save - Mailer.deliver_lost_password(token) + Mailer.lost_password(token).deliver flash[:notice] = l(:notice_account_lost_email_sent) - redirect_to :action => 'login' + redirect_to signin_path return end end @@ -85,7 +101,9 @@ @ssamr_user_details = SsamrUserDetail.new else - @user = User.new(params[:user]) + user_params = params[:user] || {} + @user = User.new + @user.safe_attributes = user_params @user.admin = false @user.register @@ -102,7 +120,9 @@ end else @user.login = params[:user][:login] - @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] + unless user_params[:identity_url].present? && user_params[:password].blank? && user_params[:password_confirmation].blank? + @user.password, @user.password_confirmation = user_params[:password], user_params[:password_confirmation] + end @ssamr_user_details = SsamrUserDetail.new(params[:ssamr_user_details]) @@ -135,19 +155,11 @@ token.destroy flash[:notice] = l(:notice_account_activated) end - redirect_to :action => 'login' + redirect_to signin_path end private - def logout_user - if User.current.logged? - cookies.delete :autologin - Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) - self.logged_user = nil - end - end - def authenticate_user if Setting.openid? && using_open_id? open_id_authenticate(params[:openid_url]) @@ -170,7 +182,7 @@ end def open_id_authenticate(openid_url) - authenticate_with_open_id(openid_url, :required => [:nickname, :fullname, :email], :return_to => signin_url) do |result, identity_url, registration| + authenticate_with_open_id(openid_url, :required => [:nickname, :fullname, :email], :return_to => signin_url, :method => :post) do |result, identity_url, registration| if result.successful? user = User.find_or_initialize_by_identity_url(identity_url) if user.new_record? @@ -211,6 +223,7 @@ end def successful_authentication(user) + logger.info "Successful authentication for '#{user.login}' from #{request.remote_ip} at #{Time.now.utc}" # Valid user self.logged_user = user # generate a key and set cookie if autologin @@ -252,9 +265,9 @@ def register_by_email_activation(user, &block) token = Token.new(:user => user, :action => "register") if user.save and token.save - Mailer.deliver_register(token) + Mailer.register(token).deliver flash[:notice] = l(:notice_account_register_done) - redirect_to :action => 'login' + redirect_to signin_path else yield if block_given? end @@ -285,7 +298,7 @@ @ssamr_user_details.save! # Sends an email to the administrators - Mailer.deliver_account_activation_request(user) + Mailer.account_activation_request(user).deliver account_pending else yield if block_given? @@ -294,6 +307,6 @@ def account_pending flash[:notice] = l(:notice_account_pending) - redirect_to :action => 'login' + redirect_to signin_path end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/activities_controller.rb --- a/app/controllers/activities_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/activities_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -55,10 +55,10 @@ end end - if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, @institution_name, events.first, User.current, current_language]) + if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, @institution_name, events.first, events.size, User.current, current_language]) respond_to do |format| format.html { - @events_by_day = events.group_by(&:event_date) + @events_by_day = events.group_by {|event| User.current.time_to_date(event.event_datetime)} render :layout => false if request.xhr? } format.atom { diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/admin_controller.rb --- a/app/controllers/admin_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/admin_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,6 +17,10 @@ class AdminController < ApplicationController layout 'admin' + menu_item :projects, :only => :projects + menu_item :plugins, :only => :plugins + menu_item :info, :only => :info + before_filter :require_admin helper :sort include SortHelper @@ -26,14 +30,12 @@ end def projects - @status = params[:status] ? params[:status].to_i : 1 - c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status]) - unless params[:name].blank? - name = "%#{params[:name].strip.downcase}%" - c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name] - end - @projects = Project.find :all, :order => 'lft', - :conditions => c.conditions + @status = params[:status] || 1 + + scope = Project.status(@status) + scope = scope.like(params[:name]) if params[:name].present? + + @projects = scope.all(:order => 'lft') render :action => "projects", :layout => false if request.xhr? end @@ -61,7 +63,7 @@ # Force ActionMailer to raise delivery errors so we can catch it ActionMailer::Base.raise_delivery_errors = true begin - @test = Mailer.deliver_test(User.current) + @test = Mailer.test_email(User.current).deliver flash[:notice] = l(:notice_email_sent, User.current.mail) rescue Exception => e flash[:error] = l(:notice_email_error, e.message) @@ -75,7 +77,7 @@ @checklist = [ [:text_default_administrator_account_changed, User.default_admin_account_changed?], [:text_file_repository_writable, File.writable?(Attachment.storage_path)], - [:text_plugin_assets_writable, File.writable?(Engines.public_directory)], + [:text_plugin_assets_writable, File.writable?(Redmine::Plugin.public_directory)], [:text_rmagick_available, Object.const_defined?(:Magick)] ] end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/application_controller.rb --- a/app/controllers/application_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/application_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -23,38 +23,58 @@ class ApplicationController < ActionController::Base include Redmine::I18n + class_attribute :accept_api_auth_actions + class_attribute :accept_rss_auth_actions + class_attribute :model_object + layout 'base' - exempt_from_layout 'builder', 'rsb' protect_from_forgery def handle_unverified_request super cookies.delete(:autologin) end - # Remove broken cookie after upgrade from 0.8.x (#4292) - # See https://rails.lighthouseapp.com/projects/8994/tickets/3360 - # TODO: remove it when Rails is fixed - before_filter :delete_broken_cookies - def delete_broken_cookies - if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/ - cookies.delete '_redmine_session' - redirect_to home_path - return false - end - end - before_filter :user_setup, :check_if_login_required, :set_localization - filter_parameter_logging :password + before_filter :session_expiration, :user_setup, :check_if_login_required, :set_localization rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token rescue_from ::Unauthorized, :with => :deny_access + rescue_from ::ActionView::MissingTemplate, :with => :missing_template include Redmine::Search::Controller include Redmine::MenuManager::MenuController helper Redmine::MenuManager::MenuHelper - Redmine::Scm::Base.all.each do |scm| - require_dependency "repository/#{scm.underscore}" + def session_expiration + if session[:user_id] + if session_expired? && !try_to_autologin + reset_session + flash[:error] = l(:error_session_expired) + redirect_to signin_url + else + session[:atime] = Time.now.utc.to_i + end + end + end + + def session_expired? + if Setting.session_lifetime? + unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60) + return true + end + end + if Setting.session_timeout? + unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60) + return true + end + end + false + end + + def start_user_session(user) + session[:user_id] = user.id + session[:ctime] = Time.now.utc.to_i + session[:atime] = Time.now.utc.to_i end def user_setup @@ -62,32 +82,57 @@ Setting.check_cache # Find the current user User.current = find_current_user + logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger end # Returns the current user or nil if no user is logged in # and starts a session if needed def find_current_user - if session[:user_id] - # existing session - (User.active.find(session[:user_id]) rescue nil) - elsif cookies[:autologin] && Setting.autologin? - # auto-login feature starts a new session - user = User.try_to_autologin(cookies[:autologin]) - session[:user_id] = user.id if user - user - elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth? - # RSS key authentication does not start a session - User.find_by_rss_key(params[:key]) - elsif Setting.rest_api_enabled? && accept_api_auth? + user = nil + unless api_request? + if session[:user_id] + # existing session + user = (User.active.find(session[:user_id]) rescue nil) + elsif autologin_user = try_to_autologin + user = autologin_user + elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth? + # RSS key authentication does not start a session + user = User.find_by_rss_key(params[:key]) + end + end + if user.nil? && Setting.rest_api_enabled? && accept_api_auth? if (key = api_key_from_request) # Use API key - User.find_by_api_key(key) + user = User.find_by_api_key(key) else # HTTP Basic, either username/password or API key/random authenticate_with_http_basic do |username, password| - User.try_to_login(username, password) || User.find_by_api_key(username) + user = User.try_to_login(username, password) || User.find_by_api_key(username) end end + # Switch user if requested by an admin user + if user && user.admin? && (username = api_switch_user_from_request) + su = User.find_by_login(username) + if su && su.active? + logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger + user = su + else + render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412 + end + end + end + user + end + + def try_to_autologin + if cookies[:autologin] && Setting.autologin? + # auto-login feature starts a new session + user = User.try_to_autologin(cookies[:autologin]) + if user + reset_session + start_user_session(user) + end + user end end @@ -96,12 +141,21 @@ reset_session if user && user.is_a?(User) User.current = user - session[:user_id] = user.id + start_user_session(user) else User.current = User.anonymous end end + # Logs out current user + def logout_user + if User.current.logged? + cookies.delete :autologin + Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) + self.logged_user = nil + end + 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 @@ -209,7 +263,7 @@ end def find_model_object - model = self.class.read_inheritable_attribute('model_object') + model = self.class.model_object if model @object = model.find(params[:id]) self.instance_variable_set('@' + controller_name.singularize, @object) if @object @@ -219,37 +273,38 @@ end def self.model_object(model) - write_inheritable_attribute('model_object', model) + self.model_object = model end - # Filter for bulk issue operations + # Find the issue whose id is the :id parameter + # Raises a Unauthorized exception if the issue is not visible + def find_issue + # Issue.visible.find(...) can not be used to redirect user to the login form + # if the issue actually exists but requires authentication + @issue = Issue.find(params[:id]) + raise Unauthorized unless @issue.visible? + @project = @issue.project + rescue ActiveRecord::RecordNotFound + render_404 + end + + # Find issues with a single :id param or :ids array param + # Raises a Unauthorized exception if one of the issues is not visible def find_issues @issues = Issue.find_all_by_id(params[:id] || params[:ids]) raise ActiveRecord::RecordNotFound if @issues.empty? - if @issues.detect {|issue| !issue.visible?} - deny_access - return - end + raise Unauthorized unless @issues.all?(&:visible?) @projects = @issues.collect(&:project).compact.uniq @project = @projects.first if @projects.size == 1 rescue ActiveRecord::RecordNotFound render_404 end - # Check if project is unique before bulk operations - def check_project_uniqueness - unless @project - # TODO: let users bulk edit/move/destroy issues from different projects - render_error 'Can not bulk edit/move/destroy issues from different projects' - return false - end - end - # make sure that the user is a member of the project (or admin) if project is private # used as a before_filter for actions that do not require any particular permission on the project def check_project_privacy - if @project && @project.active? - if @project.is_public? || User.current.member_of?(@project) || User.current.admin? + if @project && !@project.archived? + if @project.visible? true else deny_access @@ -262,12 +317,16 @@ end def back_url - params[:back_url] || request.env['HTTP_REFERER'] + url = params[:back_url] + if url.nil? && referer = request.env['HTTP_REFERER'] + url = CGI.unescape(referer.to_s) + end + url end def redirect_back_or_default(default) - back_url = CGI.unescape(params[:back_url].to_s) - if !back_url.blank? + back_url = params[:back_url].to_s + if back_url.present? begin uri = URI.parse(back_url) # do not redirect user to another host or to the login or register page @@ -291,6 +350,7 @@ return end rescue URI::InvalidURIError + logger.warn("Could not redirect to invalid URL #{back_url}") # redirect to default end end @@ -298,6 +358,19 @@ false end + # Redirects to the request referer if present, redirects to args or call block otherwise. + def redirect_to_referer_or(*args, &block) + redirect_to :back + rescue ::ActionController::RedirectBackError + if args.any? + redirect_to *args + elsif block_given? + block.call + else + raise "#redirect_to_referer_or takes arguments or a block" + end + end + def render_403(options={}) @project = nil render_error({:message => :notice_not_authorized, :status => 403}.merge(options)) @@ -321,13 +394,17 @@ format.html { render :template => 'common/error', :layout => use_layout, :status => @status } - format.atom { head @status } - format.xml { head @status } - format.js { head @status } - format.json { head @status } + format.any { head @status } end end - + + # Handler for ActionView::MissingTemplate exception + def missing_template + logger.warn "Missing template, responding with 404" + @project = nil + render_404 + end + # Filter for actions that provide an API response # but have no HTML representation for non admin users def require_admin_or_api_request @@ -360,27 +437,15 @@ @items.sort! {|x,y| y.event_datetime <=> x.event_datetime } @items = @items.slice(0, Setting.feeds_limit.to_i) @title = options[:title] || Setting.app_title - render :template => "common/feed.atom", :layout => false, + render :template => "common/feed", :formats => [:atom], :layout => false, :content_type => 'application/atom+xml' end - # TODO: remove in Redmine 1.4 - def self.accept_key_auth(*actions) - ActiveSupport::Deprecation.warn "ApplicationController.accept_key_auth is deprecated and will be removed in Redmine 1.4. Use accept_rss_auth (or accept_api_auth) instead." - accept_rss_auth(*actions) - end - - # TODO: remove in Redmine 1.4 - def accept_key_auth_actions - ActiveSupport::Deprecation.warn "ApplicationController.accept_key_auth_actions is deprecated and will be removed in Redmine 1.4. Use accept_rss_auth (or accept_api_auth) instead." - self.class.accept_rss_auth - end - def self.accept_rss_auth(*actions) if actions.any? - write_inheritable_attribute('accept_rss_auth_actions', actions) + self.accept_rss_auth_actions = actions else - read_inheritable_attribute('accept_rss_auth_actions') || [] + self.accept_rss_auth_actions || [] end end @@ -390,9 +455,9 @@ def self.accept_api_auth(*actions) if actions.any? - write_inheritable_attribute('accept_api_auth_actions', actions) + self.accept_api_auth_actions = actions else - read_inheritable_attribute('accept_api_auth_actions') || [] + self.accept_api_auth_actions || [] end end @@ -472,12 +537,17 @@ # Returns the API key present in the request def api_key_from_request if params[:key].present? - params[:key] + params[:key].to_s elsif request.headers["X-Redmine-API-Key"].present? - request.headers["X-Redmine-API-Key"] + request.headers["X-Redmine-API-Key"].to_s end end + # Returns the API 'switch user' value if present + def api_switch_user_from_request + request.headers["X-Redmine-Switch-User"].to_s.presence + end + # Renders a warning flash if obj has unsaved attachments def render_attachment_warning_if_needed(obj) flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present? @@ -506,36 +576,30 @@ render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator." end - # Renders API response on validation failure - def render_validation_errors(object) - options = { :status => :unprocessable_entity, :layout => false } - options.merge!(case params[:format] - when 'xml'; { :xml => object.errors } - when 'json'; { :json => {'errors' => object.errors} } # ActiveResource client compliance - else - raise "Unknown format #{params[:format]} in #render_validation_errors" - end - ) - render options + # Renders a 200 response for successfull updates or deletions via the API + def render_api_ok + render_api_head :ok end - # Overrides #default_template so that the api template - # is used automatically if it exists - def default_template(action_name = self.action_name) - if api_request? - begin - return self.view_paths.find_template(default_template_name(action_name), 'api') - rescue ::ActionView::MissingTemplate - # the api template was not found - # fallback to the default behaviour - end - end - super + # Renders a head API response + def render_api_head(status) + # #head would return a response body with one space + render :text => '', :status => status, :layout => nil end - # Overrides #pick_layout so that #render with no arguments + # Renders API response on validation failure + def render_validation_errors(objects) + if objects.is_a?(Array) + @error_messages = objects.map {|object| object.errors.full_messages}.flatten + else + @error_messages = objects.errors.full_messages + end + render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil + end + + # Overrides #_include_layout? so that #render with no arguments # doesn't use the layout for api requests - def pick_layout(*args) - api_request? ? nil : super + def _include_layout?(*args) + api_request? ? false : super end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/attachments_controller.rb --- a/app/controllers/attachments_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/attachments_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,16 +16,16 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class AttachmentsController < ApplicationController + before_filter :find_project, :except => :upload + before_filter :file_readable, :read_authorize, :only => [:show, :download, :thumbnail] + before_filter :delete_authorize, :only => :destroy + before_filter :authorize_global, :only => :upload + before_filter :active_authorize, :only => :toggle_active include AttachmentsHelper helper :attachments - before_filter :find_project - before_filter :file_readable, :read_authorize, :except => :destroy - before_filter :delete_authorize, :only => :destroy - before_filter :active_authorize, :only => :toggle_active - - accept_api_auth :show, :download + accept_api_auth :show, :download, :upload def show respond_to do |format| @@ -58,26 +58,66 @@ @attachment.increment_download end - # images are sent inline - send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename), - :type => detect_content_type(@attachment), - :disposition => (@attachment.image? ? 'inline' : 'attachment') - + if stale?(:etag => @attachment.digest) + # images are sent inline + send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename), + :type => detect_content_type(@attachment), + :disposition => (@attachment.image? ? 'inline' : 'attachment') + end end - verify :method => :delete, :only => :destroy + def thumbnail + if @attachment.thumbnailable? && thumbnail = @attachment.thumbnail(:size => params[:size]) + if stale?(:etag => thumbnail) + send_file thumbnail, + :filename => filename_for_content_disposition(@attachment.filename), + :type => detect_content_type(@attachment), + :disposition => 'inline' + end + else + # No thumbnail for the attachment or thumbnail could not be created + render :nothing => true, :status => 404 + end + end + + def upload + # Make sure that API users get used to set this content type + # as it won't trigger Rails' automatic parsing of the request body for parameters + unless request.content_type == 'application/octet-stream' + render :nothing => true, :status => 406 + return + end + + @attachment = Attachment.new(:file => request.raw_post) + @attachment.author = User.current + @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16) + + if @attachment.save + respond_to do |format| + format.api { render :action => 'upload', :status => :created } + end + else + respond_to do |format| + format.api { render_validation_errors(@attachment) } + end + end + end + def destroy + if @attachment.container.respond_to?(:init_journal) + @attachment.container.init_journal(User.current) + end # Make sure association callbacks are called @attachment.container.attachments.delete(@attachment) - redirect_to :back - rescue ::ActionController::RedirectBackError - redirect_to :controller => 'projects', :action => 'show', :id => @project + redirect_to_referer_or project_path(@project) end def toggle_active @attachment.active = !@attachment.active? @attachment.save! - render :layout => false + respond_to do |format| + format.js + end end private diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/auth_sources_controller.rb --- a/app/controllers/auth_sources_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/auth_sources_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,36 +17,31 @@ class AuthSourcesController < ApplicationController layout 'admin' + menu_item :ldap_authentication before_filter :require_admin - # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html) - verify :method => :post, :only => [ :destroy, :create, :update ], - :redirect_to => { :template => :index } - def index - @auth_source_pages, @auth_sources = paginate auth_source_class.name.tableize, :per_page => 10 - render "auth_sources/index" + @auth_source_pages, @auth_sources = paginate AuthSource, :per_page => 10 end def new - @auth_source = auth_source_class.new - render 'auth_sources/new' + klass_name = params[:type] || 'AuthSourceLdap' + @auth_source = AuthSource.new_subclass_instance(klass_name, params[:auth_source]) end def create - @auth_source = auth_source_class.new(params[:auth_source]) + @auth_source = AuthSource.new_subclass_instance(params[:type], params[:auth_source]) if @auth_source.save flash[:notice] = l(:notice_successful_create) redirect_to :action => 'index' else - render 'auth_sources/new' + render :action => 'new' end end def edit @auth_source = AuthSource.find(params[:id]) - render 'auth_sources/edit' end def update @@ -55,17 +50,17 @@ flash[:notice] = l(:notice_successful_update) redirect_to :action => 'index' else - render 'auth_sources/edit' + render :action => 'edit' end end def test_connection - @auth_method = AuthSource.find(params[:id]) + @auth_source = AuthSource.find(params[:id]) begin - @auth_method.test_connection + @auth_source.test_connection flash[:notice] = l(:notice_successful_connection) - rescue => text - flash[:error] = l(:error_unable_to_connect, text.message) + rescue Exception => e + flash[:error] = l(:error_unable_to_connect, e.message) end redirect_to :action => 'index' end @@ -78,10 +73,4 @@ end redirect_to :action => 'index' end - - protected - - def auth_source_class - AuthSource - end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/auto_completes_controller.rb --- a/app/controllers/auto_completes_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/auto_completes_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,27 +1,44 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + class AutoCompletesController < ApplicationController before_filter :find_project def issues @issues = [] - q = params[:q].to_s - query = (params[:scope] == "all" && Setting.cross_project_issue_relations?) ? Issue : @project.issues - if q.match(/^\d+$/) - @issues << query.visible.find_by_id(q.to_i) + q = (params[:q] || params[:term]).to_s.strip + if q.present? + scope = (params[:scope] == "all" || @project.nil? ? Issue : @project.issues).visible + if q.match(/^\d+$/) + @issues << scope.find_by_id(q.to_i) + end + @issues += scope.where("LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%").order("#{Issue.table_name}.id DESC").limit(10).all + @issues.compact! end - unless q.blank? - @issues += query.visible.find(:all, :conditions => ["LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%"], :limit => 10) - end - @issues.compact! render :layout => false end private def find_project - project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id] - @project = Project.find(project_id) + if params[:project_id].present? + @project = Project.find(params[:project_id]) + end rescue ActiveRecord::RecordNotFound render_404 end - end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/boards_controller.rb --- a/app/controllers/boards_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/boards_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,18 +17,15 @@ class BoardsController < ApplicationController default_search_scope :messages - before_filter :find_project, :find_board_if_available, :authorize + before_filter :find_project_by_project_id, :find_board_if_available, :authorize accept_rss_auth :index, :show - helper :messages - include MessagesHelper helper :sort include SortHelper helper :watchers - include WatchersHelper def index - @boards = @project.boards + @boards = @project.boards.includes(:last_message => :author).all # show the board if there is only one if @boards.size == 1 @board = @boards.first @@ -42,15 +39,19 @@ sort_init 'updated_on', 'desc' sort_update 'created_on' => "#{Message.table_name}.created_on", 'replies' => "#{Message.table_name}.replies_count", - 'updated_on' => "#{Message.table_name}.updated_on" + 'updated_on' => "COALESCE(last_replies_messages.created_on, #{Message.table_name}.created_on)" @topic_count = @board.topics.count @topic_pages = Paginator.new self, @topic_count, per_page_option, params['page'] - @topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '), - :include => [:author, {:last_reply => :author}], - :limit => @topic_pages.items_per_page, - :offset => @topic_pages.current.offset - @message = Message.new + @topics = @board.topics. + reorder("#{Message.table_name}.sticky DESC"). + includes(:last_reply). + limit(@topic_pages.items_per_page). + offset(@topic_pages.current.offset). + order(sort_clause). + preload(:author, {:last_reply => :author}). + all + @message = Message.new(:board => @board) render :action => 'show', :layout => !request.xhr? } format.atom { @@ -62,22 +63,31 @@ end end - verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index } + def new + @board = @project.boards.build + @board.safe_attributes = params[:board] + end - def new - @board = Board.new + def create + @board = @project.boards.build @board.safe_attributes = params[:board] - @board.project = @project - if request.post? && @board.save + if @board.save flash[:notice] = l(:notice_successful_create) redirect_to_settings_in_projects + else + render :action => 'new' end end def edit + end + + def update @board.safe_attributes = params[:board] - if request.post? && @board.save + if @board.save redirect_to_settings_in_projects + else + render :action => 'edit' end end @@ -91,12 +101,6 @@ redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards' end - def find_project - @project = Project.find(params[:project_id]) - rescue ActiveRecord::RecordNotFound - render_404 - end - def find_board_if_available @board = @project.boards.find(params[:id]) if params[:id] rescue ActiveRecord::RecordNotFound diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/calendars_controller.rb --- a/app/controllers/calendars_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/calendars_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/comments_controller.rb --- a/app/controllers/comments_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/comments_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,3 +1,20 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + class CommentsController < ApplicationController default_search_scope :news model_object News @@ -5,8 +22,9 @@ before_filter :find_project_from_association before_filter :authorize - verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed } def create + raise Unauthorized unless @news.commentable? + @comment = Comment.new @comment.safe_attributes = params[:comment] @comment.author = User.current @@ -17,7 +35,6 @@ redirect_to :controller => 'news', :action => 'show', :id => @news end - verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed } def destroy @news.comments.find(params[:comment_id]).destroy redirect_to :controller => 'news', :action => 'show', :id => @news @@ -33,5 +50,4 @@ @comment = nil @news end - end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/context_menus_controller.rb --- a/app/controllers/context_menus_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/context_menus_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,20 +1,32 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + class ContextMenusController < ApplicationController helper :watchers helper :issues def issues @issues = Issue.visible.all(:conditions => {:id => params[:ids]}, :include => :project) - if (@issues.size == 1) @issue = @issues.first - @allowed_statuses = @issue.new_statuses_allowed_to(User.current) - else - @allowed_statuses = @issues.map do |i| - i.new_statuses_allowed_to(User.current) - end.inject do |memo,s| - memo & s - end end + @issue_ids = @issues.map(&:id).sort + + @allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&) @projects = @issues.collect(&:project).compact.uniq @project = @projects.first if @projects.size == 1 @@ -34,14 +46,28 @@ @trackers = @project.trackers else #when multiple projects, we only keep the intersection of each set - @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a} - @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t} + @assignables = @projects.map(&:assignable_users).reduce(:&) + @trackers = @projects.map(&:trackers).reduce(:&) + end + @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&) + + @priorities = IssuePriority.active.reverse + @back = back_url + + @options_by_custom_field = {} + if @can[:edit] + custom_fields = @issues.map(&:available_custom_fields).reduce(:&).select do |f| + %w(bool list user version).include?(f.field_format) && !f.multiple? + end + custom_fields.each do |field| + values = field.possible_values_options(@projects) + if values.any? + @options_by_custom_field[field] = values + end + end end - @priorities = IssuePriority.active.reverse - @statuses = IssueStatus.find(:all, :order => 'position') - @back = back_url - + @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&) render :layout => false end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/custom_fields_controller.rb --- a/app/controllers/custom_fields_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/custom_fields_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -19,6 +19,8 @@ layout 'admin' before_filter :require_admin + before_filter :build_new_custom_field, :only => [:new, :create] + before_filter :find_custom_field, :only => [:edit, :update, :destroy] def index @custom_fields_by_type = CustomField.find(:all).group_by {|f| f.class.name } @@ -26,39 +28,51 @@ end def new - @custom_field = begin - if params[:type].to_s.match(/.+CustomField$/) - params[:type].to_s.constantize.new(params[:custom_field]) - end - rescue - end - (redirect_to(:action => 'index'); return) unless @custom_field.is_a?(CustomField) + end + def create if request.post? and @custom_field.save flash[:notice] = l(:notice_successful_create) call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field) redirect_to :action => 'index', :tab => @custom_field.class.name else - @trackers = Tracker.find(:all, :order => 'position') + render :action => 'new' end end def edit - @custom_field = CustomField.find(params[:id]) - if request.post? and @custom_field.update_attributes(params[:custom_field]) + end + + def update + if request.put? and @custom_field.update_attributes(params[:custom_field]) flash[:notice] = l(:notice_successful_update) call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field) redirect_to :action => 'index', :tab => @custom_field.class.name else - @trackers = Tracker.find(:all, :order => 'position') + render :action => 'edit' end end def destroy - @custom_field = CustomField.find(params[:id]).destroy + @custom_field.destroy redirect_to :action => 'index', :tab => @custom_field.class.name rescue flash[:error] = l(:error_can_not_delete_custom_field) redirect_to :action => 'index' end + + private + + def build_new_custom_field + @custom_field = CustomField.new_subclass_instance(params[:type], params[:custom_field]) + if @custom_field.nil? + render_404 + end + end + + def find_custom_field + @custom_field = CustomField.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/documents_controller.rb --- a/app/controllers/documents_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/documents_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,9 +18,9 @@ class DocumentsController < ApplicationController default_search_scope :documents model_object Document - before_filter :find_project, :only => [:index, :new] - before_filter :find_model_object, :except => [:index, :new] - before_filter :find_project_from_association, :except => [:index, :new] + before_filter :find_project_by_project_id, :only => [:index, :new, :create] + before_filter :find_model_object, :except => [:index, :new, :create] + before_filter :find_project_from_association, :except => [:index, :new, :create] before_filter :authorize helper :attachments @@ -49,25 +49,36 @@ def new @document = @project.documents.build @document.safe_attributes = params[:document] - if request.post? and @document.save - attachments = Attachment.attach_files(@document, params[:attachments]) + end + + def create + @document = @project.documents.build + @document.safe_attributes = params[:document] + @document.save_attachments(params[:attachments]) + if @document.save render_attachment_warning_if_needed(@document) flash[:notice] = l(:notice_successful_create) redirect_to :action => 'index', :project_id => @project + else + render :action => 'new' end end def edit - @categories = DocumentCategory.active #TODO: use it in the views + end + + def update @document.safe_attributes = params[:document] - if request.post? and @document.save + if request.put? and @document.save flash[:notice] = l(:notice_successful_update) redirect_to :action => 'show', :id => @document + else + render :action => 'edit' end end def destroy - @document.destroy + @document.destroy if request.delete? redirect_to :controller => 'documents', :action => 'index', :project_id => @project end @@ -75,14 +86,9 @@ attachments = Attachment.attach_files(@document, params[:attachments]) render_attachment_warning_if_needed(@document) - Mailer.deliver_attachments_added(attachments[:files]) if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added') + if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added') + Mailer.attachments_added(attachments[:files]).deliver + end redirect_to :action => 'show', :id => @document end - -private - def find_project - @project = Project.find(params[:project_id]) - rescue ActiveRecord::RecordNotFound - render_404 - end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/enumerations_controller.rb --- a/app/controllers/enumerations_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/enumerations_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,53 +18,53 @@ class EnumerationsController < ApplicationController layout 'admin' - before_filter :require_admin + before_filter :require_admin, :except => :index + before_filter :require_admin_or_api_request, :only => :index + before_filter :build_new_enumeration, :only => [:new, :create] + before_filter :find_enumeration, :only => [:edit, :update, :destroy] + accept_api_auth :index helper :custom_fields - include CustomFieldsHelper def index - end - - verify :method => :post, :only => [ :destroy, :create, :update ], - :redirect_to => { :action => :index } - - def new - begin - @enumeration = params[:type].constantize.new - rescue NameError - @enumeration = Enumeration.new + respond_to do |format| + format.html + format.api { + @klass = Enumeration.get_subclass(params[:type]) + if @klass + @enumerations = @klass.shared.sorted.all + else + render_404 + end + } end end + def new + end + def create - @enumeration = Enumeration.new(params[:enumeration]) - @enumeration.type = params[:enumeration][:type] - if @enumeration.save + if request.post? && @enumeration.save flash[:notice] = l(:notice_successful_create) - redirect_to :action => 'index', :type => @enumeration.type + redirect_to :action => 'index' else render :action => 'new' end end def edit - @enumeration = Enumeration.find(params[:id]) end def update - @enumeration = Enumeration.find(params[:id]) - @enumeration.type = params[:enumeration][:type] if params[:enumeration][:type] - if @enumeration.update_attributes(params[:enumeration]) + if request.put? && @enumeration.update_attributes(params[:enumeration]) flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'index', :type => @enumeration.type + redirect_to :action => 'index' else render :action => 'edit' end end def destroy - @enumeration = Enumeration.find(params[:id]) if !@enumeration.in_use? # No associated objects @enumeration.destroy @@ -77,9 +77,22 @@ return end end - @enumerations = @enumeration.class.find(:all) - [@enumeration] - #rescue - # flash[:error] = 'Unable to delete enumeration' - # redirect_to :action => 'index' + @enumerations = @enumeration.class.all - [@enumeration] + end + + private + + def build_new_enumeration + class_name = params[:enumeration] && params[:enumeration][:type] || params[:type] + @enumeration = Enumeration.new_subclass_instance(class_name, params[:enumeration]) + if @enumeration.nil? + render_404 + end + end + + def find_enumeration + @enumeration = Enumeration.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/files_controller.rb --- a/app/controllers/files_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/files_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,3 +1,20 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + class FilesController < ApplicationController menu_item :files @@ -30,7 +47,7 @@ render_attachment_warning_if_needed(container) if !attachments.empty? && !attachments[:files].blank? && Setting.notified_events.include?('file_added') - Mailer.deliver_attachments_added(attachments[:files]) + Mailer.attachments_added(attachments[:files]).deliver end redirect_to project_files_path(@project) end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/gantts_controller.rb --- a/app/controllers/gantts_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/gantts_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/groups_controller.rb --- a/app/controllers/groups_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/groups_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -19,51 +19,34 @@ layout 'admin' before_filter :require_admin + before_filter :find_group, :except => [:index, :new, :create] + accept_api_auth :index, :show, :create, :update, :destroy, :add_users, :remove_user helper :custom_fields - # GET /groups - # GET /groups.xml def index - @groups = Group.find(:all, :order => 'lastname') + @groups = Group.sorted.all respond_to do |format| - format.html # index.html.erb - format.xml { render :xml => @groups } + format.html + format.api end end - # GET /groups/1 - # GET /groups/1.xml def show - @group = Group.find(params[:id]) - respond_to do |format| - format.html # show.html.erb - format.xml { render :xml => @group } + format.html + format.api end end - # GET /groups/new - # GET /groups/new.xml def new @group = Group.new - - respond_to do |format| - format.html # new.html.erb - format.xml { render :xml => @group } - end end - # GET /groups/1/edit - def edit - @group = Group.find(params[:id], :include => :projects) - end - - # POST /groups - # POST /groups.xml def create - @group = Group.new(params[:group]) + @group = Group.new + @group.safe_attributes = params[:group] respond_to do |format| if @group.save @@ -71,102 +54,87 @@ flash[:notice] = l(:notice_successful_create) redirect_to(params[:continue] ? new_group_path : groups_path) } - format.xml { render :xml => @group, :status => :created, :location => @group } + format.api { render :action => 'show', :status => :created, :location => group_url(@group) } else format.html { render :action => "new" } - format.xml { render :xml => @group.errors, :status => :unprocessable_entity } + format.api { render_validation_errors(@group) } end end end - # PUT /groups/1 - # PUT /groups/1.xml + def edit + end + def update - @group = Group.find(params[:id]) + @group.safe_attributes = params[:group] respond_to do |format| - if @group.update_attributes(params[:group]) + if @group.save flash[:notice] = l(:notice_successful_update) format.html { redirect_to(groups_path) } - format.xml { head :ok } + format.api { render_api_ok } else format.html { render :action => "edit" } - format.xml { render :xml => @group.errors, :status => :unprocessable_entity } + format.api { render_validation_errors(@group) } end end end - # DELETE /groups/1 - # DELETE /groups/1.xml def destroy - @group = Group.find(params[:id]) @group.destroy respond_to do |format| format.html { redirect_to(groups_url) } - format.xml { head :ok } + format.api { render_api_ok } end end def add_users - @group = Group.find(params[:id]) - users = User.find_all_by_id(params[:user_ids]) - @group.users << users if request.post? + @users = User.find_all_by_id(params[:user_id] || params[:user_ids]) + @group.users << @users if request.post? respond_to do |format| format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' } - format.js { - render(:update) {|page| - page.replace_html "tab-content-users", :partial => 'groups/users' - users.each {|user| page.visual_effect(:highlight, "user-#{user.id}") } - } - } + format.js + format.api { render_api_ok } end end def remove_user - @group = Group.find(params[:id]) @group.users.delete(User.find(params[:user_id])) if request.delete? respond_to do |format| format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' } - format.js { render(:update) {|page| page.replace_html "tab-content-users", :partial => 'groups/users'} } + format.js + format.api { render_api_ok } end end def autocomplete_for_user - @group = Group.find(params[:id]) @users = User.active.not_in_group(@group).like(params[:q]).all(:limit => 100) render :layout => false end def edit_membership - @group = Group.find(params[:id]) @membership = Member.edit_membership(params[:membership_id], params[:membership], @group) @membership.save if request.post? respond_to do |format| - if @membership.valid? - format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' } - format.js { - render(:update) {|page| - page.replace_html "tab-content-memberships", :partial => 'groups/memberships' - page.visual_effect(:highlight, "member-#{@membership.id}") - } - } - else - format.js { - render(:update) {|page| - page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', '))) - } - } - end + format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' } + format.js end end def destroy_membership - @group = Group.find(params[:id]) Member.find(params[:membership_id]).destroy if request.post? respond_to do |format| format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' } - format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'groups/memberships'} } + format.js end end + + private + + def find_group + @group = Group.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/issue_categories_controller.rb --- a/app/controllers/issue_categories_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/issue_categories_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -20,10 +20,10 @@ model_object IssueCategory before_filter :find_model_object, :except => [:index, :new, :create] before_filter :find_project_from_association, :except => [:index, :new, :create] - before_filter :find_project, :only => [:index, :new, :create] + before_filter :find_project_by_project_id, :only => [:index, :new, :create] before_filter :authorize accept_api_auth :index, :show, :create, :update, :destroy - + def index respond_to do |format| format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project } @@ -41,9 +41,13 @@ def new @category = @project.issue_categories.build @category.safe_attributes = params[:issue_category] + + respond_to do |format| + format.html + format.js + end end - verify :method => :post, :only => :create def create @category = @project.issue_categories.build @category.safe_attributes = params[:issue_category] @@ -53,20 +57,13 @@ flash[:notice] = l(:notice_successful_create) redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project end - format.js do - # IE doesn't support the replace_html rjs method for select box options - render(:update) {|page| page.replace "issue_category_id", - content_tag('select', '' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]') - } - end + format.js format.api { render :action => 'show', :status => :created, :location => issue_category_path(@category) } end else respond_to do |format| format.html { render :action => 'new'} - format.js do - render(:update) {|page| page.alert(@category.errors.full_messages.join('\n')) } - end + format.js { render :action => 'new'} format.api { render_validation_errors(@category) } end end @@ -75,7 +72,6 @@ def edit end - verify :method => :put, :only => :update def update @category.safe_attributes = params[:issue_category] if @category.save @@ -84,7 +80,7 @@ flash[:notice] = l(:notice_successful_update) redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project } - format.api { head :ok } + format.api { render_api_ok } end else respond_to do |format| @@ -94,10 +90,9 @@ end end - verify :method => :delete, :only => :destroy def destroy @issue_count = @category.issues.size - if @issue_count == 0 || params[:todo] || api_request? + if @issue_count == 0 || params[:todo] || api_request? reassign_to = nil if params[:reassign_to_id] && (params[:todo] == 'reassign' || params[:todo].blank?) reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id]) @@ -105,7 +100,7 @@ @category.destroy(reassign_to) respond_to do |format| format.html { redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' } - format.api { head :ok } + format.api { render_api_ok } end return end @@ -119,10 +114,4 @@ super @category = @object end - - def find_project - @project = Project.find(params[:project_id]) - rescue ActiveRecord::RecordNotFound - render_404 - end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/issue_moves_controller.rb --- a/app/controllers/issue_moves_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,87 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class IssueMovesController < ApplicationController - menu_item :issues - - default_search_scope :issues - before_filter :find_issues, :check_project_uniqueness - before_filter :authorize - - def new - prepare_for_issue_move - render :layout => false if request.xhr? - end - - def create - prepare_for_issue_move - - if request.post? - new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id]) - unsaved_issue_ids = [] - moved_issues = [] - @issues.each do |issue| - issue.reload - issue.init_journal(User.current) - issue.current_journal.notes = @notes if @notes.present? - call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy }) - if r = issue.move_to_project(@target_project, new_tracker, {:copy => @copy, :attributes => extract_changed_attributes_for_move(params)}) - moved_issues << r - else - unsaved_issue_ids << issue.id - end - end - set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids) - - if params[:follow] - if @issues.size == 1 && moved_issues.size == 1 - redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first - else - redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project) - end - else - redirect_to :controller => 'issues', :action => 'index', :project_id => @project - end - return - end - end - - private - - def prepare_for_issue_move - @issues.sort! - @copy = params[:copy_options] && params[:copy_options][:copy] - @allowed_projects = Issue.allowed_target_projects_on_move - @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id] - @target_project ||= @project - @trackers = @target_project.trackers - @available_statuses = Workflow.available_statuses(@project) - @notes = params[:notes] - @notes ||= '' - end - - def extract_changed_attributes_for_move(params) - changed_attributes = {} - [:assigned_to_id, :status_id, :start_date, :due_date, :priority_id].each do |valid_attribute| - unless params[valid_attribute].blank? - changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute]) - end - end - changed_attributes - end - -end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/issue_relations_controller.rb --- a/app/controllers/issue_relations_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/issue_relations_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -39,27 +39,19 @@ end end - verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed } def create @relation = IssueRelation.new(params[:relation]) @relation.issue_from = @issue - if params[:relation] && m = params[:relation][:issue_to_id].to_s.match(/^#?(\d+)$/) + if params[:relation] && m = params[:relation][:issue_to_id].to_s.strip.match(/^#?(\d+)$/) @relation.issue_to = Issue.visible.find_by_id(m[1].to_i) end saved = @relation.save respond_to do |format| format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue } - format.js do + format.js { @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? } - render :update do |page| - page.replace_html "relations", :partial => 'issues/relations' - if @relation.errors.empty? - page << "$('relation_delay').value = ''" - page << "$('relation_issue_to_id').value = ''" - end - end - end + } format.api { if saved render :action => 'show', :status => :created, :location => relation_url(@relation) @@ -70,15 +62,14 @@ end end - verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed } def destroy raise Unauthorized unless @relation.deletable? @relation.destroy respond_to do |format| - format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue } - format.js { render(:update) {|page| page.remove "relation-#{@relation.id}"} } - format.api { head :ok } + format.html { redirect_to issue_path } # TODO : does this really work since @issue is always nil? What is it useful to? + format.js + format.api { render_api_ok } end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/issue_statuses_controller.rb --- a/app/controllers/issue_statuses_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/issue_statuses_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -62,7 +62,6 @@ end end - verify :method => :delete, :only => :destroy, :redirect_to => { :action => :index } def destroy IssueStatus.find(params[:id]).destroy redirect_to :action => 'index' diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/issues_controller.rb --- a/app/controllers/issues_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/issues_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -20,8 +20,7 @@ default_search_scope :issues before_filter :find_issue, :only => [:show, :edit, :update] - before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy] - before_filter :check_project_uniqueness, :only => [:move, :perform_move] + before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy] before_filter :find_project, :only => [:new, :create] before_filter :authorize, :except => [:index] before_filter :find_optional_project, :only => [:index] @@ -51,21 +50,13 @@ include SortHelper include IssuesHelper helper :timelog - helper :gantt include Redmine::Export::PDF - verify :method => [:post, :delete], - :only => :destroy, - :render => { :nothing => true, :status => :method_not_allowed } - - verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed } - verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed } - verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed } - def index retrieve_query sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria) sort_update(@query.sortable_columns) + @query.sort_criteria = sort_criteria.to_a if @query.valid? case params[:format] @@ -91,7 +82,7 @@ respond_to do |format| format.html { render :template => 'issues/index', :layout => !request.xhr? } format.api { - Issue.load_relations(@issues) if include_in_api_response?('relations') + Issue.load_visible_relations(@issues) if include_in_api_response?('relations') } format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") } format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') } @@ -109,14 +100,13 @@ end def show - @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC") + @journals = @issue.journals.includes(:user, :details).reorder("#{Journal.table_name}.id ASC").all @journals.each_with_index {|j,i| j.indice = i+1} + @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project) @journals.reverse! if User.current.wants_comments_in_reverse_order? - if User.current.allowed_to?(:view_changesets, @project) - @changesets = @issue.changesets.visible.all - @changesets.reverse! if User.current.wants_comments_in_reverse_order? - end + @changesets = @issue.changesets.visible.all + @changesets.reverse! if User.current.wants_comments_in_reverse_order? @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? } @allowed_statuses = @issue.new_statuses_allowed_to(User.current) @@ -124,10 +114,16 @@ @priorities = IssuePriority.active @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project) respond_to do |format| - format.html { render :template => 'issues/show' } + format.html { + retrieve_previous_and_next_issue_ids + render :template => 'issues/show' + } format.api format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' } - format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") } + format.pdf { + pdf = issue_to_pdf(@issue, :journals => @journals) + send_data(pdf, :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") + } end end @@ -136,20 +132,20 @@ def new respond_to do |format| format.html { render :action => 'new', :layout => !request.xhr? } - format.js { render :partial => 'attributes' } + format.js { render :partial => 'update_form' } end end def create call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue }) + @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads])) if @issue.save - attachments = Attachment.attach_files(@issue, params[:attachments]) call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue}) # Also adds the assignee to the watcher's list - if params[:issue][:assigned_to_id] && !params[:issue][:assigned_to_id].empty?: - unless @issue.watcher_ids.include?(params[:issue][:assigned_to_id]): + if params[:issue][:assigned_to_id] && !params[:issue][:assigned_to_id].empty? + unless @issue.watcher_ids.include?(params[:issue][:assigned_to_id]) @issue.add_watcher(User.find(params[:issue][:assigned_to_id])) end end @@ -157,8 +153,8 @@ respond_to do |format| format.html { render_attachment_warning_if_needed(@issue) - flash[:notice] = l(:notice_issue_successful_create, :id => "##{@issue.id}") - redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } : + flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject)) + redirect_to(params[:continue] ? { :action => 'new', :project_id => @issue.project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } : { :action => 'show', :id => @issue }) } format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) } @@ -173,9 +169,7 @@ end def edit - update_issue_from_params - - @journal = @issue.current_journal + return unless update_issue_from_params respond_to do |format| format.html { } @@ -184,21 +178,28 @@ end def update - update_issue_from_params + return unless update_issue_from_params + @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads])) + saved = false + begin + saved = @issue.save_issue_with_child_records(params, @time_entry) + rescue ActiveRecord::StaleObjectError + @conflict = true + if params[:last_journal_id] + @conflict_journals = @issue.journals_after(params[:last_journal_id]).all + @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project) + end + end - if @issue.save_issue_with_child_records(params, @time_entry) + if saved render_attachment_warning_if_needed(@issue) flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record? respond_to do |format| format.html { redirect_back_or_default({:action => 'show', :id => @issue}) } - format.api { head :ok } + format.api { render_api_ok } end else - render_attachment_warning_if_needed(@issue) - flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record? - @journal = @issue.current_journal - respond_to do |format| format.html { render :action => 'edit' } format.api { render_validation_errors(@issue) } @@ -206,32 +207,85 @@ end end - # Bulk edit a set of issues + # Bulk edit/copy a set of issues def bulk_edit @issues.sort! - @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w} - @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c} - @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a} - @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t} + @copy = params[:copy].present? + @notes = params[:notes] + + if User.current.allowed_to?(:move_issues, @projects) + @allowed_projects = Issue.allowed_target_projects_on_move + if params[:issue] + @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s} + if @target_project + target_projects = [@target_project] + end + end + end + target_projects ||= @projects + + if @copy + @available_statuses = [IssueStatus.default] + else + @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&) + end + @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&) + @assignables = target_projects.map(&:assignable_users).reduce(:&) + @trackers = target_projects.map(&:trackers).reduce(:&) + @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&) + @categories = target_projects.map {|p| p.issue_categories}.reduce(:&) + if @copy + @attachments_present = @issues.detect {|i| i.attachments.any?}.present? + @subtasks_present = @issues.detect {|i| !i.leaf?}.present? + end + + @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&) + render :layout => false if request.xhr? end def bulk_update @issues.sort! + @copy = params[:copy].present? attributes = parse_params_for_bulk_issue_attributes(params) unsaved_issue_ids = [] + moved_issues = [] + + if @copy && params[:copy_subtasks].present? + # Descendant issues will be copied with the parent task + # Don't copy them twice + @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}} + end + @issues.each do |issue| issue.reload + if @copy + issue = issue.copy({}, + :attachments => params[:copy_attachments].present?, + :subtasks => params[:copy_subtasks].present? + ) + end journal = issue.init_journal(User.current, params[:notes]) issue.safe_attributes = attributes call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue }) - unless issue.save + if issue.save + moved_issues << issue + else # Keep unsaved issue ids to display them in flash error unsaved_issue_ids << issue.id end end set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids) - redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project}) + + if params[:follow] + if @issues.size == 1 && moved_issues.size == 1 + redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first + elsif moved_issues.map(&:project).uniq.size == 1 + redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first + end + else + redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project}) + end end def destroy @@ -264,55 +318,75 @@ end respond_to do |format| format.html { redirect_back_or_default(:action => 'index', :project_id => @project) } - format.api { head :ok } + format.api { render_api_ok } end end -private - def find_issue - # Issue.visible.find(...) can not be used to redirect user to the login form - # if the issue actually exists but requires authentication - @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category]) - unless @issue.visible? - deny_access - return - end - @project = @issue.project + private + + def find_project + project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id]) + @project = Project.find(project_id) rescue ActiveRecord::RecordNotFound render_404 end - def find_project - project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id] - @project = Project.find(project_id) - rescue ActiveRecord::RecordNotFound - render_404 + def retrieve_previous_and_next_issue_ids + retrieve_query_from_session + if @query + sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria) + sort_update(@query.sortable_columns, 'issues_index_sort') + limit = 500 + issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version]) + if (idx = issue_ids.index(@issue.id)) && idx < limit + if issue_ids.size < 500 + @issue_position = idx + 1 + @issue_count = issue_ids.size + end + @prev_issue_id = issue_ids[idx - 1] if idx > 0 + @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1) + end + end end # Used by #edit and #update to set some common instance variables # from the params # TODO: Refactor, not everything in here is needed by #edit def update_issue_from_params - @allowed_statuses = @issue.new_statuses_allowed_to(User.current) - @priorities = IssuePriority.active @edit_allowed = User.current.allowed_to?(:edit_issues, @project) @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project) @time_entry.attributes = params[:time_entry] - @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil) - @issue.init_journal(User.current, @notes) - @issue.safe_attributes = params[:issue] + @issue.init_journal(User.current) + + issue_attributes = params[:issue] + if issue_attributes && params[:conflict_resolution] + case params[:conflict_resolution] + when 'overwrite' + issue_attributes = issue_attributes.dup + issue_attributes.delete(:lock_version) + when 'add_notes' + issue_attributes = issue_attributes.slice(:notes) + when 'cancel' + redirect_to issue_path(@issue) + return false + end + end # tests if the the user assigned_to_id # is in this issues watcher's list # if not, adds it. - if params[:issue] && params[:issue][:assigned_to_id] && !params[:issue][:assigned_to_id].empty?: - unless @issue.watched_by?(User.find(params[:issue][:assigned_to_id])): + if params[:issue] && params[:issue][:assigned_to_id] && !params[:issue][:assigned_to_id].empty? + unless @issue.watched_by?(User.find(params[:issue][:assigned_to_id])) @issue.add_watcher(User.find(params[:issue][:assigned_to_id])) end end + @issue.safe_attributes = issue_attributes + @priorities = IssuePriority.active + @allowed_statuses = @issue.new_statuses_allowed_to(User.current) + true end @@ -321,14 +395,24 @@ def build_new_issue_from_params if params[:id].blank? @issue = Issue.new - @issue.copy_from(params[:copy_from]) if params[:copy_from] + if params[:copy_from] + begin + @copy_from = Issue.visible.find(params[:copy_from]) + @copy_attachments = params[:copy_attachments].present? || request.get? + @copy_subtasks = params[:copy_subtasks].present? || request.get? + @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks) + rescue ActiveRecord::RecordNotFound + render_404 + return + end + end @issue.project = @project else @issue = @project.issues.visible.find(params[:id]) end @issue.project = @project - @issue.author = User.current + @issue.author ||= User.current # Tracker must be set before custom field values @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first) if @issue.tracker.nil? @@ -336,14 +420,11 @@ return false end @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date? - if params[:issue].is_a?(Hash) - @issue.safe_attributes = params[:issue] - if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record? - @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] - end - end + @issue.safe_attributes = params[:issue] + @priorities = IssuePriority.active @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true) + @available_watchers = (@issue.project.users.sort + @issue.watcher_users).uniq end def check_for_default_issue_status @@ -356,7 +437,16 @@ def parse_params_for_bulk_issue_attributes(params) attributes = (params[:issue] || {}).reject {|k,v| v.blank?} attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'} - attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values] + if custom = attributes[:custom_field_values] + custom.reject! {|k,v| v.blank?} + custom.keys.each do |k| + if custom[k].is_a?(Array) + custom[k] << '' if custom[k].delete('__none__') + else + custom[k] = '' if custom[k] == '__none__' + end + end + end attributes end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/journals_controller.rb --- a/app/controllers/journals_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/journals_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -57,26 +57,20 @@ end def new - journal = Journal.find(params[:journal_id]) if params[:journal_id] - if journal - user = journal.user - text = journal.notes + @journal = Journal.visible.find(params[:journal_id]) if params[:journal_id] + if @journal + user = @journal.user + text = @journal.notes else user = @issue.author text = @issue.description end # Replaces pre blocks with [...] text = text.to_s.strip.gsub(%r{
    ((.|\s)*?)
    }m, '[...]') - content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> " - content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n" - - render(:update) { |page| - page.<< "$('notes').value = \"#{escape_javascript content}\";" - page.show 'update' - page << "Form.Element.focus('notes');" - page << "Element.scrollTo('update');" - page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;" - } + @content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> " + @content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n" + rescue ActiveRecord::RecordNotFound + render_404 end def edit @@ -103,17 +97,9 @@ private def find_journal - @journal = Journal.find(params[:id]) + @journal = Journal.visible.find(params[:id]) @project = @journal.journalized.project rescue ActiveRecord::RecordNotFound render_404 end - - # TODO: duplicated in IssuesController - def find_issue - @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category]) - @project = @issue.project - rescue ActiveRecord::RecordNotFound - render_404 - end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/ldap_auth_sources_controller.rb --- a/app/controllers/ldap_auth_sources_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,25 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class LdapAuthSourcesController < AuthSourcesController - - protected - - def auth_source_class - AuthSourceLdap - end -end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/mail_handler_controller.rb --- a/app/controllers/mail_handler_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/mail_handler_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,10 +18,6 @@ class MailHandlerController < ActionController::Base before_filter :check_credential - verify :method => :post, - :only => :index, - :render => { :nothing => true, :status => 405 } - # Submits an incoming email to MailHandler def index options = params.dup diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/members_controller.rb --- a/app/controllers/members_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/members_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,110 +18,115 @@ class MembersController < ApplicationController model_object Member menu_item :members - before_filter :find_model_object, :except => [:index, :new, :autocomplete_for_member] - before_filter :find_project_from_association, :except => [:new, :index, :autocomplete_for_member] - before_filter :find_project, :only => [:new, :autocomplete_for_member] - before_filter :find_project_by_project_id, :only => [:index] + before_filter :find_model_object, :except => [:index, :create, :autocomplete] + before_filter :find_project_from_association, :except => [:index, :create, :autocomplete] + before_filter :find_project_by_project_id, :only => [:index, :create, :autocomplete] before_filter :authorize + accept_api_auth :index, :show, :create, :update, :destroy def index - logger.debug('in index') respond_to do |format| format.html { render :layout => false if request.xhr? } + format.api { + @offset, @limit = api_offset_and_limit + @member_count = @project.member_principals.count + @member_pages = Paginator.new self, @member_count, @limit, params['page'] + @offset ||= @member_pages.current.offset + @members = @project.member_principals.all( + :order => "#{Member.table_name}.id", + :limit => @limit, + :offset => @offset + ) + } end end - def new + def show + respond_to do |format| + format.html { head 406 } + format.api + end + end + + def create members = [] - if params[:member] && request.post? - attrs = params[:member].dup - if (user_ids = attrs.delete(:user_ids)) + if params[:membership] + if params[:membership][:user_ids] + attrs = params[:membership].dup + user_ids = attrs.delete(:user_ids) user_ids.each do |user_id| - @new_member = Member.new(:role_ids => params[:member][:role_ids], :user_id => user_id) + @new_member = Member.new(:role_ids => params[:membership][:role_ids], :user_id => user_id) members << @new_member # send notification to member - Mailer.deliver_added_to_project(@new_member, @project) - + Mailer.member_added_to_project(@new_member, @project).deliver end else - @new_member = Member.new(:role_ids => params[:member][:role_ids], :user_id => params[:member][:user_id]) + @new_member = Member.new(:role_ids => params[:membership][:role_ids], :user_id => params[:membership][:user_id]) members << @new_member # send notification to member - Mailer.deliver_added_to_project(@new_member, @project) - + Mailer.member_added_to_project(@new_member, @project).deliver end @project.members << members end + respond_to do |format| - if members.present? && members.all? {|m| m.valid? } - - format.html { redirect_to :action => 'index', :project_id => @project } - - format.js { - render(:update) {|page| - page.replace_html "memberlist", :partial => 'editlist' - page << 'hideOnLoad()' - members.each {|member| page.visual_effect(:highlight, "member-#{member.id}") } - } - } - else - - format.js { - render(:update) {|page| - errors = members.collect {|m| - m.errors.full_messages - }.flatten.uniq - - # page.alert(l(:notice_failed_to_save_members, :errors => errors.join(', '))) - } - } - - end + format.html { redirect_to :action => 'index', :project_id => @project } + format.js { @members = members } + format.api { + @member = members.first + if @member.valid? + render :action => 'show', :status => :created, :location => membership_url(@member) + else + render_validation_errors(@member) + end + } end end - def edit - if params[:member] - @member.role_ids = params[:member][:role_ids] + def update + if params[:membership] + @member.role_ids = params[:membership][:role_ids] end - if request.post? and @member.save - respond_to do |format| - format.html { redirect_to :action => 'index', :project_id => @project } - format.js { - render(:update) {|page| - page.replace_html "memberlist", :partial => 'editlist' - page << 'hideOnLoad()' - page.visual_effect(:highlight, "member-#{@member.id}") - } - } - end + saved = @member.save + respond_to do |format| + format.html { redirect_to :action => 'index', :project_id => @project } + format.js + format.api { + if saved + render_api_ok + else + render_validation_errors(@member) + end + } end end def destroy - if request.post? && @member.deletable? + if request.delete? && @member.deletable? @member.destroy end respond_to do |format| format.html { redirect_to :action => 'index', :project_id => @project } - format.js { render(:update) {|page| - page.replace_html "memberlist", :partial => 'editlist' - page << 'hideOnLoad()' - } + format.js + format.api { + if @member.destroyed? + render_api_ok + else + head :unprocessable_entity + end } end end - def autocomplete_for_member + def autocomplete @principals = Principal.active.not_member_of(@project).like(params[:q]).all(:limit => 100) logger.debug "Query for #{params[:q]} returned #{@principals.size} results" render :layout => false end - end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/messages_controller.rb --- a/app/controllers/messages_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/messages_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -22,9 +22,7 @@ before_filter :find_message, :except => [:new, :preview] before_filter :authorize, :except => [:preview, :edit, :destroy] - verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show } - verify :xhr => true, :only => :quote - + helper :boards helper :watchers helper :attachments include AttachmentsHelper @@ -57,11 +55,13 @@ @message.author = User.current @message.board = @board @message.safe_attributes = params[:message] - if request.post? && @message.save - call_hook(:controller_messages_new_after_save, { :params => params, :message => @message}) - attachments = Attachment.attach_files(@message, params[:attachments]) - render_attachment_warning_if_needed(@message) - redirect_to :action => 'show', :id => @message + if request.post? + @message.save_attachments(params[:attachments]) + if @message.save + call_hook(:controller_messages_new_after_save, { :params => params, :message => @message}) + render_attachment_warning_if_needed(@message) + redirect_to board_message_path(@board, @message) + end end end @@ -77,7 +77,7 @@ attachments = Attachment.attach_files(@reply, params[:attachments]) render_attachment_warning_if_needed(@reply) end - redirect_to :action => 'show', :id => @topic, :r => @reply + redirect_to board_message_path(@board, @topic, :r => @reply) end # Edit a message @@ -89,46 +89,41 @@ render_attachment_warning_if_needed(@message) flash[:notice] = l(:notice_successful_update) @message.reload - redirect_to :action => 'show', :board_id => @message.board, :id => @message.root, :r => (@message.parent_id && @message.id) + redirect_to board_message_path(@message.board, @message.root, :r => (@message.parent_id && @message.id)) end end # Delete a messages def destroy (render_403; return false) unless @message.destroyable_by?(User.current) + r = @message.to_param @message.destroy - redirect_to @message.parent.nil? ? - { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } : - { :action => 'show', :id => @message.parent, :r => @message } + if @message.parent + redirect_to board_message_path(@board, @message.parent, :r => r) + else + redirect_to project_board_path(@project, @board) + end end def quote - user = @message.author - text = @message.content - subject = @message.subject.gsub('"', '\"') - subject = "RE: #{subject}" unless subject.starts_with?('RE:') - content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> " - content << text.to_s.strip.gsub(%r{
    ((.|\s)*?)
    }m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n" - render(:update) { |page| - page << "$('message_subject').value = \"#{subject}\";" - page.<< "$('message_content').value = \"#{content}\";" - page.show 'reply' - page << "Form.Element.focus('message_content');" - page << "Element.scrollTo('reply');" - page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;" - } + @subject = @message.subject + @subject = "RE: #{@subject}" unless @subject.starts_with?('RE:') + + @content = "#{ll(Setting.default_language, :text_user_wrote, @message.author)}\n> " + @content << @message.content.to_s.strip.gsub(%r{
    ((.|\s)*?)
    }m, '[...]').gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n" end def preview message = @board.messages.find_by_id(params[:id]) @attachements = message.attachments if message @text = (params[:message] || params[:reply])[:content] + @previewed = message render :partial => 'common/preview' end private def find_message - find_board + return unless find_board @message = @board.messages.find(params[:id], :include => :parent) @topic = @message.root rescue ActiveRecord::RecordNotFound @@ -140,5 +135,6 @@ @project = @board.project rescue ActiveRecord::RecordNotFound render_404 + nil end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/my_controller.rb --- a/app/controllers/my_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/my_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -38,12 +38,9 @@ }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze DEFAULT_LAYOUT = { 'left' => ['myprojects', 'activitymyprojects'], - 'right' => ['tipoftheday', 'issueswatched'] + 'right' => ['colleagues', 'tipoftheday', 'issueswatched'] }.freeze - verify :xhr => true, - :only => [:add_block, :remove_block, :order_blocks] - def index page render :action => 'page' @@ -103,6 +100,24 @@ end end + # Destroys user's account + def destroy + @user = User.current + unless @user.own_account_deletable? + redirect_to :action => 'account' + return + end + + if request.post? && params[:confirm] + @user.destroy + if @user.destroyed? + logout_user + flash[:notice] = l(:notice_account_deleted) + end + redirect_to home_path + end + end + # Manage user's password def password @user = User.current @@ -155,7 +170,11 @@ @user = User.current @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup @block_options = [] - BLOCKS.each {|k, v| @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]} + BLOCKS.each do |k, v| + unless %w(top left right).detect {|f| (@blocks[f] ||= []).include?(k)} + @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize] + end + end end # Add a block to user's page @@ -163,16 +182,17 @@ # params[:block] : id of the block to add def add_block block = params[:block].to_s.underscore - (render :nothing => true; return) unless block && (BLOCKS.keys.include? block) - @user = User.current - layout = @user.pref[:my_page_layout] || {} - # remove if already present in a group - %w(top left right).each {|f| (layout[f] ||= []).delete block } - # add it on top - layout['top'].unshift block - @user.pref[:my_page_layout] = layout - @user.pref.save - render :partial => "block", :locals => {:user => @user, :block_name => block} + if block.present? && BLOCKS.key?(block) + @user = User.current + layout = @user.pref[:my_page_layout] || {} + # remove if already present in a group + %w(top left right).each {|f| (layout[f] ||= []).delete block } + # add it on top + layout['top'].unshift block + @user.pref[:my_page_layout] = layout + @user.pref.save + end + redirect_to :action => 'page_layout' end # Remove a block to user's page @@ -185,7 +205,7 @@ %w(top left right).each {|f| (layout[f] ||= []).delete block } @user.pref[:my_page_layout] = layout @user.pref.save - render :nothing => true + redirect_to :action => 'page_layout' end # Change blocks order on user's page @@ -195,7 +215,8 @@ group = params[:group] @user = User.current if group.is_a?(String) - group_items = (params["list-#{group}"] || []).collect(&:underscore) + group_items = (params["blocks"] || []).collect(&:underscore) + group_items.each {|s| s.sub!(/^block_/, '')} if group_items and group_items.is_a? Array layout = @user.pref[:my_page_layout] || {} # remove group blocks if they are presents in other groups diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/news_controller.rb --- a/app/controllers/news_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/news_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -20,13 +20,14 @@ model_object News before_filter :find_model_object, :except => [:new, :create, :index] before_filter :find_project_from_association, :except => [:new, :create, :index] - before_filter :find_project, :only => [:new, :create] + before_filter :find_project_by_project_id, :only => [:new, :create] before_filter :authorize, :except => [:index] before_filter :find_optional_project, :only => :index accept_rss_auth :index accept_api_auth :index helper :watchers + helper :attachments def index case params[:format] @@ -68,13 +69,13 @@ def create @news = News.new(:project => @project, :author => User.current) @news.safe_attributes = params[:news] - if request.post? - if @news.save - flash[:notice] = l(:notice_successful_create) - redirect_to :controller => 'news', :action => 'index', :project_id => @project - else - render :action => 'new' - end + @news.save_attachments(params[:attachments]) + if @news.save + render_attachment_warning_if_needed(@news) + flash[:notice] = l(:notice_successful_create) + redirect_to :controller => 'news', :action => 'index', :project_id => @project + else + render :action => 'new' end end @@ -83,7 +84,9 @@ def update @news.safe_attributes = params[:news] - if request.put? and @news.save + @news.save_attachments(params[:attachments]) + if @news.save + render_attachment_warning_if_needed(@news) flash[:notice] = l(:notice_successful_update) redirect_to :action => 'show', :id => @news else @@ -96,12 +99,7 @@ redirect_to :action => 'index', :project_id => @project end -private - def find_project - @project = Project.find(params[:project_id]) - rescue ActiveRecord::RecordNotFound - render_404 - end + private def find_optional_project return true unless params[:project_id] diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/previews_controller.rb --- a/app/controllers/previews_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/previews_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -26,7 +26,8 @@ if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n") @description = nil end - @notes = params[:notes] + # params[:notes] is useful for preview of notes in issue history + @notes = params[:notes] || (params[:issue] ? params[:issue][:notes] : nil) else @description = (params[:issue] ? params[:issue][:description] : nil) end @@ -34,6 +35,10 @@ end def news + if params[:id].present? && news = News.visible.find_by_id(params[:id]) + @previewed = news + @attachments = news.attachments + end @text = (params[:news] ? params[:news][:description] : nil) render :partial => 'common/preview' end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/project_enumerations_controller.rb --- a/app/controllers/project_enumerations_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/project_enumerations_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,3 +1,20 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + class ProjectEnumerationsController < ApplicationController before_filter :find_project_by_project_id before_filter :authorize diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/projects_controller.rb --- a/app/controllers/projects_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/projects_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -29,7 +29,7 @@ after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller| if controller.request.post? - controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt' + controller.send :expire_action, :controller => 'welcome', :action => 'robots' end end @@ -57,8 +57,15 @@ @project_count = Project.visible_roots.count @project_pages = Paginator.new self, @project_count, @limit, params['page'] @offset ||= @project_pages.current.offset - @projects = Project.visible_roots.all(:offset => @offset, :limit => @limit, :order => sort_clause) + @projects = Project.visible_roots.all(:offset => @offset, :limit => @limit, :order => sort_clause) render :template => 'projects/index.html.erb', :layout => !request.xhr? + +## Redmine 2.2: +# scope = Project +# unless params[:closed] +# scope = scope.active +# end +# @projects = scope.visible.order('lft').all } format.api { @offset, @limit = api_offset_and_limit @@ -85,27 +92,17 @@ def new @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") - @trackers = Tracker.all + @trackers = Tracker.sorted.all @project = Project.new @project.safe_attributes = params[:project] end - verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed } def create @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") - @trackers = Tracker.all + @trackers = Tracker.sorted.all @project = Project.new @project.safe_attributes = params[:project] - - # todo: luisf: this should be removed from here... - if params && params[:project] && !params[:project][:tag_list].nil? - new_tags = params[:project][:tag_list].to_s.downcase - - @project.tag_list = ActionController::Base.helpers.strip_tags(new_tags) - end - # end of code to be removed - if validate_is_public_key && validate_parent_id && @project.save @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') # Add current user as a project member if he is not admin @@ -135,18 +132,14 @@ def copy @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") - @trackers = Tracker.all + @trackers = Tracker.sorted.all @root_projects = Project.find(:all, :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}", :order => 'name') @source_project = Project.find(params[:id]) if request.get? @project = Project.copy_from(@source_project) - if @project - @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers? - else - redirect_to :controller => 'admin', :action => 'projects' - end + @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers? else Mailer.with_deliveries(params[:notifications] == '1') do @project = Project.new @@ -165,9 +158,10 @@ end end rescue ActiveRecord::RecordNotFound - redirect_to :controller => 'admin', :action => 'projects' + # source_project not found + render_404 end - + # Show @project def show if params[:jump] @@ -182,12 +176,8 @@ cond = @project.project_condition(Setting.display_subprojects_issues?) - @open_issues_by_tracker = Issue.visible.count(:group => :tracker, - :include => [:project, :status, :tracker], - :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false]) - @total_issues_by_tracker = Issue.visible.count(:group => :tracker, - :include => [:project, :status, :tracker], - :conditions => cond) + @open_issues_by_tracker = Issue.visible.open.where(cond).count(:group => :tracker) + @total_issues_by_tracker = Issue.visible.where(cond).count(:group => :tracker) if User.current.allowed_to?(:view_time_entries, @project) @total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f @@ -205,16 +195,13 @@ @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") @issue_category ||= IssueCategory.new @member ||= @project.members.new - @trackers = Tracker.all - @repository ||= @project.repository + @trackers = Tracker.sorted.all @wiki ||= @project.wiki end def edit end - # TODO: convert to PUT only - verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed } def update @project.safe_attributes = params[:project] if validate_parent_id && @project.save @@ -224,7 +211,7 @@ flash[:notice] = l(:notice_successful_update) redirect_to :action => 'settings', :id => @project } - format.api { head :ok } + format.api { render_api_ok } end else respond_to do |format| @@ -237,8 +224,6 @@ end end - verify :method => :post, :only => :modules, :render => {:nothing => true, :status => :method_not_allowed } - def overview @project.has_welcome_page = params[:has_welcome_page] if @project.save @@ -267,32 +252,31 @@ redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status])) end + def close + @project.close + redirect_to project_path(@project) + end + + def reopen + @project.reopen + redirect_to project_path(@project) + end + # Delete @project def destroy @project_to_destroy = @project - if request.get? - # display confirmation view - else - if api_request? || params[:confirm] - @project_to_destroy.destroy - respond_to do |format| - format.html { redirect_to :controller => 'admin', :action => 'projects' } - format.api { head :ok } - end + if api_request? || params[:confirm] + @project_to_destroy.destroy + respond_to do |format| + format.html { redirect_to :controller => 'admin', :action => 'projects' } + format.api { render_api_ok } end end # hide project in layout @project = nil end -private - def find_optional_project - return true unless params[:id] - @project = Project.find(params[:id]) - authorize - rescue ActiveRecord::RecordNotFound - render_404 - end + private def validate_is_public_key # Although is_public isn't mandatory in the project model (it gets diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/queries_controller.rb --- a/app/controllers/queries_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/queries_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -50,7 +50,6 @@ build_query_from_params end - verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed } def create @query = Query.new(params[:query]) @query.user = User.current @@ -70,7 +69,6 @@ def edit end - verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed } def update @query.attributes = params[:query] @query.project = nil if params[:query_is_for_all] @@ -86,7 +84,6 @@ end end - verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed } def destroy @query.destroy redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/reports_controller.rb --- a/app/controllers/reports_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/reports_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -22,7 +22,7 @@ def issue_report @trackers = @project.trackers @versions = @project.shared_versions.sort - @priorities = IssuePriority.all + @priorities = IssuePriority.all.reverse @categories = @project.issue_categories @assignees = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort @authors = @project.users.sort @@ -53,7 +53,7 @@ @report_title = l(:field_version) when "priority" @field = "priority_id" - @rows = IssuePriority.all + @rows = IssuePriority.all.reverse @data = Issue.by_priority(@project) @report_title = l(:field_priority) when "category" diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/repositories_controller.rb --- a/app/controllers/repositories_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/repositories_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,57 +18,78 @@ require 'SVG/Graph/Bar' require 'SVG/Graph/BarHorizontal' require 'digest/sha1' +require 'redmine/scm/adapters/abstract_adapter' class ChangesetNotFound < Exception; end class InvalidRevisionParam < Exception; end class RepositoriesController < ApplicationController menu_item :repository - menu_item :settings, :only => :edit + menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers] default_search_scope :changesets - before_filter :find_repository, :except => :edit - before_filter :find_project, :only => :edit + before_filter :find_project_by_project_id, :only => [:new, :create] + before_filter :find_repository, :only => [:edit, :update, :destroy, :committers] + before_filter :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers] + before_filter :find_changeset, :only => [:revision, :add_related_issue, :remove_related_issue] before_filter :authorize accept_rss_auth :revisions rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed + def new + scm = params[:repository_scm] || (Redmine::Scm::Base.all & Setting.enabled_scm).first + @repository = Repository.factory(scm) + @repository.is_default = @project.repository.nil? + @repository.project = @project + end + + def create + attrs = pickup_extra_info + @repository = Repository.factory(params[:repository_scm]) + @repository.safe_attributes = params[:repository] + if attrs[:attrs_extra].keys.any? + @repository.merge_extra_info(attrs[:attrs_extra]) + end + @repository.project = @project + if request.post? && @repository.save + redirect_to settings_project_path(@project, :tab => 'repositories') + else + render :action => 'new' + end + end + def edit - @repository = @project.repository + end - if !@repository + def update + params[:repository_scm]='Mercurial' + attrs = pickup_extra_info + @repository.safe_attributes = attrs[:attrs] + if attrs[:attrs_extra].keys.any? + @repository.merge_extra_info(attrs[:attrs_extra]) + end + @repository.project = @project + if request.put? && @repository.save + redirect_to settings_project_path(@project, :tab => 'repositories') + else + render :action => 'edit' + end + end - params[:repository_scm]='Mercurial' - - @repository = Repository.factory(params[:repository_scm]) - @repository.project = @project if @repository - end - if request.post? && @repository - p1 = params[:repository] - p = {} - p_extra = {} - p1.each do |k, v| - if k =~ /^extra_/ - p_extra[k] = v - else - p[k] = v - end - end - @repository.attributes = p - @repository.merge_extra_info(p_extra) - @repository.save - end - - render(:update) do |page| - page.replace_html "tab-content-repository", - :partial => 'projects/settings/repository' - if @repository && !@project.repository - @project.reload # needed to reload association - page.replace_html "main-menu", render_main_menu(@project) + def pickup_extra_info + p = {} + p_extra = {} + params[:repository].each do |k, v| + if k =~ /^extra_/ + p_extra[k] = v + else + p[k] = v end end + {:attrs => p, :attrs_extra => p_extra} end + private :pickup_extra_info def committers @committers = @repository.committers @@ -81,16 +102,13 @@ # Build a hash with repository usernames as keys and corresponding user ids as values @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h} flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'committers', :id => @project + redirect_to settings_project_path(@project, :tab => 'repositories') end end def destroy - @repository.destroy - redirect_to :controller => 'projects', - :action => 'settings', - :id => @project, - :tab => 'repository' + @repository.destroy if request.delete? + redirect_to settings_project_path(@project, :tab => 'repositories') end def show @@ -104,6 +122,7 @@ (show_error_not_found; return) unless @entries @changesets = @repository.latest_changesets(@path, @rev) @properties = @repository.properties(@path, @rev) + @repositories = @project.repositories render :action => 'show' end end @@ -134,7 +153,15 @@ end end + def raw + entry_and_raw(true) + end + def entry + entry_and_raw(false) + end + + def entry_and_raw(is_raw) @entry = @repository.entry(@path, @rev) (show_error_not_found; return) unless @entry @@ -143,11 +170,12 @@ @content = @repository.cat(@path, @rev) (show_error_not_found; return) unless @content - if 'raw' == params[:format] + if is_raw # Force the download send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) } send_type = Redmine::MimeType.of(@path) send_opt[:type] = send_type.to_s if send_type + send_opt[:disposition] = (Redmine::MimeType.is_type?('image', @path) && !is_raw ? 'inline' : 'attachment') send_data @content, send_opt else @display_raw = ((@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) || !is_entry_text_data?(@content, @path)) @@ -158,6 +186,7 @@ @changeset = @repository.find_changeset_by_name(@rev) end end + private :entry_and_raw def is_entry_text_data?(ent, path) # UTF-16 contains "\x00". @@ -190,16 +219,32 @@ end def revision - raise ChangesetNotFound if @rev.blank? - @changeset = @repository.find_changeset_by_name(@rev) - raise ChangesetNotFound unless @changeset - respond_to do |format| format.html format.js {render :layout => false} end - rescue ChangesetNotFound - show_error_not_found + end + + # Adds a related issue to a changeset + # POST /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues + def add_related_issue + @issue = @changeset.find_referenced_issue_by_id(params[:issue_id]) + if @issue && (!@issue.visible? || @changeset.issues.include?(@issue)) + @issue = nil + end + + if @issue + @changeset.issues << @issue + end + end + + # Removes a related issue from a changeset + # DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id + def remove_related_issue + @issue = Issue.visible.find_by_id(params[:issue_id]) + if @issue + @changeset.issues.delete(@issue) + end end def diff @@ -254,14 +299,24 @@ private + def find_repository + @repository = Repository.find(params[:id]) + @project = @repository.project + rescue ActiveRecord::RecordNotFound + render_404 + end + REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i - def find_repository + def find_project_repository @project = Project.find(params[:id]) - @repository = @project.repository + if params[:repository_id].present? + @repository = @project.repositories.find_by_identifier_param(params[:repository_id]) + else + @repository = @project.repository + end (render_404; return false) unless @repository - @path = params[:path].join('/') unless params[:path].nil? - @path ||= '' + @path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip @rev_to = params[:rev_to] @@ -276,6 +331,13 @@ show_error_not_found end + def find_changeset + if @rev.present? + @changeset = @repository.find_changeset_by_name(@rev) + end + show_error_not_found unless @changeset + end + def show_error_not_found render_error :message => l(:error_scm_not_found), :status => 404 end @@ -289,17 +351,17 @@ @date_to = Date.today @date_from = @date_to << 11 @date_from = Date.civil(@date_from.year, @date_from.month, 1) - commits_by_day = repository.changesets.count( + commits_by_day = Changeset.count( :all, :group => :commit_date, - :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to]) + :conditions => ["repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to]) commits_by_month = [0] * 12 - commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last } + commits_by_day.each {|c| commits_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last } - changes_by_day = repository.changes.count( - :all, :group => :commit_date, - :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to]) + changes_by_day = Change.count( + :all, :group => :commit_date, :include => :changeset, + :conditions => ["#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to]) changes_by_month = [0] * 12 - changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last } + changes_by_day.each {|c| changes_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last } fields = [] 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)} @@ -330,10 +392,10 @@ end def graph_commits_per_author(repository) - commits_by_author = repository.changesets.count(:all, :group => :committer) + commits_by_author = Changeset.count(:all, :group => :committer, :conditions => ["repository_id = ?", repository.id]) commits_by_author.to_a.sort! {|x, y| x.last <=> y.last} - changes_by_author = repository.changes.count(:all, :group => :committer) + changes_by_author = Change.count(:all, :group => :committer, :include => :changeset, :conditions => ["#{Changeset.table_name}.repository_id = ?", repository.id]) h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o} fields = commits_by_author.collect {|r| r.first} @@ -369,19 +431,3 @@ graph.burn end end - -class Date - def months_ago(date = Date.today) - (date.year - self.year)*12 + (date.month - self.month) - end - - def weeks_ago(date = Date.today) - (date.year - self.year)*52 + (date.cweek - self.cweek) - end -end - -class String - def with_leading_slash - starts_with?('/') ? self : "/#{self}" - end -end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/roles_controller.rb --- a/app/controllers/roles_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/roles_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,44 +18,66 @@ class RolesController < ApplicationController layout 'admin' - before_filter :require_admin - - verify :method => :post, :only => [ :destroy ], - :redirect_to => { :action => :index } + before_filter :require_admin, :except => [:index, :show] + before_filter :require_admin_or_api_request, :only => [:index, :show] + before_filter :find_role, :only => [:show, :edit, :update, :destroy] + accept_api_auth :index, :show def index - @role_pages, @roles = paginate :roles, :per_page => 25, :order => 'builtin, position' - render :action => "index", :layout => false if request.xhr? + respond_to do |format| + format.html { + @role_pages, @roles = paginate :roles, :per_page => 25, :order => 'builtin, position' + render :action => "index", :layout => false if request.xhr? + } + format.api { + @roles = Role.givable.all + } + end + end + + def show + respond_to do |format| + format.api + end end def new - # Prefills the form with 'Non member' role permissions + # Prefills the form with 'Non member' role permissions by default @role = Role.new(params[:role] || {:permissions => Role.non_member.permissions}) + if params[:copy].present? && @copy_from = Role.find_by_id(params[:copy]) + @role.copy_from(@copy_from) + end + @roles = Role.sorted.all + end + + def create + @role = Role.new(params[:role]) if request.post? && @role.save # workflow copy if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from])) - @role.workflows.copy(copy_from) + @role.workflow_rules.copy(copy_from) end flash[:notice] = l(:notice_successful_create) redirect_to :action => 'index' else - @permissions = @role.setable_permissions - @roles = Role.find :all, :order => 'builtin, position' + @roles = Role.sorted.all + render :action => 'new' end end def edit - @role = Role.find(params[:id]) - if request.post? and @role.update_attributes(params[:role]) + end + + def update + if request.put? and @role.update_attributes(params[:role]) flash[:notice] = l(:notice_successful_update) redirect_to :action => 'index' else - @permissions = @role.setable_permissions + render :action => 'edit' end end def destroy - @role = Role.find(params[:id]) @role.destroy redirect_to :action => 'index' rescue @@ -63,8 +85,8 @@ redirect_to :action => 'index' end - def report - @roles = Role.find(:all, :order => 'builtin, position') + def permissions + @roles = Role.sorted.all @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? } if request.post? @roles.each do |role| @@ -75,4 +97,12 @@ redirect_to :action => 'index' end end + + private + + def find_role + @role = Role.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/search_controller.rb --- a/app/controllers/search_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/search_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -34,7 +34,7 @@ when 'my_projects' User.current.memberships.collect(&:project) when 'subprojects' - @project ? (@project.self_and_descendants.active) : nil + @project ? (@project.self_and_descendants.active.all) : nil else @project end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/settings_controller.rb --- a/app/controllers/settings_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/settings_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,6 +17,7 @@ class SettingsController < ApplicationController layout 'admin' + menu_item :plugins, :only => :plugin before_filter :require_admin @@ -38,7 +39,8 @@ redirect_to :action => 'edit', :tab => params[:tab] else @options = {} - @options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] } + user_format = User::USER_FORMATS.collect{|key, value| [key, value[:setting_order]]}.sort{|a, b| a[1] <=> b[1]} + @options[:user_format] = user_format.collect{|f| [User.current.name(f[0]), f[0].to_s]} @deliveries = ActionMailer::Base.perform_deliveries @guessed_host_and_path = request.host_with_port.dup @@ -51,12 +53,12 @@ def plugin @plugin = Redmine::Plugin.find(params[:id]) if request.post? - Setting["plugin_#{@plugin.id}"] = params[:settings] + Setting.send "plugin_#{@plugin.id}=", params[:settings] flash[:notice] = l(:notice_successful_update) redirect_to :action => 'plugin', :id => @plugin.id else @partial = @plugin.settings[:partial] - @settings = Setting["plugin_#{@plugin.id}"] + @settings = Setting.send "plugin_#{@plugin.id}" end rescue Redmine::PluginNotFound render_404 diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/sys_controller.rb --- a/app/controllers/sys_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/sys_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -19,9 +19,16 @@ before_filter :check_enabled def projects - p = Project.active.has_module(:repository).find(:all, :include => :repository, :order => 'identifier') + p = Project.active.has_module(:repository).find( + :all, + :include => :repository, + :order => "#{Project.table_name}.identifier" + ) # extra_info attribute from repository breaks activeresource client - render :xml => p.to_xml(:only => [:id, :identifier, :name, :is_public, :status], :include => {:repository => {:only => [:id, :url, :is_external, :external_url]}}) + render :xml => p.to_xml( + :only => [:id, :identifier, :name, :is_public, :status], + :include => {:repository => {:only => [:id, :url, :is_external, :external_url]}} + ) end def create_project_repository @@ -30,9 +37,10 @@ render :nothing => true, :status => 409 else logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}." - project.repository = Repository.factory(params[:vendor], params[:repository]) - if project.repository && project.repository.save - render :xml => project.repository.to_xml(:only => [:id, :url]), :status => 201 + repository = Repository.factory(params[:vendor], params[:repository]) + repository.project = project + if repository.save + render :xml => {repository.class.name.underscore.gsub('/', '-') => {:id => repository.id, :url => repository.url}}, :status => 201 else render :nothing => true, :status => 422 end @@ -41,14 +49,22 @@ def fetch_changesets projects = [] + scope = Project.active.has_module(:repository) if params[:id] - projects << Project.active.has_module(:repository).find(params[:id]) + project = nil + if params[:id].to_s =~ /^\d*$/ + project = scope.find(params[:id]) + else + project = scope.find_by_identifier(params[:id]) + end + raise ActiveRecord::RecordNotFound unless project + projects << project else - projects = Project.active.has_module(:repository).find(:all, :include => :repository) + projects = scope.all end projects.each do |project| - if project.repository - project.repository.fetch_changesets + project.repositories.each do |repository| + repository.fetch_changesets end end render :nothing => true, :status => 200 diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/time_entry_reports_controller.rb --- a/app/controllers/time_entry_reports_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,209 +0,0 @@ -class TimeEntryReportsController < ApplicationController - menu_item :issues - before_filter :find_optional_project - before_filter :load_available_criterias - - helper :sort - include SortHelper - helper :issues - helper :timelog - include TimelogHelper - helper :custom_fields - include CustomFieldsHelper - - def report - @criterias = params[:criterias] || [] - @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria} - @criterias.uniq! - @criterias = @criterias[0,3] - - @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month' - - retrieve_date_range - - unless @criterias.empty? - sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ') - sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ') - sql_condition = '' - - if @project.nil? - sql_condition = Project.allowed_to_condition(User.current, :view_time_entries) - elsif @issue.nil? - sql_condition = @project.project_condition(Setting.display_subprojects_issues?) - else - sql_condition = "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}" - end - - sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours" - sql << " FROM #{TimeEntry.table_name}" - sql << time_report_joins - sql << " WHERE" - sql << " (%s) AND" % sql_condition - sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)] - sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on" - - @hours = ActiveRecord::Base.connection.select_all(sql) - - @hours.each do |row| - case @columns - when 'year' - row['year'] = row['tyear'] - when 'month' - row['month'] = "#{row['tyear']}-#{row['tmonth']}" - when 'week' - row['week'] = "#{row['tyear']}-#{row['tweek']}" - when 'day' - row['day'] = "#{row['spent_on']}" - end - end - - @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f} - - @periods = [] - # Date#at_beginning_of_ not supported in Rails 1.2.x - date_from = @from.to_time - # 100 columns max - while date_from <= @to.to_time && @periods.length < 100 - case @columns - when 'year' - @periods << "#{date_from.year}" - date_from = (date_from + 1.year).at_beginning_of_year - when 'month' - @periods << "#{date_from.year}-#{date_from.month}" - date_from = (date_from + 1.month).at_beginning_of_month - when 'week' - @periods << "#{date_from.year}-#{date_from.to_date.cweek}" - date_from = (date_from + 7.day).at_beginning_of_week - when 'day' - @periods << "#{date_from.to_date}" - date_from = date_from + 1.day - end - end - end - - respond_to do |format| - format.html { render :layout => !request.xhr? } - format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') } - end - end - - private - - # TODO: duplicated in TimelogController - def find_optional_project - if !params[:issue_id].blank? - @issue = Issue.find(params[:issue_id]) - @project = @issue.project - elsif !params[:project_id].blank? - @project = Project.find(params[:project_id]) - end - deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true) - end - - # Retrieves the date range based on predefined ranges or specific from/to param dates - # TODO: duplicated in TimelogController - def retrieve_date_range - @free_period = false - @from, @to = nil, nil - - if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?) - case params[:period].to_s - when 'today' - @from = @to = Date.today - when 'yesterday' - @from = @to = Date.today - 1 - when 'current_week' - @from = Date.today - (Date.today.cwday - 1)%7 - @to = @from + 6 - when 'last_week' - @from = Date.today - 7 - (Date.today.cwday - 1)%7 - @to = @from + 6 - when '7_days' - @from = Date.today - 7 - @to = Date.today - when 'current_month' - @from = Date.civil(Date.today.year, Date.today.month, 1) - @to = (@from >> 1) - 1 - when 'last_month' - @from = Date.civil(Date.today.year, Date.today.month, 1) << 1 - @to = (@from >> 1) - 1 - when '30_days' - @from = Date.today - 30 - @to = Date.today - when 'current_year' - @from = Date.civil(Date.today.year, 1, 1) - @to = Date.civil(Date.today.year, 12, 31) - end - elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?)) - begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end - begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end - @free_period = true - else - # default - end - - @from, @to = @to, @from if @from && @to && @from > @to - @from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today) - @to ||= (TimeEntry.latest_date_for_project(@project) || Date.today) - end - - def load_available_criterias - @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id", - :klass => Project, - :label => :label_project}, - 'version' => {:sql => "#{Issue.table_name}.fixed_version_id", - :klass => Version, - :label => :label_version}, - 'category' => {:sql => "#{Issue.table_name}.category_id", - :klass => IssueCategory, - :label => :field_category}, - 'member' => {:sql => "#{TimeEntry.table_name}.user_id", - :klass => User, - :label => :label_member}, - 'tracker' => {:sql => "#{Issue.table_name}.tracker_id", - :klass => Tracker, - :label => :label_tracker}, - 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id", - :klass => TimeEntryActivity, - :label => :label_activity}, - 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id", - :klass => Issue, - :label => :label_issue} - } - - # Add list and boolean custom fields as available criterias - custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields) - custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf| - @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)", - :format => cf.field_format, - :label => cf.name} - end if @project - - # Add list and boolean time entry custom fields - TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf| - @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)", - :format => cf.field_format, - :label => cf.name} - end - - # Add list and boolean time entry activity custom fields - TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf| - @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)", - :format => cf.field_format, - :label => cf.name} - end - - call_hook(:controller_timelog_available_criterias, { :available_criterias => @available_criterias, :project => @project }) - @available_criterias - end - - def time_report_joins - sql = '' - sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id" - sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id" - # TODO: rename hook - call_hook(:controller_timelog_time_report_joins, {:sql => sql} ) - sql - end - -end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/timelog_controller.rb --- a/app/controllers/timelog_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/timelog_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,11 +17,16 @@ class TimelogController < ApplicationController menu_item :issues - before_filter :find_project, :only => [:new, :create] + + before_filter :find_project_for_new_time_entry, :only => [:create] before_filter :find_time_entry, :only => [:show, :edit, :update] before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy] - before_filter :authorize, :except => [:index] - before_filter :find_optional_project, :only => [:index] + before_filter :authorize, :except => [:new, :index, :report] + + before_filter :find_optional_project, :only => [:index, :report] + before_filter :find_optional_project_for_new_time_entry, :only => [:new] + before_filter :authorize_global, :only => [:new, :index, :report] + accept_rss_auth :index accept_api_auth :index, :show, :create, :update, :destroy @@ -34,67 +39,76 @@ def index sort_init 'spent_on', 'desc' - sort_update 'spent_on' => 'spent_on', + sort_update 'spent_on' => ['spent_on', "#{TimeEntry.table_name}.created_on"], 'user' => 'user_id', 'activity' => 'activity_id', 'project' => "#{Project.table_name}.name", 'issue' => 'issue_id', 'hours' => 'hours' - cond = ARCondition.new + retrieve_date_range + + scope = TimeEntry.visible.spent_between(@from, @to) if @issue - cond << "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}" + scope = scope.on_issue(@issue) elsif @project - cond << @project.project_condition(Setting.display_subprojects_issues?) + scope = scope.on_project(@project, Setting.display_subprojects_issues?) end - retrieve_date_range - cond << ['spent_on BETWEEN ? AND ?', @from, @to] - respond_to do |format| format.html { # Paginate results - @entry_count = TimeEntry.visible.count(:include => [:project, :issue], :conditions => cond.conditions) + @entry_count = scope.count @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page'] - @entries = TimeEntry.visible.find(:all, - :include => [:project, :activity, :user, {:issue => :tracker}], - :conditions => cond.conditions, - :order => sort_clause, - :limit => @entry_pages.items_per_page, - :offset => @entry_pages.current.offset) - @total_hours = TimeEntry.visible.sum(:hours, :include => [:project, :issue], :conditions => cond.conditions).to_f + @entries = scope.all( + :include => [:project, :activity, :user, {:issue => :tracker}], + :order => sort_clause, + :limit => @entry_pages.items_per_page, + :offset => @entry_pages.current.offset + ) + @total_hours = scope.sum(:hours).to_f render :layout => !request.xhr? } format.api { - @entry_count = TimeEntry.visible.count(:include => [:project, :issue], :conditions => cond.conditions) + @entry_count = scope.count @offset, @limit = api_offset_and_limit - @entries = TimeEntry.visible.find(:all, - :include => [:project, :activity, :user, {:issue => :tracker}], - :conditions => cond.conditions, - :order => sort_clause, - :limit => @limit, - :offset => @offset) + @entries = scope.all( + :include => [:project, :activity, :user, {:issue => :tracker}], + :order => sort_clause, + :limit => @limit, + :offset => @offset + ) } format.atom { - entries = TimeEntry.visible.find(:all, - :include => [:project, :activity, :user, {:issue => :tracker}], - :conditions => cond.conditions, - :order => "#{TimeEntry.table_name}.created_on DESC", - :limit => Setting.feeds_limit.to_i) + entries = scope.all( + :include => [:project, :activity, :user, {:issue => :tracker}], + :order => "#{TimeEntry.table_name}.created_on DESC", + :limit => Setting.feeds_limit.to_i + ) render_feed(entries, :title => l(:label_spent_time)) } format.csv { # Export all entries - @entries = TimeEntry.visible.find(:all, - :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], - :conditions => cond.conditions, - :order => sort_clause) + @entries = scope.all( + :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], + :order => sort_clause + ) send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv') } end end + def report + retrieve_date_range + @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], @from, @to) + + respond_to do |format| + format.html { render :layout => !request.xhr? } + format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') } + end + end + def show respond_to do |format| # TODO: Implement html response @@ -106,12 +120,8 @@ def new @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today) @time_entry.safe_attributes = params[:time_entry] - - call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry }) - render :action => 'edit' end - verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed } def create @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today) @time_entry.safe_attributes = params[:time_entry] @@ -121,14 +131,26 @@ if @time_entry.save respond_to do |format| format.html { - flash[:notice] = l(:notice_successful_update) - redirect_back_or_default :action => 'index', :project_id => @time_entry.project + flash[:notice] = l(:notice_successful_create) + if params[:continue] + if params[:project_id] + redirect_to :action => 'new', :project_id => @time_entry.project, :issue_id => @time_entry.issue, + :time_entry => {:issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id}, + :back_url => params[:back_url] + else + redirect_to :action => 'new', + :time_entry => {:project_id => @time_entry.project_id, :issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id}, + :back_url => params[:back_url] + end + else + redirect_back_or_default :action => 'index', :project_id => @time_entry.project + end } format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) } end else respond_to do |format| - format.html { render :action => 'edit' } + format.html { render :action => 'new' } format.api { render_validation_errors(@time_entry) } end end @@ -136,11 +158,8 @@ def edit @time_entry.safe_attributes = params[:time_entry] - - call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry }) end - verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed } def update @time_entry.safe_attributes = params[:time_entry] @@ -152,7 +171,7 @@ flash[:notice] = l(:notice_successful_update) redirect_back_or_default :action => 'index', :project_id => @time_entry.project } - format.api { head :ok } + format.api { render_api_ok } end else respond_to do |format| @@ -184,32 +203,31 @@ redirect_back_or_default({:controller => 'timelog', :action => 'index', :project_id => @projects.first}) end - verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed } def destroy - @time_entries.each do |t| - begin + destroyed = TimeEntry.transaction do + @time_entries.each do |t| unless t.destroy && t.destroyed? - respond_to do |format| - format.html { - flash[:error] = l(:notice_unable_delete_time_entry) - redirect_to :back - } - format.api { render_validation_errors(t) } - end - return + raise ActiveRecord::Rollback end - rescue ::ActionController::RedirectBackError - redirect_to :action => 'index', :project_id => @projects.first - return end end respond_to do |format| format.html { - flash[:notice] = l(:notice_successful_delete) + if destroyed + flash[:notice] = l(:notice_successful_delete) + else + flash[:error] = l(:notice_unable_delete_time_entry) + end redirect_back_or_default(:action => 'index', :project_id => @projects.first) } - format.api { head :ok } + format.api { + if destroyed + render_api_ok + else + render_validation_errors(@time_entries) + end + } end end @@ -245,20 +263,25 @@ end end - def find_project + def find_optional_project_for_new_time_entry + if (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present? + @project = Project.find(project_id) + end if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present? @issue = Issue.find(issue_id) - @project = @issue.project - elsif (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present? - @project = Project.find(project_id) - else - render_404 - return false + @project ||= @issue.project end rescue ActiveRecord::RecordNotFound render_404 end + def find_project_for_new_time_entry + find_optional_project_for_new_time_entry + if @project.nil? + render_404 + end + end + def find_optional_project if !params[:issue_id].blank? @issue = Issue.find(params[:issue_id]) @@ -266,7 +289,6 @@ elsif !params[:project_id].blank? @project = Project.find(params[:project_id]) end - deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true) end # Retrieves the date range based on predefined ranges or specific from/to param dates @@ -286,6 +308,9 @@ when 'last_week' @from = Date.today - 7 - (Date.today.cwday - 1)%7 @to = @from + 6 + when 'last_2_weeks' + @from = Date.today - 14 - (Date.today.cwday - 1)%7 + @to = @from + 13 when '7_days' @from = Date.today - 7 @to = Date.today @@ -311,8 +336,6 @@ end @from, @to = @to, @from if @from && @to && @from > @to - @from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today) - @to ||= (TimeEntry.latest_date_for_project(@project) || Date.today) end def parse_params_for_bulk_time_entry_attributes(params) diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/trackers_controller.rb --- a/app/controllers/trackers_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/trackers_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -29,7 +29,7 @@ render :action => "index", :layout => false if request.xhr? } format.api { - @trackers = Tracker.all + @trackers = Tracker.sorted.all } end end @@ -45,7 +45,7 @@ if request.post? and @tracker.save # workflow copy if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from])) - @tracker.workflows.copy(copy_from) + @tracker.workflow_rules.copy(copy_from) end flash[:notice] = l(:notice_successful_create) redirect_to :action => 'index' @@ -59,7 +59,7 @@ @tracker ||= Tracker.find(params[:id]) @projects = Project.find(:all) end - + def update @tracker = Tracker.find(params[:id]) if request.put? and @tracker.update_attributes(params[:tracker]) @@ -71,7 +71,6 @@ render :action => 'edit' end - verify :method => :delete, :only => :destroy, :redirect_to => { :action => :index } def destroy @tracker = Tracker.find(params[:id]) unless @tracker.issues.empty? @@ -81,4 +80,22 @@ end redirect_to :action => 'index' end + + def fields + if request.post? && params[:trackers] + params[:trackers].each do |tracker_id, tracker_params| + tracker = Tracker.find_by_id(tracker_id) + if tracker + tracker.core_fields = tracker_params[:core_fields] + tracker.custom_field_ids = tracker_params[:custom_field_ids] + tracker.save + end + end + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'fields' + return + end + @trackers = Tracker.sorted.all + @custom_fields = IssueCustomField.all.sort + end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/users_controller.rb --- a/app/controllers/users_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/users_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -38,23 +38,17 @@ @limit = per_page_option end - scope = User - scope = scope.in_group(params[:group_id].to_i) if params[:group_id].present? + @status = params[:status] || 1 - @status = params[:status] ? params[:status].to_i : 1 - c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status]) + scope = User.logged.status(@status) + scope = scope.like(params[:name]) if params[:name].present? + scope = scope.in_group(params[:group_id]) if params[:group_id].present? - unless params[:name].blank? - name = "%#{params[:name].strip.downcase}%" - c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ? OR LOWER(mail) LIKE ?", name, name, name, name] - end - - @user_count = scope.count(:conditions => c.conditions) + @user_count = scope.count @user_pages = Paginator.new self, @user_count, @limit, params['page'] @offset ||= @user_pages.current.offset @users = scope.find :all, :order => sort_clause, - :conditions => c.conditions, :limit => @limit, :offset => @offset @@ -100,7 +94,6 @@ @ssamr_user_details = SsamrUserDetail.new end - verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed } def create @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option) @user.safe_attributes = params[:user] @@ -119,16 +112,18 @@ @user.ssamr_user_detail = @ssamr_user_details if @user.save + @user.pref.attributes = params[:pref] + @user.pref[:no_self_notified] = (params[:no_self_notified] == '1') @user.pref.save @ssamr_user_details.save! - Mailer.deliver_account_information(@user, params[:user][:password]) if params[:send_information] + Mailer.account_information(@user, params[:user][:password]).deliver if params[:send_information] respond_to do |format| format.html { - flash[:notice] = l(:notice_successful_create) + flash[:notice] = l(:notice_user_successful_create, :id => view_context.link_to(@user.login, user_path(@user))) redirect_to(params[:continue] ? {:controller => 'users', :action => 'new'} : {:controller => 'users', :action => 'edit', :id => @user} @@ -162,7 +157,6 @@ @membership ||= Member.new end - verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed } def update @user.admin = params[:user][:admin] if params[:user][:admin] @user.login = params[:user][:login] if params[:user][:login] @@ -201,17 +195,17 @@ @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : []) if was_activated - Mailer.deliver_account_activated(@user) + Mailer.account_activated(@user).deliver elsif @user.active? && params[:send_information] && !params[:user][:password].blank? && @user.auth_source_id.nil? - Mailer.deliver_account_information(@user, params[:user][:password]) + Mailer.account_information(@user, params[:user][:password]).deliver end respond_to do |format| format.html { flash[:notice] = l(:notice_successful_update) - redirect_to :back + redirect_to_referer_or edit_user_path(@user) } - format.api { head :ok } + format.api { render_api_ok } end else @auth_sources = AuthSource.find(:all) @@ -224,49 +218,33 @@ format.api { render_validation_errors(@user) } end end - rescue ::ActionController::RedirectBackError - redirect_to :controller => 'users', :action => 'edit', :id => @user end - verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed } def destroy @user.destroy respond_to do |format| - format.html { redirect_to(users_url) } - format.api { head :ok } + format.html { redirect_back_or_default(users_url) } + format.api { render_api_ok } end end def edit_membership @membership = Member.edit_membership(params[:membership_id], params[:membership], @user) - @membership.save if request.post? + @membership.save respond_to do |format| - if @membership.valid? - format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' } - format.js { - render(:update) {|page| - page.replace_html "tab-content-memberships", :partial => 'users/memberships' - page.visual_effect(:highlight, "member-#{@membership.id}") - } - } - else - format.js { - render(:update) {|page| - page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', '))) - } - } - end + format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' } + format.js end end def destroy_membership @membership = Member.find(params[:membership_id]) - if request.post? && @membership.deletable? + if @membership.deletable? @membership.destroy end respond_to do |format| format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' } - format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} } + format.js end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/versions_controller.rb --- a/app/controllers/versions_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/versions_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -20,7 +20,7 @@ model_object Version before_filter :find_model_object, :except => [:index, :new, :create, :close_completed] before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed] - before_filter :find_project, :only => [:index, :new, :create, :close_completed] + before_filter :find_project_by_project_id, :only => [:index, :new, :create, :close_completed] before_filter :authorize accept_api_auth :index, :show, :create, :update, :destroy @@ -39,17 +39,19 @@ @versions = @project.shared_versions || [] @versions += @project.rolled_up_versions.visible if @with_subprojects @versions = @versions.uniq.sort - @versions.reject! {|version| version.closed? || version.completed? } unless params[:completed] + unless params[:completed] + @completed_versions = @versions.select {|version| version.closed? || version.completed? } + @versions -= @completed_versions + end @issues_by_version = {} - unless @selected_tracker_ids.empty? - @versions.each do |version| - issues = version.fixed_issues.visible.find(:all, - :include => [:project, :status, :tracker, :priority], - :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids}, - :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id") - @issues_by_version[version] = issues - end + if @selected_tracker_ids.any? && @versions.any? + issues = Issue.visible.all( + :include => [:project, :status, :tracker, :priority, :fixed_version], + :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids, :fixed_version_id => @versions.map(&:id)}, + :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id" + ) + @issues_by_version = issues.group_by(&:fixed_version) end @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?} } @@ -72,15 +74,15 @@ def new @version = @project.versions.build - if params[:version] - attributes = params[:version].dup - attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing']) - @version.safe_attributes = attributes + @version.safe_attributes = params[:version] + + respond_to do |format| + format.html + format.js end end def create - # TODO: refactor with code above in #new @version = @project.versions.build if params[:version] attributes = params[:version].dup @@ -95,12 +97,7 @@ flash[:notice] = l(:notice_successful_create) redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project end - format.js do - # IE doesn't support the replace_html rjs method for select box options - render(:update) {|page| page.replace "issue_fixed_version_id", - content_tag('select', '' + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]') - } - end + format.js format.api do render :action => 'show', :status => :created, :location => version_url(@version) end @@ -108,9 +105,7 @@ else respond_to do |format| format.html { render :action => 'new' } - format.js do - render(:update) {|page| page.alert(@version.errors.full_messages.join('\n')) } - end + format.js { render :action => 'new' } format.api { render_validation_errors(@version) } end end @@ -131,7 +126,7 @@ flash[:notice] = l(:notice_successful_update) redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project } - format.api { head :ok } + format.api { render_api_ok } end else respond_to do |format| @@ -149,13 +144,12 @@ redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project end - verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed } def destroy if @version.fixed_issues.empty? @version.destroy respond_to do |format| format.html { redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project } - format.api { head :ok } + format.api { render_api_ok } end else respond_to do |format| @@ -171,16 +165,11 @@ def status_by respond_to do |format| format.html { render :action => 'show' } - format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} } + format.js end end -private - def find_project - @project = Project.find(params[:project_id]) - rescue ActiveRecord::RecordNotFound - render_404 - end + private def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil) if ids = params[:tracker_ids] @@ -189,5 +178,4 @@ @selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s } end end - end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/watchers_controller.rb --- a/app/controllers/watchers_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/watchers_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -20,10 +20,6 @@ before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch] before_filter :authorize, :only => [:new, :destroy] - verify :method => :post, - :only => [ :watch, :unwatch ], - :render => { :nothing => true, :status => :method_not_allowed } - def watch if @watched.respond_to?(:visible?) && !@watched.visible?(User.current) render_403 @@ -37,39 +33,54 @@ end def new - @watcher = Watcher.new(params[:watcher]) - @watcher.watchable = @watched - @watcher.save if request.post? - respond_to do |format| - format.html { redirect_to :back } - format.js do - render :update do |page| - page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched} - end + end + + def create + if params[:watcher].is_a?(Hash) && request.post? + user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]] + user_ids.each do |user_id| + Watcher.create(:watchable => @watched, :user_id => user_id) end end - rescue ::ActionController::RedirectBackError - render :text => 'Watcher added.', :layout => true + respond_to do |format| + format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}} + format.js + end + end + + def append + if params[:watcher].is_a?(Hash) + user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]] + @users = User.active.find_all_by_id(user_ids) + end end def destroy @watched.set_watcher(User.find(params[:user_id]), false) if request.post? respond_to do |format| format.html { redirect_to :back } - format.js do - render :update do |page| - page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched} - end - end + format.js end end + def autocomplete_for_user + @users = User.active.like(params[:q]).find(:all, :limit => 100) + if @watched + @users -= @watched.watcher_users + end + render :layout => false + end + private def find_project - klass = Object.const_get(params[:object_type].camelcase) - return false unless klass.respond_to?('watched_by') - @watched = klass.find(params[:object_id]) - @project = @watched.project + if params[:object_type] && params[:object_id] + klass = Object.const_get(params[:object_type].camelcase) + return false unless klass.respond_to?('watched_by') + @watched = klass.find(params[:object_id]) + @project = @watched.project + elsif params[:project_id] + @project = Project.visible.find_by_param(params[:project_id]) + end rescue render_404 end @@ -77,17 +88,8 @@ def set_watcher(user, watching) @watched.set_watcher(user, watching) respond_to do |format| - format.html { redirect_to :back } - format.js do - render(:update) do |page| - c = watcher_css(@watched) - page.select(".#{c}").each do |item| - page.replace_html item, watcher_link(@watched, user) - end - end - end + format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}} + format.js { render :partial => 'set_watcher', :locals => {:user => user, :watched => @watched} } end - rescue ::ActionController::RedirectBackError - render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/welcome_controller.rb --- a/app/controllers/welcome_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/welcome_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/wiki_controller.rb --- a/app/controllers/wiki_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/wiki_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -35,7 +35,8 @@ default_search_scope :wiki_pages before_filter :find_wiki, :authorize before_filter :find_existing_or_new_page, :only => [:show, :edit, :update] - before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy] + before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version] + accept_api_auth :index, :show, :update, :destroy helper :attachments include AttachmentsHelper @@ -45,7 +46,13 @@ # List of pages, sorted alphabetically and by parent (hierarchy) def index load_pages_for_index - @pages_by_parent_id = @pages.group_by(&:parent_id) + + respond_to do |format| + format.html { + @pages_by_parent_id = @pages.group_by(&:parent_id) + } + format.api + end end # List of page, by last update @@ -57,7 +64,7 @@ # display a page (in editing mode if it doesn't exist) def show if @page.new_record? - if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? + if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request? edit render :action => 'edit' else @@ -66,14 +73,13 @@ return end if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project) - # Redirects user to the current version if he's not allowed to view previous versions - redirect_to :version => nil + deny_access return end @content = @page.content_for_version(params[:version]) if User.current.allowed_to?(:export_wiki_pages, @project) if params[:format] == 'pdf' - send_data(wiki_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf") + send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf") return elsif params[:format] == 'html' export = render_to_string :action => 'export', :layout => false @@ -89,13 +95,21 @@ @content.current_version? && Redmine::WikiFormatting.supports_section_edit? - render :action => 'show' + respond_to do |format| + format.html + format.api + end end # edit an existing page or a new one def edit return render_403 unless editable? - @page.content = WikiContent.new(:page => @page) if @page.new_record? + if @page.new_record? + @page.content = WikiContent.new(:page => @page) + if params[:parent].present? + @page.parent = @page.wiki.find_page(params[:parent].to_s) + end + end @content = @page.content_for_version(params[:version]) @content.text = initial_page_content(@page) if @content.text.blank? @@ -104,7 +118,7 @@ # To prevent StaleObjectError exception when reverting to a previous version @content.version = @page.content.version - + @text = @content.text if params[:section].present? && Redmine::WikiFormatting.supports_section_edit? @section = params[:section].to_i @@ -113,50 +127,68 @@ end end - verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed } # Creates a new page or updates an existing one def update return render_403 unless editable? + was_new_page = @page.new_record? @page.content = WikiContent.new(:page => @page) if @page.new_record? + @page.safe_attributes = params[:wiki_page] - @content = @page.content_for_version(params[:version]) - @content.text = initial_page_content(@page) if @content.text.blank? - # don't keep previous comment - @content.comments = nil + @content = @page.content + content_params = params[:content] + if content_params.nil? && params[:wiki_page].is_a?(Hash) + content_params = params[:wiki_page].slice(:text, :comments, :version) + end + content_params ||= {} - if !@page.new_record? && params[:content].present? && @content.text == params[:content][:text] - attachments = Attachment.attach_files(@page, params[:attachments]) - render_attachment_warning_if_needed(@page) - # don't save if text wasn't changed - redirect_to :action => 'show', :project_id => @project, :id => @page.title - return - end - - @content.comments = params[:content][:comments] - @text = params[:content][:text] + @content.comments = content_params[:comments] + @text = content_params[:text] if params[:section].present? && Redmine::WikiFormatting.supports_section_edit? @section = params[:section].to_i @section_hash = params[:section_hash] @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash) else - @content.version = params[:content][:version] + @content.version = content_params[:version] if content_params[:version] @content.text = @text end @content.author = User.current - # if page is new @page.save will also save content, but not if page isn't a new record - if (@page.new_record? ? @page.save : @content.save) + + if @page.save_with_content attachments = Attachment.attach_files(@page, params[:attachments]) render_attachment_warning_if_needed(@page) call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page}) - redirect_to :action => 'show', :project_id => @project, :id => @page.title + + respond_to do |format| + format.html { redirect_to :action => 'show', :project_id => @project, :id => @page.title } + format.api { + if was_new_page + render :action => 'show', :status => :created, :location => url_for(:controller => 'wiki', :action => 'show', :project_id => @project, :id => @page.title) + else + render_api_ok + end + } + end else - render :action => 'edit' + respond_to do |format| + format.html { render :action => 'edit' } + format.api { render_validation_errors(@content) } + end end rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError # Optimistic locking exception - flash.now[:error] = l(:notice_locking_conflict) - render :action => 'edit' + respond_to do |format| + format.html { + flash.now[:error] = l(:notice_locking_conflict) + render :action => 'edit' + } + format.api { render_api_head :conflict } + end + rescue ActiveRecord::RecordNotSaved + respond_to do |format| + format.html { render :action => 'edit' } + format.api { render_validation_errors(@content) } + end end # rename a page @@ -171,7 +203,6 @@ end end - verify :method => :post, :only => :protect, :redirect_to => { :action => :show } def protect @page.update_attribute :protected, params[:protected] redirect_to :action => 'show', :project_id => @project, :id => @page.title @@ -180,7 +211,7 @@ # show page history def history @version_count = @page.content.versions.count - @version_pages = Paginator.new self, @version_count, per_page_option, params['p'] + @version_pages = Paginator.new self, @version_count, per_page_option, params['page'] # don't load text @versions = @page.content.versions.find :all, :select => "id, author_id, comments, updated_on, version", @@ -201,7 +232,6 @@ render_404 unless @annotate end - verify :method => :delete, :only => [:destroy], :redirect_to => { :action => :show } # Removes a wiki page and its history # Children can be either set as root pages, removed or reassigned to another parent page def destroy @@ -224,21 +254,36 @@ end else @reassignable_to = @wiki.pages - @page.self_and_descendants - return + # display the destroy form if it's a user request + return unless api_request? end end @page.destroy - redirect_to :action => 'index', :project_id => @project + respond_to do |format| + format.html { redirect_to :action => 'index', :project_id => @project } + format.api { render_api_ok } + end end - # Export wiki to a single html file + def destroy_version + return render_403 unless editable? + + @content = @page.content_for_version(params[:version]) + @content.destroy + redirect_to_referer_or :action => 'history', :id => @page.title, :project_id => @project + end + + # Export wiki to a single pdf or html file def export - if User.current.allowed_to?(:export_wiki_pages, @project) - @pages = @wiki.pages.find :all, :order => 'title' - export = render_to_string :action => 'export_multiple', :layout => false - send_data(export, :type => 'text/html', :filename => "wiki.html") - else - redirect_to :action => 'show', :project_id => @project, :id => nil + @pages = @wiki.pages.all(:order => 'title', :include => [:content, {:attachments => :author}]) + respond_to do |format| + format.html { + export = render_to_string :action => 'export_multiple', :layout => false + send_data(export, :type => 'text/html', :filename => "wiki.html") + } + format.pdf { + send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf") + } end end @@ -304,6 +349,6 @@ end def load_pages_for_index - @pages = @wiki.pages.with_updated_on.all(:order => 'title', :include => {:wiki => :project}) + @pages = @wiki.pages.with_updated_on.order("#{WikiPage.table_name}.title").includes(:wiki => :project).includes(:parent).all end end diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/wikis_controller.rb --- a/app/controllers/wikis_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/wikis_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -24,7 +24,6 @@ @wiki = @project.wiki || Wiki.new(:project => @project) @wiki.safe_attributes = params[:wiki] @wiki.save if request.post? - render(:update) {|page| page.replace_html "tab-content-wiki", :partial => 'projects/settings/wiki'} end # Delete a project's wiki diff -r ab89f95ef405 -r b2ea0641f798 app/controllers/workflows_controller.rb --- a/app/controllers/workflows_controller.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/controllers/workflows_controller.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,30 +18,27 @@ class WorkflowsController < ApplicationController layout 'admin' - before_filter :require_admin - before_filter :find_roles - before_filter :find_trackers + before_filter :require_admin, :find_roles, :find_trackers def index - @workflow_counts = Workflow.count_by_tracker_and_role + @workflow_counts = WorkflowTransition.count_by_tracker_and_role end def edit - @role = Role.find_by_id(params[:role_id]) - @tracker = Tracker.find_by_id(params[:tracker_id]) + @role = Role.find_by_id(params[:role_id]) if params[:role_id] + @tracker = Tracker.find_by_id(params[:tracker_id]) if params[:tracker_id] if request.post? - Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id]) + WorkflowTransition.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id]) (params[:issue_status] || []).each { |status_id, transitions| transitions.each { |new_status_id, options| author = options.is_a?(Array) && options.include?('author') && !options.include?('always') assignee = options.is_a?(Array) && options.include?('assignee') && !options.include?('always') - @role.workflows.build(:tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee) + WorkflowTransition.create(:role_id => @role.id, :tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee) } } if @role.save - flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker + redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker, :used_statuses_only => params[:used_statuses_only] return end end @@ -50,10 +47,10 @@ if @tracker && @used_statuses_only && @tracker.issue_statuses.any? @statuses = @tracker.issue_statuses end - @statuses ||= IssueStatus.find(:all, :order => 'position') + @statuses ||= IssueStatus.sorted.all if @tracker && @role && @statuses.any? - workflows = Workflow.all(:conditions => {:role_id => @role.id, :tracker_id => @tracker.id}) + workflows = WorkflowTransition.where(:role_id => @role.id, :tracker_id => @tracker.id).all @workflows = {} @workflows['always'] = workflows.select {|w| !w.author && !w.assignee} @workflows['author'] = workflows.select {|w| w.author} @@ -61,6 +58,35 @@ end end + def permissions + @role = Role.find_by_id(params[:role_id]) if params[:role_id] + @tracker = Tracker.find_by_id(params[:tracker_id]) if params[:tracker_id] + + if request.post? && @role && @tracker + WorkflowPermission.replace_permissions(@tracker, @role, params[:permissions] || {}) + redirect_to :action => 'permissions', :role_id => @role, :tracker_id => @tracker, :used_statuses_only => params[:used_statuses_only] + return + end + + @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true) + if @tracker && @used_statuses_only && @tracker.issue_statuses.any? + @statuses = @tracker.issue_statuses + end + @statuses ||= IssueStatus.sorted.all + + if @role && @tracker + @fields = (Tracker::CORE_FIELDS_ALL - @tracker.disabled_core_fields).map {|field| [field, l("field_"+field.sub(/_id$/, ''))]} + @custom_fields = @tracker.custom_fields + + @permissions = WorkflowPermission.where(:tracker_id => @tracker.id, :role_id => @role.id).all.inject({}) do |h, w| + h[w.old_status_id] ||= {} + h[w.old_status_id][w.field_name] = w.rule + h + end + @statuses.each {|status| @permissions[status.id] ||= {}} + end + end + def copy if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any' @@ -83,7 +109,7 @@ elsif @target_trackers.nil? || @target_roles.nil? flash.now[:error] = l(:error_workflow_copy_target) else - Workflow.copy(@source_tracker, @source_role, @target_trackers, @target_roles) + WorkflowRule.copy(@source_tracker, @source_role, @target_trackers, @target_roles) flash[:notice] = l(:notice_successful_update) redirect_to :action => 'copy', :source_tracker_id => @source_tracker, :source_role_id => @source_role end @@ -93,10 +119,10 @@ private def find_roles - @roles = Role.find(:all, :order => 'builtin, position') + @roles = Role.sorted.all end def find_trackers - @trackers = Tracker.find(:all, :order => 'position') + @trackers = Tracker.sorted.all end end diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/account_helper.rb --- a/app/helpers/account_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/account_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/admin_helper.rb --- a/app/helpers/admin_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/admin_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -20,6 +20,8 @@ module AdminHelper def project_status_options_for_select(selected) options_for_select([[l(:label_all), ''], - [l(:status_active), 1]], selected) + [l(:project_status_active), '1'], + [l(:project_status_closed), '5'], + [l(:project_status_archived), '9']], selected.to_s) end end diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/application_helper.rb --- a/app/helpers/application_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/application_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -43,18 +43,12 @@ link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) end - # Display a link to remote if user is authorized - def link_to_remote_if_authorized(name, options = {}, html_options = nil) - url = options[:url] || {} - link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action]) - end - # Displays a link to user's account page if active def link_to_user(user, options={}) if user.is_a?(User) name = h(user.name(options[:format])) - if user.active? - link_to(name, :controller => 'users', :action => 'show', :id => user) + if user.active? || (User.current.admin? && user.logged?) + link_to name, user_path(user), :class => user.css_classes else name end @@ -70,10 +64,12 @@ # link_to_issue(issue, :truncate => 6) # => Defect #6: This i... # link_to_issue(issue, :subject => false) # => Defect #6 # link_to_issue(issue, :project => true) # => Foo - Defect #6 + # link_to_issue(issue, :subject => false, :tracker => false) # => #6 # def link_to_issue(issue, options={}) title = nil subject = nil + text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}" if options[:subject] == false title = truncate(issue.subject, :length => 60) else @@ -82,11 +78,9 @@ subject = truncate(subject, :length => options[:truncate]) end end - s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, - :class => issue.css_classes, - :title => title - s << ": #{h subject}" if subject - s = "#{h issue.project} - " + s if options[:project] + s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title + s << h(": #{subject}") if subject + s = h("#{issue.project} - ") + s if options[:project] s end @@ -97,21 +91,29 @@ def link_to_attachment(attachment, options={}) text = options.delete(:text) || attachment.filename action = options.delete(:download) ? 'download' : 'show' + opt_only_path = {} + opt_only_path[:only_path] = (options[:only_path] == false ? false : true) + options.delete(:only_path) link_to(h(text), {:controller => 'attachments', :action => action, - :id => attachment, :filename => attachment.filename }, + :id => attachment, :filename => attachment.filename}.merge(opt_only_path), options) end # Generates a link to a SCM revision # Options: # * :text - Link text (default to the formatted revision) - def link_to_revision(revision, project, options={}) + def link_to_revision(revision, repository, options={}) + if repository.is_a?(Project) + repository = repository.repository + end text = options.delete(:text) || format_revision(revision) rev = revision.respond_to?(:identifier) ? revision.identifier : revision - - link_to(h(text), {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev}, - :title => l(:label_revision_id, format_revision(revision))) + link_to( + h(text), + {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev}, + :title => l(:label_revision_id, format_revision(revision)) + ) end # Generates a link to a message @@ -120,7 +122,7 @@ h(truncate(message.subject, :length => 60)), { :controller => 'messages', :action => 'show', :board_id => message.board_id, - :id => message.root, + :id => (message.parent_id || message.id), :r => (message.parent_id && message.id), :anchor => (message.parent_id ? "message-#{message.id}" : nil) }.merge(options), @@ -137,17 +139,27 @@ # link_to_project(project, {}, :class => "project") # => html options with default url (project overview) # def link_to_project(project, options={}, html_options = nil) - if project.active? + if project.archived? + h(project) + else url = {:controller => 'projects', :action => 'show', :id => project}.merge(options) link_to(h(project), url, html_options) - else - h(project) end end + def wiki_page_path(page, options={}) + url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options)) + end + + def thumbnail_tag(attachment) + link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)), + {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename}, + :title => attachment.filename + end + def toggle_link(name, id, options={}) - onclick = "Element.toggle('#{id}'); " - onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ") + onclick = "$('##{id}').toggle(); " + onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ") onclick << "return false;" link_to(name, "#", :onclick => onclick) end @@ -160,21 +172,17 @@ })) end - def prompt_to_remote(name, text, param, url, html_options = {}) - html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;" - link_to name, {}, html_options - end - def format_activity_title(text) h(truncate_single_line(text, :length => 100)) end def format_activity_day(date) - date == Date.today ? l(:label_today).titleize : format_date(date) + date == User.current.today ? l(:label_today).titleize : format_date(date) end def format_activity_description(text) - h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "
    ") + h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...') + ).gsub(/[\r\n]+/, "
    ").html_safe end def format_version_name(version) @@ -191,13 +199,46 @@ end end + # Renders a tree of projects as a nested set of unordered lists + # The given collection may be a subset of the whole project tree + # (eg. some intermediate nodes are private and can not be seen) + def render_project_nested_lists(projects) + s = '' + if projects.any? + ancestors = [] + original_project = @project + projects.sort_by(&:lft).each do |project| + # set the project environment to please macros. + @project = project + if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) + s << "
      \n" + else + ancestors.pop + s << "" + while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) + ancestors.pop + s << "
    \n" + end + end + classes = (ancestors.empty? ? 'root' : 'child') + s << "
  • " + s << h(block_given? ? yield(project) : project.name) + s << "
    \n" + ancestors << project + end + s << ("
  • \n" * ancestors.size) + @project = original_project + end + s.html_safe + end + def render_page_hierarchy(pages, node=nil, options={}) content = '' if pages[node] content << "
      \n" pages[node].each do |page| content << "
    • " - content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}, + content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil}, :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil)) content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id] content << "
    • \n" @@ -211,7 +252,7 @@ def render_flash_messages s = '' flash.each do |k,v| - s << content_tag('div', v, :class => "flash #{k}") + s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}") end s.html_safe end @@ -228,23 +269,24 @@ # Renders the project quick-jump box def render_project_jump_box return unless User.current.logged? - projects = User.current.memberships.collect(&:project).compact.uniq + projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq if projects.any? - s = '' - s.html_safe + + select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }') end end def project_tree_options_for_select(projects, options = {}) s = '' project_tree(projects) do |project, level| - name_prefix = (level > 0 ? (' ' * 2 * level + '» ') : '') + name_prefix = (level > 0 ? ' ' * 2 * level + '» ' : '').html_safe tag_options = {:value => project.id} if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project)) tag_options[:selected] = 'selected' @@ -264,40 +306,16 @@ Project.project_tree(projects, &block) end - def project_nested_ul(projects, &block) - s = '' - if projects.any? - ancestors = [] - projects.sort_by(&:lft).each do |project| - if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) - s << "
        \n" - else - ancestors.pop - s << "" - while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) - ancestors.pop - s << "
      \n" - end - end - s << "
    • " - s << yield(project).to_s - ancestors << project - end - s << ("
    \n" * ancestors.size) - end - s.html_safe - end - def principals_check_box_tags(name, principals) s = '' principals.sort.each do |principal| - - if principal.type == "User": + + if principal.type == "User" s << "\n" else s << "\n" end - + end s.html_safe end @@ -305,6 +323,9 @@ # Returns a string for users/groups option tags def principals_options_for_select(collection, selected=nil) s = '' + if collection.include?(User.current) + s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id) + end groups = '' collection.sort.each do |element| selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) @@ -313,7 +334,16 @@ unless groups.empty? s << %(#{groups}) end - s + s.html_safe + end + + # Options for the new membership projects combo-box + def options_for_membership_project_select(principal, projects) + options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") + options << project_tree_options_for_select(projects) do |p| + {:disabled => principal.projects.include?(p)} + end + options end # Truncates and returns the string as a single line @@ -331,6 +361,10 @@ end end + def anchor(text) + text.to_s.gsub(' ', '_') + end + def html_hours(text) text.gsub(%r{(\d+)\.(\d+)}, '\1.\2').html_safe end @@ -342,18 +376,25 @@ def time_tag(time) text = distance_of_time_in_words(Time.now, time) if @project - link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time)) + link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time)) else content_tag('acronym', text, :title => format_time(time)) end end + def syntax_highlight_lines(name, content) + lines = [] + syntax_highlight(name, content).each_line { |line| lines << line } + lines + end + def syntax_highlight(name, content) Redmine::SyntaxHighlighting.highlight_by_filename(content, name) end def to_path_param(path) - path.to_s.split(%r{[/\\]}).select {|p| !p.blank?} + str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/") + str.blank? ? nil : str end def pagination_links_full(paginator, count=nil, options={}) @@ -382,7 +423,7 @@ unless count.nil? html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})" - if per_page_links != false && links = per_page_links(paginator.items_per_page) + if per_page_links != false && links = per_page_links(paginator.items_per_page, count) html << " | #{links}" end end @@ -390,11 +431,23 @@ html.html_safe end - def per_page_links(selected=nil) - links = Setting.per_page_options_array.collect do |n| + def per_page_links(selected=nil, item_count=nil) + values = Setting.per_page_options_array + if item_count && values.any? + if item_count > values.first + max = values.detect {|value| value >= item_count} || item_count + else + max = item_count + end + values = values.select {|value| value <= max || value == selected} + end + if values.empty? || (values.size == 1 && values.first == selected) + return nil + end + links = values.collect do |n| n == selected ? n : link_to_content_update(n, params.merge(:per_page => n)) end - links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil + l(:label_display_per_page, links.join(', ')) end def reorder_links(name, url, method = :post) @@ -435,12 +488,12 @@ root = ancestors.shift b << link_to_project(root, {:jump => current_menu_item}, :class => 'root') if ancestors.size > 2 - b << '…' + b << '…' ancestors = ancestors[-2, 2] end b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') } - b = b.join(' » ') - b << (' »') + b = b.join(' » ').html_safe + b << (' »'.html_safe) end pname << h(@project) @@ -470,8 +523,8 @@ css << 'theme-' + theme.name end - css << 'controller-' + params[:controller] - css << 'action-' + params[:action] + css << 'controller-' + controller_name + css << 'action-' + action_name css.join(' ') end @@ -500,6 +553,8 @@ project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) only_path = options.delete(:only_path) == false ? false : true + text = text.dup + macros = catch_macros(text) text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) @parsed_headings = [] @@ -507,8 +562,8 @@ @current_section = 0 if options[:edit_section_links] parse_sections(text, project, obj, attr, only_path, options) - text = parse_non_pre_blocks(text) do |text| - [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name| + text = parse_non_pre_blocks(text, obj, macros) do |text| + [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name| send method_name, text, project, obj, attr, only_path, options end end @@ -518,10 +573,10 @@ replace_toc(text, @parsed_headings) end - text + text.html_safe end - def parse_non_pre_blocks(text) + def parse_non_pre_blocks(text, obj, macros) s = StringScanner.new(text) tags = [] parsed = '' @@ -530,6 +585,9 @@ text, full_tag, closing, tag = s[1], s[2], s[3], s[4] if tags.empty? yield text + inject_macros(text, obj, macros) if macros.any? + else + inject_macros(text, obj, macros, false) if macros.any? end parsed << text if tag @@ -547,13 +605,14 @@ while tag = tags.pop parsed << "" end - parsed.html_safe + parsed end def parse_inline_attachments(text, project, obj, attr, only_path, options) # when using an image link, try to use an attachment, if possible - if options[:attachments] || (obj && obj.respond_to?(:attachments)) - attachments = options[:attachments] || obj.attachments + attachments = options[:attachments] || [] + attachments += obj.attachments if obj.respond_to?(:attachments) + if attachments.present? text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| filename, ext, alt, alttext = $1.downcase, $2, $3, $4 # search for the picture in attachments @@ -564,9 +623,9 @@ if !desc.blank? && alttext.blank? alt = " title=\"#{desc}\" alt=\"#{desc}\"" end - "src=\"#{image_url}\"#{alt}".html_safe + "src=\"#{image_url}\"#{alt}" else - m.html_safe + m end end end @@ -610,16 +669,18 @@ when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export else wiki_page_id = page.present? ? Wiki.titleize(page) : nil - url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor) + parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil + url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, + :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent) end end link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new'))) else # project or wiki doesn't exist - all.html_safe + all end else - all.html_safe + all end end end @@ -656,27 +717,37 @@ # identifier:document:"Some document" # identifier:version:1.0.0 # identifier:source:some/file - def parse_redmine_links(text, project, obj, attr, only_path, options) - text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|forum|news|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m| - leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10 + def parse_redmine_links(text, default_project, obj, attr, only_path, options) + text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m| + leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17 link = nil + project = default_project if project_identifier project = Project.visible.find_by_identifier(project_identifier) end if esc.nil? if prefix.nil? && sep == 'r' - # project.changesets.visible raises an SQL error because of a double join on repositories - if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier)) - link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision}, - :class => 'changeset', - :title => truncate_single_line(changeset.comments, :length => 100)) + if project + repository = nil + if repo_identifier + repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} + else + repository = project.repository + end + # project.changesets.visible raises an SQL error because of a double join on repositories + if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier)) + link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision}, + :class => 'changeset', + :title => truncate_single_line(changeset.comments, :length => 100)) + end end elsif sep == '#' oid = identifier.to_i case prefix when nil - if issue = Issue.visible.find_by_id(oid, :include => :status) - link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid}, + if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status) + anchor = comment_id ? "note-#{comment_id}" : nil + link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor}, :class => issue.css_classes, :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})") end @@ -733,26 +804,37 @@ link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, :class => 'news' end - when 'commit' - if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"])) - link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier}, - :class => 'changeset', - :title => truncate_single_line(h(changeset.comments), :length => 100) - end - when 'source', 'export' - if project && project.repository && User.current.allowed_to?(:browse_repository, project) - name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$} - path, rev, anchor = $1, $3, $5 - link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, - :path => to_path_param(path), - :rev => rev, - :anchor => anchor, - :format => (prefix == 'export' ? 'raw' : nil)}, - :class => (prefix == 'export' ? 'source download' : 'source') + when 'commit', 'source', 'export' + if project + repository = nil + if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$} + repo_prefix, repo_identifier, name = $1, $2, $3 + repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} + else + repository = project.repository + end + if prefix == 'commit' + if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"])) + link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier}, + :class => 'changeset', + :title => truncate_single_line(h(changeset.comments), :length => 100) + end + else + if repository && User.current.allowed_to?(:browse_repository, project) + name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$} + path, rev, anchor = $1, $3, $5 + link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param, + :path => to_path_param(path), + :rev => rev, + :anchor => anchor}, + :class => (prefix == 'export' ? 'source download' : 'source') + end + end + repo_prefix = nil end when 'attachment' attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) - if attachments && attachment = attachments.detect {|a| a.filename == name } + if attachments && attachment = Attachment.latest_attach(attachments, name) link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment}, :class => 'attachment' end @@ -763,23 +845,24 @@ end end end - (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe + (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}")) end end - HEADING_RE = /(]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE) + HEADING_RE = /(]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE) def parse_sections(text, project, obj, attr, only_path, options) return unless options[:edit_section_links] text.gsub!(HEADING_RE) do + heading = $1 @current_section += 1 if @current_section > 1 content_tag('div', link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)), :class => 'contextual', - :title => l(:button_edit_section)) + $1 + :title => l(:button_edit_section)) + heading.html_safe else - $1 + heading end end end @@ -805,31 +888,57 @@ end end - MACROS_RE = / + MACROS_RE = /( (!)? # escaping ( \{\{ # opening tag ([\w]+) # macro name - (\(([^\}]*)\))? # optional arguments + (\(([^\n\r]*?)\))? # optional arguments + ([\n\r].*?[\n\r])? # optional block of text \}\} # closing tag ) - /x unless const_defined?(:MACROS_RE) + )/mx unless const_defined?(:MACROS_RE) - # Macros substitution - def parse_macros(text, project, obj, attr, only_path, options) + MACRO_SUB_RE = /( + \{\{ + macro\((\d+)\) + \}\} + )/x unless const_defined?(:MACRO_SUB_RE) + + # Extracts macros from text + def catch_macros(text) + macros = {} text.gsub!(MACROS_RE) do - esc, all, macro = $1, $2, $3.downcase - args = ($5 || '').split(',').each(&:strip) - if esc.nil? - begin - exec_macro(macro, obj, args) - rescue => e - "
    Error executing the #{macro} macro (#{e})
    " - end || all + all, macro = $1, $4.downcase + if macro_exists?(macro) || all =~ MACRO_SUB_RE + index = macros.size + macros[index] = all + "{{macro(#{index})}}" else all end end + macros + end + + # Executes and replaces macros in text + def inject_macros(text, obj, macros, execute=true) + text.gsub!(MACRO_SUB_RE) do + all, index = $1, $2.to_i + orig = macros.delete(index) + if execute && orig && orig =~ MACROS_RE + esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip) + if esc.nil? + h(exec_macro(macro, obj, args, block) || all) + else + h(all) + end + elsif orig + h(orig) + else + h(all) + end + end end TOC_RE = /

    \{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE) @@ -837,6 +946,8 @@ # Renders the TOC with given headings def replace_toc(text, headings) text.gsub!(TOC_RE) do + # Keep only the 4 first levels + headings = headings.select{|level, anchor, item| level <= 4} if headings.empty? '' else @@ -875,8 +986,7 @@ end def lang_options_for_select(blank=true) - (blank ? [["(auto)", ""]] : []) + - valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last } + (blank ? [["(auto)", ""]] : []) + languages_options end def label_tag_for(name, option_tags = nil, options = {}) @@ -884,26 +994,83 @@ content_tag("label", label_text) end - def labelled_tabular_form_for(*args, &proc) + def labelled_form_for(*args, &proc) args << {} unless args.last.is_a?(Hash) options = args.last - options[:html] ||= {} - options[:html][:class] = 'tabular' unless options[:html].has_key?(:class) - options.merge!({:builder => TabularFormBuilder}) + if args.first.is_a?(Symbol) + options.merge!(:as => args.shift) + end + options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) form_for(*args, &proc) end - def labelled_form_for(*args, &proc) + def labelled_fields_for(*args, &proc) args << {} unless args.last.is_a?(Hash) options = args.last - options.merge!({:builder => TabularFormBuilder}) + options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) + fields_for(*args, &proc) + end + + def labelled_remote_form_for(*args, &proc) + ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2." + args << {} unless args.last.is_a?(Hash) + options = args.last + options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true}) form_for(*args, &proc) end + def error_messages_for(*objects) + html = "" + objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact + errors = objects.map {|o| o.errors.full_messages}.flatten + if errors.any? + html << "

      \n" + errors.each do |error| + html << "
    • #{h error}
    • \n" + end + html << "
    \n" + end + html.html_safe + end + + def delete_link(url, options={}) + options = { + :method => :delete, + :data => {:confirm => l(:text_are_you_sure)}, + :class => 'icon icon-del' + }.merge(options) + + link_to l(:button_delete), url, options + end + + def preview_link(url, form, target='preview', options={}) + content_tag 'a', l(:label_preview), { + :href => "#", + :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|, + :accesskey => accesskey(:preview) + }.merge(options) + end + + def link_to_function(name, function, html_options={}) + content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options)) + end + + # Helper to render JSON in views + def raw_json(arg) + arg.to_json.to_s.gsub('/', '\/').html_safe + end + + def back_url + url = params[:back_url] + if url.nil? && referer = request.env['HTTP_REFERER'] + url = CGI.unescape(referer.to_s) + end + url + end + def back_url_hidden_field_tag - back_url = params[:back_url] || request.env['HTTP_REFERER'] - back_url = CGI.unescape(back_url.to_s) - hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank? + url = back_url + hidden_field_tag('back_url', url, :id => nil) unless url.blank? end def check_all_links(form_name) @@ -947,55 +1114,91 @@ end @context_menu_included = true end - javascript_tag "new ContextMenu('#{ url_for(url) }')" - end - - def context_menu_link(name, url, options={}) - options[:class] ||= '' - if options.delete(:selected) - options[:class] << ' icon-checked disabled' - options[:disabled] = true - end - if options.delete(:disabled) - options.delete(:method) - options.delete(:confirm) - options.delete(:onclick) - options[:class] << ' disabled' - url = '#' - end - link_to h(name), url, options + javascript_tag "contextMenuInit('#{ url_for(url) }')" end def calendar_for(field_id) include_calendar_headers_tags - image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) + - javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });") + javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });") end def include_calendar_headers_tags unless @calendar_headers_tags_included @calendar_headers_tags_included = true content_for :header_tags do - start_of_week = case Setting.start_of_week.to_i - when 1 - 'Calendar._FD = 1;' # Monday - when 7 - 'Calendar._FD = 0;' # Sunday - when 6 - 'Calendar._FD = 6;' # Saturday - else - '' # use language + start_of_week = Setting.start_of_week + start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank? + # Redmine uses 1..7 (monday..sunday) in settings and locales + # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0 + start_of_week = start_of_week.to_i % 7 + + tags = javascript_tag( + "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " + + "showOn: 'button', buttonImageOnly: true, buttonImage: '" + + path_to_image('/images/calendar.png') + + "', showButtonPanel: true};") + jquery_locale = l('jquery.locale', :default => current_language.to_s) + unless jquery_locale == 'en' + tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js") end - - javascript_include_tag('calendar/calendar') + - javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") + - javascript_tag(start_of_week) + - javascript_include_tag('calendar/calendar-setup') + - stylesheet_link_tag('calendar') + tags end end end + # Overrides Rails' stylesheet_link_tag with themes and plugins support. + # Examples: + # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults + # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets + # + def stylesheet_link_tag(*sources) + options = sources.last.is_a?(Hash) ? sources.pop : {} + plugin = options.delete(:plugin) + sources = sources.map do |source| + if plugin + "/plugin_assets/#{plugin}/stylesheets/#{source}" + elsif current_theme && current_theme.stylesheets.include?(source) + current_theme.stylesheet_path(source) + else + source + end + end + super sources, options + end + + # Overrides Rails' image_tag with themes and plugins support. + # Examples: + # image_tag('image.png') # => picks image.png from the current theme or defaults + # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets + # + def image_tag(source, options={}) + if plugin = options.delete(:plugin) + source = "/plugin_assets/#{plugin}/images/#{source}" + elsif current_theme && current_theme.images.include?(source) + source = current_theme.image_path(source) + end + super source, options + end + + # Overrides Rails' javascript_include_tag with plugins support + # Examples: + # javascript_include_tag('scripts') # => picks scripts.js from defaults + # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets + # + def javascript_include_tag(*sources) + options = sources.last.is_a?(Hash) ? sources.pop : {} + if plugin = options.delete(:plugin) + sources = sources.map do |source| + if plugin + "/plugin_assets/#{plugin}/javascripts/#{source}" + else + source + end + end + end + super sources, options + end + def content_for(name, content = nil, &block) @has_content ||= {} @has_content[name] = true @@ -1006,6 +1209,14 @@ (@has_content && @has_content[name]) || false end + def sidebar_content? + has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present? + end + + def view_layouts_base_sidebar_hook_response + @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar) + end + def email_delivery_enabled? !!ActionMailer::Base.perform_deliveries end @@ -1014,7 +1225,7 @@ # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe ') def avatar(user, options = { }) if Setting.gravatar_enabled? - options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default}) + options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default}) email = nil if user.respond_to?(:mail) email = user.mail @@ -1028,14 +1239,19 @@ end def sanitize_anchor_name(anchor) - anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java' + anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + else + # TODO: remove when ruby1.8 is no longer supported + anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + end end # Returns the javascript tags that are included in the html layout head def javascript_heads - tags = javascript_include_tag(:defaults) + tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application') unless User.current.pref.warn_on_leaving_unsaved == '0' - tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });") + tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });") end tags end diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/attachments_helper.rb --- a/app/helpers/attachments_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/attachments_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -21,12 +21,14 @@ # Displays view/delete links to the attachments of the given object # Options: # :author -- author names are not displayed if set to false + # :thumbails -- display thumbnails if enabled in settings def link_to_attachments(container, options = {}) - options.assert_valid_keys(:author) + options.assert_valid_keys(:author, :thumbnails) if container.attachments.any? options = {:deletable => container.attachments_deletable?, :author => true}.merge(options) - render :partial => 'attachments/links', :locals => {:attachments => container.attachments, :options => options} + render :partial => 'attachments/links', + :locals => {:attachments => container.attachments, :options => options, :thumbnails => (options[:thumbnails] && Setting.thumbnails_enabled?)} end end diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/auth_sources_helper.rb --- a/app/helpers/auth_sources_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/auth_sources_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,4 +18,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module AuthSourcesHelper + def auth_source_partial_name(auth_source) + "form_#{auth_source.class.name.underscore}" + end end diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/boards_helper.rb --- a/app/helpers/boards_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/boards_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,4 +18,24 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module BoardsHelper + def board_breadcrumb(item) + board = item.is_a?(Message) ? item.board : item + links = [link_to(l(:label_board_plural), project_boards_path(item.project))] + boards = board.ancestors.reverse + if item.is_a?(Message) + boards << board + end + links += boards.map {|ancestor| link_to(h(ancestor.name), project_board_path(ancestor.project, ancestor))} + breadcrumb links + end + + def boards_options_for_select(boards) + options = [] + Board.board_tree(boards) do |board, level| + label = (level > 0 ? ' ' * 2 * level + '» ' : '').html_safe + label << board.name + options << [label, board.id] + end + options + end end diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/calendars_helper.rb --- a/app/helpers/calendars_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/calendars_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/context_menus_helper.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/helpers/context_menus_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,43 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module ContextMenusHelper + def context_menu_link(name, url, options={}) + options[:class] ||= '' + if options.delete(:selected) + options[:class] << ' icon-checked disabled' + options[:disabled] = true + end + if options.delete(:disabled) + options.delete(:method) + options.delete(:data) + options[:onclick] = 'return false;' + options[:class] << ' disabled' + url = '#' + end + link_to h(name), url, options + end + + def bulk_update_custom_field_context_menu_link(field, text, value) + context_menu_link h(text), + {:controller => 'issues', :action => 'bulk_update', :ids => @issue_ids, :issue => {'custom_field_values' => {field.id => value}}, :back_url => @back}, + :method => :post, + :selected => (@issue && @issue.custom_field_value(field) == value) + end +end diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/custom_fields_helper.rb --- a/app/helpers/custom_fields_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/custom_fields_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -20,74 +20,89 @@ module CustomFieldsHelper def custom_fields_tabs - tabs = [{:name => 'IssueCustomField', :partial => 'custom_fields/index', :label => :label_issue_plural}, - {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', :label => :label_spent_time}, - {:name => 'ProjectCustomField', :partial => 'custom_fields/index', :label => :label_project_plural}, - {:name => 'VersionCustomField', :partial => 'custom_fields/index', :label => :label_version_plural}, - {:name => 'UserCustomField', :partial => 'custom_fields/index', :label => :label_user_plural}, - {:name => 'GroupCustomField', :partial => 'custom_fields/index', :label => :label_group_plural}, - {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', :label => TimeEntryActivity::OptionName}, - {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', :label => IssuePriority::OptionName}, - {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', :label => DocumentCategory::OptionName} - ] + CustomField::CUSTOM_FIELDS_TABS end # Return custom field html tag corresponding to its format def custom_field_tag(name, custom_value) custom_field = custom_value.custom_field field_name = "#{name}[custom_field_values][#{custom_field.id}]" + field_name << "[]" if custom_field.multiple? field_id = "#{name}_custom_field_values_#{custom_field.id}" + tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"} + field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format) case field_format.try(:edit_as) when "date" - text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) + + text_field_tag(field_name, custom_value.value, tag_options.merge(:size => 10)) + calendar_for(field_id) when "text" - text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%') + text_area_tag(field_name, custom_value.value, tag_options.merge(:rows => 3)) when "bool" - hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, :id => field_id) + hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, tag_options) when "list" - blank_option = custom_field.is_required? ? - (custom_field.default_value.blank? ? "" : '') : - '' - select_tag(field_name, blank_option + options_for_select(custom_field.possible_values_options(custom_value.customized), custom_value.value), :id => field_id) + blank_option = ''.html_safe + unless custom_field.multiple? + if custom_field.is_required? + unless custom_field.default_value.present? + blank_option = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '') + end + else + blank_option = content_tag('option') + end + end + s = select_tag(field_name, blank_option + options_for_select(custom_field.possible_values_options(custom_value.customized), custom_value.value), + tag_options.merge(:multiple => custom_field.multiple?)) + if custom_field.multiple? + s << hidden_field_tag(field_name, '') + end + s else - text_field_tag(field_name, custom_value.value, :id => field_id) + text_field_tag(field_name, custom_value.value, tag_options) end end # Return custom field label tag - def custom_field_label_tag(name, custom_value) + def custom_field_label_tag(name, custom_value, options={}) + required = options[:required] || custom_value.custom_field.is_required? + content_tag "label", h(custom_value.custom_field.name) + - (custom_value.custom_field.is_required? ? " *".html_safe : ""), - :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}", - :class => (custom_value.errors.empty? ? nil : "error" ) + (required ? " *".html_safe : ""), + :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}" end # Return custom field tag with its label tag - def custom_field_tag_with_label(name, custom_value) - custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value) + def custom_field_tag_with_label(name, custom_value, options={}) + custom_field_label_tag(name, custom_value, options) + custom_field_tag(name, custom_value) end def custom_field_tag_for_bulk_edit(name, custom_field, projects=nil) field_name = "#{name}[custom_field_values][#{custom_field.id}]" + field_name << "[]" if custom_field.multiple? field_id = "#{name}_custom_field_values_#{custom_field.id}" + + tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"} + field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format) case field_format.try(:edit_as) when "date" - text_field_tag(field_name, '', :id => field_id, :size => 10) + + text_field_tag(field_name, '', tag_options.merge(:size => 10)) + calendar_for(field_id) when "text" - text_area_tag(field_name, '', :id => field_id, :rows => 3, :style => 'width:90%') + text_area_tag(field_name, '', tag_options.merge(:rows => 3)) when "bool" select_tag(field_name, options_for_select([[l(:label_no_change_option), ''], [l(:general_text_yes), '1'], - [l(:general_text_no), '0']]), :id => field_id) + [l(:general_text_no), '0']]), tag_options) when "list" - select_tag(field_name, options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values_options(projects)), :id => field_id) + options = [] + options << [l(:label_no_change_option), ''] unless custom_field.multiple? + options << [l(:label_none), '__none__'] unless custom_field.is_required? + options += custom_field.possible_values_options(projects) + select_tag(field_name, options_for_select(options), tag_options.merge(:multiple => custom_field.multiple?)) else - text_field_tag(field_name, '', :id => field_id) + text_field_tag(field_name, '', tag_options) end end @@ -99,7 +114,11 @@ # Return a string used to display a custom value def format_value(value, field_format) - Redmine::CustomFieldFormat.format_value(value, field_format) # Proxy + if value.is_a?(Array) + value.collect {|v| format_value(v, field_format)}.compact.sort.join(', ') + else + Redmine::CustomFieldFormat.format_value(value, field_format) + end end # Return an array of custom field formats which can be used in select_tag @@ -111,8 +130,18 @@ def render_api_custom_values(custom_values, api) api.array :custom_fields do custom_values.each do |custom_value| - api.custom_field :id => custom_value.custom_field_id, :name => custom_value.custom_field.name do - api.value custom_value.value + attrs = {:id => custom_value.custom_field_id, :name => custom_value.custom_field.name} + attrs.merge!(:multiple => true) if custom_value.custom_field.multiple? + api.custom_field attrs do + if custom_value.value.is_a?(Array) + api.array :value do + custom_value.value.each do |value| + api.value value unless value.blank? + end + end + else + api.value custom_value.value + end end end end unless custom_values.empty? diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/documents_helper.rb --- a/app/helpers/documents_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/documents_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/enumerations_helper.rb --- a/app/helpers/enumerations_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/enumerations_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/gantt_helper.rb --- a/app/helpers/gantt_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/gantt_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/groups_helper.rb --- a/app/helpers/groups_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/groups_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,15 +18,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module GroupsHelper - # Options for the new membership projects combo-box - def options_for_membership_project_select(user, projects) - options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") - options << project_tree_options_for_select(projects) do |p| - {:disabled => (user.projects.include?(p))} - end - options - end - def group_settings_tabs tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general}, {:name => 'users', :partial => 'groups/users', :label => :label_user_plural}, diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/issue_categories_helper.rb --- a/app/helpers/issue_categories_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/issue_categories_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/issue_moves_helper.rb --- a/app/helpers/issue_moves_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -# encoding: utf-8 -# -module IssueMovesHelper -end diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/issue_relations_helper.rb --- a/app/helpers/issue_relations_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/issue_relations_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/issue_statuses_helper.rb --- a/app/helpers/issue_statuses_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/issue_statuses_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/issues_helper.rb --- a/app/helpers/issues_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/issues_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -48,13 +48,13 @@ @cached_label_priority ||= l(:field_priority) @cached_label_project ||= l(:field_project) - (link_to_issue(issue) + "

    " + - "#{@cached_label_project}: #{link_to_project(issue.project)}
    " + - "#{@cached_label_status}: #{h(issue.status.name)}
    " + - "#{@cached_label_start_date}: #{format_date(issue.start_date)}
    " + - "#{@cached_label_due_date}: #{format_date(issue.due_date)}
    " + - "#{@cached_label_assigned_to}: #{h(issue.assigned_to)}
    " + - "#{@cached_label_priority}: #{h(issue.priority.name)}").html_safe + link_to_issue(issue) + "

    ".html_safe + + "#{@cached_label_project}: #{link_to_project(issue.project)}
    ".html_safe + + "#{@cached_label_status}: #{h(issue.status.name)}
    ".html_safe + + "#{@cached_label_start_date}: #{format_date(issue.start_date)}
    ".html_safe + + "#{@cached_label_due_date}: #{format_date(issue.due_date)}
    ".html_safe + + "#{@cached_label_assigned_to}: #{h(issue.assigned_to)}
    ".html_safe + + "#{@cached_label_priority}: #{h(issue.priority.name)}".html_safe end def issue_heading(issue) @@ -65,7 +65,7 @@ s = '' ancestors = issue.root? ? [] : issue.ancestors.visible.all ancestors.each do |ancestor| - s << '
    ' + content_tag('p', link_to_issue(ancestor)) + s << '
    ' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id))) end s << '
    ' subject = h(issue.subject) @@ -80,18 +80,71 @@ def render_descendants_tree(issue) s = '
    ' issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level| + css = "issue issue-#{child.id} hascontextmenu" + css << " idnt idnt-#{level}" if level > 0 s << content_tag('tr', content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') + - content_tag('td', link_to_issue(child, :truncate => 60), :class => 'subject') + + content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') + content_tag('td', h(child.status)) + content_tag('td', link_to_user(child.assigned_to)) + content_tag('td', progress_bar(child.done_ratio, :width => '80px')), - :class => "issue issue-#{child.id} hascontextmenu #{level > 0 ? "idnt idnt-#{level}" : nil}") + :class => css) end - s << '
    ' + s << '' s.html_safe end + # Returns a link for adding a new subtask to the given issue + def link_to_new_subtask(issue) + attrs = { + :tracker_id => issue.tracker, + :parent_issue_id => issue + } + link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs)) + end + + class IssueFieldsRows + include ActionView::Helpers::TagHelper + + def initialize + @left = [] + @right = [] + end + + def left(*args) + args.any? ? @left << cells(*args) : @left + end + + def right(*args) + args.any? ? @right << cells(*args) : @right + end + + def size + @left.size > @right.size ? @left.size : @right.size + end + + def to_html + html = ''.html_safe + blank = content_tag('th', '') + content_tag('td', '') + size.times do |i| + left = @left[i] || blank + right = @right[i] || blank + html << content_tag('tr', left + right) + end + html + end + + def cells(label, text, options={}) + content_tag('th', "#{label}:", options) + content_tag('td', text, options) + end + end + + def issue_fields_rows + r = IssueFieldsRows.new + yield r + r.to_html + end + def render_custom_fields_rows(issue) return if issue.custom_field_values.empty? ordered_values = [] @@ -131,14 +184,11 @@ def sidebar_queries unless @sidebar_queries - # User can see public queries and his own queries - visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)]) - # Project specific queries and global queries - visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]) - @sidebar_queries = Query.find(:all, - :select => 'id, name, is_public', - :order => "name ASC", - :conditions => visible.conditions) + @sidebar_queries = Query.visible.all( + :order => "#{Query.table_name}.name ASC", + # Project specific queries and global queries + :conditions => (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]) + ) end @sidebar_queries end @@ -149,12 +199,14 @@ content_tag('h3', h(title)) + queries.collect {|query| - link_to(h(query.name), url_params.merge(:query_id => query)) - }.join('
    ') + css = 'query' + css << ' selected' if query == @query + link_to(h(query.name), url_params.merge(:query_id => query), :class => css) + }.join('
    ').html_safe end def render_sidebar_queries - out = '' + out = ''.html_safe queries = sidebar_queries.select {|q| !q.is_public?} out << query_links(l(:label_my_queries), queries) if queries.any? queries = sidebar_queries.select {|q| q.is_public?} @@ -162,36 +214,76 @@ out end - def show_detail(detail, no_html=false) + # Returns the textual representation of a journal details + # as an array of strings + def details_to_strings(details, no_html=false, options={}) + options[:only_path] = (options[:only_path] == false ? false : true) + strings = [] + values_by_field = {} + details.each do |detail| + if detail.property == 'cf' + field_id = detail.prop_key + field = CustomField.find_by_id(field_id) + if field && field.multiple? + values_by_field[field_id] ||= {:added => [], :deleted => []} + if detail.old_value + values_by_field[field_id][:deleted] << detail.old_value + end + if detail.value + values_by_field[field_id][:added] << detail.value + end + next + end + end + strings << show_detail(detail, no_html, options) + end + values_by_field.each do |field_id, changes| + detail = JournalDetail.new(:property => 'cf', :prop_key => field_id) + if changes[:added].any? + detail.value = changes[:added] + strings << show_detail(detail, no_html, options) + elsif changes[:deleted].any? + detail.old_value = changes[:deleted] + strings << show_detail(detail, no_html, options) + end + end + strings + end + + # Returns the textual representation of a single journal detail + def show_detail(detail, no_html=false, options={}) + multiple = false case detail.property when 'attr' field = detail.prop_key.to_s.gsub(/\_id$/, "") label = l(("field_" + field).to_sym) - case - when ['due_date', 'start_date'].include?(detail.prop_key) + case detail.prop_key + when 'due_date', 'start_date' value = format_date(detail.value.to_date) if detail.value old_value = format_date(detail.old_value.to_date) if detail.old_value - when ['project_id', 'status_id', 'tracker_id', 'assigned_to_id', 'priority_id', 'category_id', 'fixed_version_id'].include?(detail.prop_key) + when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id', + 'priority_id', 'category_id', 'fixed_version_id' value = find_name_by_reflection(field, detail.value) old_value = find_name_by_reflection(field, detail.old_value) - when detail.prop_key == 'estimated_hours' + when 'estimated_hours' value = "%0.02f" % detail.value.to_f unless detail.value.blank? old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank? - when detail.prop_key == 'parent_id' + when 'parent_id' label = l(:field_parent_issue) value = "##{detail.value}" unless detail.value.blank? old_value = "##{detail.old_value}" unless detail.old_value.blank? - when detail.prop_key == 'is_private' + when 'is_private' value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank? old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank? end when 'cf' custom_field = CustomField.find_by_id(detail.prop_key) if custom_field + multiple = custom_field.multiple? label = custom_field.name value = format_value(detail.value, custom_field.field_format) if detail.value old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value @@ -199,7 +291,8 @@ when 'attachment' label = l(:label_attachment) end - call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value }) + call_hook(:helper_issues_show_detail_after_setting, + {:detail => detail, :label => label, :value => value, :old_value => old_value }) label ||= detail.prop_key value ||= detail.value @@ -208,10 +301,17 @@ unless no_html label = content_tag('strong', label) old_value = content_tag("i", h(old_value)) if detail.old_value - old_value = content_tag("strike", old_value) if detail.old_value and detail.value.blank? - if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key) + old_value = content_tag("del", old_value) if detail.old_value and detail.value.blank? + if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key) # Link to the attachment if it has not been removed - value = link_to_attachment(a) + value = link_to_attachment(atta, :download => true, :only_path => options[:only_path]) + if options[:only_path] != false && atta.is_text? + value += link_to( + image_tag('magnifier.png'), + :controller => 'attachments', :action => 'show', + :id => atta, :filename => atta.filename + ) + end else value = content_tag("i", h(value)) if value end @@ -221,24 +321,27 @@ s = l(:text_journal_changed_no_detail, :label => label) unless no_html diff_link = link_to 'diff', - {:controller => 'journals', :action => 'diff', :id => detail.journal_id, :detail_id => detail.id}, + {:controller => 'journals', :action => 'diff', :id => detail.journal_id, + :detail_id => detail.id, :only_path => options[:only_path]}, :title => l(:label_view_diff) s << " (#{ diff_link })" end - s - elsif !detail.value.blank? + s.html_safe + elsif detail.value.present? case detail.property when 'attr', 'cf' - if !detail.old_value.blank? - l(:text_journal_changed, :label => label, :old => old_value, :new => value) + if detail.old_value.present? + l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe + elsif multiple + l(:text_journal_added, :label => label, :value => value).html_safe else - l(:text_journal_set_to, :label => label, :value => value) + l(:text_journal_set_to, :label => label, :value => value).html_safe end when 'attachment' - l(:text_journal_added, :label => label, :value => value) + l(:text_journal_added, :label => label, :value => value).html_safe end else - l(:text_journal_deleted, :label => label, :old => old_value) + l(:text_journal_deleted, :label => label, :old => old_value).html_safe end end @@ -247,7 +350,10 @@ association = Issue.reflect_on_association(field.to_sym) if association record = association.class_name.constantize.find_by_id(id) - return record.name if record + if record + record.name.force_encoding('UTF-8') if record.name.respond_to?(:force_encoding) + return record.name + end end end @@ -268,35 +374,38 @@ def issues_to_csv(issues, project, query, options={}) decimal_separator = l(:general_csv_decimal_separator) encoding = l(:general_csv_encoding) - columns = (options[:columns] == 'all' ? query.available_columns : query.columns) + columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns) + if options[:description] + if description = query.available_columns.detect {|q| q.name == :description} + columns << description + end + end export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv| # csv header fields - csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) } + - (options[:description] ? [Redmine::CodesetUtil.from_utf8(l(:field_description), encoding)] : []) + csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) } # csv lines issues.each do |issue| col_values = columns.collect do |column| s = if column.is_a?(QueryCustomFieldColumn) - cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id} + cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id} show_value(cv) else - value = issue.send(column.name) + value = column.value(issue) if value.is_a?(Date) format_date(value) elsif value.is_a?(Time) format_time(value) elsif value.is_a?(Float) - value.to_s.gsub('.', decimal_separator) + ("%.2f" % value).gsub('.', decimal_separator) else value end end s.to_s end - csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } + - (options[:description] ? [Redmine::CodesetUtil.from_utf8(issue.description, encoding)] : []) + csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } end end export diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/journals_helper.rb --- a/app/helpers/journals_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/journals_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -23,22 +23,24 @@ editable = User.current.logged? && (User.current.allowed_to?(:edit_issue_notes, issue.project) || (journal.user == User.current && User.current.allowed_to?(:edit_own_issue_notes, issue.project))) links = [] if !journal.notes.blank? - links << link_to_remote(image_tag('comment.png'), - { :url => {:controller => 'journals', :action => 'new', :id => issue, :journal_id => journal} }, - :title => l(:button_quote)) if options[:reply_links] + links << link_to(image_tag('comment.png'), + {:controller => 'journals', :action => 'new', :id => issue, :journal_id => journal}, + :remote => true, + :method => 'post', + :title => l(:button_quote)) if options[:reply_links] links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes", - { :controller => 'journals', :action => 'edit', :id => journal }, + { :controller => 'journals', :action => 'edit', :id => journal, :format => 'js' }, :title => l(:button_edit)) if editable end - content << content_tag('div', links.join(' '), :class => 'contextual') unless links.empty? + content << content_tag('div', links.join(' ').html_safe, :class => 'contextual') unless links.empty? content << textilizable(journal, :notes) css_classes = "wiki" css_classes << " editable" if editable - content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => css_classes) + content_tag('div', content.html_safe, :id => "journal-#{journal.id}-notes", :class => css_classes) end def link_to_in_place_notes_editor(text, field_id, url, options={}) - onclick = "new Ajax.Request('#{url_for(url)}', {asynchronous:true, evalScripts:true, method:'get'}); return false;" + onclick = "$.ajax({url: '#{url_for(url)}', type: 'get'}); return false;" link_to text, '#', options.merge(:onclick => onclick) end end diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/mail_handler_helper.rb --- a/app/helpers/mail_handler_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/mail_handler_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/members_helper.rb --- a/app/helpers/members_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/members_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/messages_helper.rb --- a/app/helpers/messages_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/messages_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/my_helper.rb --- a/app/helpers/my_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/my_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/news_helper.rb --- a/app/helpers/news_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/news_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/projects_helper.rb --- a/app/helpers/projects_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/projects_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -30,7 +30,7 @@ {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural}, {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural}, {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki}, - {:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository}, + {:name => 'repositories', :action => :manage_repository, :partial => 'projects/settings/repositories', :label => :label_repository_plural}, {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural}, {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities} ] @@ -58,7 +58,7 @@ s << textilizable(project.short_description, :project => project).gsub(/<[^>]+>/, '') s << "
    " end - s + s.html_safe end # Renders a tree of projects as a nested set of unordered lists @@ -89,8 +89,6 @@ s << "
    \n" ancestors << project end - s << ("\n" * ancestors.size) - @project = original_project end s.html_safe end @@ -158,7 +156,7 @@ s = a end - s + s.html_safe end @@ -192,7 +190,7 @@ @project = original_project - s + s.html_safe end @@ -253,10 +251,6 @@ versions.each do |version| grouped[version.project.name] << [version.name, version.id] end - # Add in the selected - if selected && !versions.include?(selected) - grouped[selected.project.name] << [selected.name, selected.id] - end if grouped.keys.size > 1 grouped_options_for_select(grouped, selected && selected.id) diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/queries_helper.rb --- a/app/helpers/queries_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/queries_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,9 +18,44 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module QueriesHelper + def filters_options_for_select(query) + options_for_select(filters_options(query)) + end - def operators_for_select(filter_type) - Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]} + def filters_options(query) + options = [[]] + sorted_options = query.available_filters.sort do |a, b| + ord = 0 + if !(a[1][:order] == 20 && b[1][:order] == 20) + ord = a[1][:order] <=> b[1][:order] + else + cn = (CustomField::CUSTOM_FIELDS_NAMES.index(a[1][:field].class.name) <=> + CustomField::CUSTOM_FIELDS_NAMES.index(b[1][:field].class.name)) + if cn != 0 + ord = cn + else + f = (a[1][:field] <=> b[1][:field]) + if f != 0 + ord = f + else + # assigned_to or author + ord = (a[0] <=> b[0]) + end + end + end + ord + end + options += sorted_options.map do |field, field_options| + [field_options[:name], field] + end + end + + def available_block_columns_tags(query) + tags = ''.html_safe + query.available_block_columns.each do |column| + tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column)) + " #{column.caption}", :class => 'inline') + end + tags end def column_header(column) @@ -31,11 +66,20 @@ def column_content(column, issue) value = column.value(issue) - + if value.is_a?(Array) + value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe + else + column_value(column, issue, value) + end + end + + def column_value(column, issue, value) case value.class.name when 'String' if column.name == :subject link_to(h(value), :controller => 'issues', :action => 'show', :id => issue) + elsif column.name == :description + issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : '' else h(value) end @@ -43,12 +87,14 @@ format_time(value) when 'Date' format_date(value) - when 'Fixnum', 'Float' + when 'Fixnum' if column.name == :done_ratio progress_bar(value, :width => '80px') else - h(value.to_s) + value.to_s end + when 'Float' + sprintf "%.2f", value when 'User' link_to_user value when 'Project' @@ -61,6 +107,11 @@ l(:general_text_No) when 'Issue' link_to_issue(value, :subject => false) + when 'IssueRelation' + other = value.other_issue(issue) + content_tag('span', + (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe, + :class => value.css_classes_for(issue)) else h(value) end @@ -90,6 +141,23 @@ end end + def retrieve_query_from_session + if session[:query] + if session[:query][:id] + @query = Query.find_by_id(session[:query][:id]) + return unless @query + else + @query = Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names]) + end + if session[:query].has_key?(:project_id) + @query.project_id = session[:query][:project_id] + else + @query.project = @project + end + @query + end + end + def build_query_from_params if params[:fields] || params[:f] @query.filters = {} diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/reports_helper.rb --- a/app/helpers/reports_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/reports_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/repositories_helper.rb --- a/app/helpers/repositories_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/repositories_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,9 +17,6 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -require 'iconv' -require 'redmine/codeset_util' - module RepositoriesHelper def format_revision(revision) if revision.respond_to? :format_identifier @@ -46,17 +43,17 @@ end def render_changeset_changes - changes = @changeset.changes.find(:all, :limit => 1000, :order => 'path').collect do |change| + changes = @changeset.filechanges.find(:all, :limit => 1000, :order => 'path').collect do |change| case change.action when 'A' # Detects moved/copied files if !change.from_path.blank? change.action = - @changeset.changes.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C' + @changeset.filechanges.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C' end change when 'D' - @changeset.changes.detect {|c| c.from_path == change.path} ? nil : change + @changeset.filechanges.detect {|c| c.from_path == change.path} ? nil : change else change end @@ -92,22 +89,26 @@ text = link_to(h(text), :controller => 'repositories', :action => 'show', :id => @project, + :repository_id => @repository.identifier_param, :path => path_param, :rev => @changeset.identifier) - output << "
  • #{text}
  • " + output << "
  • #{text}" output << render_changes_tree(s) + output << "
  • " elsif c = tree[file][:c] style << " change-#{c.action}" path_param = to_path_param(@repository.relative_path(c.path)) text = link_to(h(text), :controller => 'repositories', :action => 'entry', :id => @project, + :repository_id => @repository.identifier_param, :path => path_param, :rev => @changeset.identifier) unless c.action == 'D' text << " - #{h(c.revision)}" unless c.revision.blank? text << ' ('.html_safe + link_to(l(:label_diff), :controller => 'repositories', :action => 'diff', :id => @project, + :repository_id => @repository.identifier_param, :path => path_param, :rev => @changeset.identifier) + ') '.html_safe if c.action == 'M' text << ' '.html_safe + content_tag('span', h(c.from_path), :class => 'copied-from') unless c.from_path.blank? @@ -137,15 +138,7 @@ select_tag('repository_scm', options_for_select(scm_options, repository.class.name.demodulize), :disabled => (repository && !repository.new_record?), - :onchange => remote_function( - :url => { - :controller => 'repositories', - :action => 'edit', - :id => @project - }, - :method => :get, - :with => "Form.serialize(this.form)") - ) + :data => {:remote => true, :method => 'get'}) end def with_leading_slash(path) @@ -158,7 +151,7 @@ def subversion_field_tags(form, repository) content_tag('p', form.text_field(:url, :size => 60, :required => true, - :disabled => (repository && !repository.root_url.blank?)) + + :disabled => !repository.safe_attribute?('url')) + '
    '.html_safe + '(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') + content_tag('p', form.text_field(:login, :size => 30)) + @@ -173,7 +166,7 @@ content_tag('p', form.text_field( :url, :label => l(:field_path_to_repository), :size => 60, :required => true, - :disabled => (repository && !repository.new_record?))) + + :disabled => !repository.safe_attribute?('url'))) + content_tag('p', form.select( :log_encoding, [nil] + Setting::ENCODINGS, :label => l(:field_commit_logs_encoding), :required => true)) @@ -183,7 +176,7 @@ content_tag('p', form.text_field( :url, :label => l(:field_path_to_repository), :size => 60, :required => true, - :disabled => (repository && !repository.root_url.blank?) + :disabled => !repository.safe_attribute?('url') ) + '
    '.html_safe + l(:text_mercurial_repository_note)) + content_tag('p', form.select( @@ -197,7 +190,7 @@ content_tag('p', form.text_field( :url, :label => l(:field_path_to_repository), :size => 60, :required => true, - :disabled => (repository && !repository.root_url.blank?) + :disabled => !repository.safe_attribute?('url') ) + '
    '.html_safe + l(:text_git_repository_note)) + @@ -217,12 +210,12 @@ :root_url, :label => l(:field_cvsroot), :size => 60, :required => true, - :disabled => !repository.new_record?)) + + :disabled => !repository.safe_attribute?('root_url'))) + content_tag('p', form.text_field( :url, :label => l(:field_cvs_module), :size => 30, :required => true, - :disabled => !repository.new_record?)) + + :disabled => !repository.safe_attribute?('url'))) + content_tag('p', form.select( :log_encoding, [nil] + Setting::ENCODINGS, :label => l(:field_commit_logs_encoding), :required => true)) + @@ -237,7 +230,7 @@ content_tag('p', form.text_field( :url, :label => l(:field_path_to_repository), :size => 60, :required => true, - :disabled => (repository && !repository.new_record?))) + + :disabled => !repository.safe_attribute?('url'))) + content_tag('p', form.select( :log_encoding, [nil] + Setting::ENCODINGS, :label => l(:field_commit_logs_encoding), :required => true)) @@ -247,7 +240,7 @@ content_tag('p', form.text_field( :url, :label => l(:field_root_directory), :size => 60, :required => true, - :disabled => (repository && !repository.root_url.blank?))) + + :disabled => !repository.safe_attribute?('url'))) + content_tag('p', form.select( :path_encoding, [nil] + Setting::ENCODINGS, :label => l(:field_scm_path_encoding) @@ -255,60 +248,51 @@ '
    '.html_safe + l(:text_scm_path_encoding_note)) end - def index_commits(commits, heads, href_proc = nil) + def index_commits(commits, heads) return nil if commits.nil? or commits.first.parents.nil? - map = {} - commit_hashes = [] refs_map = {} - href_proc ||= Proc.new {|x|x} - heads.each{|r| refs_map[r.scmid] ||= []; refs_map[r.scmid] << r} - commits.reverse.each_with_index do |c, i| - h = {} - h[:parents] = c.parents.collect do |p| - [p.scmid, 0, 0] - end - h[:rdmid] = i - h[:space] = 0 - h[:refs] = refs_map[c.scmid].join(" ") if refs_map.include? c.scmid - h[:scmid] = c.scmid - h[:href] = href_proc.call(c.scmid) - commit_hashes << h - map[c.scmid] = h + heads.each do |head| + refs_map[head.scmid] ||= [] + refs_map[head.scmid] << head end - heads.sort! do |a,b| - a.to_s <=> b.to_s + commits_by_scmid = {} + commits.reverse.each_with_index do |commit, commit_index| + commits_by_scmid[commit.scmid] = { + :parent_scmids => commit.parents.collect { |parent| parent.scmid }, + :rdmid => commit_index, + :refs => refs_map.include?(commit.scmid) ? refs_map[commit.scmid].join(" ") : nil, + :scmid => commit.scmid, + :href => block_given? ? yield(commit.scmid) : commit.scmid + } end - j = 0 - heads.each do |h| - if map.include? h.scmid then - j = mark_chain(j += 1, map[h.scmid], map) + heads.sort! { |head1, head2| head1.to_s <=> head2.to_s } + space = nil + heads.each do |head| + if commits_by_scmid.include? head.scmid + space = index_head((space || -1) + 1, head, commits_by_scmid) end end # when no head matched anything use first commit - if j == 0 then - mark_chain(j += 1, map.values.first, map) - end - map + space ||= index_head(0, commits.first, commits_by_scmid) + return commits_by_scmid, space end - def mark_chain(mark, commit, map) - stack = [[mark, commit]] - markmax = mark + def index_head(space, commit, commits_by_scmid) + stack = [[space, commits_by_scmid[commit.scmid]]] + max_space = space until stack.empty? - current = stack.pop - m, commit = current - commit[:space] = m if commit[:space] == 0 - m1 = m - 1 - commit[:parents].each_with_index do |p, i| - psha = p[0] - if map.include? psha and map[psha][:space] == 0 then - stack << [m1 += 1, map[psha]] if i == 0 - stack = [[m1 += 1, map[psha]]] + stack if i > 0 + space, commit = stack.pop + commit[:space] = space if commit[:space].nil? + space -= 1 + commit[:parent_scmids].each_with_index do |parent_scmid, parent_index| + parent_commit = commits_by_scmid[parent_scmid] + if parent_commit and parent_commit[:space].nil? + stack.unshift [space += 1, parent_commit] end end - markmax = m1 if markmax < m1 + max_space = space if max_space < space end - markmax + max_space end # Generates a link to a downloadable archive for a revision diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/roles_helper.rb --- a/app/helpers/roles_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/roles_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/search_helper.rb --- a/app/helpers/search_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/search_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -37,7 +37,7 @@ result << content_tag('span', h(words), :class => "highlight token-#{t}") end end - result + result.html_safe end def type_label(t) @@ -63,6 +63,8 @@ links << link_to(h(text), :q => params[:q], :titles_only => params[:titles_only], :all_words => params[:all_words], :scope => params[:scope], t => 1) end - ('
      ' + links.map {|link| content_tag('li', link)}.join(' ') + '
    ') unless links.empty? + ('
      '.html_safe + + links.map {|link| content_tag('li', link)}.join(' ').html_safe + + '
    '.html_safe) unless links.empty? end end diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/settings_helper.rb --- a/app/helpers/settings_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/settings_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -44,7 +44,7 @@ setting_values = Setting.send(setting) setting_values = [] unless setting_values.is_a?(Array) - setting_label(setting, options).html_safe + + content_tag("label", l(options[:label] || "setting_#{setting}")) + hidden_field_tag("settings[#{setting}][]", '').html_safe + choices.collect do |choice| text, value = (choice.is_a?(Array) ? choice : [choice, choice]) @@ -53,9 +53,10 @@ check_box_tag( "settings[#{setting}][]", value, - Setting.send(setting).include?(value) + Setting.send(setting).include?(value), + :id => nil ) + text.to_s, - :class => 'block' + :class => (options[:inline] ? 'inline' : 'block') ) end.join.html_safe end @@ -72,13 +73,13 @@ def setting_check_box(setting, options={}) setting_label(setting, options).html_safe + - hidden_field_tag("settings[#{setting}]", 0).html_safe + + hidden_field_tag("settings[#{setting}]", 0, :id => nil).html_safe + check_box_tag("settings[#{setting}]", 1, Setting.send("#{setting}?"), options).html_safe end def setting_label(setting, options={}) label = options.delete(:label) - label != false ? content_tag("label", l(label || "setting_#{setting}")).html_safe : '' + label != false ? label_tag("settings_#{setting}", l(label || "setting_#{setting}")).html_safe : '' end # Renders a notification field for a Redmine::Notifiable option @@ -86,8 +87,20 @@ return content_tag(:label, check_box_tag('settings[notified_events][]', notifiable.name, - Setting.notified_events.include?(notifiable.name)).html_safe + + Setting.notified_events.include?(notifiable.name), :id => nil).html_safe + l_or_humanize(notifiable.name, :prefix => 'label_').html_safe, :class => notifiable.parent.present? ? "parent" : '').html_safe end + + def cross_project_subtasks_options + options = [ + [:label_disabled, ''], + [:label_cross_project_system, 'system'], + [:label_cross_project_tree, 'tree'], + [:label_cross_project_hierarchy, 'hierarchy'], + [:label_cross_project_descendants, 'descendants'] + ] + + options.map {|label, value| [l(label), value.to_s]} + end end diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/sort_helper.rb --- a/app/helpers/sort_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/sort_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -89,6 +89,10 @@ sql.blank? ? nil : sql end + def to_a + @criteria.dup + end + def add!(key, asc) @criteria.delete_if {|k,o| k == key} @criteria = [[key, asc]] + @criteria @@ -160,7 +164,8 @@ # sort_clause. # - criteria can be either an array or a hash of allowed keys # - def sort_update(criteria) + def sort_update(criteria, sort_name=nil) + sort_name ||= self.sort_name @sort_criteria = SortCriteria.new @sort_criteria.available_criteria = criteria @sort_criteria.from_param(params[:sort] || session[sort_name]) @@ -181,6 +186,10 @@ @sort_criteria.to_sql end + def sort_criteria + @sort_criteria + end + # Returns a link which sorts by the named column. # # - column is the name of an attribute in the sorted record collection. diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/timelog_helper.rb --- a/app/helpers/timelog_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/timelog_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -77,6 +77,7 @@ [l(:label_yesterday), 'yesterday'], [l(:label_this_week), 'current_week'], [l(:label_last_week), 'last_week'], + [l(:label_last_n_weeks, 2), 'last_2_weeks'], [l(:label_last_n_days, 7), '7_days'], [l(:label_this_month), 'current_month'], [l(:label_last_month), 'last_month'], @@ -118,7 +119,7 @@ entry.hours.to_s.gsub('.', decimal_separator), entry.comments ] - fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) } + fields += custom_fields.collect {|f| show_value(entry.custom_field_values.detect {|v| v.custom_field_id == f.id}) } csv << fields.collect {|c| Redmine::CodesetUtil.from_utf8( c.to_s, @@ -128,10 +129,10 @@ export end - def format_criteria_value(criteria, value) + def format_criteria_value(criteria_options, value) if value.blank? - l(:label_none) - elsif k = @available_criterias[criteria][:klass] + "[#{l(:label_none)}]" + elsif k = criteria_options[:klass] obj = k.find_by_id(value.to_i) if obj.is_a?(Issue) obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}" @@ -139,28 +140,28 @@ obj end else - format_value(value, @available_criterias[criteria][:format]) + format_value(value, criteria_options[:format]) end end - def report_to_csv(criterias, periods, hours) + def report_to_csv(report) decimal_separator = l(:general_csv_decimal_separator) export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv| # Column headers - headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) } - headers += periods + headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) } + headers += report.periods headers << l(:label_total) csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8( c.to_s, l(:general_csv_encoding) ) } # Content - report_criteria_to_csv(csv, criterias, periods, hours) + report_criteria_to_csv(csv, report.available_criteria, report.columns, report.criteria, report.periods, report.hours) # Total row str_total = Redmine::CodesetUtil.from_utf8(l(:label_total), l(:general_csv_encoding)) - row = [ str_total ] + [''] * (criterias.size - 1) + row = [ str_total ] + [''] * (report.criteria.size - 1) total = 0 - periods.each do |period| - sum = sum_hours(select_hours(hours, @columns, period.to_s)) + report.periods.each do |period| + sum = sum_hours(select_hours(report.hours, report.columns, period.to_s)) total += sum row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '') end @@ -170,26 +171,26 @@ export end - def report_criteria_to_csv(csv, criterias, periods, hours, level=0) + def report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours, level=0) decimal_separator = l(:general_csv_decimal_separator) - hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value| - hours_for_value = select_hours(hours, criterias[level], value) + hours.collect {|h| h[criteria[level]].to_s}.uniq.each do |value| + hours_for_value = select_hours(hours, criteria[level], value) next if hours_for_value.empty? row = [''] * level row << Redmine::CodesetUtil.from_utf8( - format_criteria_value(criterias[level], value).to_s, + format_criteria_value(available_criteria[criteria[level]], value).to_s, l(:general_csv_encoding) ) - row += [''] * (criterias.length - level - 1) + row += [''] * (criteria.length - level - 1) total = 0 periods.each do |period| - sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)) + sum = sum_hours(select_hours(hours_for_value, columns, period.to_s)) total += sum row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '') end row << ("%.2f" % total).gsub('.',decimal_separator) csv << row - if criterias.length > level + 1 - report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1) + if criteria.length > level + 1 + report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours_for_value, level + 1) end end end diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/trackers_helper.rb --- a/app/helpers/trackers_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/trackers_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/users_helper.rb --- a/app/helpers/users_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/users_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -21,18 +21,9 @@ def users_status_options_for_select(selected) user_count_by_status = User.count(:group => 'status').to_hash options_for_select([[l(:label_all), ''], - ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", 1], - ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", 2], - ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", 3]], selected) - end - - # Options for the new membership projects combo-box - def options_for_membership_project_select(user, projects) - options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") - options << project_tree_options_for_select(projects) do |p| - {:disabled => (user.projects.include?(p))} - end - options + ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", '1'], + ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", '2'], + ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", '3']], selected.to_s) end def user_mail_notification_options(user) diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/versions_helper.rb --- a/app/helpers/versions_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/versions_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -19,10 +19,18 @@ module VersionsHelper - STATUS_BY_CRITERIAS = %w(category tracker status priority author assigned_to) + def version_anchor(version) + if @project == version.project + anchor version.name + else + anchor "#{version.project.try(:identifier)}-#{version.name}" + end + end + + STATUS_BY_CRITERIAS = %w(tracker status priority author assigned_to category) def render_issue_status_by(version, criteria) - criteria = 'category' unless STATUS_BY_CRITERIAS.include?(criteria) + criteria = 'tracker' unless STATUS_BY_CRITERIAS.include?(criteria) h = Hash.new {|k,v| k[v] = [0, 0]} begin @@ -36,7 +44,8 @@ rescue ActiveRecord::RecordNotFound # When grouping by an association, Rails throws this exception if there's no result (bug) end - counts = h.keys.compact.sort.collect {|k| {:group => k, :total => h[k][0], :open => h[k][1], :closed => (h[k][0] - h[k][1])}} + # Sort with nil keys in last position + counts = h.keys.sort {|a,b| a.nil? ? 1 : (b.nil? ? -1 : a <=> b)}.collect {|k| {:group => k, :total => h[k][0], :open => h[k][1], :closed => (h[k][0] - h[k][1])}} max = counts.collect {|c| c[:total]}.max render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max} diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/watchers_helper.rb --- a/app/helpers/watchers_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/watchers_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -30,10 +30,8 @@ :action => (watched ? 'unwatch' : 'watch'), :object_type => object.class.to_s.underscore, :object_id => object.id} - link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)), - {:url => url}, - :href => url_for(url), - :class => (watched ? 'icon icon-fav' : 'icon icon-fav-off')) + link_to((watched ? l(:button_unwatch) : l(:button_watch)), url, + :remote => true, :method => 'post', :class => (watched ? 'icon icon-fav' : 'icon icon-fav-off')) end @@ -45,22 +43,33 @@ # Returns a comma separated list of users watching the given object def watchers_list(object) remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project) + content = ''.html_safe lis = object.watcher_users.collect do |user| - s = avatar(user, :size => "16").to_s + link_to_user(user, :class => 'user').to_s + s = ''.html_safe + s << avatar(user, :size => "16").to_s + s << link_to_user(user, :class => 'user') if remove_allowed url = {:controller => 'watchers', :action => 'destroy', :object_type => object.class.to_s.underscore, :object_id => object.id, :user_id => user} - s += ' ' + link_to_remote(image_tag('delete.png'), - {:url => url}, - :href => url_for(url), - :style => "vertical-align: middle", - :class => "delete") + s << ' ' + s << link_to(image_tag('delete.png'), url, + :remote => true, :method => 'post', :style => "vertical-align: middle", :class => "delete") end - "
  • #{ s }
  • " + content << content_tag('li', s) end - lis.empty? ? "" : "
      #{ lis.join("\n") }
    " + content.present? ? content_tag('ul', content) : content + end + + def watchers_checkboxes(object, users, checked=nil) + users.map do |user| + c = checked.nil? ? object.watched_by?(user) : checked + tag = check_box_tag 'issue[watcher_user_ids][]', user.id, c, :id => nil + content_tag 'label', "#{tag} #{h(user)}".html_safe, + :id => "issue_watcher_user_ids_#{user.id}", + :class => "floating" + end.join.html_safe end end diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/welcome_helper.rb --- a/app/helpers/welcome_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/welcome_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/wiki_helper.rb --- a/app/helpers/wiki_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/wiki_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -21,14 +21,14 @@ def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0) pages = pages.group_by(&:parent) unless pages.is_a?(Hash) - s = '' + s = ''.html_safe if pages.has_key?(parent) pages[parent].each do |page| attrs = "value='#{page.id}'" attrs << " selected='selected'" if selected == page - indent = (level > 0) ? (' ' * level * 2 + '» ') : nil + indent = (level > 0) ? (' ' * level * 2 + '» ') : '' - s << "\n" + + s << content_tag('option', (indent + h(page.pretty_title)).html_safe, :value => page.id.to_s, :selected => selected == page) + wiki_page_options_for_select(pages, selected, page, level + 1) end end @@ -37,7 +37,7 @@ def wiki_page_breadcrumb(page) breadcrumb(page.ancestors.reverse.collect {|parent| - link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project}) + link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project, :version => nil}) }) end end diff -r ab89f95ef405 -r b2ea0641f798 app/helpers/workflows_helper.rb --- a/app/helpers/workflows_helper.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/helpers/workflows_helper.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,7 @@ # encoding: utf-8 # # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,4 +18,15 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module WorkflowsHelper + def field_required?(field) + field.is_a?(CustomField) ? field.is_required? : %w(project_id tracker_id subject priority_id is_private).include?(field) + end + + def field_permission_tag(permissions, status, field) + name = field.is_a?(CustomField) ? field.id.to_s : field + options = [["", ""], [l(:label_readonly), "readonly"]] + options << [l(:label_required), "required"] unless field_required?(field) + + select_tag("permissions[#{name}][#{status.id}]", options_for_select(options, permissions[status.id][name])) + end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/attachment.rb --- a/app/models/attachment.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/attachment.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -21,9 +21,10 @@ belongs_to :container, :polymorphic => true belongs_to :author, :class_name => "User", :foreign_key => "author_id" - validates_presence_of :container, :filename, :author + validates_presence_of :filename, :author validates_length_of :filename, :maximum => 255 validates_length_of :disk_filename, :maximum => 255 + validates_length_of :description, :maximum => 255 validate :validate_max_file_size acts_as_event :title => :filename, @@ -44,14 +45,25 @@ "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"} cattr_accessor :storage_path - @@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{Rails.root}/files" + @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files") + + cattr_accessor :thumbnails_storage_path + @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails") before_save :files_to_final_location after_destroy :delete_from_disk + # Returns an unsaved copy of the attachment + def copy(attributes=nil) + copy = self.class.new + copy.attributes = self.attributes.dup.except("id", "downloads") + copy.attributes = attributes if attributes + copy + end + def validate_max_file_size - if self.filesize > Setting.attachment_max_size.to_i.kilobytes - errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes) + if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes + errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes)) end end @@ -59,21 +71,33 @@ unless incoming_file.nil? @temp_file = incoming_file if @temp_file.size > 0 - self.filename = sanitize_filename(@temp_file.original_filename) - self.disk_filename = Attachment.disk_filename(filename) - self.content_type = @temp_file.content_type.to_s.chomp - if content_type.blank? + if @temp_file.respond_to?(:original_filename) + self.filename = @temp_file.original_filename + self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding) + end + if @temp_file.respond_to?(:content_type) + self.content_type = @temp_file.content_type.to_s.chomp + end + if content_type.blank? && filename.present? self.content_type = Redmine::MimeType.of(filename) end self.filesize = @temp_file.size end end end - + def file nil end + def filename=(arg) + write_attribute :filename, sanitize_filename(arg.to_s) + if new_record? && disk_filename.blank? + self.disk_filename = Attachment.disk_filename(filename) + end + filename + end + # Copies the temporary file to its final location # and computes its MD5 hash def files_to_final_location @@ -81,10 +105,15 @@ logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") md5 = Digest::MD5.new File.open(diskfile, "wb") do |f| - buffer = "" - while (buffer = @temp_file.read(8192)) - f.write(buffer) - md5.update(buffer) + if @temp_file.respond_to?(:read) + buffer = "" + while (buffer = @temp_file.read(8192)) + f.write(buffer) + md5.update(buffer) + end + else + f.write(@temp_file) + md5.update(@temp_file) end end self.digest = md5.hexdigest @@ -96,14 +125,24 @@ end end - # Deletes file on the disk + # Deletes the file from the file system if it's not referenced by other attachments def delete_from_disk - File.delete(diskfile) if !filename.blank? && File.exist?(diskfile) + if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty? + delete_from_disk! + end end # Returns file's location on disk def diskfile - "#{@@storage_path}/#{self.disk_filename}" + File.join(self.class.storage_path, disk_filename.to_s) + end + + def title + title = filename.to_s + if description.present? + title << " (#{description})" + end + title end def increment_download @@ -111,19 +150,55 @@ end def project - container.project + container.try(:project) end def visible?(user=User.current) - container.attachments_visible?(user) + container && container.attachments_visible?(user) end def deletable?(user=User.current) - container.attachments_deletable?(user) + container && container.attachments_deletable?(user) end def image? - self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i + !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i) + end + + def thumbnailable? + image? + end + + # Returns the full path the attachment thumbnail, or nil + # if the thumbnail cannot be generated. + def thumbnail(options={}) + if thumbnailable? && readable? + size = options[:size].to_i + if size > 0 + # Limit the number of thumbnails per image + size = (size / 50) * 50 + # Maximum thumbnail size + size = 800 if size > 800 + else + size = Setting.thumbnails_size.to_i + end + size = 100 unless size > 0 + target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb") + + begin + Redmine::Thumbnail.generate(self.diskfile, target, size) + rescue => e + logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger + return nil + end + end + end + + # Deletes all thumbnails + def self.clear_thumbnails + Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file| + File.delete file + end end def is_text? @@ -139,41 +214,52 @@ File.readable?(diskfile) end + # Returns the attachment token + def token + "#{id}.#{digest}" + end + + # Finds an attachment that matches the given token and that has no container + def self.find_by_token(token) + if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/ + attachment_id, attachment_digest = $1, $2 + attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first + if attachment && attachment.container.nil? + attachment + end + end + end + # Bulk attaches a set of files to an object # # Returns a Hash of the results: # :files => array of the attached files # :unsaved => array of the files that could not be attached def self.attach_files(obj, attachments) - attached = [] - if attachments && attachments.is_a?(Hash) - attachments.each_value do |attachment| - file = attachment['file'] - next unless file && file.size > 0 - a = Attachment.create(:container => obj, - :file => file, - :description => attachment['description'].to_s.strip, - :author => User.current) - obj.attachments << a - - if a.new_record? - obj.unsaved_attachments ||= [] - obj.unsaved_attachments << a - else - attached << a - end - end - end - {:files => attached, :unsaved => obj.unsaved_attachments} + result = obj.save_attachments(attachments, User.current) + obj.attach_saved_attachments + result end def self.latest_attach(attachments, filename) - attachments.sort_by(&:created_on).reverse.detect { + attachments.sort_by(&:created_on).reverse.detect { |att| att.filename.downcase == filename.downcase } end -private + def self.prune(age=1.day) + Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all + end + + private + + # Physically deletes the file from the file system + def delete_from_disk! + if disk_filename.present? && File.exist?(diskfile) + File.delete(diskfile) + end + end + def sanitize_filename(value) # get only the filename, not the whole path just_filename = value.gsub(/^.*(\\|\/)/, '') diff -r ab89f95ef405 -r b2ea0641f798 app/models/auth_source.rb --- a/app/models/auth_source.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/auth_source.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -15,7 +15,13 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Generic exception for when the AuthSource can not be reached +# (eg. can not connect to the LDAP) +class AuthSourceException < Exception; end +class AuthSourceTimeoutException < AuthSourceException; end + class AuthSource < ActiveRecord::Base + include Redmine::SubclassFactory include Redmine::Ciphering has_many :users @@ -53,7 +59,7 @@ # Try to authenticate a user not yet registered against available sources def self.authenticate(login, password) - AuthSource.find(:all, :conditions => ["onthefly_register=?", true]).each do |source| + AuthSource.where(:onthefly_register => true).all.each do |source| begin logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug? attrs = source.authenticate(login, password) diff -r ab89f95ef405 -r b2ea0641f798 app/models/auth_source_ldap.rb --- a/app/models/auth_source_ldap.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/auth_source_ldap.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -15,40 +15,49 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +require 'iconv' require 'net/ldap' -require 'iconv' +require 'net/ldap/dn' +require 'timeout' class AuthSourceLdap < AuthSource validates_presence_of :host, :port, :attr_login validates_length_of :name, :host, :maximum => 60, :allow_nil => true - validates_length_of :account, :account_password, :base_dn, :maximum => 255, :allow_nil => true + validates_length_of :account, :account_password, :base_dn, :filter, :maximum => 255, :allow_blank => true validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true validates_numericality_of :port, :only_integer => true + validates_numericality_of :timeout, :only_integer => true, :allow_blank => true + validate :validate_filter before_validation :strip_ldap_attributes - def after_initialize + def initialize(attributes=nil, *args) + super self.port = 389 if self.port == 0 end def authenticate(login, password) return nil if login.blank? || password.blank? - attrs = get_user_dn(login) - if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password) - logger.debug "Authentication successful for '#{login}'" if logger && logger.debug? - return attrs.except(:dn) + with_timeout do + attrs = get_user_dn(login, password) + if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password) + logger.debug "Authentication successful for '#{login}'" if logger && logger.debug? + return attrs.except(:dn) + end end - rescue Net::LDAP::LdapError => text - raise "LdapError: " + text + rescue Net::LDAP::LdapError => e + raise AuthSourceException.new(e.message) end # test the connection to the LDAP def test_connection - ldap_con = initialize_ldap_con(self.account, self.account_password) - ldap_con.open { } - rescue Net::LDAP::LdapError => text - raise "LdapError: " + text + with_timeout do + ldap_con = initialize_ldap_con(self.account, self.account_password) + ldap_con.open { } + end + rescue Net::LDAP::LdapError => e + raise AuthSourceException.new(e.message) end def auth_method_name @@ -57,6 +66,30 @@ private + def with_timeout(&block) + timeout = self.timeout + timeout = 20 unless timeout && timeout > 0 + Timeout.timeout(timeout) do + return yield + end + rescue Timeout::Error => e + raise AuthSourceTimeoutException.new(e.message) + end + + def ldap_filter + if filter.present? + Net::LDAP::Filter.construct(filter) + end + rescue Net::LDAP::LdapError + nil + end + + def validate_filter + if filter.present? && ldap_filter.nil? + errors.add(:filter, :invalid) + end + end + def strip_ldap_attributes [:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr| write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil? @@ -100,14 +133,24 @@ end # Get the user's dn and any attributes for them, given their login - def get_user_dn(login) - ldap_con = initialize_ldap_con(self.account, self.account_password) + def get_user_dn(login, password) + ldap_con = nil + if self.account && self.account.include?("$login") + ldap_con = initialize_ldap_con(self.account.sub("$login", Net::LDAP::DN.escape(login)), password) + else + ldap_con = initialize_ldap_con(self.account, self.account_password) + end login_filter = Net::LDAP::Filter.eq( self.attr_login, login ) object_filter = Net::LDAP::Filter.eq( "objectClass", "*" ) attrs = {} + search_filter = object_filter & login_filter + if f = ldap_filter + search_filter = search_filter & f + end + ldap_con.search( :base => self.base_dn, - :filter => object_filter & login_filter, + :filter => search_filter, :attributes=> search_attributes) do |entry| if onthefly_register? diff -r ab89f95ef405 -r b2ea0641f798 app/models/board.rb --- a/app/models/board.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/board.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -21,26 +21,37 @@ has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC" has_many :messages, :dependent => :destroy, :order => "#{Message.table_name}.created_on DESC" belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id - acts_as_list :scope => :project_id + acts_as_tree :dependent => :nullify + acts_as_list :scope => '(project_id = #{project_id} AND parent_id #{parent_id ? "= #{parent_id}" : "IS NULL"})' acts_as_watchable validates_presence_of :name, :description validates_length_of :name, :maximum => 30 validates_length_of :description, :maximum => 255 + validate :validate_board - named_scope :visible, lambda {|*args| { :include => :project, + scope :visible, lambda {|*args| { :include => :project, :conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } } - safe_attributes 'name', 'description', 'move_to' + safe_attributes 'name', 'description', 'parent_id', 'move_to' def visible?(user=User.current) !user.nil? && user.allowed_to?(:view_messages, project) end + def reload(*args) + @valid_parents = nil + super + end + def to_s name end + def valid_parents + @valid_parents ||= project.boards - self_and_descendants + end + def reset_counters! self.class.reset_counters!(id) end @@ -53,4 +64,26 @@ " last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})", ["id = ?", board_id]) end + + def self.board_tree(boards, parent_id=nil, level=0) + tree = [] + boards.select {|board| board.parent_id == parent_id}.sort_by(&:position).each do |board| + tree << [board, level] + tree += board_tree(boards, board.id, level+1) + end + if block_given? + tree.each do |board, level| + yield board, level + end + end + tree + end + + protected + + def validate_board + if parent_id && parent_id_changed? + errors.add(:parent_id, :invalid) unless valid_parents.include?(parent) + end + end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/change.rb --- a/app/models/change.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/change.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/changeset.rb --- a/app/models/changeset.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/changeset.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -20,7 +20,7 @@ class Changeset < ActiveRecord::Base belongs_to :repository belongs_to :user - has_many :changes, :dependent => :delete_all + has_many :filechanges, :class_name => 'Change', :dependent => :delete_all has_and_belongs_to_many :issues has_and_belongs_to_many :parents, :class_name => "Changeset", @@ -31,10 +31,10 @@ :join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}", :association_foreign_key => 'changeset_id', :foreign_key => 'parent_id' - acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))}, + acts_as_event :title => Proc.new {|o| o.title}, :description => :long_comments, :datetime => :committed_on, - :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}} + :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :repository_id => o.repository.identifier_param, :rev => o.identifier}} acts_as_searchable :columns => 'comments', :include => {:repository => :project}, @@ -49,7 +49,8 @@ validates_uniqueness_of :revision, :scope => :repository_id validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true - named_scope :visible, lambda {|*args| { :include => {:repository => :project}, + scope :visible, + lambda {|*args| { :include => {:repository => :project}, :conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } } after_create :scan_for_issues @@ -157,26 +158,30 @@ else "r#{revision}" end + if repository && repository.identifier.present? + tag = "#{repository.identifier}|#{tag}" + end if ref_project && project && ref_project != project - tag = "#{project.identifier}:#{tag}" + tag = "#{project.identifier}:#{tag}" end tag end + # Returns the title used for the changeset in the activity/search results + def title + repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : '' + comm = short_comments.blank? ? '' : (': ' + short_comments) + "#{l(:label_revision)} #{format_identifier}#{repo}#{comm}" + end + # Returns the previous changeset def previous - @previous ||= Changeset.find(:first, - :conditions => ['id < ? AND repository_id = ?', - self.id, self.repository_id], - :order => 'id DESC') + @previous ||= Changeset.where(["id < ? AND repository_id = ?", id, repository_id]).order('id DESC').first end # Returns the next changeset def next - @next ||= Changeset.find(:first, - :conditions => ['id > ? AND repository_id = ?', - self.id, self.repository_id], - :order => 'id ASC') + @next ||= Changeset.where(["id > ? AND repository_id = ?", id, repository_id]).order('id ASC').first end # Creates a new Change from it's common parameters @@ -188,14 +193,14 @@ :from_revision => change[:from_revision]) end - private - # Finds an issue that can be referenced by the commit message - # i.e. an issue that belong to the repository project, a subproject or a parent project def find_referenced_issue_by_id(id) return nil if id.blank? issue = Issue.find_by_id(id.to_i, :include => :project) - if issue + if Setting.commit_cross_project_ref? + # all issues can be referenced/fixed + elsif issue + # issue that belong to the repository project, a subproject or a parent project only unless issue.project && (project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project)) @@ -205,6 +210,8 @@ issue end + private + def fix_issue(issue) status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i) if status.nil? diff -r ab89f95ef405 -r b2ea0641f798 app/models/comment.rb --- a/app/models/comment.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/comment.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/comment_observer.rb --- a/app/models/comment_observer.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/comment_observer.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,7 +18,7 @@ class CommentObserver < ActiveRecord::Observer def after_create(comment) if comment.commented.is_a?(News) && Setting.notified_events.include?('news_comment_added') - Mailer.deliver_news_comment_added(comment) + Mailer.news_comment_added(comment).deliver end end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/custom_field.rb --- a/app/models/custom_field.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/custom_field.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,6 +16,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class CustomField < ActiveRecord::Base + include Redmine::SubclassFactory + has_many :custom_values, :dependent => :delete_all acts_as_list :scope => 'type = \'#{self.class}\'' serialize :possible_values @@ -25,20 +27,46 @@ validates_length_of :name, :maximum => 30 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats - validate :validate_values + validate :validate_custom_field + before_validation :set_searchable - def initialize(attributes = nil) - super - self.possible_values ||= [] + CUSTOM_FIELDS_TABS = [ + {:name => 'IssueCustomField', :partial => 'custom_fields/index', + :label => :label_issue_plural}, + {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', + :label => :label_spent_time}, + {:name => 'ProjectCustomField', :partial => 'custom_fields/index', + :label => :label_project_plural}, + {:name => 'VersionCustomField', :partial => 'custom_fields/index', + :label => :label_version_plural}, + {:name => 'UserCustomField', :partial => 'custom_fields/index', + :label => :label_user_plural}, + {:name => 'GroupCustomField', :partial => 'custom_fields/index', + :label => :label_group_plural}, + {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', + :label => TimeEntryActivity::OptionName}, + {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', + :label => IssuePriority::OptionName}, + {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', + :label => DocumentCategory::OptionName} + ] + + CUSTOM_FIELDS_NAMES = CUSTOM_FIELDS_TABS.collect{|v| v[:name]} + + def field_format=(arg) + # cannot change format of a saved custom field + super if new_record? end - def before_validation + def set_searchable # make sure these fields are not searchable self.searchable = false if %w(int float date bool).include?(field_format) + # make sure only these fields can have multiple values + self.multiple = false unless %w(list user version).include?(field_format) true end - def validate_values + def validate_custom_field if self.field_format == "list" errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty? errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array @@ -52,10 +80,9 @@ end end - # validate default value - v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil) - v.custom_field.is_required = false - errors.add(:default_value, :invalid) unless v.valid? + if default_value.present? && !valid_field_value?(default_value) + errors.add(:default_value, :invalid) + end end def possible_values_options(obj=nil) @@ -69,12 +96,14 @@ obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]} end elsif obj.is_a?(Array) - obj.collect {|o| possible_values_options(o)}.inject {|memo, v| memo & v} + obj.collect {|o| possible_values_options(o)}.reduce(:&) else [] end + when 'bool' + [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']] else - read_attribute :possible_values + possible_values || [] end end @@ -82,15 +111,23 @@ case field_format when 'user', 'version' possible_values_options(obj).collect(&:last) + when 'bool' + ['1', '0'] else - read_attribute :possible_values + values = super() + if values.is_a?(Array) + values.each do |value| + value.force_encoding('UTF-8') if value.respond_to?(:force_encoding) + end + end + values || [] end end # Makes possible_values accept a multiline string def possible_values=(arg) if arg.is_a?(Array) - write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?}) + super(arg.compact.collect(&:strip).select {|v| !v.blank?}) else self.possible_values = arg.to_s.split(/[\n\r]+/) end @@ -117,15 +154,32 @@ casted end + def value_from_keyword(keyword, customized) + possible_values_options = possible_values_options(customized) + if possible_values_options.present? + keyword = keyword.to_s.downcase + if v = possible_values_options.detect {|text, id| text.downcase == keyword} + if v.is_a?(Array) + v.last + else + v + end + end + else + keyword + end + end + # Returns a ORDER BY clause that can used to sort customized # objects by their value of the custom field. - # Returns false, if the custom field can not be used for sorting. + # Returns nil if the custom field can not be used for sorting. def order_statement + return nil if multiple? case field_format when 'string', 'text', 'list', 'date', 'bool' # COALESCE is here to make sure that blank and NULL values are sorted equally "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" + - " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" + + " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" + " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" + " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')" when 'int', 'float' @@ -133,18 +187,74 @@ # Postgresql will raise an error if a value can not be casted! # CustomValue validations should ensure that it doesn't occur "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" + - " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" + + " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" + " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" + " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)" + when 'user', 'version' + value_class.fields_for_order_statement(value_join_alias) else nil end end + # Returns a GROUP BY clause that can used to group by custom value + # Returns nil if the custom field can not be used for grouping. + def group_statement + return nil if multiple? + case field_format + when 'list', 'date', 'bool', 'int' + order_statement + when 'user', 'version' + "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" + + " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" + + " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" + + " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')" + else + nil + end + end + + def join_for_order_statement + case field_format + when 'user', 'version' + "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" + + " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" + + " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" + + " AND #{join_alias}.custom_field_id = #{id}" + + " AND #{join_alias}.value <> ''" + + " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" + + " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" + + " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" + + " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" + + " LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" + + " ON CAST(#{join_alias}.value as decimal(60,0)) = #{value_join_alias}.id" + else + nil + end + end + + def join_alias + "cf_#{id}" + end + + def value_join_alias + join_alias + "_" + field_format + end + def <=>(field) position <=> field.position end + # Returns the class that values represent + def value_class + case field_format + when 'user', 'version' + field_format.classify.constantize + else + nil + end + end + def self.customized_class self.name =~ /^(.+)CustomField$/ begin; $1.constantize; rescue nil; end @@ -158,4 +268,59 @@ def type_name nil end + + # Returns the error messages for the given value + # or an empty array if value is a valid value for the custom field + def validate_field_value(value) + errs = [] + if value.is_a?(Array) + if !multiple? + errs << ::I18n.t('activerecord.errors.messages.invalid') + end + if is_required? && value.detect(&:present?).nil? + errs << ::I18n.t('activerecord.errors.messages.blank') + end + value.each {|v| errs += validate_field_value_format(v)} + else + if is_required? && value.blank? + errs << ::I18n.t('activerecord.errors.messages.blank') + end + errs += validate_field_value_format(value) + end + errs + end + + # Returns true if value is a valid value for the custom field + def valid_field_value?(value) + validate_field_value(value).empty? + end + + def format_in?(*args) + args.include?(field_format) + end + + protected + + # Returns the error message for the given value regarding its format + def validate_field_value_format(value) + errs = [] + if value.present? + errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp) + errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length + errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length + + # Format specific validations + case field_format + when 'int' + errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/ + when 'float' + begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end + when 'date' + errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end + when 'list' + errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value) + end + end + errs + end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/custom_field_value.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/models/custom_field_value.rb Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,50 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class CustomFieldValue + attr_accessor :custom_field, :customized, :value + + def custom_field_id + custom_field.id + end + + def true? + self.value == '1' + end + + def editable? + custom_field.editable? + end + + def visible? + custom_field.visible? + end + + def required? + custom_field.is_required? + end + + def to_s + value.to_s + end + + def validate_value + custom_field.validate_field_value(value).each do |message| + customized.errors.add(:base, custom_field.name + ' ' + message) + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 app/models/custom_value.rb --- a/app/models/custom_value.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/custom_value.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -19,9 +19,8 @@ belongs_to :custom_field belongs_to :customized, :polymorphic => true - validate :validate_custom_value - - def after_initialize + def initialize(attributes=nil, *args) + super if new_record? && custom_field && (customized_type.blank? || (customized && customized.new_record?)) self.value ||= custom_field.default_value end @@ -47,27 +46,4 @@ def to_s value.to_s end - -protected - def validate_custom_value - if value.blank? - errors.add(:value, :blank) if custom_field.is_required? and value.blank? - else - errors.add(:value, :invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp) - errors.add(:value, :too_short, :count => custom_field.min_length) if custom_field.min_length > 0 and value.length < custom_field.min_length - errors.add(:value, :too_long, :count => custom_field.max_length) if custom_field.max_length > 0 and value.length > custom_field.max_length - - # Format specific validations - case custom_field.field_format - when 'int' - errors.add(:value, :not_a_number) unless value =~ /^[+-]?\d+$/ - when 'float' - begin; Kernel.Float(value); rescue; errors.add(:value, :invalid) end - when 'date' - errors.add(:value, :not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end - when 'list' - errors.add(:value, :inclusion) unless custom_field.possible_values.include?(value) - end - end - end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/document.rb --- a/app/models/document.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/document.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ -# RedMine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -30,7 +30,7 @@ validates_presence_of :project, :title, :category validates_length_of :title, :maximum => 60 - named_scope :visible, lambda {|*args| { :include => :project, + scope :visible, lambda {|*args| { :include => :project, :conditions => Project.allowed_to_condition(args.shift || User.current, :view_documents, *args) } } safe_attributes 'category_id', 'title', 'description' @@ -39,7 +39,8 @@ !user.nil? && user.allowed_to?(:view_documents, project) end - def after_initialize + def initialize(attributes=nil, *args) + super if new_record? self.category ||= DocumentCategory.default end @@ -47,7 +48,7 @@ def updated_on unless @updated_on - a = attachments.find(:first, :order => 'created_on DESC') + a = attachments.last @updated_on = (a && a.created_on) || created_on end @updated_on diff -r ab89f95ef405 -r b2ea0641f798 app/models/document_category.rb --- a/app/models/document_category.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/document_category.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -31,4 +31,10 @@ def transfer_relations(to) documents.update_all("category_id = #{to.id}") end + + def self.default + d = super + d = first if d.nil? + d + end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/document_category_custom_field.rb --- a/app/models/document_category_custom_field.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/document_category_custom_field.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/document_observer.rb --- a/app/models/document_observer.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/document_observer.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,6 +17,6 @@ class DocumentObserver < ActiveRecord::Observer def after_create(document) - Mailer.deliver_document_added(document) if Setting.notified_events.include?('document_added') + Mailer.document_added(document).deliver if Setting.notified_events.include?('document_added') end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/enabled_module.rb --- a/app/models/enabled_module.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/enabled_module.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/enumeration.rb --- a/app/models/enumeration.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/enumeration.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,6 +16,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class Enumeration < ActiveRecord::Base + include Redmine::SubclassFactory + default_scope :order => "#{Enumeration.table_name}.position ASC" belongs_to :project @@ -27,23 +29,26 @@ before_destroy :check_integrity before_save :check_default + attr_protected :type + validates_presence_of :name validates_uniqueness_of :name, :scope => [:type, :project_id] validates_length_of :name, :maximum => 30 - named_scope :shared, :conditions => { :project_id => nil } - named_scope :active, :conditions => { :active => true } - named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}} + scope :shared, where(:project_id => nil) + scope :sorted, order("#{table_name}.position ASC") + scope :active, where(:active => true) + scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)} def self.default # Creates a fake default scope so Enumeration.default will check # it's type. STI subclasses will automatically add their own # types to the finder. if self.descends_from_active_record? - find(:first, :conditions => { :is_default => true, :type => 'Enumeration' }) + where(:is_default => true, :type => 'Enumeration').first else # STI classes are - find(:first, :conditions => { :is_default => true }) + where(:is_default => true).first end end @@ -54,7 +59,7 @@ def check_default if is_default? && is_default_changed? - Enumeration.update_all("is_default = #{connection.quoted_false}", {:type => type}) + Enumeration.update_all({:is_default => false}, {:type => type}) end end @@ -94,7 +99,7 @@ # # Note: subclasses is protected in ActiveRecord def self.get_subclasses - @@subclasses[Enumeration] + subclasses end # Does the +new+ Hash override the previous Enumeration? diff -r ab89f95ef405 -r b2ea0641f798 app/models/group.rb --- a/app/models/group.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/group.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,6 +16,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class Group < Principal + include Redmine::SafeAttributes + has_and_belongs_to_many :users, :after_add => :user_added, :after_remove => :user_removed @@ -27,11 +29,25 @@ before_destroy :remove_references_before_destroy + scope :sorted, order("#{table_name}.lastname ASC") + + safe_attributes 'name', + 'user_ids', + 'custom_field_values', + 'custom_fields', + :if => lambda {|group, user| user.admin?} + def to_s lastname.to_s end - alias :name :to_s + def name + lastname + end + + def name=(arg) + self.lastname = arg + end def user_added(user) members.each do |member| @@ -51,12 +67,12 @@ end end - def self.human_attribute_name(attribute_key_name) - attr_name = attribute_key_name + def self.human_attribute_name(attribute_key_name, *args) + attr_name = attribute_key_name.to_s if attr_name == 'lastname' attr_name = "name" end - super(attr_name) + super(attr_name, *args) end private diff -r ab89f95ef405 -r b2ea0641f798 app/models/group_custom_field.rb --- a/app/models/group_custom_field.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/group_custom_field.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/issue.rb --- a/app/models/issue.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/issue.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,6 +17,7 @@ class Issue < ActiveRecord::Base include Redmine::SafeAttributes + include Redmine::Utils::DateCalculation belongs_to :project belongs_to :tracker @@ -28,6 +29,14 @@ belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' has_many :journals, :as => :journalized, :dependent => :destroy + has_many :visible_journals, + :class_name => 'Journal', + :as => :journalized, + :conditions => Proc.new { + ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false] + }, + :readonly => true + has_many :time_entries, :dependent => :delete_all has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC" @@ -39,7 +48,7 @@ acts_as_customizable acts_as_watchable acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"], - :include => [:project, :journals], + :include => [:project, :visible_journals], # sort by id so that limited eager loading doesn't break with postgresql :order_column => "#{table_name}.id" acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"}, @@ -52,55 +61,54 @@ DONE_RATIO_OPTIONS = %w(issue_field issue_status) attr_reader :current_journal + delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true validates_presence_of :subject, :priority, :project, :tracker, :author, :status validates_length_of :subject, :maximum => 255 validates_inclusion_of :done_ratio, :in => 0..100 validates_numericality_of :estimated_hours, :allow_nil => true - validate :validate_issue + validate :validate_issue, :validate_required_fields - named_scope :visible, lambda {|*args| { :include => :project, - :conditions => Issue.visible_condition(args.shift || User.current, *args) } } + scope :visible, + lambda {|*args| { :include => :project, + :conditions => Issue.visible_condition(args.shift || User.current, *args) } } - named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status - - named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC" - named_scope :with_limit, lambda { |limit| { :limit => limit} } - named_scope :on_active_project, :include => [:status, :project, :tracker], - :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"] - - named_scope :without_version, lambda { - { - :conditions => { :fixed_version_id => nil} - } + scope :open, lambda {|*args| + is_closed = args.size > 0 ? !args.first : false + {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status} } - named_scope :with_query, lambda {|query| - { - :conditions => Query.merge_conditions(query.statement) - } - } + scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC" + scope :on_active_project, :include => [:status, :project, :tracker], + :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"] before_create :default_assign - before_save :close_duplicates, :update_done_ratio_from_issue_status + before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change + after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?} after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal + # Should be after_create but would be called before previous after_save callbacks + after_save :after_create_from_copy after_destroy :update_parent_attributes # Returns a SQL conditions string used to find all issues visible by the specified user def self.visible_condition(user, options={}) Project.allowed_to_condition(user, :view_issues, options) do |role, user| - case role.issues_visibility - when 'all' - nil - when 'default' - user_ids = [user.id] + user.groups.map(&:id) - "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))" - when 'own' - user_ids = [user.id] + user.groups.map(&:id) - "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))" + if user.logged? + case role.issues_visibility + when 'all' + nil + when 'default' + user_ids = [user.id] + user.groups.map(&:id) + "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))" + when 'own' + user_ids = [user.id] + user.groups.map(&:id) + "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))" + else + '1=0' + end else - '1=0' + "(#{table_name}.is_private = #{connection.quoted_false})" end end end @@ -108,115 +116,127 @@ # Returns true if usr or current user is allowed to view the issue def visible?(usr=nil) (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user| - case role.issues_visibility - when 'all' - true - when 'default' - !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to) - when 'own' - self.author == user || user.is_or_belongs_to?(assigned_to) + if user.logged? + case role.issues_visibility + when 'all' + true + when 'default' + !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to)) + when 'own' + self.author == user || user.is_or_belongs_to?(assigned_to) + else + false + end else - false + !self.is_private? end end end - def after_initialize + def initialize(attributes=nil, *args) + super if new_record? # set default values for new records only self.status ||= IssueStatus.default self.priority ||= IssuePriority.default + self.watcher_user_ids = [] end end + # AR#Persistence#destroy would raise and RecordNotFound exception + # if the issue was already deleted or updated (non matching lock_version). + # This is a problem when bulk deleting issues or deleting a project + # (because an issue may already be deleted if its parent was deleted + # first). + # The issue is reloaded by the nested_set before being deleted so + # the lock_version condition should not be an issue but we handle it. + def destroy + super + rescue ActiveRecord::RecordNotFound + # Stale or already deleted + begin + reload + rescue ActiveRecord::RecordNotFound + # The issue was actually already deleted + @destroyed = true + return freeze + end + # The issue was stale, retry to destroy + super + end + + def reload(*args) + @workflow_rule_by_attribute = nil + @assignable_versions = nil + super + end + # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields def available_custom_fields (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : [] end - def copy_from(arg) + # Copies attributes from another issue, arg can be an id or an Issue + def copy_from(arg, options={}) issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg) self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on") self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} self.status = issue.status + self.author = User.current + unless options[:attachments] == false + self.attachments = issue.attachments.map do |attachement| + attachement.copy(:container => self) + end + end + @copied_from = issue + @copy_options = options self end + # Returns an unsaved copy of the issue + def copy(attributes=nil, copy_options={}) + copy = self.class.new.copy_from(self, copy_options) + copy.attributes = attributes if attributes + copy + end + + # Returns true if the issue is a copy + def copy? + @copied_from.present? + end + # Moves/copies an issue to a new project and tracker # Returns the moved/copied issue on success, false on failure - def move_to_project(*args) - ret = Issue.transaction do - move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback) - end || false - end + def move_to_project(new_project, new_tracker=nil, options={}) + ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead." - def move_to_project_without_transaction(new_project, new_tracker = nil, options = {}) - options ||= {} - issue = options[:copy] ? self.class.new.copy_from(self) : self + if options[:copy] + issue = self.copy + else + issue = self + end - if new_project && issue.project_id != new_project.id - # delete issue relations - unless Setting.cross_project_issue_relations? - issue.relations_from.clear - issue.relations_to.clear - end - # issue is moved to another project - # reassign to the category with same name if any - new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name) - issue.category = new_category - # Keep the fixed_version if it's still valid in the new_project - unless new_project.shared_versions.include?(issue.fixed_version) - issue.fixed_version = nil - end - issue.project = new_project - if issue.parent && issue.parent.project_id != issue.project_id - issue.parent_issue_id = nil - end - end + issue.init_journal(User.current, options[:notes]) + + # Preserve previous behaviour + # #move_to_project doesn't change tracker automatically + issue.send :project=, new_project, true if new_tracker issue.tracker = new_tracker - issue.reset_custom_values! - end - if options[:copy] - issue.author = User.current - issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} - issue.status = if options[:attributes] && options[:attributes][:status_id] - IssueStatus.find_by_id(options[:attributes][:status_id]) - else - self.status - end end # Allow bulk setting of attributes on the issue if options[:attributes] issue.attributes = options[:attributes] end - if issue.save - if options[:copy] - if current_journal && current_journal.notes.present? - issue.init_journal(current_journal.user, current_journal.notes) - issue.current_journal.notify = false - issue.save - end - else - # Manually update project_id on related time entries - TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id}) - issue.children.each do |child| - unless child.move_to_project_without_transaction(new_project) - # Move failed and transaction was rollback'd - return false - end - end - end - else - return false - end - issue + issue.save ? issue : false end def status_id=(sid) self.status = nil - write_attribute(:status_id, sid) + result = write_attribute(:status_id, sid) + @workflow_rule_by_attribute = nil + result end def priority_id=(pid) @@ -224,13 +244,56 @@ write_attribute(:priority_id, pid) end + def category_id=(cid) + self.category = nil + write_attribute(:category_id, cid) + end + + def fixed_version_id=(vid) + self.fixed_version = nil + write_attribute(:fixed_version_id, vid) + end + def tracker_id=(tid) self.tracker = nil result = write_attribute(:tracker_id, tid) @custom_field_values = nil + @workflow_rule_by_attribute = nil result end + def project_id=(project_id) + if project_id.to_s != self.project_id.to_s + self.project = (project_id.present? ? Project.find_by_id(project_id) : nil) + end + end + + def project=(project, keep_tracker=false) + project_was = self.project + write_attribute(:project_id, project ? project.id : nil) + association_instance_set('project', project) + if project_was && project && project_was != project + @assignable_versions = nil + + unless keep_tracker || project.trackers.include?(tracker) + self.tracker = project.trackers.first + end + # Reassign to the category with same name if any + if category + self.category = project.issue_categories.find_by_name(category.name) + end + # Keep the fixed_version if it's still valid in the new_project + if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version) + self.fixed_version = nil + end + # Clear the parent task if it's no longer valid + unless valid_parent_project? + self.parent_issue_id = nil + end + @custom_field_values = nil + end + end + def description=(arg) if arg.is_a?(String) arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n") @@ -238,25 +301,38 @@ write_attribute(:description, arg) end - # Overrides attributes= so that tracker_id gets assigned first - def attributes_with_tracker_first=(new_attributes, *args) + # Overrides assign_attributes so that project and tracker get assigned first + def assign_attributes_with_project_and_tracker_first(new_attributes, *args) return if new_attributes.nil? - new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id] - if new_tracker_id - self.tracker_id = new_tracker_id + attrs = new_attributes.dup + attrs.stringify_keys! + + %w(project project_id tracker tracker_id).each do |attr| + if attrs.has_key?(attr) + send "#{attr}=", attrs.delete(attr) + end end - send :attributes_without_tracker_first=, new_attributes, *args + send :assign_attributes_without_project_and_tracker_first, attrs, *args end # Do not redefine alias chain on reload (see #4838) - alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=) + alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first) def estimated_hours=(h) write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) end + safe_attributes 'project_id', + :if => lambda {|issue, user| + if issue.new_record? + issue.copy? + elsif user.allowed_to?(:move_issues, issue.project) + projects = Issue.allowed_target_projects_on_move(user) + projects.include?(issue.project) && projects.size > 1 + end + } + safe_attributes 'tracker_id', 'status_id', - 'parent_issue_id', 'category_id', 'assigned_to_id', 'priority_id', @@ -270,58 +346,169 @@ 'custom_field_values', 'custom_fields', 'lock_version', + 'notes', :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) } safe_attributes 'status_id', 'assigned_to_id', 'fixed_version_id', 'done_ratio', + 'lock_version', + 'notes', :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? } + safe_attributes 'notes', + :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)} + + safe_attributes 'private_notes', + :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)} + + safe_attributes 'watcher_user_ids', + :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)} + safe_attributes 'is_private', :if => lambda {|issue, user| user.allowed_to?(:set_issues_private, issue.project) || (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project)) } + safe_attributes 'parent_issue_id', + :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) && + user.allowed_to?(:manage_subtasks, issue.project)} + + def safe_attribute_names(user=nil) + names = super + names -= disabled_core_fields + names -= read_only_attribute_names(user) + names + end + # Safely sets attributes # Should be called from controllers instead of #attributes= # attr_accessible is too rough because we still want things like # Issue.new(:project => foo) to work - # TODO: move workflow/permission checks from controllers to here def safe_attributes=(attrs, user=User.current) return unless attrs.is_a?(Hash) - # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed - attrs = delete_unsafe_attributes(attrs, user) - return if attrs.empty? + attrs = attrs.dup - # Tracker must be set before since new_statuses_allowed_to depends on it. - if t = attrs.delete('tracker_id') + # Project and Tracker must be set before since new_statuses_allowed_to depends on it. + if (p = attrs.delete('project_id')) && safe_attribute?('project_id') + if allowed_target_projects(user).collect(&:id).include?(p.to_i) + self.project_id = p + end + end + + if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id') self.tracker_id = t end - if attrs['status_id'] - unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i) - attrs.delete('status_id') + if (s = attrs.delete('status_id')) && safe_attribute?('status_id') + if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i) + self.status_id = s end end + attrs = delete_unsafe_attributes(attrs, user) + return if attrs.empty? + unless leaf? attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)} end - if attrs.has_key?('parent_issue_id') - if !user.allowed_to?(:manage_subtasks, project) - attrs.delete('parent_issue_id') - elsif !attrs['parent_issue_id'].blank? - attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i) + if attrs['parent_issue_id'].present? + s = attrs['parent_issue_id'].to_s + unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1])) + @invalid_parent_issue_id = attrs.delete('parent_issue_id') end end - self.attributes = attrs + if attrs['custom_field_values'].present? + attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s} + end + + if attrs['custom_fields'].present? + attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s} + end + + # mass-assignment security bypass + assign_attributes attrs, :without_protection => true end + def disabled_core_fields + tracker ? tracker.disabled_core_fields : [] + end + + # Returns the custom_field_values that can be edited by the given user + def editable_custom_field_values(user=nil) + custom_field_values.reject do |value| + read_only_attribute_names(user).include?(value.custom_field_id.to_s) + end + end + + # Returns the names of attributes that are read-only for user or the current user + # For users with multiple roles, the read-only fields are the intersection of + # read-only fields of each role + # The result is an array of strings where sustom fields are represented with their ids + # + # Examples: + # issue.read_only_attribute_names # => ['due_date', '2'] + # issue.read_only_attribute_names(user) # => [] + def read_only_attribute_names(user=nil) + workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys + end + + # Returns the names of required attributes for user or the current user + # For users with multiple roles, the required fields are the intersection of + # required fields of each role + # The result is an array of strings where sustom fields are represented with their ids + # + # Examples: + # issue.required_attribute_names # => ['due_date', '2'] + # issue.required_attribute_names(user) # => [] + def required_attribute_names(user=nil) + workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys + end + + # Returns true if the attribute is required for user + def required_attribute?(name, user=nil) + required_attribute_names(user).include?(name.to_s) + end + + # Returns a hash of the workflow rule by attribute for the given user + # + # Examples: + # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'} + def workflow_rule_by_attribute(user=nil) + return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil? + + user_real = user || User.current + roles = user_real.admin ? Role.all : user_real.roles_for_project(project) + return {} if roles.empty? + + result = {} + workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all + if workflow_permissions.any? + workflow_rules = workflow_permissions.inject({}) do |h, wp| + h[wp.field_name] ||= [] + h[wp.field_name] << wp.rule + h + end + workflow_rules.each do |attr, rules| + next if rules.size < roles.size + uniq_rules = rules.uniq + if uniq_rules.size == 1 + result[attr] = uniq_rules.first + else + result[attr] = 'required' + end + end + end + @workflow_rule_by_attribute = result if user.nil? + result + end + private :workflow_rule_by_attribute + def done_ratio if Issue.use_status_for_done_ratio? && status && status.default_done_ratio status.default_done_ratio @@ -339,11 +526,15 @@ end def validate_issue - if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? + if due_date.nil? && @attributes['due_date'].present? errors.add :due_date, :not_a_date end - if self.due_date and self.start_date and self.due_date < self.start_date + if start_date.nil? && @attributes['start_date'].present? + errors.add :start_date, :not_a_date + end + + if due_date && start_date && due_date < start_date errors.add :due_date, :greater_than_start_date end @@ -367,9 +558,11 @@ end # Checks parent issue assignment - if @parent_issue - if @parent_issue.project_id != project_id - errors.add :parent_issue_id, :not_same_project + if @invalid_parent_issue_id.present? + errors.add :parent_issue_id, :invalid + elsif @parent_issue + if !valid_parent_project?(@parent_issue) + errors.add :parent_issue_id, :invalid elsif !new_record? # moving an existing issue if @parent_issue.root_id != root_id @@ -377,7 +570,26 @@ elsif move_possible?(@parent_issue) # move accepted inside tree else - errors.add :parent_issue_id, :not_a_valid_parent + errors.add :parent_issue_id, :invalid + end + end + end + end + + # Validates the issue against additional workflow requirements + def validate_required_fields + user = new_record? ? author : current_journal.try(:user) + + required_attribute_names(user).each do |attribute| + if attribute =~ /^\d+$/ + attribute = attribute.to_i + v = custom_field_values.detect {|v| v.custom_field_id == attribute } + if v && v.value.blank? + errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank') + end + else + if respond_to?(attribute) && send(attribute).blank? + errors.add attribute, :blank end end end @@ -393,15 +605,34 @@ def init_journal(user, notes = "") @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) - @issue_before_change = self.clone - @issue_before_change.status = self.status - @custom_values_before_change = {} - self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value } - # Make sure updated_on is updated when adding a note. - updated_on_will_change! + if new_record? + @current_journal.notify = false + else + @attributes_before_change = attributes.dup + @custom_values_before_change = {} + self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value } + end @current_journal end + # Returns the id of the last journal or nil + def last_journal_id + if new_record? + nil + else + journals.maximum(:id) + end + end + + # Returns a scope for journals that have an id greater than journal_id + def journals_after(journal_id) + scope = journals.reorder("#{Journal.table_name}.id ASC") + if journal_id.present? + scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i) + end + scope + end + # Return true if the issue is closed, otherwise false def closed? self.status.is_closed? @@ -458,7 +689,21 @@ # Versions that the issue can be assigned to def assignable_versions - @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort + return @assignable_versions if @assignable_versions + + versions = project.shared_versions.open.all + if fixed_version + if fixed_version_id_changed? + # nothing to do + elsif project_id_changed? + if project.shared_versions.include?(fixed_version) + versions << fixed_version + end + else + versions << fixed_version + end + end + @assignable_versions = versions.uniq.sort end # Returns true if this issue is blocked by another issue that is still open @@ -466,37 +711,67 @@ !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil? end - # Returns an array of status that user is able to apply - def new_statuses_allowed_to(user, include_default=false) - statuses = status.find_new_statuses_allowed_to( - user.roles_for_project(project), - tracker, - author == user, - assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id - ) - statuses << status unless statuses.empty? - statuses << IssueStatus.default if include_default - statuses = statuses.uniq.sort - blocked? ? statuses.reject {|s| s.is_closed?} : statuses + # Returns an array of statuses that user is able to apply + def new_statuses_allowed_to(user=User.current, include_default=false) + if new_record? && @copied_from + [IssueStatus.default, @copied_from.status].compact.uniq.sort + else + initial_status = nil + if new_record? + initial_status = IssueStatus.default + elsif status_id_was + initial_status = IssueStatus.find_by_id(status_id_was) + end + initial_status ||= status + + statuses = initial_status.find_new_statuses_allowed_to( + user.admin ? Role.all : user.roles_for_project(project), + tracker, + author == user, + assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id + ) + statuses << initial_status unless statuses.empty? + statuses << IssueStatus.default if include_default + statuses = statuses.compact.uniq.sort + blocked? ? statuses.reject {|s| s.is_closed?} : statuses + end end - # Returns the mail adresses of users that should be notified - def recipients - notified = project.notified_users + def assigned_to_was + if assigned_to_id_changed? && assigned_to_id_was.present? + @assigned_to_was ||= User.find_by_id(assigned_to_id_was) + end + end + + # Returns the users that should be notified + def notified_users + notified = [] # Author and assignee are always notified unless they have been # locked or don't want to be notified - notified << author if author && author.active? && author.notify_about?(self) + notified << author if author if assigned_to - if assigned_to.is_a?(Group) - notified += assigned_to.users.select {|u| u.active? && u.notify_about?(self)} - else - notified << assigned_to if assigned_to.active? && assigned_to.notify_about?(self) - end + notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to]) end + if assigned_to_was + notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was]) + end + notified = notified.select {|u| u.active? && u.notify_about?(self)} + + notified += project.notified_users notified.uniq! # Remove users that can not view the issue notified.reject! {|user| !visible?(user)} - notified.collect(&:mail) + notified + end + + # Returns the email addresses that should be notified + def recipients + notified_users.collect(&:mail) + end + + # Returns the number of hours spent on this issue + def spent_hours + @spent_hours ||= time_entries.sum(:hours) || 0 end # Returns the total number of hours spent on this issue and its descendants @@ -504,12 +779,13 @@ # Example: # spent_hours => 0.0 # spent_hours => 50.2 - def spent_hours - @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0 + def total_spent_hours + @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", + :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0 end def relations - @relations ||= (relations_from + relations_to).sort + @relations ||= IssueRelations.new(self, (relations_from + relations_to).sort) end # Preloads relations for a collection of issues @@ -522,6 +798,35 @@ end end + # Preloads visible spent time for a collection of issues + def self.load_visible_spent_hours(issues, user=User.current) + if issues.any? + hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id) + issues.each do |issue| + issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0) + end + end + end + + # Preloads visible relations for a collection of issues + def self.load_visible_relations(issues, user=User.current) + if issues.any? + issue_ids = issues.map(&:id) + # Relations with issue_from in given issues and visible issue_to + relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all + # Relations with issue_to in given issues and visible issue_from + relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all + + issues.each do |issue| + relations = + relations_from.select {|relation| relation.issue_from_id == issue.id} + + relations_to.select {|relation| relation.issue_to_id == issue.id} + + issue.instance_variable_set "@relations", IssueRelations.new(issue, relations.sort) + end + end + end + # Finds an issue relation given its id. def find_relation(relation_id) IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id]) @@ -559,23 +864,58 @@ (start_date && due_date) ? due_date - start_date : 0 end - def soonest_start + # Returns the duration in working days + def working_duration + (start_date && due_date) ? working_days(start_date, due_date) : 0 + end + + def soonest_start(reload=false) + @soonest_start = nil if reload @soonest_start ||= ( - relations_to.collect{|relation| relation.successor_soonest_start} + + relations_to(reload).collect{|relation| relation.successor_soonest_start} + ancestors.collect(&:soonest_start) ).compact.max end - def reschedule_after(date) + # Sets start_date on the given date or the next working day + # and changes due_date to keep the same working duration. + def reschedule_on(date) + wd = working_duration + date = next_working_date(date) + self.start_date = date + self.due_date = add_working_days(date, wd) + end + + # Reschedules the issue on the given date or the next working day and saves the record. + # If the issue is a parent task, this is done by rescheduling its subtasks. + def reschedule_on!(date) return if date.nil? if leaf? - if start_date.nil? || start_date < date - self.start_date, self.due_date = date, date + duration - save + if start_date.nil? || start_date != date + if start_date && start_date > date + # Issue can not be moved earlier than its soonest start date + date = [soonest_start(true), date].compact.max + end + reschedule_on(date) + begin + save + rescue ActiveRecord::StaleObjectError + reload + reschedule_on(date) + save + end end else leaves.each do |leaf| - leaf.reschedule_after(date) + if leaf.start_date + # Only move subtask if it starts at the same date as the parent + # or if it starts before the given date + if start_date == leaf.start_date || date > leaf.start_date + leaf.reschedule_on!(date) + end + else + leaf.reschedule_on!(date) + end end end end @@ -596,8 +936,7 @@ # Returns a string of css classes that apply to the issue def css_classes - s = "issue status-#{status.position} " - s << "priority-#{priority.position}" + s = "issue status-#{status_id} #{priority.try(:css_classes)}" s << ' closed' if closed? s << ' overdue' if overdue? s << ' child' if child? @@ -608,8 +947,7 @@ s end - # Saves an issue, time_entry, attachments, and a journal from the parameters - # Returns false if save fails + # Saves an issue and a time_entry from the parameters def save_issue_with_child_records(params, existing_time_entry=nil) Issue.transaction do if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project) @@ -622,22 +960,13 @@ self.time_entries << @time_entry end - if valid? - attachments = Attachment.attach_files(self, params[:attachments]) + # TODO: Rename hook + Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) + if save # TODO: Rename hook - Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) - begin - if save - # TODO: Rename hook - Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) - else - raise ActiveRecord::Rollback - end - rescue ActiveRecord::StaleObjectError - attachments[:files].each(&:destroy) - errors.add :base, l(:notice_locking_conflict) - raise ActiveRecord::Rollback - end + Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) + else + raise ActiveRecord::Rollback end end end @@ -657,23 +986,44 @@ end def parent_issue_id=(arg) - parent_issue_id = arg.blank? ? nil : arg.to_i - if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id) + s = arg.to_s.strip.presence + if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1])) @parent_issue.id else @parent_issue = nil - nil + @invalid_parent_issue_id = arg end end def parent_issue_id - if instance_variable_defined? :@parent_issue + if @invalid_parent_issue_id + @invalid_parent_issue_id + elsif instance_variable_defined? :@parent_issue @parent_issue.nil? ? nil : @parent_issue.id else parent_id end end + # Returns true if issue's project is a valid + # parent issue project + def valid_parent_project?(issue=parent) + return true if issue.nil? || issue.project_id == project_id + + case Setting.cross_project_subtasks + when 'system' + true + when 'tree' + issue.project.root == project.root + when 'hierarchy' + issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project) + when 'descendants' + issue.project.is_or_is_ancestor_of?(project) + else + false + end + end + # Extracted from the ReportsController. def self.by_tracker(project) count_and_group_by(:project => project, @@ -727,24 +1077,76 @@ end # End ReportsController extraction - # Returns an array of projects that current user can move issues to - def self.allowed_target_projects_on_move - projects = [] - if User.current.admin? - # admin is allowed to move issues to any active (visible) project - projects = Project.visible.all - elsif User.current.logged? - if Role.non_member.allowed_to?(:move_issues) - projects = Project.visible.all - else - User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}} - end + # Returns an array of projects that user can assign the issue to + def allowed_target_projects(user=User.current) + if new_record? + Project.all(:conditions => Project.allowed_to_condition(user, :add_issues)) + else + self.class.allowed_target_projects_on_move(user) end - projects + end + + # Returns an array of projects that user can move issues to + def self.allowed_target_projects_on_move(user=User.current) + Project.all(:conditions => Project.allowed_to_condition(user, :move_issues)) end private + def after_project_change + # Update project_id on related time entries + TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id}) + + # Delete issue relations + unless Setting.cross_project_issue_relations? + relations_from.clear + relations_to.clear + end + + # Move subtasks that were in the same project + children.each do |child| + next unless child.project_id == project_id_was + # Change project and keep project + child.send :project=, project, true + unless child.save + raise ActiveRecord::Rollback + end + end + end + + # Callback for after the creation of an issue by copy + # * adds a "copied to" relation with the copied issue + # * copies subtasks from the copied issue + def after_create_from_copy + return unless copy? && !@after_create_from_copy_handled + + if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false + relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO) + unless relation.save + logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger + end + end + + unless @copied_from.leaf? || @copy_options[:subtasks] == false + @copied_from.children.each do |child| + unless child.visible? + # Do not copy subtasks that are not visible to avoid potential disclosure of private data + logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger + next + end + copy = Issue.new.copy_from(child, @copy_options) + copy.author = author + copy.project = project + copy.parent_issue_id = id + # Children subtasks are copied recursively + unless copy.save + logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger + end + end + end + @after_create_from_copy_handled = true + end + def update_nested_set_attributes if root_id.nil? # issue was just created @@ -799,7 +1201,7 @@ def recalculate_attributes_for(issue_id) if issue_id && p = Issue.find_by_id(issue_id) # priority = highest priority of children - if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority) + if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority) p.priority = IssuePriority.find_by_position(priority_position) end @@ -818,7 +1220,7 @@ if average == 0 average = 1 end - done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f + done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f progress = done / (average * leaves_count) p.done_ratio = progress.round end @@ -829,7 +1231,7 @@ p.estimated_hours = nil if p.estimated_hours == 0.0 # ancestors will be recursively updated - p.save(false) + p.save(:validate => false) end end @@ -838,12 +1240,12 @@ def self.update_versions(conditions=nil) # Only need to update issues with a fixed_version from # a different project and that is not systemwide shared - Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" + - " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" + - " AND #{Version.table_name}.sharing <> 'system'", - conditions), - :include => [:project, :fixed_version] - ).each do |issue| + Issue.scoped(:conditions => conditions).all( + :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" + + " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" + + " AND #{Version.table_name}.sharing <> 'system'", + :include => [:project, :fixed_version] + ).each do |issue| next if issue.project.nil? || issue.fixed_version.nil? unless issue.project.shared_versions.include?(issue.fixed_version) issue.init_journal(User.current) @@ -853,7 +1255,7 @@ end end - # Callback on attachment deletion + # Callback on file attachment def attachment_added(obj) if @current_journal && !obj.new_record? @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename) @@ -862,11 +1264,10 @@ # Callback on attachment deletion def attachment_removed(obj) - journal = init_journal(User.current) - journal.details << JournalDetail.new(:property => 'attachment', - :prop_key => obj.id, - :old_value => obj.filename) - journal.save + if @current_journal && !obj.new_record? + @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename) + @current_journal.save + end end # Default assignment based on category @@ -902,29 +1303,62 @@ end end + # Make sure updated_on is updated when adding a note + def force_updated_on_change + if @current_journal + self.updated_on = current_time_from_proper_timezone + end + end + # Saves the changes in a Journal # Called after_save def create_journal if @current_journal # attributes changes - (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c| - before = @issue_before_change.send(c) - after = send(c) - next if before == after || (before.blank? && after.blank?) - @current_journal.details << JournalDetail.new(:property => 'attr', - :prop_key => c, - :old_value => @issue_before_change.send(c), - :value => send(c)) - } - # custom fields changes - custom_values.each {|c| - next if (@custom_values_before_change[c.custom_field_id]==c.value || - (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?)) - @current_journal.details << JournalDetail.new(:property => 'cf', - :prop_key => c.custom_field_id, - :old_value => @custom_values_before_change[c.custom_field_id], - :value => c.value) - } + if @attributes_before_change + (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c| + before = @attributes_before_change[c] + after = send(c) + next if before == after || (before.blank? && after.blank?) + @current_journal.details << JournalDetail.new(:property => 'attr', + :prop_key => c, + :old_value => before, + :value => after) + } + end + if @custom_values_before_change + # custom fields changes + custom_field_values.each {|c| + before = @custom_values_before_change[c.custom_field_id] + after = c.value + next if before == after || (before.blank? && after.blank?) + + if before.is_a?(Array) || after.is_a?(Array) + before = [before] unless before.is_a?(Array) + after = [after] unless after.is_a?(Array) + + # values removed + (before - after).reject(&:blank?).each do |value| + @current_journal.details << JournalDetail.new(:property => 'cf', + :prop_key => c.custom_field_id, + :old_value => value, + :value => nil) + end + # values added + (after - before).reject(&:blank?).each do |value| + @current_journal.details << JournalDetail.new(:property => 'cf', + :prop_key => c.custom_field_id, + :old_value => nil, + :value => value) + end + else + @current_journal.details << JournalDetail.new(:property => 'cf', + :prop_key => c.custom_field_id, + :old_value => before, + :value => after) + end + } + end @current_journal.save # reset current journal init_journal @current_journal.user, @current_journal.notes diff -r ab89f95ef405 -r b2ea0641f798 app/models/issue_category.rb --- a/app/models/issue_category.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/issue_category.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -27,7 +27,7 @@ safe_attributes 'name', 'assigned_to_id' - named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}} + scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)} alias :destroy_without_reassign :destroy diff -r ab89f95ef405 -r b2ea0641f798 app/models/issue_custom_field.rb --- a/app/models/issue_custom_field.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/issue_custom_field.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/issue_observer.rb --- a/app/models/issue_observer.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/issue_observer.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,6 +17,6 @@ class IssueObserver < ActiveRecord::Observer def after_create(issue) - Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added') + Mailer.issue_add(issue).deliver if Setting.notified_events.include?('issue_added') end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/issue_priority.rb --- a/app/models/issue_priority.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/issue_priority.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,6 +18,9 @@ class IssuePriority < Enumeration has_many :issues, :foreign_key => 'priority_id' + after_destroy {|priority| priority.class.compute_position_names} + after_save {|priority| priority.class.compute_position_names if priority.position_changed? && priority.position} + OptionName = :enumeration_issue_priorities def option_name @@ -31,4 +34,35 @@ def transfer_relations(to) issues.update_all("priority_id = #{to.id}") end + + def css_classes + "priority-#{id} priority-#{position_name}" + end + + # Clears position_name for all priorities + # Called from migration 20121026003537_populate_enumerations_position_name + def self.clear_position_names + update_all :position_name => nil + end + + # Updates position_name for active priorities + # Called from migration 20121026003537_populate_enumerations_position_name + def self.compute_position_names + priorities = where(:active => true).all.sort_by(&:position) + if priorities.any? + default = priorities.detect(&:is_default?) || priorities[(priorities.size - 1) / 2] + priorities.each_with_index do |priority, index| + name = case + when priority.position == default.position + "default" + when priority.position < default.position + index == 0 ? "lowest" : "low#{index+1}" + else + index == (priorities.size - 1) ? "highest" : "high#{priorities.size - index}" + end + + update_all({:position_name => name}, :id => priority.id) + end + end + end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/issue_priority_custom_field.rb --- a/app/models/issue_priority_custom_field.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/issue_priority_custom_field.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/issue_relation.rb --- a/app/models/issue_relation.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/issue_relation.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -15,6 +15,20 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Class used to represent the relations of an issue +class IssueRelations < Array + include Redmine::I18n + + def initialize(issue, *args) + @issue = issue + super(*args) + end + + def to_s(*args) + map {|relation| "#{l(relation.label_for(@issue))} ##{relation.other_issue(@issue).id}"}.join(', ') + end +end + class IssueRelation < ActiveRecord::Base belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id' belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id' @@ -26,25 +40,37 @@ TYPE_BLOCKED = "blocked" TYPE_PRECEDES = "precedes" TYPE_FOLLOWS = "follows" + TYPE_COPIED_TO = "copied_to" + TYPE_COPIED_FROM = "copied_from" - TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1, :sym => TYPE_RELATES }, - TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2, :sym => TYPE_DUPLICATED }, - TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates, :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES }, - TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 4, :sym => TYPE_BLOCKED }, - TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks, :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS }, - TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 6, :sym => TYPE_FOLLOWS }, - TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES } - }.freeze + TYPES = { + TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, + :order => 1, :sym => TYPE_RELATES }, + TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, + :order => 2, :sym => TYPE_DUPLICATED }, + TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates, + :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES }, + TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, + :order => 4, :sym => TYPE_BLOCKED }, + TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks, + :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS }, + TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, + :order => 6, :sym => TYPE_FOLLOWS }, + TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, + :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES }, + TYPE_COPIED_TO => { :name => :label_copied_to, :sym_name => :label_copied_from, + :order => 8, :sym => TYPE_COPIED_FROM }, + TYPE_COPIED_FROM => { :name => :label_copied_from, :sym_name => :label_copied_to, + :order => 9, :sym => TYPE_COPIED_TO, :reverse => TYPE_COPIED_TO } + }.freeze validates_presence_of :issue_from, :issue_to, :relation_type validates_inclusion_of :relation_type, :in => TYPES.keys validates_numericality_of :delay, :allow_nil => true validates_uniqueness_of :issue_to_id, :scope => :issue_from_id - validate :validate_issue_relation attr_protected :issue_from_id, :issue_to_id - before_save :handle_issue_order def visible?(user=User.current) @@ -57,7 +83,8 @@ (issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project))) end - def after_initialize + def initialize(attributes=nil, *args) + super if new_record? if relation_type.blank? self.relation_type = IssueRelation::TYPE_RELATES @@ -68,14 +95,19 @@ def validate_issue_relation if issue_from && issue_to errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id - errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations? - #detect circular dependencies depending wether the relation should be reversed + unless issue_from.project_id == issue_to.project_id || + Setting.cross_project_issue_relations? + errors.add :issue_to_id, :not_same_project + end + # detect circular dependencies depending wether the relation should be reversed if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse] errors.add :base, :circular_dependency if issue_from.all_dependent_issues.include? issue_to else errors.add :base, :circular_dependency if issue_to.all_dependent_issues.include? issue_from end - errors.add :base, :cant_link_an_issue_with_a_descendant if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to) + if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to) + errors.add :base, :cant_link_an_issue_with_a_descendant + end end end @@ -95,7 +127,13 @@ end def label_for(issue) - TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow + TYPES[relation_type] ? + TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : + :unknow + end + + def css_classes_for(issue) + "rel-#{relation_type_for(issue)}" end def handle_issue_order @@ -112,18 +150,20 @@ def set_issue_to_dates soonest_start = self.successor_soonest_start if soonest_start && issue_to - issue_to.reschedule_after(soonest_start) + issue_to.reschedule_on!(soonest_start) end end def successor_soonest_start - if (TYPE_PRECEDES == self.relation_type) && delay && issue_from && (issue_from.start_date || issue_from.due_date) + if (TYPE_PRECEDES == self.relation_type) && delay && issue_from && + (issue_from.start_date || issue_from.due_date) (issue_from.due_date || issue_from.start_date) + 1 + delay end end def <=>(relation) - TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order] + r = TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order] + r == 0 ? id <=> relation.id : r end private diff -r ab89f95ef405 -r b2ea0641f798 app/models/issue_status.rb --- a/app/models/issue_status.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/issue_status.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,10 +17,10 @@ class IssueStatus < ActiveRecord::Base before_destroy :check_integrity - has_many :workflows, :foreign_key => "old_status_id" + has_many :workflows, :class_name => 'WorkflowTransition', :foreign_key => "old_status_id" acts_as_list - before_destroy :delete_workflows + before_destroy :delete_workflow_rules after_save :update_default validates_presence_of :name @@ -28,23 +28,23 @@ validates_length_of :name, :maximum => 30 validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true - named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}} + scope :sorted, order("#{table_name}.position ASC") + scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)} def update_default - IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default? + IssueStatus.update_all({:is_default => false}, ['id <> ?', id]) if self.is_default? end # Returns the default status for new issues def self.default - find(:first, :conditions =>["is_default=?", true]) + where(:is_default => true).first end # Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+ def self.update_issue_done_ratios if Issue.use_status_for_done_ratio? - IssueStatus.find(:all, :conditions => ["default_done_ratio >= 0"]).each do |status| - Issue.update_all(["done_ratio = ?", status.default_done_ratio], - ["status_id = ?", status.id]) + IssueStatus.where("default_done_ratio >= 0").all.each do |status| + Issue.update_all({:done_ratio => status.default_done_ratio}, {:status_id => status.id}) end end @@ -61,7 +61,7 @@ w.tracker_id == tracker.id && ((!w.author && !w.assignee) || (author && w.author) || (assignee && w.assignee)) end - transitions.collect{|w| w.new_status}.compact.sort + transitions.map(&:new_status).compact.sort else [] end @@ -75,12 +75,12 @@ conditions << " OR author = :true" if author conditions << " OR assignee = :true" if assignee - workflows.find(:all, - :include => :new_status, - :conditions => ["role_id IN (:role_ids) AND tracker_id = :tracker_id AND (#{conditions})", + workflows. + includes(:new_status). + where(["role_id IN (:role_ids) AND tracker_id = :tracker_id AND (#{conditions})", {:role_ids => roles.collect(&:id), :tracker_id => tracker.id, :true => true, :false => false} - ] - ).collect{|w| w.new_status}.compact.sort + ]).all. + map(&:new_status).compact.sort else [] end @@ -92,13 +92,14 @@ def to_s; name end -private + private + def check_integrity - raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id]) + raise "Can't delete status" if Issue.where(:status_id => id).any? end # Deletes associated workflows - def delete_workflows - Workflow.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}]) + def delete_workflow_rules + WorkflowRule.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}]) end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/journal.rb --- a/app/models/journal.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/journal.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -37,10 +37,15 @@ :conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" + " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"} - named_scope :visible, lambda {|*args| { - :include => {:issue => :project}, - :conditions => Issue.visible_condition(args.shift || User.current, *args) - }} + before_create :split_private_notes + + scope :visible, lambda {|*args| + user = args.shift || User.current + + includes(:issue => :project). + where(Issue.visible_condition(user, *args)). + where("(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes, *args)}))", false) + } def save(*args) # Do not save an empty journal @@ -75,6 +80,7 @@ s = 'journal' s << ' has-notes' unless notes.blank? s << ' has-details' unless details.blank? + s << ' private-notes' if private_notes? s end @@ -85,4 +91,41 @@ def notify=(arg) @notify = arg end + + def recipients + notified = journalized.notified_users + if private_notes? + notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)} + end + notified.map(&:mail) + end + + def watcher_recipients + notified = journalized.notified_watchers + if private_notes? + notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)} + end + notified.map(&:mail) + end + + private + + def split_private_notes + if private_notes? + if notes.present? + if details.any? + # Split the journal (notes/changes) so we don't have half-private journals + journal = Journal.new(:journalized => journalized, :user => user, :notes => nil, :private_notes => false) + journal.details = details + journal.save + self.details = [] + self.created_on = journal.created_on + end + else + # Blank notes should not be private + self.private_notes = false + end + end + true + end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/journal_detail.rb --- a/app/models/journal_detail.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/journal_detail.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/journal_observer.rb --- a/app/models/journal_observer.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/journal_observer.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -23,7 +23,7 @@ (Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) || (Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?) ) - Mailer.deliver_issue_edit(journal) + Mailer.issue_edit(journal).deliver end end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/mail_handler.rb --- a/app/models/mail_handler.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/mail_handler.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -29,7 +29,9 @@ @@handler_options[:issue] ||= {} - @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String) + if @@handler_options[:allow_override].is_a?(String) + @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) + end @@handler_options[:allow_override] ||= [] # Project needs to be overridable if not specified @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project) @@ -37,9 +39,21 @@ @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status) @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false) - super email + + email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding) + super(email) end + def logger + Rails.logger + end + + cattr_accessor :ignored_emails_headers + @@ignored_emails_headers = { + 'X-Auto-Response-Suppress' => 'oof', + 'Auto-Submitted' => /^auto-/ + } + # Processes incoming emails # Returns the created object (eg. an issue, a message) or false def receive(email) @@ -47,12 +61,29 @@ sender_email = email.from.to_a.first.to_s.strip # Ignore emails received from the application emission address to avoid hell cycles if sender_email.downcase == Setting.mail_from.to_s.strip.downcase - logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info + if logger && logger.info + logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" + end return false end + # Ignore auto generated emails + self.class.ignored_emails_headers.each do |key, ignored_value| + value = email.header[key] + if value + value = value.to_s.downcase + if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value + if logger && logger.info + logger.info "MailHandler: ignoring email with #{key}:#{value} header" + end + return false + end + end + end @user = User.find_by_mail(sender_email) if sender_email.present? if @user && !@user.active? - logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info + if logger && logger.info + logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" + end return false end if @user.nil? @@ -61,17 +92,23 @@ when 'accept' @user = User.anonymous when 'create' - @user = create_user_from_email(email) + @user = create_user_from_email if @user - logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info - Mailer.deliver_account_information(@user, @user.password) + if logger && logger.info + logger.info "MailHandler: [#{@user.login}] account created" + end + Mailer.account_information(@user, @user.password).deliver else - logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error + if logger && logger.error + logger.error "MailHandler: could not create account for [#{sender_email}]" + end return false end else # Default behaviour, emails from unknown users are ignored - logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info + if logger && logger.info + logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" + end return false end end @@ -81,12 +118,13 @@ private - MESSAGE_ID_RE = %r{^ user, :project => project) issue.safe_attributes = issue_attributes_from_keywords(issue) issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} - issue.subject = email.subject.to_s.chomp[0,255] + issue.subject = cleaned_up_subject if issue.subject.blank? issue.subject = '(no subject)' end @@ -144,24 +182,33 @@ end # Adds a note to an existing issue - def receive_issue_reply(issue_id) + def receive_issue_reply(issue_id, from_journal=nil) issue = Issue.find_by_id(issue_id) return unless issue # check permission unless @@handler_options[:no_permission_check] - raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project) + unless user.allowed_to?(:add_issue_notes, issue.project) || + user.allowed_to?(:edit_issues, issue.project) + raise UnauthorizedAction + end end # ignore CLI-supplied defaults for new issues @@handler_options[:issue].clear journal = issue.init_journal(user) + if from_journal && from_journal.private_notes? + # If the received email was a reply to a private note, make the added note private + issue.private_notes = true + end issue.safe_attributes = issue_attributes_from_keywords(issue) issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} journal.notes = cleaned_up_text_body add_attachments(issue) issue.save! - logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info + if logger && logger.info + logger.info "MailHandler: issue ##{issue.id} updated by #{user}" + end journal end @@ -169,7 +216,7 @@ def receive_journal_reply(journal_id) journal = Journal.find_by_id(journal_id) if journal && journal.journalized_type == 'Issue' - receive_issue_reply(journal.journalized_id) + receive_issue_reply(journal.journalized_id, journal) end end @@ -184,7 +231,7 @@ end if !message.locked? - reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip, + reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip, :content => cleaned_up_text_body) reply.author = user reply.board = message.board @@ -192,7 +239,9 @@ add_attachments(reply) reply else - logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info + if logger && logger.info + logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" + end end end end @@ -201,9 +250,10 @@ if email.attachments && email.attachments.any? email.attachments.each do |attachment| obj.attachments << Attachment.create(:container => obj, - :file => attachment, + :file => attachment.decoded, + :filename => attachment.filename, :author => user, - :content_type => attachment.content_type) + :content_type => attachment.mime_type) end end end @@ -226,7 +276,8 @@ @keywords[attr] else @keywords[attr] = begin - if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format])) + if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && + (v = extract_keyword!(plain_text_body, attr, options[:format])) v elsif !@@handler_options[:issue][attr].blank? @@handler_options[:issue][attr] @@ -240,14 +291,23 @@ def extract_keyword!(text, attr, format=nil) keys = [attr.to_s.humanize] if attr.is_a?(Symbol) - keys << l("field_#{attr}", :default => '', :locale => user.language) if user && user.language.present? - keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) if Setting.default_language.present? + if user && user.language.present? + keys << l("field_#{attr}", :default => '', :locale => user.language) + end + if Setting.default_language.present? + keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) + end end keys.reject! {|k| k.blank?} keys.collect! {|k| Regexp.escape(k)} format ||= '.+' - text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '') - $2 && $2.strip + keyword = nil + regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i + if m = text.match(regexp) + keyword = m[2].strip + text.gsub!(regexp, '') + end + keyword end def target_project @@ -269,7 +329,8 @@ 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id), 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id), 'assigned_to_id' => assigned_to.try(:id), - 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.named(k).first.try(:id), + 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && + issue.project.shared_versions.named(k).first.try(:id), 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'), 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'), 'estimated_hours' => get_keyword(:estimated_hours, :override => true), @@ -286,8 +347,8 @@ # Returns a Hash of issue custom field values extracted from keywords in the email body def custom_field_values_from_keywords(customized) customized.custom_field_values.inject({}) do |h, v| - if value = get_keyword(v.custom_field.name, :override => true) - h[v.custom_field.id.to_s] = value + if keyword = get_keyword(v.custom_field.name, :override => true) + h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized) end h end @@ -297,20 +358,13 @@ # If not found (eg. HTML-only email), returns the body with tags removed def plain_text_body return @plain_text_body unless @plain_text_body.nil? - parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten - if parts.empty? - parts << @email - end - plain_text_part = parts.detect {|p| p.content_type == 'text/plain'} - if plain_text_part.nil? - # no text/plain part found, assuming html-only email - # strip html tags and remove doctype directive - @plain_text_body = strip_tags(@email.body.to_s) - @plain_text_body.gsub! %r{^$/) + addr, name = m[2], m[1] + end + if addr.present? + user = self.class.new_user_from_attributes(addr, name) if user.save user else @@ -372,8 +435,6 @@ end end - private - # Removes the email body of text after the truncation configurations. def cleanup_body(body) delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)} @@ -388,13 +449,19 @@ keyword = keyword.to_s.downcase assignable = issue.assignable_users assignee = nil - assignee ||= assignable.detect {|a| a.mail.to_s.downcase == keyword || a.login.to_s.downcase == keyword} + assignee ||= assignable.detect {|a| + a.mail.to_s.downcase == keyword || + a.login.to_s.downcase == keyword + } if assignee.nil? && keyword.match(/ /) firstname, lastname = *(keyword.split) # "First Last Throwaway" - assignee ||= assignable.detect {|a| a.is_a?(User) && a.firstname.to_s.downcase == firstname && a.lastname.to_s.downcase == lastname} + assignee ||= assignable.detect {|a| + a.is_a?(User) && a.firstname.to_s.downcase == firstname && + a.lastname.to_s.downcase == lastname + } end if assignee.nil? - assignee ||= assignable.detect {|a| a.is_a?(Group) && a.name.downcase == keyword} + assignee ||= assignable.detect {|a| a.name.downcase == keyword} end assignee end diff -r ab89f95ef405 -r b2ea0641f798 app/models/mailer.rb --- a/app/models/mailer.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/mailer.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -15,74 +15,72 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -require 'ar_condition' - class Mailer < ActionMailer::Base layout 'mailer' helper :application helper :issues helper :custom_fields - include ActionController::UrlWriter include Redmine::I18n def self.default_url_options - h = Setting.host_name - h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank? - { :host => h, :protocol => Setting.protocol } + { :host => Setting.host_name, :protocol => Setting.protocol } end - # todo: luisf: 2Aug2012 - refactor... - def added_to_project(member, project) + # Builds a Mail::Message object used to email the specified member + # that he was added to a project + # + # Example: + # member_added_to_project(member, project) => Mail::Message object + # Mailer.member_added_to_project(member, project) => sends an email to the registered member + def member_added_to_project(member, project) + principal = Principal.find(member.user_id) + users = [] if principal.type == "User" - user = User.find(member.user_id) - user_add_to_project(user, project) + users = [User.find(member.user_id)] else users = Principal.find(member.user_id).users - users.map {|user| user_add_to_project(user, project) } + end + + users.map do |user| + + set_language_if_valid user.language + @project_url = url_for(:controller => 'projects', :action => 'show', :id => project.id) + @project_name = project.name + mail :to => user.mail, + :subject => l(:mail_subject_added_to_project, Setting.app_title) + end end - - # Builds a tmail object used to email the specified user that he was added to a project + + # Builds a Mail::Message object used to email recipients of the added issue. # # Example: - # user_add_to_project(user, project) => tmail object - # Mailer.deliver_add_to_project(user, project) => sends an email to the registered user - def user_add_to_project(user, project) - set_language_if_valid user.language - recipients user.mail - subject l(:mail_subject_added_to_project, Setting.app_title) - body :project_url => url_for(:controller => 'projects', :action => 'show', :id => project.id), - :project_name => project.name - render_multipart('added_to_project', body) - end - - # Builds a tmail object used to email recipients of the added issue. - # - # Example: - # issue_add(issue) => tmail object - # Mailer.deliver_issue_add(issue) => sends an email to issue recipients + # issue_add(issue) => Mail::Message object + # Mailer.issue_add(issue).deliver => sends an email to issue recipients def issue_add(issue) redmine_headers 'Project' => issue.project.identifier, 'Issue-Id' => issue.id, 'Issue-Author' => issue.author.login redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to message_id issue - recipients issue.recipients - cc(issue.watcher_recipients - @recipients) - subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}" - body :issue => issue, - :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue) - render_multipart('issue_add', body) + @author = issue.author + @issue = issue + @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue) + recipients = issue.recipients + cc = issue.watcher_recipients - recipients + mail :to => recipients, + :cc => cc, + :subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}" end - # Builds a tmail object used to email recipients of the edited issue. + # Builds a Mail::Message object used to email recipients of the edited issue. # # Example: - # issue_edit(journal) => tmail object - # Mailer.deliver_issue_edit(journal) => sends an email to issue recipients + # issue_edit(journal) => Mail::Message object + # Mailer.issue_edit(journal).deliver => sends an email to issue recipients def issue_edit(journal) issue = journal.journalized.reload redmine_headers 'Project' => issue.project.identifier, @@ -92,238 +90,239 @@ message_id journal references issue @author = journal.user - recipients issue.recipients + recipients = journal.recipients # Watchers in cc - cc(issue.watcher_recipients - @recipients) + cc = journal.watcher_recipients - recipients s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] " s << "(#{issue.status.name}) " if journal.new_value_for('status_id') s << issue.subject - subject s - body :issue => issue, - :journal => journal, - :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}") - - render_multipart('issue_edit', body) + @issue = issue + @journal = journal + @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}") + mail :to => recipients, + :cc => cc, + :subject => s end def reminder(user, issues, days) set_language_if_valid user.language - recipients user.mail - subject l(:mail_subject_reminder, :count => issues.size, :days => days) - body :issues => issues, - :days => days, - :issues_url => url_for(:controller => 'issues', :action => 'index', + @issues = issues + @days = days + @issues_url = url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort => 'due_date:asc') - render_multipart('reminder', body) + mail :to => user.mail, + :subject => l(:mail_subject_reminder, :count => issues.size, :days => days) end - # Builds a tmail object used to email users belonging to the added document's project. + # Builds a Mail::Message object used to email users belonging to the added document's project. # # Example: - # document_added(document) => tmail object - # Mailer.deliver_document_added(document) => sends an email to the document's project recipients + # document_added(document) => Mail::Message object + # Mailer.document_added(document).deliver => sends an email to the document's project recipients def document_added(document) redmine_headers 'Project' => document.project.identifier - recipients document.recipients - subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}" - body :document => document, - :document_url => url_for(:controller => 'documents', :action => 'show', :id => document) - render_multipart('document_added', body) + @author = User.current + @document = document + @document_url = url_for(:controller => 'documents', :action => 'show', :id => document) + mail :to => document.recipients, + :subject => "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}" end - # Builds a tmail object used to email recipients of a project when an attachements are added. + # Builds a Mail::Message object used to email recipients of a project when an attachements are added. # # Example: - # attachments_added(attachments) => tmail object - # Mailer.deliver_attachments_added(attachments) => sends an email to the project's recipients + # attachments_added(attachments) => Mail::Message object + # Mailer.attachments_added(attachments).deliver => sends an email to the project's recipients def attachments_added(attachments) container = attachments.first.container added_to = '' added_to_url = '' + @author = attachments.first.author case container.class.name when 'Project' added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container) added_to = "#{l(:label_project)}: #{container}" - recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail} + recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail} when 'Version' added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project) added_to = "#{l(:label_version)}: #{container.name}" - recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail} + recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail} when 'Document' added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id) added_to = "#{l(:label_document)}: #{container.title}" - recipients container.recipients + recipients = container.recipients end redmine_headers 'Project' => container.project.identifier - subject "[#{container.project.name}] #{l(:label_attachment_new)}" - body :attachments => attachments, - :added_to => added_to, - :added_to_url => added_to_url - render_multipart('attachments_added', body) + @attachments = attachments + @added_to = added_to + @added_to_url = added_to_url + mail :to => recipients, + :subject => "[#{container.project.name}] #{l(:label_attachment_new)}" end - # Builds a tmail object used to email recipients of a news' project when a news item is added. + # Builds a Mail::Message object used to email recipients of a news' project when a news item is added. # # Example: - # news_added(news) => tmail object - # Mailer.deliver_news_added(news) => sends an email to the news' project recipients + # news_added(news) => Mail::Message object + # Mailer.news_added(news).deliver => sends an email to the news' project recipients def news_added(news) redmine_headers 'Project' => news.project.identifier + @author = news.author message_id news - recipients news.recipients - subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}" - body :news => news, - :news_url => url_for(:controller => 'news', :action => 'show', :id => news) - render_multipart('news_added', body) + @news = news + @news_url = url_for(:controller => 'news', :action => 'show', :id => news) + mail :to => news.recipients, + :subject => "[#{news.project.name}] #{l(:label_news)}: #{news.title}" end - # Builds a tmail object used to email recipients of a news' project when a news comment is added. + # Builds a Mail::Message object used to email recipients of a news' project when a news comment is added. # # Example: - # news_comment_added(comment) => tmail object + # news_comment_added(comment) => Mail::Message object # Mailer.news_comment_added(comment) => sends an email to the news' project recipients def news_comment_added(comment) news = comment.commented redmine_headers 'Project' => news.project.identifier + @author = comment.author message_id comment - recipients news.recipients - cc news.watcher_recipients - subject "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}" - body :news => news, - :comment => comment, - :news_url => url_for(:controller => 'news', :action => 'show', :id => news) - render_multipart('news_comment_added', body) + @news = news + @comment = comment + @news_url = url_for(:controller => 'news', :action => 'show', :id => news) + mail :to => news.recipients, + :cc => news.watcher_recipients, + :subject => "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}" end - # Builds a tmail object used to email the recipients of the specified message that was posted. + # Builds a Mail::Message object used to email the recipients of the specified message that was posted. # # Example: - # message_posted(message) => tmail object - # Mailer.deliver_message_posted(message) => sends an email to the recipients + # message_posted(message) => Mail::Message object + # Mailer.message_posted(message).deliver => sends an email to the recipients def message_posted(message) redmine_headers 'Project' => message.project.identifier, 'Topic-Id' => (message.parent_id || message.id) + @author = message.author message_id message references message.parent unless message.parent.nil? - recipients(message.recipients) - cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients) - subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}" - body :message => message, - :message_url => url_for(message.event_url) - render_multipart('message_posted', body) + recipients = message.recipients + cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients) + @message = message + @message_url = url_for(message.event_url) + mail :to => recipients, + :cc => cc, + :subject => "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}" end - # Builds a tmail object used to email the recipients of a project of the specified wiki content was added. + # Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was added. # # Example: - # wiki_content_added(wiki_content) => tmail object - # Mailer.deliver_wiki_content_added(wiki_content) => sends an email to the project's recipients + # wiki_content_added(wiki_content) => Mail::Message object + # Mailer.wiki_content_added(wiki_content).deliver => sends an email to the project's recipients def wiki_content_added(wiki_content) redmine_headers 'Project' => wiki_content.project.identifier, 'Wiki-Page-Id' => wiki_content.page.id + @author = wiki_content.author message_id wiki_content - recipients wiki_content.recipients - cc(wiki_content.page.wiki.watcher_recipients - recipients) - subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}" - body :wiki_content => wiki_content, - :wiki_content_url => url_for(:controller => 'wiki', :action => 'show', + recipients = wiki_content.recipients + cc = wiki_content.page.wiki.watcher_recipients - recipients + @wiki_content = wiki_content + @wiki_content_url = url_for(:controller => 'wiki', :action => 'show', :project_id => wiki_content.project, :id => wiki_content.page.title) - render_multipart('wiki_content_added', body) + mail :to => recipients, + :cc => cc, + :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}" end - # Builds a tmail object used to email the recipients of a project of the specified wiki content was updated. + # Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was updated. # # Example: - # wiki_content_updated(wiki_content) => tmail object - # Mailer.deliver_wiki_content_updated(wiki_content) => sends an email to the project's recipients + # wiki_content_updated(wiki_content) => Mail::Message object + # Mailer.wiki_content_updated(wiki_content).deliver => sends an email to the project's recipients def wiki_content_updated(wiki_content) redmine_headers 'Project' => wiki_content.project.identifier, 'Wiki-Page-Id' => wiki_content.page.id + @author = wiki_content.author message_id wiki_content - recipients wiki_content.recipients - cc(wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients) - subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}" - body :wiki_content => wiki_content, - :wiki_content_url => url_for(:controller => 'wiki', :action => 'show', + recipients = wiki_content.recipients + cc = wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients + @wiki_content = wiki_content + @wiki_content_url = url_for(:controller => 'wiki', :action => 'show', :project_id => wiki_content.project, - :id => wiki_content.page.title), - :wiki_diff_url => url_for(:controller => 'wiki', :action => 'diff', + :id => wiki_content.page.title) + @wiki_diff_url = url_for(:controller => 'wiki', :action => 'diff', :project_id => wiki_content.project, :id => wiki_content.page.title, :version => wiki_content.version) - render_multipart('wiki_content_updated', body) + mail :to => recipients, + :cc => cc, + :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}" end - # Builds a tmail object used to email the specified user their account information. + # Builds a Mail::Message object used to email the specified user their account information. # # Example: - # account_information(user, password) => tmail object - # Mailer.deliver_account_information(user, password) => sends account information to the user + # account_information(user, password) => Mail::Message object + # Mailer.account_information(user, password).deliver => sends account information to the user def account_information(user, password) set_language_if_valid user.language - recipients user.mail - subject l(:mail_subject_register, Setting.app_title) - body :user => user, - :password => password, - :login_url => url_for(:controller => 'account', :action => 'login') - render_multipart('account_information', body) + @user = user + @password = password + @login_url = url_for(:controller => 'account', :action => 'login') + mail :to => user.mail, + :subject => l(:mail_subject_register, Setting.app_title) end - # Builds a tmail object used to email all active administrators of an account activation request. + # Builds a Mail::Message object used to email all active administrators of an account activation request. # # Example: - # account_activation_request(user) => tmail object - # Mailer.deliver_account_activation_request(user)=> sends an email to all active administrators + # account_activation_request(user) => Mail::Message object + # Mailer.account_activation_request(user).deliver => sends an email to all active administrators def account_activation_request(user) # Send the email to all active administrators - recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact - subject l(:mail_subject_account_activation_request, Setting.app_title) - body :user => user, - :url => url_for(:controller => 'users', :action => 'index', + recipients = User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact + @user = user + @url = url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc') - render_multipart('account_activation_request', body) + mail :to => recipients, + :subject => l(:mail_subject_account_activation_request, Setting.app_title) end - # Builds a tmail object used to email the specified user that their account was activated by an administrator. + # Builds a Mail::Message object used to email the specified user that their account was activated by an administrator. # # Example: - # account_activated(user) => tmail object - # Mailer.deliver_account_activated(user) => sends an email to the registered user + # account_activated(user) => Mail::Message object + # Mailer.account_activated(user).deliver => sends an email to the registered user def account_activated(user) set_language_if_valid user.language - recipients user.mail - subject l(:mail_subject_register, Setting.app_title) - body :user => user, - :login_url => url_for(:controller => 'account', :action => 'login') - render_multipart('account_activated', body) + @user = user + @login_url = url_for(:controller => 'account', :action => 'login') + mail :to => user.mail, + :subject => l(:mail_subject_register, Setting.app_title) end def lost_password(token) set_language_if_valid(token.user.language) - recipients token.user.mail - subject l(:mail_subject_lost_password, Setting.app_title) - body :token => token, - :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value) - render_multipart('lost_password', body) + @token = token + @url = url_for(:controller => 'account', :action => 'lost_password', :token => token.value) + mail :to => token.user.mail, + :subject => l(:mail_subject_lost_password, Setting.app_title) end def register(token) set_language_if_valid(token.user.language) - recipients token.user.mail - subject l(:mail_subject_register, Setting.app_title) - body :token => token, - :url => url_for(:controller => 'account', :action => 'activate', :token => token.value) - render_multipart('register', body) + @token = token + @url = url_for(:controller => 'account', :action => 'activate', :token => token.value) + mail :to => token.user.mail, + :subject => l(:mail_subject_register, Setting.app_title) end - def test(user) + def test_email(user) set_language_if_valid(user.language) - recipients user.mail - subject 'Redmine test' - body :url => url_for(:controller => 'welcome') - render_multipart('test', body) + @url = url_for(:controller => 'welcome') + mail :to => user.mail, + :subject => 'Redmine test' end # Overrides default deliver! method to prevent from sending an email @@ -334,13 +333,6 @@ (cc.nil? || cc.empty?) && (bcc.nil? || bcc.empty?) - # Set Message-Id and References - if @message_id_object - mail.message_id = self.class.message_id_for(@message_id_object) - end - if @references_objects - mail.references = @references_objects.collect {|o| self.class.message_id_for(o)} - end # Log errors when raise_delivery_errors is set to false, Rails does not raise_errors = self.class.raise_delivery_errors @@ -363,25 +355,33 @@ # * :days => how many days in the future to remind about (defaults to 7) # * :tracker => id of tracker for filtering issues (defaults to all trackers) # * :project => id or identifier of project to process (defaults to all projects) - # * :users => array of user ids who should be reminded + # * :users => array of user/group ids who should be reminded def self.reminders(options={}) days = options[:days] || 7 project = options[:project] ? Project.find(options[:project]) : nil tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil user_ids = options[:users] - s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date] - s << "#{Issue.table_name}.assigned_to_id IS NOT NULL" - s << ["#{Issue.table_name}.assigned_to_id IN (?)", user_ids] if user_ids.present? - s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}" - s << "#{Issue.table_name}.project_id = #{project.id}" if project - s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker + scope = Issue.open.where("#{Issue.table_name}.assigned_to_id IS NOT NULL" + + " AND #{Project.table_name}.status = #{Project::STATUS_ACTIVE}" + + " AND #{Issue.table_name}.due_date <= ?", days.day.from_now.to_date + ) + scope = scope.where(:assigned_to_id => user_ids) if user_ids.present? + scope = scope.where(:project_id => project.id) if project + scope = scope.where(:tracker_id => tracker.id) if tracker - issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker], - :conditions => s.conditions - ).group_by(&:assigned_to) + issues_by_assignee = scope.includes(:status, :assigned_to, :project, :tracker).all.group_by(&:assigned_to) + issues_by_assignee.keys.each do |assignee| + if assignee.is_a?(Group) + assignee.users.each do |user| + issues_by_assignee[user] ||= [] + issues_by_assignee[user] += issues_by_assignee[assignee] + end + end + end + issues_by_assignee.each do |assignee, issues| - deliver_reminder(assignee, issues, days) if assignee.is_a?(User) && assignee.active? + reminder(assignee, issues, days).deliver if assignee.is_a?(User) && assignee.active? end end @@ -394,77 +394,88 @@ ActionMailer::Base.perform_deliveries = was_enabled end - private - def initialize_defaults(method_name) - super - @initial_language = current_language - set_language_if_valid Setting.default_language - from Setting.mail_from + # Sends emails synchronously in the given block + def self.with_synched_deliveries(&block) + saved_method = ActionMailer::Base.delivery_method + if m = saved_method.to_s.match(%r{^async_(.+)$}) + synched_method = m[1] + ActionMailer::Base.delivery_method = synched_method.to_sym + ActionMailer::Base.send "#{synched_method}_settings=", ActionMailer::Base.send("async_#{synched_method}_settings") + end + yield + ensure + ActionMailer::Base.delivery_method = saved_method + end - # Common headers - headers 'X-Mailer' => 'Redmine', + def mail(headers={}) + headers.merge! 'X-Mailer' => 'Redmine', 'X-Redmine-Host' => Setting.host_name, 'X-Redmine-Site' => Setting.app_title, 'X-Auto-Response-Suppress' => 'OOF', - 'Auto-Submitted' => 'auto-generated' + 'Auto-Submitted' => 'auto-generated', + 'From' => Setting.mail_from, + 'List-Id' => "<#{Setting.mail_from.to_s.gsub('@', '.')}>" + + # Removes the author from the recipients and cc + # if he doesn't want to receive notifications about what he does + if @author && @author.logged? && @author.pref[:no_self_notified] + headers[:to].delete(@author.mail) if headers[:to].is_a?(Array) + headers[:cc].delete(@author.mail) if headers[:cc].is_a?(Array) + end + + if @author && @author.logged? + redmine_headers 'Sender' => @author.login + end + + # Blind carbon copy recipients + if Setting.bcc_recipients? + headers[:bcc] = [headers[:to], headers[:cc]].flatten.uniq.reject(&:blank?) + headers[:to] = nil + headers[:cc] = nil + end + + if @message_id_object + headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>" + end + if @references_objects + headers[:references] = @references_objects.collect {|o| "<#{self.class.message_id_for(o)}>"}.join(' ') + end + + super headers do |format| + format.text + format.html unless Setting.plain_text_mail? + end + + set_language_if_valid @initial_language end + def initialize(*args) + @initial_language = current_language + set_language_if_valid Setting.default_language + super + end + + def self.deliver_mail(mail) + return false if mail.to.blank? && mail.cc.blank? && mail.bcc.blank? + super + end + + def self.method_missing(method, *args, &block) + if m = method.to_s.match(%r{^deliver_(.+)$}) + ActiveSupport::Deprecation.warn "Mailer.deliver_#{m[1]}(*args) is deprecated. Use Mailer.#{m[1]}(*args).deliver instead." + send(m[1], *args).deliver + else + super + end + end + + private + # Appends a Redmine header field (name is prepended with 'X-Redmine-') def redmine_headers(h) - h.each { |k,v| headers["X-Redmine-#{k}"] = v } + h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s } end - # Overrides the create_mail method - def create_mail - # Removes the current user from the recipients and cc - # if he doesn't want to receive notifications about what he does - @author ||= User.current - if @author.pref[:no_self_notified] - recipients.delete(@author.mail) if recipients - cc.delete(@author.mail) if cc - end - - notified_users = [recipients, cc].flatten.compact.uniq - # Rails would log recipients only, not cc and bcc - mylogger.info "Sending email notification to: #{notified_users.join(', ')}" if mylogger - - # Blind carbon copy recipients - if Setting.bcc_recipients? - bcc(notified_users) - recipients [] - cc [] - end - super - end - - # Rails 2.3 has problems rendering implicit multipart messages with - # layouts so this method will wrap an multipart messages with - # explicit parts. - # - # https://rails.lighthouseapp.com/projects/8994/tickets/2338-actionmailer-mailer-views-and-content-type - # https://rails.lighthouseapp.com/projects/8994/tickets/1799-actionmailer-doesnt-set-template_format-when-rendering-layouts - - def render_multipart(method_name, body) - if Setting.plain_text_mail? - content_type "text/plain" - body render(:file => "#{method_name}.text.erb", - :body => body, - :layout => 'mailer.text.erb') - else - content_type "multipart/alternative" - part :content_type => "text/plain", - :body => render(:file => "#{method_name}.text.erb", - :body => body, :layout => 'mailer.text.erb') - part :content_type => "text/html", - :body => render_message("#{method_name}.html.erb", body) - end - end - - # Makes partial rendering work with Rails 1.2 (retro-compatibility) - def self.controller_path - '' - end unless respond_to?('controller_path') - # Returns a predictable Message-Id for the given object def self.message_id_for(object) # id + timestamp should reduce the odds of a collision @@ -473,11 +484,9 @@ hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}" host = Setting.mail_from.to_s.gsub(%r{^.*@}, '') host = "#{::Socket.gethostname}.redmine" if host.empty? - "<#{hash}@#{host}>" + "#{hash}@#{host}" end - private - def message_id(object) @message_id_object = object end @@ -493,6 +502,10 @@ end # Patch TMail so that message_id is not overwritten + +### NB: Redmine 2.2 no longer uses TMail I think? This function has +### been removed there + module TMail class Mail def add_message_id( fqdn = nil ) @@ -501,6 +514,3 @@ end end - - - diff -r ab89f95ef405 -r b2ea0641f798 app/models/member.rb --- a/app/models/member.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/member.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -24,9 +24,17 @@ validates_presence_of :principal, :project validates_uniqueness_of :user_id, :scope => :project_id + validate :validate_role + before_destroy :set_issue_category_nil after_destroy :unwatch_from_permission_change + def role + end + + def role= + end + def name self.user.name end @@ -75,7 +83,7 @@ end end - def before_destroy + def set_issue_category_nil if user # remove category based auto assignments for this member IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id] @@ -91,7 +99,7 @@ protected - def validate + def validate_role errors.add_on_empty :role if member_roles.empty? && roles.empty? end diff -r ab89f95ef405 -r b2ea0641f798 app/models/member_role.rb --- a/app/models/member_role.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/member_role.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -25,8 +25,9 @@ after_destroy :remove_role_from_group_users validates_presence_of :role + validate :validate_role_member - def validate + def validate_role_member errors.add :role_id, :invalid if role && !role.member? end diff -r ab89f95ef405 -r b2ea0641f798 app/models/message.rb --- a/app/models/message.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/message.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -41,11 +41,11 @@ validates_length_of :subject, :maximum => 255 validate :cannot_reply_to_locked_topic, :on => :create - after_create :add_author_as_watcher, :update_parent_last_reply + after_create :add_author_as_watcher, :reset_counters! after_update :update_messages_board - after_destroy :reset_board_counters + after_destroy :reset_counters! - named_scope :visible, lambda {|*args| { :include => {:board => :project}, + scope :visible, lambda {|*args| { :include => {:board => :project}, :conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } } safe_attributes 'subject', 'content' @@ -63,13 +63,6 @@ errors.add :base, 'Topic is locked' if root.locked? && self != root end - def update_parent_last_reply - if parent - parent.reload.update_attribute(:last_reply_id, self.id) - end - board.reset_counters! - end - def update_messages_board if board_id_changed? Message.update_all("board_id = #{board_id}", ["id = ? OR parent_id = ?", root.id, root.id]) @@ -78,7 +71,10 @@ end end - def reset_board_counters + def reset_counters! + if parent && parent.id + Message.update_all({:last_reply_id => parent.children.maximum(:id)}, {:id => parent.id}) + end board.reset_counters! end diff -r ab89f95ef405 -r b2ea0641f798 app/models/message_observer.rb --- a/app/models/message_observer.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/message_observer.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,6 +17,6 @@ class MessageObserver < ActiveRecord::Observer def after_create(message) - Mailer.deliver_message_posted(message) if Setting.notified_events.include?('message_posted') + Mailer.message_posted(message).deliver if Setting.notified_events.include?('message_posted') end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/news.rb --- a/app/models/news.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/news.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -25,6 +25,7 @@ validates_length_of :title, :maximum => 60 validates_length_of :summary, :maximum => 255 + acts_as_attachable :delete_permission => :manage_news acts_as_searchable :columns => ['title', 'summary', "#{table_name}.description"], :include => :project acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}} acts_as_activity_provider :find_options => {:include => [:project, :author]}, @@ -33,7 +34,7 @@ after_create :add_author_as_watcher - named_scope :visible, lambda {|*args| { + scope :visible, lambda {|*args| { :include => :project, :conditions => Project.allowed_to_condition(args.shift || User.current, :view_news, *args) }} @@ -44,9 +45,14 @@ !user.nil? && user.allowed_to?(:view_news, project) end + # Returns true if the news can be commented by user + def commentable?(user=User.current) + user.allowed_to?(:comment_news, project) + end + # returns latest news for projects visible by user def self.latest(user = User.current, count = 5) - find(:all, :limit => count, :conditions => Project.allowed_to_condition(user, :view_news), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") + visible(user).includes([:author, :project]).order("#{News.table_name}.created_on DESC").limit(count).all end private diff -r ab89f95ef405 -r b2ea0641f798 app/models/news_observer.rb --- a/app/models/news_observer.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/news_observer.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,6 +17,6 @@ class NewsObserver < ActiveRecord::Observer def after_create(news) - Mailer.deliver_news_added(news) if Setting.notified_events.include?('news_added') + Mailer.news_added(news).deliver if Setting.notified_events.include?('news_added') end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/principal.rb --- a/app/models/principal.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/principal.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,30 +16,52 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class Principal < ActiveRecord::Base - set_table_name "#{table_name_prefix}users#{table_name_suffix}" + self.table_name = "#{table_name_prefix}users#{table_name_suffix}" has_many :members, :foreign_key => 'user_id', :dependent => :destroy - has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name" + has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status<>#{Project::STATUS_ARCHIVED}", :order => "#{Project.table_name}.name" has_many :projects, :through => :memberships has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify # Groups and active users - named_scope :active, :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status = 1)" + scope :active, :conditions => "#{Principal.table_name}.status = 1" - named_scope :like, lambda {|q| - s = "%#{q.to_s.strip.downcase}%" - {:conditions => ["LOWER(login) LIKE :s OR LOWER(firstname) LIKE :s OR LOWER(lastname) LIKE :s OR LOWER(mail) LIKE :s", {:s => s}], - :order => 'type, login, lastname, firstname, mail' - } + scope :like, lambda {|q| + q = q.to_s + if q.blank? + where({}) + else + pattern = "%#{q}%" + sql = %w(login firstname lastname mail).map {|column| "LOWER(#{table_name}.#{column}) LIKE LOWER(:p)"}.join(" OR ") + params = {:p => pattern} + if q =~ /^(.+)\s+(.+)$/ + a, b = "#{$1}%", "#{$2}%" + sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:a) AND LOWER(#{table_name}.lastname) LIKE LOWER(:b))" + sql << " OR (LOWER(#{table_name}.firstname) LIKE LOWER(:b) AND LOWER(#{table_name}.lastname) LIKE LOWER(:a))" + params.merge!(:a => a, :b => b) + end + where(sql, params) + end + } + + # Principals that are members of a collection of projects + scope :member_of, lambda {|projects| + projects = [projects] unless projects.is_a?(Array) + if projects.empty? + where("1=0") + else + ids = projects.map(&:id) + where("#{Principal.table_name}.status = 1 AND #{Principal.table_name}.id IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids) + end } # Principals that are not members of projects - named_scope :not_member_of, lambda {|projects| + scope :not_member_of, lambda {|projects| projects = [projects] unless projects.is_a?(Array) if projects.empty? - {:conditions => "1=0"} + where("1=0") else ids = projects.map(&:id) - {:conditions => ["#{Principal.table_name}.id NOT IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids]} + where("#{Principal.table_name}.id NOT IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids) end } diff -r ab89f95ef405 -r b2ea0641f798 app/models/project.rb --- a/app/models/project.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/project.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -20,6 +20,7 @@ # Project statuses STATUS_ACTIVE = 1 + STATUS_CLOSED = 5 STATUS_ARCHIVED = 9 # Maximum length for project identifiers @@ -27,7 +28,7 @@ # Specific overidden Activities has_many :time_entry_activities - has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}" + has_many :members, :include => [:principal, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}" has_many :memberships, :class_name => 'Member' has_many :member_principals, :class_name => 'Member', :include => :principal, @@ -37,7 +38,7 @@ has_many :enabled_modules, :dependent => :delete_all has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" - has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker] + has_many :issues, :dependent => :destroy, :include => [:status, :tracker] has_many :issue_changes, :through => :issues, :source => :journals has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC" has_many :time_entries, :dependent => :delete_all @@ -46,7 +47,8 @@ has_many :news, :dependent => :destroy, :include => :author has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name" has_many :boards, :dependent => :destroy, :order => "position ASC" - has_one :repository, :dependent => :destroy + has_one :repository, :conditions => ["is_default = ?", true] + has_many :repositories, :dependent => :destroy has_many :changesets, :through => :repository has_one :wiki, :dependent => :destroy # Custom field for the project issues @@ -75,19 +77,40 @@ validates_length_of :homepage, :maximum => 255 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH # donwcase letters, digits, dashes but not digits only - validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? } + validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? } # reserved words validates_exclusion_of :identifier, :in => %w( new ) + after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?} before_destroy :delete_all_members - named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } } - named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"} - named_scope :all_public, { :conditions => { :is_public => true } } - named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }} - named_scope :visible_roots, lambda { { :conditions => Project.root_visible_by(User.current) } } + scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } } + scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"} + scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} } + scope :all_public, { :conditions => { :is_public => true } } + scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }} + scope :visible_roots, lambda { { :conditions => Project.root_visible_by(User.current) } } + scope :allowed_to, lambda {|*args| + user = User.current + permission = nil + if args.first.is_a?(Symbol) + permission = args.shift + else + user = args.shift + permission = args.shift + end + { :conditions => Project.allowed_to_condition(user, permission, *args) } + } + scope :like, lambda {|arg| + if arg.blank? + {} + else + pattern = "%#{arg.to_s.strip.downcase}%" + {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]} + end + } - def initialize(attributes = nil) + def initialize(attributes=nil, *args) super initialized = (attributes || {}).stringify_keys @@ -101,7 +124,7 @@ self.enabled_module_names = Setting.default_projects_modules end if !initialized.key?('trackers') && !initialized.key?('tracker_ids') - self.trackers = Tracker.all + self.trackers = Tracker.sorted.all end end @@ -110,7 +133,7 @@ end def identifier_frozen? - errors[:identifier].nil? && !(new_record? || identifier.blank?) + errors[:identifier].blank? && !(new_record? || identifier.blank?) end # returns latest created projects @@ -145,12 +168,11 @@ # * :with_subprojects => limit the condition to project and its subprojects # * :member => limit the condition to the user projects def self.allowed_to_condition(user, permission, options={}) - base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" - if perm = Redmine::AccessControl.permission(permission) - unless perm.project_module.nil? - # If the permission belongs to a project module, make sure the module is enabled - base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" - end + perm = Redmine::AccessControl.permission(permission) + base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}") + if perm && perm.project_module + # If the permission belongs to a project module, make sure the module is enabled + base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" end if options[:project] project_statement = "#{Project.table_name}.id = #{options[:project].id}" @@ -170,7 +192,7 @@ end if user.logged? user.projects_by_role.each do |role, projects| - if role.allowed_to?(permission) + if role.allowed_to?(permission) && projects.any? statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})" end end @@ -256,6 +278,23 @@ end end + def self.find_by_param(*args) + self.find(*args) + end + + def reload(*args) + @shared_versions = nil + @rolled_up_versions = nil + @rolled_up_trackers = nil + @all_issue_custom_fields = nil + @all_time_entry_custom_fields = nil + @to_param = nil + @allowed_parents = nil + @allowed_permissions = nil + @actions_allowed = nil + super + end + def to_param # id is used for projects with a numeric identifier (compatibility) @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier) @@ -292,6 +331,14 @@ update_attribute :status, STATUS_ACTIVE end + def close + self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED + end + + def reopen + self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE + end + # Returns an array of projects the project can be moved to # by the current user def allowed_parents @@ -342,22 +389,7 @@ # Nothing to do true elsif p.nil? || (p.active? && move_possible?(p)) - # Insert the project so that target's children or root projects stay alphabetically sorted - sibs = (p.nil? ? self.class.roots : p.children) - to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase } - if to_be_inserted_before - move_to_left_of(to_be_inserted_before) - elsif p.nil? - if sibs.empty? - # move_to_root adds the project in first (ie. left) position - move_to_root - else - move_to_right_of(sibs.last) unless self == sibs.last - end - else - # move_to_child_of adds the project in last (ie.right) position - move_to_child_of(p) - end + set_or_update_position_under(p) Issue.update_versions_from_hierarchy_change(self) true else @@ -366,12 +398,22 @@ end end + # Recalculates all lft and rgt values based on project names + # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid + # Used in BuildProjectsTree migration + def self.rebuild_tree! + transaction do + update_all "lft = NULL, rgt = NULL" + rebuild!(false) + end + end + # Returns an array of the trackers used by the project and its active sub projects def rolled_up_trackers @rolled_up_trackers ||= Tracker.find(:all, :joins => :projects, :select => "DISTINCT #{Tracker.table_name}.*", - :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt], + :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt], :order => "#{Tracker.table_name}.position") end @@ -390,20 +432,20 @@ def rolled_up_versions @rolled_up_versions ||= Version.scoped(:include => :project, - :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt]) + :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt]) end # Returns a scope of the Versions used by the project def shared_versions if new_record? Version.scoped(:include => :project, - :conditions => "#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND #{Version.table_name}.sharing = 'system'") + :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'") else @shared_versions ||= begin r = root? ? self : root Version.scoped(:include => :project, :conditions => "#{Project.table_name}.id = #{id}" + - " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" + + " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" + " #{Version.table_name}.sharing = 'system'" + " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" + " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" + @@ -445,7 +487,7 @@ # Returns the users that should be notified on project events def notified_users # TODO: User part should be extracted to User#notify_about? - members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user} + members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal} end # Returns an array of all custom fields enabled for project issues @@ -496,6 +538,13 @@ s << ' root' if root? s << ' child' if child? s << (leaf? ? ' leaf' : ' parent') + unless active? + if archived? + s << ' archived' + else + s << ' closed' + end + end s end @@ -539,11 +588,20 @@ end end - # Return true if this project is allowed to do the specified action. + # Return true if this project allows to do the specified action. # action can be: # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') # * a permission Symbol (eg. :edit_project) def allows_to?(action) + if archived? + # No action allowed on archived projects + return false + end + unless active? || Redmine::AccessControl.read_action?(action) + # No write action allowed on closed projects + return false + end + # No action allowed on disabled modules if action.is_a? Hash allowed_actions.include? "#{action[:controller]}/#{action[:action]}" else @@ -693,7 +751,7 @@ def copy_wiki(project) # Check that the source project has a wiki first unless project.wiki.nil? - self.wiki ||= Wiki.new + wiki = self.wiki || Wiki.new wiki.attributes = project.wiki.attributes.dup.except("id", "project_id") wiki_pages_map = {} project.wiki.pages.each do |page| @@ -705,6 +763,8 @@ wiki.pages << new_wiki_page wiki_pages_map[page.id] = new_wiki_page end + + self.wiki = wiki wiki.save # Reproduce page hierarchy project.wiki.pages.each do |page| @@ -735,27 +795,30 @@ end # Copies issues from +project+ - # Note: issues assigned to a closed version won't be copied due to validation rules def copy_issues(project) # Stores the source issue id as a key and the copied issues as the # value. Used to map the two togeather for issue relations. issues_map = {} + # Store status and reopen locked/closed versions + version_statuses = versions.reject(&:open?).map {|version| [version, version.status]} + version_statuses.each do |version, status| + version.update_attribute :status, 'open' + end + # Get issues sorted by root_id, lft so that parent issues # get copied before their children project.issues.find(:all, :order => 'root_id, lft').each do |issue| new_issue = Issue.new - new_issue.copy_from(issue) + new_issue.copy_from(issue, :subtasks => false, :link => false) new_issue.project = self - # Reassign fixed_versions by name, since names are unique per - # project and the versions for self are not yet saved - if issue.fixed_version - new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first + # Reassign fixed_versions by name, since names are unique per project + if issue.fixed_version && issue.fixed_version.project == project + new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name} end - # Reassign the category by name, since names are unique per - # project and the categories for self are not yet saved + # Reassign the category by name, since names are unique per project if issue.category - new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first + new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name} end # Parent issue if issue.parent_id @@ -772,6 +835,11 @@ end end + # Restore locked/closed version statuses + version_statuses.each do |version, status| + version.update_attribute :status, status + end + # Relations after in case issues related each other project.issues.each do |issue| new_issue = issues_map[issue.id] @@ -826,7 +894,7 @@ # Copies queries from +project+ def copy_queries(project) project.queries.each do |query| - new_query = Query.new + new_query = ::Query.new new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria") new_query.sort_criteria = query.sort_criteria if query.sort_criteria new_query.project = self @@ -901,4 +969,28 @@ end update_attribute :status, STATUS_ARCHIVED end + + def update_position_under_parent + set_or_update_position_under(parent) + end + + # Inserts/moves the project so that target's children or root projects stay alphabetically sorted + def set_or_update_position_under(target_parent) + sibs = (target_parent.nil? ? self.class.roots : target_parent.children) + to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase } + + if to_be_inserted_before + move_to_left_of(to_be_inserted_before) + elsif target_parent.nil? + if sibs.empty? + # move_to_root adds the project in first (ie. left) position + move_to_root + else + move_to_right_of(sibs.last) unless self == sibs.last + end + else + # move_to_child_of adds the project in last (ie.right) position + move_to_child_of(target_parent) + end + end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/project_custom_field.rb --- a/app/models/project_custom_field.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/project_custom_field.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/query.rb --- a/app/models/query.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/query.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -27,6 +27,7 @@ self.groupable = name.to_s end self.default_order = options[:default_order] + @inline = options.key?(:inline) ? options[:inline] : true @caption_key = options[:caption] || "field_#{name}" end @@ -38,11 +39,15 @@ def sortable? !@sortable.nil? end - + def sortable @sortable.is_a?(Proc) ? @sortable.call : @sortable end + def inline? + @inline + end + def value(issue) issue.send name end @@ -57,10 +62,8 @@ def initialize(custom_field) self.name = "cf_#{custom_field.id}".to_sym self.sortable = custom_field.order_statement || false - if %w(list date bool int).include?(custom_field.field_format) - self.groupable = custom_field.order_statement - end - self.groupable ||= false + self.groupable = custom_field.group_statement || false + @inline = true @cf = custom_field end @@ -73,8 +76,8 @@ end def value(issue) - cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id} - cv && @cf.cast_value(cv.value) + cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)} + cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first end def css_classes @@ -94,7 +97,7 @@ attr_protected :project_id, :user_id - validates_presence_of :name, :on => :save + validates_presence_of :name validates_length_of :name, :maximum => 255 validate :validate_query_filters @@ -103,20 +106,25 @@ "o" => :label_open_issues, "c" => :label_closed_issues, "!*" => :label_none, - "*" => :label_all, + "*" => :label_any, ">=" => :label_greater_or_equal, "<=" => :label_less_or_equal, "><" => :label_between, " :label_in_less_than, ">t+" => :label_in_more_than, + "> :label_in_the_next_days, "t+" => :label_in, "t" => :label_today, "w" => :label_this_week, ">t-" => :label_less_than_ago, " :label_more_than_ago, + "> :label_in_the_past_days, "t-" => :label_ago, "~" => :label_contains, - "!~" => :label_not_contains } + "!~" => :label_not_contains, + "=p" => :label_any_issues_in_project, + "=!p" => :label_any_issues_not_in_project, + "!p" => :label_no_issues_in_project} cattr_reader :operators @@ -124,12 +132,13 @@ :list_status => [ "o", "=", "!", "c", "*" ], :list_optional => [ "=", "!", "!*", "*" ], :list_subprojects => [ "*", "!*", "=" ], - :date => [ "=", ">=", "<=", "><", "t+", "t+", "t", "w", ">t-", " [ "=", ">=", "<=", "><", ">t-", " [ "=", "~", "!", "!~" ], - :text => [ "~", "!~" ], + :date => [ "=", ">=", "<=", "><", "t+", ">t-", " [ "=", ">=", "<=", "><", ">t-", " [ "=", "~", "!", "!~", "!*", "*" ], + :text => [ "~", "!~", "!*", "*" ], :integer => [ "=", ">=", "<=", "><", "!*", "*" ], - :float => [ "=", ">=", "<=", "><", "!*", "*" ] } + :float => [ "=", ">=", "<=", "><", "!*", "*" ], + :relation => ["=", "=p", "=!p", "!p", "!*", "*"]} cattr_reader :operators_by_filter_type @@ -144,16 +153,18 @@ QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true), QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'), QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true), - QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true), + QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true), QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), + QueryColumn.new(:relations, :caption => :label_related_issues), + QueryColumn.new(:description, :inline => false) ] cattr_reader :available_columns - named_scope :visible, lambda {|*args| + scope :visible, lambda {|*args| user = args.shift || User.current base = Project.allowed_to_condition(user, :view_issues, *args) user_id = user.logged? ? user.id : 0 @@ -163,13 +174,9 @@ } } - def initialize(attributes = nil) + def initialize(attributes=nil, *args) super attributes self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } - end - - def after_initialize - # Store the fact that project is nil (used in #editable_by?) @is_for_all = project.nil? end @@ -178,20 +185,20 @@ if values_for(field) case type_for(field) when :integer - errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) } + add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) } when :float - errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) } + add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) } when :date, :date_past case operator_for(field) when "=", ">=", "<=", "><" - errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) } - when ">t-", "t-", "t+", " 'activerecord.errors.messages') + errors.add(:base, m) + end + # Returns true if the query is visible to +user+ or the current user. def visible?(user=User.current) (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id) @@ -212,90 +224,181 @@ is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project) end + def trackers + @trackers ||= project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers + end + + # Returns a hash of localized labels for all filter operators + def self.operators_labels + operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h} + end + def available_filters return @available_filters if @available_filters - - trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers - - @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } }, - "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } }, - "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } }, - "subject" => { :type => :text, :order => 8 }, - "created_on" => { :type => :date_past, :order => 9 }, - "updated_on" => { :type => :date_past, :order => 10 }, - "start_date" => { :type => :date, :order => 11 }, - "due_date" => { :type => :date, :order => 12 }, - "estimated_hours" => { :type => :float, :order => 13 }, - "done_ratio" => { :type => :integer, :order => 14 }} - + @available_filters = { + "status_id" => { + :type => :list_status, :order => 0, + :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } + }, + "tracker_id" => { + :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } + }, + "priority_id" => { + :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } + }, + "subject" => { :type => :text, :order => 8 }, + "created_on" => { :type => :date_past, :order => 9 }, + "updated_on" => { :type => :date_past, :order => 10 }, + "start_date" => { :type => :date, :order => 11 }, + "due_date" => { :type => :date, :order => 12 }, + "estimated_hours" => { :type => :float, :order => 13 }, + "done_ratio" => { :type => :integer, :order => 14 } + } + IssueRelation::TYPES.each do |relation_type, options| + @available_filters[relation_type] = { + :type => :relation, :order => @available_filters.size + 100, + :label => options[:name] + } + end principals = [] if project principals += project.principals.sort + unless project.leaf? + subprojects = project.descendants.visible.all + if subprojects.any? + @available_filters["subproject_id"] = { + :type => :list_subprojects, :order => 13, + :values => subprojects.collect{|s| [s.name, s.id.to_s] } + } + principals += Principal.member_of(subprojects) + end + end else - all_projects = Project.visible.all if all_projects.any? # members of visible projects - principals += Principal.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort - + principals += Principal.member_of(all_projects) # project filter project_values = [] - Project.project_tree(all_projects) do |p, level| - prefix = (level > 0 ? ('--' * level + ' ') : '') - project_values << ["#{prefix}#{p.name}", p.id.to_s] + if User.current.logged? && User.current.memberships.any? + project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"] end - @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty? + project_values += all_projects_values + @available_filters["project_id"] = { + :type => :list, :order => 1, :values => project_values + } unless project_values.empty? end end + principals.uniq! + principals.sort! users = principals.select {|p| p.is_a?(User)} assigned_to_values = [] assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? - assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] } - @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty? + assigned_to_values += (Setting.issue_group_assignment? ? + principals : users).collect{|s| [s.name, s.id.to_s] } + @available_filters["assigned_to_id"] = { + :type => :list_optional, :order => 4, :values => assigned_to_values + } unless assigned_to_values.empty? author_values = [] author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? author_values += users.collect{|s| [s.name, s.id.to_s] } - @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty? + @available_filters["author_id"] = { + :type => :list, :order => 5, :values => author_values + } unless author_values.empty? group_values = Group.all.collect {|g| [g.name, g.id.to_s] } - @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty? + @available_filters["member_of_group"] = { + :type => :list_optional, :order => 6, :values => group_values + } unless group_values.empty? role_values = Role.givable.collect {|r| [r.name, r.id.to_s] } - @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty? + @available_filters["assigned_to_role"] = { + :type => :list_optional, :order => 7, :values => role_values + } unless role_values.empty? if User.current.logged? - @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] } + @available_filters["watcher_id"] = { + :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] + } end if project # project specific filters categories = project.issue_categories.all unless categories.empty? - @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } } + @available_filters["category_id"] = { + :type => :list_optional, :order => 6, + :values => categories.collect{|s| [s.name, s.id.to_s] } + } end versions = project.shared_versions.all unless versions.empty? - @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } } - end - unless project.leaf? - subprojects = project.descendants.visible.all - unless subprojects.empty? - @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } } - end + @available_filters["fixed_version_id"] = { + :type => :list_optional, :order => 7, + :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } + } end add_custom_fields_filters(project.all_issue_custom_fields) else # global filters for cross project issue list system_shared_versions = Version.visible.find_all_by_sharing('system') unless system_shared_versions.empty? - @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } } + @available_filters["fixed_version_id"] = { + :type => :list_optional, :order => 7, + :values => system_shared_versions.sort.collect{|s| + ["#{s.project.name} - #{s.name}", s.id.to_s] + } + } end - add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true})) + add_custom_fields_filters( + IssueCustomField.find(:all, + :conditions => { + :is_filter => true, + :is_for_all => true + })) + end + add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version + if User.current.allowed_to?(:set_issues_private, nil, :global => true) || + User.current.allowed_to?(:set_own_issues_private, nil, :global => true) + @available_filters["is_private"] = { + :type => :list, :order => 16, + :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] + } + end + Tracker.disabled_core_fields(trackers).each {|field| + @available_filters.delete field + } + @available_filters.each do |field, options| + options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, '')) end @available_filters end + # Returns a representation of the available filters for JSON serialization + def available_filters_as_json + json = {} + available_filters.each do |field, options| + json[field] = options.slice(:type, :name, :values).stringify_keys + end + json + end + + def all_projects + @all_projects ||= Project.visible.all + end + + def all_projects_values + return @all_projects_values if @all_projects_values + + values = [] + Project.project_tree(all_projects) do |p, level| + prefix = (level > 0 ? ('--' * level + ' ') : '') + values << ["#{prefix}#{p.name}", p.id.to_s] + end + @all_projects_values = values + end + def add_filter(field, operator, values) # values must be an array return unless values.nil? || values.is_a?(Array) @@ -351,16 +454,40 @@ def label_for(field) label = available_filters[field][:name] if available_filters.has_key?(field) - label ||= field.gsub(/\_id$/, "") + label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field) end def available_columns return @available_columns if @available_columns - @available_columns = ::Query.available_columns + @available_columns = ::Query.available_columns.dup @available_columns += (project ? project.all_issue_custom_fields : IssueCustomField.find(:all) ).collect {|cf| QueryCustomFieldColumn.new(cf) } + + if User.current.allowed_to?(:view_time_entries, project, :global => true) + index = nil + @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours} + index = (index ? index + 1 : -1) + # insert the column after estimated_hours or at the end + @available_columns.insert index, QueryColumn.new(:spent_hours, + :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)", + :default_order => 'desc', + :caption => :label_spent_time + ) + end + + if User.current.allowed_to?(:set_issues_private, nil, :global => true) || + User.current.allowed_to?(:set_own_issues_private, nil, :global => true) + @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private") + end + + disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')} + @available_columns.reject! {|column| + disabled_fields.include?(column.name.to_s) + } + + @available_columns end def self.available_columns=(v) @@ -391,6 +518,22 @@ end.compact end + def inline_columns + columns.select(&:inline?) + end + + def block_columns + columns.reject(&:inline?) + end + + def available_inline_columns + available_columns.select(&:inline?) + end + + def available_block_columns + available_columns.reject(&:inline?) + end + def default_columns_names @default_columns_names ||= begin default_columns = Setting.issue_list_default_columns.map(&:to_sym) @@ -412,7 +555,7 @@ end def has_column?(column) - column_names && column_names.include?(column.name) + column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column) end def has_default_columns? @@ -424,7 +567,7 @@ if arg.is_a?(Hash) arg = arg.keys.sort.collect {|k| arg[k]} end - c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']} + c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']} write_attribute(:sort_criteria, c) end @@ -440,12 +583,17 @@ sort_criteria && sort_criteria[arg] && sort_criteria[arg].last end + def sort_criteria_order_for(key) + sort_criteria.detect {|k, order| key.to_s == k}.try(:last) + end + # Returns the SQL sort order that should be prepended for grouping def group_by_sort_order if grouped? && (column = group_by_column) + order = sort_criteria_order_for(column.name) || column.default_order column.sortable.is_a?(Array) ? - column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') : - "#{column.sortable} #{column.default_order}" + column.sortable.collect {|s| "#{s} #{order}"}.join(',') : + "#{column.sortable} #{order}" end end @@ -508,7 +656,13 @@ end end - if field =~ /^cf_(\d+)$/ + if field == 'project_id' + if v.delete('mine') + v += User.current.memberships.map(&:project_id).map(&:to_s) + end + end + + if field =~ /cf_(\d+)$/ # custom field filters_clauses << sql_for_custom_field(field, operator, v, $1) elsif respond_to?("sql_for_#{field}_field") @@ -538,7 +692,7 @@ r = nil if grouped? begin - # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value + # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement) rescue ActiveRecord::RecordNotFound r = {nil => issue_count} @@ -558,15 +712,36 @@ def issues(options={}) order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',') order_option = nil if order_option.blank? - - joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil - Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq, + issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq, :conditions => statement, :order => order_option, - :joins => joins, + :joins => joins_for_order_statement(order_option), :limit => options[:limit], :offset => options[:offset] + + if has_column?(:spent_hours) + Issue.load_visible_spent_hours(issues) + end + if has_column?(:relations) + Issue.load_visible_relations(issues) + end + issues + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + # Returns the issues ids + def issue_ids(options={}) + order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',') + order_option = nil if order_option.blank? + + Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq, + :conditions => statement, + :order => order_option, + :joins => joins_for_order_statement(order_option), + :limit => options[:limit], + :offset => options[:offset]).find_ids rescue ::ActiveRecord::StatementInvalid => e raise StatementInvalid.new(e.message) end @@ -627,10 +802,10 @@ "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" + " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))" when "=", "!" - role_cond = value.any? ? + role_cond = value.any? ? "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" : "1=0" - + sw = operator == "!" ? 'NOT' : '' nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : '' "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" + @@ -638,12 +813,76 @@ end end + def sql_for_is_private_field(field, operator, value) + op = (operator == "=" ? 'IN' : 'NOT IN') + va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',') + + "#{Issue.table_name}.is_private #{op} (#{va})" + end + + def sql_for_relations(field, operator, value, options={}) + relation_options = IssueRelation::TYPES[field] + return relation_options unless relation_options + + relation_type = field + join_column, target_join_column = "issue_from_id", "issue_to_id" + if relation_options[:reverse] || options[:reverse] + relation_type = relation_options[:reverse] || relation_type + join_column, target_join_column = target_join_column, join_column + end + + sql = case operator + when "*", "!*" + op = (operator == "*" ? 'IN' : 'NOT IN') + "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')" + when "=", "!" + op = (operator == "=" ? 'IN' : 'NOT IN') + "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})" + when "=p", "=!p", "!p" + op = (operator == "!p" ? 'NOT IN' : 'IN') + comp = (operator == "=!p" ? '<>' : '=') + "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})" + end + + if relation_options[:sym] == field && !options[:reverse] + sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)] + sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ") + else + sql + end + end + + IssueRelation::TYPES.keys.each do |relation_type| + alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations + end + private def sql_for_custom_field(field, operator, value, custom_field_id) db_table = CustomValue.table_name db_field = 'value' - "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " + + filter = @available_filters[field] + return nil unless filter + if filter[:format] == 'user' + if value.delete('me') + value.push User.current.id.to_s + end + end + not_in = nil + if operator == '!' + # Makes ! operator work for custom fields with multiple values + operator = '=' + not_in = 'NOT' + end + customized_key = "id" + customized_class = Issue + if field =~ /^(.+)\.cf_/ + assoc = $1 + customized_key = "#{assoc}_id" + customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil + raise "Unknown Issue association #{assoc}" unless customized_class + end + "#{Issue.table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " + sql_for_field(field, operator, value, db_table, db_field, true) + ')' end @@ -657,9 +896,17 @@ when :date, :date_past sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil)) when :integer - sql = "#{db_table}.#{db_field} = #{value.first.to_i}" + if is_custom_filter + sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})" + else + sql = "#{db_table}.#{db_field} = #{value.first.to_i}" + end when :float - sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}" + if is_custom_filter + sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})" + else + sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}" + end else sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" end @@ -685,7 +932,7 @@ sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil) else if is_custom_filter - sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f}" + sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})" else sql = "#{db_table}.#{db_field} >= #{value.first.to_f}" end @@ -695,7 +942,7 @@ sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil)) else if is_custom_filter - sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f}" + sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})" else sql = "#{db_table}.#{db_field} <= #{value.first.to_f}" end @@ -705,30 +952,44 @@ sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil)) else if is_custom_filter - sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f}" + sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})" else sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}" end end when "o" - sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id" + sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id" when "c" - sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id" + sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id" + when ">t-" - sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0) + # >= today - n days + sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil) when "t+" + # >= today + n days sql = relative_date_clause(db_table, db_field, value.first.to_i, nil) when "= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) @@ -744,7 +1005,8 @@ return sql end - def add_custom_fields_filters(custom_fields) + def add_custom_fields_filters(custom_fields, assoc=nil) + return unless custom_fields.present? @available_filters ||= {} custom_fields.select(&:is_filter?).each do |field| @@ -763,11 +1025,37 @@ options = { :type => :float, :order => 20 } when "user", "version" next unless project - options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20} + values = field.possible_values_options(project) + if User.current.logged? && field.field_format == 'user' + values.unshift ["<< #{l(:label_me)} >>", "me"] + end + options = { :type => :list_optional, :values => values, :order => 20} else options = { :type => :string, :order => 20 } end - @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name }) + filter_id = "cf_#{field.id}" + filter_name = field.name + if assoc.present? + filter_id = "#{assoc}.#{filter_id}" + filter_name = l("label_attribute_of_#{assoc}", :name => filter_name) + end + @available_filters[filter_id] = options.merge({ + :name => filter_name, + :format => field.field_format, + :field => field + }) + end + end + + def add_associations_custom_fields_filters(*associations) + fields_by_class = CustomField.where(:is_filter => true).group_by(&:class) + associations.each do |assoc| + association_klass = Issue.reflect_on_association(assoc).klass + fields_by_class.each do |field_class, fields| + if field_class.customized_class <= association_klass + add_custom_fields_filters(fields, assoc) + end + end end end @@ -776,12 +1064,18 @@ s = [] if from from_yesterday = from - 1 - from_yesterday_utc = Time.gm(from_yesterday.year, from_yesterday.month, from_yesterday.day) - s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_utc.end_of_day)]) + from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day) + if self.class.default_timezone == :utc + from_yesterday_time = from_yesterday_time.utc + end + s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)]) end if to - to_utc = Time.gm(to.year, to.month, to.day) - s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_utc.end_of_day)]) + to_time = Time.local(to.year, to.month, to.day) + if self.class.default_timezone == :utc + to_time = to_time.utc + end + s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)]) end s.join(' AND ') end @@ -790,4 +1084,24 @@ def relative_date_clause(table, field, days_from, days_to) date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil)) end + + # Additional joins required for the given sort options + def joins_for_order_statement(order_options) + joins = [] + + if order_options + if order_options.include?('authors') + joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id" + end + order_options.scan(/cf_\d+/).uniq.each do |name| + column = available_columns.detect {|c| c.name.to_s == name} + join = column && column.custom_field.join_for_order_statement + if join + joins << join + end + end + end + + joins.any? ? joins.join(' ') : nil + end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/repository.rb --- a/app/models/repository.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/repository.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -19,33 +19,55 @@ class Repository < ActiveRecord::Base include Redmine::Ciphering + include Redmine::SafeAttributes + + # Maximum length for repository identifiers + IDENTIFIER_MAX_LENGTH = 255 belongs_to :project has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC" - has_many :changes, :through => :changesets + has_many :filechanges, :class_name => 'Change', :through => :changesets serialize :extra_info + before_save :check_default + # Raw SQL to delete changesets and changes in the database # has_many :changesets, :dependent => :destroy is too slow for big repositories before_destroy :clear_changesets validates_length_of :password, :maximum => 255, :allow_nil => true + validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true + validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? } + validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true + validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph) + # donwcase letters, digits, dashes, underscores but not digits only + validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :allow_blank => true # Checks if the SCM is enabled when creating a repository validate :repo_create_validation, :on => :create + safe_attributes 'identifier', + 'login', + 'password', + 'path_encoding', + 'log_encoding', + 'is_default' + + safe_attributes 'url', + :if => lambda {|repository, user| repository.new_record?} + def repo_create_validation unless Setting.enabled_scm.include?(self.class.name.demodulize) errors.add(:type, :invalid) end end - def self.human_attribute_name(attribute_key_name) - attr_name = attribute_key_name + def self.human_attribute_name(attribute_key_name, *args) + attr_name = attribute_key_name.to_s if attr_name == "log_encoding" attr_name = "commit_logs_encoding" end - super(attr_name) + super(attr_name, *args) end # Removes leading and trailing whitespace @@ -71,9 +93,13 @@ end def scm - @scm ||= self.scm_adapter.new(url, root_url, + unless @scm + @scm = self.scm_adapter.new(url, root_url, login, password, path_encoding) - update_attribute(:root_url, @scm.root_url) if root_url.blank? + if root_url.blank? && @scm.root_url.present? + update_attribute(:root_url, @scm.root_url) + end + end @scm end @@ -81,6 +107,52 @@ self.class.scm_name end + def name + if identifier.present? + identifier + elsif is_default? + l(:field_repository_is_default) + else + scm_name + end + end + + def identifier=(identifier) + super unless identifier_frozen? + end + + def identifier_frozen? + errors[:identifier].blank? && !(new_record? || identifier.blank?) + end + + def identifier_param + if is_default? + nil + elsif identifier.present? + identifier + else + id.to_s + end + end + + def <=>(repository) + if is_default? + -1 + elsif repository.is_default? + 1 + else + identifier.to_s <=> repository.identifier.to_s + end + end + + def self.find_by_identifier_param(param) + if param.to_s =~ /^\d+$/ + find_by_id(param) + else + find_by_identifier(param) + end + end + def merge_extra_info(arg) h = extra_info || {} return h if arg.nil? @@ -117,7 +189,9 @@ end def entries(path=nil, identifier=nil) - scm.entries(path, identifier) + entries = scm.entries(path, identifier) + load_entries_changesets(entries) + entries end def branches @@ -159,8 +233,9 @@ # Finds and returns a revision with a number or the beginning of a hash def find_changeset_by_name(name) return nil if name.blank? - changesets.find(:first, :conditions => (name.match(/^\d*$/) ? - ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%'])) + s = name.to_s + changesets.find(:first, :conditions => (s.match(/^\d*$/) ? + ["revision = ?", s] : ["revision LIKE ?", s + '%'])) end def latest_changeset @@ -177,7 +252,7 @@ :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC", :limit => limit) else - changes.find( + filechanges.find( :all, :include => {:changeset => :user}, :conditions => ["path = ?", path.with_leading_slash], @@ -249,10 +324,10 @@ # Can be called periodically by an external script # eg. ruby script/runner "Repository.fetch_changesets" def self.fetch_changesets - Project.active.has_module(:repository).find(:all, :include => :repository).each do |project| - if project.repository + Project.active.has_module(:repository).all.each do |project| + project.repositories.each do |repository| begin - project.repository.fetch_changesets + repository.fetch_changesets rescue Redmine::Scm::Adapters::CommandFailed => e logger.error "scm: error during fetching changesets: #{e.message}" end @@ -318,12 +393,47 @@ ret end + def set_as_default? + new_record? && project && !Repository.first(:conditions => {:project_id => project.id}) + end + + protected + + def check_default + if !is_default? && set_as_default? + self.is_default = true + end + if is_default? && is_default_changed? + Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id]) + end + end + + def load_entries_changesets(entries) + if entries + entries.each do |entry| + if entry.lastrev && entry.lastrev.identifier + entry.changeset = find_changeset_by_name(entry.lastrev.identifier) + end + end + end + end + private + # Deletes repository data def clear_changesets - cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}" + cs = Changeset.table_name + ch = Change.table_name + ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}" + cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}" + connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})") connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})") + connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})") connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}") + clear_extra_info_of_changesets + end + + def clear_extra_info_of_changesets end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/repository/bazaar.rb --- a/app/models/repository/bazaar.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/repository/bazaar.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -15,18 +15,18 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -require 'redmine/scm/adapters/bazaar_adapter' +require_dependency 'redmine/scm/adapters/bazaar_adapter' class Repository::Bazaar < Repository attr_protected :root_url validates_presence_of :url, :log_encoding - def self.human_attribute_name(attribute_key_name) - attr_name = attribute_key_name + def self.human_attribute_name(attribute_key_name, *args) + attr_name = attribute_key_name.to_s if attr_name == "url" attr_name = "path_to_repository" end - super(attr_name) + super(attr_name, *args) end def self.scm_adapter_class @@ -37,7 +37,28 @@ 'Bazaar' end + def entry(path=nil, identifier=nil) + scm.bzr_path_encodig = log_encoding + scm.entry(path, identifier) + end + + def cat(path, identifier=nil) + scm.bzr_path_encodig = log_encoding + scm.cat(path, identifier) + end + + def annotate(path, identifier=nil) + scm.bzr_path_encodig = log_encoding + scm.annotate(path, identifier) + end + + def diff(path, rev, rev_to) + scm.bzr_path_encodig = log_encoding + scm.diff(path, rev, rev_to) + end + def entries(path=nil, identifier=nil) + scm.bzr_path_encodig = log_encoding entries = scm.entries(path, identifier) if entries entries.each do |e| @@ -63,9 +84,12 @@ end end end + load_entries_changesets(entries) + entries end def fetch_changesets + scm.bzr_path_encodig = log_encoding scm_info = scm.info if scm_info # latest revision found in database @@ -78,7 +102,7 @@ while (identifier_from <= scm_revision) # loads changesets by batches of 200 identifier_to = [identifier_from + 199, scm_revision].min - revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true) + revisions = scm.revisions('', identifier_to, identifier_from) transaction do revisions.reverse_each do |revision| changeset = Changeset.create(:repository => self, diff -r ab89f95ef405 -r b2ea0641f798 app/models/repository/cvs.rb --- a/app/models/repository/cvs.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/repository/cvs.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -15,20 +15,23 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -require 'redmine/scm/adapters/cvs_adapter' +require_dependency 'redmine/scm/adapters/cvs_adapter' require 'digest/sha1' class Repository::Cvs < Repository validates_presence_of :url, :root_url, :log_encoding - def self.human_attribute_name(attribute_key_name) - attr_name = attribute_key_name + safe_attributes 'root_url', + :if => lambda {|repository, user| repository.new_record?} + + def self.human_attribute_name(attribute_key_name, *args) + attr_name = attribute_key_name.to_s if attr_name == "root_url" attr_name = "cvsroot" elsif attr_name == "url" attr_name = "cvs_module" end - super(attr_name) + super(attr_name, *args) end def self.scm_adapter_class @@ -54,7 +57,7 @@ if entries entries.each() do |entry| if ( ! entry.lastrev.nil? ) && ( ! entry.lastrev.revision.nil? ) - change=changes.find_by_revision_and_path( + change = filechanges.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) ) if change @@ -66,6 +69,7 @@ end end end + load_entries_changesets(entries) entries end @@ -94,7 +98,7 @@ if rev_to.to_i > 0 changeset_to = changesets.find_by_revision(rev_to) end - changeset_from.changes.each() do |change_from| + changeset_from.filechanges.each() do |change_from| revision_from = nil revision_to = nil if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path)) @@ -102,7 +106,7 @@ end if revision_from if changeset_to - changeset_to.changes.each() do |change_to| + changeset_to.filechanges.each() do |change_to| revision_to = change_to.revision if change_to.path == change_from.path end end @@ -133,7 +137,7 @@ # only add the change to the database, if it doen't exists. the cvs log # is not exclusive at all. tmp_time = revision.time.clone - unless changes.find_by_path_and_revision( + unless filechanges.find_by_path_and_revision( scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision] ) @@ -181,10 +185,9 @@ end # Renumber new changesets in chronological order - changesets.find( - :all, + Changeset.all( :order => 'committed_on ASC, id ASC', - :conditions => "revision LIKE 'tmp%'" + :conditions => ["repository_id = ? AND revision LIKE 'tmp%'", id] ).each do |changeset| changeset.update_attribute :revision, next_revision_number end diff -r ab89f95ef405 -r b2ea0641f798 app/models/repository/darcs.rb --- a/app/models/repository/darcs.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/repository/darcs.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -15,17 +15,17 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -require 'redmine/scm/adapters/darcs_adapter' +require_dependency 'redmine/scm/adapters/darcs_adapter' class Repository::Darcs < Repository validates_presence_of :url, :log_encoding - def self.human_attribute_name(attribute_key_name) - attr_name = attribute_key_name + def self.human_attribute_name(attribute_key_name, *args) + attr_name = attribute_key_name.to_s if attr_name == "url" attr_name = "path_to_repository" end - super(attr_name) + super(attr_name, *args) end def self.scm_adapter_class @@ -66,6 +66,7 @@ end end end + load_entries_changesets(entries) entries end @@ -79,7 +80,7 @@ return nil if patch_from.nil? patch_to = changesets.find_by_revision(rev_to) if rev_to if path.blank? - path = patch_from.changes.collect{|change| change.path}.join(' ') + path = patch_from.filechanges.collect{|change| change.path}.join(' ') end patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil) : nil end diff -r ab89f95ef405 -r b2ea0641f798 app/models/repository/filesystem.rb --- a/app/models/repository/filesystem.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/repository/filesystem.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # FileSystem adapter # File written by Paul Rivier, at Demotera. @@ -18,18 +18,18 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -require 'redmine/scm/adapters/filesystem_adapter' +require_dependency 'redmine/scm/adapters/filesystem_adapter' class Repository::Filesystem < Repository attr_protected :root_url validates_presence_of :url - def self.human_attribute_name(attribute_key_name) - attr_name = attribute_key_name + def self.human_attribute_name(attribute_key_name, *args) + attr_name = attribute_key_name.to_s if attr_name == "url" attr_name = "root_directory" end - super(attr_name) + super(attr_name, *args) end def self.scm_adapter_class @@ -44,10 +44,6 @@ false end - def entries(path=nil, identifier=nil) - scm.entries(path, identifier) - end - def fetch_changesets nil end diff -r ab89f95ef405 -r b2ea0641f798 app/models/repository/git.rb --- a/app/models/repository/git.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/repository/git.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # Copyright (C) 2007 Patrick Aljord patcito@Å‹mail.com # # This program is free software; you can redistribute it and/or @@ -16,18 +16,18 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -require 'redmine/scm/adapters/git_adapter' +require_dependency 'redmine/scm/adapters/git_adapter' class Repository::Git < Repository attr_protected :root_url validates_presence_of :url - def self.human_attribute_name(attribute_key_name) - attr_name = attribute_key_name + def self.human_attribute_name(attribute_key_name, *args) + attr_name = attribute_key_name.to_s if attr_name == "url" attr_name = "path_to_repository" end - super(attr_name) + super(attr_name, *args) end def self.scm_adapter_class @@ -87,28 +87,27 @@ end def find_changeset_by_name(name) - return nil if name.nil? || name.empty? - e = changesets.find(:first, :conditions => ['revision = ?', name.to_s]) - return e if e - changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"]) + if name.present? + changesets.where(:revision => name.to_s).first || + changesets.where('scmid LIKE ?', "#{name}%").first + end end def entries(path=nil, identifier=nil) - scm.entries(path, - identifier, - options = {:report_last_commit => extra_report_last_commit}) + entries = scm.entries(path, identifier, :report_last_commit => extra_report_last_commit) + load_entries_changesets(entries) + entries end # With SCMs that have a sequential commit numbering, # such as Subversion and Mercurial, # Redmine is able to be clever and only fetch changesets # going forward from the most recent one it knows about. - # + # # However, Git does not have a sequential commit numbering. # # In order to fetch only new adding revisions, - # Redmine needs to parse revisions per branch. - # Branch "last_scmid" is for this requirement. + # Redmine needs to save "heads". # # In Git and Mercurial, revisions are not in date order. # Redmine Mercurial fixed issues. @@ -131,9 +130,17 @@ def fetch_changesets scm_brs = branches return if scm_brs.nil? || scm_brs.empty? + h1 = extra_info || {} h = h1.dup - h["branches"] ||= {} + repo_heads = scm_brs.map{ |br| br.scmid } + h["heads"] ||= [] + prev_db_heads = h["heads"].dup + if prev_db_heads.empty? + prev_db_heads += heads_from_branches_hash + end + return if prev_db_heads.sort == repo_heads.sort + h["db_consistent"] ||= {} if changesets.count == 0 h["db_consistent"]["ordering"] = 1 @@ -144,51 +151,97 @@ merge_extra_info(h) self.save end - scm_brs.each do |br1| - br = br1.to_s - from_scmid = nil - from_scmid = h["branches"][br]["last_scmid"] if h["branches"][br] - h["branches"][br] ||= {} - scm.revisions('', from_scmid, br, {:reverse => true}) do |rev| - db_rev = find_changeset_by_name(rev.revision) - transaction do - if db_rev.nil? - db_saved_rev = save_revision(rev) - parents = {} - parents[db_saved_rev] = rev.parents unless rev.parents.nil? - parents.each do |ch, chparents| - ch.parents = chparents.collect{|rp| find_changeset_by_name(rp)}.compact - end - end - h["branches"][br]["last_scmid"] = rev.scmid - merge_extra_info(h) - self.save - end + save_revisions(prev_db_heads, repo_heads) + end + + def save_revisions(prev_db_heads, repo_heads) + h = {} + opts = {} + opts[:reverse] = true + opts[:excludes] = prev_db_heads + opts[:includes] = repo_heads + + revisions = scm.revisions('', nil, nil, opts) + return if revisions.blank? + + # Make the search for existing revisions in the database in a more sufficient manner + # + # Git branch is the reference to the specific revision. + # Git can *delete* remote branch and *re-push* branch. + # + # $ git push remote :branch + # $ git push remote branch + # + # After deleting branch, revisions remain in repository until "git gc". + # On git 1.7.2.3, default pruning date is 2 weeks. + # So, "git log --not deleted_branch_head_revision" return code is 0. + # + # After re-pushing branch, "git log" returns revisions which are saved in database. + # So, Redmine needs to scan revisions and database every time. + # + # This is replacing the one-after-one queries. + # Find all revisions, that are in the database, and then remove them from the revision array. + # Then later we won't need any conditions for db existence. + # Query for several revisions at once, and remove them from the revisions array, if they are there. + # Do this in chunks, to avoid eventual memory problems (in case of tens of thousands of commits). + # If there are no revisions (because the original code's algorithm filtered them), + # then this part will be stepped over. + # We make queries, just if there is any revision. + limit = 100 + offset = 0 + revisions_copy = revisions.clone # revisions will change + while offset < revisions_copy.size + recent_changesets_slice = changesets.find( + :all, + :conditions => [ + 'scmid IN (?)', + revisions_copy.slice(offset, limit).map{|x| x.scmid} + ] + ) + # Subtract revisions that redmine already knows about + recent_revisions = recent_changesets_slice.map{|c| c.scmid} + revisions.reject!{|r| recent_revisions.include?(r.scmid)} + offset += limit + end + + revisions.each do |rev| + transaction do + # There is no search in the db for this revision, because above we ensured, + # that it's not in the db. + save_revision(rev) end end + h["heads"] = repo_heads.dup + merge_extra_info(h) + self.save end + private :save_revisions def save_revision(rev) - changeset = Changeset.new( + parents = (rev.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact + changeset = Changeset.create( :repository => self, :revision => rev.identifier, :scmid => rev.scmid, :committer => rev.author, :committed_on => rev.time, - :comments => rev.message + :comments => rev.message, + :parents => parents ) - if changeset.save - rev.paths.each do |file| - Change.create( - :changeset => changeset, - :action => file[:action], - :path => file[:path]) - end + unless changeset.new_record? + rev.paths.each { |change| changeset.create_change(change) } end changeset end private :save_revision + def heads_from_branches_hash + h1 = extra_info || {} + h = h1.dup + h["branches"] ||= {} + h['branches'].map{|br, hs| hs['last_scmid']} + end + def latest_changesets(path,rev,limit=10) revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false) return [] if revisions.nil? || revisions.empty? @@ -202,4 +255,15 @@ :order => 'committed_on DESC' ) end + + def clear_extra_info_of_changesets + return if extra_info.nil? + v = extra_info["extra_report_last_commit"] + write_attribute(:extra_info, nil) + h = {} + h["extra_report_last_commit"] = v + merge_extra_info(h) + self.save + end + private :clear_extra_info_of_changesets end diff -r ab89f95ef405 -r b2ea0641f798 app/models/repository/mercurial.rb --- a/app/models/repository/mercurial.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/repository/mercurial.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -15,7 +15,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -require 'redmine/scm/adapters/mercurial_adapter' +require_dependency 'redmine/scm/adapters/mercurial_adapter' class Repository::Mercurial < Repository # sort changesets by revision number @@ -29,12 +29,12 @@ # number of changesets to fetch at once FETCH_AT_ONCE = 100 - def self.human_attribute_name(attribute_key_name) - attr_name = attribute_key_name + def self.human_attribute_name(attribute_key_name, *args) + attr_name = attribute_key_name.to_s if attr_name == "url" attr_name = "path_to_repository" end - super(attr_name) + super(attr_name, *args) end def self.scm_adapter_class @@ -73,14 +73,15 @@ # Finds and returns a revision with a number or the beginning of a hash def find_changeset_by_name(name) - return nil if name.nil? || name.empty? - if /[^\d]/ =~ name or name.to_s.size > 8 - e = changesets.find(:first, :conditions => ['scmid = ?', name.to_s]) + return nil if name.blank? + s = name.to_s + if /[^\d]/ =~ s or s.size > 8 + cs = changesets.where(:scmid => s).first else - e = changesets.find(:first, :conditions => ['revision = ?', name.to_s]) + cs = changesets.where(:revision => s).first end - return e if e - changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"]) # last ditch + return cs if cs + changesets.where('scmid LIKE ?', "#{s}%").first end # Returns the latest changesets for +path+; sorted by revision number @@ -137,19 +138,18 @@ logger.debug "Fetching changesets for repository #{url}" if logger (db_rev + 1).step(scm_rev, FETCH_AT_ONCE) do |i| - transaction do - scm.each_revision('', i, [i + FETCH_AT_ONCE - 1, scm_rev].min) do |re| + scm.each_revision('', i, [i + FETCH_AT_ONCE - 1, scm_rev].min) do |re| + transaction do + parents = (re.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact cs = Changeset.create(:repository => self, :revision => re.revision, :scmid => re.scmid, :committer => re.author, :committed_on => re.time, - :comments => re.message) - re.paths.each { |e| cs.create_change(e) } - parents = {} - parents[cs] = re.parents unless re.parents.nil? - parents.each do |ch, chparents| - ch.parents = chparents.collect{|rp| find_changeset_by_name(rp)}.compact + :comments => re.message, + :parents => parents) + unless cs.new_record? + re.paths.each { |e| cs.create_change(e) } end end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/repository/subversion.rb --- a/app/models/repository/subversion.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/repository/subversion.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -15,7 +15,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -require 'redmine/scm/adapters/subversion_adapter' +require_dependency 'redmine/scm/adapters/subversion_adapter' class Repository::Subversion < Repository attr_protected :root_url @@ -40,7 +40,12 @@ def latest_changesets(path, rev, limit=10) revisions = scm.revisions(path, rev, nil, :limit => limit) - revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC", :include => :user) : [] + if revisions + identifiers = revisions.collect(&:identifier).compact + changesets.where(:revision => identifiers).reorder("committed_on DESC").includes(:repository, :user).all + else + [] + end end # Returns a path relative to the url of the repository @@ -81,6 +86,24 @@ end end + protected + + def load_entries_changesets(entries) + return unless entries + + entries_with_identifier = entries.select {|entry| entry.lastrev && entry.lastrev.identifier.present?} + identifiers = entries_with_identifier.map {|entry| entry.lastrev.identifier}.compact.uniq + + if identifiers.any? + changesets_by_identifier = changesets.where(:revision => identifiers).includes(:user, :repository).all.group_by(&:revision) + entries_with_identifier.each do |entry| + if m = changesets_by_identifier[entry.lastrev.identifier] + entry.changeset = m.first + end + end + end + end + private # Returns the relative url of the repository diff -r ab89f95ef405 -r b2ea0641f798 app/models/role.rb --- a/app/models/role.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/role.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,6 +16,19 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class Role < ActiveRecord::Base + # Custom coder for the permissions attribute that should be an + # array of symbols. Rails 3 uses Psych which can be *unbelievably* + # slow on some platforms (eg. mingw32). + class PermissionsAttributeCoder + def self.load(str) + str.to_s.scan(/:([a-z0-9_]+)/).flatten.map(&:to_sym) + end + + def self.dump(value) + YAML.dump(value) + end + end + # Built-in roles BUILTIN_NON_MEMBER = 1 BUILTIN_ANONYMOUS = 2 @@ -26,16 +39,17 @@ ['own', :label_issues_visibility_own] ] - named_scope :givable, { :conditions => "builtin = 0", :order => 'position' } - named_scope :builtin, lambda { |*args| - compare = 'not' if args.first == true - { :conditions => "#{compare} builtin = 0" } + scope :sorted, order("#{table_name}.builtin ASC, #{table_name}.position ASC") + scope :givable, order("#{table_name}.position ASC").where(:builtin => 0) + scope :builtin, lambda { |*args| + compare = (args.first == true ? 'not' : '') + where("#{compare} builtin = 0") } before_destroy :check_deletable - has_many :workflows, :dependent => :delete_all do + has_many :workflow_rules, :dependent => :delete_all do def copy(source_role) - Workflow.copy(nil, source_role, nil, proxy_owner) + WorkflowRule.copy(nil, source_role, nil, proxy_association.owner) end end @@ -43,7 +57,7 @@ has_many :members, :through => :member_roles acts_as_list - serialize :permissions, Array + serialize :permissions, ::Role::PermissionsAttributeCoder attr_protected :builtin validates_presence_of :name @@ -53,8 +67,13 @@ :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first), :if => lambda {|role| role.respond_to?(:issues_visibility)} - def permissions - read_attribute(:permissions) || [] + # Copies attributes from another role, arg can be an id or a Role + def copy_from(arg, options={}) + return unless arg.present? + role = arg.is_a?(Role) ? arg : Role.find_by_id(arg.to_s) + self.attributes = role.attributes.dup.except("id", "name", "position", "builtin", "permissions") + self.permissions = role.permissions.dup + self end def permissions=(perms) @@ -86,7 +105,15 @@ end def <=>(role) - role ? position <=> role.position : -1 + if role + if builtin == role.builtin + position <=> role.position + else + builtin <=> role.builtin + end + else + -1 + end end def to_s @@ -106,6 +133,11 @@ self.builtin != 0 end + # Return true if the role is the anonymous role + def anonymous? + builtin == 2 + end + # Return true if the role is a project member role def member? !self.builtin? @@ -133,7 +165,7 @@ # Find all the roles that can be given to a project member def self.find_all_givable - find(:all, :conditions => {:builtin => 0}, :order => 'position') + Role.givable.all end # Return the builtin 'non member' role. If the role doesn't exist, @@ -164,7 +196,7 @@ end def self.find_or_create_system_role(builtin, name) - role = first(:conditions => {:builtin => builtin}) + role = where(:builtin => builtin).first if role.nil? role = create(:name => name, :position => 0) do |r| r.builtin = builtin diff -r ab89f95ef405 -r b2ea0641f798 app/models/setting.rb --- a/app/models/setting.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/setting.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/time_entry.rb --- a/app/models/time_entry.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/time_entry.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -42,14 +42,34 @@ before_validation :set_project_if_nil validate :validate_time_entry - named_scope :visible, lambda {|*args| { + scope :visible, lambda {|*args| { :include => :project, :conditions => Project.allowed_to_condition(args.shift || User.current, :view_time_entries, *args) }} + scope :on_issue, lambda {|issue| { + :include => :issue, + :conditions => "#{Issue.table_name}.root_id = #{issue.root_id} AND #{Issue.table_name}.lft >= #{issue.lft} AND #{Issue.table_name}.rgt <= #{issue.rgt}" + }} + scope :on_project, lambda {|project, include_subprojects| { + :include => :project, + :conditions => project.project_condition(include_subprojects) + }} + scope :spent_between, lambda {|from, to| + if from && to + {:conditions => ["#{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", from, to]} + elsif from + {:conditions => ["#{TimeEntry.table_name}.spent_on >= ?", from]} + elsif to + {:conditions => ["#{TimeEntry.table_name}.spent_on <= ?", to]} + else + {} + end + } - safe_attributes 'hours', 'comments', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values' + safe_attributes 'hours', 'comments', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values', 'custom_fields' - def after_initialize + def initialize(attributes=nil, *args) + super if new_record? && self.activity.nil? if default_activity = TimeEntryActivity.default self.activity_id = default_activity.id @@ -72,6 +92,15 @@ write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h) end + def hours + h = read_attribute(:hours) + if h.is_a?(Float) + h.round(2) + else + h + end + end + # tyear, tmonth, tweek assigned where setting spent_on attributes # these attributes make time aggregations easier def spent_on=(date) @@ -88,20 +117,4 @@ def editable_by?(usr) (usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project) end - - def self.earilest_date_for_project(project=nil) - finder_conditions = ARCondition.new(Project.allowed_to_condition(User.current, :view_time_entries)) - if project - finder_conditions << ["project_id IN (?)", project.hierarchy.collect(&:id)] - end - TimeEntry.minimum(:spent_on, :include => :project, :conditions => finder_conditions.conditions) - end - - def self.latest_date_for_project(project=nil) - finder_conditions = ARCondition.new(Project.allowed_to_condition(User.current, :view_time_entries)) - if project - finder_conditions << ["project_id IN (?)", project.hierarchy.collect(&:id)] - end - TimeEntry.maximum(:spent_on, :include => :project, :conditions => finder_conditions.conditions) - end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/time_entry_activity.rb --- a/app/models/time_entry_activity.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/time_entry_activity.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/time_entry_activity_custom_field.rb --- a/app/models/time_entry_activity_custom_field.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/time_entry_activity_custom_field.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/time_entry_custom_field.rb --- a/app/models/time_entry_custom_field.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/time_entry_custom_field.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/token.rb --- a/app/models/token.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/token.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -34,12 +34,12 @@ # Delete all expired tokens def self.destroy_expired - Token.delete_all ["action <> 'feeds' AND created_on < ?", Time.now - @@validity_time] + Token.delete_all ["action NOT IN (?) AND created_on < ?", ['feeds', 'api'], Time.now - @@validity_time] end private def self.generate_token_value - ActiveSupport::SecureRandom.hex(20) + Redmine::Utils.random_hex(20) end # Removes obsolete tokens (same user and action) diff -r ab89f95ef405 -r b2ea0641f798 app/models/tracker.rb --- a/app/models/tracker.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/tracker.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,11 +16,18 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class Tracker < ActiveRecord::Base + + CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze + # Fields that can be disabled + # Other (future) fields should be appended, not inserted! + CORE_FIELDS = %w(assigned_to_id category_id fixed_version_id parent_issue_id start_date due_date estimated_hours done_ratio).freeze + CORE_FIELDS_ALL = (CORE_FIELDS_UNDISABLABLE + CORE_FIELDS).freeze + before_destroy :check_integrity has_many :issues - has_many :workflows, :dependent => :delete_all do + has_many :workflow_rules, :dependent => :delete_all do def copy(source_tracker) - Workflow.copy(source_tracker, nil, proxy_owner, nil) + WorkflowRule.copy(source_tracker, nil, proxy_association.owner, nil) end end @@ -28,20 +35,19 @@ has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id' acts_as_list + attr_protected :field_bits + validates_presence_of :name validates_uniqueness_of :name validates_length_of :name, :maximum => 30 - named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}} + scope :sorted, order("#{table_name}.position ASC") + scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)} def to_s; name end def <=>(tracker) - name <=> tracker.name - end - - def self.all - find(:all, :order => 'position') + position <=> tracker.position end # Returns an array of IssueStatus that are used @@ -53,16 +59,57 @@ return [] end - ids = Workflow. - connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{Workflow.table_name} WHERE tracker_id = #{id}"). + ids = WorkflowTransition. + connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{WorkflowTransition.table_name} WHERE tracker_id = #{id} AND type = 'WorkflowTransition'"). flatten. uniq @issue_statuses = IssueStatus.find_all_by_id(ids).sort end + def disabled_core_fields + i = -1 + @disabled_core_fields ||= CORE_FIELDS.select { i += 1; (fields_bits || 0) & (2 ** i) != 0} + end + + def core_fields + CORE_FIELDS - disabled_core_fields + end + + def core_fields=(fields) + raise ArgumentError.new("Tracker.core_fields takes an array") unless fields.is_a?(Array) + + bits = 0 + CORE_FIELDS.each_with_index do |field, i| + unless fields.include?(field) + bits |= 2 ** i + end + end + self.fields_bits = bits + @disabled_core_fields = nil + core_fields + end + + # Returns the fields that are disabled for all the given trackers + def self.disabled_core_fields(trackers) + if trackers.present? + trackers.uniq.map(&:disabled_core_fields).reduce(:&) + else + [] + end + end + + # Returns the fields that are enabled for one tracker at least + def self.core_fields(trackers) + if trackers.present? + trackers.uniq.map(&:core_fields).reduce(:|) + else + CORE_FIELDS.dup + end + end + private def check_integrity - raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id]) + raise Exception.new("Can't delete tracker") if Issue.where(:tracker_id => self.id).any? end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/user.rb --- a/app/models/user.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/user.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -28,11 +28,41 @@ # Different ways of displaying/sorting users USER_FORMATS = { - :firstname_lastname => {:string => '#{firstname} #{lastname}', :order => %w(firstname lastname id)}, - :firstname => {:string => '#{firstname}', :order => %w(firstname id)}, - :lastname_firstname => {:string => '#{lastname} #{firstname}', :order => %w(lastname firstname id)}, - :lastname_coma_firstname => {:string => '#{lastname}, #{firstname}', :order => %w(lastname firstname id)}, - :username => {:string => '#{login}', :order => %w(login id)}, + :firstname_lastname => { + :string => '#{firstname} #{lastname}', + :order => %w(firstname lastname id), + :setting_order => 1 + }, + :firstname_lastinitial => { + :string => '#{firstname} #{lastname.to_s.chars.first}.', + :order => %w(firstname lastname id), + :setting_order => 2 + }, + :firstname => { + :string => '#{firstname}', + :order => %w(firstname id), + :setting_order => 3 + }, + :lastname_firstname => { + :string => '#{lastname} #{firstname}', + :order => %w(lastname firstname id), + :setting_order => 4 + }, + :lastname_coma_firstname => { + :string => '#{lastname}, #{firstname}', + :order => %w(lastname firstname id), + :setting_order => 5 + }, + :lastname => { + :string => '#{lastname}', + :order => %w(lastname id), + :setting_order => 6 + }, + :username => { + :string => '#{login}', + :order => %w(login id), + :setting_order => 7 + }, } MAIL_NOTIFICATION_OPTIONS = [ @@ -52,33 +82,34 @@ has_one :api_token, :class_name => 'Token', :conditions => "action='api'" belongs_to :auth_source + scope :logged, :conditions => "#{User.table_name}.status <> #{STATUS_ANONYMOUS}" + scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} } + has_one :ssamr_user_detail, :dependent => :destroy, :class_name => 'SsamrUserDetail' accepts_nested_attributes_for :ssamr_user_detail has_one :author - # Active non-anonymous users scope - named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}" - acts_as_customizable attr_accessor :password, :password_confirmation attr_accessor :last_before_login_on # Prevents unauthorized assignments attr_protected :login, :admin, :password, :password_confirmation, :hashed_password - + + LOGIN_LENGTH_LIMIT = 60 + MAIL_LENGTH_LIMIT = 60 + validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } + validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false + validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false - # TODO: is this validation correct validates_presence_of :ssamr_user_detail - - validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false - validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false # Login must contain lettres, numbers, underscores only validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i - validates_length_of :login, :maximum => 30 + validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT validates_length_of :firstname, :lastname, :maximum => 30 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_blank => true - validates_length_of :mail, :maximum => 60, :allow_nil => true + validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true validates_confirmation_of :password, :allow_nil => true validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true validate :validate_password_length @@ -89,13 +120,13 @@ validates_acceptance_of :terms_and_conditions, :on => :create, :message => :must_accept_terms_and_conditions - named_scope :in_group, lambda {|group| + scope :in_group, lambda {|group| group_id = group.is_a?(Group) ? group.id : group.to_i - { :conditions => ["#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] } + where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id) } - named_scope :not_in_group, lambda {|group| + scope :not_in_group, lambda {|group| group_id = group.is_a?(Group) ? group.id : group.to_i - { :conditions => ["#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] } + where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id) } def set_mail_notification @@ -139,8 +170,11 @@ # Returns the user that matches provided login and password, or nil def self.try_to_login(login, password) + login = login.to_s + password = password.to_s + # Make sure no one can sign in with an empty password - return nil if password.to_s.empty? + return nil if password.empty? user = find_by_login(login) if user # user is already in local database @@ -173,7 +207,7 @@ # Returns the user who matches the given autologin +key+ or nil def self.try_to_autologin(key) - tokens = Token.find_all_by_action_and_value('autologin', key) + tokens = Token.find_all_by_action_and_value('autologin', key.to_s) # Make sure there's only 1 token that matches the key if tokens.size == 1 token = tokens.first @@ -263,7 +297,7 @@ # Does the backend storage allow this user to change their password? def change_password_allowed? - return true if auth_source_id.blank? + return true if auth_source.nil? return auth_source.allow_password_changes? end @@ -293,14 +327,18 @@ # Return user's RSS key (a 40 chars long string), used to access feeds def rss_key - token = self.rss_token || Token.create(:user => self, :action => 'feeds') - token.value + if rss_token.nil? + create_rss_token(:action => 'feeds') + end + rss_token.value end # Return user's API key (a 40 chars long string), used to access the API def api_key - token = self.api_token || self.create_api_token(:action => 'api') - token.value + if api_token.nil? + create_api_token(:action => 'api') + end + api_token.value end # Return an array of project ids for which the user has explicitly turned mail notifications on @@ -333,28 +371,28 @@ # Find a user account by matching the exact login and then a case-insensitive # version. Exact matches will be given priority. def self.find_by_login(login) - # force string comparison to be case sensitive on MySQL - type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : '' - # First look for an exact match - user = first(:conditions => ["#{type_cast} login = ?", login]) - # Fail over to case-insensitive if none was found - user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase]) + user = where(:login => login).all.detect {|u| u.login == login} + unless user + # Fail over to case-insensitive if none was found + user = where("LOWER(login) = ?", login.to_s.downcase).first + end + user end def self.find_by_rss_key(key) - token = Token.find_by_value(key) + token = Token.find_by_action_and_value('feeds', key.to_s) token && token.user.active? ? token.user : nil end def self.find_by_api_key(key) - token = Token.find_by_action_and_value('api', key) + token = Token.find_by_action_and_value('api', key.to_s) token && token.user.active? ? token.user : nil end # Makes find_by_mail case-insensitive def self.find_by_mail(mail) - find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase]) + where("LOWER(mail) = ?", mail.to_s.downcase).first end # Returns true if the default admin account can no longer be used @@ -366,6 +404,17 @@ name end + CSS_CLASS_BY_STATUS = { + STATUS_ANONYMOUS => 'anon', + STATUS_ACTIVE => 'active', + STATUS_REGISTERED => 'registered', + STATUS_LOCKED => 'locked' + } + + def css_classes + "user #{CSS_CLASS_BY_STATUS[status]}" + end + # Returns the current day according to user's time zone def today if time_zone.nil? @@ -375,6 +424,15 @@ end end + # Returns the day of +time+ according to user's time zone + def time_to_date(time) + if time_zone.nil? + time.to_date + else + time.in_time_zone(time_zone).to_date + end + end + def logged? true end @@ -387,7 +445,7 @@ def roles_for_project(project) roles = [] # No role on archived projects - return roles unless project && project.active? + return roles if project.nil? || project.archived? if logged? # Find project membership membership = memberships.detect {|m| m.project_id == project.id} @@ -413,10 +471,13 @@ def projects_by_role return @projects_by_role if @projects_by_role - @projects_by_role = Hash.new {|h,k| h[k]=[]} + @projects_by_role = Hash.new([]) memberships.each do |membership| - membership.roles.each do |role| - @projects_by_role[role] << membership.project if membership.project + if membership.project + membership.roles.each do |role| + @projects_by_role[role] = [] unless @projects_by_role.key?(role) + @projects_by_role[role] << membership.project + end end end @projects_by_role.each do |role, projects| @@ -448,26 +509,23 @@ # or falls back to Non Member / Anonymous permissions depending if the user is logged def allowed_to?(action, context, options={}, &block) if context && context.is_a?(Project) - # No action allowed on archived projects - return false unless context.active? - # No action allowed on disabled modules return false unless context.allows_to?(action) # Admin users are authorized for anything else return true if admin? roles = roles_for_project(context) return false unless roles - roles.detect {|role| + roles.any? {|role| (context.is_public? || role.member?) && role.allowed_to?(action) && (block_given? ? yield(role, self) : true) } elsif context && context.is_a?(Array) - # Authorize if user is authorized on every element of the array - context.map do |project| - allowed_to?(action, project, options, &block) - end.inject do |memo,allowed| - memo && allowed + if context.empty? + false + else + # Authorize if user is authorized on every element of the array + context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&) end elsif options[:global] # Admin users are always authorized @@ -476,7 +534,7 @@ # authorize if user has at least one role that has this permission roles = memberships.collect {|m| m.roles}.flatten.uniq roles << (self.logged? ? Role.non_member : Role.anonymous) - roles.detect {|role| + roles.any? {|role| role.allowed_to?(action) && (block_given? ? yield(role, self) : true) } @@ -491,6 +549,12 @@ allowed_to?(action, nil, options.reverse_merge(:global => true), &block) end + # Returns true if the user is allowed to delete his own account + def own_account_deletable? + Setting.unsubscribe? && + (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?) + end + safe_attributes 'login', 'firstname', 'lastname', @@ -518,7 +582,7 @@ true when 'selected' # user receives notifications for created/assigned issues on unselected projects - if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to)) + if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)) true else false @@ -526,13 +590,13 @@ when 'none' false when 'only_my_events' - if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to)) + if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)) true else false end when 'only_assigned' - if object.is_a?(Issue) && is_or_belongs_to?(object.assigned_to) + if object.is_a?(Issue) && (is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)) true else false @@ -559,7 +623,7 @@ # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only # one anonymous user per database. def self.anonymous - anonymous_user = AnonymousUser.find(:first) + anonymous_user = AnonymousUser.first if anonymous_user.nil? anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0) raise 'Unable to create the anonymous user.' if anonymous_user.new_record? @@ -572,11 +636,11 @@ # This method is used in the SaltPasswords migration and is to be kept as is def self.salt_unsalted_passwords! transaction do - User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user| + User.where("salt IS NULL OR salt = ''").find_each do |user| next if user.hashed_password.blank? salt = User.generate_salt hashed_password = User.hash_password("#{salt}#{user.hashed_password}") - User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] ) + User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password) end end end @@ -608,8 +672,8 @@ Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] # Remove private queries and keep public ones - Query.delete_all ['user_id = ? AND is_public = ?', id, false] - Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] + ::Query.delete_all ['user_id = ? AND is_public = ?', id, false] + ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] Token.delete_all ['user_id = ?', id] Watcher.delete_all ['user_id = ?', id] @@ -624,14 +688,15 @@ # Returns a 128bits random salt as a hex string (32 chars long) def self.generate_salt - ActiveSupport::SecureRandom.hex(16) + Redmine::Utils.random_hex(16) end end class AnonymousUser < User + validate :validate_anonymous_uniqueness, :on => :create - def validate_on_create + def validate_anonymous_uniqueness # There should be only one AnonymousUser in the database errors.add :base, 'An anonymous user already exists.' if AnonymousUser.find(:first) end @@ -648,6 +713,10 @@ def time_zone; nil end def rss_key; nil end + def pref + UserPreference.new(:user => self) + end + # Anonymous user can not be destroyed def destroy false diff -r ab89f95ef405 -r b2ea0641f798 app/models/user_custom_field.rb --- a/app/models/user_custom_field.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/user_custom_field.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/user_preference.rb --- a/app/models/user_preference.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/user_preference.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -21,12 +21,14 @@ attr_protected :others, :user_id - def initialize(attributes = nil) + before_save :set_others_hash + + def initialize(attributes=nil, *args) super self.others ||= {} end - def before_save + def set_others_hash self.others ||= {} end @@ -42,7 +44,7 @@ if attribute_present? attr_name super else - h = read_attribute(:others).dup || {} + h = (read_attribute(:others) || {}).dup h.update(attr_name => value) write_attribute(:others, h) value diff -r ab89f95ef405 -r b2ea0641f798 app/models/version.rb --- a/app/models/version.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/version.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -33,11 +33,13 @@ validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true validates_inclusion_of :status, :in => VERSION_STATUSES validates_inclusion_of :sharing, :in => VERSION_SHARINGS + validate :validate_version - named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}} - named_scope :open, :conditions => {:status => 'open'} - named_scope :visible, lambda {|*args| { :include => :project, - :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } } + scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)} + scope :open, where(:status => 'open') + scope :visible, lambda {|*args| + includes(:project).where(Project.allowed_to_condition(args.first || User.current, :view_issues)) + } safe_attributes 'name', 'description', @@ -78,7 +80,7 @@ # Returns the total reported time for this version def spent_hours - @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f + @spent_hours ||= TimeEntry.joins(:issue).where("#{Issue.table_name}.fixed_version_id = ?", id).sum(:hours).to_f end def closed? @@ -91,7 +93,7 @@ # Returns true if the version is completed: due date reached and no open issues def completed? - effective_date && (effective_date <= Date.today) && (open_issues_count == 0) + effective_date && (effective_date < Date.today) && (open_issues_count == 0) end def behind_schedule? @@ -133,17 +135,20 @@ # Returns assigned issues count def issues_count - @issue_count ||= fixed_issues.count + load_issue_counts + @issue_count end # Returns the total amount of open issues for this version. def open_issues_count - @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status) + load_issue_counts + @open_issues_count end # Returns the total amount of closed issues for this version. def closed_issues_count - @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status) + load_issue_counts + @closed_issues_count end def wiki_page @@ -159,13 +164,13 @@ "#{project} - #{name}" end - # Versions are sorted by effective_date and "Project Name - Version name" - # Those with no effective_date are at the end, sorted by "Project Name - Version name" + # Versions are sorted by effective_date and name + # Those with no effective_date are at the end, sorted by name def <=>(version) if self.effective_date if version.effective_date if self.effective_date == version.effective_date - "#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}" + name == version.name ? id <=> version.id : name <=> version.name else self.effective_date <=> version.effective_date end @@ -176,11 +181,18 @@ if version.effective_date 1 else - "#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}" + name == version.name ? id <=> version.id : name <=> version.name end end end + def self.fields_for_order_statement(table=nil) + table ||= table_name + ["(CASE WHEN #{table}.effective_date IS NULL THEN 1 ELSE 0 END)", "#{table}.effective_date", "#{table}.name", "#{table}.id"] + end + + scope :sorted, order(fields_for_order_statement) + # Returns the sharings that +user+ can set the version to def allowed_sharings(user = User.current) VERSION_SHARINGS.select do |s| @@ -204,6 +216,21 @@ private + def load_issue_counts + unless @issue_count + @open_issues_count = 0 + @closed_issues_count = 0 + fixed_issues.count(:all, :group => :status).each do |status, count| + if status.is_closed? + @closed_issues_count += count + else + @open_issues_count += count + end + end + @issue_count = @open_issues_count + @closed_issues_count + end + end + # Update the issue's fixed versions. Used if a version's sharing changes. def update_issues_from_sharing_change if sharing_changed? @@ -242,12 +269,16 @@ if issues_count > 0 ratio = open ? 'done_ratio' : 100 - done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}", - :include => :status, - :conditions => ["is_closed = ?", !open]).to_f + done = fixed_issues.open(open).sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}").to_f progress = done / (estimated_average * issues_count) end progress end end + + def validate_version + if effective_date.nil? && @attributes['effective_date'].present? + errors.add :effective_date, :not_a_date + end + end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/version_custom_field.rb --- a/app/models/version_custom_field.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/version_custom_field.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/watcher.rb --- a/app/models/watcher.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/watcher.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -21,6 +21,7 @@ validates_presence_of :user validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id] + validate :validate_user # Unwatch things that users are no longer allowed to view def self.prune(options={}) @@ -37,7 +38,7 @@ protected - def validate + def validate_user errors.add :user_id, :invalid unless user.nil? || user.active? end diff -r ab89f95ef405 -r b2ea0641f798 app/models/wiki.rb --- a/app/models/wiki.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/wiki.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/wiki_content.rb --- a/app/models/wiki_content.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/wiki_content.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ -# RedMine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -18,7 +18,7 @@ require 'zlib' class WikiContent < ActiveRecord::Base - set_locking_column :version + self.locking_column = 'version' belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id' belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' validates_presence_of :text @@ -73,6 +73,8 @@ "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " + "LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"} + after_destroy :page_update_after_destroy + def text=(plain) case Setting.wiki_compression when 'gzip' @@ -91,14 +93,16 @@ end def text - @text ||= case compression - when 'gzip' - str = Zlib::Inflate.inflate(data) + @text ||= begin + str = case compression + when 'gzip' + Zlib::Inflate.inflate(data) + else + # uncompressed data + data + end str.force_encoding("UTF-8") if str.respond_to?(:force_encoding) str - else - # uncompressed data - data end end @@ -113,10 +117,31 @@ # Returns the previous version or nil def previous - @previous ||= WikiContent::Version.find(:first, - :order => 'version DESC', - :include => :author, - :conditions => ["wiki_content_id = ? AND version < ?", wiki_content_id, version]) + @previous ||= WikiContent::Version. + reorder('version DESC'). + includes(:author). + where("wiki_content_id = ? AND version < ?", wiki_content_id, version).first + end + + # Returns the next version or nil + def next + @next ||= WikiContent::Version. + reorder('version ASC'). + includes(:author). + where("wiki_content_id = ? AND version > ?", wiki_content_id, version).first + end + + private + + # Updates page's content if the latest version is removed + # or destroys the page if it was the only version + def page_update_after_destroy + latest = page.content.versions.reorder("#{self.class.table_name}.version DESC").first + if latest && page.content.version != latest.version + raise ActiveRecord::Rollback unless page.content.revert_to!(latest) + elsif latest.nil? + raise ActiveRecord::Rollback unless page.destroy + end end end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/wiki_content_observer.rb --- a/app/models/wiki_content_observer.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/wiki_content_observer.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,12 +17,12 @@ class WikiContentObserver < ActiveRecord::Observer def after_create(wiki_content) - Mailer.deliver_wiki_content_added(wiki_content) if Setting.notified_events.include?('wiki_content_added') + Mailer.wiki_content_added(wiki_content).deliver if Setting.notified_events.include?('wiki_content_added') end def after_update(wiki_content) if wiki_content.text_changed? - Mailer.deliver_wiki_content_updated(wiki_content) if Setting.notified_events.include?('wiki_content_updated') + Mailer.wiki_content_updated(wiki_content).deliver if Setting.notified_events.include?('wiki_content_updated') end end end diff -r ab89f95ef405 -r b2ea0641f798 app/models/wiki_page.rb --- a/app/models/wiki_page.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/wiki_page.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -19,6 +19,8 @@ require 'enumerator' class WikiPage < ActiveRecord::Base + include Redmine::SafeAttributes + belongs_to :wiki has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy acts_as_attachable :delete_permission => :delete_wiki_pages_attachments @@ -47,15 +49,19 @@ before_save :handle_redirects # eager load information about last updates, without loading text - named_scope :with_updated_on, { - :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on", + scope :with_updated_on, { + :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on, #{WikiContent.table_name}.version", :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id" } # Wiki pages that are protected by default DEFAULT_PROTECTED_PAGES = %w(sidebar) - def after_initialize + safe_attributes 'parent_id', 'parent_title', + :if => lambda {|page, user| page.new_record? || user.allowed_to?(:rename_wiki_pages, page.project)} + + def initialize(attributes=nil, *args) + super if new_record? && DEFAULT_PROTECTED_PAGES.include?(title.to_s.downcase) self.protected = true end @@ -105,11 +111,13 @@ def diff(version_to=nil, version_from=nil) version_to = version_to ? version_to.to_i : self.content.version - version_from = version_from ? version_from.to_i : version_to - 1 - version_to, version_from = version_from, version_to unless version_from < version_to + content_to = content.versions.find_by_version(version_to) + content_from = version_from ? content.versions.find_by_version(version_from.to_i) : content_to.try(:previous) + return nil unless content_to && content_from - content_to = content.versions.find_by_version(version_to) - content_from = content.versions.find_by_version(version_from) + if content_from.version > content_to.version + content_to, content_from = content_from, content_to + end (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil end @@ -136,7 +144,10 @@ unless @updated_on if time = read_attribute(:updated_on) # content updated_on was eager loaded with the page - @updated_on = Time.parse(time) rescue nil + begin + @updated_on = (self.class.default_timezone == :utc ? Time.parse(time.to_s).utc : Time.parse(time.to_s).localtime) + rescue + end else @updated_on = content && content.updated_on end @@ -163,6 +174,21 @@ self.parent = parent_page end + # Saves the page and its content if text was changed + def save_with_content + ret = nil + transaction do + if new_record? + # Rails automatically saves associated content + ret = save + else + ret = save && (content.text_changed? ? content.save : true) + end + raise ActiveRecord::Rollback unless ret + end + ret + end + protected def validate_parent_title diff -r ab89f95ef405 -r b2ea0641f798 app/models/wiki_redirect.rb --- a/app/models/wiki_redirect.rb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/models/wiki_redirect.rb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License diff -r ab89f95ef405 -r b2ea0641f798 app/models/workflow.rb --- a/app/models/workflow.rb Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +0,0 @@ -# Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -class Workflow < ActiveRecord::Base - belongs_to :role - belongs_to :old_status, :class_name => 'IssueStatus', :foreign_key => 'old_status_id' - belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id' - - validates_presence_of :role, :old_status, :new_status - - # Returns workflow transitions count by tracker and role - def self.count_by_tracker_and_role - counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{Workflow.table_name} GROUP BY role_id, tracker_id") - roles = Role.find(:all, :order => 'builtin, position') - trackers = Tracker.find(:all, :order => 'position') - - result = [] - trackers.each do |tracker| - t = [] - roles.each do |role| - row = counts.detect {|c| c['role_id'].to_s == role.id.to_s && c['tracker_id'].to_s == tracker.id.to_s} - t << [role, (row.nil? ? 0 : row['c'].to_i)] - end - result << [tracker, t] - end - - result - end - - # Find potential statuses the user could be allowed to switch issues to - def self.available_statuses(project, user=User.current) - Workflow.find(:all, - :include => :new_status, - :conditions => {:role_id => user.roles_for_project(project).collect(&:id)}). - collect(&:new_status). - compact. - uniq. - sort - end - - # Copies workflows from source to targets - def self.copy(source_tracker, source_role, target_trackers, target_roles) - unless source_tracker.is_a?(Tracker) || source_role.is_a?(Role) - raise ArgumentError.new("source_tracker or source_role must be specified") - end - - target_trackers = [target_trackers].flatten.compact - target_roles = [target_roles].flatten.compact - - target_trackers = Tracker.all if target_trackers.empty? - target_roles = Role.all if target_roles.empty? - - target_trackers.each do |target_tracker| - target_roles.each do |target_role| - copy_one(source_tracker || target_tracker, - source_role || target_role, - target_tracker, - target_role) - end - end - end - - # Copies a single set of workflows from source to target - def self.copy_one(source_tracker, source_role, target_tracker, target_role) - unless source_tracker.is_a?(Tracker) && !source_tracker.new_record? && - source_role.is_a?(Role) && !source_role.new_record? && - target_tracker.is_a?(Tracker) && !target_tracker.new_record? && - target_role.is_a?(Role) && !target_role.new_record? - - raise ArgumentError.new("arguments can not be nil or unsaved objects") - end - - if source_tracker == target_tracker && source_role == target_role - false - else - transaction do - delete_all :tracker_id => target_tracker.id, :role_id => target_role.id - connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, role_id, old_status_id, new_status_id, author, assignee)" + - " SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id, author, assignee" + - " FROM #{Workflow.table_name}" + - " WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id}" - end - true - end - end -end diff -r ab89f95ef405 -r b2ea0641f798 app/models/workflow_permission.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/models/workflow_permission.rb Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,45 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class WorkflowPermission < WorkflowRule + validates_inclusion_of :rule, :in => %w(readonly required) + validate :validate_field_name + + # Replaces the workflow permissions for the given tracker and role + # + # Example: + # WorkflowPermission.replace_permissions role, tracker, {'due_date' => {'1' => 'readonly', '2' => 'required'}} + def self.replace_permissions(tracker, role, permissions) + destroy_all(:tracker_id => tracker.id, :role_id => role.id) + + permissions.each { |field, rule_by_status_id| + rule_by_status_id.each { |status_id, rule| + if rule.present? + WorkflowPermission.create(:role_id => role.id, :tracker_id => tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule) + end + } + } + end + + protected + + def validate_field_name + unless Tracker::CORE_FIELDS_ALL.include?(field_name) || field_name.to_s.match(/^\d+$/) + errors.add :field_name, :invalid + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 app/models/workflow_rule.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/models/workflow_rule.rb Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,73 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class WorkflowRule < ActiveRecord::Base + self.table_name = "#{table_name_prefix}workflows#{table_name_suffix}" + + belongs_to :role + belongs_to :tracker + belongs_to :old_status, :class_name => 'IssueStatus', :foreign_key => 'old_status_id' + belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id' + + validates_presence_of :role, :tracker, :old_status + + # Copies workflows from source to targets + def self.copy(source_tracker, source_role, target_trackers, target_roles) + unless source_tracker.is_a?(Tracker) || source_role.is_a?(Role) + raise ArgumentError.new("source_tracker or source_role must be specified") + end + + target_trackers = [target_trackers].flatten.compact + target_roles = [target_roles].flatten.compact + + target_trackers = Tracker.sorted.all if target_trackers.empty? + target_roles = Role.all if target_roles.empty? + + target_trackers.each do |target_tracker| + target_roles.each do |target_role| + copy_one(source_tracker || target_tracker, + source_role || target_role, + target_tracker, + target_role) + end + end + end + + # Copies a single set of workflows from source to target + def self.copy_one(source_tracker, source_role, target_tracker, target_role) + unless source_tracker.is_a?(Tracker) && !source_tracker.new_record? && + source_role.is_a?(Role) && !source_role.new_record? && + target_tracker.is_a?(Tracker) && !target_tracker.new_record? && + target_role.is_a?(Role) && !target_role.new_record? + + raise ArgumentError.new("arguments can not be nil or unsaved objects") + end + + if source_tracker == target_tracker && source_role == target_role + false + else + transaction do + delete_all :tracker_id => target_tracker.id, :role_id => target_role.id + connection.insert "INSERT INTO #{WorkflowRule.table_name} (tracker_id, role_id, old_status_id, new_status_id, author, assignee, field_name, rule, type)" + + " SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id, author, assignee, field_name, rule, type" + + " FROM #{WorkflowRule.table_name}" + + " WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id}" + end + true + end + end +end diff -r ab89f95ef405 -r b2ea0641f798 app/models/workflow_transition.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/models/workflow_transition.rb Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,39 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class WorkflowTransition < WorkflowRule + validates_presence_of :new_status + + # Returns workflow transitions count by tracker and role + def self.count_by_tracker_and_role + counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{table_name} WHERE type = 'WorkflowTransition' GROUP BY role_id, tracker_id") + roles = Role.sorted.all + trackers = Tracker.sorted.all + + result = [] + trackers.each do |tracker| + t = [] + roles.each do |role| + row = counts.detect {|c| c['role_id'].to_s == role.id.to_s && c['tracker_id'].to_s == tracker.id.to_s} + t << [role, (row.nil? ? 0 : row['c'].to_i)] + end + result << [tracker, t] + end + + result + end +end diff -r ab89f95ef405 -r b2ea0641f798 app/views/account/login.html.erb --- a/app/views/account/login.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/account/login.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -1,11 +1,11 @@ <%= call_hook :view_account_login_top %>
    -<% form_tag({:action=> "login"}) do %> +<%= form_tag(signin_path) do %> <%= back_url_hidden_field_tag %> - + @@ -28,7 +28,7 @@
    <%= text_field_tag 'username', nil, :tabindex => '1' %><%= text_field_tag 'username', params[:username], :tabindex => '1' %>
    <% if Setting.lost_password? %> - <%= link_to l(:label_password_lost), :controller => 'account', :action => 'lost_password' %> + <%= link_to l(:label_password_lost), lost_password_path %> <% end %> @@ -36,7 +36,12 @@
    -<%= javascript_tag "Form.Element.focus('username');" %> <% end %>
    <%= call_hook :view_account_login_bottom %> + +<% if params[:username].present? %> +<%= javascript_tag "$('#password').focus();" %> +<% else %> +<%= javascript_tag "$('#username').focus();" %> +<% end %> diff -r ab89f95ef405 -r b2ea0641f798 app/views/account/lost_password.html.erb --- a/app/views/account/lost_password.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/account/lost_password.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -1,11 +1,11 @@

    <%=l(:label_password_lost)%>

    -
    -<% form_tag({:action=> "lost_password"}, :class => "tabular") do %> - -

    -<%= text_field_tag 'mail', nil, :size => 40 %> -<%= submit_tag l(:button_submit) %>

    - +<%= form_tag(lost_password_path) do %> +
    +

    + + <%= text_field_tag 'mail', nil, :size => 40 %> + <%= submit_tag l(:button_submit) %> +

    +
    <% end %> -
    diff -r ab89f95ef405 -r b2ea0641f798 app/views/account/password_recovery.html.erb --- a/app/views/account/password_recovery.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/account/password_recovery.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -2,14 +2,19 @@ <%= error_messages_for 'user' %> -<% form_tag({:token => @token.value}) do %> -
    -

    -<%= password_field_tag 'new_password', nil, :size => 25 %>
    -<%= l(:text_caracters_minimum, :count => Setting.password_min_length) %>

    +<%= form_tag(lost_password_path) do %> + <%= hidden_field_tag 'token', @token.value %> +
    +

    + + <%= password_field_tag 'new_password', nil, :size => 25 %> + <%= l(:text_caracters_minimum, :count => Setting.password_min_length) %> +

    -

    -<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %>

    -
    -

    <%= submit_tag l(:button_save) %>

    +

    + + <%= password_field_tag 'new_password_confirmation', nil, :size => 25 %> +

    +
    +

    <%= submit_tag l(:button_save) %>

    <% end %> diff -r ab89f95ef405 -r b2ea0641f798 app/views/account/register.html.erb --- a/app/views/account/register.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/account/register.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -3,65 +3,56 @@

    <%=l(:label_register)%> <%=link_to l(:label_login_with_open_id_option), signin_url if Setting.openid? %>

    -<% form_tag({:action => 'register'}, :class => "tabular") do %> +<%= l(:text_who_can_register).html_safe %> + +<%= labelled_form_for @user, :url => register_path do |f| %> <%= error_messages_for 'user' %> -
    - +
    <% if @user.auth_source_id.nil? %> -

    -<%= text_field 'user', 'login', :size => 25 %>

    +

    <%= f.text_field :login, :size => 25, :required => true %>

    -

    -<%= password_field_tag 'password', nil, :size => 25 %>
    -<%= l(:text_caracters_minimum, :count => Setting.password_min_length) %>

    +

    <%= f.password_field :password, :size => 25, :required => true %> + <%= l(:text_caracters_minimum, :count => Setting.password_min_length) %>

    -

    -<%= password_field_tag 'password_confirmation', nil, :size => 25 %>

    +

    <%= f.password_field :password_confirmation, :size => 25, :required => true %>

    <% end %> -

    -<%= text_field 'user', 'firstname' %>

    - -

    -<%= text_field 'user', 'lastname' %>

    - -

    -<%= text_field 'user', 'mail' %>

    +

    <%= f.text_field :firstname, :required => true %>

    +

    <%= f.text_field :lastname, :required => true %>

    +

    <%= f.text_field :mail, :required => true %>

    + +<%= labelled_fields_for :ssamr_user_details, @ssamr_user_details do |fields| %> +

    <%=l(:label_ssamr_details)%>

    + +

    + <%= fields.text_area :description, :rows => 3, :cols => 40, :required => true, :class => 'wiki-edit' %> + <%=l(:text_user_ssamr_description_info).html_safe%> +

    -

    <%=l(:label_ssamr_details)%>

    - - <% fields_for :ssamr_user_details, :builder => TabularFormBuilder, :lang => current_language do |ssamr_user_detail| %> -

    - <%= ssamr_user_detail.text_area :description, :rows => 3, :cols => 40, :required => true, :class => 'wiki-edit' %> - <%=l(:text_user_ssamr_description_info)%> -

    +

    + <%= fields.radio_button :institution_type, true %> + <%= fields.collection_select(:institution_id, Institution.find(:all, :order => "institutions.order"), :id, :name, {:selected => @selected_institution_id, :prompt => true}).gsub('&', '&').html_safe %> +

    -

    - <%= ssamr_user_detail.radio_button :institution_type, true %> - <%= ssamr_user_detail.collection_select(:institution_id, Institution.find(:all, :order => "institutions.order"), :id, :name, {:selected => @selected_institution_id, :prompt => true}).gsub('&', '&') %> -

    +

    + <%= fields.radio_button :institution_type, false %> Other: + <%= fields.text_field(:other_institution) %> +

    +<% end %> + -

    - <%= ssamr_user_detail.radio_button :institution_type, false %> Other: - <%= ssamr_user_detail.text_field(:other_institution) %> -

    - <% end %> - <% if Setting.openid? %> -

    -<%= text_field 'user', 'identity_url' %>

    +

    <%= f.text_field :identity_url %>

    <% end %> <% @user.custom_field_values.select {|v| v.editable? || v.required?}.each do |value| %>

    <%= custom_field_tag_with_label :user, value %>

    <% end %> -
    <%= check_box :user, :terms_and_conditions %> <%= l(:accept_terms_and_conditions) %> <%= link_to("Terms and Conditions", "https://code.soundsoftware.ac.uk/projects/soundsoftware-site/wiki/TandCs", {:target => "_blank"}) %>. @@ -70,3 +61,4 @@ <%= submit_tag l(:button_submit) %> <% end %> + diff -r ab89f95ef405 -r b2ea0641f798 app/views/activities/index.html.erb --- a/app/views/activities/index.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/activities/index.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -6,7 +6,7 @@ l(:label_institution_activity, h(@institution_name)) end else - l(:label_user_activity, link_to_user(@author)) + l(:label_user_activity, link_to_user(@author)).html_safe end %>

    <%= l(:label_date_from_to, :start => format_date(@date_to - @days), :end => format_date(@date_to-1)) %>

    @@ -50,7 +50,7 @@ <% end %> <% content_for :sidebar do %> -<% form_tag({}, :method => :get) do %> +<%= form_tag({}, :method => :get) do %>

    <%= l(:label_activity) %>

    <% @activity.event_types.each do |t| %> <%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %> diff -r ab89f95ef405 -r b2ea0641f798 app/views/admin/_menu.html.erb --- a/app/views/admin/_menu.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/admin/_menu.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,3 @@

    -
      - <%= render_menu :admin_menu %> -
    + <%= render_menu :admin_menu %>
    diff -r ab89f95ef405 -r b2ea0641f798 app/views/admin/_no_data.html.erb --- a/app/views/admin/_no_data.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/admin/_no_data.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -1,5 +1,5 @@
    -<% form_tag({:action => 'default_configuration'}) do %> +<%= form_tag({:action => 'default_configuration'}) do %> <%= simple_format(l(:text_no_configuration_data)) %>

    <%= l(:field_language) %>: <%= select_tag 'lang', options_for_select(lang_options_for_select(false), current_language.to_s) %> diff -r ab89f95ef405 -r b2ea0641f798 app/views/admin/info.html.erb --- a/app/views/admin/info.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/admin/info.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -1,6 +1,6 @@

    <%=l(:label_information_plural)%>

    -

    <%= Redmine::Info.versioned_name %> (<%= @db_adapter_name %>)

    +

    <%= Redmine::Info.versioned_name %>

    <% @checklist.each do |label, result| %> @@ -11,5 +11,9 @@ <% end %>
    +
    +
    +
    <%= Redmine::Info.environment %>
    +
    <% html_title(l(:label_information_plural)) -%> diff -r ab89f95ef405 -r b2ea0641f798 app/views/admin/projects.html.erb --- a/app/views/admin/projects.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/admin/projects.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -4,7 +4,7 @@

    <%=l(:label_project_plural)%>

    -<% form_tag({}, :method => :get) do %> +<%= form_tag({}, :method => :get) do %>
    <%= l(:label_filter_plural) %> <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> @@ -27,14 +27,14 @@ <% project_tree(@projects) do |project, level| %> <%= project.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>"> - <%= link_to_project(project, {:action => 'settings'}, :title => project.short_description) %> + <%= link_to_project(project, {:action => (project.active? ? 'settings' : 'show')}, :title => project.short_description) %> <%= checked_image project.is_public? %> <%= format_date(project.created_on) %> - <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project, :status => params[:status] }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %> - <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project, :status => params[:status] }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %> + <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project, :status => params[:status] }, :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-lock') unless project.archived? %> + <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project, :status => params[:status] }, :method => :post, :class => 'icon icon-unlock') if project.archived? && (project.parent.nil? || !project.parent.archived?) %> <%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %> - <%= link_to(l(:button_delete), project_destroy_confirm_path(project), :class => 'icon icon-del') %> + <%= link_to(l(:button_delete), project_path(project), :method => :delete, :class => 'icon icon-del') %> <% end %> diff -r ab89f95ef405 -r b2ea0641f798 app/views/attachments/_form.html.erb --- a/app/views/attachments/_form.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/attachments/_form.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -1,11 +1,19 @@ +<% if defined?(container) && container && container.saved_attachments %> + <% container.saved_attachments.each_with_index do |attachment, i| %> + + <%= h(attachment.filename) %> (<%= number_to_human_size(attachment.filesize) %>) + <%= hidden_field_tag "attachments[p#{i}][token]", "#{attachment.id}.#{attachment.digest}" %> + + <% end %> +<% end %> - <%= file_field_tag 'attachments[1][file]', :size => 30, :id => nil, :class => 'file', + <%= file_field_tag 'attachments[1][file]', :id => nil, :class => 'file', :onchange => "checkFileSize(this, #{Setting.attachment_max_size.to_i.kilobytes}, '#{escape_javascript(l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)))}');" -%> - + <%= text_field_tag 'attachments[1][description]', '', :id => nil, :class => 'description', :maxlength => 255, :placeholder => l(:label_optional_description) %> <%= link_to_function(image_tag('delete.png'), 'removeFileField(this)', :title => (l(:button_delete))) %> -
    <%= link_to l(:label_add_another_file), '#', :onclick => 'addFileField(); return false;' %> -(<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>) - + +<%= link_to l(:label_add_another_file), '#', :onclick => 'addFileField(); return false;', :class => 'add_attachment' %> +(<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>) diff -r ab89f95ef405 -r b2ea0641f798 app/views/attachments/_links.html.erb --- a/app/views/attachments/_links.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/attachments/_links.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -1,11 +1,16 @@
    <% for attachment in attachments %> -

    <%= link_to_attachment attachment, :class => 'icon icon-attachment' -%> -<%= h(" - #{attachment.description}") unless attachment.description.blank? %> +

    <%= link_to_attachment attachment, :class => 'icon icon-attachment', :download => true -%> + <% if attachment.is_text? %> + <%= link_to image_tag('magnifier.png'), + :controller => 'attachments', :action => 'show', + :id => attachment, :filename => attachment.filename %> + <% end %> + <%= h(" - #{attachment.description}") unless attachment.description.blank? %> <%= number_to_human_size attachment.filesize %><%= ", " + l(:label_x_downloads, :count => attachment.downloads) unless attachment.downloads == 0 %> <% if options[:deletable] %> <%= link_to image_tag('delete.png'), attachment_path(attachment), - :confirm => l(:text_are_you_sure), + :data => {:confirm => l(:text_are_you_sure)}, :method => :delete, :class => 'delete', :title => l(:button_delete) %> @@ -15,4 +20,14 @@ <% end %>

    <% end %> +<% if defined?(thumbnails) && thumbnails %> + <% images = attachments.select(&:thumbnailable?) %> + <% if images.any? %> +
    + <% images.each do |attachment| %> +
    <%= thumbnail_tag(attachment) %>
    + <% end %> +
    + <% end %> +<% end %>
    diff -r ab89f95ef405 -r b2ea0641f798 app/views/attachments/diff.html.erb --- a/app/views/attachments/diff.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/attachments/diff.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -6,16 +6,14 @@

    <%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%> (<%= number_to_human_size @attachment.filesize %>)

    -

    -<% form_tag({}, :method => 'get') do %> - - <%= select_tag 'type', - options_for_select( - [[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type), - :onchange => "if (this.value != '') {this.form.submit()}" %> +<%= form_tag({}, :method => 'get') do %> +

    + <%= l(:label_view_diff) %>: + + +

    <% end %> -

    -<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %> +<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type, :diff_style => nil} %> <% html_title @attachment.filename %> diff -r ab89f95ef405 -r b2ea0641f798 app/views/attachments/toggle_active.html.erb --- a/app/views/attachments/toggle_active.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -<%= -file = Attachment.find(params[:id]) -active_id = "active-" + file.id.to_s -link_to_remote image_tag(file.active? ? 'fav.png' : 'fav_off.png'), - :url => {:controller => 'attachments', :action => 'toggle_active', :project_id => @project.id, :id => file}, - :update => active_id -%> diff -r ab89f95ef405 -r b2ea0641f798 app/views/attachments/toggle_active.js.erb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/views/attachments/toggle_active.js.erb Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,1 @@ +$('#active-<%= @attachment.id %>').html('<%= link_to image_tag(@attachment.active? ? 'fav.png' : 'fav_off.png'), {:controller => 'attachments', :action => 'toggle_active', :project_id => @project.id, :id => @attachment}, :remote => true %>'); diff -r ab89f95ef405 -r b2ea0641f798 app/views/attachments/upload.api.rsb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/views/attachments/upload.api.rsb Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,3 @@ +api.upload do + api.token @attachment.token +end diff -r ab89f95ef405 -r b2ea0641f798 app/views/auth_sources/_form_auth_source_ldap.html.erb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/views/auth_sources/_form_auth_source_ldap.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -0,0 +1,50 @@ +<%= error_messages_for 'auth_source' %> + +
    + +

    +<%= text_field 'auth_source', 'name' %>

    + +

    +<%= text_field 'auth_source', 'host' %>

    + +

    +<%= text_field 'auth_source', 'port', :size => 6 %> <%= check_box 'auth_source', 'tls' %> LDAPS

    + +

    +<%= text_field 'auth_source', 'account' %>

    + +

    +<%= password_field 'auth_source', 'account_password', :name => 'ignore', + :value => ((@auth_source.new_record? || @auth_source.account_password.blank?) ? '' : ('x'*15)), + :onfocus => "this.value=''; this.name='auth_source[account_password]';", + :onchange => "this.name='auth_source[account_password]';" %>

    + +

    +<%= text_field 'auth_source', 'base_dn', :size => 60 %>

    + +

    +<%= text_field 'auth_source', 'filter', :size => 60 %>

    + +

    +<%= text_field 'auth_source', 'timeout', :size => 4 %>

    + +

    +<%= check_box 'auth_source', 'onthefly_register' %>

    +
    + +
    <%=l(:label_attribute_plural)%> +

    +<%= text_field 'auth_source', 'attr_login', :size => 20 %>

    + +

    +<%= text_field 'auth_source', 'attr_firstname', :size => 20 %>

    + +

    +<%= text_field 'auth_source', 'attr_lastname', :size => 20 %>

    + +

    +<%= text_field 'auth_source', 'attr_mail', :size => 20 %>

    +
    + + diff -r ab89f95ef405 -r b2ea0641f798 app/views/auth_sources/edit.html.erb --- a/app/views/auth_sources/edit.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/auth_sources/edit.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -1,7 +1,6 @@

    <%=l(:label_auth_source)%> (<%= h(@auth_source.auth_method_name) %>)

    -<% form_tag({:action => 'update', :id => @auth_source}, :class => "tabular") do %> - <%= render :partial => 'form' %> +<%= form_tag({:action => 'update', :id => @auth_source}, :method => :put, :class => "tabular") do %> + <%= render :partial => auth_source_partial_name(@auth_source) %> <%= submit_tag l(:button_save) %> <% end %> - diff -r ab89f95ef405 -r b2ea0641f798 app/views/auth_sources/index.html.erb --- a/app/views/auth_sources/index.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/auth_sources/index.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -20,12 +20,8 @@ <%= h source.host %> <%= h source.users.count %> - <%= link_to l(:button_test), :action => 'test_connection', :id => source %> - <%= link_to l(:button_delete), { :action => 'destroy', :id => source }, - :method => :post, - :confirm => l(:text_are_you_sure), - :class => 'icon icon-del', - :disabled => source.users.any? %> + <%= link_to l(:button_test), {:action => 'test_connection', :id => source}, :class => 'icon icon-test' %> + <%= delete_link auth_source_path(source) %> <% end %> diff -r ab89f95ef405 -r b2ea0641f798 app/views/auth_sources/new.html.erb --- a/app/views/auth_sources/new.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/auth_sources/new.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -1,6 +1,7 @@

    <%=l(:label_auth_source_new)%> (<%= h(@auth_source.auth_method_name) %>)

    -<% form_tag({:action => 'create'}, :class => "tabular") do %> - <%= render :partial => 'form' %> +<%= form_tag({:action => 'create'}, :class => "tabular") do %> + <%= hidden_field_tag 'type', @auth_source.type %> + <%= render :partial => auth_source_partial_name(@auth_source) %> <%= submit_tag l(:button_create) %> <% end %> diff -r ab89f95ef405 -r b2ea0641f798 app/views/auto_completes/issues.html.erb --- a/app/views/auto_completes/issues.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/auto_completes/issues.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -1,9 +1,7 @@ -
      -<% if @issues.any? -%> - <% @issues.each do |issue| -%> - <%= content_tag 'li', h("#{issue.tracker} ##{issue.id}: #{issue.subject}"), :id => issue.id %> - <% end -%> -<% else -%> - <%= content_tag("li", l(:label_none), :style => 'display:none') %> -<% end -%> -
    +<%= raw @issues.map {|issue| { + 'id' => issue.id, + 'label' => "#{issue.tracker} ##{issue.id}: #{truncate issue.subject.to_s, :length => 60}", + 'value' => issue.id + } + }.to_json +%> diff -r ab89f95ef405 -r b2ea0641f798 app/views/boards/_form.html.erb --- a/app/views/boards/_form.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/boards/_form.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -1,8 +1,9 @@ -<%= error_messages_for 'board' %> +<%= error_messages_for @board %> - -
    +

    <%= f.text_field :name, :required => true %>

    <%= f.text_field :description, :required => true, :size => 80 %>

    +<% if @board.valid_parents.any? %> +

    <%= f.select :parent_id, boards_options_for_select(@board.valid_parents), :include_blank => true, :label => :field_board_parent %>

    +<% end %>
    - diff -r ab89f95ef405 -r b2ea0641f798 app/views/boards/edit.html.erb --- a/app/views/boards/edit.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/boards/edit.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -1,6 +1,6 @@

    <%= l(:label_board) %>

    -<% labelled_tabular_form_for :board, @board, :url => {:action => 'edit', :id => @board} do |f| %> +<%= labelled_form_for @board, :url => project_board_path(@project, @board) do |f| %> <%= render :partial => 'form', :locals => {:f => f} %> <%= submit_tag l(:button_save) %> <% end %> diff -r ab89f95ef405 -r b2ea0641f798 app/views/boards/index.html.erb --- a/app/views/boards/index.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/boards/index.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -8,21 +8,19 @@ <%= l(:label_message_last) %> -<% for board in @boards %> +<% Board.board_tree(@boards) do |board, level| %> - + <%= link_to h(board.name), {:action => 'show', :id => board}, :class => "board" %>
    <%=h board.description %> - <%= board.topics_count %> - <%= board.messages_count %> - - + <%= board.topics_count %> + <%= board.messages_count %> + <% if board.last_message %> <%= authoring board.last_message.created_on, board.last_message.author %>
    <%= link_to_message board.last_message %> <% end %> -
    <% end %> diff -r ab89f95ef405 -r b2ea0641f798 app/views/boards/new.html.erb --- a/app/views/boards/new.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/boards/new.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -1,6 +1,6 @@

    <%= l(:label_board_new) %>

    -<% labelled_tabular_form_for :board, @board, :url => {:action => 'new'} do |f| %> +<%= labelled_form_for @board, :url => project_boards_path(@project) do |f| %> <%= render :partial => 'form', :locals => {:f => f} %> <%= submit_tag l(:button_create) %> <% end %> diff -r ab89f95ef405 -r b2ea0641f798 app/views/boards/show.html.erb --- a/app/views/boards/show.html.erb Thu Jun 20 08:46:39 2013 +0100 +++ b/app/views/boards/show.html.erb Thu Jun 20 08:47:50 2013 +0100 @@ -1,27 +1,21 @@ -<%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}) %> +<%= board_breadcrumb(@board) %>
    <%= link_to_if_authorized l(:label_message_new), {:controller => 'messages', :action => 'new', :board_id => @board}, :class => 'icon icon-add', - :onclick => 'Element.show("add-message"); Form.Element.focus("message_subject"); return false;' %> + :onclick => 'showAndScrollTo("add-message", "message_subject"); return false;' %> <%= watcher_tag(@board, User.current) %>