scrivito_sdk 0.16.0 → 0.17.0

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/scrivito/default_cms_controller.rb +3 -3
  3. data/app/controllers/scrivito/objs_controller.rb +18 -7
  4. data/app/controllers/scrivito/webservice_controller.rb +13 -1
  5. data/app/controllers/scrivito/workspaces_controller.rb +5 -1
  6. data/app/helpers/scrivito/cms_asset_helper.rb +8 -1
  7. data/app/helpers/scrivito/default_cms_routing_helper.rb +2 -4
  8. data/app/helpers/scrivito/display_helper.rb +0 -7
  9. data/app/helpers/scrivito/editing_helper.rb +10 -8
  10. data/app/helpers/scrivito/layout_helper.rb +4 -11
  11. data/config/ca-bundle.crt +1773 -1416
  12. data/config/cms_routes.rb +2 -2
  13. data/config/routes.rb +1 -0
  14. data/lib/assets/javascripts/scrivito_editing.js +969 -533
  15. data/lib/assets/stylesheets/scrivito_editing.css +99 -9
  16. data/lib/generators/cms/migration/templates/migration.erb +34 -6
  17. data/lib/generators/cms/widget/templates/migration.erb +3 -6
  18. data/lib/scrivito/attribute.rb +158 -0
  19. data/lib/scrivito/attribute_collection.rb +72 -0
  20. data/lib/scrivito/attribute_content.rb +39 -3
  21. data/lib/scrivito/basic_obj.rb +48 -27
  22. data/lib/scrivito/basic_widget.rb +15 -5
  23. data/lib/scrivito/client_config.rb +46 -19
  24. data/lib/scrivito/cms_field_tag.rb +1 -1
  25. data/lib/scrivito/cms_rest_api/attribute_serializer.rb +6 -1
  26. data/lib/scrivito/cms_rest_api/blob_uploader.rb +1 -1
  27. data/lib/scrivito/configuration.rb +32 -2
  28. data/lib/scrivito/connection_manager.rb +1 -6
  29. data/lib/scrivito/content_conversion.rb +11 -7
  30. data/lib/scrivito/editing_context.rb +12 -0
  31. data/lib/scrivito/gem_info.rb +13 -0
  32. data/lib/scrivito/membership.rb +26 -0
  33. data/lib/scrivito/memberships_collection.rb +78 -0
  34. data/lib/scrivito/migrations/migration.rb +1 -1
  35. data/lib/scrivito/migrations/migration_dsl.rb +37 -0
  36. data/lib/scrivito/obj_class.rb +282 -0
  37. data/lib/scrivito/obj_data.rb +20 -1
  38. data/lib/scrivito/obj_params_parser.rb +2 -0
  39. data/lib/scrivito/obj_search_builder.rb +1 -1
  40. data/lib/scrivito/obj_search_enumerator.rb +11 -6
  41. data/lib/scrivito/objs_collection.rb +130 -0
  42. data/lib/scrivito/restriction_set.rb +54 -0
  43. data/lib/scrivito/user.rb +114 -0
  44. data/lib/scrivito/user_definition.rb +159 -0
  45. data/lib/scrivito/widget_garbage_collection.rb +4 -4
  46. data/lib/scrivito/workspace.rb +13 -78
  47. data/lib/scrivito/workspace_data_from_service.rb +2 -0
  48. metadata +15 -5
@@ -23,6 +23,20 @@ module Scrivito
23
23
 
24
24
  SPECIAL_KEYS = Set.new(%w[ body title blob ])
25
25
 
26
+ ATTRIBUTE_DEFAULT_VALUES = {
27
+ "string" => "",
28
+ "text" => "",
29
+ "html" => "",
30
+ "multienum" => [],
31
+ "linklist" => [],
32
+ "referencelist" => [],
33
+ "widget" => [],
34
+ "enum" => nil,
35
+ "binary" => nil,
36
+ "date" => nil,
37
+ "reference" => nil,
38
+ "link" => nil,
39
+ }.freeze
26
40
 
27
41
  def value_of(attribute_name)
28
42
  value_and_type_of(attribute_name).first
