mongoid_ext 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.document +5 -0
  2. data/Gemfile +20 -0
  3. data/Gemfile.lock +50 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +17 -0
  6. data/Rakefile +44 -0
  7. data/VERSION +1 -0
  8. data/bin/mongoid_console +85 -0
  9. data/lib/mongoid_ext.rb +71 -0
  10. data/lib/mongoid_ext/criteria_ext.rb +15 -0
  11. data/lib/mongoid_ext/document_ext.rb +29 -0
  12. data/lib/mongoid_ext/file.rb +86 -0
  13. data/lib/mongoid_ext/file_list.rb +74 -0
  14. data/lib/mongoid_ext/file_server.rb +69 -0
  15. data/lib/mongoid_ext/filter.rb +266 -0
  16. data/lib/mongoid_ext/filter/parser.rb +71 -0
  17. data/lib/mongoid_ext/filter/result_set.rb +75 -0
  18. data/lib/mongoid_ext/js/filter.js +41 -0
  19. data/lib/mongoid_ext/js/find_tags.js +26 -0
  20. data/lib/mongoid_ext/js/tag_cloud.js +28 -0
  21. data/lib/mongoid_ext/modifiers.rb +93 -0
  22. data/lib/mongoid_ext/mongo_mapper.rb +63 -0
  23. data/lib/mongoid_ext/paranoia.rb +100 -0
  24. data/lib/mongoid_ext/patches.rb +17 -0
  25. data/lib/mongoid_ext/random.rb +23 -0
  26. data/lib/mongoid_ext/slugizer.rb +84 -0
  27. data/lib/mongoid_ext/storage.rb +110 -0
  28. data/lib/mongoid_ext/tags.rb +26 -0
  29. data/lib/mongoid_ext/types/embedded_hash.rb +25 -0
  30. data/lib/mongoid_ext/types/open_struct.rb +15 -0
  31. data/lib/mongoid_ext/types/timestamp.rb +15 -0
  32. data/lib/mongoid_ext/types/translation.rb +51 -0
  33. data/lib/mongoid_ext/update.rb +11 -0
  34. data/lib/mongoid_ext/versioning.rb +189 -0
  35. data/lib/mongoid_ext/voteable.rb +104 -0
  36. data/mongoid_ext.gemspec +129 -0
  37. data/test/helper.rb +30 -0
  38. data/test/models.rb +80 -0
  39. data/test/support/custom_matchers.rb +55 -0
  40. data/test/test_filter.rb +51 -0
  41. data/test/test_modifiers.rb +65 -0
  42. data/test/test_paranoia.rb +40 -0
  43. data/test/test_random.rb +57 -0
  44. data/test/test_slugizer.rb +66 -0
  45. data/test/test_storage.rb +110 -0
  46. data/test/test_tags.rb +47 -0
  47. data/test/test_update.rb +16 -0
  48. data/test/test_versioning.rb +55 -0
  49. data/test/test_voteable.rb +77 -0
  50. data/test/types/test_open_struct.rb +22 -0
  51. data/test/types/test_set.rb +26 -0
  52. data/test/types/test_timestamp.rb +40 -0
  53. metadata +301 -0
