populate-me 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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
+