campbellhay-mongo 1.0.14

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ Gemfile.lock
2
+ *.gem
3
+
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
@@ -0,0 +1,14 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'campbellhay-mongo'
3
+ s.version = "1.0.14"
4
+ s.platform = Gem::Platform::RUBY
5
+ s.summary = "The CampbellHay Mongo toolbox"
6
+ s.description = "The CampbellHay Mongo toolbox"
7
+ s.files = `git ls-files`.split("\n").sort
8
+ s.require_path = './lib'
9
+ s.author = "Mickael Riga"
10
+ s.email = "mig@campbellhay.com"
11
+ s.homepage = "http://www.campbellhay.com"
12
+ s.add_dependency('mongo', '>=1.8.2')
13
+ s.add_development_dependency('bacon')
14
+ end
data/gempush ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+ mv *.gem "/Users/mig/web_apps/CHA_gems/public/gems"
3
+ cd "/Users/mig/web_apps/CHA_gems"
4
+ gem generate_index -d './public'
5
+ git add .
6
+ git commit -am 'Update gems'
7
+ git push heroku master
@@ -0,0 +1,221 @@
1
+ # encoding: utf-8
2
+
3
+ require 'mongo_mutation'
4
+
5
+ module MongoCrushyform
6
+
7
+ module ClassMethods
8
+
9
+ def crushyform_types
10
+ @crushyform_types ||= {
11
+ :none => proc{''},
12
+ :string => proc do |m,c,o|
13
+ if o[:autocompleted]
14
+ values = o[:autocomplete_options] || m.class.collection.distinct(c)
15
+ js = <<-EOJS
16
+ <script type="text/javascript" charset="utf-8">
17
+ $(function(){
18
+ $( "##{m.field_id_for(c)}" ).autocomplete({source: ["#{values.join('","')}"]});
19
+ });
20
+ </script>
21
+ EOJS
22
+ end
23
+ tag = "<input type='%s' name='%s' value=\"%s\" id='%s' class='%s' %s />%s\n" % [o[:input_type]||'text', o[:input_name], o[:input_value], m.field_id_for(c), o[:input_class], o[:required]&&'required', o[:required]]
24
+ "#{tag}#{js}"
25
+ end,
26
+ :slug => proc do |m,c,o|
27
+ crushyform_types[:string].call(m,c,o)
28
+ end,
29
+ :boolean => proc do |m,c,o|
30
+ crushid = m.field_id_for(c)
31
+ s = ['checked', nil]
32
+ s.reverse! unless o[:input_value]
33
+ out = "<span class='%s'>"
34
+ out += "<input type='radio' name='%s' value='true' id='%s' %s /> <label for='%s'>Yes</label> "
35
+ out += "<input type='radio' name='%s' value='false' id='%s-no' %s /> <label for='%s-no'>No</label>"
36
+ out += "</span>\n"
37
+ out % [o[:input_class], o[:input_name], crushid, s[0], crushid, o[:input_name], crushid, s[1], crushid]
38
+ end,
39
+ :text => proc do |m,c,o|
40
+ "<textarea name='%s' id='%s' class='%s' %s>%s</textarea>%s\n" % [o[:input_name], m.field_id_for(c), o[:input_class], o[:required]&&'required', o[:input_value], o[:required]]
41
+ end,
42
+ :date => proc do |m,c,o|
43
+ o[:input_value] = o[:input_value].strftime("%Y-%m-%d") if o[:input_value].respond_to?(:strftime)
44
+ o[:required] = "%s Format: yyyy-mm-dd" % [o[:required]]
45
+ crushyform_types[:string].call(m,c,o)
46
+ end,
47
+ :time => proc do |m,c,o|
48
+ o[:input_value] = o[:input_value].strftime("%T") if o[:input_value].respond_to?(:strftime)
49
+ o[:required] = "%s Format: hh:mm:ss" % [o[:required]]
50
+ crushyform_types[:string].call(m,c,o)
51
+ end,
52
+ :datetime => proc do |m,c,o|
53
+ o[:input_value] = o[:input_value].strftime("%Y-%m-%d %T") if o[:input_value].respond_to?(:strftime)
54
+ o[:required] = "%s Format: yyyy-mm-dd hh:mm:ss" % [o[:required]]
55
+ crushyform_types[:string].call(m,c,o)
56
+ end,
57
+ :parent => proc do |m,c,o|
58
+ parent_class = o[:parent_class].nil? ? Kernel.const_get(c.sub(/^id_/, '')) : m.resolve_class(o[:parent_class])
59
+ option_list = parent_class.to_dropdown(o[:input_value])
60
+ "<select name='%s' id='%s' class='%s'>%s</select>\n" % [o[:input_name], m.field_id_for(c), o[:input_class], option_list]
61
+ end,
62
+ :children => proc do |m,c,o|
63
+ children_class = o[:children_class].nil? ? Kernel.const_get(c.sub(/^ids_/, '')) : m.resolve_class(o[:children_class])
64
+ opts = o.update({
65
+ :multiple=>true,
66
+ :select_options=>children_class.dropdown_cache
67
+ })
68
+ @crushyform_types[:select].call(m,c,opts)
69
+ end,
70
+ :attachment => proc do |m,c,o|
71
+ deleter = "<input type='checkbox' name='#{o[:input_name]}' class='deleter' value='nil' /> Delete this file<br />" unless m.doc[c].nil?
72
+ "%s%s<input type='file' name='%s' id='%s' class='%s' />%s\n" % [m.to_thumb(c), deleter, o[:input_name], m.field_id_for(c), o[:input_class], o[:required]]
73
+ end,
74
+ :select => proc do |m,c,o|
75
+ out = "<select name='%s%s' id='%s' class='%s' %s title='-- Select --'>\n" % [o[:input_name], ('[]' if o[:multiple]), m.field_id_for(c), o[:input_class], ('multiple' if o[:multiple])]
76
+ o[:select_options] = m.__send__(o[:select_options]) unless o[:select_options].kind_of?(Array)
77
+ select_options = o[:select_options].dup
78
+ if (o[:multiple] && !o[:input_value].nil? && o[:input_value].size>1)
79
+ o[:input_value].reverse.each do |v|
80
+ elem = select_options.find{|x| x==v||(x||[])[1]==v }
81
+ select_options.unshift(select_options.delete(elem)) unless elem.nil?
82
+ end
83
+ end
84
+ if select_options.kind_of?(Array)
85
+ select_options.each do |op|
86
+ key,val = op.kind_of?(Array) ? [op[0],op[1]] : [op,op]
87
+ if key==:optgroup
88
+ out << "<optgroup label='%s'>\n" % [val]
89
+ elsif key==:closegroup
90
+ out << "</optgroup>\n"
91
+ else
92
+ selected = 'selected' if (val==o[:input_value] || (o[:input_value]||[]).include?(val))
93
+ out << "<option value='%s' %s>%s</option>\n" % [val,selected,key]
94
+ end
95
+ end
96
+ end
97
+ out << "</select>%s\n" % [o[:required]]
98
+ end,
99
+ :string_list => proc do |m,c,o|
100
+ if o[:autocompleted]
101
+ values = o[:autocomplete_options] || m.class.collection.distinct(c)
102
+ js = <<-EOJS
103
+ <script type="text/javascript" charset="utf-8">
104
+ $(function(){
105
+ $( "##{m.field_id_for(c)}" )
106
+ .bind( "keydown", function( event ) {
107
+ if ( event.keyCode === $.ui.keyCode.TAB &&
108
+ $( this ).data( "autocomplete" ).menu.active ) {
109
+ event.preventDefault();
110
+ }
111
+ })
112
+ .autocomplete({
113
+ minLength: 0,
114
+ source: function( request, response ) {
115
+ response($.ui.autocomplete.filter(["#{values.join('","')}"], request.term.split(/,\s*/).pop()));
116
+ },
117
+ focus: function() { return false; },
118
+ select: function( event, ui ) {
119
+ var terms = this.value.split(/,\s*/);
120
+ terms.pop();
121
+ terms.push(ui.item.value);
122
+ terms.push("");
123
+ this.value = terms.join( ", " );
124
+ return false;
125
+ }
126
+ });
127
+ });
128
+ </script>
129
+ EOJS
130
+ o[:autocompleted] = false # reset so that it does not autocomplete for :string type below
131
+ end
132
+ tag = @crushyform_types[:string].call(m,c,o.update({:input_value=>(o[:input_value]||[]).join(',')}))
133
+ "#{tag}#{js}"
134
+ end,
135
+ :permalink => proc do |instance, column_name, options|
136
+ values = "<option value=''>Or Browse the list</option>\n"
137
+ tag = @crushyform_types[:string].call(instance, column_name, options)
138
+ return tag if options[:permalink_classes].nil?
139
+ options[:permalink_classes].each do |sym|
140
+ c = Kernel.const_get sym
141
+ entries = c.find
142
+ unless entries.count==0
143
+ values << "<optgroup label='#{c.human_name}'>\n"
144
+ entries.each do |e|
145
+ values << "<option value='#{e.permalink}' #{'selected' if e.permalink==options[:input_value]}>#{e.to_label}</option>\n"
146
+ end
147
+ values << "</optgroup>\n"
148
+ end
149
+ end
150
+ "#{tag}<br />\n<select name='__permalink' class='permalink-dropdown'>\n#{values}</select>\n"
151
+ end
152
+ }
153
+ end
154
+
155
+ # What represents a required field
156
+ # Can be overriden
157
+ def crushyfield_required; "<span class='crushyfield-required'> *</span>"; end
158
+ # Stolen from ERB
159
+ def html_escape(s)
160
+ s.to_s.gsub(/&/, "&amp;").gsub(/\"/, "&quot;").gsub(/>/, "&gt;").gsub(/</, "&lt;")
161
+ end
162
+ # Cache dropdown options for children classes to use
163
+ # Meant to be reseted each time an entry is created, updated or destroyed
164
+ # So it is only rebuild once required after the list has changed
165
+ # Maintaining an array and not rebuilding it all might be faster
166
+ # But it will not happen much so that it is fairly acceptable
167
+ def to_dropdown(selection=nil, nil_name='** UNDEFINED **')
168
+ dropdown_cache.inject("<option value=''>#{nil_name}</option>\n") do |out, row|
169
+ selected = 'selected' if row[1]==selection
170
+ "%s%s%s%s" % [out, row[2], selected, row[3]]
171
+ end
172
+ end
173
+ def dropdown_cache
174
+ @dropdown_cache ||= self.find({},:fields=>['_id',label_column]).inject([]) do |out,row|
175
+ out.push([row.to_label, row.id.to_s, "<option value='#{row.id}' ", ">#{row.to_label}</option>\n"])
176
+ end
177
+ end
178
+ def reset_dropdown_cache; @dropdown_cache = nil; end
179
+
180
+ end
181
+
182
+ module InstanceMethods
183
+ def crushyform(columns=model.schema.keys, action=nil, meth='POST')
184
+ columns.delete('_id')
185
+ fields = columns.inject(""){|out,c|out+crushyfield(c)}
186
+ enctype = fields.match(/type='file'/) ? "enctype='multipart/form-data'" : ''
187
+ action.nil? ? fields : "<form action='%s' method='%s' %s>%s</form>\n" % [action, meth, enctype, fields]
188
+ end
189
+ # crushyfield is crushyinput but with label+error
190
+ def crushyfield(col, o={})
191
+ return '' if (o[:type]==:none || model.schema[col][:type]==:none)
192
+ default_field_name = col[/^id_/] ? Kernel.const_get(col.sub(/^id_/, '')).human_name : col.tr('_', ' ').capitalize
193
+ field_name = o[:name] || model.schema[col][:name] || default_field_name
194
+ error_list = errors_on(col).map{|e|" - #{e}"} if !errors_on(col).nil?
195
+ invisibility = "style='display: none;'" if (o[:invisible]==true || model.schema[col][:invisible]==true)
196
+ "<p class='crushyfield %s' %s><label for='%s'>%s</label><span class='crushyfield-error-list'>%s</span><br />\n%s</p>\n" % [error_list&&'crushyfield-error', invisibility, field_id_for(col), field_name, error_list, crushyinput(col, o)]
197
+ end
198
+ def crushyinput(col, o={})
199
+ o = model.schema[col].dup.update(o)
200
+ o[:input_name] ||= "model[#{col}]"
201
+ o[:input_value] = o[:input_value].nil? ? self[col] : o[:input_value]
202
+ o[:input_value] = model.html_escape(o[:input_value]) if (o[:input_value].is_a?(String) && o[:html_escape]!=false)
203
+ o[:required] = o[:required]==true ? model.crushyfield_required : o[:required]
204
+ crushyform_type = model.crushyform_types[o[:type]] || model.crushyform_types[:string]
205
+ crushyform_type.call(self,col,o)
206
+ end
207
+ # Provide a thumbnail for the column
208
+ def to_thumb(c)
209
+ current = @doc[c]
210
+ if current.respond_to?(:[])
211
+ "<img src='/gridfs/#{@doc[c]['stash_thumb_gif']}' width='100' onerror=\"this.style.display='none'\" />\n"
212
+ end
213
+ end
214
+ # Reset dropdowns on hooks
215
+ def after_save; model.reset_dropdown_cache; super; end
216
+ def after_delete; model.reset_dropdown_cache; super; end
217
+ # Fix types
218
+ def fix_type_string_list(k,v); @doc[k] = v.to_s.strip.split(/\s*,\s*/).compact if v.is_a?(String); end
219
+ end
220
+
221
+ end
@@ -0,0 +1,272 @@
1
+ # encoding: utf-8
2
+
3
+ require 'mongo'
4
+
5
+ module MongoMutation
6
+
7
+ def self.included(weak)
8
+ weak.extend(MutateClass)
9
+ weak.db = DB if defined?(DB)
10
+ weak.schema = BSON::OrderedHash.new
11
+ weak.relationships = BSON::OrderedHash.new
12
+ end
13
+
14
+ module MutateClass
15
+ attr_accessor :db, :schema, :relationships
16
+ attr_writer :label_column, :slug_column, :sorting_order
17
+
18
+ LABEL_COLUMNS = ['title', 'label', 'fullname', 'full_name', 'surname', 'lastname', 'last_name', 'name', 'firstname', 'first_name', 'login', 'caption', 'reference', 'file_name', 'body', '_id']
19
+ def label_column; @label_column ||= LABEL_COLUMNS.find{|c| @schema.keys.include?(c)||c=='_id'}; end
20
+ def slug_column; @slug_column ||= (@schema.find{|k,v| v[:type]==:slug}||[])[0]; end
21
+ def foreign_key_name(plural=false); "id#{'s' if plural}_"+self.name; end
22
+ def human_name; self.name.gsub(/([A-Z])/, ' \1')[1..-1]; end
23
+ def human_plural_name; human_name+'s'; end
24
+ def collection; db[self.name]; end
25
+ def ref(id)
26
+ if id.is_a?(String)&&BSON::ObjectId.legal?(id)
27
+ id = BSON::ObjectId.from_string(id)
28
+ elsif !id.is_a?(BSON::ObjectId)
29
+ id = ''
30
+ end
31
+ {'_id'=>id}
32
+ end
33
+ def find(selector={},opts={})
34
+ selector.update(opts.delete(:selector)||{})
35
+ opts = {:sort=>self.sorting_order}.update(opts)
36
+ collection.find(selector,opts).extend(CursorMutation)
37
+ end
38
+ def find_one(spec_or_object_id=nil,opts={})
39
+ spec_or_object_id.nil? ? spec_or_object_id = opts.delete(:selector) : spec_or_object_id.update(opts.delete(:selector)||{})
40
+ opts = {:sort=>self.sorting_order}.update(opts)
41
+ item = collection.find_one(spec_or_object_id,opts)
42
+ item.nil? ? nil : self.new(item)
43
+ end
44
+ def count(opts={}); collection.count(opts); end
45
+
46
+ def sorting_order
47
+ @sorting_order ||= if @schema.key?('position')&&!@schema['position'][:scope].nil?
48
+ [[@schema['position'][:scope], :asc], ['position', :asc]]
49
+ elsif @schema.key?('position')
50
+ [['position', :asc],['_id', :asc]]
51
+ else
52
+ ['_id', :asc]
53
+ end
54
+ end
55
+
56
+ def sort(id_list)
57
+ id_list.each_with_index do |id, position|
58
+ collection.update(ref(id), {'$set' => {'position'=>position}})
59
+ end
60
+ end
61
+
62
+ # CRUD
63
+ def get(id, opts={}); doc = collection.find_one(ref(id), opts); doc.nil? ? nil : self.new(doc); end
64
+ def delete(id); collection.remove(ref(id)); end
65
+
66
+ def is_unique(doc={})
67
+ return unless collection.count==0
68
+ doc = {'_id'=>BSON::ObjectId('000000000000000000000000')}.update(doc)
69
+ d = self.new
70
+ d.doc.update(doc)
71
+ d.save
72
+ end
73
+
74
+ private
75
+ def slot(name,opts={})
76
+ @schema[name] = {:type=>:string}.update(opts)
77
+ define_method(name) { @doc[name] }
78
+ define_method("#{name}=") { |x| @doc[name] = x }
79
+ end
80
+ def image_slot(name='image',opts={})
81
+ slot name, {:type=>:attachment}.update(opts)
82
+ slot "#{name}_tooltip"
83
+ slot "#{name}_alternative_text"
84
+ end
85
+ def has_many(k,opts={}); @relationships[k] = opts; end
86
+ end
87
+
88
+ attr_accessor :doc, :old_doc, :errors, :is_new
89
+ def initialize(document=nil); @errors={}; @doc = document || default_doc; end
90
+ def default_doc
91
+ @is_new = true
92
+ out = {}
93
+ model.schema.each { |k,v| out.store(k,v[:default].is_a?(Proc) ? v[:default].call : v[:default]) }
94
+ out
95
+ end
96
+ def model; self.class; end
97
+ def id; @doc['_id']; end
98
+ def [](field); @doc[field]; end
99
+ def []=(field,val); @doc[field] = val; end
100
+ def to_label; @doc[model.label_column].to_s.tr("\n\r", ' '); end
101
+ ACCENTS_FROM = "ÀÁÂÃÄÅàáâãäåĀāĂ㥹ÇçĆćĈĉĊċČčÐðĎďĐđÈÉÊËèéêëĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħÌÍÎÏìíîïĨĩĪīĬĭĮįİıĴĵĶķĸĹĺĻļĽľĿŀŁłÑñŃńŅņŇňʼnŊŋÒÓÔÕÖØòóôõöøŌōŎŏŐőŔŕŖŗŘřŚśŜŝŞşŠšſŢţŤťŦŧÙÚÛÜùúûüŨũŪūŬŭŮůŰűŲųŴŵÝýÿŶŷŸŹźŻżŽž"
102
+ ACCENTS_TO = "AAAAAAaaaaaaAaAaAaCcCcCcCcCcDdDdDdEEEEeeeeEeEeEeEeEeGgGgGgGgHhHhIIIIiiiiIiIiIiIiIiJjKkkLlLlLlLlLlNnNnNnNnnNnOOOOOOooooooOoOoOoRrRrRrSsSsSsSssTtTtTtUUUUuuuuUuUuUuUuUuUuWwYyyYyYZzZzZz"
103
+ def auto_slug
104
+ s = self.to_label.tr(ACCENTS_FROM,ACCENTS_TO).tr(' .,;:?!/\'"()[]{}<>','-').gsub(/&/, 'and')
105
+ defined?(::Rack::Utils) ? ::Rack::Utils.escape(s) : s
106
+ end
107
+ def to_slug; @doc[model.column_slug]||self.auto_slug; end
108
+ def to_param; "#{@doc['_id']}-#{to_label.scan(/\w+/).join('-')}"; end
109
+ def field_id_for(col); "%s-%s-%s" % [id||'new',model.name,col]; end
110
+
111
+ # relationships
112
+ def resolve_class(k); k.kind_of?(Class) ? k : Kernel.const_get(k); end
113
+ def parent(k, opts={})
114
+ if k.kind_of?(String)
115
+ key = k
116
+ klass = resolve_class(model.schema[k][:parent_class])
117
+ else
118
+ klass = resolve_class(k)
119
+ key = klass.foreign_key_name
120
+ end
121
+ klass.get(@doc[key], opts)
122
+ end
123
+ def slot_children(k, opts={})
124
+ if k.kind_of?(String)
125
+ key = k
126
+ klass = resolve_class(model.schema[k][:children_class])
127
+ else
128
+ klass = resolve_class(k)
129
+ key = klass.foreign_key_name(true)
130
+ end
131
+ ids = (@doc[key]||[]).map{|i| BSON::ObjectId.from_string(i) }
132
+ selector = {'_id'=>{'$in'=>ids}}
133
+ sort_proc = proc{ |a,b| ids.index(a['_id'])<=>ids.index(b['_id']) }
134
+ klass.find(selector, opts).to_a.sort(&sort_proc)
135
+ end
136
+ def first_slot_child(k, opts={})
137
+ if k.kind_of?(String)
138
+ key = k
139
+ klass = resolve_class(model.schema[k][:children_class])
140
+ else
141
+ klass = resolve_class(k)
142
+ key = klass.foreign_key_name(true)
143
+ end
144
+ klass.get((@doc[key]||[])[0], opts)
145
+ end
146
+ def children(k,opts={})
147
+ k = resolve_class(k)
148
+ slot_name = opts.delete(:slot_name) || model.foreign_key_name
149
+ k.find({slot_name=>@doc['_id'].to_s}, opts)
150
+ end
151
+ def first_child(k,opts={})
152
+ k = resolve_class(k)
153
+ slot_name = opts.delete(:slot_name) || model.foreign_key_name
154
+ d = k.find_one({slot_name=>@doc['_id'].to_s}, opts)
155
+ end
156
+ def children_count(k,sel={})
157
+ k = resolve_class(k)
158
+ slot_name = sel.delete(:slot_name) || model.foreign_key_name
159
+ k.collection.count(:query => {slot_name=>@doc['_id'].to_s}.update(sel))
160
+ end
161
+
162
+ # CRUD
163
+ def delete
164
+ before_delete
165
+ model.delete(@doc['_id'])
166
+ after_delete
167
+ end
168
+
169
+ # saving and hooks
170
+ def new?; @is_new ||= !@doc.key?('_id'); end
171
+ def update_doc(fields); @old_doc = @doc.dup; @doc.update(fields); @is_new = false; self; end
172
+ # Getter and setter in one
173
+ def errors_on(col,message=nil)
174
+ message.nil? ? @errors[col] : @errors[col] = (@errors[col]||[]) << message
175
+ end
176
+ def before_delete; @old_doc = @doc.dup; end
177
+ alias before_destroy before_delete
178
+ def after_delete
179
+ model.relationships.each do |k,v|
180
+ Kernel.const_get(k).find({model.foreign_key_name=>@old_doc['_id'].to_s}).each{|m| m.delete} unless v[:independent]
181
+ end
182
+ end
183
+ alias after_destroy after_delete
184
+ def valid?
185
+ before_validation
186
+ validate
187
+ after_validation
188
+ @errors.empty?
189
+ end
190
+ def before_validation
191
+ @errors = {}
192
+ @doc.each do |k,v|
193
+ next unless model.schema.key?(k)
194
+ type = k=='_id' ? :primary_key : model.schema[k][:type]
195
+ fix_method = "fix_type_#{type}"
196
+ if v==''
197
+ default = model.schema[k][:default]
198
+ @doc[k] = default.is_a?(Proc) ? default.call : default
199
+ else
200
+ self.__send__(fix_method, k, v) if self.respond_to?(fix_method)
201
+ end
202
+ end
203
+ end
204
+ def validate; end
205
+ def after_validation; end
206
+ def fix_type_integer(k,v); @doc[k] = v.to_i; end
207
+ def fix_type_boolean(k,v); @doc[k] = (v=='true'||v==true) ? true : false; end
208
+ def fix_type_slug(k,v); @doc[k] = self.auto_slug if v.to_s==''; end
209
+ def fix_type_date(k,v)
210
+ if v.is_a?(String)
211
+ if v[/\d\d\d\d-\d\d-\d\d/]
212
+ @doc[k] = ::Time.utc(*v.split('-'))
213
+ else
214
+ default = model.schema[k][:default]
215
+ @doc[k] = default.is_a?(Proc) ? default.call : default
216
+ end
217
+ end
218
+ end
219
+ def fix_type_datetime(k,v)
220
+ if v.is_a?(String)
221
+ if v[/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/]
222
+ @doc[k] = ::Time.utc(*v.split(/[-:\s]/))
223
+ else
224
+ default = model.schema[k][:default]
225
+ @doc[k] = default.is_a?(Proc) ? default.call : default
226
+ end
227
+ end
228
+ end
229
+
230
+ def save
231
+ return nil unless valid?
232
+ before_save
233
+ if new?
234
+ before_create
235
+ id = model.collection.insert(@doc)
236
+ @doc['_id'] = id
237
+ after_create
238
+ else
239
+ before_update
240
+ id = model.collection.update({'_id'=>@doc['_id']}, @doc)
241
+ after_update
242
+ end
243
+ after_save
244
+ id.nil? ? nil : self
245
+ end
246
+ def before_save; end
247
+ def before_create; end
248
+ def before_update; end
249
+ def after_save; end
250
+ def after_create; @is_new = false; end
251
+ def after_update; end
252
+
253
+ # ==========
254
+ # = Cursor =
255
+ # ==========
256
+ module CursorMutation
257
+ def self.extended(into)
258
+ into.set_mutant_class
259
+ end
260
+ def set_mutant_class
261
+ @mutant_class = Kernel.const_get(collection.name)
262
+ end
263
+ def next
264
+ n = super
265
+ n.nil? ? nil : @mutant_class.new(n)
266
+ end
267
+ # legacy
268
+ def each_mutant(&b); each(&b); end
269
+ def each_mutant_with_index(&b); each_with_index(&b); end
270
+ end
271
+
272
+ end
@@ -0,0 +1,155 @@
1
+ # encoding: utf-8
2
+
3
+ require 'mongo_mutation'
4
+
5
+ module MongoStash
6
+
7
+ def self.included(base)
8
+ MongoStash.classes << base
9
+ base.extend(ClassMethods)
10
+ base.gridfs = GRID
11
+ end
12
+
13
+ module ClassMethods
14
+ attr_accessor :gridfs
15
+ def all_after_stash
16
+ self.collection.find.each do |i|
17
+ self.schema.each do |k,v|
18
+ obj = self.new(i)
19
+ obj.after_stash(k) if v[:type]==:attachment&&obj[k].to_s!=''
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def build_image_tag(col='image', style='original', html_attributes={})
26
+ return '' if @doc[col].nil?||@doc[col][style].nil?
27
+ title_field, alt_field = col+'_tooltip', col+'_alternative_text'
28
+ title = @doc[title_field] if model.schema.keys.include?(title_field)
29
+ alt = @doc[alt_field] if model.schema.keys.include?(alt_field)
30
+ html_attributes = {:src => "/gridfs/#{@doc[col][style]}", :title => title, :alt => alt}.update(html_attributes)
31
+ html_attributes = html_attributes.map do |k,v|
32
+ %{#{k}="#{model.html_escape(v.to_s)}"}
33
+ end.join(' ')
34
+ "<img #{html_attributes} />"
35
+ end
36
+
37
+ def fix_type_attachment(k,v)
38
+ if v=='nil'
39
+ delete_files_for(k) unless new?
40
+ @doc[k] = nil
41
+ elsif v.is_a?(Hash)&&v.key?(:tempfile)
42
+ delete_files_for(k) unless new?
43
+ @temp_attachments ||= {}
44
+ @temp_attachments[k] = v
45
+ attachment_id = model.gridfs.put(v[:tempfile], {:filename=>v[:filename], :content_type=>v[:type]})
46
+ @doc[k] = {'original'=>attachment_id}
47
+ end
48
+ end
49
+
50
+ def delete_files_for(col)
51
+ obj = (@old_doc||@doc)[col]
52
+ if obj.respond_to?(:each)
53
+ obj.each do |k,v|
54
+ model.gridfs.delete(v)
55
+ end
56
+ end
57
+ end
58
+
59
+ def after_delete
60
+ super
61
+ model.schema.each do |k,v|
62
+ delete_files_for(k) if v[:type]==:attachment
63
+ end
64
+ end
65
+
66
+ def after_save
67
+ super
68
+ unless @temp_attachments.nil?
69
+ @temp_attachments.each do |k,v|
70
+ after_stash(k)
71
+ end
72
+ end
73
+ end
74
+
75
+ def after_stash(col); end
76
+
77
+ def convert(col, convert_steps, style)
78
+ return if @doc[col].nil?
79
+ if @temp_attachments.nil? || @temp_attachments[col].nil?
80
+ f = model.gridfs.get(@doc[col]['original'])
81
+ return unless f.content_type[/^image\//]
82
+ src = Tempfile.new('MongoStash_src')
83
+ src.binmode
84
+ src.write(f.read(4096)) until f.eof?
85
+ src.close
86
+ @temp_attachments ||= {}
87
+ @temp_attachments[col] ||= {}
88
+ @temp_attachments[col][:tempfile] = src
89
+ @temp_attachments[col][:type] = f.content_type
90
+ else
91
+ return unless @temp_attachments[col][:type][/^image\//]
92
+ src = @temp_attachments[col][:tempfile]
93
+ end
94
+ model.gridfs.delete(@doc[col][style]) unless @doc[col][style].nil?
95
+ ext = style[/[a-zA-Z]+$/].insert(0,'.')
96
+ content_type = Rack::Mime.mime_type(ext)
97
+ unless content_type[/^image\//]
98
+ ext = '.jpg'
99
+ content_type = 'image/jpeg'
100
+ end
101
+ dest = Tempfile.new(['MongoStash_dest', ext])
102
+ dest.binmode
103
+ dest.close
104
+ system "convert \"#{src.path}\" #{convert_steps} \"#{dest.path}\""
105
+ filename = "#{model.name}/#{self.id}/#{style}"
106
+ attachment_id = model.gridfs.put(dest.open, {:filename=>filename, :content_type=>content_type})
107
+ @doc[col] = @doc[col].update({style=>attachment_id})
108
+ model.collection.update({'_id'=>@doc['_id']}, @doc)
109
+ #src.close!
110
+ dest.close!
111
+ end
112
+
113
+ class << self
114
+ attr_accessor :classes
115
+ MongoStash.classes = []
116
+
117
+ def all_after_stash
118
+ MongoStash.classes.each do |m|
119
+ m.all_after_stash
120
+ end
121
+ end
122
+
123
+ def fix_dots_in_keys(c, for_real=false)
124
+ puts "\n#{c}" unless for_real
125
+ selection = c.schema.select{|k,v| v[:type]==:attachment }
126
+ img_keys = selection.respond_to?(:keys) ? selection.keys : selection.map{|a|a[0]}
127
+ c.find({}, {:fields=>img_keys}).each do |e|
128
+ old_hash = e.doc.select{|k,v| img_keys.include?(k) }
129
+ fixed_hash = Marshal.load(Marshal.dump(old_hash))
130
+ img_keys.each do |k|
131
+ (fixed_hash[k]||{}).keys.each do |style|
132
+ fixed_hash[k][style.tr('.','_')] = fixed_hash[k].delete(style)
133
+ end
134
+ end
135
+ next if old_hash==fixed_hash
136
+
137
+ if for_real
138
+ c.collection.update({'_id'=>e.id}, {'$set'=>fixed_hash})
139
+ else
140
+ puts old_hash.inspect
141
+ puts fixed_hash.inspect
142
+ end
143
+
144
+ end
145
+ end
146
+
147
+ def all_fix_dots_in_keys(for_real=false)
148
+ MongoStash.classes.each do |c|
149
+ MongoStash.fix_dots_in_keys(c, for_real)
150
+ end
151
+ end
152
+
153
+ end
154
+
155
+ end
@@ -0,0 +1,161 @@
1
+ Encoding.default_internal = Encoding.default_external = Encoding::UTF_8 if RUBY_VERSION >= '1.9.0'
2
+
3
+ require 'rubygems'
4
+ require 'bacon'
5
+ $:.unshift './lib'
6
+ require 'mongo_mutation'
7
+
8
+ MONGO = ::Mongo::MongoClient.new
9
+ class NoDB; include MongoMutation; end
10
+ DB = MONGO['test-mongo-mutation']
11
+
12
+ class Naked; include MongoMutation; end
13
+
14
+ class Person
15
+ include MongoMutation
16
+ slot "name" # no options
17
+ slot "surname"
18
+ slot "age", :type=>:integer, :default=>18
19
+ image_slot "portrait"
20
+ slot 'subscribed_on', :type=>:date, :default=>proc{Date.today}
21
+ end
22
+
23
+ class Address
24
+ include MongoMutation
25
+ slot "body"
26
+ end
27
+
28
+ class Article
29
+ include MongoMutation
30
+ slot 'title'
31
+ slot 'content', :type=>:text
32
+ image_slot
33
+ end
34
+
35
+ describe "MongoMutation" do
36
+
37
+ describe ".db" do
38
+ it "Should be set to DB constant by default" do
39
+ NoDB.db.should==nil
40
+ Naked.db.should==DB
41
+ end
42
+ end
43
+
44
+ shared "Empty BSON" do
45
+ it "Should be set to an empty BSON ordered hash by default" do
46
+ @bson.class.should==::BSON::OrderedHash
47
+ @bson.empty?.should==true
48
+ end
49
+ end
50
+
51
+ describe ".schema" do
52
+ before { @bson = Naked.schema }
53
+ behaves_like "Empty BSON"
54
+ end
55
+
56
+ describe ".relationships" do
57
+ before { @bson = Naked.relationships }
58
+ behaves_like "Empty BSON"
59
+ end
60
+
61
+ shared "Basic slot" do
62
+ it "Adds the declared slot to the schema" do
63
+ @klass.schema.key?(@key).should==true
64
+ end
65
+ it "Defines getters and setters" do
66
+ @klass.new.respond_to?(@key).should==true
67
+ @klass.new.respond_to?(@key+'=').should==true
68
+ end
69
+ end
70
+
71
+ describe ".slot" do
72
+ before { @klass = Person; @key = 'age' }
73
+ behaves_like "Basic slot"
74
+ it "Keeps the options in schema" do
75
+ Person.schema['age'][:default].should==18
76
+ Person.schema['age'][:type].should==:integer
77
+ end
78
+ it "Sets :type option to :string by default if not provided" do
79
+ Person.schema['name'][:type].should==:string
80
+ end
81
+ end
82
+
83
+ shared "Correctly typed" do
84
+ it "Has the correct type" do
85
+ @klass.schema[@key][:type].should==@type
86
+ end
87
+ end
88
+
89
+ shared "Image slot" do
90
+ describe "Reference slot" do
91
+ before { @type = :attachment }
92
+ behaves_like "Basic slot"
93
+ behaves_like "Correctly typed"
94
+ end
95
+ describe "Tooltip slot" do
96
+ before { @key = @key+'_tooltip'; @type = :string }
97
+ behaves_like "Basic slot"
98
+ behaves_like "Correctly typed"
99
+ end
100
+ describe "Alt text slot" do
101
+ before { @key = @key+'_alternative_text'; @type = :string }
102
+ behaves_like "Basic slot"
103
+ behaves_like "Correctly typed"
104
+ end
105
+ end
106
+
107
+ describe ".image_slot" do
108
+ describe "Name provided" do
109
+ before { @klass = Person; @key = 'portrait' }
110
+ behaves_like "Image slot"
111
+ end
112
+ describe "Name not provided" do
113
+ before { @klass = Article; @key = 'image' }
114
+ behaves_like "Image slot"
115
+ end
116
+ end
117
+
118
+ shared "Has error recipient" do
119
+ it "Has an empty recipent for errors" do
120
+ @inst.errors.should=={}
121
+ end
122
+ end
123
+
124
+ describe "#initialize" do
125
+ describe "With fields" do
126
+ before { @inst = Person.new({'age'=>42, 'name'=>'Bozo', 'car'=>'Jaguar'}) }
127
+ it "Should be flagged as new if the doc has no _id yet" do
128
+ @inst.new?.should==true
129
+ end
130
+ it "Should not be flagged as new if the doc has an _id" do
131
+ @inst['_id'] = 'abc'
132
+ @inst.new?.should==false
133
+ end
134
+ it "Should pass all keys to the doc" do
135
+ @inst['name'].should=='Bozo'
136
+ @inst['car'].should=='Jaguar'
137
+ end
138
+ behaves_like "Has error recipient"
139
+ end
140
+ describe "Fresh new one" do
141
+ before { @inst = Person.new }
142
+ it "Should be flagged as new" do
143
+ @inst.new?.should==true
144
+ end
145
+ it "Has default values correctly set" do
146
+ @inst['age'].should==18 # direct value
147
+ @inst['subscribed_on'].class.should==Date # with proc
148
+ end
149
+ behaves_like "Has error recipient"
150
+ end
151
+ end
152
+
153
+ describe "#model" do
154
+ it "Is a shortcut for self.class" do
155
+ Naked.new.model.should==Naked
156
+ end
157
+ end
158
+
159
+ end
160
+
161
+ MONGO.drop_database('test-mongo-mutation')
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: campbellhay-mongo
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.14
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mickael Riga
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mongo
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.8.2
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.8.2
30
+ - !ruby/object:Gem::Dependency
31
+ name: bacon
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: The CampbellHay Mongo toolbox
47
+ email: mig@campbellhay.com
48
+ executables: []
49
+ extensions: []
50
+ extra_rdoc_files: []
51
+ files:
52
+ - .gitignore
53
+ - Gemfile
54
+ - campbellhay-mongo.gemspec
55
+ - gempush
56
+ - lib/mongo_crushyform.rb
57
+ - lib/mongo_mutation.rb
58
+ - lib/mongo_stash.rb
59
+ - test/spec_mutation.rb
60
+ homepage: http://www.campbellhay.com
61
+ licenses: []
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - ./lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ! '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubyforge_project:
80
+ rubygems_version: 1.8.23
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: The CampbellHay Mongo toolbox
84
+ test_files: []