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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +38 -0
- data/app/helpers/exports_helper.rb +25 -0
- data/app/views/export_filters/_export_filter.html.erb +7 -0
- data/app/views/export_items/_export_item.html.erb +8 -0
- data/app/views/export_items/edit.html.erb +15 -0
- data/app/views/exports/_edit.html.erb +7 -0
- data/app/views/exports/_filters.html.erb +9 -0
- data/app/views/exports/_form.html.erb +25 -0
- data/app/views/exports/_local_form.html.erb +1 -0
- data/app/views/exports/_rexport_model.html.erb +17 -0
- data/app/views/exports/_show.html.erb +53 -0
- data/app/views/exports/edit.html.erb +1 -0
- data/app/views/exports/index.html.erb +33 -0
- data/app/views/exports/new.html.erb +7 -0
- data/app/views/exports/show.html.erb +1 -0
- data/config/routes.rb +11 -0
- data/db/migrate/20091105182959_create_export_tables.rb +34 -0
- data/lib/rexport.rb +17 -0
- data/lib/rexport/data_fields.rb +152 -0
- data/lib/rexport/export_filter_methods.rb +34 -0
- data/lib/rexport/export_filters_controller_methods.rb +13 -0
- data/lib/rexport/export_item_methods.rb +45 -0
- data/lib/rexport/export_item_sortings_controller_methods.rb +13 -0
- data/lib/rexport/export_items_controller_methods.rb +32 -0
- data/lib/rexport/export_methods.rb +272 -0
- data/lib/rexport/exports_controller_methods.rb +86 -0
- data/lib/rexport/tree_node.rb +45 -0
- data/lib/rexport/version.rb +3 -0
- data/test/factories.rb +86 -0
- data/test/jenkins.bash +3 -0
- data/test/log/test.log +61761 -0
- data/test/test_helper.rb +168 -0
- data/test/unit/data_field_test.rb +25 -0
- data/test/unit/data_fields_test.rb +117 -0
- data/test/unit/export_methods_test.rb +97 -0
- data/test/unit/rexport_model_test.rb +35 -0
- data/test/unit/tree_node_test.rb +43 -0
- 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,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
|