##// END OF EJS Templates
Bulk edit workflows for multiple trackers/roles (#16164)....
Jean-Philippe Lang -
r12649:b6c794d16b47
parent child
Show More
@@ -0,0 +1,93
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.expand_path('../../test_helper', __FILE__)
19
20 class WorkflowTransitionTest < ActiveSupport::TestCase
21 fixtures :roles, :trackers, :issue_statuses
22
23 def setup
24 WorkflowTransition.delete_all
25 end
26
27 def test_replace_transitions_should_create_enabled_transitions
28 w = WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2)
29
30 transitions = {'1' => {
31 '2' => {'always' => '1'},
32 '3' => {'always' => '1'}
33 }}
34 assert_difference 'WorkflowTransition.count' do
35 WorkflowTransition.replace_transitions(Tracker.find(1), Role.find(1), transitions)
36 end
37 assert WorkflowTransition.where(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3).exists?
38 end
39
40 def test_replace_transitions_should_delete_disabled_transitions
41 w1 = WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2)
42 w2 = WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
43
44 transitions = {'1' => {
45 '2' => {'always' => '0'},
46 '3' => {'always' => '1'}
47 }}
48 assert_difference 'WorkflowTransition.count', -1 do
49 WorkflowTransition.replace_transitions(Tracker.find(1), Role.find(1), transitions)
50 end
51 assert !WorkflowTransition.exists?(w1.id)
52 end
53
54 def test_replace_transitions_should_create_enabled_additional_transitions
55 transitions = {'1' => {
56 '2' => {'always' => '0', 'assignee' => '0', 'author' => '1'}
57 }}
58 assert_difference 'WorkflowTransition.count' do
59 WorkflowTransition.replace_transitions(Tracker.find(1), Role.find(1), transitions)
60 end
61 w = WorkflowTransition.where(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2).first
62 assert w
63 assert_equal false, w.assignee
64 assert_equal true, w.author
65 end
66
67 def test_replace_transitions_should_delete_disabled_additional_transitions
68 w = WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :assignee => true)
69
70 transitions = {'1' => {
71 '2' => {'always' => '0', 'assignee' => '0', 'author' => '0'}
72 }}
73 assert_difference 'WorkflowTransition.count', -1 do
74 WorkflowTransition.replace_transitions(Tracker.find(1), Role.find(1), transitions)
75 end
76 assert !WorkflowTransition.exists?(w.id)
77 end
78
79 def test_replace_transitions_should_update_additional_transitions
80 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :assignee => true)
81
82 transitions = {'1' => {
83 '2' => {'always' => '0', 'assignee' => '0', 'author' => '1'}
84 }}
85 assert_no_difference 'WorkflowTransition.count' do
86 WorkflowTransition.replace_transitions(Tracker.find(1), Role.find(1), transitions)
87 end
88 w = WorkflowTransition.where(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2).first
89 assert w
90 assert_equal false, w.assignee
91 assert_equal true, w.author
92 end
93 end
@@ -1,129 +1,140
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class WorkflowsController < ApplicationController
19 19 layout 'admin'
20 20
21 before_filter :require_admin, :find_roles, :find_trackers
21 before_filter :require_admin
22 22
23 23 def index
24 24 @workflow_counts = WorkflowTransition.count_by_tracker_and_role
25 25 end
26 26
27 27 def edit
28 @role = Role.find_by_id(params[:role_id]) if params[:role_id]
29 @tracker = Tracker.find_by_id(params[:tracker_id]) if params[:tracker_id]
28 find_trackers_roles_and_statuses_for_edit
30 29
31 if request.post?
32 WorkflowTransition.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
33 (params[:issue_status] || []).each { |status_id, transitions|
34 transitions.each { |new_status_id, options|
35 author = options.is_a?(Array) && options.include?('author') && !options.include?('always')
36 assignee = options.is_a?(Array) && options.include?('assignee') && !options.include?('always')
37 WorkflowTransition.create(:role_id => @role.id, :tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee)
38 }
39 }
40 if @role.save
41 flash[:notice] = l(:notice_successful_update)
42 redirect_to workflows_edit_path(:role_id => @role, :tracker_id => @tracker, :used_statuses_only => params[:used_statuses_only])
43 return
30 if request.post? && @roles && @trackers && params[:transitions]
31 transitions = params[:transitions].deep_dup
32 transitions.each do |old_status_id, transitions_by_new_status|
33 transitions_by_new_status.each do |new_status_id, transition_by_rule|
34 transition_by_rule.reject! {|rule, transition| transition == 'no_change'}
35 end
44 36 end
37 WorkflowTransition.replace_transitions(@trackers, @roles, transitions)
38 flash[:notice] = l(:notice_successful_update)
39 redirect_to_referer_or workflows_edit_path
40 return
45 41 end
46 42
47 @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
48 if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
49 @statuses = @tracker.issue_statuses
50 end
51 @statuses ||= IssueStatus.sorted.all
52
53 if @tracker && @role && @statuses.any?
54 workflows = WorkflowTransition.where(:role_id => @role.id, :tracker_id => @tracker.id).all
43 if @trackers && @roles && @statuses.any?
44 workflows = WorkflowTransition.where(:role_id => @roles.map(&:id), :tracker_id => @trackers.map(&:id)).all
55 45 @workflows = {}
56 46 @workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
57 47 @workflows['author'] = workflows.select {|w| w.author}
58 48 @workflows['assignee'] = workflows.select {|w| w.assignee}
59 49 end
60 50 end
61 51
62 52 def permissions
63 @role = Role.find_by_id(params[:role_id]) if params[:role_id]
64 @tracker = Tracker.find_by_id(params[:tracker_id]) if params[:tracker_id]
53 find_trackers_roles_and_statuses_for_edit
65 54
66 if request.post? && @role && @tracker
67 WorkflowPermission.replace_permissions(@tracker, @role, params[:permissions] || {})
55 if request.post? && @roles && @trackers && params[:permissions]
56 permissions = params[:permissions].deep_dup
57 permissions.each { |field, rule_by_status_id|
58 rule_by_status_id.reject! {|status_id, rule| rule == 'no_change'}
59 }
60 WorkflowPermission.replace_permissions(@trackers, @roles, permissions)
68 61 flash[:notice] = l(:notice_successful_update)
69 redirect_to workflows_permissions_path(:role_id => @role, :tracker_id => @tracker, :used_statuses_only => params[:used_statuses_only])
62 redirect_to_referer_or workflows_permissions_path
70 63 return
71 64 end
72 65
73 @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
74 if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
75 @statuses = @tracker.issue_statuses
76 end
77 @statuses ||= IssueStatus.sorted.all
78
79 if @role && @tracker
80 @fields = (Tracker::CORE_FIELDS_ALL - @tracker.disabled_core_fields).map {|field| [field, l("field_"+field.sub(/_id$/, ''))]}
81 @custom_fields = @tracker.custom_fields
82 @permissions = WorkflowPermission.
83 where(:tracker_id => @tracker.id, :role_id => @role.id).inject({}) do |h, w|
84 h[w.old_status_id] ||= {}
85 h[w.old_status_id][w.field_name] = w.rule
86 h
87 end
66 if @roles && @trackers
67 @fields = (Tracker::CORE_FIELDS_ALL - @trackers.map(&:disabled_core_fields).reduce(:&)).map {|field| [field, l("field_"+field.sub(/_id$/, ''))]}
68 @custom_fields = @trackers.map(&:custom_fields).flatten.uniq.sort
69 @permissions = WorkflowPermission.rules_by_status_id(@trackers, @roles)
88 70 @statuses.each {|status| @permissions[status.id] ||= {}}
89 71 end
90 72 end
91 73
92 74 def copy
75 @roles = Role.sorted
76 @trackers = Tracker.sorted
77
93 78 if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
94 79 @source_tracker = nil
95 80 else
96 81 @source_tracker = Tracker.find_by_id(params[:source_tracker_id].to_i)
97 82 end
98 83 if params[:source_role_id].blank? || params[:source_role_id] == 'any'
99 84 @source_role = nil
100 85 else
101 86 @source_role = Role.find_by_id(params[:source_role_id].to_i)
102 87 end
103 88 @target_trackers = params[:target_tracker_ids].blank? ?
104 89 nil : Tracker.where(:id => params[:target_tracker_ids]).all
105 90 @target_roles = params[:target_role_ids].blank? ?
106 91 nil : Role.where(:id => params[:target_role_ids]).all
107 92 if request.post?
108 93 if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?)
109 94 flash.now[:error] = l(:error_workflow_copy_source)
110 95 elsif @target_trackers.blank? || @target_roles.blank?
111 96 flash.now[:error] = l(:error_workflow_copy_target)
112 97 else
113 98 WorkflowRule.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
114 99 flash[:notice] = l(:notice_successful_update)
115 100 redirect_to workflows_copy_path(:source_tracker_id => @source_tracker, :source_role_id => @source_role)
116 101 end
117 102 end
118 103 end
119 104
120 105 private
121 106
107 def find_trackers_roles_and_statuses_for_edit
108 find_roles
109 find_trackers
110 find_statuses
111 end
112
122 113 def find_roles
123 @roles = Role.sorted.all
114 ids = Array.wrap(params[:role_id])
115 if ids == ['all']
116 @roles = Role.sorted.all
117 elsif ids.present?
118 @roles = Role.where(:id => ids).all
119 end
120 @roles = nil if @roles.blank?
124 121 end
125 122
126 123 def find_trackers
127 @trackers = Tracker.sorted.all
124 ids = Array.wrap(params[:tracker_id])
125 if ids == ['all']
126 @trackers = Tracker.sorted.all
127 elsif ids.present?
128 @trackers = Tracker.where(:id => ids).all
129 end
130 @trackers = nil if @trackers.blank?
131 end
132
133 def find_statuses
134 @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
135 if @trackers && @used_statuses_only
136 @statuses = @trackers.map(&:issue_statuses).flatten.uniq.sort.presence
137 end
138 @statuses ||= IssueStatus.sorted.all
128 139 end
129 140 end
@@ -1,41 +1,91
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module WorkflowsHelper
21 def options_for_workflow_select(name, objects, selected, options={})
22 option_tags = ''.html_safe
23 multiple = false
24 if selected
25 if selected.size == objects.size
26 selected = 'all'
27 else
28 selected = selected.map(&:id)
29 if selected.size > 1
30 multiple = true
31 end
32 end
33 else
34 selected = objects.first.try(:id)
35 end
36 all_tag_options = {:value => 'all', :selected => (selected == 'all')}
37 if multiple
38 all_tag_options.merge!(:style => "display:none;")
39 end
40 option_tags << content_tag('option', 'All', all_tag_options)
41 option_tags << options_from_collection_for_select(objects, "id", "name", selected)
42 select_tag name, option_tags, {:multiple => multiple}.merge(options)
43 end
44
21 45 def field_required?(field)
22 46 field.is_a?(CustomField) ? field.is_required? : %w(project_id tracker_id subject priority_id is_private).include?(field)
23 47 end
24 48
25 def field_permission_tag(permissions, status, field, role)
49 def field_permission_tag(permissions, status, field, roles)
26 50 name = field.is_a?(CustomField) ? field.id.to_s : field
27 51 options = [["", ""], [l(:label_readonly), "readonly"]]
28 52 options << [l(:label_required), "required"] unless field_required?(field)
29 53 html_options = {}
30 selected = permissions[status.id][name]
54
55 if perm = permissions[status.id][name]
56 if perm.uniq.size > 1 || perm.size < @roles.size * @trackers.size
57 options << [l(:label_no_change_option), "no_change"]
58 selected = 'no_change'
59 else
60 selected = perm.first
61 end
62 end
63
64 hidden = field.is_a?(CustomField) &&
65 !field.visible? &&
66 !roles.detect {|role| role.custom_fields.to_a.include?(field)}
31 67
32 hidden = field.is_a?(CustomField) && !field.visible? && !role.custom_fields.to_a.include?(field)
33 68 if hidden
34 69 options[0][0] = l(:label_hidden)
35 70 selected = ''
36 71 html_options[:disabled] = true
37 72 end
38 73
39 select_tag("permissions[#{name}][#{status.id}]", options_for_select(options, selected), html_options)
74 select_tag("permissions[#{status.id}][#{name}]", options_for_select(options, selected), html_options)
75 end
76
77 def transition_tag(workflows, old_status, new_status, name)
78 w = workflows.select {|w| w.old_status_id == old_status.id && w.new_status_id == new_status.id}.size
79
80 tag_name = "transitions[#{ old_status.id }][#{new_status.id}][#{name}]"
81 if w == 0 || w == @roles.size * @trackers.size
82
83 hidden_field_tag(tag_name, "0") +
84 check_box_tag(tag_name, "1", w != 0,
85 :class => "old-status-#{old_status.id} new-status-#{new_status.id}")
86 else
87 select_tag tag_name,
88 options_for_select([["Oui", "1"], ["Non", "0"], [l(:label_no_change_option), "no_change"]], "no_change")
89 end
40 90 end
41 91 end
@@ -1,45 +1,68
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class WorkflowPermission < WorkflowRule
19 19 validates_inclusion_of :rule, :in => %w(readonly required)
20 20 validate :validate_field_name
21 21
22 # Replaces the workflow permissions for the given tracker and role
22 # Returns the workflow permissions for the given trackers and roles
23 # grouped by status_id
23 24 #
24 25 # Example:
25 # WorkflowPermission.replace_permissions role, tracker, {'due_date' => {'1' => 'readonly', '2' => 'required'}}
26 def self.replace_permissions(tracker, role, permissions)
27 destroy_all(:tracker_id => tracker.id, :role_id => role.id)
26 # WorkflowPermission.rules_by_status_id trackers, roles
27 # # => {1 => {'start_date' => 'required', 'due_date' => 'readonly'}}
28 def self.rules_by_status_id(trackers, roles)
29 WorkflowPermission.where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id)).inject({}) do |h, w|
30 h[w.old_status_id] ||= {}
31 h[w.old_status_id][w.field_name] ||= []
32 h[w.old_status_id][w.field_name] << w.rule
33 h
34 end
35 end
28 36
29 permissions.each { |field, rule_by_status_id|
30 rule_by_status_id.each { |status_id, rule|
31 if rule.present?
32 WorkflowPermission.create(:role_id => role.id, :tracker_id => tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule)
33 end
37 # Replaces the workflow permissions for the given trackers and roles
38 #
39 # Example:
40 # WorkflowPermission.replace_permissions trackers, roles, {'1' => {'start_date' => 'required', 'due_date' => 'readonly'}}
41 def self.replace_permissions(trackers, roles, permissions)
42 trackers = Array.wrap trackers
43 roles = Array.wrap roles
44
45 transaction do
46 permissions.each { |status_id, rule_by_field|
47 rule_by_field.each { |field, rule|
48 destroy_all(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id), :old_status_id => status_id, :field_name => field)
49 if rule.present?
50 trackers.each do |tracker|
51 roles.each do |role|
52 WorkflowPermission.create(:role_id => role.id, :tracker_id => tracker.id, :old_status_id => status_id, :field_name => field, :rule => rule)
53 end
54 end
55 end
56 }
34 57 }
35 }
58 end
36 59 end
37 60
38 61 protected
39 62
40 63 def validate_field_name
41 64 unless Tracker::CORE_FIELDS_ALL.include?(field_name) || field_name.to_s.match(/^\d+$/)
42 65 errors.add :field_name, :invalid
43 66 end
44 67 end
45 68 end
@@ -1,39 +1,104
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class WorkflowTransition < WorkflowRule
19 19 validates_presence_of :new_status
20 20
21 21 # Returns workflow transitions count by tracker and role
22 22 def self.count_by_tracker_and_role
23 23 counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{table_name} WHERE type = 'WorkflowTransition' GROUP BY role_id, tracker_id")
24 24 roles = Role.sorted.all
25 25 trackers = Tracker.sorted.all
26 26
27 27 result = []
28 28 trackers.each do |tracker|
29 29 t = []
30 30 roles.each do |role|
31 31 row = counts.detect {|c| c['role_id'].to_s == role.id.to_s && c['tracker_id'].to_s == tracker.id.to_s}
32 32 t << [role, (row.nil? ? 0 : row['c'].to_i)]
33 33 end
34 34 result << [tracker, t]
35 35 end
36 36
37 37 result
38 38 end
39
40 def self.replace_transitions(trackers, roles, transitions)
41 trackers = Array.wrap trackers
42 roles = Array.wrap roles
43
44 transaction do
45 records = WorkflowTransition.where(:tracker_id => trackers.map(&:id), :role_id => roles.map(&:id)).all
46
47 transitions.each do |old_status_id, transitions_by_new_status|
48 transitions_by_new_status.each do |new_status_id, transition_by_rule|
49 transition_by_rule.each do |rule, transition|
50 trackers.each do |tracker|
51 roles.each do |role|
52 w = records.select {|r|
53 r.old_status_id == old_status_id.to_i &&
54 r.new_status_id == new_status_id.to_i &&
55 r.tracker_id == tracker.id &&
56 r.role_id == role.id &&
57 !r.destroyed?
58 }
59
60 if rule == 'always'
61 w = w.select {|r| !r.author && !r.assignee}
62 else
63 w = w.select {|r| r.author || r.assignee}
64 end
65 if w.size > 1
66 w[1..-1].each(&:destroy)
67 end
68 w = w.first
69
70 if transition == "1" || transition == true
71 unless w
72 w = WorkflowTransition.new(:old_status_id => old_status_id, :new_status_id => new_status_id, :tracker_id => tracker.id, :role_id => role.id)
73 records << w
74 end
75 w.author = true if rule == "author"
76 w.assignee = true if rule == "assignee"
77 w.save if w.changed?
78 elsif w
79 if rule == 'always'
80 w.destroy
81 elsif rule == 'author'
82 if w.assignee
83 w.author = false
84 w.save if w.changed?
85 else
86 w.destroy
87 end
88 elsif rule == 'assignee'
89 if w.author
90 w.assignee = false
91 w.save if w.changed?
92 else
93 w.destroy
94 end
95 end
96 end
97 end
98 end
99 end
100 end
101 end
102 end
103 end
39 104 end
@@ -1,41 +1,40
1 <table class="list transitions transitions-<%= name %>">
1 <table class="list workflows transitions transitions-<%= name %>">
2 2 <thead>
3 3 <tr>
4 4 <th>
5 5 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input')",
6 6 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
7 7 <%=l(:label_current_status)%>
8 8 </th>
9 9 <th colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
10 10 </tr>
11 11 <tr>
12 12 <td></td>
13 13 <% for new_status in @statuses %>
14 14 <td style="width:<%= 75 / @statuses.size %>%;">
15 15 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.new-status-#{new_status.id}')",
16 16 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
17 17 <%=h new_status.name %>
18 18 </td>
19 19 <% end %>
20 20 </tr>
21 21 </thead>
22 22 <tbody>
23 23 <% for old_status in @statuses %>
24 24 <tr class="<%= cycle("odd", "even") %>">
25 25 <td class="name">
26 26 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.old-status-#{old_status.id}')",
27 27 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
28 28
29 29 <%=h old_status.name %>
30 30 </td>
31 31 <% for new_status in @statuses -%>
32 32 <% checked = workflows.detect {|w| w.old_status_id == old_status.id && w.new_status_id == new_status.id} %>
33 33 <td class="<%= checked ? 'enabled' : '' %>">
34 <%= check_box_tag "issue_status[#{ old_status.id }][#{new_status.id}][]", name, checked,
35 :class => "old-status-#{old_status.id} new-status-#{new_status.id}" %>
34 <%= transition_tag workflows, old_status, new_status, name %>
36 35 </td>
37 36 <% end -%>
38 37 </tr>
39 38 <% end %>
40 39 </tbody>
41 40 </table>
@@ -1,56 +1,75
1 1 <%= render :partial => 'action_menu' %>
2 2
3 3 <%= title l(:label_workflow) %>
4 4
5 5 <div class="tabs">
6 6 <ul>
7 <li><%= link_to l(:label_status_transitions), {:action => 'edit', :role_id => @role, :tracker_id => @tracker}, :class => 'selected' %></li>
8 <li><%= link_to l(:label_fields_permissions), {:action => 'permissions', :role_id => @role, :tracker_id => @tracker} %></li>
7 <li><%= link_to l(:label_status_transitions), workflows_edit_path(:role_id => @roles, :tracker_id => @trackers), :class => 'selected' %></li>
8 <li><%= link_to l(:label_fields_permissions), workflows_permissions_path(:role_id => @roles, :tracker_id => @trackers) %></li>
9 9 </ul>
10 10 </div>
11 11
12 12 <p><%=l(:text_workflow_edit)%>:</p>
13 13
14 14 <%= form_tag({}, :method => 'get') do %>
15 15 <p>
16 16 <label><%=l(:label_role)%>:
17 <%= select_tag 'role_id', options_from_collection_for_select(@roles, "id", "name", @role && @role.id) %></label>
17 <%= options_for_workflow_select 'role_id[]', Role.sorted, @roles, :id => 'role_id', :class => 'expandable' %>
18 </label>
19 <a href="#" data-expands="#role_id"><%= image_tag 'bullet_toggle_plus.png' %></a>
18 20
19 21 <label><%=l(:label_tracker)%>:
20 <%= select_tag 'tracker_id', options_from_collection_for_select(@trackers, "id", "name", @tracker && @tracker.id) %></label>
22 <%= options_for_workflow_select 'tracker_id[]', Tracker.sorted, @trackers, :id => 'tracker_id', :class => 'expandable' %>
23 </label>
24 <a href="#" data-expands="#tracker_id"><%= image_tag 'bullet_toggle_plus.png' %></a>
21 25
22 26 <%= submit_tag l(:button_edit), :name => nil %>
23 27
24 28 <%= hidden_field_tag 'used_statuses_only', '0' %>
25 29 <label><%= check_box_tag 'used_statuses_only', '1', @used_statuses_only %> <%= l(:label_display_used_statuses_only) %></label>
26 30
27 31 </p>
28 32 <% end %>
29 33
30 <% if @tracker && @role && @statuses.any? %>
34 <% if @trackers && @roles && @statuses.any? %>
31 35 <%= form_tag({}, :id => 'workflow_form' ) do %>
32 <%= hidden_field_tag 'tracker_id', @tracker.id %>
33 <%= hidden_field_tag 'role_id', @role.id %>
36 <%= @trackers.map {|tracker| hidden_field_tag 'tracker_id[]', tracker.id}.join.html_safe %>
37 <%= @roles.map {|role| hidden_field_tag 'role_id[]', role.id}.join.html_safe %>
34 38 <%= hidden_field_tag 'used_statuses_only', params[:used_statuses_only] %>
35 39 <div class="autoscroll">
36 40 <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
37 41
38 42 <fieldset class="collapsible" style="padding: 0; margin-top: 0.5em;">
39 43 <legend onclick="toggleFieldset(this);"><%= l(:label_additional_workflow_transitions_for_author) %></legend>
40 44 <div id="author_workflows" style="margin: 0.5em 0 0.5em 0;">
41 45 <%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %>
42 46 </div>
43 47 </fieldset>
44 48 <%= javascript_tag "hideFieldset($('#author_workflows'))" unless @workflows['author'].present? %>
45 49
46 50 <fieldset class="collapsible" style="padding: 0;">
47 51 <legend onclick="toggleFieldset(this);"><%= l(:label_additional_workflow_transitions_for_assignee) %></legend>
48 52 <div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;">
49 53 <%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %>
50 54 </div>
51 55 </fieldset>
52 56 <%= javascript_tag "hideFieldset($('#assignee_workflows'))" unless @workflows['assignee'].present? %>
53 57 </div>
54 58 <%= submit_tag l(:button_save) %>
55 59 <% end %>
56 60 <% end %>
61
62 <%= javascript_tag do %>
63 $("a[data-expands]").click(function(e){
64 e.preventDefault();
65 var target = $($(this).attr("data-expands"));
66 if (target.attr("multiple")) {
67 target.attr("multiple", false);
68 target.find("option[value=all]").show();
69 } else {
70 target.attr("multiple", true);
71 target.find("option[value=all]").attr("selected", false).hide();
72 }
73 });
74
75 <% end %>
@@ -1,106 +1,123
1 1 <%= render :partial => 'action_menu' %>
2 2
3 3 <%= title l(:label_workflow) %>
4 4
5 5 <div class="tabs">
6 6 <ul>
7 <li><%= link_to l(:label_status_transitions), {:action => 'edit', :role_id => @role, :tracker_id => @tracker} %></li>
8 <li><%= link_to l(:label_fields_permissions), {:action => 'permissions', :role_id => @role, :tracker_id => @tracker}, :class => 'selected' %></li>
7 <li><%= link_to l(:label_status_transitions), workflows_edit_path(:role_id => @roles, :tracker_id => @trackers) %></li>
8 <li><%= link_to l(:label_fields_permissions), workflows_permissions_path(:role_id => @roles, :tracker_id => @trackers), :class => 'selected' %></li>
9 9 </ul>
10 10 </div>
11 11
12 12 <p><%=l(:text_workflow_edit)%>:</p>
13 13
14 14 <%= form_tag({}, :method => 'get') do %>
15 15 <p>
16 16 <label><%=l(:label_role)%>:
17 <%= select_tag 'role_id', options_from_collection_for_select(@roles, "id", "name", @role && @role.id) %></label>
17 <%= options_for_workflow_select 'role_id[]', Role.sorted, @roles, :id => 'role_id', :class => 'expandable' %>
18 </label>
19 <a href="#" data-expands="#role_id"><%= image_tag 'bullet_toggle_plus.png' %></a>
18 20
19 21 <label><%=l(:label_tracker)%>:
20 <%= select_tag 'tracker_id', options_from_collection_for_select(@trackers, "id", "name", @tracker && @tracker.id) %></label>
22 <%= options_for_workflow_select 'tracker_id[]', Tracker.sorted, @trackers, :id => 'tracker_id', :class => 'expandable' %>
23 </label>
24 <a href="#" data-expands="#tracker_id"><%= image_tag 'bullet_toggle_plus.png' %></a>
21 25
22 26 <%= submit_tag l(:button_edit), :name => nil %>
23 27
24 28 <%= hidden_field_tag 'used_statuses_only', '0' %>
25 29 <label><%= check_box_tag 'used_statuses_only', '1', @used_statuses_only %> <%= l(:label_display_used_statuses_only) %></label>
26 30 </p>
27 31 <% end %>
28 32
29 <% if @tracker && @role && @statuses.any? %>
33 <% if @trackers && @roles && @statuses.any? %>
30 34 <%= form_tag({}, :id => 'workflow_form' ) do %>
31 <%= hidden_field_tag 'tracker_id', @tracker.id %>
32 <%= hidden_field_tag 'role_id', @role.id %>
35 <%= @trackers.map {|tracker| hidden_field_tag 'tracker_id[]', tracker.id}.join.html_safe %>
36 <%= @roles.map {|role| hidden_field_tag 'role_id[]', role.id}.join.html_safe %>
33 37 <%= hidden_field_tag 'used_statuses_only', params[:used_statuses_only] %>
34 38 <div class="autoscroll">
35 <table class="list fields_permissions">
39 <table class="list workflows fields_permissions">
36 40 <thead>
37 41 <tr>
38 42 <th>
39 43 </th>
40 44 <th colspan="<%= @statuses.length %>"><%=l(:label_issue_status)%></th>
41 45 </tr>
42 46 <tr>
43 47 <td></td>
44 48 <% for status in @statuses %>
45 49 <td style="width:<%= 75 / @statuses.size %>%;">
46 50 <%=h status.name %>
47 51 </td>
48 52 <% end %>
49 53 </tr>
50 54 </thead>
51 55 <tbody>
52 56 <tr class="group open">
53 57 <td colspan="<%= @statuses.size + 1 %>">
54 58 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
55 59 <%= l(:field_core_fields) %>
56 60 </td>
57 61 </tr>
58 62 <% @fields.each do |field, name| %>
59 63 <tr class="<%= cycle("odd", "even") %>">
60 64 <td class="name">
61 65 <%=h name %> <%= content_tag('span', '*', :class => 'required') if field_required?(field) %>
62 66 </td>
63 67 <% for status in @statuses -%>
64 68 <td class="<%= @permissions[status.id][field] %>">
65 <%= field_permission_tag(@permissions, status, field, @role) %>
69 <%= field_permission_tag(@permissions, status, field, @roles) %>
66 70 <% unless status == @statuses.last %><a href="#" class="repeat-value">&#187;</a><% end %>
67 71 </td>
68 72 <% end -%>
69 73 </tr>
70 74 <% end %>
71 75 <% if @custom_fields.any? %>
72 76 <tr class="group open">
73 77 <td colspan="<%= @statuses.size + 1 %>">
74 78 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
75 79 <%= l(:label_custom_field_plural) %>
76 80 </td>
77 81 </tr>
78 82 <% @custom_fields.each do |field| %>
79 83 <tr class="<%= cycle("odd", "even") %>">
80 84 <td class="name">
81 85 <%=h field.name %> <%= content_tag('span', '*', :class => 'required') if field_required?(field) %>
82 86 </td>
83 87 <% for status in @statuses -%>
84 88 <td class="<%= @permissions[status.id][field.id.to_s] %>">
85 <%= field_permission_tag(@permissions, status, field, @role) %>
89 <%= field_permission_tag(@permissions, status, field, @roles) %>
86 90 <% unless status == @statuses.last %><a href="#" class="repeat-value">&#187;</a><% end %>
87 91 </td>
88 92 <% end -%>
89 93 </tr>
90 94 <% end %>
91 95 <% end %>
92 96 </tbody>
93 97 </table>
94 98 </div>
95 99 <%= submit_tag l(:button_save) %>
96 100 <% end %>
97 101 <% end %>
98 102
99 103 <%= javascript_tag do %>
100 104 $("a.repeat-value").click(function(e){
101 105 e.preventDefault();
102 106 var td = $(this).closest('td');
103 107 var selected = td.find("select").find(":selected").val();
104 108 td.nextAll('td').find("select").val(selected);
105 109 });
110
111 $("a[data-expands]").click(function(e){
112 e.preventDefault();
113 var target = $($(this).attr("data-expands"));
114 if (target.attr("multiple")) {
115 target.attr("multiple", false);
116 target.find("option[value=all]").show();
117 } else {
118 target.attr("multiple", true);
119 target.find("option[value=all]").attr("selected", false).hide();
120 }
121 });
122
106 123 <% end %>
@@ -1,1187 +1,1191
1 1 html {overflow-y:scroll;}
2 2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
3 3
4 4 h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;}
5 5 #content h1, h2, h3, h4 {color: #555;}
6 6 h2, .wiki h1 {font-size: 20px;}
7 7 h3, .wiki h2 {font-size: 16px;}
8 8 h4, .wiki h3 {font-size: 13px;}
9 9 h4 {border-bottom: 1px dotted #bbb;}
10 10
11 11 /***** Layout *****/
12 12 #wrapper {background: white;}
13 13
14 14 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
15 15 #top-menu ul {margin: 0; padding: 0;}
16 16 #top-menu li {
17 17 float:left;
18 18 list-style-type:none;
19 19 margin: 0px 0px 0px 0px;
20 20 padding: 0px 0px 0px 0px;
21 21 white-space:nowrap;
22 22 }
23 23 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
24 24 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
25 25
26 26 #account {float:right;}
27 27
28 28 #header {min-height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 20px 6px; position:relative;}
29 29 #header a {color:#f8f8f8;}
30 30 #header h1 a.ancestor { font-size: 80%; }
31 31 #quick-search {float:right;}
32 32
33 33 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
34 34 #main-menu ul {margin: 0; padding: 0;}
35 35 #main-menu li {
36 36 float:left;
37 37 list-style-type:none;
38 38 margin: 0px 2px 0px 0px;
39 39 padding: 0px 0px 0px 0px;
40 40 white-space:nowrap;
41 41 }
42 42 #main-menu li a {
43 43 display: block;
44 44 color: #fff;
45 45 text-decoration: none;
46 46 font-weight: bold;
47 47 margin: 0;
48 48 padding: 4px 10px 4px 10px;
49 49 }
50 50 #main-menu li a:hover {background:#759FCF; color:#fff;}
51 51 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
52 52
53 53 #admin-menu ul {margin: 0; padding: 0;}
54 54 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
55 55
56 56 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
57 57 #admin-menu a.projects { background-image: url(../images/projects.png); }
58 58 #admin-menu a.users { background-image: url(../images/user.png); }
59 59 #admin-menu a.groups { background-image: url(../images/group.png); }
60 60 #admin-menu a.roles { background-image: url(../images/database_key.png); }
61 61 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
62 62 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
63 63 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
64 64 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
65 65 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
66 66 #admin-menu a.settings { background-image: url(../images/changeset.png); }
67 67 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
68 68 #admin-menu a.info { background-image: url(../images/help.png); }
69 69 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
70 70
71 71 #main {background-color:#EEEEEE;}
72 72
73 73 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
74 74 * html #sidebar{ width: 22%; }
75 75 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
76 76 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
77 77 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
78 78 #sidebar .contextual { margin-right: 1em; }
79 79 #sidebar ul {margin: 0; padding: 0;}
80 80 #sidebar ul li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
81 81
82 82 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
83 83 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
84 84 html>body #content { min-height: 600px; }
85 85 * html body #content { height: 600px; } /* IE */
86 86
87 87 #main.nosidebar #sidebar{ display: none; }
88 88 #main.nosidebar #content{ width: auto; border-right: 0; }
89 89
90 90 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
91 91
92 92 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
93 93 #login-form table td {padding: 6px;}
94 94 #login-form label {font-weight: bold;}
95 95 #login-form input#username, #login-form input#password { width: 300px; }
96 96
97 97 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
98 98 div.modal h3.title {display:none;}
99 99 div.modal p.buttons {text-align:right; margin-bottom:0;}
100 100
101 101 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
102 102
103 103 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
104 104
105 105 /***** Links *****/
106 106 a, a:link, a:visited{ color: #169; text-decoration: none; }
107 107 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
108 108 a img{ border: 0; }
109 109
110 110 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
111 111 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
112 112 a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;}
113 113
114 114 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
115 115 #sidebar a.selected:hover {text-decoration:none;}
116 116 #admin-menu a {line-height:1.7em;}
117 117 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
118 118
119 119 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
120 120 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
121 121
122 122 a#toggle-completed-versions {color:#999;}
123 123 /***** Tables *****/
124 124 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
125 125 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
126 126 table.list td {text-align:center; vertical-align:top; padding-right:10px;}
127 127 table.list td.id { width: 2%; text-align: center;}
128 128 table.list td.name, table.list td.description, table.list td.subject, table.list td.comments, table.list td.roles {text-align: left;}
129 129 table.list td.tick {width:15%}
130 130 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
131 131 table.list td.checkbox input {padding:0px;}
132 132 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
133 133 table.list td.buttons a { padding-right: 0.6em; }
134 134 table.list td.reorder {width:15%; white-space:nowrap; text-align:center; }
135 135 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
136 136
137 137 tr.project td.name a { white-space:nowrap; }
138 138 tr.project.closed, tr.project.archived { color: #aaa; }
139 139 tr.project.closed a, tr.project.archived a { color: #aaa; }
140 140
141 141 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
142 142 tr.project.idnt-1 td.name {padding-left: 0.5em;}
143 143 tr.project.idnt-2 td.name {padding-left: 2em;}
144 144 tr.project.idnt-3 td.name {padding-left: 3.5em;}
145 145 tr.project.idnt-4 td.name {padding-left: 5em;}
146 146 tr.project.idnt-5 td.name {padding-left: 6.5em;}
147 147 tr.project.idnt-6 td.name {padding-left: 8em;}
148 148 tr.project.idnt-7 td.name {padding-left: 9.5em;}
149 149 tr.project.idnt-8 td.name {padding-left: 11em;}
150 150 tr.project.idnt-9 td.name {padding-left: 12.5em;}
151 151
152 152 tr.issue { text-align: center; white-space: nowrap; }
153 153 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations { white-space: normal; }
154 154 tr.issue td.relations { text-align: left; }
155 155 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
156 156 tr.issue td.relations span {white-space: nowrap;}
157 157 table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;}
158 158 table.issues td.description pre {white-space:normal;}
159 159
160 160 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
161 161 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
162 162 tr.issue.idnt-2 td.subject {padding-left: 2em;}
163 163 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
164 164 tr.issue.idnt-4 td.subject {padding-left: 5em;}
165 165 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
166 166 tr.issue.idnt-6 td.subject {padding-left: 8em;}
167 167 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
168 168 tr.issue.idnt-8 td.subject {padding-left: 11em;}
169 169 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
170 170
171 171 table.issue-report {table-layout:fixed;}
172 172
173 173 tr.entry { border: 1px solid #f8f8f8; }
174 174 tr.entry td { white-space: nowrap; }
175 175 tr.entry td.filename {width:30%; text-align:left;}
176 176 tr.entry td.filename_no_report {width:70%; text-align:left;}
177 177 tr.entry td.size { text-align: right; font-size: 90%; }
178 178 tr.entry td.revision, tr.entry td.author { text-align: center; }
179 179 tr.entry td.age { text-align: right; }
180 180 tr.entry.file td.filename a { margin-left: 16px; }
181 181 tr.entry.file td.filename_no_report a { margin-left: 16px; }
182 182
183 183 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
184 184 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
185 185
186 186 tr.changeset { height: 20px }
187 187 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
188 188 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
189 189 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
190 190 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
191 191
192 192 table.files tbody th {text-align:left;}
193 193 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
194 194 table.files tr.file td.digest { font-size: 80%; }
195 195
196 196 table.members td.roles, table.memberships td.roles { width: 45%; }
197 197
198 198 tr.message { height: 2.6em; }
199 199 tr.message td.subject { padding-left: 20px; }
200 200 tr.message td.created_on { white-space: nowrap; }
201 201 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
202 202 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
203 203 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
204 204
205 205 tr.version.closed, tr.version.closed a { color: #999; }
206 206 tr.version td.name { padding-left: 20px; }
207 207 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
208 208 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
209 209
210 210 tr.user td {width:13%;white-space: nowrap;}
211 211 tr.user td.username, tr.user td.firstname, tr.user td.lastname, tr.user td.email {text-align:left;}
212 212 tr.user td.email { width:18%; }
213 213 tr.user.locked, tr.user.registered { color: #aaa; }
214 214 tr.user.locked a, tr.user.registered a { color: #aaa; }
215 215
216 216 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
217 217
218 218 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
219 219
220 220 tr.time-entry { text-align: center; white-space: nowrap; }
221 221 tr.time-entry td.issue, tr.time-entry td.comments { text-align: left; white-space: normal; }
222 222 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
223 223 td.hours .hours-dec { font-size: 0.9em; }
224 224
225 225 table.plugins td { vertical-align: middle; }
226 226 table.plugins td.configure { text-align: right; padding-right: 1em; }
227 227 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
228 228 table.plugins span.description { display: block; font-size: 0.9em; }
229 229 table.plugins span.url { display: block; font-size: 0.9em; }
230 230
231 231 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; text-align:left; }
232 232 table.list tbody tr.group span.count {position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
233 233 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
234 234 tr.group:hover a.toggle-all { display:inline;}
235 235 a.toggle-all:hover {text-decoration:none;}
236 236
237 237 table.list tbody tr:hover { background-color:#ffffdd; }
238 238 table.list tbody tr.group:hover { background-color:inherit; }
239 239 table td {padding:2px;}
240 240 table p {margin:0;}
241 241 .odd {background-color:#f6f7f8;}
242 242 .even {background-color: #fff;}
243 243
244 244 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
245 245 a.sort.asc { background-image: url(../images/sort_asc.png); }
246 246 a.sort.desc { background-image: url(../images/sort_desc.png); }
247 247
248 248 table.attributes { width: 100% }
249 249 table.attributes th { vertical-align: top; text-align: left; }
250 250 table.attributes td { vertical-align: top; }
251 251
252 252 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
253 253 table.boards td.last-message {text-align:left;font-size:80%;}
254 254
255 255 table.messages td.last_message {text-align:left;}
256 256
257 #query_form_content {font-size:90%;}
258
257 259 table.query-columns {
258 260 border-collapse: collapse;
259 261 border: 0;
260 262 }
261 263
262 264 table.query-columns td.buttons {
263 265 vertical-align: middle;
264 266 text-align: center;
265 267 }
266 268
267 269 td.center {text-align:center;}
268 270
269 271 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
270 272
271 273 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
272 274 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
273 275 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
274 276 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
275 277
276 278 #watchers select {width: 95%; display: block;}
277 279 #watchers a.delete {opacity: 0.4; vertical-align: middle;}
278 280 #watchers a.delete:hover {opacity: 1;}
279 281 #watchers img.gravatar {margin: 0 4px 2px 0;}
280 282
281 283 span#watchers_inputs {overflow:auto; display:block;}
282 284 span.search_for_watchers {display:block;}
283 285 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
284 286 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
285 287
286 288
287 289 .highlight { background-color: #FCFD8D;}
288 290 .highlight.token-1 { background-color: #faa;}
289 291 .highlight.token-2 { background-color: #afa;}
290 292 .highlight.token-3 { background-color: #aaf;}
291 293
292 294 .box{
293 295 padding:6px;
294 296 margin-bottom: 10px;
295 297 background-color:#f6f6f6;
296 298 color:#505050;
297 299 line-height:1.5em;
298 300 border: 1px solid #e4e4e4;
299 301 }
300 302
301 303 div.square {
302 304 border: 1px solid #999;
303 305 float: left;
304 306 margin: .3em .4em 0 .4em;
305 307 overflow: hidden;
306 308 width: .6em; height: .6em;
307 309 }
308 310 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
309 311 .contextual input, .contextual select {font-size:0.9em;}
310 312 .message .contextual { margin-top: 0; }
311 313
312 314 .splitcontent {overflow:auto;}
313 315 .splitcontentleft{float:left; width:49%;}
314 316 .splitcontentright{float:right; width:49%;}
315 317 form {display: inline;}
316 318 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
317 319 fieldset {border: 1px solid #e4e4e4; margin:0;}
318 320 legend {color: #484848;}
319 321 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
320 322 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
321 323 blockquote blockquote { margin-left: 0;}
322 324 abbr { border-bottom: 1px dotted; cursor: help; }
323 325 textarea.wiki-edit {width:99%; resize:vertical;}
324 326 li p {margin-top: 0;}
325 327 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
326 328 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
327 329 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
328 330 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
329 331
330 332 div.issue div.subject div div { padding-left: 16px; }
331 333 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
332 334 div.issue div.subject>div>p { margin-top: 0.5em; }
333 335 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
334 336 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;}
335 337 div.issue .next-prev-links {color:#999;}
336 338 div.issue table.attributes th {width:22%;}
337 339 div.issue table.attributes td {width:28%;}
338 340
339 341 #issue_tree table.issues, #relations table.issues { border: 0; }
340 342 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
341 343 #relations td.buttons {padding:0;}
342 344
343 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
345 fieldset.collapsible {border-width: 1px 0 0 0;}
344 346 fieldset.collapsible>legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
345 347 fieldset.collapsible.collapsed>legend { background-image: url(../images/arrow_collapsed.png); }
346 348
347 349 fieldset#date-range p { margin: 2px 0 2px 0; }
348 350 fieldset#filters table { border-collapse: collapse; }
349 351 fieldset#filters table td { padding: 0; vertical-align: middle; }
350 352 fieldset#filters tr.filter { height: 2.1em; }
351 353 fieldset#filters td.field { width:230px; }
352 354 fieldset#filters td.operator { width:180px; }
353 355 fieldset#filters td.operator select {max-width:170px;}
354 356 fieldset#filters td.values { white-space:nowrap; }
355 357 fieldset#filters td.values select {min-width:130px;}
356 358 fieldset#filters td.values input {height:1em;}
357 359 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
358 360
359 361 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
360 362 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
361 363
362 364 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
363 365 div#issue-changesets div.changeset { padding: 4px;}
364 366 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
365 367 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
366 368
367 369 .journal ul.details img {margin:0 0 -3px 4px;}
368 370 div.journal {overflow:auto;}
369 371 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
370 372
371 373 div#activity dl, #search-results { margin-left: 2em; }
372 374 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
373 375 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
374 376 div#activity dt.me .time { border-bottom: 1px solid #999; }
375 377 div#activity dt .time { color: #777; font-size: 80%; }
376 378 div#activity dd .description, #search-results dd .description { font-style: italic; }
377 379 div#activity span.project:after, #search-results span.project:after { content: " -"; }
378 380 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
379 381 div#activity dt.grouped {margin-left:5em;}
380 382 div#activity dd.grouped {margin-left:9em;}
381 383
382 384 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
383 385
384 386 div#search-results-counts {float:right;}
385 387 div#search-results-counts ul { margin-top: 0.5em; }
386 388 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
387 389
388 390 dt.issue { background-image: url(../images/ticket.png); }
389 391 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
390 392 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
391 393 dt.issue-note { background-image: url(../images/ticket_note.png); }
392 394 dt.changeset { background-image: url(../images/changeset.png); }
393 395 dt.news { background-image: url(../images/news.png); }
394 396 dt.message { background-image: url(../images/message.png); }
395 397 dt.reply { background-image: url(../images/comments.png); }
396 398 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
397 399 dt.attachment { background-image: url(../images/attachment.png); }
398 400 dt.document { background-image: url(../images/document.png); }
399 401 dt.project { background-image: url(../images/projects.png); }
400 402 dt.time-entry { background-image: url(../images/time.png); }
401 403
402 404 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
403 405
404 406 div#roadmap .related-issues { margin-bottom: 1em; }
405 407 div#roadmap .related-issues td.checkbox { display: none; }
406 408 div#roadmap .wiki h1:first-child { display: none; }
407 409 div#roadmap .wiki h1 { font-size: 120%; }
408 410 div#roadmap .wiki h2 { font-size: 110%; }
409 411 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
410 412
411 413 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
412 414 div#version-summary fieldset { margin-bottom: 1em; }
413 415 div#version-summary fieldset.time-tracking table { width:100%; }
414 416 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
415 417
416 418 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
417 419 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
418 420 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
419 421 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
420 422 table#time-report .hours-dec { font-size: 0.9em; }
421 423
422 424 div.wiki-page .contextual a {opacity: 0.4}
423 425 div.wiki-page .contextual a:hover {opacity: 1}
424 426
425 427 form .attributes select { width: 60%; }
426 428 input#issue_subject { width: 99%; }
427 429 select#issue_done_ratio { width: 95px; }
428 430
429 431 ul.projects {margin:0; padding-left:1em;}
430 432 ul.projects ul {padding-left:1.6em;}
431 433 ul.projects.root {margin:0; padding:0;}
432 434 ul.projects li {list-style-type:none;}
433 435
434 436 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
435 437 #projects-index ul.projects li.root {margin-bottom: 1em;}
436 438 #projects-index ul.projects li.child {margin-top: 1em;}
437 439 #projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
438 440 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
439 441
440 442 #notified-projects>ul, #tracker_project_ids>ul, #custom_field_project_ids>ul {max-height:250px; overflow-y:auto;}
441 443
442 444 #related-issues li img {vertical-align:middle;}
443 445
444 446 ul.properties {padding:0; font-size: 0.9em; color: #777;}
445 447 ul.properties li {list-style-type:none;}
446 448 ul.properties li span {font-style:italic;}
447 449
448 450 .total-hours { font-size: 110%; font-weight: bold; }
449 451 .total-hours span.hours-int { font-size: 120%; }
450 452
451 453 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
452 454 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
453 455
454 456 #workflow_copy_form select { width: 200px; }
455 457 table.transitions td.enabled {background: #bfb;}
456 table.fields_permissions select {font-size:90%}
458 #workflow_form table select {font-size:90%; max-width:100px;}
457 459 table.fields_permissions td.readonly {background:#ddd;}
458 460 table.fields_permissions td.required {background:#d88;}
459 461
462 select.expandable {vertical-align:top;}
463
460 464 textarea#custom_field_possible_values {width: 95%; resize:vertical}
461 465 textarea#custom_field_default_value {width: 95%; resize:vertical}
462 466
463 467 input#content_comments {width: 99%}
464 468
465 469 p.pagination {margin-top:8px; font-size: 90%}
466 470
467 471 /***** Tabular forms ******/
468 472 .tabular p{
469 473 margin: 0;
470 474 padding: 3px 0 3px 0;
471 475 padding-left: 180px; /* width of left column containing the label elements */
472 476 min-height: 1.8em;
473 477 clear:left;
474 478 }
475 479
476 480 html>body .tabular p {overflow:hidden;}
477 481
478 482 .tabular input, .tabular select {max-width:95%}
479 483 .tabular textarea {width:95%; resize:vertical;}
480 484 .tabular span[title] {border-bottom:1px dotted #aaa;}
481 485
482 486 .tabular label{
483 487 font-weight: bold;
484 488 float: left;
485 489 text-align: right;
486 490 /* width of left column */
487 491 margin-left: -180px;
488 492 /* width of labels. Should be smaller than left column to create some right margin */
489 493 width: 175px;
490 494 }
491 495
492 496 .tabular label.floating{
493 497 font-weight: normal;
494 498 margin-left: 0px;
495 499 text-align: left;
496 500 width: 270px;
497 501 }
498 502
499 503 .tabular label.block{
500 504 font-weight: normal;
501 505 margin-left: 0px !important;
502 506 text-align: left;
503 507 float: none;
504 508 display: block;
505 509 width: auto !important;
506 510 }
507 511
508 512 .tabular label.inline{
509 513 font-weight: normal;
510 514 float:none;
511 515 margin-left: 5px !important;
512 516 width: auto;
513 517 }
514 518
515 519 label.no-css {
516 520 font-weight: inherit;
517 521 float:none;
518 522 text-align:left;
519 523 margin-left:0px;
520 524 width:auto;
521 525 }
522 526 input#time_entry_comments { width: 90%;}
523 527
524 528 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
525 529
526 530 .tabular.settings p{ padding-left: 300px; }
527 531 .tabular.settings label{ margin-left: -300px; width: 295px; }
528 532 .tabular.settings textarea { width: 99%; }
529 533
530 534 .settings.enabled_scm table {width:100%}
531 535 .settings.enabled_scm td.scm_name{ font-weight: bold; }
532 536
533 537 fieldset.settings label { display: block; }
534 538 fieldset#notified_events .parent { padding-left: 20px; }
535 539
536 540 span.required {color: #bb0000;}
537 541 .summary {font-style: italic;}
538 542
539 543 .check_box_group {
540 544 display:block;
541 545 width:95%;
542 546 max-height:300px;
543 547 overflow-y:auto;
544 548 padding:2px 4px 4px 2px;
545 549 background:#fff;
546 550 border:1px solid #9EB1C2;
547 551 border-radius:2px
548 552 }
549 553 .check_box_group label {
550 554 font-weight: normal;
551 555 margin-left: 0px !important;
552 556 text-align: left;
553 557 float: none;
554 558 display: block;
555 559 width: auto;
556 560 }
557 561 .check_box_group.bool_cf {border:0; background:inherit;}
558 562 .check_box_group.bool_cf label {display: inline;}
559 563
560 564 #attachments_fields input.description {margin-left:4px; width:340px;}
561 565 #attachments_fields span {display:block; white-space:nowrap;}
562 566 #attachments_fields input.filename {border:0; height:1.8em; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
563 567 #attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
564 568 #attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
565 569 #attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
566 570 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
567 571 a.remove-upload:hover {text-decoration:none !important;}
568 572
569 573 div.fileover { background-color: lavender; }
570 574
571 575 div.attachments { margin-top: 12px; }
572 576 div.attachments p { margin:4px 0 2px 0; }
573 577 div.attachments img { vertical-align: middle; }
574 578 div.attachments span.author { font-size: 0.9em; color: #888; }
575 579
576 580 div.thumbnails {margin-top:0.6em;}
577 581 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
578 582 div.thumbnails img {margin: 3px;}
579 583
580 584 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
581 585 .other-formats span + span:before { content: "| "; }
582 586
583 587 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
584 588
585 589 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
586 590 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
587 591
588 592 textarea.text_cf {width:95%; resize:vertical;}
589 593 input.string_cf, input.link_cf {width:95%;}
590 594 select.bool_cf {width:auto !important;}
591 595
592 596 #tab-content-modules fieldset p {margin:3px 0 4px 0;}
593 597
594 598 #tab-content-members .splitcontentleft, #tab-content-memberships .splitcontentleft, #tab-content-users .splitcontentleft {width: 64%;}
595 599 #tab-content-members .splitcontentright, #tab-content-memberships .splitcontentright, #tab-content-users .splitcontentright {width: 34%;}
596 600 #tab-content-members fieldset, #tab-content-memberships fieldset, #tab-content-users fieldset {padding:1em; margin-bottom: 1em;}
597 601 #tab-content-members fieldset legend, #tab-content-memberships fieldset legend, #tab-content-users fieldset legend {font-weight: bold;}
598 602 #tab-content-members fieldset label, #tab-content-memberships fieldset label, #tab-content-users fieldset label {display: block;}
599 603 #tab-content-members #principals, #tab-content-users #principals {max-height: 400px; overflow: auto;}
600 604
601 605 #tab-content-memberships .splitcontentright select {width:90%}
602 606
603 607 #users_for_watcher {height: 200px; overflow:auto;}
604 608 #users_for_watcher label {display: block;}
605 609
606 610 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
607 611
608 612 input#principal_search, input#user_search {width:90%}
609 613
610 614 input.autocomplete {
611 615 background: #fff url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px !important;
612 616 border:1px solid #9EB1C2; border-radius:2px; height:1.5em;
613 617 }
614 618 input.autocomplete.ajax-loading {
615 619 background-image: url(../images/loading.gif);
616 620 }
617 621
618 622 .role-visibility {padding-left:2em;}
619 623
620 624 /***** Flash & error messages ****/
621 625 #errorExplanation, div.flash, .nodata, .warning, .conflict {
622 626 padding: 4px 4px 4px 30px;
623 627 margin-bottom: 12px;
624 628 font-size: 1.1em;
625 629 border: 2px solid;
626 630 }
627 631
628 632 div.flash {margin-top: 8px;}
629 633
630 634 div.flash.error, #errorExplanation {
631 635 background: url(../images/exclamation.png) 8px 50% no-repeat;
632 636 background-color: #ffe3e3;
633 637 border-color: #dd0000;
634 638 color: #880000;
635 639 }
636 640
637 641 div.flash.notice {
638 642 background: url(../images/true.png) 8px 5px no-repeat;
639 643 background-color: #dfffdf;
640 644 border-color: #9fcf9f;
641 645 color: #005f00;
642 646 }
643 647
644 648 div.flash.warning, .conflict {
645 649 background: url(../images/warning.png) 8px 5px no-repeat;
646 650 background-color: #FFEBC1;
647 651 border-color: #FDBF3B;
648 652 color: #A6750C;
649 653 text-align: left;
650 654 }
651 655
652 656 .nodata, .warning {
653 657 text-align: center;
654 658 background-color: #FFEBC1;
655 659 border-color: #FDBF3B;
656 660 color: #A6750C;
657 661 }
658 662
659 663 #errorExplanation ul { font-size: 0.9em;}
660 664 #errorExplanation h2, #errorExplanation p { display: none; }
661 665
662 666 .conflict-details {font-size:80%;}
663 667
664 668 /***** Ajax indicator ******/
665 669 #ajax-indicator {
666 670 position: absolute; /* fixed not supported by IE */
667 671 background-color:#eee;
668 672 border: 1px solid #bbb;
669 673 top:35%;
670 674 left:40%;
671 675 width:20%;
672 676 font-weight:bold;
673 677 text-align:center;
674 678 padding:0.6em;
675 679 z-index:100;
676 680 opacity: 0.5;
677 681 }
678 682
679 683 html>body #ajax-indicator { position: fixed; }
680 684
681 685 #ajax-indicator span {
682 686 background-position: 0% 40%;
683 687 background-repeat: no-repeat;
684 688 background-image: url(../images/loading.gif);
685 689 padding-left: 26px;
686 690 vertical-align: bottom;
687 691 }
688 692
689 693 /***** Calendar *****/
690 694 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
691 695 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
692 696 table.cal thead th.week-number {width: auto;}
693 697 table.cal tbody tr {height: 100px;}
694 698 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
695 699 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
696 700 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
697 701 table.cal td.odd p.day-num {color: #bbb;}
698 702 table.cal td.today {background:#ffffdd;}
699 703 table.cal td.today p.day-num {font-weight: bold;}
700 704 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
701 705 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
702 706 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
703 707 p.cal.legend span {display:block;}
704 708
705 709 /***** Tooltips ******/
706 710 .tooltip{position:relative;z-index:24;}
707 711 .tooltip:hover{z-index:25;color:#000;}
708 712 .tooltip span.tip{display: none; text-align:left;}
709 713
710 714 div.tooltip:hover span.tip{
711 715 display:block;
712 716 position:absolute;
713 717 top:12px; left:24px; width:270px;
714 718 border:1px solid #555;
715 719 background-color:#fff;
716 720 padding: 4px;
717 721 font-size: 0.8em;
718 722 color:#505050;
719 723 }
720 724
721 725 img.ui-datepicker-trigger {
722 726 cursor: pointer;
723 727 vertical-align: middle;
724 728 margin-left: 4px;
725 729 }
726 730
727 731 /***** Progress bar *****/
728 732 table.progress {
729 733 border-collapse: collapse;
730 734 border-spacing: 0pt;
731 735 empty-cells: show;
732 736 text-align: center;
733 737 float:left;
734 738 margin: 1px 6px 1px 0px;
735 739 }
736 740
737 741 table.progress td { height: 1em; }
738 742 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
739 743 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
740 744 table.progress td.todo { background: #eee none repeat scroll 0%; }
741 745 p.percent {font-size: 80%;}
742 746 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
743 747
744 748 #roadmap table.progress td { height: 1.2em; }
745 749 /***** Tabs *****/
746 750 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
747 751 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
748 752 #content .tabs ul li {
749 753 float:left;
750 754 list-style-type:none;
751 755 white-space:nowrap;
752 756 margin-right:4px;
753 757 background:#fff;
754 758 position:relative;
755 759 margin-bottom:-1px;
756 760 }
757 761 #content .tabs ul li a{
758 762 display:block;
759 763 font-size: 0.9em;
760 764 text-decoration:none;
761 765 line-height:1.3em;
762 766 padding:4px 6px 4px 6px;
763 767 border: 1px solid #ccc;
764 768 border-bottom: 1px solid #bbbbbb;
765 769 background-color: #f6f6f6;
766 770 color:#999;
767 771 font-weight:bold;
768 772 border-top-left-radius:3px;
769 773 border-top-right-radius:3px;
770 774 }
771 775
772 776 #content .tabs ul li a:hover {
773 777 background-color: #ffffdd;
774 778 text-decoration:none;
775 779 }
776 780
777 781 #content .tabs ul li a.selected {
778 782 background-color: #fff;
779 783 border: 1px solid #bbbbbb;
780 784 border-bottom: 1px solid #fff;
781 785 color:#444;
782 786 }
783 787
784 788 #content .tabs ul li a.selected:hover {background-color: #fff;}
785 789
786 790 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
787 791
788 792 button.tab-left, button.tab-right {
789 793 font-size: 0.9em;
790 794 cursor: pointer;
791 795 height:24px;
792 796 border: 1px solid #ccc;
793 797 border-bottom: 1px solid #bbbbbb;
794 798 position:absolute;
795 799 padding:4px;
796 800 width: 20px;
797 801 bottom: -1px;
798 802 }
799 803
800 804 button.tab-left {
801 805 right: 20px;
802 806 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
803 807 border-top-left-radius:3px;
804 808 }
805 809
806 810 button.tab-right {
807 811 right: 0;
808 812 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
809 813 border-top-right-radius:3px;
810 814 }
811 815
812 816 /***** Diff *****/
813 817 .diff_out { background: #fcc; }
814 818 .diff_out span { background: #faa; }
815 819 .diff_in { background: #cfc; }
816 820 .diff_in span { background: #afa; }
817 821
818 822 .text-diff {
819 823 padding: 1em;
820 824 background-color:#f6f6f6;
821 825 color:#505050;
822 826 border: 1px solid #e4e4e4;
823 827 }
824 828
825 829 /***** Wiki *****/
826 830 div.wiki table {
827 831 border-collapse: collapse;
828 832 margin-bottom: 1em;
829 833 }
830 834
831 835 div.wiki table, div.wiki td, div.wiki th {
832 836 border: 1px solid #bbb;
833 837 padding: 4px;
834 838 }
835 839
836 840 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
837 841
838 842 div.wiki .external {
839 843 background-position: 0% 60%;
840 844 background-repeat: no-repeat;
841 845 padding-left: 12px;
842 846 background-image: url(../images/external.png);
843 847 }
844 848
845 849 div.wiki a.new {color: #b73535;}
846 850
847 851 div.wiki ul, div.wiki ol {margin-bottom:1em;}
848 852
849 853 div.wiki pre {
850 854 margin: 1em 1em 1em 1.6em;
851 855 padding: 8px;
852 856 background-color: #fafafa;
853 857 border: 1px solid #e2e2e2;
854 858 width:auto;
855 859 overflow-x: auto;
856 860 overflow-y: hidden;
857 861 }
858 862
859 863 div.wiki ul.toc {
860 864 background-color: #ffffdd;
861 865 border: 1px solid #e4e4e4;
862 866 padding: 4px;
863 867 line-height: 1.2em;
864 868 margin-bottom: 12px;
865 869 margin-right: 12px;
866 870 margin-left: 0;
867 871 display: table
868 872 }
869 873 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
870 874
871 875 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
872 876 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
873 877 div.wiki ul.toc ul { margin: 0; padding: 0; }
874 878 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
875 879 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
876 880 div.wiki ul.toc a {
877 881 font-size: 0.9em;
878 882 font-weight: normal;
879 883 text-decoration: none;
880 884 color: #606060;
881 885 }
882 886 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
883 887
884 888 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
885 889 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
886 890 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
887 891
888 892 div.wiki img {vertical-align:middle; max-width:100%;}
889 893
890 894 /***** My page layout *****/
891 895 .block-receiver {
892 896 border:1px dashed #c0c0c0;
893 897 margin-bottom: 20px;
894 898 padding: 15px 0 15px 0;
895 899 }
896 900
897 901 .mypage-box {
898 902 margin:0 0 20px 0;
899 903 color:#505050;
900 904 line-height:1.5em;
901 905 }
902 906
903 907 .handle {cursor: move;}
904 908
905 909 a.close-icon {
906 910 display:block;
907 911 margin-top:3px;
908 912 overflow:hidden;
909 913 width:12px;
910 914 height:12px;
911 915 background-repeat: no-repeat;
912 916 cursor:pointer;
913 917 background-image:url('../images/close.png');
914 918 }
915 919 a.close-icon:hover {background-image:url('../images/close_hl.png');}
916 920
917 921 /***** Gantt chart *****/
918 922 .gantt_hdr {
919 923 position:absolute;
920 924 top:0;
921 925 height:16px;
922 926 border-top: 1px solid #c0c0c0;
923 927 border-bottom: 1px solid #c0c0c0;
924 928 border-right: 1px solid #c0c0c0;
925 929 text-align: center;
926 930 overflow: hidden;
927 931 }
928 932
929 933 .gantt_hdr.nwday {background-color:#f1f1f1;}
930 934
931 935 .gantt_subjects { font-size: 0.8em; }
932 936 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
933 937
934 938 .task {
935 939 position: absolute;
936 940 height:8px;
937 941 font-size:0.8em;
938 942 color:#888;
939 943 padding:0;
940 944 margin:0;
941 945 line-height:16px;
942 946 white-space:nowrap;
943 947 }
944 948
945 949 .task.label {width:100%;}
946 950 .task.label.project, .task.label.version { font-weight: bold; }
947 951
948 952 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
949 953 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
950 954 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
951 955
952 956 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
953 957 .task_late.parent, .task_done.parent { height: 3px;}
954 958 .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;}
955 959 .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;}
956 960
957 961 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
958 962 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
959 963 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
960 964 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
961 965
962 966 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
963 967 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
964 968 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
965 969 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
966 970
967 971 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
968 972 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
969 973
970 974 /***** Icons *****/
971 975 .icon {
972 976 background-position: 0% 50%;
973 977 background-repeat: no-repeat;
974 978 padding-left: 20px;
975 979 padding-top: 2px;
976 980 padding-bottom: 3px;
977 981 }
978 982
979 983 .icon-add { background-image: url(../images/add.png); }
980 984 .icon-edit { background-image: url(../images/edit.png); }
981 985 .icon-copy { background-image: url(../images/copy.png); }
982 986 .icon-duplicate { background-image: url(../images/duplicate.png); }
983 987 .icon-del { background-image: url(../images/delete.png); }
984 988 .icon-move { background-image: url(../images/move.png); }
985 989 .icon-save { background-image: url(../images/save.png); }
986 990 .icon-cancel { background-image: url(../images/cancel.png); }
987 991 .icon-multiple { background-image: url(../images/table_multiple.png); }
988 992 .icon-folder { background-image: url(../images/folder.png); }
989 993 .open .icon-folder { background-image: url(../images/folder_open.png); }
990 994 .icon-package { background-image: url(../images/package.png); }
991 995 .icon-user { background-image: url(../images/user.png); }
992 996 .icon-projects { background-image: url(../images/projects.png); }
993 997 .icon-help { background-image: url(../images/help.png); }
994 998 .icon-attachment { background-image: url(../images/attachment.png); }
995 999 .icon-history { background-image: url(../images/history.png); }
996 1000 .icon-time { background-image: url(../images/time.png); }
997 1001 .icon-time-add { background-image: url(../images/time_add.png); }
998 1002 .icon-stats { background-image: url(../images/stats.png); }
999 1003 .icon-warning { background-image: url(../images/warning.png); }
1000 1004 .icon-fav { background-image: url(../images/fav.png); }
1001 1005 .icon-fav-off { background-image: url(../images/fav_off.png); }
1002 1006 .icon-reload { background-image: url(../images/reload.png); }
1003 1007 .icon-lock { background-image: url(../images/locked.png); }
1004 1008 .icon-unlock { background-image: url(../images/unlock.png); }
1005 1009 .icon-checked { background-image: url(../images/true.png); }
1006 1010 .icon-details { background-image: url(../images/zoom_in.png); }
1007 1011 .icon-report { background-image: url(../images/report.png); }
1008 1012 .icon-comment { background-image: url(../images/comment.png); }
1009 1013 .icon-summary { background-image: url(../images/lightning.png); }
1010 1014 .icon-server-authentication { background-image: url(../images/server_key.png); }
1011 1015 .icon-issue { background-image: url(../images/ticket.png); }
1012 1016 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
1013 1017 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
1014 1018 .icon-passwd { background-image: url(../images/textfield_key.png); }
1015 1019 .icon-test { background-image: url(../images/bullet_go.png); }
1016 1020
1017 1021 .icon-file { background-image: url(../images/files/default.png); }
1018 1022 .icon-file.text-plain { background-image: url(../images/files/text.png); }
1019 1023 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
1020 1024 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
1021 1025 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
1022 1026 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
1023 1027 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
1024 1028 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
1025 1029 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
1026 1030 .icon-file.text-css { background-image: url(../images/files/css.png); }
1027 1031 .icon-file.text-html { background-image: url(../images/files/html.png); }
1028 1032 .icon-file.image-gif { background-image: url(../images/files/image.png); }
1029 1033 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
1030 1034 .icon-file.image-png { background-image: url(../images/files/image.png); }
1031 1035 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
1032 1036 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
1033 1037 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
1034 1038 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
1035 1039
1036 1040 img.gravatar {
1037 1041 padding: 2px;
1038 1042 border: solid 1px #d5d5d5;
1039 1043 background: #fff;
1040 1044 vertical-align: middle;
1041 1045 }
1042 1046
1043 1047 div.issue img.gravatar {
1044 1048 float: left;
1045 1049 margin: 0 6px 0 0;
1046 1050 padding: 5px;
1047 1051 }
1048 1052
1049 1053 div.issue table img.gravatar {
1050 1054 height: 14px;
1051 1055 width: 14px;
1052 1056 padding: 2px;
1053 1057 float: left;
1054 1058 margin: 0 0.5em 0 0;
1055 1059 }
1056 1060
1057 1061 h2 img.gravatar {margin: -2px 4px -4px 0;}
1058 1062 h3 img.gravatar {margin: -4px 4px -4px 0;}
1059 1063 h4 img.gravatar {margin: -6px 4px -4px 0;}
1060 1064 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1061 1065 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1062 1066 /* Used on 12px Gravatar img tags without the icon background */
1063 1067 .icon-gravatar {float: left; margin-right: 4px;}
1064 1068
1065 1069 #activity dt, .journal {clear: left;}
1066 1070
1067 1071 .journal-link {float: right;}
1068 1072
1069 1073 h2 img { vertical-align:middle; }
1070 1074
1071 1075 .hascontextmenu { cursor: context-menu; }
1072 1076
1073 1077 /* Custom JQuery styles */
1074 1078 .ui-datepicker-title select {width:70px !important; margin-top:-2px !important; margin-right:4px !important;}
1075 1079
1076 1080
1077 1081 /************* CodeRay styles *************/
1078 1082 .syntaxhl div {display: inline;}
1079 1083 .syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;}
1080 1084 .syntaxhl .code pre { overflow: auto }
1081 1085 .syntaxhl .debug { color: white !important; background: blue !important; }
1082 1086
1083 1087 .syntaxhl .annotation { color:#007 }
1084 1088 .syntaxhl .attribute-name { color:#b48 }
1085 1089 .syntaxhl .attribute-value { color:#700 }
1086 1090 .syntaxhl .binary { color:#509 }
1087 1091 .syntaxhl .char .content { color:#D20 }
1088 1092 .syntaxhl .char .delimiter { color:#710 }
1089 1093 .syntaxhl .char { color:#D20 }
1090 1094 .syntaxhl .class { color:#258; font-weight:bold }
1091 1095 .syntaxhl .class-variable { color:#369 }
1092 1096 .syntaxhl .color { color:#0A0 }
1093 1097 .syntaxhl .comment { color:#385 }
1094 1098 .syntaxhl .comment .char { color:#385 }
1095 1099 .syntaxhl .comment .delimiter { color:#385 }
1096 1100 .syntaxhl .complex { color:#A08 }
1097 1101 .syntaxhl .constant { color:#258; font-weight:bold }
1098 1102 .syntaxhl .decorator { color:#B0B }
1099 1103 .syntaxhl .definition { color:#099; font-weight:bold }
1100 1104 .syntaxhl .delimiter { color:black }
1101 1105 .syntaxhl .directive { color:#088; font-weight:bold }
1102 1106 .syntaxhl .doc { color:#970 }
1103 1107 .syntaxhl .doc-string { color:#D42; font-weight:bold }
1104 1108 .syntaxhl .doctype { color:#34b }
1105 1109 .syntaxhl .entity { color:#800; font-weight:bold }
1106 1110 .syntaxhl .error { color:#F00; background-color:#FAA }
1107 1111 .syntaxhl .escape { color:#666 }
1108 1112 .syntaxhl .exception { color:#C00; font-weight:bold }
1109 1113 .syntaxhl .float { color:#06D }
1110 1114 .syntaxhl .function { color:#06B; font-weight:bold }
1111 1115 .syntaxhl .global-variable { color:#d70 }
1112 1116 .syntaxhl .hex { color:#02b }
1113 1117 .syntaxhl .imaginary { color:#f00 }
1114 1118 .syntaxhl .include { color:#B44; font-weight:bold }
1115 1119 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1116 1120 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1117 1121 .syntaxhl .instance-variable { color:#33B }
1118 1122 .syntaxhl .integer { color:#06D }
1119 1123 .syntaxhl .key .char { color: #60f }
1120 1124 .syntaxhl .key .delimiter { color: #404 }
1121 1125 .syntaxhl .key { color: #606 }
1122 1126 .syntaxhl .keyword { color:#939; font-weight:bold }
1123 1127 .syntaxhl .label { color:#970; font-weight:bold }
1124 1128 .syntaxhl .local-variable { color:#963 }
1125 1129 .syntaxhl .namespace { color:#707; font-weight:bold }
1126 1130 .syntaxhl .octal { color:#40E }
1127 1131 .syntaxhl .operator { }
1128 1132 .syntaxhl .predefined { color:#369; font-weight:bold }
1129 1133 .syntaxhl .predefined-constant { color:#069 }
1130 1134 .syntaxhl .predefined-type { color:#0a5; font-weight:bold }
1131 1135 .syntaxhl .preprocessor { color:#579 }
1132 1136 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1133 1137 .syntaxhl .regexp .content { color:#808 }
1134 1138 .syntaxhl .regexp .delimiter { color:#404 }
1135 1139 .syntaxhl .regexp .modifier { color:#C2C }
1136 1140 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1137 1141 .syntaxhl .reserved { color:#080; font-weight:bold }
1138 1142 .syntaxhl .shell .content { color:#2B2 }
1139 1143 .syntaxhl .shell .delimiter { color:#161 }
1140 1144 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1141 1145 .syntaxhl .string .char { color: #46a }
1142 1146 .syntaxhl .string .content { color: #46a }
1143 1147 .syntaxhl .string .delimiter { color: #46a }
1144 1148 .syntaxhl .string .modifier { color: #46a }
1145 1149 .syntaxhl .symbol .content { color:#d33 }
1146 1150 .syntaxhl .symbol .delimiter { color:#d33 }
1147 1151 .syntaxhl .symbol { color:#d33 }
1148 1152 .syntaxhl .tag { color:#070 }
1149 1153 .syntaxhl .type { color:#339; font-weight:bold }
1150 1154 .syntaxhl .value { color: #088; }
1151 1155 .syntaxhl .variable { color:#037 }
1152 1156
1153 1157 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1154 1158 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1155 1159 .syntaxhl .change { color: #bbf; background: #007; }
1156 1160 .syntaxhl .head { color: #f8f; background: #505 }
1157 1161 .syntaxhl .head .filename { color: white; }
1158 1162
1159 1163 .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; }
1160 1164 .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; }
1161 1165
1162 1166 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1163 1167 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1164 1168 .syntaxhl .change .change { color: #88f }
1165 1169 .syntaxhl .head .head { color: #f4f }
1166 1170
1167 1171 /***** Media print specific styles *****/
1168 1172 @media print {
1169 1173 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1170 1174 #main { background: #fff; }
1171 1175 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1172 1176 #wiki_add_attachment { display:none; }
1173 1177 .hide-when-print { display: none; }
1174 1178 .autoscroll {overflow-x: visible;}
1175 1179 table.list {margin-top:0.5em;}
1176 1180 table.list th, table.list td {border: 1px solid #aaa;}
1177 1181 }
1178 1182
1179 1183 /* Accessibility specific styles */
1180 1184 .hidden-for-sighted {
1181 1185 position:absolute;
1182 1186 left:-10000px;
1183 1187 top:auto;
1184 1188 width:1px;
1185 1189 height:1px;
1186 1190 overflow:hidden;
1187 1191 }
@@ -1,344 +1,354
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class WorkflowsControllerTest < ActionController::TestCase
21 21 fixtures :roles, :trackers, :workflows, :users, :issue_statuses
22 22
23 23 def setup
24 24 User.current = nil
25 25 @request.session[:user_id] = 1 # admin
26 26 end
27 27
28 28 def test_index
29 29 get :index
30 30 assert_response :success
31 31 assert_template 'index'
32 32
33 33 count = WorkflowTransition.where(:role_id => 1, :tracker_id => 2).count
34 34 assert_tag :tag => 'a', :content => count.to_s,
35 35 :attributes => { :href => '/workflows/edit?role_id=1&amp;tracker_id=2' }
36 36 end
37 37
38 38 def test_get_edit
39 39 get :edit
40 40 assert_response :success
41 41 assert_template 'edit'
42 assert_not_nil assigns(:roles)
43 assert_not_nil assigns(:trackers)
44 42 end
45 43
46 44 def test_get_edit_with_role_and_tracker
47 45 WorkflowTransition.delete_all
48 46 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3)
49 47 WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5)
50 48
51 49 get :edit, :role_id => 2, :tracker_id => 1
52 50 assert_response :success
53 51 assert_template 'edit'
54 52
55 53 # used status only
56 54 assert_not_nil assigns(:statuses)
57 55 assert_equal [2, 3, 5], assigns(:statuses).collect(&:id)
58 56
59 57 # allowed transitions
60 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
61 :name => 'issue_status[3][5][]',
62 :value => 'always',
63 :checked => 'checked' }
58 assert_select 'input[type=checkbox][name=?][value=1][checked=checked]', 'transitions[3][5][always]'
64 59 # not allowed
65 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
66 :name => 'issue_status[3][2][]',
67 :value => 'always',
68 :checked => nil }
60 assert_select 'input[type=checkbox][name=?][value=1]:not([checked=checked])', 'transitions[3][2][always]'
69 61 # unused
70 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
71 :name => 'issue_status[1][1][]' }
62 assert_select 'input[type=checkbox][name=?]', 'transitions[1][1][always]', 0
72 63 end
73 64
74 65 def test_get_edit_with_role_and_tracker_and_all_statuses
75 66 WorkflowTransition.delete_all
76 67
77 68 get :edit, :role_id => 2, :tracker_id => 1, :used_statuses_only => '0'
78 69 assert_response :success
79 70 assert_template 'edit'
80 71
81 72 assert_not_nil assigns(:statuses)
82 73 assert_equal IssueStatus.count, assigns(:statuses).size
83 74
84 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
85 :name => 'issue_status[1][1][]',
86 :value => 'always',
87 :checked => nil }
75 assert_select 'input[type=checkbox][name=?]', 'transitions[1][1][always]'
88 76 end
89 77
90 78 def test_post_edit
79 WorkflowTransition.delete_all
80
91 81 post :edit, :role_id => 2, :tracker_id => 1,
92 :issue_status => {
93 '4' => {'5' => ['always']},
94 '3' => {'1' => ['always'], '2' => ['always']}
82 :transitions => {
83 '4' => {'5' => {'always' => '1'}},
84 '3' => {'1' => {'always' => '1'}, '2' => {'always' => '1'}}
95 85 }
96 assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
86 assert_response 302
97 87
98 88 assert_equal 3, WorkflowTransition.where(:tracker_id => 1, :role_id => 2).count
99 89 assert_not_nil WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2).first
100 90 assert_nil WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4).first
101 91 end
102 92
103 93 def test_post_edit_with_additional_transitions
94 WorkflowTransition.delete_all
95
104 96 post :edit, :role_id => 2, :tracker_id => 1,
105 :issue_status => {
106 '4' => {'5' => ['always']},
107 '3' => {'1' => ['author'], '2' => ['assignee'], '4' => ['author', 'assignee']}
97 :transitions => {
98 '4' => {'5' => {'always' => '1', 'author' => '0', 'assignee' => '0'}},
99 '3' => {'1' => {'always' => '0', 'author' => '1', 'assignee' => '0'},
100 '2' => {'always' => '0', 'author' => '0', 'assignee' => '1'},
101 '4' => {'always' => '0', 'author' => '1', 'assignee' => '1'}}
108 102 }
109 assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
103 assert_response 302
110 104
111 105 assert_equal 4, WorkflowTransition.where(:tracker_id => 1, :role_id => 2).count
112 106
113 107 w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5).first
114 108 assert ! w.author
115 109 assert ! w.assignee
116 110 w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1).first
117 111 assert w.author
118 112 assert ! w.assignee
119 113 w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2).first
120 114 assert ! w.author
121 115 assert w.assignee
122 116 w = WorkflowTransition.where(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4).first
123 117 assert w.author
124 118 assert w.assignee
125 119 end
126 120
127 def test_clear_workflow
128 assert WorkflowTransition.where(:role_id => 1, :tracker_id => 2).count > 0
129
130 post :edit, :role_id => 1, :tracker_id => 2
131 assert_equal 0, WorkflowTransition.where(:role_id => 1, :tracker_id => 2).count
132 end
133
134 121 def test_get_permissions
135 122 get :permissions
136 123
137 124 assert_response :success
138 125 assert_template 'permissions'
139 assert_not_nil assigns(:roles)
140 assert_not_nil assigns(:trackers)
141 126 end
142 127
143 128 def test_get_permissions_with_role_and_tracker
144 129 WorkflowPermission.delete_all
145 130 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required')
146 131 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
147 132 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly')
148 133
149 134 get :permissions, :role_id => 1, :tracker_id => 2
150 135 assert_response :success
151 136 assert_template 'permissions'
152 137
153 assert_select 'input[name=role_id][value=1]'
154 assert_select 'input[name=tracker_id][value=2]'
138 assert_select 'input[name=?][value=1]', 'role_id[]'
139 assert_select 'input[name=?][value=2]', 'tracker_id[]'
155 140
156 141 # Required field
157 assert_select 'select[name=?]', 'permissions[assigned_to_id][2]' do
142 assert_select 'select[name=?]', 'permissions[2][assigned_to_id]' do
158 143 assert_select 'option[value=]'
159 144 assert_select 'option[value=][selected=selected]', 0
160 145 assert_select 'option[value=readonly]', :text => 'Read-only'
161 146 assert_select 'option[value=readonly][selected=selected]', 0
162 147 assert_select 'option[value=required]', :text => 'Required'
163 148 assert_select 'option[value=required][selected=selected]'
164 149 end
165 150
166 151 # Read-only field
167 assert_select 'select[name=?]', 'permissions[fixed_version_id][3]' do
152 assert_select 'select[name=?]', 'permissions[3][fixed_version_id]' do
168 153 assert_select 'option[value=]'
169 154 assert_select 'option[value=][selected=selected]', 0
170 155 assert_select 'option[value=readonly]', :text => 'Read-only'
171 156 assert_select 'option[value=readonly][selected=selected]'
172 157 assert_select 'option[value=required]', :text => 'Required'
173 158 assert_select 'option[value=required][selected=selected]', 0
174 159 end
175 160
176 161 # Other field
177 assert_select 'select[name=?]', 'permissions[due_date][3]' do
162 assert_select 'select[name=?]', 'permissions[3][due_date]' do
178 163 assert_select 'option[value=]'
179 164 assert_select 'option[value=][selected=selected]', 0
180 165 assert_select 'option[value=readonly]', :text => 'Read-only'
181 166 assert_select 'option[value=readonly][selected=selected]', 0
182 167 assert_select 'option[value=required]', :text => 'Required'
183 168 assert_select 'option[value=required][selected=selected]', 0
184 169 end
185 170 end
186 171
187 172 def test_get_permissions_with_required_custom_field_should_not_show_required_option
188 173 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :tracker_ids => [1], :is_required => true)
189 174
190 175 get :permissions, :role_id => 1, :tracker_id => 1
191 176 assert_response :success
192 177 assert_template 'permissions'
193 178
194 179 # Custom field that is always required
195 180 # The default option is "(Required)"
196 assert_select 'select[name=?]', "permissions[#{cf.id}][3]" do
181 assert_select 'select[name=?]', "permissions[3][#{cf.id}]" do
197 182 assert_select 'option[value=]'
198 183 assert_select 'option[value=readonly]', :text => 'Read-only'
199 184 assert_select 'option[value=required]', 0
200 185 end
201 186 end
202 187
203 188 def test_get_permissions_should_disable_hidden_custom_fields
204 189 cf1 = IssueCustomField.generate!(:tracker_ids => [1], :visible => true)
205 190 cf2 = IssueCustomField.generate!(:tracker_ids => [1], :visible => false, :role_ids => [1])
206 191 cf3 = IssueCustomField.generate!(:tracker_ids => [1], :visible => false, :role_ids => [1, 2])
207 192
208 193 get :permissions, :role_id => 2, :tracker_id => 1
209 194 assert_response :success
210 195 assert_template 'permissions'
211 196
212 assert_select 'select[name=?]:not(.disabled)', "permissions[#{cf1.id}][1]"
213 assert_select 'select[name=?]:not(.disabled)', "permissions[#{cf3.id}][1]"
197 assert_select 'select[name=?]:not(.disabled)', "permissions[1][#{cf1.id}]"
198 assert_select 'select[name=?]:not(.disabled)', "permissions[1][#{cf3.id}]"
214 199
215 assert_select 'select[name=?][disabled=disabled]', "permissions[#{cf2.id}][1]" do
200 assert_select 'select[name=?][disabled=disabled]', "permissions[1][#{cf2.id}]" do
216 201 assert_select 'option[value=][selected=selected]', :text => 'Hidden'
217 202 end
218 203 end
219 204
220 def test_get_permissions_with_role_and_tracker_and_all_statuses
205 def test_get_permissions_with_missing_permissions_for_roles_should_default_to_no_change
206 WorkflowPermission.delete_all
207 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :field_name => 'assigned_to_id', :rule => 'required')
208
209 get :permissions, :role_id => [1, 2], :tracker_id => 2
210 assert_response :success
211
212 assert_select 'select[name=?]', 'permissions[1][assigned_to_id]' do
213 assert_select 'option[selected]', 1
214 assert_select 'option[selected][value=no_change]'
215 end
216 end
217
218 def test_get_permissions_with_different_permissions_for_roles_should_default_to_no_change
219 WorkflowPermission.delete_all
220 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :field_name => 'assigned_to_id', :rule => 'required')
221 WorkflowPermission.create!(:role_id => 2, :tracker_id => 2, :old_status_id => 1, :field_name => 'assigned_to_id', :rule => 'readonly')
222
223 get :permissions, :role_id => [1, 2], :tracker_id => 2
224 assert_response :success
225
226 assert_select 'select[name=?]', 'permissions[1][assigned_to_id]' do
227 assert_select 'option[selected]', 1
228 assert_select 'option[selected][value=no_change]'
229 end
230 end
231
232 def test_get_permissions_with_same_permissions_for_roles_should_default_to_permission
233 WorkflowPermission.delete_all
234 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 1, :field_name => 'assigned_to_id', :rule => 'required')
235 WorkflowPermission.create!(:role_id => 2, :tracker_id => 2, :old_status_id => 1, :field_name => 'assigned_to_id', :rule => 'required')
236
237 get :permissions, :role_id => [1, 2], :tracker_id => 2
238 assert_response :success
239
240 assert_select 'select[name=?]', 'permissions[1][assigned_to_id]' do
241 assert_select 'option[selected]', 1
242 assert_select 'option[selected][value=required]'
243 end
244 end
245
246 def test_get_permissions_with_role_and_tracker_and_all_statuses_should_show_all_statuses
221 247 WorkflowTransition.delete_all
222 248
223 249 get :permissions, :role_id => 1, :tracker_id => 2, :used_statuses_only => '0'
224 250 assert_response :success
225 251 assert_equal IssueStatus.sorted.all, assigns(:statuses)
226 252 end
227 253
228 254 def test_post_permissions
229 255 WorkflowPermission.delete_all
230 256
231 257 post :permissions, :role_id => 1, :tracker_id => 2, :permissions => {
232 'assigned_to_id' => {'1' => '', '2' => 'readonly', '3' => ''},
233 'fixed_version_id' => {'1' => 'required', '2' => 'readonly', '3' => ''},
234 'due_date' => {'1' => '', '2' => '', '3' => ''},
258 '1' => {'assigned_to_id' => '', 'fixed_version_id' => 'required', 'due_date' => ''},
259 '2' => {'assigned_to_id' => 'readonly', 'fixed_version_id' => 'readonly', 'due_date' => ''},
260 '3' => {'assigned_to_id' => '', 'fixed_version_id' => '', 'due_date' => ''}
235 261 }
236 assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2'
262 assert_response 302
237 263
238 264 workflows = WorkflowPermission.all
239 265 assert_equal 3, workflows.size
240 266 workflows.each do |workflow|
241 267 assert_equal 1, workflow.role_id
242 268 assert_equal 2, workflow.tracker_id
243 269 end
244 270 assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'assigned_to_id' && wf.rule == 'readonly'}
245 271 assert workflows.detect {|wf| wf.old_status_id == 1 && wf.field_name == 'fixed_version_id' && wf.rule == 'required'}
246 272 assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'fixed_version_id' && wf.rule == 'readonly'}
247 273 end
248 274
249 def test_post_permissions_should_clear_permissions
250 WorkflowPermission.delete_all
251 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required')
252 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
253 wf1 = WorkflowPermission.create!(:role_id => 1, :tracker_id => 3, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
254 wf2 = WorkflowPermission.create!(:role_id => 2, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly')
255
256 post :permissions, :role_id => 1, :tracker_id => 2
257 assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2'
258
259 workflows = WorkflowPermission.all
260 assert_equal 2, workflows.size
261 assert wf1.reload
262 assert wf2.reload
263 end
264
265 275 def test_get_copy
266 276 get :copy
267 277 assert_response :success
268 278 assert_template 'copy'
269 279 assert_select 'select[name=source_tracker_id]' do
270 280 assert_select 'option[value=1]', :text => 'Bug'
271 281 end
272 282 assert_select 'select[name=source_role_id]' do
273 283 assert_select 'option[value=2]', :text => 'Developer'
274 284 end
275 285 assert_select 'select[name=?]', 'target_tracker_ids[]' do
276 286 assert_select 'option[value=3]', :text => 'Support request'
277 287 end
278 288 assert_select 'select[name=?]', 'target_role_ids[]' do
279 289 assert_select 'option[value=1]', :text => 'Manager'
280 290 end
281 291 end
282 292
283 293 def test_post_copy_one_to_one
284 294 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
285 295
286 296 post :copy, :source_tracker_id => '1', :source_role_id => '2',
287 297 :target_tracker_ids => ['3'], :target_role_ids => ['1']
288 298 assert_response 302
289 299 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
290 300 end
291 301
292 302 def test_post_copy_one_to_many
293 303 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
294 304
295 305 post :copy, :source_tracker_id => '1', :source_role_id => '2',
296 306 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
297 307 assert_response 302
298 308 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 1)
299 309 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
300 310 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 3)
301 311 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 3)
302 312 end
303 313
304 314 def test_post_copy_many_to_many
305 315 source_t2 = status_transitions(:tracker_id => 2, :role_id => 2)
306 316 source_t3 = status_transitions(:tracker_id => 3, :role_id => 2)
307 317
308 318 post :copy, :source_tracker_id => 'any', :source_role_id => '2',
309 319 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
310 320 assert_response 302
311 321 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 1)
312 322 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 1)
313 323 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 3)
314 324 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 3)
315 325 end
316 326
317 327 def test_post_copy_with_incomplete_source_specification_should_fail
318 328 assert_no_difference 'WorkflowRule.count' do
319 329 post :copy,
320 330 :source_tracker_id => '', :source_role_id => '2',
321 331 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
322 332 assert_response 200
323 333 assert_select 'div.flash.error', :text => 'Please select a source tracker or role'
324 334 end
325 335 end
326 336
327 337 def test_post_copy_with_incomplete_target_specification_should_fail
328 338 assert_no_difference 'WorkflowRule.count' do
329 339 post :copy,
330 340 :source_tracker_id => '1', :source_role_id => '2',
331 341 :target_tracker_ids => ['2', '3']
332 342 assert_response 200
333 343 assert_select 'div.flash.error', :text => 'Please select target tracker(s) and role(s)'
334 344 end
335 345 end
336 346
337 347 # Returns an array of status transitions that can be compared
338 348 def status_transitions(conditions)
339 349 WorkflowTransition.
340 350 where(conditions).
341 351 order('tracker_id, role_id, old_status_id, new_status_id').
342 352 collect {|w| [w.old_status, w.new_status_id]}
343 353 end
344 354 end
General Comments 0
You need to be logged in to leave comments. Login now