populate-me 0.12.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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +20 -0
  5. data/README.md +655 -0
  6. data/Rakefile +14 -0
  7. data/example/config.ru +100 -0
  8. data/lib/populate_me.rb +2 -0
  9. data/lib/populate_me/admin.rb +157 -0
  10. data/lib/populate_me/admin/__assets__/css/asmselect.css +63 -0
  11. data/lib/populate_me/admin/__assets__/css/jquery-ui.min.css +6 -0
  12. data/lib/populate_me/admin/__assets__/css/main.css +244 -0
  13. data/lib/populate_me/admin/__assets__/img/help/children.png +0 -0
  14. data/lib/populate_me/admin/__assets__/img/help/create.png +0 -0
  15. data/lib/populate_me/admin/__assets__/img/help/delete.png +0 -0
  16. data/lib/populate_me/admin/__assets__/img/help/edit.png +0 -0
  17. data/lib/populate_me/admin/__assets__/img/help/form.png +0 -0
  18. data/lib/populate_me/admin/__assets__/img/help/list.png +0 -0
  19. data/lib/populate_me/admin/__assets__/img/help/login.png +0 -0
  20. data/lib/populate_me/admin/__assets__/img/help/logout.png +0 -0
  21. data/lib/populate_me/admin/__assets__/img/help/menu.png +0 -0
  22. data/lib/populate_me/admin/__assets__/img/help/overview.png +0 -0
  23. data/lib/populate_me/admin/__assets__/img/help/save.png +0 -0
  24. data/lib/populate_me/admin/__assets__/img/help/sort.png +0 -0
  25. data/lib/populate_me/admin/__assets__/img/help/sublist.png +0 -0
  26. data/lib/populate_me/admin/__assets__/js/asmselect.js +412 -0
  27. data/lib/populate_me/admin/__assets__/js/columnav.js +87 -0
  28. data/lib/populate_me/admin/__assets__/js/jquery-ui.min.js +7 -0
  29. data/lib/populate_me/admin/__assets__/js/main.js +388 -0
  30. data/lib/populate_me/admin/__assets__/js/mustache.js +578 -0
  31. data/lib/populate_me/admin/__assets__/js/sortable.js +2 -0
  32. data/lib/populate_me/admin/views/help.erb +94 -0
  33. data/lib/populate_me/admin/views/page.erb +189 -0
  34. data/lib/populate_me/api.rb +124 -0
  35. data/lib/populate_me/attachment.rb +186 -0
  36. data/lib/populate_me/document.rb +192 -0
  37. data/lib/populate_me/document_mixins/admin_adapter.rb +149 -0
  38. data/lib/populate_me/document_mixins/callbacks.rb +125 -0
  39. data/lib/populate_me/document_mixins/outcasting.rb +83 -0
  40. data/lib/populate_me/document_mixins/persistence.rb +95 -0
  41. data/lib/populate_me/document_mixins/schema.rb +198 -0
  42. data/lib/populate_me/document_mixins/typecasting.rb +70 -0
  43. data/lib/populate_me/document_mixins/validation.rb +44 -0
  44. data/lib/populate_me/file_system_attachment.rb +40 -0
  45. data/lib/populate_me/grid_fs_attachment.rb +103 -0
  46. data/lib/populate_me/mongo.rb +160 -0
  47. data/lib/populate_me/s3_attachment.rb +120 -0
  48. data/lib/populate_me/variation.rb +38 -0
  49. data/lib/populate_me/version.rb +4 -0
  50. data/populate-me.gemspec +34 -0
  51. data/test/helper.rb +37 -0
  52. data/test/test_admin.rb +183 -0
  53. data/test/test_api.rb +246 -0
  54. data/test/test_attachment.rb +167 -0
  55. data/test/test_document.rb +128 -0
  56. data/test/test_document_admin_adapter.rb +221 -0
  57. data/test/test_document_callbacks.rb +151 -0
  58. data/test/test_document_outcasting.rb +247 -0
  59. data/test/test_document_persistence.rb +83 -0
  60. data/test/test_document_schema.rb +280 -0
  61. data/test/test_document_typecasting.rb +128 -0
  62. data/test/test_grid_fs_attachment.rb +239 -0
  63. data/test/test_mongo.rb +324 -0
  64. data/test/test_s3_attachment.rb +281 -0
  65. data/test/test_variation.rb +91 -0
  66. data/test/test_version.rb +11 -0
  67. metadata +294 -0
