infopark_fiona7 1.2.0.0.1 → 1.2.0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/scrivito_patches/models/obj.js +2 -6
  3. data/infopark_fiona7.gemspec +1 -1
  4. data/lib/fiona7/attribute_readers/attribute_reader.rb +17 -0
  5. data/lib/fiona7/attribute_readers/binary_as_binary.rb +12 -0
  6. data/lib/fiona7/attribute_readers/binary_as_linklist.rb +26 -0
  7. data/lib/fiona7/attribute_readers/date_as_date.rb +18 -0
  8. data/lib/fiona7/attribute_readers/factory.rb +91 -0
  9. data/lib/fiona7/attribute_readers/helpers/html_deserializer.rb +21 -0
  10. data/lib/fiona7/attribute_readers/helpers/json_deserializer.rb +11 -0
  11. data/lib/fiona7/attribute_readers/helpers/link_deserializer.rb +32 -0
  12. data/lib/fiona7/attribute_readers/html_as_html.rb +14 -0
  13. data/lib/fiona7/attribute_readers/link_as_linklist.rb +13 -0
  14. data/lib/fiona7/attribute_readers/linklist_as_linklist.rb +15 -0
  15. data/lib/fiona7/attribute_readers/multienum_as_multienum.rb +12 -0
  16. data/lib/fiona7/attribute_readers/multienum_as_text.rb +14 -0
  17. data/lib/fiona7/attribute_readers/number_as_string.rb +11 -0
  18. data/lib/fiona7/attribute_readers/reference_as_linklist.rb +14 -0
  19. data/lib/fiona7/attribute_readers/reference_as_string.rb +11 -0
  20. data/lib/fiona7/attribute_readers/referencelist_as_linklist.rb +16 -0
  21. data/lib/fiona7/attribute_readers/referencelist_as_text.rb +14 -0
  22. data/lib/fiona7/attribute_readers/simple.rb +18 -0
  23. data/lib/fiona7/attribute_readers/stringlist_as_text.rb +18 -0
  24. data/lib/fiona7/attribute_readers/widgetlist_as_linklist.rb +33 -0
  25. data/lib/fiona7/attribute_type_mapper.rb +76 -0
  26. data/lib/fiona7/attribute_writers/attribute_writer.rb +16 -0
  27. data/lib/fiona7/attribute_writers/binary_as_binary.rb +74 -0
  28. data/lib/fiona7/attribute_writers/binary_as_linklist.rb +86 -0
  29. data/lib/fiona7/attribute_writers/date_as_date.rb +11 -0
  30. data/lib/fiona7/attribute_writers/factory.rb +90 -0
  31. data/lib/fiona7/attribute_writers/helpers/html_serializer.rb +21 -0
  32. data/lib/fiona7/attribute_writers/helpers/json_serializer.rb +11 -0
  33. data/lib/fiona7/attribute_writers/helpers/link_serializer.rb +37 -0
  34. data/lib/fiona7/attribute_writers/html_as_html.rb +12 -0
  35. data/lib/fiona7/attribute_writers/link_as_linklist.rb +18 -0
  36. data/lib/fiona7/attribute_writers/linklist_as_linklist.rb +16 -0
  37. data/lib/fiona7/attribute_writers/multienum_as_multienum.rb +11 -0
  38. data/lib/fiona7/attribute_writers/multienum_as_text.rb +12 -0
  39. data/lib/fiona7/attribute_writers/number_as_string.rb +11 -0
  40. data/lib/fiona7/attribute_writers/reference_as_linklist.rb +17 -0
  41. data/lib/fiona7/attribute_writers/reference_as_string.rb +11 -0
  42. data/lib/fiona7/attribute_writers/referencelist_as_linklist.rb +19 -0
  43. data/lib/fiona7/attribute_writers/referencelist_as_text.rb +13 -0
  44. data/lib/fiona7/attribute_writers/simple.rb +11 -0
  45. data/lib/fiona7/attribute_writers/stringlist_as_text.rb +16 -0
  46. data/lib/fiona7/attribute_writers/widgetlist_as_linklist.rb +33 -0
  47. data/lib/fiona7/builder/obj_builder.rb +12 -201
  48. data/lib/fiona7/controllers/rest_api/obj_controller.rb +23 -5
  49. data/lib/fiona7/engine.rb +39 -0
  50. data/lib/fiona7/json/obj_decorator.rb +29 -121
  51. data/lib/fiona7/prefetch/obj_prefetch.rb +42 -0
  52. data/lib/fiona7/prefetch/widget_resolver_prefetch.rb +36 -0
  53. data/lib/fiona7/scrivito_patches/date_attribute.rb +16 -0
  54. data/lib/fiona7/tools/attribute_remover.rb +70 -0
  55. data/lib/fiona7/type_register.rb +9 -1
  56. data/lib/fiona7/version.rb +1 -1
  57. data/lib/fiona7/widget_resolver.rb +6 -6
  58. metadata +51 -4
