Project

General

Profile

Patch #30919 » GCF-3.2-final1.patch

Frederico Camara, 2019-03-01 17:22

View differences:

app/controllers/projects_controller.rb
162 162
    @member ||= @project.members.new
163 163
    @trackers = Tracker.sorted.to_a
164 164
    @wiki ||= @project.wiki || Wiki.new(:project => @project)
165
    @cfs=AttributeGroup.joins(:custom_fields).joins(:tracker).
166
      where(project_id: @project, tracker_id: @trackers, :custom_fields => {id: @project.all_issue_custom_fields.pluck(:id)}).
167
      pluck("trackers.id", "id", "name", "position","attribute_group_fields.id", "attribute_group_fields.position",
168
            "custom_fields.id", "custom_fields.name", "custom_fields.position").sort_by{|x| [x[3], x[5]]}
165 169
  end
166 170

  
167 171
  def edit
......
194 198
    redirect_to settings_project_path(@project, :tab => 'modules')
195 199
  end
196 200

  
201
  def groupissuescustomfields
202
    # clean invalid values: invalid cfs, empty cf lists, empty groups
203
    group_issues_custom_fields = (JSON.parse params[:group_issues_custom_fields]).
204
      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})}.
205
      each{|tid,v| v.delete_if{|k,v| v["cfs"].blank?}}.
206
      delete_if{|k,v| v.blank?}
207

  
208
    groups = AttributeGroup.where(project_id: @project.id).collect(&:id)
209
    fields = AttributeGroupField.where(attribute_group_id: groups).collect(&:id) 
210
    group_issues_custom_fields.each do |tid,v|
211
      v.each do |gp, g|
212
        gid = groups.shift
213
        if gid.nil?
214
          gid=AttributeGroup.create(project_id: @project.id, tracker_id: tid, name: g["name"].nil? ? nil : g["name"], position: gp).id
215
        else
216
          AttributeGroup.update(gid, project_id: @project.id, tracker_id: tid, name: g["name"].nil? ? nil : g["name"], position: gp)
217
        end
218
        g['cfs'].each do |cfp, cf|
219
          cfid = fields.shift
220
          if cfid.nil?
221
            AttributeGroupField.create(attribute_group_id: gid, custom_field_id: cf, position: cfp)
222
          else
223
            AttributeGroupField.update(cfid, attribute_group_id: gid, custom_field_id: cf, position: cfp)
224
          end
225
        end
226
      end
227
    end
228
    AttributeGroupField.where(id: fields).delete_all
229
    AttributeGroup.where(id: groups).destroy_all
230
    flash[:notice] = l(:notice_successful_update)
231
    redirect_to settings_project_path(@project, :tab => 'groupissuescustomfields')
232
  end
233

  
197 234
  def archive
198 235
    unless @project.archive
199 236
      flash[:error] = l(:error_can_not_archive_project)
app/helpers/issues_helper.rb
210 210
    r.to_html
211 211
  end
212 212

  
213
  def group_by_keys(project_id, tracker_id, custom_field_values)
214
    keys_grouped = AttributeGroupField.joins(:attribute_group).
215
      where(:attribute_groups => {project_id: project_id, tracker_id: tracker_id}).
216
      order("attribute_groups.position", :position).pluck(:name, :custom_field_id).group_by(&:shift)
217
    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) |
218
      custom_field_values.select{|y| ! keys_grouped.values.flatten.include?(y.custom_field[:id])}}
219
    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}
220
    custom_fields_grouped
221
  end
222

  
213 223
  def render_custom_fields_rows(issue)
214
    values = issue.visible_custom_field_values
215
    return if values.empty?
216
    half = (values.size / 2.0).ceil
217
    issue_fields_rows do |rows|
218
      values.each_with_index do |value, i|
219
        css = "cf_#{value.custom_field.id}"
220
        m = (i < half ? :left : :right)
221
        rows.send m, custom_field_name_tag(value.custom_field), show_value(value), :class => css