@@ -0,0 +1,40 @@
1
+ require 'populate_me/attachment'
2
+
3
+ module PopulateMe
4
+
5
+ class FileSystemAttachment < Attachment
6
+
7
+ set :root, File.expand_path('public')
8
+
9
+ def next_available_filename filename
10
+ FileUtils.mkdir_p self.location_root
11
+ ext = File.extname(filename)
12
+ base = File.basename(filename,ext)
13
+ i = 0
14
+ loop do
15
+ suffix = i==0 ? '' : "-#{i}"
16
+ potential_filename = [base,suffix,ext].join
17
+ potential_location = self.location_for_filename(potential_filename)
18
+ if File.exist?(potential_location)
19
+ i += 1
20
+ else
21
+ filename = potential_filename
22
+ break
23
+ end
24
+ end
25
+ filename
26
+ end
27
+
28
+ def perform_create hash
29
+ return File.basename(hash[:variation_path]) unless WebUtils.blank?(hash[:variation_path])
30
+ source = hash[:tempfile].path
31
+ filename = self.next_available_filename hash[:filename]
32
+ destination = self.location_for_filename filename
33
+ FileUtils.cp source, destination
34
+ filename
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+
@@ -0,0 +1,103 @@
1
+ require 'populate_me/attachment'
2
+ require 'mongo'
3
+ require 'rack/grid_serve'
4
+
5
+ module PopulateMe
6
+
7
+ class MissingMongoDBError < StandardError; end
8
+
9
+ class GridFSAttachment < Attachment
10
+
11
+ # set :url_prefix, '/attachment'
12
+
13
+ # Attachee_prefix is moved on field_value for gridfs
14
+ def url variation_name=:original
15
+ return nil if WebUtils.blank?(self.field_filename(variation_name))
16
+ "#{settings.url_prefix.sub(/\/$/,'')}/#{self.field_filename(variation_name)}"
17
+ end
18
+ # Attachee_prefix is moved on field_value for gridfs
19
+ def location_root
20
+ File.join(
21
+ settings.root,
22
+ settings.url_prefix
23
+ )
24
+ end
25
+
26
+ def next_available_filename filename
27
+ ext = File.extname(filename)
28
+ base = "#{attachee_prefix}/#{File.basename(filename,ext)}"
29
+ i = 0
30
+ loop do
31
+ suffix = i==0 ? '' : "-#{i}"
32
+ potential_filename = [base,suffix,ext].join
33
+ if settings.db.fs.find(filename: potential_filename).count>0
34
+ i += 1
35
+ else
36
+ filename = potential_filename
37
+ break
38
+ end
39
+ end
40
+ filename
41
+ end
42
+
43
+ def perform_create hash
44
+ if hash[:variation].nil?
45
+ fn = next_available_filename(hash[:filename])
46
+ file = hash[:tempfile]
47
+ type = hash[:type]
48
+ else
49
+ fn = WebUtils.filename_variation hash[:future_field_value], hash[:variation].name, hash[:variation].ext
50
+ file = File.open(hash[:variation_path])
51
+ type = Rack::Mime.mime_type ".#{hash[:variation].ext}"
52
+ end
53
+ settings.db.fs.upload_from_stream(
54
+ fn,
55
+ file, {
56
+ content_type: type,
57
+ metadata: {
58
+ parent_collection: (self.document.class.respond_to?(:collection) ? self.document.class.collection.name : self.attachee_prefix),
59
+ }
60
+ }
61
+ )
62
+ file.close unless hash[:variation].nil?
63
+ fn
64
+ end
65
+
66
+ def deletable? variation_name=:original
67
+ !WebUtils.blank? self.field_filename(variation_name)
68
+ # Fine since deleting a non-existent file does not raise an error in mongo
69
+ end
70
+
71
+ def perform_delete variation_name=:original
72
+ gridfile = settings.db.fs.find(filename: self.field_filename(variation_name)).first
73
+ settings.db.fs.delete(gridfile['_id']) unless gridfile.nil?
74
+ end
75
+
76
+ class << self
77
+
78
+ def ensure_db
79
+ raise MissingMongoDBError, "Attachment class #{self.name} does not have a Mongo database." if settings.db.nil?
80
+ end
81
+
82
+ def middleware
83
+ Rack::GridServe
84
+ end
85
+
86
+ def middleware_options
87
+ [
88
+ {
89
+ prefix: settings.url_prefix.dup.gsub(/^\/|\/$/,''),
90
+ db: settings.db,
91
+ # lookup: :path
92
+ }
93
+ ]
94
+ end
95
+
96
+ end
97
+
98
+ end
99
+
100
+ GridFS = GridFSAttachment
101
+
102
+ end
103
+
@@ -0,0 +1,160 @@
1
+ require 'populate_me/document'
2
+ require 'mongo'
3
+
4
+ module PopulateMe
5
+
6
+ class MissingMongoDBError < StandardError; end
7
+
8
+ class Mongo < Document
9
+
10
+ self.settings.instance_eval do
11
+ def collection_name
12
+ puts 'yo'
13
+ if self[:collection_name].respond_to? :call
14
+ self[:collection_name].call
15
+ else
16
+ self[:collection_name]
17
+ end
18
+ end
19
+ end
20
+
21
+ class << self
22
+
23
+ def inherited sub
24
+ super
25
+ # self.inherited is not useful anymore because we use ::collection_name
26
+ # so that the class name exist when this is run.
27
+ # Which is not the case with dynamically created classes.
28
+ #
29
+ # But we'll keep it for legacy code in the meantime.
30
+ # Decide if we want to keep it.
31
+ #
32
+ # If statment is here for dynamically created classes,
33
+ # because Class.new.name==nil and then it breaks (see tests).
34
+ unless sub.name.nil?
35
+ sub.set :collection_name, WebUtils.dasherize_class_name(sub.name)
36
+ end
37
+ end
38
+
39
+ def collection_name
40
+ self.settings.collection_name || WebUtils.dasherize_class_name(self.name)
41
+ end
42
+
43
+ def collection
44
+ raise MissingMongoDBError, "Document class #{self.name} does not have a Mongo database." if settings.db.nil?
45
+ settings.db[self.collection_name]
46
+ end
47
+
48
+ def set_id_field
49
+ field :_id, {type: :id}
50
+ end
51
+
52
+ def sort_by f, direction=1
53
+ direction = 1 if direction==:asc
54
+ direction = -1 if direction==:desc
55
+ if f.is_a?(Hash)
56
+ @current_sort = f
57
+ elsif f.is_a?(Array)
58
+ @current_sort = f.inject({}) do |h,pair|
59
+ h.store(pair[0], pair[1])
60
+ h
61
+ end
62
+ else
63
+ raise(ArgumentError) unless [1,-1].include? direction
64
+ raise(ArgumentError) unless self.new.respond_to? f
65
+ @current_sort = {f => direction}
66
+ end
67
+ self
68
+ end
69
+
70
+ def id_string_key
71
+ (self.fields.keys[0]||'_id').to_s
72
+ end
73
+
74
+ def set_indexes f, ids=[]
75
+ if self.fields[f.to_sym][:direction]==:desc
76
+ ids = ids.dup.reverse
77
+ end
78
+ requests = ids.each_with_index.inject([]) do |list, (theid, i)|
79
+ theid = string_or_object_id theid
80
+ list << {update_one:
81
+ {
82
+ filter: {self.id_string_key=>theid},
83
+ update: {'$set'=>{f=>i}}
84
+ }
85
+ }
86
+ end
87
+ collection.bulk_write requests
88
+ end
89
+
90
+ def admin_get theid
91
+ return self.admin_get_multiple(theid) if theid.is_a?(Array)
92
+ theid = string_or_object_id theid
93
+ self.cast{ collection.find({id_string_key => theid}).first }
94
+ end
95
+ alias_method :[], :admin_get
96
+
97
+ def admin_get_multiple theids, o={sort: nil}
98
+ theids = theids.uniq.compact.map{|theid| string_or_object_id(theid) }
99
+ self.admin_find(o.merge({
100
+ query: {id_string_key => {'$in' => theids} }
101
+ }))
102
+ end
103
+
104
+ def admin_find o={}
105
+ query = o.delete(:query) || {}
106
+ o[:sort] ||= @current_sort
107
+ if o.key?(:fields)
108
+ o[:projection] = o[:fields].inject({}) do |h, f|
109
+ h[f.to_sym] = 1
110
+ h
111
+ end
112
+ o.delete(:fields)
113
+ end
114
+ self.cast{ collection.find(query, o) }
115
+ end
116
+
117
+ def admin_find_first o={}
118
+ self.admin_find(o.merge({limit: 1}))[0]
119
+ end
120
+
121
+ def admin_distinct field, o={}
122
+ query = o.delete(:query) || {}
123
+ self.collection.distinct field, query, o
124
+ end
125
+
126
+ def string_or_object_id theid
127
+ if BSON::ObjectId.legal?(theid)
128
+ BSON::ObjectId.from_string(theid)
129
+ else
130
+ theid
131
+ end
132
+ end
133
+
134
+ end
135
+
136
+ attr_accessor :_id
137
+
138
+ def id; @_id; end
139
+ def id= value; @_id = value; end
140
+
141
+ def perform_create
142
+ result = self.class.collection.insert_one(self.to_h)
143
+ if result.ok? and self.id.nil?
144
+ self.id = result.inserted_id
145
+ end
146
+ self.id
147
+ end
148
+
149
+ def perform_update
150
+ self.class.collection.update_one({'_id'=> self.id}, self.to_h)
151
+ self.id
152
+ end
153
+
154
+ def perform_delete
155
+ self.class.collection.delete_one({'_id'=> self.id})
156
+ end
157
+
158
+ end
159
+ end
160
+
@@ -0,0 +1,120 @@
1
+ require 'populate_me/attachment'
2
+ require 'aws-sdk-s3'
3
+
4
+ module PopulateMe
5
+
6
+ class MissingBucketError < StandardError; end
7
+
8
+ class S3Attachment < Attachment
9
+
10
+ # For S3 this option behaves a bit differently.
11
+ # Because S3 file are served directly instead of using a middleware,
12
+ # the url_prefix is just used in the key name before the attachee_prefix.
13
+ # It helps saving keys under /public for example which is a common idiom
14
+ # to save file in a path public by default.
15
+ # This option can be overriden at the field level.
16
+ set :url_prefix, '/public'
17
+
18
+ # Attachee_prefix is moved on field_value for S3
19
+ # as well as url_prefix
20
+ def url variation_name=:original
21
+ return nil if WebUtils.blank?(self.field_filename(variation_name))
22
+ "#{settings.bucket.url}/#{self.field_filename(variation_name)}"
23
+ end
24
+ # Attachee_prefix is moved on field_value for S3
25
+ # as well as url_prefix
26
+ def location_root
27
+ File.join(
28
+ settings.root,
29
+ settings.url_prefix
30
+ )
31
+ end
32
+
33
+ def local_url_prefix
34
+ (
35
+ self.field_options[:url_prefix] ||
36
+ self.document.class.settings.s3_url_prefix ||
37
+ settings.url_prefix
38
+ ).gsub(/^\/|\/$/,'')
39
+ end
40
+
41
+ def next_available_filename filename
42
+ ext = File.extname(filename)
43
+ base = File.join(
44
+ local_url_prefix,
45
+ attachee_prefix,
46
+ File.basename(filename,ext)
47
+ ).gsub(/^\//,'')
48
+ i = 0
49
+ loop do
50
+ suffix = i==0 ? '' : "-#{i}"
51
+ potential_filename = [base,suffix,ext].join
52
+ if settings.bucket.object(potential_filename).exists?
53
+ i += 1
54
+ else
55
+ filename = potential_filename
56
+ break
57
+ end
58
+ end
59
+ filename
60
+ end
61
+
62
+ def perform_create hash
63
+ if hash[:variation].nil?
64
+ fn = next_available_filename(hash[:filename])
65
+ file = hash[:tempfile]
66
+ type = hash[:type]
67
+ else
68
+ fn = WebUtils.filename_variation hash[:future_field_value], hash[:variation].name, hash[:variation].ext
69
+ file = File.open(hash[:variation_path])
70
+ type = Rack::Mime.mime_type ".#{hash[:variation].ext}"
71
+ end
72
+ settings.bucket.put_object({
73
+ acl: self.field_options[:acl] || 'public-read',
74
+ key: fn,
75
+ content_type: type,
76
+ body: file,
77
+ metadata: {
78
+ parent_collection: (self.document.class.respond_to?(:collection) ? self.document.class.collection.name : self.attachee_prefix),
79
+ }
80
+ })
81
+ file.close unless hash[:variation].nil?
82
+ fn
83
+ end
84
+
85
+ def deletable? variation_name=:original
86
+ !WebUtils.blank? self.field_filename(variation_name)
87
+ # Fine since deleting a non-existent file does not raise an error in S3
88
+ end
89
+
90
+ def perform_delete variation_name=:original
91
+ s3file = settings.bucket.object(self.field_filename(variation_name))
92
+ s3file.delete unless s3file.nil?
93
+ end
94
+
95
+ class << self
96
+
97
+ def ensure_bucket
98
+ raise MissingBucketError, "Attachment class #{self.name} does not have an S3 bucket." if settings.bucket.nil?
99
+ end
100
+
101
+ def middleware
102
+ nil
103
+ end
104
+
105
+ # def middleware_options
106
+ # [
107
+ # {
108
+ # prefix: settings.url_prefix.dup.gsub(/^\/|\/$/,''),
109
+ # db: settings.db,
110
+ # # lookup: :path
111
+ # }
112
+ # ]
113
+ # end
114
+
115
+ end
116
+
117
+ end
118
+
119
+ end
120
+
@@ -0,0 +1,38 @@
1
+ module PopulateMe
2
+
3
+ class Variation < Struct.new :name, :ext, :job
4
+
5
+ # Simple class to deal with variations of an attachment
6
+ # Mainly variation of images using ImageMagick
7
+ # but it could be anything else like creating the pdf version
8
+ # of a text file
9
+
10
+ def initialize name, ext, job_as_proc=nil, &job_as_block
11
+ super name, ext, job_as_proc||job_as_block
12
+ end
13
+
14
+ class << self
15
+
16
+ def new_image_magick_job name, ext, convert_string, options={}
17
+ o = {
18
+ strip: true, progressive: true,
19
+ }.merge(options)
20
+ defaults = ""
21
+ defaults << "-strip " if o[:strip]
22
+ defaults << "-interlace Plane " if o[:progressive] and [:jpg,:jpeg].include?(ext.to_sym)
23
+ job = lambda{ |src,dst|
24
+ Kernel.system "convert \"#{src}\" #{defaults}#{convert_string} \"#{dst}\""
25
+ }
26
+ self.new name, ext, job
27
+ end
28
+
29
+ def default
30
+ self.new_image_magick_job(:populate_me_thumb, :jpg, "-flatten -resize '400x225^' -gravity center -extent 400x225")
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+