naranya_ecm-sdk 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/Gemfile +8 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +33 -0
  6. data/Rakefile +1 -0
  7. data/dev/create_cvm.rb +31 -0
  8. data/dev/init.rb +2 -0
  9. data/dev/load_cvm.rb +12 -0
  10. data/dev/search.rb +3 -0
  11. data/lib/naranya_ecm/behaviors/localizable.rb +41 -0
  12. data/lib/naranya_ecm/behaviors/timestampable.rb +22 -0
  13. data/lib/naranya_ecm/behaviors.rb +9 -0
  14. data/lib/naranya_ecm/cache/key.rb +73 -0
  15. data/lib/naranya_ecm/cache/methods.rb +78 -0
  16. data/lib/naranya_ecm/cache.rb +9 -0
  17. data/lib/naranya_ecm/has_many_patch.rb +105 -0
  18. data/lib/naranya_ecm/lifecycles/content_lifecycle.rb +36 -0
  19. data/lib/naranya_ecm/lifecycles/lifecycleable.rb +43 -0
  20. data/lib/naranya_ecm/lifecycles/version_lifecycle.rb +75 -0
  21. data/lib/naranya_ecm/lifecycles.rb +10 -0
  22. data/lib/naranya_ecm/models/category.rb +42 -0
  23. data/lib/naranya_ecm/models/content.rb +102 -0
  24. data/lib/naranya_ecm/models/content_version.rb +38 -0
  25. data/lib/naranya_ecm/models/download_authorization.rb +55 -0
  26. data/lib/naranya_ecm/models/lifecycle.rb +36 -0
  27. data/lib/naranya_ecm/models/media_resource.rb +33 -0
  28. data/lib/naranya_ecm/models.rb +13 -0
  29. data/lib/naranya_ecm/search/hit.rb +17 -0
  30. data/lib/naranya_ecm/search/methods.rb +26 -0
  31. data/lib/naranya_ecm/search/query.rb +296 -0
  32. data/lib/naranya_ecm/search/results.rb +161 -0
  33. data/lib/naranya_ecm/search.rb +14 -0
  34. data/lib/naranya_ecm/site_from_configuration.rb +15 -0
  35. data/lib/naranya_ecm-sdk/version.rb +3 -0
  36. data/lib/naranya_ecm-sdk.rb +84 -0
  37. data/naranya_ecm-sdk.gemspec +33 -0
  38. data/spec/models/category_spec.rb +11 -0
  39. data/spec/models/content_spec.rb +11 -0
  40. data/spec/models/content_version_spec.rb +7 -0
  41. data/spec/models/download_authorization.rb +7 -0
  42. data/spec/models/media_spec.rb +7 -0
  43. data/spec/models/module_spec.rb +18 -0
  44. data/spec/spec_helper.rb +47 -0
  45. data/spec/support/naranya_ecms_shared_specs.rb +33 -0
  46. metadata +236 -0
