rexport 0.4.0

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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +17 -0
  4. data/Rakefile +38 -0
  5. data/app/helpers/exports_helper.rb +25 -0
  6. data/app/views/export_filters/_export_filter.html.erb +7 -0
  7. data/app/views/export_items/_export_item.html.erb +8 -0
  8. data/app/views/export_items/edit.html.erb +15 -0
  9. data/app/views/exports/_edit.html.erb +7 -0
  10. data/app/views/exports/_filters.html.erb +9 -0
  11. data/app/views/exports/_form.html.erb +25 -0
  12. data/app/views/exports/_local_form.html.erb +1 -0
  13. data/app/views/exports/_rexport_model.html.erb +17 -0
  14. data/app/views/exports/_show.html.erb +53 -0
  15. data/app/views/exports/edit.html.erb +1 -0
  16. data/app/views/exports/index.html.erb +33 -0
  17. data/app/views/exports/new.html.erb +7 -0
  18. data/app/views/exports/show.html.erb +1 -0
  19. data/config/routes.rb +11 -0
  20. data/db/migrate/20091105182959_create_export_tables.rb +34 -0
  21. data/lib/rexport.rb +17 -0
  22. data/lib/rexport/data_fields.rb +152 -0
  23. data/lib/rexport/export_filter_methods.rb +34 -0
  24. data/lib/rexport/export_filters_controller_methods.rb +13 -0
  25. data/lib/rexport/export_item_methods.rb +45 -0
  26. data/lib/rexport/export_item_sortings_controller_methods.rb +13 -0
  27. data/lib/rexport/export_items_controller_methods.rb +32 -0
  28. data/lib/rexport/export_methods.rb +272 -0
  29. data/lib/rexport/exports_controller_methods.rb +86 -0
  30. data/lib/rexport/tree_node.rb +45 -0
  31. data/lib/rexport/version.rb +3 -0
  32. data/test/factories.rb +86 -0
  33. data/test/jenkins.bash +3 -0
  34. data/test/log/test.log +61761 -0
  35. data/test/test_helper.rb +168 -0
  36. data/test/unit/data_field_test.rb +25 -0
  37. data/test/unit/data_fields_test.rb +117 -0
  38. data/test/unit/export_methods_test.rb +97 -0
  39. data/test/unit/rexport_model_test.rb +35 -0
  40. data/test/unit/tree_node_test.rb +43 -0
  41. metadata +165 -0
