redmine_crm 0.0.23 → 0.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/Gemfile +1 -1
  4. data/README.md +166 -33
  5. data/Rakefile +3 -12
  6. data/bitbucket-pipelines.yml +42 -0
  7. data/config/currency_iso.json +12 -0
  8. data/doc/CHANGELOG +81 -2
  9. data/lib/redmine_crm/acts_as_draftable/draft.rb +40 -0
  10. data/lib/redmine_crm/acts_as_draftable/rcrm_acts_as_draftable.rb +170 -0
  11. data/lib/redmine_crm/acts_as_list/list.rb +282 -0
  12. data/lib/redmine_crm/{rcrm_acts_as_taggable.rb → acts_as_taggable/rcrm_acts_as_taggable.rb} +105 -93
  13. data/lib/redmine_crm/acts_as_taggable/tag.rb +81 -0
  14. data/lib/redmine_crm/acts_as_taggable/tag_list.rb +111 -0
  15. data/lib/redmine_crm/acts_as_taggable/tagging.rb +16 -0
  16. data/lib/redmine_crm/acts_as_viewed/rcrm_acts_as_viewed.rb +274 -0
  17. data/lib/redmine_crm/{rcrm_acts_as_votable.rb → acts_as_votable/rcrm_acts_as_votable.rb} +15 -14
  18. data/lib/redmine_crm/acts_as_votable/rcrm_acts_as_voter.rb +20 -0
  19. data/lib/redmine_crm/{votable.rb → acts_as_votable/votable.rb} +54 -65
  20. data/lib/redmine_crm/{vote.rb → acts_as_votable/vote.rb} +6 -8
  21. data/lib/redmine_crm/{voter.rb → acts_as_votable/voter.rb} +29 -34
  22. data/lib/redmine_crm/assets_manager.rb +43 -0
  23. data/lib/redmine_crm/colors_helper.rb +192 -0
  24. data/lib/redmine_crm/compatibility/application_controller_patch.rb +33 -0
  25. data/lib/redmine_crm/currency/formatting.rb +0 -3
  26. data/lib/redmine_crm/currency/heuristics.rb +1 -1
  27. data/lib/redmine_crm/currency/loader.rb +5 -6
  28. data/lib/redmine_crm/helpers/external_assets_helper.rb +19 -0
  29. data/lib/redmine_crm/helpers/form_tag_helper.rb +76 -0
  30. data/lib/redmine_crm/helpers/tags_helper.rb +1 -3
  31. data/lib/redmine_crm/helpers/vote_helper.rb +29 -32
  32. data/lib/redmine_crm/liquid/drops/issues_drop.rb +191 -0
  33. data/lib/redmine_crm/liquid/drops/news_drop.rb +54 -0
  34. data/lib/redmine_crm/liquid/drops/projects_drop.rb +86 -0
  35. data/lib/redmine_crm/liquid/drops/time_entries_drop.rb +65 -0
  36. data/lib/redmine_crm/liquid/drops/users_drop.rb +68 -0
  37. data/lib/redmine_crm/liquid/filters/arrays.rb +187 -0
  38. data/lib/redmine_crm/liquid/filters/base.rb +217 -0
  39. data/lib/redmine_crm/liquid/filters/colors.rb +31 -0
  40. data/lib/redmine_crm/money_helper.rb +2 -4
  41. data/lib/redmine_crm/version.rb +1 -1
  42. data/lib/redmine_crm.rb +56 -21
  43. data/redmine_crm.gemspec +9 -4
  44. data/test/acts_as_draftable/draft_test.rb +29 -0
  45. data/test/acts_as_draftable/rcrm_acts_as_draftable_test.rb +179 -0
  46. data/test/{acts_as_taggable_test.rb → acts_as_taggable/rcrm_acts_as_taggable_test.rb} +117 -156
  47. data/test/acts_as_taggable/tag_list_test.rb +34 -0
  48. data/test/acts_as_taggable/tag_test.rb +72 -0
  49. data/test/acts_as_taggable/tagging_test.rb +15 -0
  50. data/test/{viewed_test.rb → acts_as_viewed/rcrm_acts_as_viewed_test.rb} +17 -15
  51. data/test/acts_as_votable/rcrm_acts_as_votable_test.rb +19 -0
  52. data/test/acts_as_votable/rcrm_acts_as_voter_test.rb +14 -0
  53. data/test/{votable_model_test.rb → acts_as_votable/votable_test.rb} +34 -5
  54. data/test/{voter_model_test.rb → acts_as_votable/voter_test.rb} +8 -8
  55. data/test/currency_test.rb +10 -10
  56. data/test/database.yml +14 -14
  57. data/test/fixtures/issues.yml +13 -1
  58. data/test/fixtures/news.yml +8 -0
  59. data/test/fixtures/projects.yml +10 -0
  60. data/test/fixtures/users.yml +6 -2
  61. data/test/liquid/drops/issues_drop_test.rb +34 -0
  62. data/test/liquid/drops/news_drop_test.rb +38 -0
  63. data/test/liquid/drops/projects_drop_test.rb +44 -0
  64. data/test/liquid/drops/uses_drop_test.rb +36 -0
  65. data/test/liquid/filters/arrays_filter_test.rb +31 -0
  66. data/test/liquid/filters/base_filter_test.rb +63 -0
  67. data/test/liquid/filters/colors_filter_test.rb +33 -0
  68. data/test/liquid/liquid_helper.rb +34 -0
  69. data/test/models/issue.rb +14 -0
  70. data/test/models/news.rb +3 -0
  71. data/test/models/project.rb +8 -0
  72. data/test/{fixtures → models}/user.rb +5 -1
  73. data/test/{fixtures → models}/vote_classes.rb +0 -21
  74. data/test/money_helper_test.rb +5 -5
  75. data/test/schema.rb +33 -10
  76. data/test/test_helper.rb +20 -72
  77. data/vendor/assets/images/vcard.png +0 -0
  78. data/vendor/assets/javascripts/Chart.bundle.min.js +16 -0
  79. data/vendor/assets/javascripts/select2.js +3 -0
  80. data/vendor/assets/javascripts/select2_helpers.js +186 -0
  81. data/vendor/assets/stylesheets/select2.css +414 -0
  82. metadata +162 -38
  83. data/lib/redmine_crm/rcrm_acts_as_viewed.rb +0 -287
  84. data/lib/redmine_crm/rcrm_acts_as_voter.rb +0 -27
  85. data/lib/redmine_crm/tag.rb +0 -81
  86. data/lib/redmine_crm/tag_list.rb +0 -112
  87. data/lib/redmine_crm/tagging.rb +0 -20
  88. data/test/fixtures/issue.rb +0 -14
  89. data/test/tag_test.rb +0 -64
  90. data/test/tagging_test.rb +0 -14
  91. data/test/votable_test.rb +0 -17
