redmine_crm 0.0.23 → 0.0.53
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -1
- data/README.md +166 -33
- data/Rakefile +3 -12
- data/app/controllers/redmine_crm_controller.rb +26 -0
- data/app/views/redmine_crm/_money.html.erb +44 -0
- data/app/views/redmine_crm/settings.html.erb +10 -0
- data/bitbucket-pipelines.yml +54 -0
- data/config/currency_iso.json +12 -0
- data/config/locales/en.yml +13 -0
- data/config/locales/ru.yml +13 -0
- data/config/routes.rb +5 -0
- data/doc/CHANGELOG +123 -2
- data/lib/redmine_crm/acts_as_draftable/draft.rb +40 -0
- data/lib/redmine_crm/acts_as_draftable/rcrm_acts_as_draftable.rb +172 -0
- data/lib/redmine_crm/acts_as_list/list.rb +282 -0
- data/lib/redmine_crm/{rcrm_acts_as_taggable.rb → acts_as_taggable/rcrm_acts_as_taggable.rb} +112 -93
- data/lib/redmine_crm/acts_as_taggable/tag.rb +81 -0
- data/lib/redmine_crm/acts_as_taggable/tag_list.rb +111 -0
- data/lib/redmine_crm/acts_as_taggable/tagging.rb +16 -0
- data/lib/redmine_crm/acts_as_viewed/rcrm_acts_as_viewed.rb +274 -0
- data/lib/redmine_crm/{rcrm_acts_as_votable.rb → acts_as_votable/rcrm_acts_as_votable.rb} +15 -14
- data/lib/redmine_crm/acts_as_votable/rcrm_acts_as_voter.rb +20 -0
- data/lib/redmine_crm/{votable.rb → acts_as_votable/votable.rb} +54 -65
- data/lib/redmine_crm/{vote.rb → acts_as_votable/vote.rb} +6 -8
- data/lib/redmine_crm/{voter.rb → acts_as_votable/voter.rb} +29 -34
- data/lib/redmine_crm/assets_manager.rb +43 -0
- data/lib/redmine_crm/colors_helper.rb +192 -0
- data/lib/redmine_crm/compatibility/application_controller_patch.rb +33 -0
- data/lib/redmine_crm/currency/formatting.rb +5 -8
- data/lib/redmine_crm/currency/heuristics.rb +1 -1
- data/lib/redmine_crm/currency/loader.rb +5 -6
- data/lib/redmine_crm/currency.rb +28 -17
- data/lib/redmine_crm/engine.rb +4 -0
- data/lib/redmine_crm/helpers/external_assets_helper.rb +19 -0
- data/lib/redmine_crm/helpers/form_tag_helper.rb +76 -0
- data/lib/redmine_crm/helpers/tags_helper.rb +1 -3
- data/lib/redmine_crm/helpers/vote_helper.rb +29 -32
- data/lib/redmine_crm/hooks/views_layouts_hook.rb +11 -0
- data/lib/redmine_crm/liquid/drops/issues_drop.rb +191 -0
- data/lib/redmine_crm/liquid/drops/news_drop.rb +54 -0
- data/lib/redmine_crm/liquid/drops/projects_drop.rb +86 -0
- data/lib/redmine_crm/liquid/drops/time_entries_drop.rb +65 -0
- data/lib/redmine_crm/liquid/drops/users_drop.rb +68 -0
- data/lib/redmine_crm/liquid/filters/arrays.rb +187 -0
- data/lib/redmine_crm/liquid/filters/base.rb +217 -0
- data/lib/redmine_crm/liquid/filters/colors.rb +31 -0
- data/lib/redmine_crm/money_helper.rb +17 -18
- data/lib/redmine_crm/settings/money.rb +46 -0
- data/lib/redmine_crm/settings.rb +53 -0
- data/lib/redmine_crm/version.rb +1 -1
- data/lib/redmine_crm.rb +60 -21
- data/redmine_crm.gemspec +12 -6
- data/test/acts_as_draftable/draft_test.rb +29 -0
- data/test/acts_as_draftable/rcrm_acts_as_draftable_test.rb +178 -0
- data/test/{acts_as_taggable_test.rb → acts_as_taggable/rcrm_acts_as_taggable_test.rb} +117 -156
- data/test/acts_as_taggable/tag_list_test.rb +34 -0
- data/test/acts_as_taggable/tag_test.rb +72 -0
- data/test/acts_as_taggable/tagging_test.rb +15 -0
- data/test/{viewed_test.rb → acts_as_viewed/rcrm_acts_as_viewed_test.rb} +17 -15
- data/test/acts_as_votable/rcrm_acts_as_votable_test.rb +19 -0
- data/test/acts_as_votable/rcrm_acts_as_voter_test.rb +14 -0
- data/test/{votable_model_test.rb → acts_as_votable/votable_test.rb} +34 -5
- data/test/{voter_model_test.rb → acts_as_votable/voter_test.rb} +8 -8
- data/test/currency_test.rb +10 -10
- data/test/database.yml +14 -14
- data/test/fixtures/issues.yml +13 -1
- data/test/fixtures/news.yml +8 -0
- data/test/fixtures/projects.yml +10 -0
- data/test/fixtures/users.yml +6 -2
- data/test/liquid/drops/issues_drop_test.rb +34 -0
- data/test/liquid/drops/news_drop_test.rb +38 -0
- data/test/liquid/drops/projects_drop_test.rb +44 -0
- data/test/liquid/drops/uses_drop_test.rb +36 -0
- data/test/liquid/filters/arrays_filter_test.rb +31 -0
- data/test/liquid/filters/base_filter_test.rb +63 -0
- data/test/liquid/filters/colors_filter_test.rb +33 -0
- data/test/liquid/liquid_helper.rb +34 -0
- data/test/models/issue.rb +14 -0
- data/test/models/news.rb +3 -0
- data/test/models/project.rb +8 -0
- data/test/{fixtures → models}/user.rb +5 -1
- data/test/{fixtures → models}/vote_classes.rb +0 -21
- data/test/money_helper_test.rb +5 -5
- data/test/schema.rb +33 -10
- data/test/test_helper.rb +20 -72
- data/vendor/assets/images/money.png +0 -0
- data/vendor/assets/images/vcard.png +0 -0
- data/vendor/assets/javascripts/Chart.bundle.min.js +16 -0
- data/vendor/assets/javascripts/select2.js +2 -0
- data/vendor/assets/javascripts/select2_helpers.js +192 -0
- data/vendor/assets/stylesheets/money.css +3 -0
- data/vendor/assets/stylesheets/select2.css +424 -0
- metadata +190 -40
- data/lib/redmine_crm/rcrm_acts_as_viewed.rb +0 -287
- data/lib/redmine_crm/rcrm_acts_as_voter.rb +0 -27
- data/lib/redmine_crm/tag.rb +0 -81
- data/lib/redmine_crm/tag_list.rb +0 -112
- data/lib/redmine_crm/tagging.rb +0 -20
- data/test/fixtures/issue.rb +0 -14
- data/test/tag_test.rb +0 -64
- data/test/tagging_test.rb +0 -14
- 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
|
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
|
71
|
-
votes_name_table
|
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
|
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
|
-
|
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
|
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
|
-
|
81
|
-
|
82
|
-
|
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
|
78
|
+
if votes_for.count == 0 || options[:duplicate]
|
87
79
|
# this voter has never voted
|
88
|
-
|
89
|
-
|
90
|
-
|
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 =
|
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.
|
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
|
117
|
-
return false if args[:voter].nil?
|
118
|
-
|
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
|
121
|
-
|
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
|
-
|
111
|
+
true
|
125
112
|
end
|
126
113
|
|
127
|
-
def vote_up
|
128
|
-
self.vote_by :voter => voter, :vote => true,
|
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
|
132
|
-
self.vote_by :voter => voter, :vote => false,
|
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
|
136
|
-
|
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
|
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
|
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.
|
236
|
+
self.update_attributes(updates, :without_protection => true) if !updates.empty?
|
245
237
|
else
|
246
|
-
self.update_attributes(updates) if updates.
|
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
|
243
|
+
def find_votes_for(extra_conditions = {})
|
254
244
|
votes_for.where(extra_conditions)
|
255
245
|
end
|
256
246
|
|
257
|
-
def get_up_votes
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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?
|
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
|