@@ -15,6 +15,9 @@ require 'fiona7/assert'
15
15
  require 'fiona7/json/reverse_obj_decorator'
16
16
  require 'fiona7/json/typeless_obj_decorator'
17
17
 
18
+ require 'fiona7/prefetch/obj_prefetch'
19
+ require 'fiona7/prefetch/widget_resolver_prefetch'
20
+
18
21
  module Fiona7
19
22
  module Controllers
20
23
  module RestAPI
@@ -61,10 +64,13 @@ module Fiona7
61
64
  # FIXME: code duplication with fetch_by_id
62
65
  obj_ids = payload.with_indifferent_access[:ids]
63
66
  klass = revision_id.start_with?('f') ? ReleasedObj : EditedObj
64
- objs = klass.where(obj_id: obj_ids)
65
- objs_arr = obj_ids.map {|obj_id| objs.find {|o| o.id == obj_id.to_i } }
66
67
 
67
- decorated = objs_arr.map {|obj| obj ? Fiona7::JSON::ReverseObjDecorator.new(klass, obj) : nil }
68
+ # preloads all objects in one query
69
+ objs_arr = Prefetch::ObjPrefetch.new(klass, obj_ids).find_many(obj_ids)
70
+ # preloads all widgets in one query
71
+ widget_prefetch = Prefetch::WidgetResolverPrefetch.new(klass, objs_arr.compact)
72
+
73
+ decorated = objs_arr.map {|obj| obj ? Fiona7::JSON::ReverseObjDecorator.new(klass, obj, widget_prefetch.widget_resolver(obj)) : nil }
68
74
  response = ::JSON.parse(::ActiveSupport::JSON.encode({"results" => decorated}))
69
75
  return response
70
76
  end
@@ -112,11 +118,23 @@ module Fiona7
112
118
  conti = {}
113
119
  end
114
120
 
121
+ # This simulates the behavior of the backend:
122
+ # In absence of other input backend always sorts by ID
123
+ # or SCORE if an elastic search for such request would
124
+ # produce a score.
125
+ if (params[:query]||[]).any?{|op| (op[:operator]||op["operator"]).to_s =~ /search/ }
126
+ default_sort_order = :desc
127
+ default_sort_by = :_score
128
+ else
129
+ default_sort_order = :asc
130
+ default_sort_by = :id
131
+ end
132
+
115
133
  offset = conti[:offset] || params[:offset] || 0
116
134
  # 10 is the default value in original implementation
117
135
  size = conti[:size] || params[:size] || 10
118
- order = (conti[:sort_order] || params[:sort_order] || :asc).to_sym
119
- sort = (conti[:sort_by] || params[:sort_by] || :id).to_sym
136
+ order = (conti[:sort_order] || params[:sort_order] || default_sort_order).to_sym
137
+ sort = (conti[:sort_by] || params[:sort_by] || default_sort_by).to_sym
120
138
 
121
139
  if params[:query].present?
122
140
  normalize_query_params(params[:query])