224
    s=''
225
    group_by_keys(issue.project_id, issue.tracker_id, issue.visible_custom_field_values).each do |title, values|
226
      if values.present?
227
        s << content_tag('h4', title, :style => 'background: #0001; padding: 0.3em;') unless title.nil?
228
        half = (values.size / 2.0).ceil
229
        s << issue_fields_rows do |rows|
230
          values.each_with_index do |value, i|
231
            css = "cf_#{value.custom_field.id}"
232
            m = (i < half ? :left : :right)
233
            rows.send m, custom_field_name_tag(value.custom_field), show_value(value), :class => css
234
          end
235
        end
222 236
      end
223 237
    end
238
    s.html_safe
224 239
  end
225 240

  
226 241
  # Returns the path for updating the issue form
......
307 322
        items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
308 323
      end
309 324
    end
310
    issue.visible_custom_field_values(user).each do |value|
311
      items << "#{value.custom_field.name}: #{show_value(value, false)}"
325
    group_by_keys(issue.project_id, issue.tracker_id, issue.visible_custom_field_values(user)).each do |title, values|
326
      if values.present?
327
        if title.nil?
328
          values.each do |value|
329
            items << "#{value.custom_field.name}: #{show_value(value, false)}"
330
          end
331
        else   
332
          item = [ "#{title}" ]
333
          values.each do |value|
334
            item << "#{value.custom_field.name}: #{show_value(value, false)}"
335
          end
336
          items << item
337
        end
338
      end
312 339
    end
313 340
    items
314 341
  end
......
316 343
  def render_email_issue_attributes(issue, user, html=false)
317 344
    items = email_issue_attributes(issue, user)
318 345
    if html
319
      content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
346
      content_tag('ul', items.select{|s| s.is_a? String}.map{|s| content_tag('li', s)}.join("\n").html_safe) + "\n" +
347
      items.select{|s| !s.is_a? String}.map{|item| content_tag('div', item.shift) + "\n" +
348
        content_tag('ul', item.map{|s| content_tag('li', s)}.join("\n").html_safe)}.join("\n").html_safe
320 349
    else
321
      items.map{|s| "* #{s}"}.join("\n")
350
      items.select{|s| s.is_a? String}.map{|s| "* #{s}"}.join("\n") + "\n" +
351
      items.select{|s| !s.is_a? String}.map{|item| "#{item.shift}\n" + item.map{|s| "* #{s}"}.join("\n")}.join("\n")
322 352
    end
323 353
  end
