diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a43038a..89a799a 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -177,6 +177,10 @@ class ProjectsController < ApplicationController
@version_status = params[:version_status] || 'open'
@version_name = params[:version_name]
@versions = @project.shared_versions.status(@version_status).like(@version_name).sorted
+ @cfs=AttributeGroup.joins(:custom_fields).joins(:tracker).
+ where(project_id: @project, tracker_id: @trackers, :custom_fields => {id: @project.all_issue_custom_fields.pluck(:id)}).
+ pluck("trackers.id", "id", "name", "position","attribute_group_fields.id", "attribute_group_fields.position",
+ "custom_fields.id", "custom_fields.name", "custom_fields.position").sort_by{|x| [x[3], x[5]]}
end
def edit
@@ -203,6 +207,39 @@ class ProjectsController < ApplicationController
end
end
+ def groupissuescustomfields
+ # clean invalid values: invalid cfs, empty cf lists, empty groups
+ group_issues_custom_fields = (JSON.parse params[:group_issues_custom_fields]).
+ each{|tid,v| v.replace(v.select{|k,v| v["cfs"] ? v["cfs"].delete_if{|k,v| @project.all_issue_custom_fields.pluck(:id).include?(v)} : v})}.
+ each{|tid,v| v.delete_if{|k,v| v["cfs"].blank?}}.
+ delete_if{|k,v| v.blank?}
+
+ groups = AttributeGroup.where(project_id: @project.id).collect(&:id)
+ fields = AttributeGroupField.where(attribute_group_id: groups).collect(&:id)
+ group_issues_custom_fields.each do |tid,v|
+ v.each do |gp, g|
+ gid = groups.shift
+ if gid.nil?
+ gid=AttributeGroup.create(project_id: @project.id, tracker_id: tid, name: g["name"].nil? ? nil : g["name"], position: gp).id
+ else
+ AttributeGroup.update(gid, project_id: @project.id, tracker_id: tid, name: g["name"].nil? ? nil : g["name"], position: gp)
+ end
+ g['cfs'].each do |cfp, cf|
+ cfid = fields.shift
+ if cfid.nil?
+ AttributeGroupField.create(attribute_group_id: gid, custom_field_id: cf, position: cfp)
+ else
+ AttributeGroupField.update(cfid, attribute_group_id: gid, custom_field_id: cf, position: cfp)
+ end
+ end
+ end
+ end
+ AttributeGroupField.where(id: fields).delete_all
+ AttributeGroup.where(id: groups).destroy_all
+ flash[:notice] = l(:notice_successful_update)
+ redirect_to settings_project_path(@project, :tab => 'groupissuescustomfields')
+ end
+
def archive
unless @project.archive
flash[:error] = l(:error_can_not_archive_project)
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index aee8444..25529a0 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -227,41 +227,53 @@ module IssuesHelper
r.to_html
end
- def render_half_width_custom_fields_rows(issue)
- values = issue.visible_custom_field_values.reject {|value| value.custom_field.full_width_layout?}
- return if values.empty?
- half = (values.size / 2.0).ceil
- issue_fields_rows do |rows|
- values.each_with_index do |value, i|
- css = "cf_#{value.custom_field.id}"
- attr_value = show_value(value)
- if value.custom_field.text_formatting == 'full'
- attr_value = content_tag('div', attr_value, class: 'wiki')
- end
- m = (i < half ? :left : :right)
- rows.send m, custom_field_name_tag(value.custom_field), attr_value, :class => css
- end
- end
+ def group_by_keys(project_id, tracker_id, custom_field_values)
+ keys_grouped = AttributeGroupField.joins(:attribute_group).
+ where(:attribute_groups => {project_id: project_id, tracker_id: tracker_id}).
+ order("attribute_groups.position", :position).pluck(:name, :custom_field_id).group_by(&:shift)
+ custom_fields_grouped = { nil => (keys_grouped[nil].nil? ? [] : keys_grouped[nil].map{|n| custom_field_values.select{|x| x.custom_field[:id] == n[0]}}.flatten) |
+ custom_field_values.select{|y| ! keys_grouped.values.flatten.include?(y.custom_field[:id])}}
+ keys_grouped.reject{|k,v| k == nil}.each{|k,v| custom_fields_grouped[k] = v.map{|n| custom_field_values.select{|x| x.custom_field[:id] == n[0]}}.flatten}
+ custom_fields_grouped
end
- def render_full_width_custom_fields_rows(issue)
- values = issue.visible_custom_field_values.select {|value| value.custom_field.full_width_layout?}
- return if values.empty?
-
+ def render_custom_fields_rows(issue)
s = ''.html_safe
- values.each_with_index do |value, i|
- attr_value = show_value(value)
- next if attr_value.blank?
-
- if value.custom_field.text_formatting == 'full'
- attr_value = content_tag('div', attr_value, class: 'wiki')
+ group_by_keys(issue.project_id, issue.tracker_id, issue.visible_custom_field_values).each do |title, values|
+ if values.present?
+ s += content_tag('h4', title, :style => 'background: #0001; padding: 0.3em;') unless title.nil?
+ while values.present?
+ if values[0].custom_field.full_width_layout?
+ while values.present? && values[0].custom_field.full_width_layout?
+ value=values.shift
+ attr_value = show_value(value)
+ if value.custom_field.text_formatting == 'full'
+ attr_value = content_tag('div', attr_value, class: 'wiki')
+ end
+ content = content_tag('div', custom_field_name_tag(value.custom_field) + ":", :class => 'label') +
+ content_tag('div', attr_value, :class => 'value')
+ content = content_tag('div', content, :class => "cf_#{value.custom_field.id} attribute")
+ s += content_tag('div', content, :class => 'splitcontent')
+ end
+ else
+ lr_values = []
+ while values.present? && ! values[0].custom_field.full_width_layout?
+ lr_values += [ values.shift ]
+ end
+ half = (lr_values.size / 2.0).ceil
+ s += issue_fields_rows do |rows|
+ lr_values.each_with_index do |value, i|
+ attr_value = show_value(value)
+ if value.custom_field.text_formatting == 'full'
+ attr_value = content_tag('div', attr_value, class: 'wiki')
+ end
+ m = (i < half ? :left : :right)
+ rows.send m, custom_field_name_tag(value.custom_field), attr_value, :class => "cf_#{value.custom_field.id}"
+ end
+ end
+ end
+ end
end
-
- content =
- content_tag('hr') +
- content_tag('p', content_tag('strong', custom_field_name_tag(value.custom_field) )) +
- content_tag('div', attr_value, class: 'value')
- s << content_tag('div', content, class: "cf_#{value.custom_field.id} attribute")
end
s
end
@@ -320,11 +332,15 @@ module IssuesHelper
end
end
end
- issue.visible_custom_field_values(user).each do |value|
- if html
- items << content_tag('strong', "#{value.custom_field.name}: ") + show_value(value, false)
- else
- items << "#{value.custom_field.name}: #{show_value(value, false)}"
+ group_by_keys(issue.project_id, issue.tracker_id, issue.visible_custom_field_values(user)).each do |title, values|
+ if values.present?
+ item = [ (html ? content_tag('strong', "#{title}") : "#{title}") ] unless title.nil?
+ values.each do |value|
+ (title.nil? ? items : item) << (html ?
+ content_tag('strong', "#{value.custom_field.name}: ") + show_value(value, false) :
+ "#{value.custom_field.name}: #{show_value(value, false)}")
+ end
+ items << item unless title.nil?
end
end
items
@@ -333,9 +349,12 @@ module IssuesHelper
def render_email_issue_attributes(issue, user, html=false)
items = email_issue_attributes(issue, user, html)
if html
- content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe, :class => "details")
+ content_tag('ul', items.select{|s| s.is_a? String}.map{|s| content_tag('li', s)}.join("\n").html_safe, :class => "details") + "\n" +
+ items.select{|s| !s.is_a? String}.map{|item| content_tag('div', item.shift) + "\n" +
+ content_tag('ul', item.map{|s| content_tag('li', s)}.join("\n").html_safe, :class => "details")}.join("\n").html_safe
else
- items.map{|s| "* #{s}"}.join("\n")
+ items.select{|s| s.is_a? String}.map{|s| "* #{s}"}.join("\n") + "\n" +
+ items.select{|s| !s.is_a? String}.map{|item| "#{item.shift}\n" + item.map{|s| "* #{s}"}.join("\n")}.join("\n")
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 7945461..42124f1 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -27,7 +27,8 @@ module ProjectsHelper
{:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
{:name => 'repositories', :action => :manage_repository, :partial => 'projects/settings/repositories', :label => :label_repository_plural},
{:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
- {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :label_time_tracking}
+ {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :label_time_tracking},
+ {:name => 'groupissuescustomfields', :action => :edit_project, :partial => 'projects/settings/groupissuescustomfields', :label => :grouped_cf}
]
tabs.
select {|tab| User.current.allowed_to?(tab[:action], @project)}.
diff --git a/app/models/attribute_group.rb b/app/models/attribute_group.rb
new file mode 100644
index 0000000..52d7ac3
--- /dev/null
+++ b/app/models/attribute_group.rb
@@ -0,0 +1,9 @@
+class AttributeGroup < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :tracker
+ has_many :attribute_group_fields, :dependent => :delete_all
+ has_many :custom_fields, :through => :attribute_group_fields
+ acts_as_positioned
+
+ scope :sorted, lambda { order(:position) }
+end
diff --git a/app/models/attribute_group_field.rb b/app/models/attribute_group_field.rb
new file mode 100644
index 0000000..c040138
--- /dev/null
+++ b/app/models/attribute_group_field.rb
@@ -0,0 +1,7 @@
+class AttributeGroupField < ActiveRecord::Base
+ belongs_to :attribute_group
+ belongs_to :custom_field
+ acts_as_positioned
+
+ scope :sorted, lambda { order(:position) }
+end
diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb
index 008ef49..41293a4 100644
--- a/app/models/custom_field.rb
+++ b/app/models/custom_field.rb
@@ -24,6 +24,7 @@ class CustomField < ActiveRecord::Base
:class_name => 'CustomFieldEnumeration',
:dependent => :delete_all
has_many :custom_values, :dependent => :delete_all
+ has_many :attribute_group_fields, :dependent => :delete_all
has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
acts_as_positioned
serialize :possible_values
diff --git a/app/models/project.rb b/app/models/project.rb
index 6d91d57..2888a9e 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -51,6 +51,8 @@ class Project < ActiveRecord::Base
has_many :changesets, :through => :repository
has_one :wiki, :dependent => :destroy
# Custom field for the project issues
+ has_many :attribute_groups, :dependent => :destroy
+ has_many :attribute_group_fields, :through => :attribute_groups
has_and_belongs_to_many :issue_custom_fields,
lambda {order(:position)},
:class_name => 'IssueCustomField',
diff --git a/app/models/tracker.rb b/app/models/tracker.rb
index d90ab3e..a4e9d79 100644
--- a/app/models/tracker.rb
+++ b/app/models/tracker.rb
@@ -30,6 +30,8 @@ class Tracker < ActiveRecord::Base
has_many :workflow_rules, :dependent => :delete_all
has_and_belongs_to_many :projects
has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
+ has_many :attribute_groups, :dependent => :destroy
+ has_many :attribute_group_fields, :through => :attribute_groups
acts_as_positioned
validates_presence_of :default_status
diff --git a/app/views/issues/_form_custom_fields.html.erb b/app/views/issues/_form_custom_fields.html.erb
index 13bedd5..f2991e8 100644
--- a/app/views/issues/_form_custom_fields.html.erb
+++ b/app/views/issues/_form_custom_fields.html.erb
@@ -1,23 +1,31 @@
-<% custom_field_values = @issue.editable_custom_field_values %>
-<% custom_field_values_full_width = custom_field_values.select { |value| value.custom_field.full_width_layout? } %>
-<% custom_field_values -= custom_field_values_full_width %>
-
-<% if custom_field_values.present? %>
+<% group_by_keys(@issue.project_id, @issue.tracker_id, @issue.editable_custom_field_values).each do |title,values| %>
+<% if values.present? %>
+<%= content_tag('h4', title, :style => 'background: #0001; padding: 0.3em;') unless title.nil? %>
+<% while values.present? %>
+<% if values[0].custom_field.full_width_layout? %>
+<% while values.present? && values[0].custom_field.full_width_layout? %>
+<% value = values.shift %>
+
<%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %>
+<% end %>
+<% else %>
+<% lr_values = [] %>
+<% while values.present? && ! values[0].custom_field.full_width_layout? %>
+<% lr_values += [ values.shift ] %>
+<% end %>
<% i = 0 %>
-<% split_on = (custom_field_values.size / 2.0).ceil - 1 %>
-<% custom_field_values.each do |value| %>
-
<%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %>
-<% if i == split_on -%>
+<% split_on = (lr_values.size / 2.0).ceil - 1 %>
+<% lr_values.each do |value| %>
+
<%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %>
+<% if i == split_on %>
-<% end -%>
-<% i += 1 -%>
-<% end -%>
+<% end %>
+<% i += 1 %>
+<% end %>
+<% end %>
<% end %>
-
-<% custom_field_values_full_width.each do |value| %>
- <%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %>
+<% end %>
<% end %>
diff --git a/app/views/issues/show.html.erb b/app/views/issues/show.html.erb
index 5a1cc85..244fee2 100644
--- a/app/views/issues/show.html.erb
+++ b/app/views/issues/show.html.erb
@@ -71,7 +71,7 @@
rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time'
end
end %>
-<%= render_half_width_custom_fields_rows(@issue) %>
+<%= render_custom_fields_rows(@issue) %>
<%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
@@ -94,8 +94,6 @@ end %>
<%= link_to_attachments @issue, :thumbnails => true %>
<% end %>
-<%= render_full_width_custom_fields_rows(@issue) %>
-
<%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
<% if !@issue.leaf? || User.current.allowed_to?(:manage_subtasks, @project) %>
diff --git a/app/views/projects/settings/_groupissuescustomfields.html.erb b/app/views/projects/settings/_groupissuescustomfields.html.erb
new file mode 100644
index 0000000..58f8318
--- /dev/null
+++ b/app/views/projects/settings/_groupissuescustomfields.html.erb
@@ -0,0 +1,140 @@
+<%= select_tag "tracker_id", options_from_collection_for_select(@project.trackers.collect, "id", "name"), {:required => true, :onchange => "refresh_trackers(this);"} %>
+
+
+
+
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 36d2099..66cf55d 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1223,6 +1223,9 @@ en:
description_issue_category_reassign: Choose issue category
description_wiki_subpages_reassign: Choose new parent page
text_repository_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.
Once saved, the identifier cannot be changed.'
+ grouped_cf: Grouped Custom Fields
+ global_cf_position: Global Custom Fields Position
+ changed_cf_position: Changed Custom Fields Position
text_login_required_html: When not requiring authentication, public projects and their contents are openly available on the network. You can edit the applicable permissions.
label_login_required_yes: "Yes"
label_login_required_no: "No, allow anonymous access to public projects"
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 1dcf52b..0b9d445 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -1239,6 +1239,9 @@ pt-BR:
permission_view_news: Ver notícias
label_no_preview_alternative_html: Visualização não disponível. Faça o %{link} do arquivo.
label_no_preview_download: download
+ grouped_cf: Agrupar campos personalizados
+ global_cf_position: Posição global de campos personalizados
+ changed_cf_position: Alterar posição de campos personalizados
setting_close_duplicate_issues: Fechar tarefas duplicadas automaticamente
error_exceeds_maximum_hours_per_day: Não é possível registrar mais de %{max_hours} horas no mesmo dia (%{logged_hours} horas já foram registradas)
setting_time_entry_list_defaults: Registro de horas padrão
diff --git a/config/routes.rb b/config/routes.rb
index 0344982..9e27382 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -105,6 +105,7 @@ Rails.application.routes.draw do
member do
get 'settings(/:tab)', :action => 'settings', :as => 'settings'
+ post 'groupissuescustomfields'
post 'archive'
post 'unarchive'
post 'close'
diff --git a/db/migrate/20180913211420_create_attribute_groups.rb b/db/migrate/20180913211420_create_attribute_groups.rb
new file mode 100644
index 0000000..e878f01
--- /dev/null
+++ b/db/migrate/20180913211420_create_attribute_groups.rb
@@ -0,0 +1,12 @@
+class CreateAttributeGroups < ActiveRecord::Migration
+ def change
+ create_table :attribute_groups do |t|
+ t.references :project, index: true, foreign_key: true
+ t.references :tracker, index: true, foreign_key: true
+ t.string :name
+ t.integer :position, :default => 1, :null => false
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20180913212008_create_attribute_group_fields.rb b/db/migrate/20180913212008_create_attribute_group_fields.rb
new file mode 100644
index 0000000..8f44a38
--- /dev/null
+++ b/db/migrate/20180913212008_create_attribute_group_fields.rb
@@ -0,0 +1,11 @@
+class CreateAttributeGroupFields < ActiveRecord::Migration
+ def change
+ create_table :attribute_group_fields do |t|
+ t.references :attribute_group, index: true, foreign_key: true
+ t.references :custom_field, index: true, foreign_key: true
+ t.integer :position
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20190301162408_change_group_position_attributes.rb b/db/migrate/20190301162408_change_group_position_attributes.rb
new file mode 100644
index 0000000..3b7ae98
--- /dev/null
+++ b/db/migrate/20190301162408_change_group_position_attributes.rb
@@ -0,0 +1,6 @@
+class ChangeGroupPositionAttributes < ActiveRecord::Migration
+ def change
+ change_column_null :attribute_groups, :position, true
+ change_column_default :attribute_groups, :position, nil
+ end
+end
diff --git a/lib/redmine.rb b/lib/redmine.rb
index 6e7d109..3ec4221 100644
--- a/lib/redmine.rb
+++ b/lib/redmine.rb
@@ -77,7 +77,7 @@ Redmine::AccessControl.map do |map|
map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true, :read => true
map.permission :search_project, {:search => :index}, :public => true, :read => true
map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
- map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
+ map.permission :edit_project, {:projects => [:settings, :edit, :update, :groupissuescustomfields]}, :require => :member
map.permission :close_project, {:projects => [:close, :reopen]}, :require => :member, :read => true
map.permission :select_project_modules, {:projects => :modules}, :require => :member
map.permission :view_members, {:members => [:index, :show]}, :public => true, :read => true
diff --git a/lib/redmine/export/pdf/issues_pdf_helper.rb b/lib/redmine/export/pdf/issues_pdf_helper.rb
index 7e2c8a8..2621c3a 100644
--- a/lib/redmine/export/pdf/issues_pdf_helper.rb
+++ b/lib/redmine/export/pdf/issues_pdf_helper.rb
@@ -45,21 +45,21 @@ module Redmine
pdf.SetFontStyle('',8)
pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}")
pdf.ln
-
+
left = []
left << [l(:field_status), issue.status]
left << [l(:field_priority), issue.priority]
left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id')
left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id')
left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id')
-
+
right = []
right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date')
right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date')
right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio')
right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours')
right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project)
-
+
rows = left.size > right.size ? left.size : right.size
while left.size < rows
left << nil
@@ -68,12 +68,6 @@ module Redmine
right << nil
end
- custom_field_values = issue.visible_custom_field_values.reject {|value| value.custom_field.full_width_layout?}
- half = (custom_field_values.size / 2.0).ceil
- custom_field_values.each_with_index do |custom_value, i|
- (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)]
- end
-
if pdf.get_rtl
border_first_top = 'RT'
border_last_top = 'LT'
@@ -85,7 +79,8 @@ module Redmine
border_first = 'L'
border_last = 'R'
end
-
+ border_middle_top = 'T'
+
rows = left.size > right.size ? left.size : right.size
rows.times do |i|
heights = []
@@ -100,26 +95,88 @@ module Redmine
item = right[i]
heights << pdf.get_string_height(60, item ? item.last.to_s : "")
height = heights.max
-
+
item = left[i]
pdf.SetFontStyle('B',9)
pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0)
pdf.SetFontStyle('',9)
- pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 0)
-
+ pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_middle_top : ""), '', 0, 0)
+
item = right[i]
pdf.SetFontStyle('B',9)
- pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0)
+ pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_middle_top : ""), '', 0, 0)
pdf.SetFontStyle('',9)
pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 2)
-
+
pdf.set_x(base_x)
end
-
+
+ group_by_keys(issue.project_id, issue.tracker_id, issue.visible_custom_field_values).each do |title, values|
+ if values.present?
+ unless title.nil?
+ pdf.RDMCell(35+155, 5, title, "LRT", 1)
+ end
+
+ while values.present?
+ if values[0].custom_field.full_width_layout?
+ while values.present? && values[0].custom_field.full_width_layout?
+ value = values.shift
+ pdf.SetFontStyle('B',9)
+ pdf.RDMCell(35, 5, "#{value.custom_field.name}:", 'L', 0)
+ pdf.SetFontStyle('',9)
+ pdf.RDMCell(155, 5, show_value(value, false).to_s, 'R', 1)
+ end
+ else
+ lr_values = []
+ while values.present? && ! values[0].custom_field.full_width_layout?
+ lr_values += [ values.shift ]
+ end
+
+ half = (lr_values.size / 2.0).ceil
+ left = []
+ right = []
+ lr_values.each_with_index do |custom_value, i|
+ (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)]
+ end
+
+ rows = left.size > right.size ? left.size : right.size
+ rows.times do |i|
+ heights = []
+ pdf.SetFontStyle('B',9)
+ item = left[i]
+ heights << pdf.get_string_height(35, item ? "#{item.first}:" : "")
+ item = right[i]
+ heights << pdf.get_string_height(35, item ? "#{item.first}:" : "")
+ pdf.SetFontStyle('',9)
+ item = left[i]
+ heights << pdf.get_string_height(60, item ? item.last.to_s : "")
+ item = right[i]
+ heights << pdf.get_string_height(60, item ? item.last.to_s : "")
+ height = heights.max
+
+ item = left[i]
+ pdf.SetFontStyle('B',9)
+ pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", border_first, '', 0, 0)
+ pdf.SetFontStyle('',9)
+ pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", "", '', 0, 0)
+
+ item = right[i]
+ pdf.SetFontStyle('B',9)
+ pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", "", '', 0, 0)
+ pdf.SetFontStyle('',9)
+ pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", border_last, '', 0, 2)
+
+ pdf.set_x(base_x)
+ end
+ end
+ end
+ end
+ end
+
pdf.SetFontStyle('B',9)
pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1)
pdf.SetFontStyle('',9)
-
+
# Set resize image scale
pdf.set_image_scale(1.6)
text = textilizable(issue, :description,
@@ -130,17 +187,6 @@ module Redmine
)
pdf.RDMwriteFormattedCell(35+155, 5, '', '', text, issue.attachments, "LRB")
- custom_field_values = issue.visible_custom_field_values.select {|value| value.custom_field.full_width_layout?}
- custom_field_values.each do |value|
- text = show_value(value, false)
- next if text.blank?
-
- pdf.SetFontStyle('B',9)
- pdf.RDMCell(35+155, 5, value.custom_field.name, "LRT", 1)
- pdf.SetFontStyle('',9)
- pdf.RDMwriteHTMLCell(35+155, 5, '', '', text, issue.attachments, "LRB")
- end
-
unless issue.leaf?
truncate_length = (!is_cjk? ? 90 : 65)
pdf.SetFontStyle('B',9)
@@ -157,7 +203,7 @@ module Redmine
pdf.ln
end
end
-
+
relations = issue.relations.select { |r| r.other_issue(issue).visible? }
unless relations.empty?
truncate_length = (!is_cjk? ? 80 : 60)
@@ -185,7 +231,7 @@ module Redmine
end
pdf.RDMCell(190,5, "", "T")
pdf.ln
-
+
if issue.changesets.any? &&
User.current.allowed_to?(:view_changesets, issue.project)
pdf.SetFontStyle('B',9)
@@ -205,7 +251,7 @@ module Redmine
pdf.ln
end
end
-
+
if assoc[:journals].present?
pdf.SetFontStyle('B',9)
pdf.RDMCell(190,5, l(:label_history), "B")
@@ -234,7 +280,7 @@ module Redmine
pdf.ln
end
end
-
+
if issue.attachments.any?
pdf.SetFontStyle('B',9)
pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
@@ -261,7 +307,7 @@ module Redmine
pdf.footer_date = format_date(User.current.today)
pdf.set_auto_page_break(false)
pdf.add_page("L")
-
+
# Landscape A4 = 210 x 297 mm
page_height = pdf.get_page_height # 210
page_width = pdf.get_page_width # 297
@@ -269,7 +315,7 @@ module Redmine
right_margin = pdf.get_original_margins['right'] # 10
bottom_margin = pdf.get_footer_margin
row_height = 4
-
+
# column widths
table_width = page_width - right_margin - left_margin
col_width = []
@@ -277,13 +323,13 @@ module Redmine
col_width = calc_col_width(issues, query, table_width, pdf)
table_width = col_width.inject(0, :+)
end
-
+
# use full width if the description or last_notes are displayed
if table_width > 0 && (query.has_column?(:description) || query.has_column?(:last_notes))
col_width = col_width.map {|w| w * (page_width - right_margin - left_margin) / table_width}
table_width = col_width.inject(0, :+)
end
-
+
# title
pdf.SetFontStyle('B',11)
pdf.RDMCell(190, 8, title)
@@ -317,10 +363,10 @@ module Redmine
end
previous_group = group
end
-
+
# fetch row values
col_values = fetch_row_values(issue, query, level)
-
+
# make new page if it doesn't fit on the current one
base_y = pdf.get_y
max_height = get_issues_to_pdf_write_cells(pdf, col_values, col_width)
@@ -330,11 +376,11 @@ module Redmine
render_table_header(pdf, query, col_width, row_height, table_width)
base_y = pdf.get_y
end
-
+
# write the cells on page
issues_to_pdf_write_cells(pdf, col_values, col_width, max_height)
pdf.set_y(base_y + max_height)
-
+
if query.has_column?(:description) && issue.description?
pdf.set_x(10)
pdf.set_auto_page_break(true, bottom_margin)
@@ -349,7 +395,7 @@ module Redmine
pdf.set_auto_page_break(false)
end
end
-
+
if issues.size == Setting.issues_export_limit.to_i
pdf.SetFontStyle('B',10)
pdf.RDMCell(0, row_height, '...')
diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css
index 0f708bf..566c300 100644
--- a/public/stylesheets/application.css
+++ b/public/stylesheets/application.css
@@ -475,9 +475,10 @@ div.issue p.author {margin-top:0.5em;}
div.issue span.private, div.journal span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
div.issue .next-prev-links {color:#999;}
div.issue .attributes {margin-top: 2em;}
-div.issue .attributes .attribute {padding-left:180px; clear:left; min-height: 1.8em;}
-div.issue .attributes .attribute .label {width: 170px; margin-left:-180px; font-weight:bold; float:left; overflow:hidden; text-overflow: ellipsis;}
+div.issue .attributes .attribute {padding-left:225px; clear:left; min-height: 1.8em;}
+div.issue .attributes .attribute .label {width: 215px; margin-left:-225px; font-weight:bold; float:left; overflow:hidden; text-overflow: ellipsis;}
div.issue .attribute .value {overflow:auto; text-overflow: ellipsis;}
+div.issue .attribute .value p {margin: 0 0 0.8em 0;}
div.issue.overdue .due-date .value { color: #c22; }
#issue_tree table.issues, #relations table.issues { border: 0; }
@@ -1549,3 +1550,48 @@ img {
max-height: 100%;
max-width: 100%;
}
+
+/* Custom Field Groups */
+.sortable_groups {
+ background: #eee;
+ border: 1px solid #888;
+ width: 445px;
+ min-height: 30px;
+ margin: 5px;
+ padding: 5px;
+}
+.sortable_groups div {
+ background: #ddf;
+ border: 1px solid #888;
+ min-height: 30px;
+ margin: 5px;
+ padding: 5px;
+}
+.sortable_items {
+ background: #eee;
+ border: 1px solid #888;
+ width: 400px;
+ min-height: 30px;
+ margin: 5px;
+ padding: 5px;
+}
+.sortable_items li {
+ background: #ffffff;
+ border: 1px solid #888;
+ margin: 5px;
+ padding: 5px;
+ list-style-type: none;
+ font-size: 1.2em;
+}
+.not_a_group {
+ padding: 5px 27px;
+}
+.ui-sortable-handle:before {
+ content:url('/images/reorder.png');
+ margin-right: 5px;
+}
+.ui-sortable-placeholder {
+ background: #8888ff;
+ border: 1px solid #888;
+ visibility: visible;
+}
diff --git a/test/fixtures/attribute_group_fields.yml b/test/fixtures/attribute_group_fields.yml
new file mode 100644
index 0000000..006a407
--- /dev/null
+++ b/test/fixtures/attribute_group_fields.yml
@@ -0,0 +1,11 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+ attribute_group_id:
+ custom_field_id:
+ position: 1
+
+two:
+ attribute_group_id:
+ custom_field_id:
+ position: 1
diff --git a/test/fixtures/attribute_groups.yml b/test/fixtures/attribute_groups.yml
new file mode 100644
index 0000000..7b7aed5
--- /dev/null
+++ b/test/fixtures/attribute_groups.yml
@@ -0,0 +1,13 @@
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+
+one:
+ project_id:
+ tracker_id:
+ name: MyString
+ position: 1
+
+two:
+ project_id:
+ tracker_id:
+ name: MyString
+ position: 1