@@ -0,0 +1,42 @@
1
+ require 'active_resource'
2
+
3
+ module NaranyaEcm
4
+ module Models
5
+ class Category < ::ActiveResource::Base
6
+
7
+ include SiteFromConfiguration
8
+
9
+ include NaranyaEcm::Behaviors::Timestampable
10
+
11
+ include NaranyaEcm::Search::Methods
12
+ include NaranyaEcm::Cache::Methods
13
+
14
+ class NameTranslations < ::ActiveResource::Base
15
+ include NaranyaEcm::Behaviors::Localizable
16
+ end
17
+
18
+ self.include_root_in_json = true
19
+
20
+ # Definir los atributos conocidos:
21
+ schema do
22
+ # define each attribute separately
23
+ string :id, :code, :created_at, :updated_at
24
+
25
+ # unsupported types should be left as strings
26
+ # overload the accessor methods if you need to convert them
27
+ #attribute 'created_at', 'string'
28
+ attribute 'name_translations', 'string'
29
+ attribute 'description_translations', 'string'
30
+
31
+ end
32
+
33
+ delegate :name, to: :name_translations
34
+ delegate :description, to: :title_translations
35
+
36
+ def contents
37
+ Content.find :all, params: { category_id: self.id }
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,102 @@
1
+ require 'active_resource'
2
+
3
+ module NaranyaEcm
4
+ module Models
5
+ class Content < ::ActiveResource::Base
6
+
7
+ include ActiveModel::Dirty
8
+
9
+ include SiteFromConfiguration
10
+
11
+ include NaranyaEcm::Search::Methods
12
+ include NaranyaEcm::Cache::Methods
13
+
14
+ #include HasManyPatch
15
+
16
+ class TitleTranslations < ::ActiveResource::Base
17
+ include NaranyaEcm::Behaviors::Localizable
18
+ end
19
+
20
+ class DescriptionTranslations < ::ActiveResource::Base
21
+ include NaranyaEcm::Behaviors::Localizable
22
+ end
23
+
24
+ class ContentRating < HashWithIndifferentAccess
25
+ def initialize(*args)
26
+ super()
27
+ given_attrs = args.shift
28
+ given_attrs.each { |k,v| self[k] = v } unless given_attrs.nil?
29
+ end
30
+ end
31
+
32
+ self.include_root_in_json = true
33
+
34
+ has_many :versions, class_name: "naranya_ecm/models/content_version"
35
+
36
+ has_one :current_version, class_name: "naranya_ecm/models/content_version"
37
+ belongs_to :category, class_name: "naranya_ecm/models/category"
38
+
39
+ # Definir los atributos conocidos:
40
+ schema do
41
+ # define each attribute separately
42
+ string :id, :type, :lifecycle_name, :lifecycle_state, :content_rating, :created_at, :updated_at
43
+
44
+ string :author
45
+ string :main_url
46
+
47
+ # unsupported types should be left as strings
48
+ # overload the accessor methods if you need to convert them
49
+ #attribute 'created_at', 'string'
50
+ attribute 'title_translations', 'string'
51
+ attribute 'description_translations', 'string'
52
+ attribute 'keywords', 'string'
53
+
54
+ attribute 'media_resources', 'string'
55
+ attribute 'current_version', 'string'
56
+
57
+ # Extra attributes:
58
+ NaranyaEcm.options['extra_attributes']['content'].each do |key, val|
59
+ attribute key, val
60
+ end
61
+
62
+ end
63
+
64
+ schema_attrs = self.schema.keys.map(&:to_sym)
65
+ define_attribute_methods(*schema_attrs)
66
+ schema_attrs.each do |attr_sym|
67
+ # getter:
68
+ define_method(attr_sym) { @attributes[attr_sym] }
69
+ # setter:
70
+ define_method("#{attr_sym}=".to_sym) do |value|
71
+ self.send "#{attr_sym}_will_change!".to_sym unless value == @attributes[attr_sym]
72
+ @attributes[attr_sym] = value
73
+ end
74
+ end
75
+
76
+ delegate :title, to: :title_translations
77
+ delegate :description, to: :description_translations
78
+
79
+ include NaranyaEcm::Behaviors::Timestampable
80
+
81
+ # include NaranyaEcm::Lifecycles::Lifecycleable
82
+ # ##################################################################
83
+ # # StateMachine:
84
+ # state_machine :lifecycle_state, initial: :draft do
85
+
86
+ # state :draft
87
+
88
+ # state :awaiting_validation
89
+
90
+ # state :rejected
91
+
92
+ # state :validated
93
+
94
+ # state :published
95
+
96
+ # state :deactivated
97
+
98
+ # end
99
+
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,38 @@
1
+ require 'active_resource'
2
+
3
+ module NaranyaEcm
4
+ module Models
5
+ class ContentVersion < ::ActiveResource::Base
6
+
7
+ include SiteFromConfiguration
8
+ include NaranyaEcm::Behaviors::Timestampable
9
+
10
+ self.include_root_in_json = true
11
+
12
+ # Definir los atributos conocidos:
13
+ schema do
14
+ # define each attribute separately
15
+ string :id, :name, :lifecycle_name, :lifecycle_state
16
+
17
+ integer :code
18
+
19
+ end
20
+
21
+ class DescriptionTranslations < ::ActiveResource::Base
22
+ include NaranyaEcm::Behaviors::Localizable
23
+ end
24
+
25
+ delegate :description, to: :description_translations
26
+
27
+ belongs_to :content,
28
+ class_name: "naranya_ecm/models/content"
29
+
30
+ has_many :media_resources,
31
+ class_name: "naranya_ecm/models/media_resource"
32
+
33
+ #include NaranyaEcm::Lifecycles::Lifecycleable
34
+ # include NaranyaEcm::Lifecycles::VersionLifecycle
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,55 @@
1
+ require 'active_resource'
2
+
3
+ module NaranyaEcm
4
+ module Models
5
+ class DownloadAuthorization < ActiveResource::Base
6
+
7
+ class Tokens < ::ActiveResource::Base
8
+ # Delegar el acceso tipo "Enumerable":
9
+ delegate :any?, to: :@attributes
10
+
11
+ # Re-establece el acceso tipo Hash:
12
+ delegate :[], to: :@attributes
13
+ delegate :[]=, to: :@attributes
14
+ self.site = "http://www.example.com"
15
+
16
+ def to_query_params
17
+ @attributes.map { |key, value| "#{key}=#{value}" }.join '&'
18
+ end
19
+ end
20
+
21
+ include SiteFromConfiguration
22
+ include NaranyaEcm::Behaviors::Timestampable
23
+
24
+ self.include_root_in_json = true
25
+
26
+ # Definir los atributos conocidos:
27
+ schema do
28
+ string :id, :media_resource_id, :expires_at, :tokens, :created_at, :updated_at
29
+ end
30
+
31
+ belongs_to :media_resource, class_name: "naranya_ecm/models/media_resource"
32
+
33
+ def media_resource=(given_media_resource)
34
+ @attributes[:media_resource_id] = given_media_resource.id
35
+ end
36
+
37
+ def initialize(attributes = {}, persisted = false)
38
+ if given_media_resource = attributes.delete(:media_resource)
39
+ attributes[:media_resource_id] = given_media_resource.id
40
+ end
41
+ super
42
+ end
43
+
44
+ def authorized_url
45
+ unless @authorized_url
46
+ uri = URI(self.media_resource.downloadable_url)
47
+ uri.query = self.tokens.to_query_params
48
+ @authorized_url = uri.to_s
49
+ end
50
+ @authorized_url
51
+ end
52
+
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,36 @@
1
+
2
+ module NaranyaEcm
3
+ module Models
4
+
5
+ # Value Object de Lifecycle de documentos / versiones / media / etc.
6
+ class Lifecycle < String
7
+ include Comparable
8
+
9
+ KNOWN_LIFECYCLES = %w(simple validatable)
10
+ .map { |m| m.to_sym }
11
+ .freeze
12
+
13
+ def is_lifecycle?(given_lifecycle)
14
+ given_lifecycle.to_s == self
15
+ end
16
+
17
+ KNOWN_LIFECYCLES.each do |lifecycle_name|
18
+ define_method("is_#{lifecycle_name}?") do
19
+ is_lifecycle?(lifecycle_name)
20
+ end
21
+ end
22
+
23
+ def method_missing(method_name, *args, &block)
24
+ if method_name.to_s =~ /^is_(\w+)\?$/
25
+ is_lifecycle?($1)
26
+ else
27
+ super # You *must* call super if you don't handle the
28
+ # method, otherwise you'll mess up Ruby's method
29
+ # lookup.
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ require 'active_resource'
2
+
3
+ module NaranyaEcm
4
+ module Models
5
+ class MediaResource < ActiveResource::Base
6
+
7
+ include SiteFromConfiguration
8
+ include NaranyaEcm::Behaviors::Timestampable
9
+
10
+ self.include_root_in_json = true
11
+
12
+ # Definir los atributos conocidos:
13
+ schema do
14
+ # define each attribute separately
15
+ string :id, :access_type, :type, :role, :downloadable_url
16
+
17
+ string :author
18
+ string :main_url
19
+
20
+ # unsupported types should be left as strings
21
+ # overload the accessor methods if you need to convert them
22
+ #attribute 'created_at', 'string'
23
+ attribute 'created_at', 'string'
24
+ attribute 'updated_at', 'string'
25
+ attribute 'compatible_device_ids', 'string'
26
+ attribute 'compatible_device_oses', 'string'
27
+ attribute 'compatible_device_os_versions', 'string'
28
+
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,13 @@
1
+ require 'active_support/dependencies/autoload'
2
+
3
+ module NaranyaEcm
4
+ module Models
5
+ extend ActiveSupport::Autoload
6
+ autoload :Category
7
+ autoload :Content
8
+ autoload :ContentVersion
9
+ autoload :Lifecycle
10
+ autoload :DownloadAuthorization
11
+ autoload :MediaResource
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ module NaranyaEcm
2
+ module Search
3
+ class Hit
4
+ attr_reader :id, :type, :updated_at, :order, :score
5
+ attr_accessor :cache_key
6
+ def initialize(attributes={})
7
+ @id = attributes.delete :_id
8
+ @type = attributes.delete :_type
9
+ @score = attributes.delete :_score
10
+
11
+ source = attributes.delete :_source
12
+ @updated_at = source.has_key?(:updated_at) ? source.delete(:updated_at).to_datetime : nil
13
+ @order = attributes.delete :order
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ require 'active_support/concern'
2
+ module NaranyaEcm
3
+ module Search
4
+ module Methods
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ attr_reader :search_score, :search_order
9
+ end
10
+
11
+ def merge_search_hit_data(hit)
12
+ @search_score, @search_order = hit.score, hit.order
13
+ end
14
+
15
+ module ClassMethods
16
+ def search(*args)
17
+ Results.new(*args, self)
18
+ end
19
+ def kick_search(terms, options={})
20
+ search(Query.build_searchkick_like(terms, options))
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,296 @@
1
+ module NaranyaEcm
2
+ module Search
3
+ class Query < Hash
4
+ class << self
5
+ def build_searchkick_like(term, options = {})
6
+ if term.is_a?(Hash)
7
+ options = term
8
+ term = nil
9
+ else
10
+ term = term.to_s
11
+ end
12
+
13
+ fields =
14
+ if options[:fields]
15
+ if options[:autocomplete]
16
+ options[:fields].map{|f| "#{f}.autocomplete" }
17
+ else
18
+ options[:fields].map do |value|
19
+ k, v = value.is_a?(Hash) ? value.to_a.first : [value, :word]
20
+ "#{k}.#{v == :word ? "analyzed" : v}"
21
+ end
22
+ end
23
+ else
24
+ ["_all"]
25
+ end
26
+
27
+ operator = options[:partial] ? "or" : "and"
28
+
29
+ # model and eagar loading
30
+ load = options[:load].nil? ? true : options[:load]
31
+ load = (options[:include] ? {include: options[:include]} : true) if load
32
+
33
+ # pagination
34
+ page = [options[:page].to_i, 1].max
35
+ per_page = (options[:limit] || options[:per_page] || 10).to_i
36
+ offset = options[:offset] || (page - 1) * per_page
37
+
38
+ all = term == "*"
39
+
40
+ if options[:query]
41
+ payload = options[:query]
42
+ elsif options[:similar]
43
+ payload = {
44
+ more_like_this: {
45
+ fields: fields,
46
+ like_text: term,
47
+ min_doc_freq: 1,
48
+ min_term_freq: 1,
49
+ analyzer: "searchkick_search2"
50
+ }
51
+ }
52
+ elsif all
53
+ payload = {
54
+ match_all: {}
55
+ }
56
+ else
57
+ if options[:autocomplete]
58
+ payload = {
59
+ multi_match: {
60
+ fields: fields,
61
+ query: term,
62
+ analyzer: "searchkick_autocomplete_search"
63
+ }
64
+ }
65
+ else
66
+ queries = []
67
+ fields.each do |field|
68
+ if field == "_all" or field.end_with?(".analyzed")
69
+ shared_options = {
70
+ fields: [field],
71
+ query: term,
72
+ use_dis_max: false,
73
+ operator: operator,
74
+ cutoff_frequency: 0.001
75
+ }
76
+ queries.concat [
77
+ {multi_match: shared_options.merge(boost: 10, analyzer: "searchkick_search")},
78
+ {multi_match: shared_options.merge(boost: 10, analyzer: "searchkick_search2")}
79
+ ]
80
+ if options[:misspellings] != false
81
+ distance = (options[:misspellings].is_a?(Hash) && options[:misspellings][:distance]) || 1
82
+ queries.concat [
83
+ {multi_match: shared_options.merge(fuzziness: distance, max_expansions: 3, analyzer: "searchkick_search")},
84
+ {multi_match: shared_options.merge(fuzziness: distance, max_expansions: 3, analyzer: "searchkick_search2")}
85
+ ]
86
+ end
87
+ else
88
+ queries << {
89
+ multi_match: {
90
+ fields: [field],
91
+ query: term,
92
+ analyzer: "searchkick_autocomplete_search"
93
+ }
94
+ }
95
+ end
96
+ end
97
+
98
+ payload = {
99
+ dis_max: {
100
+ queries: queries
101
+ }
102
+ }
103
+ end
104
+
105
+ end
106
+
107
+ custom_filters = []
108
+
109
+ if options[:boost]
110
+ custom_filters << {
111
+ filter: {
112
+ exists: {
113
+ field: options[:boost]
114
+ }
115
+ },
116
+ script: "log(doc['#{options[:boost]}'].value + 2.718281828)"
117
+ }
118
+ end
119
+
120
+ if custom_filters.any?
121
+ payload = {
122
+ custom_filters_score: {
123
+ query: payload,
124
+ filters: custom_filters,
125
+ score_mode: "total"
126
+ }
127
+ }
128
+ end
129
+
130
+ payload = {
131
+ query: payload,
132
+ size: per_page,
133
+ from: offset
134
+ }
135
+ payload[:explain] = options[:explain] if options[:explain]
136
+
137
+ # order
138
+ if options[:order]
139
+ order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc}
140
+ payload[:sort] = Hash[ order.map{|k, v| [k.to_s == "id" ? :_id : k, v] } ]
141
+ end
142
+
143
+ term_filters =
144
+ proc do |field, value|
145
+ if value.is_a?(Array) # in query
146
+ if value.any?
147
+ {or: value.map{|v| term_filters.call(field, v) }}
148
+ else
149
+ {terms: {field => value}} # match nothing
150
+ end
151
+ elsif value.nil?
152
+ {missing: {"field" => field, existence: true, null_value: true}}
153
+ else
154
+ {term: {field => value}}
155
+ end
156
+ end
157
+
158
+ # where
159
+ where_filters =
160
+ proc do |where|
161
+ filters = []
162
+ (where || {}).each do |field, value|
163
+ field = :_id if field.to_s == "id"
164
+
165
+ if field == :or
166
+ value.each do |or_clause|
167
+ filters << {or: or_clause.map{|or_statement| {and: where_filters.call(or_statement)} }}
168
+ end
169
+ else
170
+ # expand ranges
171
+ if value.is_a?(Range)
172
+ value = {gte: value.first, (value.exclude_end? ? :lt : :lte) => value.last}
173
+ end
174
+
175
+ if value.is_a?(Hash)
176
+ if value[:near]
177
+ filters << {
178
+ geo_distance: {
179
+ field => value.delete(:near).map(&:to_f).reverse,
180
+ distance: value.delete(:within) || "50mi"
181
+ }
182
+ }
183
+ end
184
+
185
+ if value[:top_left]
186
+ filters << {
187
+ geo_bounding_box: {
188
+ field => {
189
+ top_left: value.delete(:top_left).map(&:to_f).reverse,
190
+ bottom_right: value.delete(:bottom_right).map(&:to_f).reverse
191
+ }
192
+ }
193
+ }
194
+ end
195
+
196
+ value.each do |op, op_value|
197
+ if op == :not # not equal
198
+ filters << {not: term_filters.call(field, op_value)}
199
+ elsif op == :all
200
+ filters << {terms: {field => op_value, execution: "and"}}
201
+ else
202
+ range_query =
203
+ case op
204
+ when :gt
205
+ {from: op_value, include_lower: false}
206
+ when :gte
207
+ {from: op_value, include_lower: true}
208
+ when :lt
209
+ {to: op_value, include_upper: false}
210
+ when :lte
211
+ {to: op_value, include_upper: true}
212
+ else
213
+ raise "Unknown where operator"
214
+ end
215
+ filters << {range: {field => range_query}}
216
+ end
217
+ end
218
+ else
219
+ filters << term_filters.call(field, value)
220
+ end
221
+ end
222
+ end
223
+ filters
224
+ end
225
+
226
+ # filters
227
+ filters = where_filters.call(options[:where])
228
+ if filters.any?
229
+ payload[:filter] = {
230
+ and: filters
231
+ }
232
+ end
233
+
234
+ # facets
235
+ facet_limits = {}
236
+ if options[:facets]
237
+ facets = options[:facets] || {}
238
+ if facets.is_a?(Array) # convert to more advanced syntax
239
+ facets = Hash[ facets.map{|f| [f, {}] } ]
240
+ end
241
+
242
+ payload[:facets] = {}
243
+ facets.each do |field, facet_options|
244
+ # ask for extra facets due to
245
+ # https://github.com/elasticsearch/elasticsearch/issues/1305
246
+
247
+ if facet_options[:ranges]
248
+ payload[:facets][field] = {
249
+ range: {
250
+ field.to_sym => facet_options[:ranges]
251
+ }
252
+ }
253
+ else
254
+ payload[:facets][field] = {
255
+ terms: {
256
+ field: field,
257
+ size: facet_options[:limit] ? facet_options[:limit] + 150 : 100000
258
+ }
259
+ }
260
+ end
261
+
262
+ facet_limits[field] = facet_options[:limit] if facet_options[:limit]
263
+
264
+ # offset is not possible
265
+ # http://elasticsearch-users.115913.n3.nabble.com/Is-pagination-possible-in-termsStatsFacet-td3422943.html
266
+
267
+ facet_filters = where_filters.call(facet_options[:where])
268
+ if facet_filters.any?
269
+ payload[:facets][field][:facet_filter] = {
270
+ and: {
271
+ filters: facet_filters
272
+ }
273
+ }
274
+ end
275
+ end
276
+ end
277
+
278
+ # highlight
279
+ if options[:highlight]
280
+ payload[:highlight] = {
281
+ fields: Hash[ fields.map{|f| [f, {}] } ]
282
+ }
283
+ if options[:highlight].is_a?(Hash) and tag = options[:highlight][:tag]
284
+ payload[:highlight][:pre_tags] = [tag]
285
+ payload[:highlight][:post_tags] = [tag.to_s.gsub(/\A</, "</")]
286
+ end
287
+ end
288
+
289
+ payload[:_source] = 'updated_at'
290
+
291
+ self.new.merge payload
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end