@@ -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 do
117
+ has_many :viewings, foreign_key: :viewer_id, class_name: viewing_class.to_s, dependent: :delete_all
118
+ end
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.record_timestamps = false
148
+ target.save(:validate => false, :touch => false)
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
@@ -3,32 +3,30 @@ require 'active_record'
3
3
  module RedmineCrm
4
4
  module ActsAsVotable #:nodoc:
5
5
  module Votable #:nodoc:
6
-
7
6
  def votable?
8
7
  false
9
8
  end
10
9
 
11
10
  def rcrm_acts_as_votable
12
- require 'redmine_crm/votable'
13
- include ActsAsVotable::Votable
11
+ require 'redmine_crm/acts_as_votable/votable'
12
+ include RedmineCrm::ActsAsVotable::Votable
14
13
 
15
14
  class_eval do
16
15
  def self.votable?
17
16
  true
18
17
  end
19
18
  end
20
-
21
19
  end
22
20
 
23
21
  def create_index(table_name, column_name)
24
22
  return if self.connection.index_exists?(table_name, column_name)
25
-
23
+
26
24
  self.connection.add_index table_name, column_name
27
25
  end
28
26
 
29
- def create_votable_table options = {}
27
+ def create_votable_table(options = {})
30
28
  votes_name_table = options[:votes] || :votes