@@ -105,7 +119,12 @@ module Scrivito
105
119
  [ObjData::MissingAttribute.new, nil]
106
120
  end
107
121
  else
108
- value_and_type
122
+ value, type = value_and_type
123
+ if value.nil? && has_custom_attribute?(attribute_name)
124
+ value = ATTRIBUTE_DEFAULT_VALUES[type]
125
+ end
126
+
127
+ [value, type]
109
128
  end
110
129
  end
111
130
 
@@ -50,6 +50,8 @@ module Scrivito
50
50
  ContentConversion.convert_html_links(value, @host, @port)
51
51
  when 'linklist'
52
52
  ContentConversion.convert_linklist_urls(value, @host, @port)
53
+ when 'link'
54
+ ContentConversion.convert_link(value, @host, @port)
53
55
  when 'widget'
54
56
  widget_field_params.convert(value)
55
57
  else
@@ -22,7 +22,7 @@ class ObjSearchBuilder < Struct.new(:query)
22
22
  end
23
23
 
24
24
  def enumerator
25
- @enumerator ||= ObjSearchEnumerator.new
25
+ @enumerator ||= ObjSearchEnumerator.new(Workspace.current)
26
26
  end
27
27
 
28
28
  def set_predicates
@@ -110,8 +110,11 @@ module Scrivito
110
110
 
111
111
  include Enumerable
112
112
 
113
+ attr_reader :workspace
114
+
113
115
  attr_reader :query
114
- def initialize
116
+ def initialize(workspace)
117
+ @workspace = workspace
115
118
  @options = {}
116
119
  end
117
120
 
@@ -284,11 +287,13 @@ module Scrivito
284
287
  end
285
288
 
286
289
  def format(name)
287
- if @formatter = Configuration.obj_formats[name]
288
- self
289
- else
290
+ @formatter = Configuration.obj_formats[name]
291
+
292
+ unless @formatter
290
293
  raise UnregisteredObjFormat, "The format with name '#{name}' is not registered."
291
294
  end
295
+
296
+ self
292
297
  end
293
298
 
294
299
  # @api public
@@ -340,7 +345,7 @@ module Scrivito
340
345
  request_result = CmsRestApi.get(resource_path, search_dsl(offset))
341
346
 
342
347
  obj_ids = request_result['results'].map { |result| result['id'] || result['_id'] }
343
- objs = Obj.find_including_deleted(obj_ids)
348
+ objs = workspace.objs.find_including_deleted(obj_ids)
344
349
 
345
350
  @size = request_result['total'].to_i
346
351
 
@@ -348,7 +353,7 @@ module Scrivito
348
353
  end
349
354
 
350
355
  def resource_path
351
- "workspaces/#{Workspace.current.id}/objs/search"
356
+ "workspaces/#{workspace.id}/objs/search"
352
357
  end
353
358
 
354
359
  def search_dsl(offset)