324 354

  
app/helpers/projects_helper.rb
27 27
            {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
28 28
            {:name => 'repositories', :action => :manage_repository, :partial => 'projects/settings/repositories', :label => :label_repository_plural},
29 29
            {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
30
            {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
30
            {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities},
31
            {:name => 'groupissuescustomfields', :action => :edit_project, :partial => 'projects/settings/groupissuescustomfields', :label => :grouped_cf}
31 32
            ]
32 33
    tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
33 34
  end
app/models/attribute_group.rb
1
class AttributeGroup < ActiveRecord::Base
2
  belongs_to :project
3
  belongs_to :tracker
4
  has_many :attribute_group_fields, :dependent => :delete_all
5
  has_many :custom_fields, :through => :attribute_group_fields
6
  acts_as_list
7

  
8
  scope :sorted, lambda { order(:position) }
9
end
app/models/attribute_group_field.rb
1
class AttributeGroupField < ActiveRecord::Base
2
  belongs_to :attribute_group
3
  belongs_to :custom_field
4
  acts_as_list
5

  
6
  scope :sorted, lambda { order(:position) }
7
end
app/models/custom_field.rb
23 23
           :class_name => 'CustomFieldEnumeration',
24 24
           :dependent => :delete_all
25 25
  has_many :custom_values, :dependent => :delete_all
26
  has_many :attribute_group_fields, :dependent => :delete_all
26 27
  has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
27 28
  acts_as_list :scope => 'type = \'#{self.class}\''
28 29
  serialize :possible_values
app/models/project.rb
50 50
  has_many :changesets, :through => :repository
51 51
  has_one :wiki, :dependent => :destroy
52 52
  # Custom field for the project issues
53
  has_many :attribute_groups, :dependent => :destroy
54
  has_many :attribute_group_fields, :through => :attribute_groups
53 55
  has_and_belongs_to_many :issue_custom_fields,
54 56
                          lambda {order("#{CustomField.table_name}.position")},
55 57
                          :class_name => 'IssueCustomField',
app/models/tracker.rb
34 34

  
35 35
  has_and_belongs_to_many :projects
36 36
  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'
37
  has_many :attribute_groups, :dependent => :destroy
38
  has_many :attribute_group_fields, :through => :attribute_groups
37 39
  acts_as_list
38 40

  
39 41
  attr_protected :fields_bits
app/views/issues/_form_custom_fields.html.erb
1
<% custom_field_values = @issue.editable_custom_field_values %>
2
<% if custom_field_values.present? %>
1
<% group_by_keys(@issue.project_id, @issue.tracker_id, @issue.editable_custom_field_values).each do |title,values| %>
2
<% if values.present? %>
3
<%= content_tag('h4', title, :style => 'background: #0001; padding: 0.3em;') unless title.nil? %>
3 4
<div class="splitcontent">
4 5
<div class="splitcontentleft">
5 6
<% i = 0 %>
6
<% split_on = (custom_field_values.size / 2.0).ceil - 1 %>
7
<% custom_field_values.each do |value| %>
7
<% split_on = (values.size / 2.0).ceil - 1 %>
8
<% values.each do |value| %>
8 9
  <p><%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %></p>
9
<% if i == split_on -%>
10
<% if i == split_on %>
10 11
</div><div class="splitcontentright">
11
<% end -%>
12
<% i += 1 -%>
13
<% end -%>
12
<% end %>
13
<% i += 1 %>
14
<% end %>
14 15
</div>
15 16
</div>
16 17
<% end %>
18
<% end %>
app/views/projects/settings/_groupissuescustomfields.html.erb
1
<p><%= select_tag "tracker_id", options_from_collection_for_select(@project.trackers.collect, "id", "name"), {:required => true, :onchange => "refresh_trackers(this);"} %><p>
2

  
3
<div id='custom_fields_form'>
4
  <% @project.trackers.each do |t| %>
5
  <div id='tracker_<%= t.id %>' class='tracker'>
6
    <span style="color: #888;"><%= l(:changed_cf_position) %></span>
7
    <ul class='sortable_items sortable_tracker-<%= t.id %> nil not_a_group'>
8
      <% @cfs.select{|x| x[0]==t.id && x[2]==nil}.each do |x| %>
9
      <li id="<%= x[6] %>" pos="<%= x[8] %>"><%= x[7] %></li>
10
      <% end %>
11
    </ul>
12
    <span style="color: #888;"><%= l(:global_cf_position) %></span>
13
    <ul class="sortable_items sortable_tracker-<%= t.id %> unsorted not_a_group">
14
      <% Issue.new(project_id: @project.id, tracker_id: t.id).custom_field_values.
15
           select{|x| ! @cfs.select{|c| c[0]==t.id}.map{|c| c[6]}.include?(x.custom_field_id)}.
16
           collect{|x| [x.custom_field.id, x.custom_field.name, x.custom_field.position]}.each do |x| %>
17
      <li id="<%= x[0] %>" pos="<%= x[2] %>"><%= x[1] %></li>
18
      <% end %>
19
    </ul>
20
    <span style="color: #888;"><%= l(:grouped_cf) %></span>
21
    <div class='sortable_groups groups_tracker-<%= t.id %>'>
22
      <% @cfs.select{|x| x[0]==t.id && x[2]!=nil}.map{|x| x[2]}.uniq.each do |g| %>
23
      <div class="sortable_group">
24
        <input type="text" value="<%= g %>" style="background: #ddf; padding: 5px; width: 360px"/>
25
        <img src="/images/delete.png" onclick="remove_label(this, 'tracker-<%= t.id %>');"/>
26
        <ul class='sortable_items sortable_tracker-<%= t.id %>'>
27
          <% @cfs.select{|x| x[0]==t.id && x[2]==g}.each do |x| %>
28
          <li id="<%= x[6] %>" pos="<%= x[8] %>"><%= x[7] %></li>
29
          <% end %>
30
        </ul>
31
      </div>
32
      <% end %>
33
      <div>
34
        <input type="text" value="" style="background: #ddf; padding: 5px; width: 370px"/>
35
        <img src="/images/add.png" style="" onclick="add_label(this, 'tracker-<%= t.id %>');"/>
36
      </div>
37
    </div>
38
  </div>
39
  <% end %>
40
<%= form_for @project, :url => { :action => 'groupissuescustomfields', :id => @project },
41
            :html => {:id => 'groupissuescustomfields-form',
42
                      :method => :post} do |f| %>
43

  
44
<%= hidden_field :group_issues_custom_fields, '', :id => 'group_issues_custom_fields', :name => 'group_issues_custom_fields' %>
45
<p><%= submit_tag l(:button_save), :onclick => "fill_json_data();" %></p>
46
<% end %>
47
</div>
48

  
49
<script>
50
function init_sortables(labelclass) {
51
  $( '.groups_' + labelclass ).sortable({
52
    items: 'div.sortable_group',
53
    connectWith: '.groups_' + labelclass,
54
    start: function(event, ui) {
55
      ui.placeholder.height(ui.item.height());
56
    },
57
    axis: 'y'
58
  });
59
  $( '.sortable_' + labelclass ).sortable({
60
    connectWith: '.sortable_' + labelclass,
61
    update: function(event, ui) {
62
      if (ui.item.parent().hasClass('unsorted')) {
63
        ui.item.parent().prepend(ui.item);
64
        ui.item.parent().children('li').each(function () {
65
          if (parseInt($(this).attr('pos')) < parseInt(ui.item.attr('pos'))) {
66
            ui.item.insertAfter(this);
67
          }
68
        });
69
      }
70
    },
71
    axis: 'y'
72
  }).disableSelection();
73
}
74
function refresh_trackers(tracker) {
75
  $('.tracker').hide();
76
  $('#tracker_' + tracker.value).show();
77
}
78
function add_label(label, labelclass) {
79
  var old_label = $(label), new_label = $(label).parent().clone();
80
  new_label.children().first().val('').trigger('change');
81
  old_label.attr('src','/images/delete.png').attr('onclick','remove_label(this, "' + labelclass + '");');
82
  old_label.prev().width('360px');
83
  old_label.parent().addClass('ui-sortable-handle sortable_group');
84
  old_label.parent().sortable({
85
    connectWith: '.groups_' + labelclass
86
  }).disableSelection();
87
  old_label.parent().append('<ul id="' + labelclass + '_" class="sortable_items sortable_' + labelclass + '"/>');
88
  old_label.next().sortable({
89
    connectWith: '.sortable_' + labelclass
90
  }).disableSelection();
91
  old_label.parent().parent().append(new_label);
92
}
93
function remove_label(label, labelclass) {
94
  $(label).next().children().each(function () {
95
    var item = this;
96
    $('.unsorted.sortable_' + labelclass).prepend(this);
97
    $('.unsorted.sortable_' + labelclass).children('li').each(function () {
98
      if (parseInt($(this).attr('pos')) < parseInt($(item).attr('pos'))) {
99
        $(item).insertAfter(this);
100
      }
101
    });
102
  });
103
  $(label).parent().hide();
104
}
105
function fill_json_data() {
106
  var r = {}, gp = 0, cp = 0;
107
  $('#custom_fields_form').children('div').each(function () {
108
    var tracker_id = this.id.split('_').pop();
109
    gp = 0, cp = 0;
110

  
111
    // Group 'nil'
112
    r[tracker_id] = {};
113
    r[tracker_id][++gp] = {'cfs': {}};
114
    $(this).children('.nil').children().each(function () {
115
      cp++;
116
      r[tracker_id][gp]['cfs'][cp] = this.id;
117
    });
118
    if (! r[tracker_id][gp]['cfs'][1]) { delete r[tracker_id][gp]; }
119
    $(this).children('.sortable_groups').children().each(function () {
120
      if ($(this).children('ul').length) {
121
        r[tracker_id][++gp] = {
122
          'name': $(this).children('input').val(),
123
          'cfs': {}
124
        };
125
        cp = 0;
126
        $(this).children('ul').children().each(function () {
127
          r[tracker_id][gp]['cfs'][++cp] = this.id;
128
        });
129
        if (! r[tracker_id][gp]['cfs'][1]) { r[tracker_id][gp] = ''; }
130
      }
131
    });
132
    if ( ! r[tracker_id] ) { r[tracker_id] = ''; }
133
  });
134
  console.log(r);
135
  $('#group_issues_custom_fields').val(JSON.stringify(r)); }
136
$('#tracker_id').change();
137
<% @project.trackers.each do |t| %>
138
init_sortables('tracker-<%= t.id %>');
139
<% end %>
140
</script>
config/locales/en.yml
1170 1170
  description_date_from: Enter start date
1171 1171
  description_date_to: Enter end date
1172 1172
  text_repository_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.'
1173

  
1174
  grouped_cf: Grouped Custom Fields
1175
  global_cf_position: Global Custom Fields Position
1176
  changed_cf_position: Changed Custom Fields Position
config/locales/pt-BR.yml
1203 1203
  label_default_values_for_new_users: Valor padrão para novos usuários
1204 1204
  error_cannot_reassign_time_entries_to_an_issue_about_to_be_deleted: Spent time cannot
1205 1205
    be reassigned to an issue that is about to be deleted
1206

  
1207
  grouped_cf: Agrupar campos personalizados
1208
  global_cf_position: Posição global de campos personalizados
1209
  changed_cf_position: Alterar posição de campos personalizados
config/routes.rb
101 101
    member do
102 102
      get 'settings(/:tab)', :action => 'settings', :as => 'settings'
103 103
      post 'modules'
104
      post 'groupissuescustomfields'
104 105
      post 'archive'
105 106
      post 'unarchive'
106 107
      post 'close'
db/migrate/20180913211420_create_attribute_groups.rb
1
class CreateAttributeGroups < ActiveRecord::Migration
2
  def change
3
    create_table :attribute_groups do |t|
4
      t.references :project, index: true, foreign_key: true
5
      t.references :tracker, index: true, foreign_key: true
6
      t.string :name
7
      t.integer :position, :default => 1, :null => false
8

  
9
      t.timestamps null: false
10
    end
11
  end
12
end
db/migrate/20180913212008_create_attribute_group_fields.rb
1
class CreateAttributeGroupFields < ActiveRecord::Migration
2
  def change
3
    create_table :attribute_group_fields do |t|
4
      t.references :attribute_group, index: true, foreign_key: true
5
      t.references :custom_field, index: true, foreign_key: true
6
      t.integer :position
7

  
8
      t.timestamps null: false
9
    end
10
  end
11
end
db/migrate/20190301162408_change_group_position_attributes.rb
1
class ChangeGroupPositionAttributes < ActiveRecord::Migration
2
  def change
3
    change_column_null :attribute_groups, :position, true
4
    change_column_default :attribute_groups, :position, nil
5
  end
6
end
lib/redmine.rb
77 77
  map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true, :read => true
78 78
  map.permission :search_project, {:search => :index}, :public => true, :read => true
79 79
  map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
80
  map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
80
  map.permission :edit_project, {:projects => [:settings, :edit, :update, :groupissuescustomfields]}, :require => :member
81 81
  map.permission :close_project, {:projects => [:close, :reopen]}, :require => :member, :read => true
82 82
  map.permission :select_project_modules, {:projects => :modules}, :require => :member
83 83
  map.permission :view_members, {:members => [:index, :show]}, :public => true, :read => true
lib/redmine/export/pdf/issues_pdf_helper.rb
45 45
          pdf.SetFontStyle('',8)
46 46
          pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}")
47 47
          pdf.ln
48
  
48

  
49 49
          left = []
50 50
          left << [l(:field_status), issue.status]
51 51
          left << [l(:field_priority), issue.priority]
52 52
          left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id')
53 53
          left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id')
54 54
          left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id')
55
  
55

  
56 56
          right = []
57 57
          right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date')
58 58
          right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date')
59 59
          right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio')
60 60
          right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours')
61 61
          right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project)
