campbellhay-mongo 1.0.14

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.
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: []