##// END OF EJS Templates
Adds custom fields to documents (#7249)....
Jean-Philippe Lang -
r13622:ae4eb4788132
parent child
Show More
@@ -0,0 +1,22
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 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 class DocumentCustomField < CustomField
19 def type_name
20 :label_document_plural
21 end
22 end
@@ -1,94 +1,95
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 DocumentsController < ApplicationController
19 19 default_search_scope :documents
20 20 model_object Document
21 21 before_filter :find_project_by_project_id, :only => [:index, :new, :create]
22 22 before_filter :find_model_object, :except => [:index, :new, :create]
23 23 before_filter :find_project_from_association, :except => [:index, :new, :create]
24 24 before_filter :authorize
25 25
26 26 helper :attachments
27 helper :custom_fields
27 28
28 29 def index
29 30 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
30 31 documents = @project.documents.includes(:attachments, :category).to_a
31 32 case @sort_by
32 33 when 'date'
33 34 @grouped = documents.group_by {|d| d.updated_on.to_date }
34 35 when 'title'
35 36 @grouped = documents.group_by {|d| d.title.first.upcase}
36 37 when 'author'
37 38 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
38 39 else
39 40 @grouped = documents.group_by(&:category)
40 41 end
41 42 @document = @project.documents.build
42 43 render :layout => false if request.xhr?
43 44 end
44 45
45 46 def show
46 47 @attachments = @document.attachments.to_a
47 48 end
48 49
49 50 def new
50 51 @document = @project.documents.build
51 52 @document.safe_attributes = params[:document]
52 53 end
53 54
54 55 def create
55 56 @document = @project.documents.build
56 57 @document.safe_attributes = params[:document]
57 58 @document.save_attachments(params[:attachments])
58 59 if @document.save
59 60 render_attachment_warning_if_needed(@document)
60 61 flash[:notice] = l(:notice_successful_create)
61 62 redirect_to project_documents_path(@project)
62 63 else
63 64 render :action => 'new'
64 65 end
65 66 end
66 67
67 68 def edit
68 69 end
69 70
70 71 def update
71 72 @document.safe_attributes = params[:document]
72 73 if @document.save
73 74 flash[:notice] = l(:notice_successful_update)
74 75 redirect_to document_path(@document)
75 76 else
76 77 render :action => 'edit'
77 78 end
78 79 end
79 80
80 81 def destroy
81 82 @document.destroy if request.delete?
82 83 redirect_to project_documents_path(@project)
83 84 end
84 85
85 86 def add_attachment
86 87 attachments = Attachment.attach_files(@document, params[:attachments])
87 88 render_attachment_warning_if_needed(@document)
88 89
89 90 if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
90 91 Mailer.attachments_added(attachments[:files]).deliver
91 92 end
92 93 redirect_to document_path(@document)
93 94 end
94 95 end
@@ -1,159 +1,161
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 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 CustomFieldsHelper
21 21
22 22 CUSTOM_FIELDS_TABS = [
23 23 {:name => 'IssueCustomField', :partial => 'custom_fields/index',
24 24 :label => :label_issue_plural},
25 25 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index',
26 26 :label => :label_spent_time},
27 27 {:name => 'ProjectCustomField', :partial => 'custom_fields/index',
28 28 :label => :label_project_plural},
29 29 {:name => 'VersionCustomField', :partial => 'custom_fields/index',
30 30 :label => :label_version_plural},
31 {:name => 'DocumentCustomField', :partial => 'custom_fields/index',
32 :label => :label_document_plural},
31 33 {:name => 'UserCustomField', :partial => 'custom_fields/index',
32 34 :label => :label_user_plural},
33 35 {:name => 'GroupCustomField', :partial => 'custom_fields/index',
34 36 :label => :label_group_plural},
35 37 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index',
36 38 :label => TimeEntryActivity::OptionName},
37 39 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index',
38 40 :label => IssuePriority::OptionName},
39 41 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index',
40 42 :label => DocumentCategory::OptionName}
41 43 ]
42 44
43 45 def render_custom_fields_tabs(types)
44 46 tabs = CUSTOM_FIELDS_TABS.select {|h| types.include?(h[:name]) }
45 47 render_tabs tabs
46 48 end
47 49
48 50 def custom_field_type_options
49 51 CUSTOM_FIELDS_TABS.map {|h| [l(h[:label]), h[:name]]}
50 52 end
51 53
52 54 def render_custom_field_format_partial(form, custom_field)
53 55 partial = custom_field.format.form_partial
54 56 if partial
55 57 render :partial => custom_field.format.form_partial, :locals => {:f => form, :custom_field => custom_field}
56 58 end
57 59 end
58 60
59 61 def custom_field_tag_name(prefix, custom_field)
60 62 name = "#{prefix}[custom_field_values][#{custom_field.id}]"
61 63 name << "[]" if custom_field.multiple?
62 64 name
63 65 end
64 66
65 67 def custom_field_tag_id(prefix, custom_field)
66 68 "#{prefix}_custom_field_values_#{custom_field.id}"
67 69 end
68 70
69 71 # Return custom field html tag corresponding to its format
70 72 def custom_field_tag(prefix, custom_value)
71 73 custom_value.custom_field.format.edit_tag self,
72 74 custom_field_tag_id(prefix, custom_value.custom_field),
73 75 custom_field_tag_name(prefix, custom_value.custom_field),
74 76 custom_value,
75 77 :class => "#{custom_value.custom_field.field_format}_cf"
76 78 end
77 79
78 80 # Return custom field label tag
79 81 def custom_field_label_tag(name, custom_value, options={})
80 82 required = options[:required] || custom_value.custom_field.is_required?
81 83 title = custom_value.custom_field.description.presence
82 84 content = content_tag 'span', custom_value.custom_field.name, :title => title
83 85
84 86 content_tag "label", content +
85 87 (required ? " <span class=\"required\">*</span>".html_safe : ""),
86 88 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}"
87 89 end
88 90
89 91 # Return custom field tag with its label tag
90 92 def custom_field_tag_with_label(name, custom_value, options={})
91 93 custom_field_label_tag(name, custom_value, options) + custom_field_tag(name, custom_value)
92 94 end
93 95
94 96 # Returns the custom field tag for when bulk editing objects
95 97 def custom_field_tag_for_bulk_edit(prefix, custom_field, objects=nil, value='')
96 98 custom_field.format.bulk_edit_tag self,
97 99 custom_field_tag_id(prefix, custom_field),
98 100 custom_field_tag_name(prefix, custom_field),
99 101 custom_field,
100 102 objects,
101 103 value,
102 104 :class => "#{custom_field.field_format}_cf"
103 105 end
104 106
105 107 # Return a string used to display a custom value
106 108 def show_value(custom_value, html=true)
107 109 format_object(custom_value, html)
108 110 end
109 111
110 112 # Return a string used to display a custom value
111 113 def format_value(value, custom_field)
112 114 format_object(custom_field.format.formatted_value(self, custom_field, value, false), false)
113 115 end
114 116
115 117 # Return an array of custom field formats which can be used in select_tag
116 118 def custom_field_formats_for_select(custom_field)
117 119 Redmine::FieldFormat.as_select(custom_field.class.customized_class.name)
118 120 end
119 121
120 122 # Yields the given block for each custom field value of object that should be
121 123 # displayed, with the custom field and the formatted value as arguments
122 124 def render_custom_field_values(object, &block)
123 125 object.visible_custom_field_values.each do |custom_value|
124 126 formatted = show_value(custom_value)
125 127 if formatted.present?
126 128 yield custom_value.custom_field, formatted
127 129 end
128 130 end
129 131 end
130 132
131 133 # Renders the custom_values in api views
132 134 def render_api_custom_values(custom_values, api)
133 135 api.array :custom_fields do
134 136 custom_values.each do |custom_value|
135 137 attrs = {:id => custom_value.custom_field_id, :name => custom_value.custom_field.name}
136 138 attrs.merge!(:multiple => true) if custom_value.custom_field.multiple?
137 139 api.custom_field attrs do
138 140 if custom_value.value.is_a?(Array)
139 141 api.array :value do
140 142 custom_value.value.each do |value|
141 143 api.value value unless value.blank?
142 144 end
143 145 end
144 146 else
145 147 api.value custom_value.value
146 148 end
147 149 end
148 150 end
149 151 end unless custom_values.empty?
150 152 end
151 153
152 154 def edit_tag_style_tag(form, options={})
153 155 select_options = [[l(:label_drop_down_list), ''], [l(:label_checkboxes), 'check_box']]
154 156 if options[:include_radio]
155 157 select_options << [l(:label_radio_buttons), 'radio']
156 158 end
157 159 form.select :edit_tag_style, select_options, :label => :label_display
158 160 end
159 161 end
@@ -1,74 +1,75
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 Document < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20 belongs_to :project
21 21 belongs_to :category, :class_name => "DocumentCategory"
22 22 acts_as_attachable :delete_permission => :delete_documents
23 acts_as_customizable
23 24
24 25 acts_as_searchable :columns => ['title', "#{table_name}.description"],
25 26 :preload => :project
26 27 acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
27 28 :author => Proc.new {|o| o.attachments.reorder("#{Attachment.table_name}.created_on ASC").first.try(:author) },
28 29 :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
29 30 acts_as_activity_provider :scope => preload(:project)
30 31
31 32 validates_presence_of :project, :title, :category
32 33 validates_length_of :title, :maximum => 60
33 34 attr_protected :id
34 35
35 36 after_create :send_notification
36 37
37 38 scope :visible, lambda {|*args|
38 39 joins(:project).
39 40 where(Project.allowed_to_condition(args.shift || User.current, :view_documents, *args))
40 41 }
41 42
42 safe_attributes 'category_id', 'title', 'description'
43 safe_attributes 'category_id', 'title', 'description', 'custom_fields', 'custom_field_values'
43 44
44 45 def visible?(user=User.current)
45 46 !user.nil? && user.allowed_to?(:view_documents, project)
46 47 end
47 48
48 49 def initialize(attributes=nil, *args)
49 50 super
50 51 if new_record?
51 52 self.category ||= DocumentCategory.default
52 53 end
53 54 end
54 55
55 56 def updated_on
56 57 unless @updated_on
57 58 a = attachments.last
58 59 @updated_on = (a && a.created_on) || created_on
59 60 end
60 61 @updated_on
61 62 end
62 63
63 64 def notified_users
64 65 project.notified_users.reject {|user| !visible?(user)}
65 66 end
66 67
67 68 private
68 69
69 70 def send_notification
70 71 if Setting.notified_events.include?('document_added')
71 72 Mailer.document_added(self).deliver
72 73 end
73 74 end
74 75 end
@@ -1,15 +1,19
1 1 <%= error_messages_for @document %>
2 2
3 3 <div class="box tabular">
4 4 <p><%= f.select :category_id, DocumentCategory.active.collect {|c| [c.name, c.id]} %></p>
5 5 <p><%= f.text_field :title, :required => true, :size => 60 %></p>
6 6 <p><%= f.text_area :description, :cols => 60, :rows => 15, :class => 'wiki-edit' %></p>
7
8 <% @document.custom_field_values.each do |value| %>
9 <p><%= custom_field_tag_with_label :document, value %></p>
10 <% end %>
7 11 </div>
8 12
9 13 <%= wikitoolbar_for 'document_description' %>
10 14
11 15 <% if @document.new_record? %>
12 16 <div class="box tabular">
13 17 <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form', :locals => {:container => @document} %></p>
14 18 </div>
15 19 <% end %>
@@ -1,32 +1,41
1 1 <div class="contextual">
2 2 <% if User.current.allowed_to?(:edit_documents, @project) %>
3 3 <%= link_to l(:button_edit), edit_document_path(@document), :class => 'icon icon-edit', :accesskey => accesskey(:edit) %>
4 4 <% end %>
5 5 <% if User.current.allowed_to?(:delete_documents, @project) %>
6 6 <%= delete_link document_path(@document) %>
7 7 <% end %>
8 8 </div>
9 9
10 10 <h2><%=h @document.title %></h2>
11 11
12 12 <p><em><%=h @document.category.name %><br />
13 13 <%= format_date @document.created_on %></em></p>
14
15 <% if @document.custom_field_values.any? %>
16 <ul>
17 <% render_custom_field_values(@document) do |custom_field, formatted| %>
18 <li><span class="label"><%= custom_field.name %>:</span> <%= formatted %></li>
19 <% end %>
20 </ul>
21 <% end %>
22
14 23 <div class="wiki">
15 24 <%= textilizable @document, :description, :attachments => @document.attachments %>
16 25 </div>
17 26
18 27 <h3><%= l(:label_attachment_plural) %></h3>
19 28 <%= link_to_attachments @document %>
20 29
21 30 <% if authorize_for('documents', 'add_attachment') %>
22 31 <p><%= link_to l(:label_attachment_new), {}, :onclick => "$('#add_attachment_form').show(); return false;",
23 32 :id => 'attach_files_link' %></p>
24 33 <%= form_tag({ :controller => 'documents', :action => 'add_attachment', :id => @document }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
25 34 <div class="box">
26 35 <p><%= render :partial => 'attachments/form' %></p>
27 36 </div>
28 37 <%= submit_tag l(:button_add) %>
29 38 <% end %>
30 39 <% end %>
31 40
32 41 <% html_title @document.title -%>
@@ -1,714 +1,714
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 module Redmine
19 19 module FieldFormat
20 20 def self.add(name, klass)
21 21 all[name.to_s] = klass.instance
22 22 end
23 23
24 24 def self.delete(name)
25 25 all.delete(name.to_s)
26 26 end
27 27
28 28 def self.all
29 29 @formats ||= Hash.new(Base.instance)
30 30 end
31 31
32 32 def self.available_formats
33 33 all.keys
34 34 end
35 35
36 36 def self.find(name)
37 37 all[name.to_s]
38 38 end
39 39
40 40 # Return an array of custom field formats which can be used in select_tag
41 41 def self.as_select(class_name=nil)
42 42 formats = all.values.select do |format|
43 43 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
44 44 end
45 45 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
46 46 end
47 47
48 48 class Base
49 49 include Singleton
50 50 include Redmine::I18n
51 51 include ERB::Util
52 52
53 53 class_attribute :format_name
54 54 self.format_name = nil
55 55
56 56 # Set this to true if the format supports multiple values
57 57 class_attribute :multiple_supported
58 58 self.multiple_supported = false
59 59
60 60 # Set this to true if the format supports textual search on custom values
61 61 class_attribute :searchable_supported
62 62 self.searchable_supported = false
63 63
64 64 # Restricts the classes that the custom field can be added to
65 65 # Set to nil for no restrictions
66 66 class_attribute :customized_class_names
67 67 self.customized_class_names = nil
68 68
69 69 # Name of the partial for editing the custom field
70 70 class_attribute :form_partial
71 71 self.form_partial = nil
72 72
73 73 class_attribute :change_as_diff
74 74 self.change_as_diff = false
75 75
76 76 def self.add(name)
77 77 self.format_name = name
78 78 Redmine::FieldFormat.add(name, self)
79 79 end
80 80 private_class_method :add
81 81
82 82 def self.field_attributes(*args)
83 83 CustomField.store_accessor :format_store, *args
84 84 end
85 85
86 86 field_attributes :url_pattern
87 87
88 88 def name
89 89 self.class.format_name
90 90 end
91 91
92 92 def label
93 93 "label_#{name}"
94 94 end
95 95
96 96 def cast_custom_value(custom_value)
97 97 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
98 98 end
99 99
100 100 def cast_value(custom_field, value, customized=nil)
101 101 if value.blank?
102 102 nil
103 103 elsif value.is_a?(Array)
104 104 casted = value.map do |v|
105 105 cast_single_value(custom_field, v, customized)
106 106 end
107 107 casted.compact.sort
108 108 else
109 109 cast_single_value(custom_field, value, customized)
110 110 end
111 111 end
112 112
113 113 def cast_single_value(custom_field, value, customized=nil)
114 114 value.to_s
115 115 end
116 116
117 117 def target_class
118 118 nil
119 119 end
120 120
121 121 def possible_custom_value_options(custom_value)
122 122 possible_values_options(custom_value.custom_field, custom_value.customized)
123 123 end
124 124
125 125 def possible_values_options(custom_field, object=nil)
126 126 []
127 127 end
128 128
129 129 # Returns the validation errors for custom_field
130 130 # Should return an empty array if custom_field is valid
131 131 def validate_custom_field(custom_field)
132 132 []
133 133 end
134 134
135 135 # Returns the validation error messages for custom_value
136 136 # Should return an empty array if custom_value is valid
137 137 def validate_custom_value(custom_value)
138 138 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
139 139 errors = values.map do |value|
140 140 validate_single_value(custom_value.custom_field, value, custom_value.customized)
141 141 end
142 142 errors.flatten.uniq
143 143 end
144 144
145 145 def validate_single_value(custom_field, value, customized=nil)
146 146 []
147 147 end
148 148
149 149 def formatted_custom_value(view, custom_value, html=false)
150 150 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
151 151 end
152 152
153 153 def formatted_value(view, custom_field, value, customized=nil, html=false)
154 154 casted = cast_value(custom_field, value, customized)
155 155 if html && custom_field.url_pattern.present?
156 156 texts_and_urls = Array.wrap(casted).map do |single_value|
157 157 text = view.format_object(single_value, false).to_s
158 158 url = url_from_pattern(custom_field, single_value, customized)
159 159 [text, url]
160 160 end
161 161 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to text, url}
162 162 links.join(', ').html_safe
163 163 else
164 164 casted
165 165 end
166 166 end
167 167
168 168 # Returns an URL generated with the custom field URL pattern
169 169 # and variables substitution:
170 170 # %value% => the custom field value
171 171 # %id% => id of the customized object
172 172 # %project_id% => id of the project of the customized object if defined
173 173 # %project_identifier% => identifier of the project of the customized object if defined
174 174 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
175 175 def url_from_pattern(custom_field, value, customized)
176 176 url = custom_field.url_pattern.to_s.dup
177 177 url.gsub!('%value%') {value.to_s}
178 178 url.gsub!('%id%') {customized.id.to_s}
179 179 url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
180 180 url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
181 181 if custom_field.regexp.present?
182 182 url.gsub!(%r{%m(\d+)%}) do
183 183 m = $1.to_i
184 184 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
185 185 matches[m].to_s
186 186 end
187 187 end
188 188 end
189 189 url
190 190 end
191 191 protected :url_from_pattern
192 192
193 193 def edit_tag(view, tag_id, tag_name, custom_value, options={})
194 194 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
195 195 end
196 196
197 197 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
198 198 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
199 199 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
200 200 end
201 201
202 202 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
203 203 if custom_field.is_required?
204 204 ''.html_safe
205 205 else
206 206 view.content_tag('label',
207 207 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
208 208 :class => 'inline'
209 209 )
210 210 end
211 211 end
212 212 protected :bulk_clear_tag
213 213
214 214 def query_filter_options(custom_field, query)
215 215 {:type => :string}
216 216 end
217 217
218 218 def before_custom_field_save(custom_field)
219 219 end
220 220
221 221 # Returns a ORDER BY clause that can used to sort customized
222 222 # objects by their value of the custom field.
223 223 # Returns nil if the custom field can not be used for sorting.
224 224 def order_statement(custom_field)
225 225 # COALESCE is here to make sure that blank and NULL values are sorted equally
226 226 "COALESCE(#{join_alias custom_field}.value, '')"
227 227 end
228 228
229 229 # Returns a GROUP BY clause that can used to group by custom value
230 230 # Returns nil if the custom field can not be used for grouping.
231 231 def group_statement(custom_field)
232 232 nil
233 233 end
234 234
235 235 # Returns a JOIN clause that is added to the query when sorting by custom values
236 236 def join_for_order_statement(custom_field)
237 237 alias_name = join_alias(custom_field)
238 238
239 239 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
240 240 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
241 241 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
242 242 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
243 243 " AND (#{custom_field.visibility_by_project_condition})" +
244 244 " AND #{alias_name}.value <> ''" +
245 245 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
246 246 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
247 247 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
248 248 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
249 249 end
250 250
251 251 def join_alias(custom_field)
252 252 "cf_#{custom_field.id}"
253 253 end
254 254 protected :join_alias
255 255 end
256 256
257 257 class Unbounded < Base
258 258 def validate_single_value(custom_field, value, customized=nil)
259 259 errs = super
260 260 value = value.to_s
261 261 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
262 262 errs << ::I18n.t('activerecord.errors.messages.invalid')
263 263 end
264 264 if custom_field.min_length && value.length < custom_field.min_length
265 265 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
266 266 end
267 267 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
268 268 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
269 269 end
270 270 errs
271 271 end
272 272 end
273 273
274 274 class StringFormat < Unbounded
275 275 add 'string'
276 276 self.searchable_supported = true
277 277 self.form_partial = 'custom_fields/formats/string'
278 278 field_attributes :text_formatting
279 279
280 280 def formatted_value(view, custom_field, value, customized=nil, html=false)
281 281 if html
282 282 if custom_field.url_pattern.present?
283 283 super
284 284 elsif custom_field.text_formatting == 'full'
285 285 view.textilizable(value, :object => customized)
286 286 else
287 287 value.to_s
288 288 end
289 289 else
290 290 value.to_s
291 291 end
292 292 end
293 293 end
294 294
295 295 class TextFormat < Unbounded
296 296 add 'text'
297 297 self.searchable_supported = true
298 298 self.form_partial = 'custom_fields/formats/text'
299 299 self.change_as_diff = true
300 300
301 301 def formatted_value(view, custom_field, value, customized=nil, html=false)
302 302 if html
303 303 if custom_field.text_formatting == 'full'
304 304 view.textilizable(value, :object => customized)
305 305 else
306 306 view.simple_format(html_escape(value))
307 307 end
308 308 else
309 309 value.to_s
310 310 end
311 311 end
312 312
313 313 def edit_tag(view, tag_id, tag_name, custom_value, options={})
314 314 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
315 315 end
316 316
317 317 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
318 318 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
319 319 '<br />'.html_safe +
320 320 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
321 321 end
322 322
323 323 def query_filter_options(custom_field, query)
324 324 {:type => :text}
325 325 end
326 326 end
327 327
328 328 class LinkFormat < StringFormat
329 329 add 'link'
330 330 self.searchable_supported = false
331 331 self.form_partial = 'custom_fields/formats/link'
332 332
333 333 def formatted_value(view, custom_field, value, customized=nil, html=false)
334 334 if html
335 335 if custom_field.url_pattern.present?
336 336 url = url_from_pattern(custom_field, value, customized)
337 337 else
338 338 url = value.to_s
339 339 unless url =~ %r{\A[a-z]+://}i
340 340 # no protocol found, use http by default
341 341 url = "http://" + url
342 342 end
343 343 end
344 344 view.link_to value.to_s, url
345 345 else
346 346 value.to_s
347 347 end
348 348 end
349 349 end
350 350
351 351 class Numeric < Unbounded
352 352 self.form_partial = 'custom_fields/formats/numeric'
353 353
354 354 def order_statement(custom_field)
355 355 # Make the database cast values into numeric
356 356 # Postgresql will raise an error if a value can not be casted!
357 357 # CustomValue validations should ensure that it doesn't occur
358 358 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
359 359 end
360 360 end
361 361
362 362 class IntFormat < Numeric
363 363 add 'int'
364 364
365 365 def label
366 366 "label_integer"
367 367 end
368 368
369 369 def cast_single_value(custom_field, value, customized=nil)
370 370 value.to_i
371 371 end
372 372
373 373 def validate_single_value(custom_field, value, customized=nil)
374 374 errs = super
375 375 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
376 376 errs
377 377 end
378 378
379 379 def query_filter_options(custom_field, query)
380 380 {:type => :integer}
381 381 end
382 382
383 383 def group_statement(custom_field)
384 384 order_statement(custom_field)
385 385 end
386 386 end
387 387
388 388 class FloatFormat < Numeric
389 389 add 'float'
390 390
391 391 def cast_single_value(custom_field, value, customized=nil)
392 392 value.to_f
393 393 end
394 394
395 395 def validate_single_value(custom_field, value, customized=nil)
396 396 errs = super
397 397 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
398 398 errs
399 399 end
400 400
401 401 def query_filter_options(custom_field, query)
402 402 {:type => :float}
403 403 end
404 404 end
405 405
406 406 class DateFormat < Unbounded
407 407 add 'date'
408 408 self.form_partial = 'custom_fields/formats/date'
409 409
410 410 def cast_single_value(custom_field, value, customized=nil)
411 411 value.to_date rescue nil
412 412 end
413 413
414 414 def validate_single_value(custom_field, value, customized=nil)
415 415 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
416 416 []
417 417 else
418 418 [::I18n.t('activerecord.errors.messages.not_a_date')]
419 419 end
420 420 end
421 421
422 422 def edit_tag(view, tag_id, tag_name, custom_value, options={})
423 423 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
424 424 view.calendar_for(tag_id)
425 425 end
426 426
427 427 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
428 428 view.text_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
429 429 view.calendar_for(tag_id) +
430 430 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
431 431 end
432 432
433 433 def query_filter_options(custom_field, query)
434 434 {:type => :date}
435 435 end
436 436
437 437 def group_statement(custom_field)
438 438 order_statement(custom_field)
439 439 end
440 440 end
441 441
442 442 class List < Base
443 443 self.multiple_supported = true
444 444 field_attributes :edit_tag_style
445 445
446 446 def edit_tag(view, tag_id, tag_name, custom_value, options={})
447 447 if custom_value.custom_field.edit_tag_style == 'check_box'
448 448 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
449 449 else
450 450 select_edit_tag(view, tag_id, tag_name, custom_value, options)
451 451 end
452 452 end
453 453
454 454 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
455 455 opts = []
456 456 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
457 457 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
458 458 opts += possible_values_options(custom_field, objects)
459 459 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
460 460 end
461 461
462 462 def query_filter_options(custom_field, query)
463 463 {:type => :list_optional, :values => possible_values_options(custom_field, query.project)}
464 464 end
465 465
466 466 protected
467 467
468 468 # Renders the edit tag as a select tag
469 469 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
470 470 blank_option = ''.html_safe
471 471 unless custom_value.custom_field.multiple?
472 472 if custom_value.custom_field.is_required?
473 473 unless custom_value.custom_field.default_value.present?
474 474 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
475 475 end
476 476 else
477 477 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
478 478 end
479 479 end
480 480 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
481 481 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
482 482 if custom_value.custom_field.multiple?
483 483 s << view.hidden_field_tag(tag_name, '')
484 484 end
485 485 s
486 486 end
487 487
488 488 # Renders the edit tag as check box or radio tags
489 489 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
490 490 opts = []
491 491 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
492 492 opts << ["(#{l(:label_none)})", '']
493 493 end
494 494 opts += possible_custom_value_options(custom_value)
495 495 s = ''.html_safe
496 496 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
497 497 opts.each do |label, value|
498 498 value ||= label
499 499 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
500 500 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
501 501 # set the id on the first tag only
502 502 tag_id = nil
503 503 s << view.content_tag('label', tag + ' ' + label)
504 504 end
505 505 if custom_value.custom_field.multiple?
506 506 s << view.hidden_field_tag(tag_name, '')
507 507 end
508 508 css = "#{options[:class]} check_box_group"
509 509 view.content_tag('span', s, options.merge(:class => css))
510 510 end
511 511 end
512 512
513 513 class ListFormat < List
514 514 add 'list'
515 515 self.searchable_supported = true
516 516 self.form_partial = 'custom_fields/formats/list'
517 517
518 518 def possible_custom_value_options(custom_value)
519 519 options = possible_values_options(custom_value.custom_field)
520 520 missing = [custom_value.value].flatten.reject(&:blank?) - options
521 521 if missing.any?
522 522 options += missing
523 523 end
524 524 options
525 525 end
526 526
527 527 def possible_values_options(custom_field, object=nil)
528 528 custom_field.possible_values
529 529 end
530 530
531 531 def validate_custom_field(custom_field)
532 532 errors = []
533 533 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
534 534 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
535 535 errors
536 536 end
537 537
538 538 def validate_custom_value(custom_value)
539 539 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
540 540 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
541 541 if invalid_values.any?
542 542 [::I18n.t('activerecord.errors.messages.inclusion')]
543 543 else
544 544 []
545 545 end
546 546 end
547 547
548 548 def group_statement(custom_field)
549 549 order_statement(custom_field)
550 550 end
551 551 end
552 552
553 553 class BoolFormat < List
554 554 add 'bool'
555 555 self.multiple_supported = false
556 556 self.form_partial = 'custom_fields/formats/bool'
557 557
558 558 def label
559 559 "label_boolean"
560 560 end
561 561
562 562 def cast_single_value(custom_field, value, customized=nil)
563 563 value == '1' ? true : false
564 564 end
565 565
566 566 def possible_values_options(custom_field, object=nil)
567 567 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
568 568 end
569 569
570 570 def group_statement(custom_field)
571 571 order_statement(custom_field)
572 572 end
573 573
574 574 def edit_tag(view, tag_id, tag_name, custom_value, options={})
575 575 case custom_value.custom_field.edit_tag_style
576 576 when 'check_box'
577 577 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
578 578 when 'radio'
579 579 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
580 580 else
581 581 select_edit_tag(view, tag_id, tag_name, custom_value, options)
582 582 end
583 583 end
584 584
585 585 # Renders the edit tag as a simple check box
586 586 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
587 587 s = ''.html_safe
588 588 s << view.hidden_field_tag(tag_name, '0', :id => nil)
589 589 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
590 590 view.content_tag('span', s, options)
591 591 end
592 592 end
593 593
594 594 class RecordList < List
595 self.customized_class_names = %w(Issue TimeEntry Version Project)
595 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
596 596
597 597 def cast_single_value(custom_field, value, customized=nil)
598 598 target_class.find_by_id(value.to_i) if value.present?
599 599 end
600 600
601 601 def target_class
602 602 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
603 603 end
604 604
605 605 def reset_target_class
606 606 @target_class = nil
607 607 end
608 608
609 609 def possible_custom_value_options(custom_value)
610 610 options = possible_values_options(custom_value.custom_field, custom_value.customized)
611 611 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
612 612 if missing.any?
613 613 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
614 614 options.sort_by!(&:first)
615 615 end
616 616 options
617 617 end
618 618
619 619 def order_statement(custom_field)
620 620 if target_class.respond_to?(:fields_for_order_statement)
621 621 target_class.fields_for_order_statement(value_join_alias(custom_field))
622 622 end
623 623 end
624 624
625 625 def group_statement(custom_field)
626 626 "COALESCE(#{join_alias custom_field}.value, '')"
627 627 end
628 628
629 629 def join_for_order_statement(custom_field)
630 630 alias_name = join_alias(custom_field)
631 631
632 632 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
633 633 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
634 634 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
635 635 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
636 636 " AND (#{custom_field.visibility_by_project_condition})" +
637 637 " AND #{alias_name}.value <> ''" +
638 638 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
639 639 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
640 640 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
641 641 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
642 642 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
643 643 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
644 644 end
645 645
646 646 def value_join_alias(custom_field)
647 647 join_alias(custom_field) + "_" + custom_field.field_format
648 648 end
649 649 protected :value_join_alias
650 650 end
651 651
652 652 class UserFormat < RecordList
653 653 add 'user'
654 654 self.form_partial = 'custom_fields/formats/user'
655 655 field_attributes :user_role
656 656
657 657 def possible_values_options(custom_field, object=nil)
658 658 if object.is_a?(Array)
659 659 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
660 660 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
661 661 elsif object.respond_to?(:project) && object.project
662 662 scope = object.project.users
663 663 if custom_field.user_role.is_a?(Array)
664 664 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
665 665 if role_ids.any?
666 666 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
667 667 end
668 668 end
669 669 scope.sorted.collect {|u| [u.to_s, u.id.to_s]}
670 670 else
671 671 []
672 672 end
673 673 end
674 674
675 675 def before_custom_field_save(custom_field)
676 676 super
677 677 if custom_field.user_role.is_a?(Array)
678 678 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
679 679 end
680 680 end
681 681 end
682 682
683 683 class VersionFormat < RecordList
684 684 add 'version'
685 685 self.form_partial = 'custom_fields/formats/version'
686 686 field_attributes :version_status
687 687
688 688 def possible_values_options(custom_field, object=nil)
689 689 if object.is_a?(Array)
690 690 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
691 691 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
692 692 elsif object.respond_to?(:project) && object.project
693 693 scope = object.project.shared_versions
694 694 if custom_field.version_status.is_a?(Array)
695 695 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
696 696 if statuses.any?
697 697 scope = scope.where(:status => statuses.map(&:to_s))
698 698 end
699 699 end
700 700 scope.sort.collect {|u| [u.to_s, u.id.to_s]}
701 701 else
702 702 []
703 703 end
704 704 end
705 705
706 706 def before_custom_field_save(custom_field)
707 707 super
708 708 if custom_field.version_status.is_a?(Array)
709 709 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
710 710 end
711 711 end
712 712 end
713 713 end
714 714 end
General Comments 0
You need to be logged in to leave comments. Login now