@@ -0,0 +1,74 @@
1
+ module MongoidExt
2
+ class FileList < EmbeddedHash
3
+ attr_accessor :parent_document
4
+
5
+ def put(id, io, metadata = {})
6
+ if !parent_document.new_record?
7
+ filename = id
8
+ if io.respond_to?(:original_filename)
9
+ filename = io.original_filename
10
+ elsif io.respond_to?(:path) && io.path
11
+ filename = ::File.basename(io.path)
12
+ elsif io.kind_of?(String)
13
+ io = StringIO.new(io)
14
+ end
15
+
16
+ get(id).put(filename, io, metadata)
17
+ else
18
+ (@_pending_files ||= {})[id] = [io, metadata]
19
+ end
20
+ end
21
+
22
+ def files
23
+ ids = self.keys
24
+ ids.delete("_id")
25
+ ids.map {|v| get(v) }
26
+ end
27
+
28
+ def each_file(&block)
29
+ (self.keys-["_id"]).each do |key|
30
+ file = self.get(key)
31
+ yield key, file
32
+ end
33
+ end
34
+
35
+ def get(id)
36
+ if id.kind_of?(MongoidExt::File)
37
+ self[id.id] = id
38
+ return id
39
+ end
40
+
41
+ id = id.to_s.gsub(".", "_")
42
+ file = self[id]
43
+ if file.nil?
44
+ file = self[id] = MongoidExt::File.new
45
+ elsif file.class == BSON::OrderedHash
46
+ file = self[id] = MongoidExt::File.new(file)
47
+ end
48
+
49
+ file._root_document = parent_document
50
+ file
51
+ end
52
+
53
+ def sync_files
54
+ if @_pending_files
55
+ @_pending_files.each do |filename, data|
56
+ put(filename, data[0], data[1])
57
+ end
58
+ @_pending_files = nil
59
+ end
60
+ end
61
+
62
+ def delete(id)
63
+ file = self.get(id)
64
+ super(id)
65
+ file.delete
66
+ end
67
+
68
+ def destroy_files
69
+ each_file do |id, file|
70
+ get(id).delete
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,69 @@
1
+ require 'time'
2
+ require 'rack/utils'
3
+
4
+ module MongoidExt
5
+ class FileServer
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ if env["PATH_INFO"] =~ /^\/_files\/([^\/?]+)/
12
+ @model = $1.classify.constantize rescue nil
13
+ return forbidden if @model.nil?
14
+
15
+ dup._call(env)
16
+ else
17
+ @app.call(env)
18
+ end
19
+ end
20
+
21
+ def _call(env)
22
+ request = Rack::Request.new(env)
23
+ params = request.GET
24
+
25
+ @file = @model.find_file_from_params(params, request)
26
+ return not_found if @file.nil?
27
+
28
+ if @file.present?
29
+ serving
30
+ else
31
+ not_found
32
+ end
33
+ end
34
+
35
+ def forbidden
36
+ body = "Forbidden\n"
37
+ [403, {"Content-Type" => "text/plain",
38
+ "Content-Length" => body.size.to_s,
39
+ "X-Cascade" => "pass"},
40
+ [body]]
41
+ end
42
+
43
+ def serving
44
+ body = self
45
+ [200, {
46
+ "Last-Modified" => Time.now.httpdate,
47
+ "Content-Type" => @file.content_type,
48
+ "Content-Length" => @file.size.to_s
49
+ }, body]
50
+ end
51
+
52
+ def not_found
53
+ body = "File not found: #{@path_info}\n"
54
+ [404, {"Content-Type" => "text/plain",
55
+ "Content-Length" => body.size.to_s,
56
+ "X-Cascade" => "pass"},
57
+ [body]]
58
+ end
59
+
60
+ def each
61
+ f = @file.get
62
+ while part = f.read(8192)
63
+ yield part
64
+ break if part.empty?
65
+ end
66
+ end
67
+ end
68
+ end
69
+
@@ -0,0 +1,266 @@
1
+ module MongoidExt
2
+ module Filter
3
+ def self.included(klass)
4
+ begin
5
+ require 'lingua/stemmer'
6
+ rescue LoadError
7
+ $stderr.puts "install ruby-stemmer `gem install ruby-stemmer` to activate the full text search support"
8
+ end
9
+
10
+ klass.class_eval do
11
+ extend ClassMethods
12
+
13
+ field :_keywords, :type => Set, :default => Set.new
14
+ index :_keywords
15
+
16
+ before_save :_update_keywords
17
+
18
+ attr_accessor :search_score
19
+ end
20
+ end
21
+
22
+ module ClassMethods
23
+ def filterable_keys(*keys)
24
+ @filterable_keys ||= Set.new
25
+ @filterable_keys += keys
26
+
27
+ @filterable_keys
28
+ end
29
+
30
+ def language(lang = 'en')
31
+ @language ||= lang
32
+ end
33
+
34
+ def filter_conditions(query, opts = {})
35
+ stemmer = nil
36
+ language = opts.delete(:language) || 'en'
37
+
38
+ if defined?(Lingua::Stemmer)
39
+ stemmer = MongoidExt::Filter.build_stemmer(language)
40
+ end
41
+
42
+ @parser = MongoidExt::Filter::Parser.new(stemmer)
43
+ parsed_query = @parser.parse(query)
44
+
45
+ [parsed_query, _build_filter_query(parsed_query)]
46
+ end
47
+
48
+ def filter(query, opts = {})
49
+ min_score = opts.delete(:min_score) || 0.0
50
+ limit = opts.delete(:per_page) || 25
51
+ page = opts.delete(:page) || 1
52
+ select = opts.delete(:select) || self.fields.keys
53
+
54
+ parsed_query, conds = self.filter_conditions(query, opts)
55
+ query = Mongoid::Criteria.new(self)
56
+ conds = query.where(opts).where(conds).selector
57
+
58
+ results = self.db.nolock_eval("function(collection, q, config) { return filter(collection, q, config); }", self.collection_name, conds, {:words => parsed_query[:words].to_a, :stemmed => parsed_query[:stemmed].to_a, :limit => limit, :min_score => min_score, :select => select })
59
+
60
+ pagination = MongoidExt::Filter::ResultSet.new(results["total_entries"], parsed_query, conds, page, limit)
61
+
62
+ pagination.subject = results['results'].map do |result|
63
+ item = self.new(result['doc'])
64
+ item.search_score = result['score']
65
+
66
+ item
67
+ end
68
+
69
+ pagination
70
+ end
71
+
72
+ def _build_filter_query(parsed_query)
73
+ mongoquery = {}
74
+
75
+ if !parsed_query[:tokens].empty?
76
+ keywords = []
77
+
78
+ parsed_query[:tokens].each do |t|
79
+ keywords << /^#{Regexp.escape(t)}/i if t.size > 2
80
+ end
81
+
82
+ mongoquery[:_keywords] = { :$in => keywords }
83
+ end
84
+
85
+ mongoquery.merge!(map_filter_operators(parsed_query[:quotes], parsed_query[:operators]))
86
+
87
+ mongoquery
88
+ end
89
+
90
+ def map_filter_operators(quotes, ops)
91
+ mongoquery = {}
92
+
93
+ if ops["is"]
94
+ ops["is"].each do |d|
95
+ mongoquery[d] = true
96
+ end
97
+ end
98
+
99
+ if ops["not"]
100
+ ops["not"].each do |d|
101
+ mongoquery[d] = false
102
+ end
103
+ end
104
+
105
+ mongoquery
106
+ end
107
+ end
108
+
109
+ protected
110
+ def _update_keywords
111
+ lang = self.class.language
112
+ if lang.kind_of?(Symbol)
113
+ lang = send(lang)
114
+ elsif lang.kind_of?(Proc)
115
+ lang = lang.call(self)
116
+ end
117
+
118
+ stemmer = nil
119
+ if defined?(Lingua)
120
+ stemmer = MongoidExt::Filter.build_stemmer(lang)
121
+ end
122
+
123
+ stop_words = []
124
+ if self.respond_to?("#{lang}_stop_words")
125
+ stop_words = Set.new(self.send("#{lang}_stop_words"))
126
+ end
127
+
128
+ self._keywords = Set.new
129
+ self.class.filterable_keys.each do |key|
130
+ self._keywords += keywords_for_value(read_attribute(key), stemmer, stop_words)
131
+ end
132
+ end
133
+
134
+ private
135
+ def keywords_for_value(val, stemmer=nil, stop_words = [])
136
+ if val.kind_of?(String)
137
+ words = []
138
+ val.downcase.split.each do |word|
139
+ next if word.length < 3
140
+ next if stop_words.include?(word)
141
+
142
+ deword = word.split(%r{'|"|\/|\\|\?|\+|_|\-|\>|\>})
143
+
144
+ deword.each do |word|
145
+ next if word.empty?
146
+
147
+ stem = word
148
+ if stemmer
149
+ stem = stemmer.stem(word)
150
+ end
151
+ normalize_string = lambda {|str| ActiveSupport::Multibyte::Chars.new(str).normalize(:kd).gsub(/[^\x00-\x7F]/n, '').to_s }
152
+
153
+ normalized = normalize_string.call(word)
154
+
155
+ if word != normalized
156
+ words << normalized
157
+ end
158
+
159
+ words << word
160
+ if stem
161
+ normalized=normalize_string.call(stem)
162
+
163
+ words << normalized if stem != normalized
164
+ words << stem if stem != word
165
+ end
166
+ end
167
+ end
168
+
169
+ words
170
+ elsif val.kind_of?(Array)
171
+ val.map { |e| keywords_for_value(e, stemmer, stop_words) }.flatten
172
+ elsif val.kind_of?(Hash)
173
+ val.map { |k, v| keywords_for_value(v, stemmer, stop_words) }.flatten
174
+ elsif val
175
+ [val]
176
+ else
177
+ []
178
+ end
179
+ end
180
+
181
+ def self.build_stemmer(language)
182
+ begin
183
+ return Lingua::Stemmer.new(:language => language)
184
+ rescue Lingua::StemmerError
185
+ return nil
186
+ end
187
+ end
188
+
189
+ def en_stop_words
190
+ ["a", "about", "above", "after", "again", "against", "all", "am", "an",
191
+ "and", "any", "are", "aren't", "as", "at", "be", "because", "been",
192
+ "before", "being", "below", "between", "both", "but", "by", "cannot",
193
+ "can't", "could", "couldn't", "did", "didn't", "do", "does", "doesn't",
194
+ "doing", "don't", "down", "during", "each", "few", "for", "from",
195
+ "further", "had", "hadn't", "has", "hasn't", "have", "haven't", "having",
196
+ "he", "he'd", "he'll", "her", "here", "here's", "hers", "herself", "he's",
197
+ "him", "himself", "his", "how", "how's", "i", "i'd", "if", "i'll", "i'm",
198
+ "in", "into", "is", "isn't", "it", "its", "it's", "itself", "i've", "let's",
199
+ "me", "more", "most", "mustn't", "my", "myself", "no", "nor", "not", "of",
200
+ "off", "on", "once", "only", "or", "other", "ought", "our", "ours", "ourselves",
201
+ "out", "over", "own", "same", "shan't", "she", "she'd", "she'll", "she's",
202
+ "should", "shouldn't", "so", "some", "such", "than", "that", "that's", "the",
203
+ "their", "theirs", "them", "themselves", "then", "there", "there's", "these",
204
+ "they", "they'd", "they'll", "they're", "they've", "this", "those", "through",
205
+ "to", "too", "under", "until", "up", "very", "was", "wasn't", "we", "we'd",
206
+ "we'll", "were", "we're", "weren't", "we've", "what", "what's", "when",
207
+ "when's", "where", "where's", "which", "while", "who", "whom", "who's",
208
+ "why", "why's", "with", "won't", "would", "wouldn't", "you", "you'd",
209
+ "you'll", "your", "you're", "yours", "yourself", "yourselves", "you've",
210
+ "the", "how"]
211
+ end
212
+
213
+ def es_stop_words
214
+ ["de", "la", "que", "el", "en", "y", "a",
215
+ "los", "del", "se", "las", "por", "un", "para", "con", "no", "una", "su",
216
+ "al", "lo", "como", "mas", "pero", "sus", "le", "ya", "o", "este", "sus",
217
+ "si", "porque", "esta", "entre",
218
+ "cuando", "muy", "sin", "sobre", "tambien", "me", "hasta", "hay",
219
+ "donde", "quien", "desde", "todo", "nos", "durante", "todos", "uno", "les",
220
+ "ni", "contra", "otros", "ese", "eso", "ante", "ellos", "e", "esto",
221
+ "mi", "antes", "algunos", "que", "unos", "yo", "otro",
222
+ "otras", "otra", "al", "tanto", "esa", "estos", "mucho", "quienes",
223
+ "nada", "muchos", "cual", "poco", "ella", "estar", "estas", "algunas",
224
+ "algo", "nosotros", "mi", "mis", "tus", "ellas", "sus", "una", "uno",
225
+ "nosotras", "vosostros", "vosostras", "os", "mio", "mia",
226
+ "mios", "mias", "tuyo", "tuya", "tuyos", "tuyas", "suyo",
227
+ "suya", "suyos", "suyas", "nuestro", "nuestra", "nuestros", "nuestras",
228
+ "vuestro", "vuestra", "vuestros", "vuestras", "esos", "esas", "estoy",
229
+ "estas", "esta", "estamos", "estais", "estan",
230
+ "este", "estes", "estemos", "esteis", "esten",
231
+ "estare", "estareis", "estara", "estaremos", "como",
232
+ "estareis", "estarin", "estaria", "estarias",
233
+ "estariamos", "estariais", "estarian", "estaba",
234
+ "estabas", "estabamos", "estabais", "estaban", "estuve",
235
+ "estuviste", "estuvo", "estuvimos", "estuvisteis", "estuvieron", "estuviera",
236
+ "estuvieras", "estuvieramos", "estuvierais", "estuvieran", "estuviese",
237
+ "estuvieses", "estuviesemos", "estuvieseis", "estuviesen", "estando",
238
+ "estado", "estada", "estados", "estadas", "estad", "he", "has", "ha", "hemos",
239
+ "habeis", "han", "haya", "hayas", "hayamos", "hayais", "hayan",
240
+ "habre", "habras", "habra", "habremos", "habreis",
241
+ "habran", "habria", "habrias", "habriamos",
242
+ "habriais", "habrian", "habia", "habias",
243
+ "habiamos", "habiais", "habian", "hube", "hubiste",
244
+ "hubo", "hubimos", "hubisteis", "hubieron", "hubiera", "hubieras", "hubieramos",
245
+ "hubierais", "hubieran", "hubiese", "hubieses", "hubiesemos", "hubieseis",
246
+ "hubiesen", "habiendo", "habido", "habida", "habidos", "habidas", "soy", "eres",
247
+ "es", "somos", "sois", "son", "sea", "seas", "seamos", "seais",
248
+ "sean", "sere", "seras", "sera", "seremos", "sereis",
249
+ "seran", "seria", "serias", "seriamos", "seriais",
250
+ "serian", "era", "eras", "eramos", "erais", "eran", "fui",
251
+ "fuiste", "fue", "fuimos", "fuisteis", "fueron", "fuera", "fueras",
252
+ "fueramos", "fuerais", "fueran", "fuese", "fueses", "fuesemos",
253
+ "fueseis", "fuesen", "sintiendo", "sentido", "sentida", "sentidos", "sentidas",
254
+ "siente", "sentid", "tengo", "tienes", "tiene", "tenemos", "teneis",
255
+ "tienen", "tenga", "tengas", "tengamos", "tengais", "tengan", "tendre",
256
+ "tendras", "tendra", "tendremos", "tendreis", "tendran",
257
+ "tendria", "tendrias", "tendriamos", "tendriais",
258
+ "tendrian", "tenia", "tenias", "teniamos",
259
+ "teniais", "tenian", "tuve", "tuviste", "tuvo", "tuvimos",
260
+ "tuvisteis", "tuvieron", "tuviera", "tuvieras", "tuvieramos",
261
+ "tuvierais", "tuvieran", "tuviese", "tuvieses", "tuviesemos",
262
+ "tuvieseis", "tuviesen", "teniendo", "tenido", "tenida", "tenidos", "tenidas", "tened"]
263
+ end
264
+ end
265
+ end
266
+
@@ -0,0 +1,71 @@
1
+ module MongoidExt
2
+ module Filter
3
+ class Parser
4
+ def initialize(stemmer)
5
+ @stemmer = stemmer
6
+ end
7
+
8
+ def parse(query)
9
+ query, quotes = parse_quoted_text(query)
10
+ query, ops = parse_operators(query)
11
+ query.gsub!(/\s+/, ' ')
12
+ query.gsub!(/\?|\!|\:|\)|\(/, "")
13
+
14
+ words = Set.new(query.downcase.split(/\s+/))
15
+
16
+ stemmed = stem(words)
17
+ tokens = words + stemmed
18
+
19
+ {:query => query,
20
+ :words => words,
21
+ :stemmed => stemmed,
22
+ :operators => ops,
23
+ :tokens => tokens,
24
+ :quotes => quotes}
25
+ end
26
+
27
+ private
28
+ def parse_quoted_text(query)
29
+ quotes = []
30
+ loop do
31
+ m = query.match(/"([^"]+"|-)/)
32
+ if m
33
+ query = $` + $'
34
+ quotes << m[1].gsub('"', "")
35
+ else
36
+ break
37
+ end
38
+ end
39
+
40
+ [query, quotes]
41
+ end
42
+
43
+ def parse_operators(query)
44
+ ops = {}
45
+
46
+ loop do
47
+ m = query.match(/(is|lang|not|by|score):(\S+)/)
48
+ if m
49
+ query = $` + $'
50
+ (ops[m[1]] ||= []) << m[2]
51
+ else
52
+ break
53
+ end
54
+ end
55
+ [query, ops]
56
+ end
57
+
58
+ def stem(words)
59
+ tokens = []
60
+ if @stemmer
61
+ words.each do |word|
62
+ stem = @stemmer.stem(word)
63
+ tokens << stem if word.size > 2 && !words.include?(stem)
64
+ end
65
+ end
66
+
67
+ tokens
68
+ end
69
+ end
70
+ end
71
+ end