62
  
62

  
63 63
          rows = left.size > right.size ? left.size : right.size
64 64
          while left.size < rows
65 65
            left << nil
......
67 67
          while right.size < rows
68 68
            right << nil
69 69
          end
70
  
71
          half = (issue.visible_custom_field_values.size / 2.0).ceil
72
          issue.visible_custom_field_values.each_with_index do |custom_value, i|
73
            (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)]
74
          end
75
  
70

  
76 71
          if pdf.get_rtl
77 72
            border_first_top = 'RT'
78 73
            border_last_top  = 'LT'
......
84 79
            border_first = 'L'
85 80
            border_last  = 'R'
86 81
          end
87
  
82
          border_middle_top  = 'T'
83

  
88 84
          rows = left.size > right.size ? left.size : right.size
89 85
          rows.times do |i|
90 86
            heights = []
......
99 95
            item = right[i]
100 96
            heights << pdf.get_string_height(60, item ? item.last.to_s  : "")
101 97
            height = heights.max
102
  
98

  
103 99
            item = left[i]
104 100
            pdf.SetFontStyle('B',9)
105 101
            pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", (i == 0 ? border_first_top : border_first), '', 0, 0)
106 102
            pdf.SetFontStyle('',9)
107
            pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 0)
108
  
103
            pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_middle_top : ""), '', 0, 0)
