redmine_crm 0.0.23 → 0.0.25

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/doc/CHANGELOG +4 -0
  4. data/lib/redmine_crm.rb +28 -20
  5. data/lib/redmine_crm/{rcrm_acts_as_taggable.rb → acts_as_taggable/rcrm_acts_as_taggable.rb} +93 -87
  6. data/lib/redmine_crm/acts_as_taggable/tag.rb +81 -0
  7. data/lib/redmine_crm/acts_as_taggable/tag_list.rb +111 -0
  8. data/lib/redmine_crm/acts_as_taggable/tagging.rb +16 -0
  9. data/lib/redmine_crm/acts_as_viewed/rcrm_acts_as_viewed.rb +274 -0
  10. data/lib/redmine_crm/{rcrm_acts_as_votable.rb → acts_as_votable/rcrm_acts_as_votable.rb} +8 -11
  11. data/lib/redmine_crm/acts_as_votable/rcrm_acts_as_voter.rb +20 -0
  12. data/lib/redmine_crm/{votable.rb → acts_as_votable/votable.rb} +36 -47
  13. data/lib/redmine_crm/{vote.rb → acts_as_votable/vote.rb} +7 -10
  14. data/lib/redmine_crm/{voter.rb → acts_as_votable/voter.rb} +29 -34
  15. data/lib/redmine_crm/currency/formatting.rb +0 -3
  16. data/lib/redmine_crm/currency/heuristics.rb +1 -1
  17. data/lib/redmine_crm/currency/loader.rb +1 -1
  18. data/lib/redmine_crm/helpers/tags_helper.rb +1 -3
  19. data/lib/redmine_crm/helpers/vote_helper.rb +29 -32
  20. data/lib/redmine_crm/liquid/drops/issues_drop.rb +66 -0
  21. data/lib/redmine_crm/liquid/drops/news_drop.rb +54 -0
  22. data/lib/redmine_crm/liquid/drops/projects_drop.rb +86 -0
  23. data/lib/redmine_crm/liquid/drops/users_drop.rb +72 -0
  24. data/lib/redmine_crm/liquid/filters/arrays.rb +178 -0
  25. data/lib/redmine_crm/liquid/filters/base.rb +208 -0
  26. data/lib/redmine_crm/version.rb +1 -1
  27. data/redmine_crm.gemspec +1 -1
  28. data/test/{acts_as_taggable_test.rb → acts_as_taggable/rcrm_acts_as_taggable_test.rb} +114 -151
  29. data/test/acts_as_taggable/tag_list_test.rb +38 -0
  30. data/test/acts_as_taggable/tag_test.rb +74 -0
  31. data/test/acts_as_taggable/tagging_test.rb +15 -0
  32. data/test/{viewed_test.rb → acts_as_viewed/rcrm_acts_as_viewed_test.rb} +17 -15
  33. data/test/{votable_test.rb → acts_as_votable/rcrm_acts_as_votable_test.rb} +3 -3
  34. data/test/acts_as_votable/rcrm_acts_as_voter_test.rb +12 -0
  35. data/test/{votable_model_test.rb → acts_as_votable/votable_test.rb} +4 -4
  36. data/test/{voter_model_test.rb → acts_as_votable/voter_test.rb} +7 -7
  37. data/test/currency_test.rb +10 -10
  38. data/test/fixtures/issue.rb +6 -2
  39. data/test/fixtures/issues.yml +13 -1
  40. data/test/fixtures/news.rb +3 -0
  41. data/test/fixtures/news.yml +8 -0
  42. data/test/fixtures/project.rb +8 -0
  43. data/test/fixtures/projects.yml +10 -0
  44. data/test/fixtures/user.rb +5 -1
  45. data/test/fixtures/users.yml +3 -2
  46. data/test/fixtures/vote_classes.rb +2 -3
  47. data/test/liquid/drops/issues_drop_test.rb +34 -0
  48. data/test/liquid/drops/liquid_test.rb +52 -0
  49. data/test/liquid/drops/news_drop_test.rb +38 -0
  50. data/test/liquid/drops/projects_drop_test.rb +44 -0
  51. data/test/liquid/drops/uses_drop_test.rb +36 -0
  52. data/test/liquid/filters/arrays_filter_test.rb +24 -0
  53. data/test/liquid/filters/base_filter_test.rb +63 -0
  54. data/test/liquid/liquid_helper.rb +32 -0
  55. data/test/money_helper_test.rb +5 -5
  56. data/test/schema.rb +21 -9
  57. data/test/test_helper.rb +26 -25
  58. metadata +76 -28
  59. data/lib/redmine_crm/rcrm_acts_as_viewed.rb +0 -287
  60. data/lib/redmine_crm/rcrm_acts_as_voter.rb +0 -27
  61. data/lib/redmine_crm/tag.rb +0 -81
  62. data/lib/redmine_crm/tag_list.rb +0 -112
  63. data/lib/redmine_crm/tagging.rb +0 -20
  64. data/test/tag_test.rb +0 -64
  65. data/test/tagging_test.rb +0 -14
