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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/README.md +166 -33
  4. data/Rakefile +3 -12
  5. data/app/controllers/redmine_crm_controller.rb +26 -0
  6. data/app/views/redmine_crm/_money.html.erb +44 -0
  7. data/app/views/redmine_crm/settings.html.erb +10 -0
  8. data/bitbucket-pipelines.yml +54 -0
  9. data/config/currency_iso.json +12 -0
  10. data/config/locales/en.yml +13 -0
  11. data/config/locales/ru.yml +13 -0
  12. data/config/routes.rb +5 -0
  13. data/doc/CHANGELOG +123 -2
  14. data/lib/redmine_crm/acts_as_draftable/draft.rb +40 -0
  15. data/lib/redmine_crm/acts_as_draftable/rcrm_acts_as_draftable.rb +172 -0
  16. data/lib/redmine_crm/acts_as_list/list.rb +282 -0
  17. data/lib/redmine_crm/{rcrm_acts_as_taggable.rb → acts_as_taggable/rcrm_acts_as_taggable.rb} +112 -93
  18. data/lib/redmine_crm/acts_as_taggable/tag.rb +81 -0
  19. data/lib/redmine_crm/acts_as_taggable/tag_list.rb +111 -0
  20. data/lib/redmine_crm/acts_as_taggable/tagging.rb +16 -0
  21. data/lib/redmine_crm/acts_as_viewed/rcrm_acts_as_viewed.rb +274 -0
  22. data/lib/redmine_crm/{rcrm_acts_as_votable.rb → acts_as_votable/rcrm_acts_as_votable.rb} +15 -14
  23. data/lib/redmine_crm/acts_as_votable/rcrm_acts_as_voter.rb +20 -0
  24. data/lib/redmine_crm/{votable.rb → acts_as_votable/votable.rb} +54 -65
  25. data/lib/redmine_crm/{vote.rb → acts_as_votable/vote.rb} +6 -8
  26. data/lib/redmine_crm/{voter.rb → acts_as_votable/voter.rb} +29 -34
  27. data/lib/redmine_crm/assets_manager.rb +43 -0
  28. data/lib/redmine_crm/colors_helper.rb +192 -0
  29. data/lib/redmine_crm/compatibility/application_controller_patch.rb +33 -0
  30. data/lib/redmine_crm/currency/formatting.rb +5 -8
  31. data/lib/redmine_crm/currency/heuristics.rb +1 -1
  32. data/lib/redmine_crm/currency/loader.rb +5 -6
  33. data/lib/redmine_crm/currency.rb +28 -17
  34. data/lib/redmine_crm/engine.rb +4 -0
  35. data/lib/redmine_crm/helpers/external_assets_helper.rb +19 -0
  36. data/lib/redmine_crm/helpers/form_tag_helper.rb +76 -0
  37. data/lib/redmine_crm/helpers/tags_helper.rb +1 -3
  38. data/lib/redmine_crm/helpers/vote_helper.rb +29 -32
  39. data/lib/redmine_crm/hooks/views_layouts_hook.rb +11 -0
  40. data/lib/redmine_crm/liquid/drops/issues_drop.rb +191 -0
  41. data/lib/redmine_crm/liquid/drops/news_drop.rb +54 -0
  42. data/lib/redmine_crm/liquid/drops/projects_drop.rb +86 -0
  43. data/lib/redmine_crm/liquid/drops/time_entries_drop.rb +65 -0
  44. data/lib/redmine_crm/liquid/drops/users_drop.rb +68 -0
  45. data/lib/redmine_crm/liquid/filters/arrays.rb +187 -0
  46. data/lib/redmine_crm/liquid/filters/base.rb +217 -0
  47. data/lib/redmine_crm/liquid/filters/colors.rb +31 -0
  48. data/lib/redmine_crm/money_helper.rb +17 -18
  49. data/lib/redmine_crm/settings/money.rb +46 -0
  50. data/lib/redmine_crm/settings.rb +53 -0
  51. data/lib/redmine_crm/version.rb +1 -1
  52. data/lib/redmine_crm.rb +60 -21
  53. data/redmine_crm.gemspec +12 -6
  54. data/test/acts_as_draftable/draft_test.rb +29 -0
  55. data/test/acts_as_draftable/rcrm_acts_as_draftable_test.rb +178 -0
  56. data/test/{acts_as_taggable_test.rb → acts_as_taggable/rcrm_acts_as_taggable_test.rb} +117 -156
  57. data/test/acts_as_taggable/tag_list_test.rb +34 -0
  58. data/test/acts_as_taggable/tag_test.rb +72 -0
  59. data/test/acts_as_taggable/tagging_test.rb +15 -0
  60. data/test/{viewed_test.rb → acts_as_viewed/rcrm_acts_as_viewed_test.rb} +17 -15
  61. data/test/acts_as_votable/rcrm_acts_as_votable_test.rb +19 -0
  62. data/test/acts_as_votable/rcrm_acts_as_voter_test.rb +14 -0
  63. data/test/{votable_model_test.rb → acts_as_votable/votable_test.rb} +34 -5
  64. data/test/{voter_model_test.rb → acts_as_votable/voter_test.rb} +8 -8
  65. data/test/currency_test.rb +10 -10
  66. data/test/database.yml +14 -14
  67. data/test/fixtures/issues.yml +13 -1
  68. data/test/fixtures/news.yml +8 -0
  69. data/test/fixtures/projects.yml +10 -0
  70. data/test/fixtures/users.yml +6 -2
  71. data/test/liquid/drops/issues_drop_test.rb +34 -0
  72. data/test/liquid/drops/news_drop_test.rb +38 -0
  73. data/test/liquid/drops/projects_drop_test.rb +44 -0
  74. data/test/liquid/drops/uses_drop_test.rb +36 -0
  75. data/test/liquid/filters/arrays_filter_test.rb +31 -0
  76. data/test/liquid/filters/base_filter_test.rb +63 -0
  77. data/test/liquid/filters/colors_filter_test.rb +33 -0
  78. data/test/liquid/liquid_helper.rb +34 -0
  79. data/test/models/issue.rb +14 -0
  80. data/test/models/news.rb +3 -0
  81. data/test/models/project.rb +8 -0
  82. data/test/{fixtures → models}/user.rb +5 -1
  83. data/test/{fixtures → models}/vote_classes.rb +0 -21
  84. data/test/money_helper_test.rb +5 -5
  85. data/test/schema.rb +33 -10
  86. data/test/test_helper.rb +20 -72
  87. data/vendor/assets/images/money.png +0 -0
  88. data/vendor/assets/images/vcard.png +0 -0
  89. data/vendor/assets/javascripts/Chart.bundle.min.js +16 -0
  90. data/vendor/assets/javascripts/select2.js +2 -0
  91. data/vendor/assets/javascripts/select2_helpers.js +192 -0
  92. data/vendor/assets/stylesheets/money.css +3 -0
  93. data/vendor/assets/stylesheets/select2.css +424 -0
  94. metadata +190 -40
  95. data/lib/redmine_crm/rcrm_acts_as_viewed.rb +0 -287
  96. data/lib/redmine_crm/rcrm_acts_as_voter.rb +0 -27
  97. data/lib/redmine_crm/tag.rb +0 -81
  98. data/lib/redmine_crm/tag_list.rb +0 -112
  99. data/lib/redmine_crm/tagging.rb +0 -20
  100. data/test/fixtures/issue.rb +0 -14
  101. data/test/tag_test.rb +0 -64
  102. data/test/tagging_test.rb +0 -14
  103. data/test/votable_test.rb +0 -17