data/lib/fiona7/engine.rb CHANGED
@@ -59,6 +59,9 @@ require 'fiona7/type_synchronizer'
59
59
  require 'fiona7/naive_search_engine'
60
60
  require 'fiona7/verity_search_engine'
61
61
 
62
+ # tools
63
+ require 'fiona7/tools/attribute_remover'
64
+
62
65
  module Fiona7
63
66
  mattr_accessor :mode
64
67
  mattr_accessor :login
@@ -71,6 +74,42 @@ module Fiona7
71
74
  :release_obj => []
72
75
  }
73
76
 
77
+ # Allows to specify custom typing for virtual attribute types
78
+ # Besides the built in mapping:
79
+ # link: :linklist,
80
+ # reference: :linklist,
81
+ # referencelist: :linklist,
82
+ # widgetlist: :linklist,
83
+ # binary: :linklist,
84
+ # number: :string,
85
+ # stringlist: :text
86
+ #
87
+ # Following other options are supported:
88
+ # reference: :string,
89
+ # referencelist: :text
90
+ #
91
+ # Custom mappings can be specified per obj_class:
92
+ #
93
+ # custom_attribute_types = {
94
+ # 'MyPage' => {
95
+ # child_order: :text
96
+ # }
97
+ # }
98
+ #
99
+ # or per attribute:
100
+ #
101
+ # custom_attribute_types = {
102
+ # child_order: :text
103
+ # }
104
+ #
105
+ # or using a callable entity like a proc:
106
+ #
107
+ # custom_attribute_types = lambda do |obj_class, attribute|
108
+ # :text if attribute == :child_order
109
+ # end
110
+ mattr_accessor :custom_attribute_types
111
+ self.custom_attribute_types = {}
112
+
74
113
  mattr_accessor :search_engine
75
114
  self.search_engine = Fiona7::VeritySearchEngine
76
115
 
@@ -6,10 +6,11 @@ module Fiona7
6
6
  end
7
7
 
8
8
  require 'fiona7/assert'
9
- require 'fiona7/link_converter/fiona_to_scrivito'
10
9
  require 'fiona7/json/widget_decorator'
11
10
  require 'fiona7/widget_resolver'
12
- require 'fiona7/blob_id_generator'
11
+ require 'fiona7/prefetch/obj_prefetch'
12
+
13
+ require 'fiona7/attribute_readers/factory'
13
14
 
14
15
  module Fiona7
15
16
  module JSON
@@ -18,12 +19,10 @@ module Fiona7
18
19
  @klass = klass
19
20
  @obj = obj
20
21
 
21
- @link_converter = LinkConverter::FionaToScrivito.new(obj)
22
-
23
22
  if widget_resolver
24
23
  @widget_resolver = widget_resolver
25
24
  elsif (widget_links = @obj.attr_values["X_widget_pool"])
26
- @widget_resolver = WidgetResolver.new(widget_links, @klass)
25
+ @widget_resolver = WidgetResolver.new(widget_links, Prefetch::ObjPrefetch.new(klass))
27
26
  end
28
27
  end
29
28
 
@@ -58,137 +57,46 @@ module Fiona7
58
57
 
59
58
  def add_custom_attributes(attrs, *args)
60
59
  type_definition = Fiona7::TypeRegister.instance.read_mangled(@obj.obj_class)
60
+ factory = Fiona7::AttributeReaders::Factory.new(@obj, type_definition, @widget_resolver)
61
+
61
62
  type_definition.attrs.each do |attribute|
62
63
  virtual_attribute_name = attribute.name
63
64
  real_attribute_name = attribute.real_name
64
65
  virtual_attribute_type = attribute.type.to_sym
