scrivito_sdk 0.16.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
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