@@ -0,0 +1,34 @@
1
+ module Rexport #:nodoc:
2
+ module ExportFilterMethods
3
+ def self.included(base)
4
+ base.class_eval do
5
+ include InstanceMethods
6
+
7
+ belongs_to :export
8
+ validates_presence_of :filter_field
9
+ end
10
+ end
11
+
12
+ module InstanceMethods
13
+ def display_value
14
+ return value unless filter_field[/_id$/]
15
+ path = filter_field.split('.')
16
+ foreign_key = path.pop
17
+ association = export.get_klass_from_path(path).reflect_on_all_associations(:belongs_to).detect do |association|
18
+ association.foreign_key == foreign_key
19
+ end
20
+ return 'UNDEFINED ASSOCIATION' unless association
21
+ begin
22
+ object = association.klass.find(value)
23
+ return object.respond_to?(:name) ? object.name : object.to_s
24
+ rescue ActiveRecord::RecordNotFound
25
+ return 'ASSOCIATED OBJECT NOT FOUND'
26
+ end
27
+ end
28
+
29
+ def attributes_for_copy
30
+ attributes.slice('filter_field', 'value')
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ module Rexport
2
+ module ExportFiltersControllerMethods
3
+ # DELETE /export_filters/1
4
+ def destroy
5
+ @export_filter = ExportFilter.find(params[:id])
6
+ @export_filter.destroy
7
+
8
+ respond_to do |format|
9
+ format.html { redirect_to(@export_filter.export) }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,45 @@
1
+ module Rexport #:nodoc:
2
+ module ExportItemMethods
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ base.class_eval do
6
+ include InstanceMethods
7
+
8
+ acts_as_list :scope => :export
9
+ belongs_to :export
10
+ before_validation :replace_blank_name_with_rexport_field
11
+ validates_presence_of :name, :rexport_field
12
+ scope :ordered, -> { order :position }
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def resort(export_item_ids)
18
+ export_item_ids.each_index do |index|
19
+ position = index + 1
20
+ export_item = find(export_item_ids[index].gsub(/[^0-9]/,''))
21
+ export_item.update_attribute(:position, position) if export_item.position != position
22
+ end
23
+ end
24
+ end
25
+
26
+ module InstanceMethods
27
+ def attributes_for_copy
28
+ attributes.slice('position', 'name', 'rexport_field')
29
+ end
30
+
31
+ #######
32
+ private
33
+ #######
34
+
35
+ def replace_blank_name_with_rexport_field
36
+ return unless name.blank?
37
+ self.name = if rexport_field.include?('.')
38
+ rexport_field.split('.').values_at(-2..-1).map {|v| v.titleize}.join(' - ')
39
+ else
40
+ rexport_field.titleize
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,13 @@
1
+ module Rexport
2
+ module ExportItemSortingsControllerMethods
3
+ def update
4
+ ExportItem.resort(params[:sorted_items])
5
+
6
+ respond_to do |format|
7
+ format.js do
8
+ render :nothing => true
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ module Rexport
2
+ module ExportItemsControllerMethods
3
+ # GET /export_item/1/edit
4
+ def edit
5
+ @export_item = ExportItem.find(params[:id])
6
+ end
7
+
8
+ # PUT /export_item/1
9
+ def update
10
+ @export_item = ExportItem.find(params[:id])
11
+
12
+ respond_to do |format|
13
+ if @export_item.update_attributes(params[:export_item])
14
+ flash[:notice] = 'ExportItem was successfully updated.'
15
+ format.html { redirect_to(export_path(@export_item.export)) }
16
+ else
17
+ format.html { render :action => "edit" }
18
+ end
19
+ end
20
+ end
21
+
22
+ # DELETE /export_item/1
23
+ def destroy
24
+ @export_item = ExportItem.find(params[:id])
25
+ @export_item.destroy
26
+
27
+ respond_to do |format|
28
+ format.html { redirect_to(export_path(@export_item.export)) }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,272 @@
1
+ require 'csv'
2
+
3
+ module Rexport #:nodoc:
4
+
5
+ SAMPLE_SIZE = 5
6
+
7
+ class RexportModel
8
+ attr_accessor :klass, :path
9
+
10
+ def initialize(klass, path = nil)
11
+ self.klass = klass
12
+ self.path = path.to_s unless path.blank?
13
+ end
14
+
15
+ def rexport_fields_array
16
+ klass.rexport_fields_array
17
+ end
18
+
19
+ def field_path(field_name)
20
+ [path, field_name].compact * '.'
21
+ end
22
+
23
+ def collection_from_association(association)
24
+ return klass.send("find_#{association}_for_rexport") if klass.respond_to?("find_#{association}_for_rexport")
25
+ klass.reflect_on_association(association.to_sym).klass.all
26
+ end
27
+
28
+ def filter_column(field)
29
+ return field.method unless field.method.include?('.')
30
+ association = field.method.split('.').first
31
+ klass.reflect_on_association(association.to_sym).foreign_key
32
+ end
33
+
34
+ def name
35
+ klass.name
36
+ end
37
+ end
38
+
39
+ module ExportMethods
40
+ def self.included(base)
41
+ base.extend ClassMethods
42
+ base.class_eval do
43
+ include InstanceMethods
44
+
45
+ has_many :export_items, :dependent => :destroy
46
+ has_many :export_filters, :dependent => :destroy
47
+ validates_presence_of :name, :model_name
48
+ after_save :save_export_items
49
+ scope :alphabetical, -> { order :name }
50
+ scope :categorical, -> { order :model_name }
51
+ scope :by_model, ->(model_name) { where(model_name: model_name) }
52
+ end
53
+ end
54
+
55
+ module ClassMethods
56
+ def models
57
+ %w(override_this_method)
58
+ end
59
+
60
+ def enabled
61
+ find(:all).select {|export| export.enabled? }
62
+ end
63
+ end
64
+
65
+ module InstanceMethods
66
+ def full_name
67
+ "#{model_name.pluralize} - #{name}"
68
+ end
69
+
70
+ # Returns a string with the export data
71
+ def to_s
72
+ String.new.tap do |result|
73
+ result << header * '|' << "\n"
74
+ records.each do |record|
75
+ result << record * '|' << "\n"
76
+ end
77
+ end
78
+ end
79
+
80
+ # Returns a csv string with the export data
81
+ def to_csv(objects = nil)
82
+ seed_records(objects) unless objects.nil?
83
+ CSV.generate do |csv|
84
+ csv << header
85
+ records.each do |record|
86
+ csv << record
87
+ end
88
+ end
89
+ end
90
+
91
+ # Returns an array with the header names from the associated export_items
92
+ def header
93
+ export_items.ordered.map {|i| i.name}
94
+ end
95
+
96
+ # Returns the export model class
97
+ def export_model
98
+ model_name.constantize
99
+ end
100
+
101
+ # Returns an array of RexportModels including export_model and associated rexport capable models
102
+ def rexport_models
103
+ @rexport_models ||= get_rexport_models(export_model)
104
+ end
105
+
106
+ # Returns the records for the export
107
+ def records
108
+ @records ||= get_records
109
+ end
110
+
111
+ # Returns a limited number of records for the export
112
+ def sample_records
113
+ get_records(Rexport::SAMPLE_SIZE)
114
+ end
115
+
116
+ # Returns a class based on a path array
117
+ def get_klass_from_path(path, klass = export_model)
118
+ return klass unless (association_name = path.shift)
119
+ get_klass_from_path(path, klass.reflect_on_association(association_name.to_sym).klass)
120
+ end
121
+
122
+ def has_rexport_field?(rexport_field)
123
+ rexport_fields.include?(rexport_field)
124
+ end
125
+
126
+ def rexport_fields=(rexport_fields)
127
+ @rexport_fields = if rexport_fields.respond_to?(:keys)
128
+ @set_position = false
129
+ rexport_fields.keys
130
+ else
131
+ @set_position = true
132
+ rexport_fields
133
+ end
134
+ end
135
+
136
+ def export_filter_attributes=(attributes)
137
+ attributes.each do |field, value|
138
+ if value.blank?
139
+ filter = export_filters.find_by_filter_field(field)
140
+ filter.destroy if filter
141
+ elsif new_record?
142
+ export_filters.build(:filter_field => field, :value => value)
143
+ else
144
+ filter = export_filters.find_or_create_by_filter_field(field)
145
+ filter.update_attribute(:value, value)
146
+ end
147
+ end
148
+ end
149
+
150
+ def filter_value(filter_field)
151
+ filter = export_filters.detect {|f| f.filter_field == filter_field}
152
+ filter ? filter.value : nil
153
+ end
154
+
155
+ def copy
156
+ self.class.create(attributes_for_copy) do |new_export|
157
+ export_items.ordered.each { |item| new_export.export_items.build(item.attributes_for_copy) }
158
+ export_filters.each { |filter| new_export.export_filters.build(filter.attributes_for_copy) }
159
+ end
160
+ end
161
+
162
+ def modifiable?
163
+ # override to disable edit and destroy links for specific exports
164
+ true
165
+ end
166
+
167
+ def enabled?
168
+ export_model.count > 0
169
+ end
170
+
171
+ #########
172
+ protected
173
+ #########
174
+
175
+ def get_records(limit = nil)
176
+ get_export_values(export_model.where(build_conditions).includes(build_include).limit(limit))
177
+ end
178
+
179
+ def seed_records(objects)
180
+ @records = get_export_values(objects)
181
+ end
182
+
183
+ def get_export_values(objects)
184
+ objects.map { |object| object.export(rexport_methods) }
185
+ end
186
+
187
+ def get_rexport_models(rexport_model, result = [], path = nil)
188
+ return unless rexport_model.respond_to?(:rexport_fields)
189
+ result << RexportModel.new(rexport_model, path)
190
+ get_associations(rexport_model).each do |associated_model|
191
+ # prevent infinite loop by checking if this class is already in the result set
192
+ unless result.detect { |rexport_model| rexport_model.klass == associated_model.klass }
193
+ get_rexport_models(associated_model.klass, result, [path, associated_model.name].compact * '.')
194
+ end
195
+ end
196
+ return result
197
+ end
198
+
199
+ def get_associations(rexport_model)
200
+ (rexport_model.reflect_on_all_associations(:belongs_to) + rexport_model.reflect_on_all_associations(:has_one))
201
+ end
202
+
203
+ def build_include
204
+ root = Rexport::TreeNode.new('root')
205
+ (rexport_methods + filter_fields).select {|m| m.include?('.')}.each do |method|
206
+ root.add_child(method.split('.').values_at(0..-2))
207
+ end
208
+ root.to_include
209
+ end
210
+
211
+ def build_conditions
212
+ Hash.new.tap do |conditions|
213
+ export_filters.each do |filter|
214
+ conditions[get_database_field(filter.filter_field)] = filter.value
215
+ end
216
+ end
217
+ end
218
+
219
+ def get_database_field(field)
220
+ path = field.split('.')
221
+ field = path.pop
222
+ "#{get_klass_from_path(path).table_name}.#{field}"
223
+ end
224
+
225
+ def rexport_methods
226
+ @rexport_methods ||= export_model.get_rexport_methods(ordered_rexport_fields)
227
+ end
228
+
229
+ def rexport_fields
230
+ @rexport_fields ||= export_items.map {|i| i.rexport_field}
231
+ end
232
+
233
+ def ordered_rexport_fields
234
+ export_items.ordered.map {|i| i.rexport_field}
235
+ end
236
+
237
+ def filter_fields
238
+ export_filters.map {|f| f.filter_field}
239
+ end
240
+
241
+ def save_export_items
242
+ export_items.each do |export_item|
243
+ unless rexport_fields.include?(export_item.rexport_field)
244
+ export_item.destroy
245
+ end
246
+ end
247
+
248
+ export_items.reset
249
+
250
+ position = 0
251
+ rexport_fields.each do |rexport_field|
252
+ position += 1
253
+ export_item = export_items.detect {|i| i.rexport_field == rexport_field } || export_items.create(:rexport_field => rexport_field)
254
+ export_item.update_attribute(:position, position) if @set_position && export_item.position != position
255
+ end
256
+
257
+ export_items.reset
258
+ @rexport_fields = nil
259
+ return true
260
+ end
261
+
262
+ def attributes_for_copy
263
+ attributes.slice('model_name', 'description').merge(name: find_unique_name(self.name))
264
+ end
265
+
266
+ def find_unique_name(original_name, suffix = 0)
267
+ new_name = suffix == 0 ? "#{original_name} Copy" : "#{original_name} Copy [#{suffix}]"
268
+ self.class.find_by_name(new_name) ? find_unique_name(original_name, suffix += 1) : new_name
269
+ end
270
+ end
271
+ end
272
+ end
@@ -0,0 +1,86 @@
1
+ module Rexport
2
+ module ExportsControllerMethods
3
+ # GET /exports
4
+ def index
5
+ @exports = Export.categorical.alphabetical
6
+
7
+ respond_to do |format|
8
+ format.html # index.html.erb
9
+ end
10
+ end
11
+
12
+ # GET /exports/1
13
+ def show
14
+ @export = Export.find(params[:id])
15
+
16
+ respond_to do |format|
17
+ format.html # show.html.erb
18
+ format.csv { send_data(@export.to_csv, :type => export_content_type, :filename => filename) }
19
+ end
20
+ end
21
+
22
+ # GET /exports/new
23
+ def new
24
+ @export = Export.new(params[:export])
25
+
26
+ respond_to do |format|
27
+ format.html # new.html.erb
28
+ end
29
+ end
30
+
31
+ # GET /exports/1/edit
32
+ def edit
33
+ @export = Export.find(params[:id])
34
+ end
35
+
36
+ # POST /exports
37
+ def create
38
+ @export = params[:original_export_id] ? Export.find(params[:original_export_id]).copy : Export.new(params[:export])
39
+
40
+ respond_to do |format|
41
+ if @export.save
42
+ flash[:notice] = 'Export was successfully created.'
43
+ format.html { redirect_to(@export) }
44
+ else
45
+ format.html { render :action => "new" }
46
+ end
47
+ end
48
+ end
49
+
50
+ # PUT /exports/1
51
+ def update
52
+ @export = Export.find(params[:id])
53
+
54
+ respond_to do |format|
55
+ if @export.update_attributes(params[:export])
56
+ flash[:notice] = 'Export was successfully updated.'
57
+ format.html { redirect_to(@export) }
58
+ else
59
+ format.html { render :action => "edit" }
60
+ end
61
+ end
62
+ end
63
+
64
+ # DELETE /exports/1
65
+ def destroy
66
+ @export = Export.find(params[:id])
67
+ @export.destroy
68
+
69
+ respond_to do |format|
70
+ format.html { redirect_to(exports_url) }
71
+ end
72
+ end
73
+
74
+ #######
75
+ private
76
+ #######
77
+
78
+ def export_content_type
79
+ request.user_agent =~ /windows/i ? 'application/vnd.ms-excel' : 'text/csv'
80
+ end
81
+
82
+ def filename
83
+ "#{@export.model_name}_#{@export.name.gsub(/ /, '_')}_#{Time.now.strftime('%Y%m%d')}.csv"
84
+ end
85
+ end
86
+ end