65
-
66
- val = case virtual_attribute_type
67
- when :link
68
- deserialize_link(@obj.attr_values[real_attribute_name].try(:first))
69
- when :linklist
70
- (@obj.attr_values[real_attribute_name] || []).map {|link| deserialize_link(link) }
71
- when :multienum
72
- @obj[real_attribute_name] || []
73
- when :reference
74
- deserialize_reference(@obj.attr_values[real_attribute_name].try(:first))
75
- when :referencelist
76
- (@obj.attr_values[real_attribute_name] || []).map {|link| deserialize_reference(link) }.compact
77
- when :date
78
- if real_attribute_name == "valid_from" || real_attribute_name == "valid_until"
79
- # a bug in fiona connector -> [valid_(from|until)] returns a raw string
80
- @obj[real_attribute_name]
81
- else
82
- @obj[real_attribute_name].try(:utc).try(:to_iso)
83
- end
84
- when :widgetlist
85
- deserialize_widget_field(@obj.attr_values[real_attribute_name] || [])
86
- when :html
87
- deserialize_html(@obj[real_attribute_name])
88
- when :markdown
89
- nil
90
- when :binary
91
- if !Fiona7.mode == :legacy || !@obj.binary?
92
- deserialize_binary(@obj.attr_values[real_attribute_name].try(:first))
93
- else
94
- {id: Fiona7::BlobIdGenerator.new(@obj.id, @obj.last_changed).call}
95
- end
96
- when :enum, :multienum, :text, :string
97
- if real_attribute_name == "suppress_export"
98
- @obj[real_attribute_name].to_s
99
- else
100
- @obj[real_attribute_name]
101
- end
102
- when :number
103
- @obj[real_attribute_name].to_f
104
- when :stringlist
105
- if Fiona7.mode == :legacy && real_attribute_name == "channels"
106
- @obj["channels"] || []
107
- else
108
- # TODO: get rid of this rescue nil
109
- ::JSON.parse(@obj[real_attribute_name]) rescue nil
110
- end
66
+ # This is probably the most important line in this whole class.
67
+ # An intuitive way of reading the real attribute type
68
+ # would be to just look-up in the attribute definition.
69
+ #
70
+ # But this approach is wrong. Consider the following case:
71
+ # An attribute :child_order is created with real type :linklist
72
+ # This proves problematic (because of the unwanted links in CMS)
73
+ # and is later changed to :text. But the CMS still contains
74
+ # "old" objects with :child_order modelled as :linklist.
75
+ # Therefore we must lookup the correct typing in the attribute itself.
76
+ #
77
+ # It is also possible that the obj does not have any content for the
78
+ # attribute. In this case we just use the type provided by
79
+ # the type definition.
80
+ real_attribute_type = ((@obj.attr_dict.send(:attr_defs) || {})[real_attribute_name.to_s]||{})["type"] ||
81
+ attribute.real_type
82
+
83
+ worker = factory.call(real_attribute_name, virtual_attribute_type, real_attribute_type)
84
+ if worker.nil?
85
+ Rails.logger.error("Unable to deserialize #{virtual_attribute_name} in #{type_definition.name}")
86
+ val = nil
111
87
  else
112
- Assert.success(
113
- false,
114
- "Unknown attribute type #{virtual_attribute_type} for attribute: #{virtual_attribute_name}"
115
- )
88
+ val = worker.call
116
89
  end
117
90
 
118
91
  # TODO: better handling for attribute types
119
92
  if virtual_attribute_type == :text
120
93
  virtual_attribute_type = :string
121
94
  end
122
- attrs[virtual_attribute_name] = [val, virtual_attribute_type]
123
- end
124
- end
125
95
 