@@ -0,0 +1,81 @@
1
+ module RedmineCrm
2
+ module ActsAsTaggable #:nodoc:
3
+ class Tag < ActiveRecord::Base #:nodoc:
4
+ has_many :taggings, :dependent => :destroy
5
+
6
+ validates_presence_of :name
7
+ validates :name, :uniqueness => { :message => " not uniq tag" }
8
+ validates :name, :presence => true
9
+ cattr_accessor :destroy_unused
10
+ self.destroy_unused = false
11
+
12
+ attr_accessible :name if defined?(ActiveModel::MassAssignmentSecurity)
13
+
14
+ # LIKE is used for cross-database case-insensitivity
15
+ def self.find_or_create_with_like_by_name(name)
16
+ # find(:first, :conditions => ["name LIKE ?", name]) || create(:name => name)
17
+ where("LOWER(name) LIKE LOWER(?)", name).first || create(:name => name)
18
+ end
19
+
20
+ def ==(object)
21
+ super || (object.is_a?(Tag) && name == object.name)
22
+ end
23
+
24
+ def to_s
25
+ name
26
+ end
27
+
28
+ def count
29
+ read_attribute(:count).to_i
30
+ end
31
+
32
+ class << self
33
+ # Calculate the tag counts for all tags.
34
+ # :start_at - Restrict the tags to those created after a certain time
35
+ # :end_at - Restrict the tags to those created before a certain time
36
+ # :conditions - A piece of SQL conditions to add to the query
37
+ # :limit - The maximum number of tags to return
38
+ # :order - A piece of SQL to order by. Eg 'count desc' or 'taggings.created_at desc'
39
+ # :at_least - Exclude tags with a frequency less than the given value
40
+ # :at_most - Exclude tags with a frequency greater than the given value
41
+ def counts(options = {})
42
+ opt = options_for_counts(options)
43
+ select(opt[:select]).where(opt[:conditions]).joins(opt[:joins]).group(opt[:group])
44
+ end
45
+
46
+ def options_for_counts(options = {})
47
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :joins
48
+ options = options.dup
49
+
50
+ start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
51
+ end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
52
+
53
+ conditions = [
54
+ (sanitize_sql(options.delete(:conditions)) if options[:conditions]),
55
+ start_at,
56
+ end_at
57
+ ].compact
58
+
59
+ conditions = conditions.join(' AND ') if conditions.any?
60
+
61
+ joins = ["INNER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
62
+ joins << options.delete(:joins) if options[:joins]
63
+
64
+ at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
65
+ at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
66
+ having = 'COUNT(*) > 0'
67
+ having = [having, at_least, at_most].compact.join(' AND ')
68
+ group_by = "#{Tag.table_name}.id, #{Tag.table_name}.name"
69
+ # group_by << " AND #{having}" unless having.blank?
70
+
71
+ { :select => "#{Tag.table_name}.id, #{Tag.table_name}.name, COUNT(*) AS count",
72
+ :joins => joins.join(" "),
73
+ :conditions => conditions,
74
+ :group => group_by,
75
+ :having => having
76
+ }.update(options)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,111 @@
1
+ module RedmineCrm
2
+ module ActsAsTaggable #:nodoc:
3
+ class TagList < Array #:nodoc:
4
+ cattr_accessor :delimiter
5
+ self.delimiter = ','
6
+
7
+ def initialize(*args)
8
+ add(*args)
9
+ end
10
+
11
+ # Add tags to the tag_list. Duplicate or blank tags will be ignored.
12
+ #
13
+ # tag_list.add("Fun", "Happy")
14
+ #
15
+ # Use the <tt>:parse</tt> option to add an unparsed tag string.
16
+ #
17
+ # tag_list.add("Fun, Happy", :parse => true)
18
+ def add(*names)
19
+ extract_and_apply_options!(names)
20
+ concat(names)
21
+ clean!
22
+ self
23
+ end
24
+
25
+ # Remove specific tags from the tag_list.
26
+ #
27
+ # tag_list.remove("Sad", "Lonely")
28
+ #
29
+ # Like #add, the <tt>:parse</tt> option can be used to remove multiple tags in a string.
30
+ #
31
+ # tag_list.remove("Sad, Lonely", :parse => true)
32
+ def remove(*names)
33
+ extract_and_apply_options!(names)
34
+ delete_if { |name| names.include?(name) }
35
+ self
36
+ end
37
+
38
+ # Toggle the presence of the given tags.
39
+ # If a tag is already in the list it is removed, otherwise it is added.
40
+ def toggle(*names)
41
+ extract_and_apply_options!(names)
42
+
43
+ names.each do |name|
44
+ include?(name) ? delete(name) : push(name)
45
+ end
46
+
47
+ clean!
48
+ self
49
+ end
50
+
51
+ # Transform the tag_list into a tag string suitable for edting in a form.
52
+ # The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
53
+ #
54
+ # tag_list = TagList.new("Round", "Square,Cube")
55
+ # tag_list.to_s # 'Round, "Square,Cube"'
56
+ def to_s
57
+ clean!
58
+
59
+ map do |name|
60
+ name.include?(delimiter) ? "\"#{name}\"" : name
61
+ end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
62
+ end
63
+
64
+ private
65
+ # Remove whitespace, duplicates, and blanks.
66
+ def clean!
67
+ reject!(&:blank?)
68
+ map!(&:strip)
69
+ uniq!
70
+ end
71
+
72
+ def extract_and_apply_options!(args)
73
+ options = args.last.is_a?(Hash) ? args.pop : {}
74
+ options.assert_valid_keys :parse
75
+
76
+ args.map! { |a| self.class.from(a) } if options[:parse]
77
+
78
+ args.flatten!
79
+ end
80
+
81
+ class << self
82
+ # Returns a new TagList using the given tag string.
83
+ #
84
+ # tag_list = TagList.from("One , Two, Three")
85
+ # tag_list # ["One", "Two", "Three"]
86
+ def from(source)
87
+ tag_list = new
88
+
89
+ case source
90
+ when Array
91
+ tag_list.add(source)
92
+ else
93
+ string = source.to_s.dup
94
+
95
+ # Parse the quoted tags
96
+ [
97
+ /\s*#{delimiter}\s*(['"])(.*?)\1\s*/,
98
+ /^\s*(['"])(.*?)\1\s*#{delimiter}?/
99
+ ].each do |re|
100
+ string.gsub!(re) { tag_list << $2; "" }
101
+ end
102
+
103
+ tag_list.add(string.split(delimiter))
104
+ end
105
+
106
+ tag_list
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,16 @@
1
+ module RedmineCrm
2
+ module ActsAsTaggable #:nodoc:
3
+ class Tagging < ActiveRecord::Base #:nodoc:
4
+ belongs_to :tag
5
+ belongs_to :taggable, :polymorphic => true
6
+
7
+ after_destroy :destroy_tag_if_unused
8
+
9
+ private
10
+
11
+ def destroy_tag_if_unused
12
+ tag.destroy if Tag.destroy_unused && tag.taggings.count.zero?
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,274 @@
1
+ # Copyright (c) 2008 Damian Martinelli
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+ module RedmineCrm
22
+ module ActsAsViewed #:nodoc:
23
+ # == acts_as_viewed
24
+ # Adds views count capabilities to any ActiveRecord object.
25
+ # It has the ability to work with objects that have or don't special fields to keep a tally of the
26
+ # viewings for each object.
27
+ # In addition it will by default use the User model as the viewer object and keep the viewings per-user.
28
+ # It can be configured to use another class.
29
+ # The IP address are used to not repeat views from the same ip. Only one view are count by user or IP.
30
+ #
31
+ # Special methods are provided to create the viewings table and if needed, to add the special fields needed
32
+ # to keep per-objects viewings fast for access to viewed objects. Can be easily used in migrations.
33
+ #
34
+ # == Example of usage:
35
+ #
36
+ # class Video < ActiveRecord::Base
37
+ # acts_as_viewed
38
+ # end
39
+ #
40
+ # In a controller:
41
+ #
42
+ # bill = User.find_by_name 'bill'
43
+ # batman = Video.find_by_title 'Batman'
44
+ # toystory = Video.find_by_title 'Toy Story'
45
+ #
46
+ # batman.view request.remote_addr, bill
47
+ # toystory.view request.remote_addr, bill
48
+ #
49
+ # batman.view_count # => 1
50
+ #
51
+ #
52
+ module Viewed
53
+ class ViewedError < RuntimeError; end
54
+
55
+ def self.included(base) #:nodoc:
56
+ base.extend(ClassMethods)
57
+ end
58
+
59
+ module ClassMethods
60
+ # Make the model viewable.
61
+ # The Viewing model, holding the details of the viewings, will be created dynamically if it doesn't exist.
62
+ #
63
+ # * Adds a <tt>has_many :viewings</tt> association to the model for easy retrieval of the detailed viewings.
64
+ # * Adds a <tt>has_many :viewers</tt> association to the object.
65
+ # * Adds a <tt>has_many :viewings</tt> associations to the viewer class.
66
+ #
67
+ # === Options
68
+ # * <tt>:viewing_class</tt> -
69
+ # class of the model used for the viewings. Defaults to Viewing. This class will be dynamically created if not already defined.
70
+ # If the class is predefined, it must have in it the following definitions:
71
+ # <tt>belongs_to :viewed, :polymorphic => true</tt>
72
+ # <tt>belongs_to :viewer, :class_name => 'User', :foreign_key => :viewer_id</tt> replace user with the viewer class if needed.
73
+ # * <tt>:viewer_class</tt> -
74
+ # class of the model that creates the viewing.
75
+ # Defaults to User This class will NOT be created, so it must be defined in the app.
76
+ # Use the IP address to prevent multiple viewings from the same client.
77
+ #
78
+ def rcrm_acts_as_viewed(options = {})
79
+ # don't allow multiple calls
80
+ return if self.included_modules.include?(ActsAsViewed::Viewed::ViewMethods)
81
+ send :include, ActsAsViewed::Viewed::ViewMethods
82
+
83
+ # Create the model for ratings if it doesn't yet exist
84
+ viewing_class = options[:viewing_class] || 'Viewing'
85
+ viewer_class = options[:viewer_class] || 'User'
86
+
87
+ unless Object.const_defined?(viewing_class)
88
+ Object.class_eval <<-EOV
89
+ class #{viewing_class} < ActiveRecord::Base
90
+ belongs_to :viewed, :polymorphic => true
91
+ belongs_to :viewer, :class_name => #{viewer_class}, :foreign_key => :viewer_id
92
+ end
93
+ EOV
94
+ end
95
+
96
+ # Rails < 3
97
+ # write_inheritable_attribute( :acts_as_viewed_options ,
98
+ # { :viewing_class => viewing_class,
99
+ # :viewer_class => viewer_class } )
100
+ # class_inheritable_reader :acts_as_viewed_options
101
+
102
+ # Rails >= 3
103
+ class_attribute :acts_as_viewed_options
104
+ self.acts_as_viewed_options = { :viewing_class => viewing_class,
105
+ :viewer_class => viewer_class }
106
+ class_eval do
107
+ has_many :viewings, :as => :viewed, :dependent => :delete_all, :class_name => viewing_class.to_s
108
+ has_many(:viewers, :through => :viewings, :class_name => viewer_class.to_s)
109
+
110
+ before_create :init_viewing_fields
111
+ end
112
+
113
+ # Add to the User (or whatever the viewer is) a has_many viewings
114
+ viewer_as_class = viewer_class.constantize
115
+ return if viewer_as_class.instance_methods.include?('find_in_viewings')
116
+ viewer_as_class.class_eval <<-EOS
117
+ has_many :viewings, :foreign_key => :viewer_id, :class_name => #{viewing_class.to_s}
118
+ EOS
119
+ end
120
+ end
121
+
122
+ module ViewMethods
123
+ def self.included(base) #:nodoc:
124
+ base.extend ClassMethods
125
+ end
126
+
127
+ # Is this object viewed already?
128
+ def viewed?
129
+ return (!self.views.nil? && self.views > 0) if attributes.has_key? 'views'
130
+ !viewings.first.nil?
131
+ end
132
+
133
+ # Get the number of viewings for this object based on the views field,
134
+ # or with a SQL query if the viewed objects doesn't have the views field
135
+ def view_count
136
+ return ("#{self.total_views}(#{self.views})" || 0) if attributes.has_key? 'views'
137
+ viewings.count
138
+ end
139
+
140
+ # Change views count (total_views and views) if it's existing in object
141
+ # If options[:only_total] == true count of unique views doesn't change
142
+ def increase_views_count(options)
143
+ if attributes.has_key?('views') && attributes.has_key?('total_views')
144
+ target = self
145
+ target.views = ((target.views || 0) + 1) unless options[:only_total]
146
+ target.total_views = ((target.total_views || 0) + 1)
147
+ target.save(:validate => false)
148
+ # target.save_without_validation
149
+ end
150
+ end
151
+
152
+ # View the object with or without a viewer - create new or update as needed
153
+ #
154
+ # * <tt>ip</tt> - the viewer ip
155
+ # * <tt>viewer</tt> - an object of the viewer class. Must be valid and with an id to be used. Or nil
156
+ def view(ip, viewer = nil)
157
+ # Sanity checks for the parameters
158
+ viewing_class = acts_as_viewed_options[:viewing_class].constantize
159
+ if viewer && !(acts_as_viewed_options[:viewer_class].constantize === viewer)
160
+ raise ViewedError, "the viewer object must be the one used when defining acts_as_viewed (or a descendent of it). other objects are not acceptable"
161
+ end
162
+
163
+ viewing_class.transaction do
164
+ if !viewed_by? ip, viewer
165
+ view = viewing_class.new
166
+ view.viewer_id = viewer.id if viewer && !viewer.id.nil?
167
+ view.ip = ip
168
+ viewings << view
169
+ view.save
170
+ increase_views_count(:only_total => false)
171
+ else
172
+ increase_views_count(:only_total => true)
173
+ end
174
+ true
175
+ end
176
+ end
177
+
178
+ # Check if an item was already viewed by the given viewer
179
+ def viewed_by?(ip, viewer = nil)
180
+ if viewer && !viewer.nil? && !(acts_as_viewed_options[:viewer_class].constantize === viewer)
181
+ raise ViewedError, "the viewer object must be the one used when defining acts_as_viewed (or a descendent of it). other objects are not acceptable"
182
+ end
183
+ if viewer && !viewer.id.nil? && !viewer.anonymous?
184
+ return viewings.where("viewer_id = '#{viewer.id}'").any?
185
+ else
186
+ return viewings.where("ip = '#{ip}'").any?
187
+ end
188
+ end
189
+
190
+ private
191
+
192
+ def init_viewing_fields #:nodoc:
193
+ self.views ||= 0 if attributes.has_key?('views')
194
+ end
195
+ end
196
+
197
+ module ClassMethods
198
+ # Generate the viewings columns on a table, to be used when creating the table
199
+ # in a migration. This is the preferred way to do in a migration that creates
200
+ # new tables as it will make it as part of the table creation, and not generate
201
+ # ALTER TABLE calls after the fact
202
+ def generate_viewings_columns(table)
203
+ table.column :views, :integer # uniq views
204
+ table.column :total_views, :integer
205
+ end
206
+
207
+ # Create the needed columns for acts_as_viewed.
208
+ # To be used during migration, but can also be used in other places.
209
+ def add_viewings_columns
210
+ if !self.content_columns.find { |c| 'views' == c.name }
211
+ self.connection.add_column table_name, :views, :integer, :default => '0'
212
+ self.connection.add_column table_name, :total_views, :integer, :default => '0'
213
+ self.reset_column_information
214
+ end
215
+ end
216
+
217
+ # Remove the acts_as_viewed specific columns added with add_viewings_columns
218
+ # To be used during migration, but can also be used in other places
219
+ def remove_viewings_columns
220
+ if self.content_columns.find { |c| 'views' == c.name }
221
+ self.connection.remove_column table_name, :views
222
+ self.connection.remove_column table_name, :total_views
223
+ self.reset_column_information
224
+ end
225
+ end
226
+
227
+ # Create the viewings table
228
+ # === Options hash:
229
+ # * <tt>:table_name</tt> - use a table name other than viewings
230
+ # To be used during migration, but can also be used in other places
231
+ def create_viewings_table(options = {})
232
+ name = options[:table_name] || :viewings
233
+ if !self.connection.table_exists?(name)
234
+ self.connection.create_table(name) do |t|
235
+ t.column :viewer_id, :integer
236
+ t.column :viewed_id, :integer
237
+ t.column :viewed_type, :string
238
+ t.column :ip, :string, :limit => '24'
239
+ t.column :created_at, :datetime
240
+ end
241
+
242
+ self.connection.add_index(name, :viewer_id)
243
+ self.connection.add_index(name, [:viewed_type, :viewed_id])
244
+ end
245
+ end
246
+
247
+ # Drop the viewings table.
248
+ # === Options hash:
249
+ # * <tt>:table_name</tt> - the name of the viewings table, defaults to viewings
250
+ # To be used during migration, but can also be used in other places
251
+ def drop_viewings_table(options = {})
252
+ name = options[:table_name] || :viewings
253
+ if self.connection.table_exists?(name)
254
+ self.connection.drop_table(name)
255
+ end
256
+ end
257
+
258
+ # Find all viewings for a specific viewer.
259
+ def find_viewed_by(viewer)
260
+ viewing_class = acts_as_viewed_options[:viewing_class].constantize
261
+ if !(acts_as_viewed_options[:viewer_class].constantize === viewer)
262
+ raise ViewedError, "The viewer object must be the one used when defining acts_as_viewed (or a descendent of it). other objects are not acceptable"
263
+ end
264
+ raise ViewedError, 'Viewer must be a valid and existing object' if viewer.nil? || viewer.id.nil?
265
+ raise ViewedError, 'Viewer must be a valid viewer' if !viewing_class.column_names.include?('viewer_id')
266
+ conds = ['viewed_type = ? AND viewer_id = ?', self.name, viewer.id]
267
+ acts_as_viewed_options[:viewing_class].constantize.where(conds).collect { |r| r.viewed_type.constantize.find_by_id r.viewed.id }
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+ ActiveRecord::Base.send :include, RedmineCrm::ActsAsViewed::Viewed