104

  
109 105
            item = right[i]
110 106
            pdf.SetFontStyle('B',9)
111
            pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "",  (i == 0 ? border_first_top : border_first), '', 0, 0)
107
            pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "",  (i == 0 ? border_middle_top : ""), '', 0, 0)
112 108
            pdf.SetFontStyle('',9)
113 109
            pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", (i == 0 ? border_last_top : border_last), '', 0, 2)
114
  
110

  
115 111
            pdf.set_x(base_x)
116 112
          end
117
  
113

  
114
          group_by_keys(issue.project_id, issue.tracker_id, issue.visible_custom_field_values).each do |title, values|
115
            if values.present?
116
              pdf.RDMCell(35+155, 5, title, "LRT", 1) unless title.nil?
117

  
118
              half = (values.size / 2.0).ceil
119
              left = []
120
              right = []
121
              values.each_with_index do |custom_value, i|
122
                (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value, false)]
123
              end
124

  
125
              rows = left.size > right.size ? left.size : right.size
126
              rows.times do |i|
127
                heights = []
128
                pdf.SetFontStyle('B',9)
129
                item = left[i]
130
                heights << pdf.get_string_height(35, item ? "#{item.first}:" : "")
131
                item = right[i]
132
                heights << pdf.get_string_height(35, item ? "#{item.first}:" : "")