126
- def deserialize_link(link)
127
- if link
128
- deserialized = {
129
- # remove possible external prefix for protcol-less urls
130
- url: link["url"].try(:gsub, /\Aexternal:/, ''),
131
- title: link["title"],
132
- target: link["target"],
133
- # content service uses destination
134
- destination: link["destination"].to_s,
135
- # rest api uses obj_id
136
- obj_id: link["destination"].to_s,
137
- query: link["search"],
138
- fragment: link["fragment"]
139
- }
140
-
141
- # TODO: refactor this code
142
- if deserialized[:url].present?
143
- deserialized.delete(:destination)
144
- deserialized.delete(:obj_id)
145
- end
146
-
147
- deserialized
148
- end
149
- end
150
-
151
- def deserialize_reference(link)
152
- if link && link["type"] == "internal"
153
- link["destination"].to_s
154
- end
155
- end
156
-
157
- def deserialize_widget_field(linklist)
158
- if !linklist.empty?
159
- widget_ids = []
160
- widgets = linklist.map do |link|
161
- widget_id = link["title"]
162
- # sometimes a released page references an unreleased widget
163
- # it is then impossible to load it into widget_pool
164
- # but the reference in the widgetlist field is still there
165
- # therefore we need to check if the widget has been loaded
166
- # into the pool - only then it can be included
167
- if valid_widget_id?(widget_id)
168
- widget_ids << widget_id
169
- end
170
- end
171
-
172
- widget_ids
173
- end
174
- end
175
-
176
- def deserialize_html(html)
177
- @link_converter.convert(html)
178
- end
179
-
180
- def deserialize_binary(link)
181
- if link
182
- binary_id = link["title"]
183
- if binary_id.length == 32 && binary_id =~ /0{16,}0*[0-9]+/
184
- {id: binary_id}
185
- end
96
+ attrs[virtual_attribute_name] = [val, virtual_attribute_type]
186
97
  end
187
98
  end
188
99
 
189
- def valid_widget_id?(widget_id)
190
- @widget_resolver && @widget_resolver.path_map[widget_id]
191
- end
192
100
 
193
101
  def replace_widget_pool(attrs, *args)
194
102
  if (widget_links = @obj.attr_values["X_widget_pool"]) && @widget_resolver