@@ -0,0 +1,172 @@
1
+ # The MIT License (MIT)
2
+
3
+ # Copyright (c) 2016-2018 Georg Ledermann
4
+
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ module RedmineCrm
24
+ module ActsAsDraftable #:nodoc: all
25
+ module Base
26
+ ALLOWED_DRAFT_OPTIONS = [:parent]
27
+
28
+ def draftable?
29
+ false
30
+ end
31
+
32
+ def rcrm_acts_as_draftable(options = {})
33
+ raise ArgumentError unless options.is_a?(Hash)
34
+ raise ArgumentError unless options.keys.all? { |k| ALLOWED_DRAFT_OPTIONS.include?(k) }
35
+
36
+ class_attribute :draft_parent
37
+
38
+ if options[:parent]
39
+ parent_class = self.reflect_on_all_associations(:belongs_to).find { |a| a.name == options[:parent] }.try(:klass)
40
+ raise ArgumentError unless parent_class
41
+
42
+ unless parent_class.method_defined?(:drafts)
43
+ parent_class.class_eval do
44
+ def drafts(user)
45
+ Draft.where(user: user, parent: self)
46
+ end
47
+
48
+ def self.child_drafts(user)
49
+ Draft.where(user: user, parent_type: self.base_class.name)
50
+ end
51
+ end
52
+ end
53
+
54
+ self.draft_parent = options[:parent]
55
+ end
56
+
57
+ attr_accessor :draft_id
58
+ before_save :clear_draft
59
+
60
+ extend RedmineCrm::ActsAsDraftable::ClassMethods
61
+ include RedmineCrm::ActsAsDraftable::InstanceMethods
62
+ end
63
+ end # Base
64
+
65
+ module ClassMethods
66
+ def draftable?
67
+ true
68
+ end
69
+
70
+ def from_draft(draft_or_id)
71
+ draft = draft_or_id.is_a?(Draft) ? draft_or_id : Draft.find(draft_or_id)
72
+ raise ArgumentError unless draft.target_type == name
73
+
74
+ target = draft.target_type.constantize.new
75
+ target.load_from_draft(draft.data)
76
+
77
+ target.draft_id = draft.id
78
+ target
79
+ end
80
+
81
+ def drafts(user = nil)
82
+ drafts = Draft.where(target_type: name)
83
+ drafts = drafts.where(user_id: user.id) if user
84
+ drafts
85
+ end
86
+ end # ClassMethods
87
+
88
+ module InstanceMethods
89
+ def save_draft
90
+ return false unless new_record? || changed?
91
+
92
+ draft = self.draft || Draft.new
93
+ draft.data = dump_to_draft
94
+ draft.user_id = User.current.try(:id)
95
+ draft.parent = parent_for_draft
96
+ draft.target_type = self.class.name
97
+
98
+ result = draft.save
99
+ self.draft_id = draft.id if result
100
+ result
101
+ end
102
+
103
+ def update_draft(attributes)
104
+ with_transaction_returning_status do
105
+ assign_attributes(attributes)
106
+ save_draft
107
+ end
108
+ end
109
+
110
+ def draft
111
+ Draft.find_by_id(draft_id)
112
+ end
113
+
114
+ def parent_for_draft
115
+ return self if id.present?
116
+
117
+ self.class.draft_parent.present? ? send(self.class.draft_parent) : self
118
+ end
119
+
120
+ def last_draft
121
+ @last_draft ||= RedmineCrm::ActsAsDraftable::Draft.where(
122
+ target_type: self.class.name,
123
+ parent_type: parent_for_draft.try(:class).try(:to_s),
124
+ parent_id: parent_for_draft.try(:id),
125
+ user_id: User.current.try(:id)
126
+ ).last
127
+ end
128
+
129
+ def dump_to_draft
130
+ instance_values.default = nil
131
+ Marshal.dump(instance_values)
132
+ end
133
+
134
+ def load_from_draft(string)
135
+ values = Marshal.load(string)
136
+
137
+ values.each do |name, value|
138
+ instance_variable_set("@#{name}", value)
139
+ end
140
+ end
141
+
142
+ def clear_draft
143
+ if last_draft && last_draft.destroy
144
+ self.draft_id = nil
145
+ end
146
+ end
147
+ end # InstanceMethods
148
+
149
+ module Migration
150
+ def create_drafts_table
151
+ return if connection.table_exists?(:drafts)
152
+
153
+ connection.create_table :drafts do |t|
154
+ t.string :target_type, limit: 150, null: false
155
+ t.references :user
156
+ t.references :parent, polymorphic: true
157
+ t.binary :data, limit: 16777215, null: false
158
+ t.datetime :updated_at, null: false
159
+ end
160
+
161
+ connection.add_index :drafts, [:user_id, :target_type]
162
+ end
163
+
164
+ def drop_drafts_table
165
+ connection.drop_table :drafts if connection.table_exists?(:drafts)
166
+ end
167
+ end # Migration
168
+ end
169
+ end
170
+
171
+ ActiveRecord::Base.extend RedmineCrm::ActsAsDraftable::Base
172
+ ActiveRecord::Base.extend RedmineCrm::ActsAsDraftable::Migration
@@ -0,0 +1,282 @@
1
+ module RedmineCrm
2
+ module ActsAsList
3
+ module List
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
9
+ # The class that has this specified needs to have a +position+ column defined as an integer on
10
+ # the mapped database table.
11
+ #
12
+ # Todo list example:
13
+ #
14
+ # class TodoList < ActiveRecord::Base
15
+ # has_many :todo_items, :order => "position"
16
+ # end
17
+ #
18
+ # class TodoItem < ActiveRecord::Base
19
+ # belongs_to :todo_list
20
+ # acts_as_list :scope => :todo_list
21
+ # end
22
+ #
23
+ # todo_list.first.move_to_bottom
24
+ # todo_list.last.move_higher
25
+ module ClassMethods
26
+ # Configuration options are:
27
+ #
28
+ # * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
29
+ # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
30
+ # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
31
+ # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
32
+ # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
33
+ def rcrm_acts_as_list(options = {})
34
+
35
+ configuration = { :column => "position", :scope => "1 = 1" }
36
+ configuration.update(options) if options.is_a?(Hash)
37
+
38
+ configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
39
+
40
+ if configuration[:scope].is_a?(Symbol)
41
+ scope_condition_method = %(
42
+ def scope_condition
43
+ if #{configuration[:scope].to_s}.nil?
44
+ "#{configuration[:scope].to_s} IS NULL"
45
+ else
46
+ "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
47
+ end
48
+ end
49
+ )
50
+ else
51
+ scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
52
+ end
53
+
54
+ class_eval <<-EOV
55
+ include RedmineCrm::ActsAsList::List::InstanceMethods
56
+
57
+ def rcrm_acts_as_list_class
58
+ ::#{self.name}
59
+ end
60
+
61
+ def position_column
62
+ '#{configuration[:column]}'
63
+ end
64
+
65
+ #{scope_condition_method}
66
+
67
+ before_destroy :remove_from_list
68
+ before_create :add_to_list_bottom
69
+ EOV
70
+ end
71
+ end
72
+
73
+ # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
74
+ # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
75
+ # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
76
+ # the first in the list of all chapters.
77
+ module InstanceMethods
78
+ # Insert the item at the given position (defaults to the top position of 1).
79
+ def insert_at(position = 1)
80
+ insert_at_position(position)
81
+ end
82
+
83
+ # Swap positions with the next lower item, if one exists.
84
+ def move_lower
85
+ return unless lower_item
86
+
87
+ rcrm_acts_as_list_class.transaction do
88
+ lower_item.decrement_position
89
+ increment_position
90
+ end
91
+ end
92
+
93
+ # Swap positions with the next higher item, if one exists.
94
+ def move_higher
95
+ return unless higher_item
96
+
97
+ rcrm_acts_as_list_class.transaction do
98
+ higher_item.increment_position
99
+ decrement_position
100
+ end
101
+ end
102
+
103
+ # Move to the bottom of the list. If the item is already in the list, the items below it have their
104
+ # position adjusted accordingly.
105
+ def move_to_bottom
106
+ return unless in_list?
107
+ rcrm_acts_as_list_class.transaction do
108
+ decrement_positions_on_lower_items
109
+ assume_bottom_position
110
+ end
111
+ end
112
+
113
+ # Move to the top of the list. If the item is already in the list, the items above it have their
114
+ # position adjusted accordingly.
115
+ def move_to_top
116
+ return unless in_list?
117
+ rcrm_acts_as_list_class.transaction do
118
+ increment_positions_on_higher_items
119
+ assume_top_position
120
+ end
121
+ end
122
+
123
+ # Move to the given position
124
+ def move_to=(pos)
125
+ case pos.to_s
126
+ when 'highest'
127
+ move_to_top
128
+ when 'higher'
129
+ move_higher
130
+ when 'lower'
131
+ move_lower
132
+ when 'lowest'
133
+ move_to_bottom
134
+ end
135
+ reset_positions_in_list
136
+ end
137
+
138
+ def reset_positions_in_list
139
+ rcrm_acts_as_list_class.where(scope_condition).reorder("#{position_column} ASC, id ASC").each_with_index do |item, i|
140
+ unless item.send(position_column) == (i + 1)
141
+ rcrm_acts_as_list_class.where({:id => item.id}).
142
+ update_all({position_column => (i + 1)})
143
+ end
144
+ end
145
+ end
146
+
147
+ # Removes the item from the list.
148
+ def remove_from_list
149
+ if in_list?
150
+ decrement_positions_on_lower_items
151
+ update_attribute position_column, nil
152
+ end
153
+ end
154
+
155
+ # Increase the position of this item without adjusting the rest of the list.
156
+ def increment_position
157
+ return unless in_list?
158
+ update_attribute position_column, self.send(position_column).to_i + 1
159
+ end
160
+
161
+ # Decrease the position of this item without adjusting the rest of the list.
162
+ def decrement_position
163
+ return unless in_list?
164
+ update_attribute position_column, self.send(position_column).to_i - 1
165
+ end
166
+
167
+ # Return +true+ if this object is the first in the list.
168
+ def first?
169
+ return false unless in_list?
170
+ self.send(position_column) == 1
171
+ end
172
+
173
+ # Return +true+ if this object is the last in the list.
174
+ def last?
175
+ return false unless in_list?
176
+ self.send(position_column) == bottom_position_in_list
177
+ end
178
+
179
+ # Return the next higher item in the list.
180
+ def higher_item
181
+ return nil unless in_list?
182
+ rcrm_acts_as_list_class.where(
183
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
184
+ ).first
185
+ end
186
+
187
+ # Return the next lower item in the list.
188
+ def lower_item
189
+ return nil unless in_list?
190
+ rcrm_acts_as_list_class.where(
191
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
192
+ ).first
193
+ end
194
+
195
+ # Test if this record is in a list
196
+ def in_list?
197
+ !send(position_column).nil?
198
+ end
199
+
200
+ private
201
+
202
+ def add_to_list_top
203
+ increment_positions_on_all_items
204
+ end
205
+
206
+ def add_to_list_bottom
207
+ self[position_column] = bottom_position_in_list.to_i + 1
208
+ end
209
+
210
+ # Overwrite this method to define the scope of the list changes
211
+ def scope_condition() "1" end
212
+
213
+ # Returns the bottom position number in the list.
214
+ # bottom_position_in_list # => 2
215
+ def bottom_position_in_list(except = nil)
216
+ item = bottom_item(except)
217
+ item ? item.send(position_column) : 0
218
+ end
219
+
220
+ # Returns the bottom item
221
+ def bottom_item(except = nil)
222
+ conditions = scope_condition
223
+ conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
224
+ rcrm_acts_as_list_class.where(conditions).reorder("#{position_column} DESC").first
225
+ end
226
+
227
+ # Forces item to assume the bottom position in the list.
228
+ def assume_bottom_position
229
+ update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
230
+ end
231
+
232
+ # Forces item to assume the top position in the list.
233
+ def assume_top_position
234
+ update_attribute(position_column, 1)
235
+ end
236
+
237
+ # This has the effect of moving all the higher items up one.
238
+ def decrement_positions_on_higher_items(position)
239
+ rcrm_acts_as_list_class.
240
+ where("#{scope_condition} AND #{position_column} <= #{position}").
241
+ update_all("#{position_column} = (#{position_column} - 1)")
242
+ end
243
+
244
+ # This has the effect of moving all the lower items up one.
245
+ def decrement_positions_on_lower_items
246
+ return unless in_list?
247
+ rcrm_acts_as_list_class.
248
+ where("#{scope_condition} AND #{position_column} > #{send(position_column).to_i}").
249
+ update_all("#{position_column} = (#{position_column} - 1)")
250
+ end
251
+
252
+ # This has the effect of moving all the higher items down one.
253
+ def increment_positions_on_higher_items
254
+ return unless in_list?
255
+ rcrm_acts_as_list_class.
256
+ where("#{scope_condition} AND #{position_column} < #{send(position_column).to_i}").
257
+ update_all("#{position_column} = (#{position_column} + 1)")
258
+ end
259
+
260
+ # This has the effect of moving all the lower items down one.
261
+ def increment_positions_on_lower_items(position)
262
+ rcrm_acts_as_list_class.
263
+ where("#{scope_condition} AND #{position_column} >= #{position}").
264
+ update_all("#{position_column} = (#{position_column} + 1)")
265
+ end
266
+
267
+ # Increments position (<tt>position_column</tt>) of all items in the list.
268
+ def increment_positions_on_all_items
269
+ rcrm_acts_as_list_class.
270
+ where("#{scope_condition}").
271
+ update_all("#{position_column} = (#{position_column} + 1)")
272
+ end
273
+
274
+ def insert_at_position(position)
275
+ remove_from_list
276
+ increment_positions_on_lower_items(position)
277
+ self.update_attribute(position_column, position)
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end