@@ -0,0 +1,130 @@
1
+ module Scrivito
2
+ # This class allows you to retrieve Objects from a specific working copy.
3
+ # You can get an instance by accessing {Workspace#objs}.
4
+ # @api public
5
+ class ObjsCollection
6
+ attr_reader :workspace
7
+
8
+ def initialize(workspace)
9
+ @workspace = workspace
10
+ end
11
+
12
+ # Find a {BasicObj Obj} by its id.
13
+ # If the parameter is an Array containing ids, return a list of corresponding Objs.
14
+ # @param [String, Integer, Array<String, Integer>]id_or_list
15
+ # @return [Obj, Array<Obj>]
16
+ # @api public
17
+ def find(id_or_list)
18
+ find_filtering_deleted(id_or_list, false)
19
+ end
20
+
21
+ # Find a {BasicObj Obj} by its id.
22
+ # If the parameter is an Array containing ids, return a list of corresponding Objs.
23
+ # The results include deleted objects as well.
24
+ # @param [String, Integer, Array<String, Integer>]id_or_list
25
+ # @return [Obj, Array<Obj>]
26
+ # @api public
27
+ def find_including_deleted(id_or_list)
28
+ find_filtering_deleted(id_or_list, true)
29
+ end
30
+
31
+ # Find the {BasicObj Obj} with the given path.
32
+ # Returns +nil+ if no matching Obj exists.
33
+ # @param [String] path Path of the {BasicObj Obj}.
34
+ # @return [Obj]
35
+ # @api public
36
+ def find_by_path(path)
37
+ find_by(:path, [path]).first.first
38
+ end
39
+
40
+ # Returns the {BasicObj Obj} with the given permalink, or +nil+ if no matching Obj exists.
41
+ # @param [String] permalink The permalink of the {BasicObj Obj}.
42
+ # @return [Obj]
43
+ # @api public
44
+ def find_by_permalink(permalink)
45
+ find_by(:permalink, [permalink]).first.first
46
+ end
47
+
48
+ # Returns a {ObjSearchEnumerator} with the given initial subquery consisting of the four arguments.
49
+ #
50
+ # Note that +field+ and +value+ can also be arrays for searching several fields or searching for several values.
51
+ #
52
+ # {ObjSearchEnumerator}s can be chained using one of the chainable methods (e.g. {ObjSearchEnumerator#and} and {ObjSearchEnumerator#and_not}).
53
+ #
54
+ # @param [Symbol, String, Array<Symbol, String>] field See {ObjSearchEnumerator#and} for details
55
+ # @param [Symbol, String] operator See {ObjSearchEnumerator#and} for details
56
+ # @param [String, Array<String>] value See {ObjSearchEnumerator#and} for details
57
+ # @param [Hash] boost See {ObjSearchEnumerator#and} for details
58
+ # @return [ObjSearchEnumerator]
59
+ # @api public
60
+ def where(field, operator, value, boost = nil)
61
+ ObjSearchEnumerator.new(workspace)
62
+ .and(field, operator, value, boost)
63
+ end
64
+
65
+ # Returns a {ObjSearchEnumerator} of all {BasicObj Obj}s.
66
+ # @return [ObjSearchEnumerator]
67
+ # @api public
68
+ def all
69
+ ObjSearchEnumerator.new(workspace).batch_size(1000)
70
+ end
71
+
72
+ # Returns a {ObjSearchEnumerator} of all Objs with the given +obj_class+.
73
+ # @param [String] obj_class Name of the ObjClass.
74
+ # @return [ObjSearchEnumerator]
75
+ # @api public
76
+ def find_all_by_obj_class(obj_class)
77
+ all.and(:_obj_class, :equals, obj_class)
78
+ end
79
+
80
+ def find_by_parent_path(path)
81
+ find_by(:ppath, [path]).first
82
+ end
83
+
84
+ def find_by_id(id)
85
+ find_by(:id, [id]).first.first
86
+ end
87
+
88
+ def find_by_paths(paths)
89
+ find_by(:path, paths).map(&:first)
90
+ end
91
+
92
+ private
93
+
94
+ def find_filtering_deleted(id_or_list, include_deleted)
95
+ case id_or_list
96
+ when Array
97
+ find_by(:id, id_or_list, include_deleted).map(&:first).compact
98
+ else
99
+ obj = find_by(:id, [id_or_list.to_s], include_deleted).first.first
100
+ obj or raise ResourceNotFound, "Could not find Obj with id #{id_or_list}"
101
+ end
102
+ end
103
+
104
+ # accepts the name of an "obj_by" - view, a list of keys
105
+ # and an "include_deleted" flag
106
+ # returns a list of lists of Objs: a list of Objs for each given keys.
107
+ def find_by(view, keys, include_deleted = false)
108
+ if include_deleted
109
+ finder_method_name = :find_obj_data_including_deleted_by
110
+ else
111
+ finder_method_name = :find_obj_data_by
112
+ end
113
+
114
+ result = CmsBackend.instance.public_send(
115
+ finder_method_name,
116
+ workspace.revision,
117
+ view,
118
+ keys)
119
+
120
+ result.map do |list|
121
+ list.map do |obj_data|
122
+ obj = BasicObj.instantiate(obj_data)
123
+ obj.revision = workspace.revision
124
+
125
+ obj
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,54 @@
1
+ module Scrivito
2
+ class RestrictionSet
3
+ Restriction = Struct.new(:attribute, :callable) do
4
+ def call(obj)
5
+ if skip?(obj)
6
+ false
7
+ else
8
+ value = obj[attribute]
9
+
10
+ callable.call(value)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def skip?(obj)
17
+ !obj.has_attribute?(attribute) || obj.type_of_attribute(attribute) == 'widget'
18
+ end
19
+ end
20
+
21
+ def add(options, &block)
22
+ options = options.with_indifferent_access
23
+ attribute = options.fetch(:uses) do
24
+ raise ScrivitoError, 'No "uses" option given when adding a publish restriction'
25
+ end
26
+
27
+ unless block_given?
28
+ raise ScrivitoError, 'No block provided when adding a publish restriction'
29
+ end
30
+
31
+ restrictions << Restriction.new(attribute, block)
32
+ end
33
+
34
+ def restriction_messages_for(obj)
35
+ restriction_messages = restrictions.inject([]) do |messages, restriction|
36
+ message = restriction.call(obj)
37
+
38
+ if message
39
+ messages << message.to_s
40
+ end
41
+
42
+ messages
43
+ end
44
+
45
+ restriction_messages.uniq
46
+ end
47
+
48
+ private
49
+
50
+ def restrictions
51
+ @restrictions ||= []
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,114 @@
1
+ module Scrivito
2
+ # @api beta
3
+ class User < Struct.new(:id, :abilities, :description_proc, :suggest_users_proc, :restriction_set)
4
+ class << self
5
+ # Defines a new user.
6
+ # @api beta
7
+ # @param [String] id the unique, unalterable id of the user.
8
+ # The user id is used to associate the user with the corresponding CMS resources.
9
+ # It will be persisted in the CMS.
10
+ # @raise [Scrivito::ScrivitoError] if id is blank
11
+ # @raise [Scrivito::ScrivitoError] if id is more than 64 characters long
12
+ # @yieldparam [Scrivito::UserDefinition] user object to define abilities on
13
+ # @see Scrivito::UserDefinition#can
14
+ # @see Scrivito::UserDefinition#description
15
+ # @example
16
+ # Scrivito::User.define('alice') do |user|
17
+ # user.can(:publish_workspace) { true }
18
+ # end
19
+ #
20
+ # Scrivito::User.define('bob') do |user|
21
+ # user.description { 'Bob Doe' }
22
+ # user.can(:publish_workspace) { true }
23
+ # end
24
+ def define(id, &block)
25
+ assert_valid_id(id)
26
+ user_definition = UserDefinition.new(id)
27
+ yield user_definition
28
+ user_definition.user
29
+ end
30
+
31
+ def anonymous_admin
32
+ User.new(
33
+ id: nil,
34
+ abilities: Hash.new(-> {true}).with_indifferent_access,
35
+ description_proc: nil,
36
+ suggest_users_proc: nil
37
+ )
38
+ end
39
+
40
+ def find(id)
41
+ if Configuration.find_user_proc
42
+ user = Scrivito::Configuration.find_user_proc.call(id)
43
+ assert_valid_user(user)
44
+ user
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def assert_valid_id(id)
51
+ raise ScrivitoError.new('User id can not be blank') if id.blank?
52
+ raise ScrivitoError.new('User id is too long (max length 64)') if id.length > 64
53
+ end
54
+
55
+ def assert_valid_user(user)
56
+ unless user.is_a?(User) || user.nil?
57
+ raise ScrivitoError.new("Expected an instance of #{self} or nil, but got #{user.inspect}")
58
+ end
59
+ end
60
+ end
61
+
62
+ def initialize(options)
63
+ super(*options.values_at(:id, :abilities, :description_proc,
64
+ :suggest_users_proc, :restriction_set))
65
+ end
66
+
67
+ def able_to?(ability_name)
68
+ !!abilities[ability_name].call
69
+ end
70
+
71
+ # Verfies if the User is able to publish changes to a certain {BasicObj Obj}
72
+ #
73
+ # @api beta
74
+ # @param [BasicObj] obj the obj that should be published
75
+ # @return [Boolean] true if the user is allowed to publish otherwise false
76
+ def can_publish?(obj)
77
+ restriction_messages_for(obj).empty?
78
+ end
79
+
80
+ # Checks if the User is able to publish changes and returns the message
81
+ # specified in a {UserDefinition#restrict_obj_publish} callback if they are not
82
+ # If the user can publish the obj an empty array is returned
83
+ #
84
+ # @api beta
85
+ # @param [BasicObj] obj the obj that should be published
86
+ # @return [Array<String>] Hints why the user can't publish
87
+ def restriction_messages_for(obj)
88
+ return [] if able_to?(UserDefinition::ADMINISTRATE_CMS_ABILITY)
89
+
90
+ if obj.modification == Modification::EDITED
91
+ base_revision_obj = obj.in_revision(obj.revision.workspace.base_revision)
92
+
93
+ restriction_set.restriction_messages_for(obj) |
94
+ restriction_set.restriction_messages_for(base_revision_obj)
95
+ else
96
+ restriction_set.restriction_messages_for(obj)
97
+ end
98
+ end
99
+
100
+ def description
101
+ @description ||= calculate_description
102
+ end
103
+
104
+ def suggest_users(input)
105
+ suggest_users_proc ? suggest_users_proc.call(input) : []
106
+ end
107
+
108
+ private
109
+
110
+ def calculate_description
111
+ description_proc ? description_proc.call : id
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,159 @@
1
+ module Scrivito
2
+ # @api beta
3
+ class UserDefinition < Struct.new(:user_id)
4
+ # Allows user to publish a workspace.
5
+ # @api beta
6
+ PUBLISH_WORKSPACE_ABILITY = 'publish_workspace'
7
+
8
+ # Allows user to do everything.
9
+ # @api beta
10
+ ADMINISTRATE_CMS_ABILITY = 'administrate_cms'
11
+
12
+ # @api beta
13
+ ABILITY_NAMES = [
14
+ ADMINISTRATE_CMS_ABILITY,
15
+ PUBLISH_WORKSPACE_ABILITY,
16
+ ]
17
+
18
+ # Defines a user ability.
19
+ # @api beta
20
+ # @param [String, Symbol] ability_name name of the ability
21
+ # @param [Proc] ability_proc proc to check if the ability can be granted
22
+ # @raise [Scrivito::ScrivitoError] if the given ability name is unknown
23
+ # @raise [Scrivito::ScrivitoError] if no ability proc is given
24
+ # @yieldreturn If the block returns a "truthy" value, then the ability will be granted
25
+ # @yieldreturn If the block returns a "falsy" value, then the ability will be prohibited
26
+ # @see ABILITY_NAMES
27
+ # @see Scrivito::User.define
28
+ # @example
29
+ # allowed_user = nil
30
+ #
31
+ # alice = Scrivito::User.define('alice') do |user|
32
+ # user.can(:publish_workspace) { allowed_user == 'alice' }
33
+ # end
34
+ #
35
+ # bob = Scrivito::User.define('bob') do |user|
36
+ # user.can(:publish_workspace) { allowed_user == 'bob' }
37
+ # end
38
+ #
39
+ # # The users `alice` and `bob` will both be not allowed to publish workspace.
40
+ #
41
+ # allowed_user = 'alice'
42
+ # # The user `alice` will now be allowed to publish workspace,
43
+ # # but the user `bob` will still be not allowed.
44
+ #
45
+ # allowed_user = 'bob'
46
+ # # The user `bob` will now be allowed to publish workspace,
47
+ # # but the user `alice` will be not allowed again.
48
+ def can(ability_name, &ability_proc)
49
+ assert_valid_ability(ability_name, ability_proc)
50
+ abilities[ability_name] = ability_proc
51
+ end
52
+
53
+ # Defines the user description to be displayed, when the user is shown in the in-place GUI.
54
+ # @api beta
55
+ # @param [Proc] description_proc proc to calculate the description. Defaults to the the user id.
56
+ # @note The description is calculated "lazy".
57
+ # @note The calculated description will be cached.
58
+ # @see Scrivito::User.define
59
+ # @example
60
+ # alice = Scrivito::User.define('alice') {}
61
+ # # User `alice` will be displayed as "alice" in the in-place GUI.
62
+ #
63
+ # bob = Scrivito::User.define('bob') do |user|
64
+ # user.description { 'Bob Doe' }
65
+ # end
66
+ # # User `bob` will be displayed as "Bob Doe" in the in-place GUI.
67
+ def description(&description_proc)
68
+ @description_proc = description_proc
69
+ end
70
+
71
+ # Defines the proc for fetching users for the user autocompletion of the in-place GUI.
72
+ # The user autocompletion is for example used in the details dialog of a workspace.
73
+ # @api beta
74
+ # @param [Proc] suggest_users_proc proc for fetching users to be suggested in the in-place GUI
75
+ # @yieldparam [String] input an arbitrary string from the input field of a user autocompletion,
76
+ # e.g. the first letters of a user name
77
+ # @yieldreturn [Array<Scrivito::User>] users that were found for the given input string
78
+ # @note Only the first 20 of the returnes users will be displayed in the in-place GUI.
79
+ # @note +suggest_users_proc+ may also be invoked with an empty string.
80
+ # @example
81
+ # class MyUserModel
82
+ # def to_scrivito_user
83
+ # Scrivito::User.define(id) do |user|
84
+ # user.suggest_users do |input|
85
+ # MyUserModel.find_by_prefix(input).map(&:to_scrivito_user)
86
+ # end
87
+ # end
88
+ # end
89
+ # end
90
+ def suggest_users(&suggest_users_proc)
91
+ @suggest_users_proc = suggest_users_proc
92
+ end
93
+
94
+ # Lets you restrict the ability of a user to publish a certain object. Each
95
+ # registered callback can access a certain attribute of an object. Multiple
96
+ # callbacks are possible
97
+ #
98
+ # @api beta
99
+ # @param [Hash] options
100
+ # @option options [Symbol] :uses the attribute you need in the callback
101
+ # @yield [attribute] the value of the specified attribute
102
+ # @yieldreturn [String, false] either return a message for the user or false if
103
+ # no restriction is needed
104
+ #
105
+ # @note the callback is only called with {BasicObj Objs} that have the attribute
106
+ # specified by the :uses option and if it is not a {BasicWidget Widget}-attribute
107
+ #
108
+ # @example
109
+ # class MyUserModel
110
+ # def to_scrivito_user
111
+ # Scrivito::User.define(id) do |user|
112
+ # user.restrict_obj_publish(uses: :_path) do |path|
113
+ # if path.start_with?("/en")
114
+ # false
115
+ # else
116
+ # "You are only allowed to edit the English site"
117
+ # end
118
+ # end
119
+ #
120
+ # user.restrict_obj_publish(uses: :_obj_class) do |obj_class|
121
+ # if obj_class == "BlogPost"
122
+ # false
123
+ # else
124
+ # "You are only allowed to edit Blog Posts"
125
+ # end
126
+ # end
127
+ # end
128
+ # end
129
+ # end
130
+ def restrict_obj_publish(options, &block)
131
+ restriction_set.add(options, &block)
132
+ end
133
+
134
+ def user
135
+ User.new(id: user_id, abilities: abilities, description_proc: @description_proc,
136
+ suggest_users_proc: @suggest_users_proc, restriction_set: restriction_set)
137
+ end
138
+
139
+ private
140
+
141
+ def restriction_set
142
+ @restriction_set ||= RestrictionSet.new
143
+ end
144
+
145
+ def abilities
146
+ @abilities ||= Hash.new(-> { false }).with_indifferent_access
147
+ end
148
+
149
+ def assert_valid_ability(ability_name, ability_proc)
150
+ unless ABILITY_NAMES.include?(ability_name.to_s)
151
+ raise ScrivitoError.new("'#{ability_name}' is not a valid ability name")
152
+ end
153
+
154
+ unless ability_proc
155
+ raise ScrivitoError.new("No proc given for ability '#{ability_name}'")
156
+ end
157
+ end
158
+ end
159
+ end