@@ -0,0 +1,42 @@
1
+ require 'set'
2
+
3
+ module Fiona7
4
+ module Prefetch
5
+ class ObjPrefetch
6
+ def initialize(klass, prefetch_ids=[])
7
+ self.klass = klass
8
+ self.prefetched_ids = Set.new
9
+ self.object_id_map = {}
10
+
11
+ self.prefetch_objects(prefetch_ids)
12
+ end
13
+
14
+ # Returns an object with given ID or nil if no matching object found
15
+ def find_one(id)
16
+ self.find_many([id]).first
17
+ end
18
+
19
+ # Returns an array with objects for given IDs.
20
+ # The array is ordered according to the IDs given.
21
+ # For missing objects the array is filled with nil values
22
+ def find_many(ids)
23
+ self.prefetch_objects(ids)
24
+ ids.map { |id| self.object_id_map[id.to_i] }
25
+ end
26
+
27
+ protected
28
+ attr_accessor :klass, :prefetched_ids, :object_id_map
29
+
30
+ def prefetch_objects(prefetch_ids)
31
+ missing = prefetch_ids.select {|id| !self.prefetched_ids.include?(id) }
32
+
33
+ return if missing.empty?
34
+
35
+ self.klass.where(obj_id: missing).each do |obj|
36
+ self.prefetched_ids << obj.id
37
+ self.object_id_map[obj.id] = obj
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,36 @@
1
+ require 'fiona7/prefetch/obj_prefetch'
2
+ require 'fiona7/widget_resolver'
3
+
4
+ module Fiona7
5
+ module Prefetch
6
+ class WidgetResolverPrefetch
7
+ def initialize(klass, objects=[])
8
+ widget_object_ids = self.collect_widget_object_ids(objects)
9
+
10
+ self.obj_prefetch = ObjPrefetch.new(klass, widget_object_ids)
11
+ end
12
+
13
+ # Returns a widget resolver for given object.
14
+ # If the object is already known to the prefetcher,
15
+ # then widgets will be loaded from memory,
16
+ # instead of database.
17
+ def widget_resolver(object)
18
+ WidgetResolver.new(
19
+ object.attr_values["X_widget_pool"]||[],
20
+ self.obj_prefetch
21
+ )
22
+ end
23
+
24
+ protected
25
+ attr_accessor :obj_prefetch
26
+
27
+ def collect_widget_object_ids(objects)
28
+ objects.map do |object|
29
+ (object.attr_values["X_widget_pool"]||[]).map do |widget_hash|
30
+ widget_hash["destination"].try(:to_i)
31
+ end
32
+ end.flatten.compact
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ require 'scrivito/date_attribute'
2
+
3
+ module Scrivito
4
+ module DateAttribute
5
+ def self.deserialize_from_backend(iso_date_time)
6
+ return nil unless iso_date_time
7
+ #return nil if iso_date.to_s.blank?
8
+
9
+ if iso_date_time.to_s =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/
10
+ Time.utc($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i).in_time_zone
11
+ else
12
+ raise "The value is not a valid ISO date time: #{iso_date_time.inspect}"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,70 @@
1
+ require 'fiona7/obj_class_name_mangler'
2
+ require 'fiona7/attribute_name_mangler'
3
+
4
+ module Fiona7
5
+ module Tools
6
+ # This class is used to remove attributes from obj classes.
7
+ # Normally it is not required, because the user can just
8
+ # remove an attribute from the model definition and the
9
+ # attribute becomes invisible.
10
+ # The attribute still persists, but becomes inaccessible.
11
+ #
12
+ # In some cases it may be required to garbage collect
13
+ # the removed attribute, for example to change the type
14
+ # of an attribute, the attribute has to be removed first.
15
+ #
16
+ # This class behaves differently for different configrations.
17
+ # In standalone mode the attribute is removed from
18
+ # the specified obj class and also from CMS.
19
+ # In legacy mode the attribute is removed from
20
+ # the specified obj class. If there are no more obj classes
21
+ # referencing this attribute, it also gets deleted from CMS.
22
+ class AttributeRemover
23
+ def initialize(obj_class, attr_name)
24
+ self.obj_class = ObjClassNameMangler.new(obj_class).mangle
25
+ self.attr_name = AttributeNameMangler.new(attr_name, obj_class).mangle
26
+
27
+ self.original_attr_name = attr_name
28
+ self.original_obj_class = obj_class
29
+ end
30
+
31
+ def remove
32
+ if rc_attr = RailsConnector::Attribute.find_by_attribute_name(self.attr_name)
33
+ # fetch the names of all obj classes referencing this attribute
34
+ obj_classes = rc_attr.obj_class_defs.map(&:name)
35
+ if obj_classes.include?(self.obj_class)
36
+ cm_obj_class = Reactor::Cm::ObjClass.get(self.obj_class)
37
+ # this removes the attribute from the obj class
38
+ cm_obj_class.attributes = (cm_obj_class.attributes - [self.attr_name])
39
+ # flush reactor cache
40
+ RailsConnector::Meta::EagerLoader.instance.forget_obj_class(self.obj_class)
41
+ # flush type register caches
42
+ Fiona7::TypeRegister.instance.send(:load, self.original_obj_class)
43
+ if typedef = Fiona7::TypeRegister.instance.usr_defs[self.original_obj_class]
44
+ # There is a very subtle bug hidden here:
45
+ # If you have an enum or multienum attribute, remove it and
46
+ # trigger synchronize (via write request) it will be created
47
+ # as string or text.
48
+ #
49
+ # Easiest way to prevent this bug is to restart the app
50
+ # between the requests.
51
+ typedef.remove_attribute(self.original_attr_name)
52
+ end
53
+ # one reference less!
54
+ obj_classes.delete(self.obj_class)
55
+ end
56
+
57
+ # NOTE: this will always be true in standlone mode
58
+ if obj_classes.empty?
59
+ # attribute can be garbage collected
60
+ cm_attr = Reactor::Cm::Attribute.get(self.attr_name)
61
+ cm_attr.delete!
62
+ end
63
+ end
64
+ end
65
+
66
+ protected
67
+ attr_accessor :obj_class, :attr_name, :original_obj_class, :original_attr_name
68
+ end
69
+ end
70
+ end