# HG changeset patch # User Chris Cannam # Date 1357560102 0 # Node ID 433d4f72a19b132153feeb0179633427103d65d8 # Parent 5f33065ddc4b0ddb79a29e39acf8fce519b3a1b2 Update to Redmine SVN revision 11137 on 2.2-stable branch diff -r 5f33065ddc4b -r 433d4f72a19b .gitignore --- a/.gitignore Wed Jun 27 14:54:18 2012 +0100 +++ b/.gitignore Mon Jan 07 12:01:42 2013 +0000 @@ -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 5f33065ddc4b -r 433d4f72a19b .hgignore --- a/.hgignore Wed Jun 27 14:54:18 2012 +0100 +++ b/.hgignore Mon Jan 07 12:01:42 2013 +0000 @@ -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,9 +21,12 @@ public/plugin_assets tmp/* tmp/cache/* +tmp/pdf/* tmp/sessions/* tmp/sockets/* tmp/test/* +tmp/thumbnails/* +vendor/cache vendor/rails *.rbc diff -r 5f33065ddc4b -r 433d4f72a19b .svn/pristine/01/016b47aef50027cc7a73f1b3fdde3506ed5b4072.svn-base --- a/.svn/pristine/01/016b47aef50027cc7a73f1b3fdde3506ed5b4072.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/03/03803ec02697cfe8f89b59eb47e58dc9312a0502.svn-base --- a/.svn/pristine/03/03803ec02697cfe8f89b59eb47e58dc9312a0502.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/04/04389a310fa0201bca85505cd492c1ab5010af2e.svn-base --- a/.svn/pristine/04/04389a310fa0201bca85505cd492c1ab5010af2e.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/04/04fbda284b00bec052d898fc58aac289855e9000.svn-base --- a/.svn/pristine/04/04fbda284b00bec052d898fc58aac289855e9000.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/05/053c71b830593b5a759e13f9676269b7c9bf4645.svn-base --- a/.svn/pristine/05/053c71b830593b5a759e13f9676269b7c9bf4645.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/05/053ccbadc6aeeda1db20f88d64520439b568415d.svn-base --- a/.svn/pristine/05/053ccbadc6aeeda1db20f88d64520439b568415d.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/05/058be0b3a53e81bfbd2f18f23f7014355fe7cd0a.svn-base --- a/.svn/pristine/05/058be0b3a53e81bfbd2f18f23f7014355fe7cd0a.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/05/05f81f90a3570490e6ea8a94e643c12e2ec9cbe3.svn-base --- a/.svn/pristine/05/05f81f90a3570490e6ea8a94e643c12e2ec9cbe3.svn-base Wed Jun 27 14:54:18 2012 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,879 +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 Project < ActiveRecord::Base - include Redmine::SafeAttributes - - # Project statuses - STATUS_ACTIVE = 1 - STATUS_ARCHIVED = 9 - - # Maximum length for project identifiers - IDENTIFIER_MAX_LENGTH = 100 - - # 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 :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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/0d/0d2311ec4452abd2d98cbb99aaf512d8571b9e28.svn-base --- a/.svn/pristine/0d/0d2311ec4452abd2d98cbb99aaf512d8571b9e28.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/0f/0f12084a6a3192cb0d61cbd2f3ef681df419cac4.svn-base --- a/.svn/pristine/0f/0f12084a6a3192cb0d61cbd2f3ef681df419cac4.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/0f/0f71516e3a6532ada8045e94e42972a11451afc4.svn-base --- a/.svn/pristine/0f/0f71516e3a6532ada8045e94e42972a11451afc4.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/14/14d47feeb8d09d5c60a469b92e31b17db8244cde.svn-base --- a/.svn/pristine/14/14d47feeb8d09d5c60a469b92e31b17db8244cde.svn-base Wed Jun 27 14:54:18 2012 +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 WikiHelper - - def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0) - pages = pages.group_by(&:parent) unless pages.is_a?(Hash) - s = '' - 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 - - s << "\n" + - wiki_page_options_for_select(pages, selected, page, level + 1) - end - end - s - end - - 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}) - }) - end -end diff -r 5f33065ddc4b -r 433d4f72a19b .svn/pristine/18/182b1b357cf2d5daa1faf5abe56eab7ea9d5bbd5.svn-base --- a/.svn/pristine/18/182b1b357cf2d5daa1faf5abe56eab7ea9d5bbd5.svn-base Wed Jun 27 14:54:18 2012 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,155 +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 '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 - Setting.notified_events = ['message_posted'] - - post :new, :board_id => 1, - :message => { :subject => 'Test created message', - :content => 'Message body'} - 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_kind_of TMail::Mail, mail - assert_equal "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] Test created message", mail.subject - assert mail.body.include?('Message body') - # 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_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 - post :destroy, :board_id => 1, :id => 1 - assert_redirected_to '/projects/ecookbook/boards/1' - assert_nil Message.find_by_id(1) - end - - def test_quote - @request.session[:user_id] = 2 - xhr :get, :quote, :board_id => 1, :id => 3 - assert_response :success - assert_select_rjs :show, 'reply' - end -end diff -r 5f33065ddc4b -r 433d4f72a19b .svn/pristine/1a/1abc7d9bb63cfe6a27d8a0852d2c50e8db93327c.svn-base --- a/.svn/pristine/1a/1abc7d9bb63cfe6a27d8a0852d2c50e8db93327c.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/20/208e5f65518788d2e8e359431914f6d85fea6a8a.svn-base --- a/.svn/pristine/20/208e5f65518788d2e8e359431914f6d85fea6a8a.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/22/2239f10ea5e4bc85a8ddbe53928f7d28bb9ad174.svn-base --- a/.svn/pristine/22/2239f10ea5e4bc85a8ddbe53928f7d28bb9ad174.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/24/243de4e756633dfdaa9bf4c362c62553c376fead.svn-base --- a/.svn/pristine/24/243de4e756633dfdaa9bf4c362c62553c376fead.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/25/25a248b972bed2adf3bfae26d2e777b6d1e986f8.svn-base --- a/.svn/pristine/25/25a248b972bed2adf3bfae26d2e777b6d1e986f8.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/2b/2b0ee259d41451978f73ac2e9b9372c5b4686f27.svn-base --- a/.svn/pristine/2b/2b0ee259d41451978f73ac2e9b9372c5b4686f27.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/2c/2c03b2451a9c38796d0008f31034571e1a5677c0.svn-base --- a/.svn/pristine/2c/2c03b2451a9c38796d0008f31034571e1a5677c0.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/30/30e01132e9ab5b3c9ae62f165db3ff3e34e482a8.svn-base --- a/.svn/pristine/30/30e01132e9ab5b3c9ae62f165db3ff3e34e482a8.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/32/32621c66778dffd99e5e7238a5aa46a6518b1b3c.svn-base --- a/.svn/pristine/32/32621c66778dffd99e5e7238a5aa46a6518b1b3c.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/33/332d3d52b592a1156755d111d7a6b0f8eac58495.svn-base --- a/.svn/pristine/33/332d3d52b592a1156755d111d7a6b0f8eac58495.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/34/34bbd22558c309af2608a5aaf343302b2d47bfb5.svn-base --- a/.svn/pristine/34/34bbd22558c309af2608a5aaf343302b2d47bfb5.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/34/34e8f3b289ef523b2e5c39a872d6efc48111b2f3.svn-base --- a/.svn/pristine/34/34e8f3b289ef523b2e5c39a872d6efc48111b2f3.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/36/36a71efadc331c3001a59098a85ebc847b7a529a.svn-base --- a/.svn/pristine/36/36a71efadc331c3001a59098a85ebc847b7a529a.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/36/36da07b1ee2e0ce6db594d8c9206610d637ebf42.svn-base --- a/.svn/pristine/36/36da07b1ee2e0ce6db594d8c9206610d637ebf42.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/36/36ee24f61bb993329a397c77fd4b05efc6e92acd.svn-base --- a/.svn/pristine/36/36ee24f61bb993329a397c77fd4b05efc6e92acd.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/3a/3a513868a533431476e8232f1b95f5f62246b3de.svn-base --- a/.svn/pristine/3a/3a513868a533431476e8232f1b95f5f62246b3de.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/3d/3d0a99ce133ffd8be7f79466600a3a56ffab0213.svn-base --- a/.svn/pristine/3d/3d0a99ce133ffd8be7f79466600a3a56ffab0213.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/3f/3fb6e89a6e5f14a7dfc6f31c98abfe75827ecbea.svn-base --- a/.svn/pristine/3f/3fb6e89a6e5f14a7dfc6f31c98abfe75827ecbea.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/3f/3fc095ae15466e551cacbea7f0776a64d74a2acd.svn-base --- a/.svn/pristine/3f/3fc095ae15466e551cacbea7f0776a64d74a2acd.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/3f/3fe4e91401e89e608109800b1ea5a303da548232.svn-base --- a/.svn/pristine/3f/3fe4e91401e89e608109800b1ea5a303da548232.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/41/41dbe19704933be68922a961147886290a8f9c62.svn-base --- a/.svn/pristine/41/41dbe19704933be68922a961147886290a8f9c62.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/45/451613f5e947795f045fad0d3c035a4d108e9d4d.svn-base --- a/.svn/pristine/45/451613f5e947795f045fad0d3c035a4d108e9d4d.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/45/455894853af15224c178defd4436e0297eee3c97.svn-base --- a/.svn/pristine/45/455894853af15224c178defd4436e0297eee3c97.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/4d/4d303c92e6935bfe0bbc913277addc1ba08aef2e.svn-base --- a/.svn/pristine/4d/4d303c92e6935bfe0bbc913277addc1ba08aef2e.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/4e/4e65aa6af49f7dc6aa8157532f6bed6bfe2ae6d6.svn-base --- a/.svn/pristine/4e/4e65aa6af49f7dc6aa8157532f6bed6bfe2ae6d6.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/52/526a207c3d794968f2a7c5b89f01aadb8abb89d2.svn-base --- a/.svn/pristine/52/526a207c3d794968f2a7c5b89f01aadb8abb89d2.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/53/537c76b0b6db46cea1550518e810dd1d4d5b897a.svn-base --- a/.svn/pristine/53/537c76b0b6db46cea1550518e810dd1d4d5b897a.svn-base Wed Jun 27 14:54:18 2012 +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 5f33065ddc4b -r 433d4f72a19b .svn/pristine/53/53eb3f2e1882327db7415ab09e0ccb8d4b2a4d5f.svn-base --- a/.svn/pristine/53/53eb3f2e1882327db7415ab09e0ccb8d4b2a4d5f.svn-base Wed Jun 27 14:54:18 2012 +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 "