133
                pdf.SetFontStyle('',9)
134
                item = left[i]
135
                heights << pdf.get_string_height(60, item ? item.last.to_s  : "")
136
                item = right[i]
137
                heights << pdf.get_string_height(60, item ? item.last.to_s  : "")
138
                height = heights.max
139

  
140
                item = left[i]
141
                pdf.SetFontStyle('B',9)
142
                pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "", border_first, '', 0, 0)
143
                pdf.SetFontStyle('',9)
144
                pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", "", '', 0, 0)
145

  
146
                item = right[i]
147
                pdf.SetFontStyle('B',9)
148
                pdf.RDMMultiCell(35, height, item ? "#{item.first}:" : "",  "", '', 0, 0)
149
                pdf.SetFontStyle('',9)
150
                pdf.RDMMultiCell(60, height, item ? item.last.to_s : "", border_last, '', 0, 2)
151

  
152
                pdf.set_x(base_x)
153
              end
154
            end
155
          end
156

  
118 157
          pdf.SetFontStyle('B',9)
119 158
          pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1)
120 159
          pdf.SetFontStyle('',9)
121
  
160

  
122 161
          # Set resize image scale
123 162
          pdf.set_image_scale(1.6)
124 163
          text = textilizable(issue, :description,
......
128 167
            :inline_attachments => false
129 168
          )