31
-
29
+
32
30
  if !self.connection.table_exists?(votes_name_table)
33
31
  self.connection.create_table(votes_name_table) do |t|
34
32
  t.references :votable, :polymorphic => true
@@ -37,6 +35,7 @@ module RedmineCrm
37
35
  t.column :vote_flag, :boolean
38
36
  t.column :vote_scope, :string
39
37
  t.column :vote_weight, :integer
38
+ t.column :vote_ip, :string
40
39
 
41
40
  t.timestamps
42
41
  end
@@ -48,7 +47,8 @@ module RedmineCrm
48
47
  :voter_type => :string,
49
48
  :vote_flag => :boolean,
50
49
  :vote_scope => :string,
51
- :vote_weight => :integer
50
+ :vote_weight => :integer,
51
+ :vote_ip => :string
52
52
  }
53
53
  fields.each do |name, type|
54
54
  if !self.connection.column_exists?(votes_name_table, name)
@@ -59,21 +59,22 @@ module RedmineCrm
59
59
  end
60
60
 
61
61
  if self.parent::VERSION::MAJOR < 4
62
- create_index votes_name_table, [:votable_id, :votable_type]
63
- create_index votes_name_table, [:voter_id, :voter_type]
62
+ create_index votes_name_table, [:votable_id, :votable_type, :vote_ip]
63
+ create_index votes_name_table, [:voter_id, :voter_type, :vote_ip]
64
64
  end
65
65
 
66
66
  create_index votes_name_table, [:voter_id, :voter_type, :vote_scope]
67
67
  create_index votes_name_table, [:votable_id, :votable_type, :vote_scope]
68
+ create_index votes_name_table, [:voter_type, :vote_scope, :vote_ip]
69
+ create_index votes_name_table, [:votable_type, :vote_scope, :vote_ip]
68
70
  end
69
71
 
70
- def drop_votable_table options = {}
71
- votes_name_table = options[:votes] || :votes
72
+ def drop_votable_table(options = {})
73
+ votes_name_table = options[:votes] || :votes
72
74
  if self.connection.table_exists?(votes_name_table)
73
75
  self.connection.drop_table votes_name_table
74
76
  end
75
77
  end
76
-
77
78
  end
78
79
  end
79
- end
80
+ end
@@ -0,0 +1,20 @@
1
+ module RedmineCrm
2
+ module ActsAsVotable
3
+ module Voter
4
+ def voter?
5
+ false
6
+ end
7
+
8
+ def rcrm_acts_as_voter(*args)
9
+ require 'redmine_crm/acts_as_votable/voter'
10
+ include RedmineCrm::ActsAsVotable::Voter
11
+
12
+ class_eval do
13
+ def self.voter?
14
+ true
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -3,11 +3,9 @@ require 'redmine_crm/helpers/vote_helper'
3
3
  module RedmineCrm
4
4
  module ActsAsVotable
5
5
  module Votable
6
-
7
6
  include ActsAsVotable::Helpers::Words
8
7
 
9
- def self.included base
10
-
8
+ def self.included(base)
11
9
  # allow the user to define these himself