130 169
          pdf.RDMwriteFormattedCell(35+155, 5, '', '', text, issue.attachments, "LRB")
131
  
170

  
132 171
          unless issue.leaf?
133 172
            truncate_length = (!is_cjk? ? 90 : 65)
134 173
            pdf.SetFontStyle('B',9)
......
145 184
              pdf.ln
146 185
            end
147 186
          end
148
  
187

  
149 188
          relations = issue.relations.select { |r| r.other_issue(issue).visible? }
150 189
          unless relations.empty?
151 190
            truncate_length = (!is_cjk? ? 80 : 60)
......
173 212
          end
174 213
          pdf.RDMCell(190,5, "", "T")
175 214
          pdf.ln
176
  
215

  
177 216
          if issue.changesets.any? &&
178 217
               User.current.allowed_to?(:view_changesets, issue.project)
179 218
            pdf.SetFontStyle('B',9)
......
193 232
              pdf.ln
194 233
            end
195 234
          end
196
  
235

  
197 236
          if assoc[:journals].present?
198 237
            pdf.SetFontStyle('B',9)
199 238
            pdf.RDMCell(190,5, l(:label_history), "B")
......
222 261
              pdf.ln
223 262
            end
224 263
          end
225
  
264

  
226 265
          if issue.attachments.any?
227 266
            pdf.SetFontStyle('B',9)
228 267
            pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
......
249 288
          pdf.footer_date = format_date(Date.today)
250 289
          pdf.set_auto_page_break(false)
251 290
          pdf.add_page("L")
252
  
291

  
253 292
          # Landscape A4 = 210 x 297 mm
254 293
          page_height   = pdf.get_page_height # 210
255 294
          page_width    = pdf.get_page_width  # 297
......
257 296
          right_margin  = pdf.get_original_margins['right'] # 10
258 297
          bottom_margin = pdf.get_footer_margin
259 298
          row_height    = 4
260
  
299

  
261 300
          # column widths
262 301
          table_width = page_width - right_margin - left_margin
263 302
          col_width = []
......
265 304
            col_width = calc_col_width(issues, query, table_width, pdf)
266 305
            table_width = col_width.inject(0, :+)
267 306
          end
268
  
307

  
269 308
          # use full width if the description is displayed
270 309
          if table_width > 0 && query.has_column?(:description)
271 310
            col_width = col_width.map {|w| w * (page_width - right_margin - left_margin) / table_width}
272 311
            table_width = col_width.inject(0, :+)
273 312
          end
274
  
313

  
275 314
          # title
276 315
          pdf.SetFontStyle('B',11)
277 316
          pdf.RDMCell(190, 8, title)
......
303 342
              end
304 343
              previous_group = group
305 344
            end
306
  
345

  
307 346
            # fetch row values
308 347
            col_values = fetch_row_values(issue, query, level)
309
  
348

  
310 349
            # make new page if it doesn't fit on the current one
311 350
            base_y     = pdf.get_y
312 351
            max_height = get_issues_to_pdf_write_cells(pdf, col_values, col_width)
......
316 355
              render_table_header(pdf, query, col_width, row_height, table_width)
317 356
              base_y = pdf.get_y
318 357
            end
319
  
358

  
320 359
            # write the cells on page
321 360
            issues_to_pdf_write_cells(pdf, col_values, col_width, max_height)
322 361
            pdf.set_y(base_y + max_height)
323
  
362

  
324 363
            if query.has_column?(:description) && issue.description?
325 364
              pdf.set_x(10)
326 365
              pdf.set_auto_page_break(true, bottom_margin)
......
328 367
              pdf.set_auto_page_break(false)
329 368
            end
330 369
          end
331
  
370

  
332 371
          if issues.size == Setting.issues_export_limit.to_i
333 372
            pdf.SetFontStyle('B',10)
334 373
            pdf.RDMCell(0, row_height, '...')
public/stylesheets/application.css
361 361
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;}
362 362
div.issue .next-prev-links {color:#999;}
363 363
div.issue .attributes {margin-top: 2em;}
364
div.issue .attribute {padding-left:180px; clear:left; min-height: 1.8em;}
365
div.issue .attribute .label {width: 170px; margin-left:-180px; font-weight:bold; float:left;}
364
div.issue .attribute {padding-left:225px; clear:left; min-height: 1.8em;}
365
div.issue .attribute .label {width: 215px; margin-left:-225px; font-weight:bold; float:left;}
366
div.issue .attribute .value p {margin: 0 0 0.8em 0;}
366 367
div.issue.overdue .due-date .value { color: #c22; }
367 368

  
368 369
#issue_tree table.issues, #relations table.issues { border: 0; }
......
1313 1314
  height:1px;
1314 1315
  overflow:hidden;
1315 1316
}
1317

  
1318
/* Custom Field Groups */
1319
.sortable_groups {
1320
  background: #eee;
1321
  border: 1px solid #888;
1322
  width: 445px;
1323
  min-height: 30px;
1324
  margin: 5px;
1325
  padding: 5px;
1326
}
1327
.sortable_groups div {
1328
  background: #ddf;
1329
  border: 1px solid #888;
1330
  min-height: 30px;
1331
  margin: 5px;
1332
  padding: 5px;
1333
}
1334
.sortable_items {
1335
  background: #eee;
1336
  border: 1px solid #888;
1337
  width: 400px;
1338
  min-height: 30px;
1339
  margin: 5px;
1340
  padding: 5px;
1341
}
1342
.sortable_items li {
1343
  background: #ffffff;
1344
  border: 1px solid #888;
1345
  margin: 5px;
1346
  padding: 5px;
1347
  list-style-type: none;
1348
  font-size: 1.2em;
1349
}
1350
.not_a_group {
1351
  padding: 5px 27px;
1352
}
1353
.ui-sortable-handle:before {
1354
  content:url('/images/reorder.png');
1355
  margin-right: 5px;
1356
}
1357
.ui-sortable-placeholder {
1358
  background: #8888ff;
1359
  border: 1px solid #888;
1360
  visibility: visible;
1361
}
test/fixtures/attribute_group_fields.yml
1
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2

  
3
one:
4
  attribute_group_id: 
5
  custom_field_id: 
6
  position: 1
7

  
8
two:
9
  attribute_group_id: 
10
  custom_field_id: 
11
  position: 1
test/fixtures/attribute_groups.yml
1
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2

  
3
one:
4
  project_id: 
5
  tracker_id: 
6
  name: MyString
7
  position: 1
8

  
9
two:
10
  project_id: 
11
  tracker_id: 
12
  name: MyString
13
  position: 1
(13-13/24)