12
10
  aliases = {
13
11
 
@@ -45,14 +43,13 @@ module RedmineCrm
45
43
  alias_method(new_method, method)
46
44
  end
47
45
  end
48
-
49
46
  end
50
47
  end
51
48
 
52
49
  attr_accessor :vote_registered
53
50
 
54
51
  def vote_registered?
55
- return self.vote_registered
52
+ self.vote_registered
56
53
  end
57
54
 
58
55
  def default_conditions
@@ -63,8 +60,7 @@ module RedmineCrm
63
60
  end
64
61
 
65
62
  # voting
66
- def vote_by args = {}
67
-
63
+ def vote_by(args = {})
68
64
  options = {
69
65
  :vote => true,
70
66
  :vote_scope => nil
@@ -72,71 +68,68 @@ module RedmineCrm
72
68
 
73
69
  self.vote_registered = false
74
70
 
75
- if options[:voter].nil?
76
- return false
77
- end
71
+ return false if options[:voter].nil?
78
72
 
79
73
  # find the vote
80
- _votes_ = find_votes_for({
81
- :voter_id => options[:voter].id,
82
- :vote_scope => options[:vote_scope],
83
- :voter_type => options[:voter].class.base_class.name
84
- })
74
+ vote_conditions = { :vote_scope => options[:vote_scope], :voter_type => options[:voter].class.base_class.name }
75
+ vote_conditions.merge!(options[:vote_by_ip] ? { :vote_ip => options[:vote_ip] } : { :voter_id => options[:voter].id} )
76
+ votes_for = find_votes_for(vote_conditions)
85
77
 
86
- if _votes_.count == 0 or options[:duplicate]
78
+ if votes_for.count == 0 || options[:duplicate]
87
79
  # this voter has never voted
88
- vote = RedmineCrm::ActsAsVotable::Vote.new(
89
- :votable => self,
90
- :voter => options[:voter],
91
- :vote_scope => options[:vote_scope]
92
- )
80
+ vote_params = { :votable => self, :voter => options[:voter], :vote_scope => options[:vote_scope] }
81
+ vote_params[:vote_ip] = options[:vote_ip] if options[:vote_ip]
82
+ vote = RedmineCrm::ActsAsVotable::Vote.new(vote_params)
93
83
  else
94
84
  # this voter is potentially changing his vote
95
- vote = _votes_.last
85
+ vote = votes_for.last
96
86
  end
97
87
 
98
- last_update = vote.updated_at
99
-
100
88
  vote.vote_flag = votable_words.meaning_of(options[:vote])
101
89
 
102
- #Allowing for a vote_weight to be associated with every vote. Could change with every voter object
90
+ # Allowing for a vote_weight to be associated with every vote. Could change with every voter object
103
91
  vote.vote_weight = (options[:vote_weight].to_i if options[:vote_weight].present?) || 1
104
92
 
105
- if vote.save
106
- self.vote_registered = true if last_update != vote.updated_at
107
- update_cached_votes options[:vote_scope]
108
- return true
109
- else
110
- self.vote_registered = false
111
- return false
112
- end
93
+ return false if vote.invalid?
113
94
 
95
+ self.vote_registered = vote.changed?
96
+ vote.save!(validate: false)
97
+ update_cached_votes(options[:vote_scope])
98
+ vote
114
99
  end
115
100
 
116
- def unvote args = {}
117
- return false if args[:voter].nil?
118
- _votes_ = find_votes_for(:voter_id => args[:voter].id, :vote_scope => args[:vote_scope], :voter_type => args[:voter].class.base_class.name)
101
+ def unvote(args = {})
102
+ return false if (!args[:vote_by_ip] && args[:voter].nil?) || (args[:vote_by_ip] && args[:vote_ip].nil?)
103
+ vote_conditions = { :vote_scope => args[:vote_scope], :voter_type => args[:voter].class.base_class.name }
104
+ vote_conditions.merge!(args[:vote_by_ip] ? { :vote_ip => args[:vote_ip] } : { :voter_id => args[:voter].id})
105
+ votes_for = find_votes_for(vote_conditions)
119
106
 
120
- return true if _votes_.size == 0
121
- _votes_.each(&:destroy)
107
+ return true if votes_for.empty?
108
+ votes_for.each(&:destroy)
122
109
  update_cached_votes args[:vote_scope]
123
110
  self.vote_registered = false if votes_for.count == 0
124
- return true
111
+ true
125
112
  end
126
113
 
127
- def vote_up voter, options={}
128
- self.vote_by :voter => voter, :vote => true, :vote_scope => options[:vote_scope], :vote_weight => options[:vote_weight]
114
+ def vote_up(voter, options={})
115
+ self.vote_by :voter => voter, :vote => true,
116
+ :vote_scope => options[:vote_scope], :vote_weight => options[:vote_weight], :vote_ip => options[:vote_ip],
117
+ :vote_by_ip => options[:vote_by_ip]
129
118
  end
130
119
 
131
- def vote_down voter, options={}
132
- self.vote_by :voter => voter, :vote => false, :vote_scope => options[:vote_scope], :vote_weight => options[:vote_weight]
120
+ def vote_down(voter, options={})
121
+ self.vote_by :voter => voter, :vote => false,
122
+ :vote_scope => options[:vote_scope], :vote_weight => options[:vote_weight], :vote_ip => options[:vote_ip],
123
+ :vote_by_ip => options[:vote_by_ip]
133
124
  end
134
125
 
135
- def unvote_by voter, options = {}
136
- self.unvote :voter => voter, :vote_scope => options[:vote_scope] #Does not need vote_weight since the votes_for are anyway getting destroyed
126
+ def unvote_by(voter, options = {})
127
+ # Does not need vote_weight since the votes_for are anyway getting destroyed
128
+ self.unvote :voter => voter, :vote_scope => options[:vote_scope], :vote_ip => options[:vote_ip],
129
+ :vote_by_ip => options[:vote_by_ip]
137
130
  end
138
131
 
139
- def scope_cache_field field, vote_scope
132
+ def scope_cache_field(field, vote_scope)
140
133
  return field if vote_scope.nil?
141
134
 
142
135
  case field
@@ -172,8 +165,7 @@ module RedmineCrm
172
165
  end
173
166
 
174
167
  # caching
175
- def update_cached_votes vote_scope = nil
176
-
168
+ def update_cached_votes(vote_scope = nil)
177
169
  updates = {}
178
170
 
179
171
  if self.respond_to?(:cached_votes_total=)
@@ -239,55 +231,52 @@ module RedmineCrm
239
231
  updates[scope_cache_field :cached_weighted_average, vote_scope] = weighted_average(true, vote_scope)
240
232
  end
241
233
  end
242
-
234
+ self.record_timestamps = false
243
235
  if (::ActiveRecord::VERSION::MAJOR == 3) && (::ActiveRecord::VERSION::MINOR != 0)
244
- self.update_attributes(updates, :without_protection => true) if updates.size > 0
236
+ self.update_attributes(updates, :without_protection => true) if !updates.empty?
245
237
  else
246
- self.update_attributes(updates) if updates.size > 0
238
+ self.update_attributes(updates) if !updates.empty?
247
239
  end
248
-
249
240
  end
250
241
 
251
-
252
242
  # results
253
- def find_votes_for extra_conditions = {}
243
+ def find_votes_for(extra_conditions = {})
254
244
  votes_for.where(extra_conditions)
255
245
  end
256
246
 
257
- def get_up_votes options={}
247
+ def get_up_votes(options = {})
258
248
  vote_scope_hash = scope_or_empty_hash(options[:vote_scope])
259
249
  find_votes_for({:vote_flag => true}.merge(vote_scope_hash))
260
250
  end
261
251
 
262
- def get_down_votes options={}
252
+ def get_down_votes(options = {})
263
253
  vote_scope_hash = scope_or_empty_hash(options[:vote_scope])
264
- find_votes_for({:vote_flag => false}.merge(vote_scope_hash))
254
+ find_votes_for({ :vote_flag => false }.merge(vote_scope_hash))
265
255
  end
266
256
 
267
-
268
257
  # counting
269
- def count_votes_total skip_cache = false, vote_scope = nil
258
+ def count_votes_total(skip_cache = false, vote_scope = nil)
270
259
  if !skip_cache && self.respond_to?(scope_cache_field :cached_votes_total, vote_scope)
271
260
  return self.send(scope_cache_field :cached_votes_total, vote_scope)
272
261
  end
273
262
  find_votes_for(scope_or_empty_hash(vote_scope)).count
274
263
  end
275
264
 
276
- def count_votes_up skip_cache = false, vote_scope = nil
265
+ def count_votes_up(skip_cache = false, vote_scope = nil)
277
266
  if !skip_cache && self.respond_to?(scope_cache_field :cached_votes_up, vote_scope)
278
267
  return self.send(scope_cache_field :cached_votes_up, vote_scope)
279
268
  end
280
269
  get_up_votes(:vote_scope => vote_scope).count
281
270
  end
282
271
 
283
- def count_votes_down skip_cache = false, vote_scope = nil
272
+ def count_votes_down(skip_cache = false, vote_scope = nil)
284
273
  if !skip_cache && self.respond_to?(scope_cache_field :cached_votes_down, vote_scope)
285
274
  return self.send(scope_cache_field :cached_votes_down, vote_scope)
286
275
  end
287
276
  get_down_votes(:vote_scope => vote_scope).count
288
277
  end
289
278
 
290
- def weighted_total skip_cache = false, vote_scope = nil
279
+ def weighted_total(skip_cache = false, vote_scope = nil)
291
280
  if !skip_cache && self.respond_to?(scope_cache_field :cached_weighted_total, vote_scope)
292
281
  return self.send(scope_cache_field :cached_weighted_total, vote_scope)
293
282
  end
@@ -296,7 +285,7 @@ module RedmineCrm
296
285
  ups + downs
297
286
  end
298
287
 
299
- def weighted_score skip_cache = false, vote_scope = nil
288
+ def weighted_score(skip_cache = false, vote_scope = nil)
300
289
  if !skip_cache && self.respond_to?(scope_cache_field :cached_weighted_score, vote_scope)
301
290
  return self.send(scope_cache_field :cached_weighted_score, vote_scope)
302
291
  end
@@ -305,7 +294,7 @@ module RedmineCrm
305
294
  ups - downs
306
295
  end
307
296
 
308
- def weighted_average skip_cache = false, vote_scope = nil
297
+ def weighted_average(skip_cache = false, vote_scope = nil)
309
298
  if !skip_cache && self.respond_to?(scope_cache_field :cached_weighted_average, vote_scope)
310
299
  return self.send(scope_cache_field :cached_weighted_average, vote_scope)
311
300
  end
@@ -319,7 +308,7 @@ module RedmineCrm
319
308
  end
320
309
 
321
310
  # voters
322
- def voted_on_by? voter
311
+ def voted_on_by?(voter)
323
312
  votes = find_votes_for :voter_id => voter.id, :voter_type => voter.class.base_class.name
324
313
  votes.count > 0
325
314
  end
@@ -3,28 +3,26 @@ require 'redmine_crm/helpers/vote_helper'
3
3
  module RedmineCrm
4
4
  module ActsAsVotable
5
5
  class Vote < ActiveRecord::Base
6
-
7
6
  include Helpers::Words
8
7
 
9
8
  if defined?(ProtectedAttributes) || ::ActiveRecord::VERSION::MAJOR < 4
10
9
  attr_accessible :votable_id, :votable_type,
11
10
  :voter_id, :voter_type,
12
11
  :votable, :voter,
13
- :vote_flag, :vote_scope
12
+ :vote_flag, :vote_scope,
13
+ :vote_ip
14
14
  end
15
15
 
16
16
  belongs_to :votable, :polymorphic => true
17
17
  belongs_to :voter, :polymorphic => true
18
18
 
19
- scope :up, lambda{ where(:vote_flag => true) }
20
- scope :down, lambda{ where(:vote_flag => false) }
21
- scope :for_type, lambda{ |klass| where(:votable_type => klass) }
22
- scope :by_type, lambda{ |klass| where(:voter_type => klass) }
19
+ scope :up, lambda { where(:vote_flag => true) }
20
+ scope :down, lambda { where(:vote_flag => false) }
21
+ scope :for_type, lambda { |klass| where(:votable_type => klass.to_s) }
22
+ scope :by_type, lambda { |klass| where(:voter_type => klass.to_s) }
23
23
 
24
24
  validates_presence_of :votable_id
25
25
  validates_presence_of :voter_id
26
-
27
26
  end
28
-
29
27
  end
30
28
  end