alchemy_cms 6.0.0.b1 → 6.0.0.pre.b5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/README.md +14 -2
  4. data/Rakefile +37 -23
  5. data/alchemy_cms.gemspec +1 -1
  6. data/app/assets/stylesheets/alchemy/_extends.scss +15 -2
  7. data/app/assets/stylesheets/alchemy/archive.scss +16 -1
  8. data/app/assets/stylesheets/alchemy/fonts.scss +0 -0
  9. data/app/assets/stylesheets/tinymce/skins/alchemy/fonts/tinymce-small.svg +0 -0
  10. data/app/assets/stylesheets/tinymce/skins/alchemy/fonts/tinymce-small.ttf +0 -0
  11. data/app/assets/stylesheets/tinymce/skins/alchemy/fonts/tinymce-small.woff +0 -0
  12. data/app/assets/stylesheets/tinymce/skins/alchemy/fonts/tinymce.svg +0 -0
  13. data/app/assets/stylesheets/tinymce/skins/alchemy/fonts/tinymce.ttf +0 -0
  14. data/app/assets/stylesheets/tinymce/skins/alchemy/fonts/tinymce.woff +0 -0
  15. data/app/assets/stylesheets/tinymce/skins/alchemy/img/anchor.gif +0 -0
  16. data/app/assets/stylesheets/tinymce/skins/alchemy/img/loader.gif +0 -0
  17. data/app/assets/stylesheets/tinymce/skins/alchemy/img/object.gif +0 -0
  18. data/app/assets/stylesheets/tinymce/skins/alchemy/img/trans.gif +0 -0
  19. data/app/assets/stylesheets/tinymce/skins/alchemy/skin.min.css.scss +0 -0
  20. data/app/controllers/alchemy/admin/attachments_controller.rb +3 -3
  21. data/app/controllers/alchemy/admin/elements_controller.rb +1 -0
  22. data/app/controllers/alchemy/admin/pages_controller.rb +1 -9
  23. data/app/controllers/alchemy/admin/pictures_controller.rb +21 -8
  24. data/app/controllers/alchemy/admin/resources_controller.rb +84 -10
  25. data/app/controllers/alchemy/api/elements_controller.rb +12 -8
  26. data/app/controllers/alchemy/api/pages_controller.rb +4 -2
  27. data/app/decorators/alchemy/element_editor.rb +7 -4
  28. data/app/decorators/alchemy/ingredient_editor.rb +5 -1
  29. data/app/helpers/alchemy/elements_block_helper.rb +14 -6
  30. data/app/models/alchemy/attachment.rb +24 -7
  31. data/app/models/alchemy/element/element_essences.rb +14 -3
  32. data/app/models/alchemy/element/element_ingredients.rb +11 -3
  33. data/app/models/alchemy/element/presenters.rb +18 -1
  34. data/app/models/alchemy/element.rb +0 -14
  35. data/app/models/alchemy/ingredient.rb +21 -62
  36. data/app/models/alchemy/page/page_natures.rb +1 -10
  37. data/app/models/alchemy/page/page_scopes.rb +4 -0
  38. data/app/models/alchemy/page.rb +13 -4
  39. data/app/models/alchemy/page_version.rb +1 -1
  40. data/app/models/alchemy/picture.rb +14 -38
  41. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +1 -1
  42. data/app/views/alchemy/admin/attachments/index.html.erb +2 -3
  43. data/app/views/alchemy/admin/elements/create.js.erb +1 -1
  44. data/app/views/alchemy/admin/elements/destroy.js.erb +1 -3
  45. data/app/views/alchemy/admin/elements/fold.js.erb +2 -2
  46. data/app/views/alchemy/admin/elements/update.js.erb +1 -1
  47. data/app/views/alchemy/admin/pages/_toolbar.html.erb +1 -1
  48. data/app/views/alchemy/admin/pages/edit.html.erb +1 -1
  49. data/app/views/alchemy/admin/pages/index.html.erb +2 -9
  50. data/app/views/alchemy/admin/partials/_search_form.html.erb +9 -0
  51. data/app/views/alchemy/admin/pictures/_archive.html.erb +1 -1
  52. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -1
  53. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +4 -2
  54. data/app/views/alchemy/admin/pictures/index.html.erb +8 -3
  55. data/app/views/alchemy/admin/resources/_filter.html.erb +12 -0
  56. data/app/views/alchemy/admin/resources/_filter_bar.html.erb +14 -17
  57. data/app/views/alchemy/admin/resources/_form.html.erb +2 -0
  58. data/app/views/alchemy/admin/resources/_table_header.html.erb +15 -0
  59. data/app/views/alchemy/admin/resources/index.html.erb +3 -11
  60. data/app/views/alchemy/ingredients/_boolean_editor.html.erb +1 -1
  61. data/app/views/alchemy/ingredients/_file_editor.html.erb +3 -1
  62. data/app/views/alchemy/ingredients/_headline_editor.html.erb +1 -1
  63. data/app/views/alchemy/ingredients/_html_editor.html.erb +1 -1
  64. data/app/views/alchemy/ingredients/_link_editor.html.erb +8 -8
  65. data/app/views/alchemy/ingredients/_node_editor.html.erb +1 -0
  66. data/app/views/alchemy/ingredients/_page_editor.html.erb +1 -0
  67. data/app/views/alchemy/ingredients/_picture_editor.html.erb +7 -6
  68. data/app/views/alchemy/ingredients/_select_editor.html.erb +1 -0
  69. data/app/views/alchemy/ingredients/_text_editor.html.erb +5 -4
  70. data/config/locales/alchemy.en.yml +85 -49
  71. data/lib/alchemy/forms/builder.rb +21 -1
  72. data/lib/alchemy/resource_filter.rb +40 -0
  73. data/lib/alchemy/resources_helper.rb +1 -16
  74. data/lib/alchemy/test_support/shared_ingredient_examples.rb +21 -4
  75. data/lib/alchemy/tinymce.rb +4 -0
  76. data/lib/alchemy/upgrader/tasks/ingredients_migrator.rb +18 -7
  77. data/lib/alchemy/version.rb +1 -1
  78. data/lib/alchemy_cms.rb +1 -0
  79. data/lib/generators/alchemy/menus/templates/node.html.erb +1 -1
  80. data/lib/generators/alchemy/menus/templates/node.html.haml +1 -1
  81. data/lib/generators/alchemy/menus/templates/node.html.slim +1 -1
  82. data/lib/generators/alchemy/menus/templates/wrapper.html.erb +1 -1
  83. data/lib/generators/alchemy/menus/templates/wrapper.html.haml +1 -1
  84. data/lib/generators/alchemy/menus/templates/wrapper.html.slim +1 -1
  85. data/package.json +1 -1
  86. metadata +7 -7
  87. data/app/views/alchemy/admin/attachments/_filter_bar.html.erb +0 -29
  88. data/app/views/alchemy/admin/pictures/_filter_bar.html.erb +0 -30
@@ -9,21 +9,25 @@ module Alchemy
9
9
  # If you want to only load a specific type of element pass ?named=an_element_name
10
10
  #
11
11
  def index
12
- if params[:page_id].present?
13
- @page = Page.find(params[:page_id])
14
- @elements = @page.elements.not_nested
12
+ # Fix for cancancan not able to merge multiple AR scopes for logged in users
13
+ if cannot? :manage, Alchemy::Element
14
+ @elements = Alchemy::Element.accessible_by(current_ability, :index)
15
15
  else
16
- @elements = Element.not_nested.joins(:page_version).merge(PageVersion.published)
16
+ @elements = Alchemy::Element.all
17
17
  end
18
18
 
19
- # Fix for cancancan not able to merge multiple AR scopes for logged in users
20
- if cannot? :manage, Alchemy::Element
21
- @elements = @elements.accessible_by(current_ability, :index)
19
+ @elements = @elements.not_nested.joins(:page_version).merge(PageVersion.published)
20
+
21
+ if params[:page_id].present?
22
+ @elements = @elements.includes(:page).where(alchemy_pages: { id: params[:page_id] })
23
+ else
24
+ @elements = @elements.includes(*element_includes)
22
25
  end
26
+
23
27
  if params[:named].present?
24
28
  @elements = @elements.named(params[:named])
25
29
  end
26
- @elements = @elements.includes(*element_includes).order(:position)
30
+ @elements = @elements.order(:position)
27
31
 
28
32
  render json: @elements, adapter: :json, root: "elements"
29
33
  end
@@ -7,10 +7,12 @@ module Alchemy
7
7
  # Returns all pages as json object
8
8
  #
9
9
  def index
10
- @pages = Language.current&.pages.presence || Alchemy::Page.none
11
10
  # Fix for cancancan not able to merge multiple AR scopes for logged in users
12
11
  if cannot? :edit_content, Alchemy::Page
13
- @pages = @pages.accessible_by(current_ability, :index)
12
+ @pages = Alchemy::Page.accessible_by(current_ability, :index)
13
+ @pages = @pages.where(language: Language.current)
14
+ else
15
+ @pages = Language.current&.pages.presence || Alchemy::Page.none
14
16
  end
15
17
  @pages = @pages.includes(*page_includes)
16
18
  @pages = @pages.ransack(params[:q]).result
@@ -26,7 +26,7 @@ module Alchemy
26
26
  # @return Array<Alchemy::IngredientEditor>
27
27
  def ingredients
28
28
  element.definition.fetch(:ingredients, []).map do |ingredient|
29
- Alchemy::IngredientEditor.new(find_or_create_ingredient(ingredient[:role]))
29
+ Alchemy::IngredientEditor.new(find_or_create_ingredient(ingredient))
30
30
  end
31
31
  end
32
32
 
@@ -121,9 +121,12 @@ module Alchemy
121
121
  Alchemy::Content.create(element: element, name: name)
122
122
  end
123
123
 
124
- def find_or_create_ingredient(role)
125
- element.ingredients.find { |i| i.role == role } ||
126
- Ingredient.create(element: element, role: role)
124
+ def find_or_create_ingredient(definition)
125
+ element.ingredients.detect { |i| i.role == definition[:role] } ||
126
+ element.ingredients.create!(
127
+ role: definition[:role],
128
+ type: Alchemy::Ingredient.normalize_type(definition[:type]),
129
+ )
127
130
  end
128
131
  end
129
132
  end
@@ -64,8 +64,12 @@ module Alchemy
64
64
  "element[ingredients_attributes][#{form_field_counter}][#{column}]"
65
65
  end
66
66
 
67
+ # Returns a unique string to be passed to a form field id.
68
+ #
69
+ # @param column [String] A Ingredient column_name. Default is 'value'
70
+ #
67
71
  def form_field_id(column = "value")
68
- "element_ingredients_attributes_#{form_field_counter}_#{column}"
72
+ "element_#{element.id}_ingredient_#{id}_#{column}"
69
73
  end
70
74
 
71
75
  # Fixes Rails partial renderer calling to_model on the object
@@ -51,21 +51,23 @@ module Alchemy
51
51
  # If the element uses +ingredients+ it returns the +value+ of the ingredient record.
52
52
  #
53
53
  def ingredient(name)
54
- element.ingredient(name).presence || element.ingredient_by_role(name)&.value
54
+ element.ingredient(name)
55
55
  end
56
56
 
57
- deprecate ingredient: :value, deprecator: Alchemy::Deprecation
58
-
59
57
  # Returns the value of one of the element's ingredients.
60
58
  #
61
59
  def value(name)
62
- element.ingredient_by_role(name)&.value
60
+ element.value_for(name)
63
61
  end
64
62
 
65
- # Returns true if the given content or ingredient has been filled by the user.
63
+ # Returns true if the given content or ingredient has a value.
66
64
  #
67
65
  def has?(name)
68
- element.has_ingredient?(name) || element.has_value_for?(name)
66
+ if element.ingredient_definitions.any?
67
+ element.has_value_for?(name)
68
+ else
69
+ element.has_ingredient?(name)
70
+ end
69
71
  end
70
72
 
71
73
  # Return's the given content's essence.
@@ -75,6 +77,12 @@ module Alchemy
75
77
  end
76
78
 
77
79
  deprecate essence: "Use `ingredient_by_role` instead", deprecator: Alchemy::Deprecation
80
+
81
+ # Return's the ingredient record by given role.
82
+ #
83
+ def ingredient_by_role(role)
84
+ element.ingredient_by_role(role)
85
+ end
78
86
  end
79
87
 
80
88
  # Block-level helper for element views. Constructs a DOM element wrapping
@@ -35,6 +35,10 @@ module Alchemy
35
35
  has_many :elements, through: :contents
36
36
  has_many :pages, through: :elements
37
37
 
38
+ scope :by_file_type, ->(file_type) { where(file_mime_type: file_type) }
39
+ scope :recent, -> { where("#{table_name}.created_at > ?", Time.current - 24.hours).order(:created_at) }
40
+ scope :without_tag, -> { left_outer_joins(:taggings).where(gutentag_taggings: { id: nil }) }
41
+
38
42
  # We need to define this method here to have it available in the validations below.
39
43
  class << self
40
44
  # The class used to generate URLs for attachments
@@ -51,6 +55,26 @@ module Alchemy
51
55
  @_url_class = klass
52
56
  end
53
57
 
58
+ def alchemy_resource_filters
59
+ [
60
+ {
61
+ name: :by_file_type,
62
+ values: distinct.pluck(:file_mime_type).map { |type| [Alchemy.t(type, scope: "mime_types"), type] }.sort_by(&:first),
63
+ },
64
+ {
65
+ name: :misc,
66
+ values: %w(recent last_upload without_tag),
67
+ },
68
+ ]
69
+ end
70
+
71
+ def last_upload
72
+ last_id = Attachment.maximum(:id)
73
+ return Attachment.all unless last_id
74
+
75
+ where(id: last_id)
76
+ end
77
+
54
78
  def searchable_alchemy_resource_attributes
55
79
  %w(name file_name)
56
80
  end
@@ -58,13 +82,6 @@ module Alchemy
58
82
  def allowed_filetypes
59
83
  Config.get(:uploader).fetch("allowed_filetypes", {}).fetch("alchemy/attachments", [])
60
84
  end
61
-
62
- def file_types_for_select
63
- file_types = Alchemy::Attachment.pluck(:file_mime_type).uniq.map do |type|
64
- [Alchemy.t(type, scope: "mime_types"), type]
65
- end
66
- file_types.sort_by(&:first)
67
- end
68
85
  end
69
86
 
70
87
  validates_presence_of :file
@@ -5,10 +5,20 @@ module Alchemy
5
5
  module ElementEssences
6
6
  # Returns the contents essence value (aka. ingredient) for passed content name.
7
7
  def ingredient(name)
8
- content = content_by_name(name)
9
- return nil if content.blank?
8
+ ing = ingredient_by_role(name)
9
+ if ing
10
+ Alchemy::Deprecation.warn <<~WARN
11
+ Using `element.ingredient` to get the value of an ingredient is deprecated and will change in Alchemy 6.1
12
+ If you want to read the value of an elements ingredient please use `element.value_for(:ingredient_role)` instead.
13
+ The next version of Alchemy will return a `Alchemy::Ingredient` record instead.
14
+ WARN
15
+ ing.value
16
+ else
17
+ content = content_by_name(name)
18
+ return nil if content.blank?
10
19
 
11
- content.ingredient
20
+ content.ingredient
21
+ end
12
22
  end
13
23
 
14
24
  # True if the element has a content for given name,
@@ -16,6 +26,7 @@ module Alchemy
16
26
  def has_ingredient?(name)
17
27
  ingredient(name).present?
18
28
  end
29
+ deprecate has_ingredient?: :has_value_for?, deprecator: Alchemy::Deprecation
19
30
 
20
31
  # Returns all essence errors in the format of:
21
32
  #
@@ -22,6 +22,11 @@ module Alchemy
22
22
  validates_associated :ingredients, on: :update
23
23
  end
24
24
 
25
+ # The value of an ingredient of the element by role
26
+ def value_for(role)
27
+ ingredient_by_role(role)&.value
28
+ end
29
+
25
30
  # Find first ingredient from element by given role.
26
31
  def ingredient_by_role(role)
27
32
  ingredients.detect { |ingredient| ingredient.role == role.to_s }
@@ -87,7 +92,7 @@ module Alchemy
87
92
  # True if the element has a ingredient for given name
88
93
  # that has a non blank value.
89
94
  def has_value_for?(role)
90
- ingredient_by_role(role)&.value.present?
95
+ value_for(role).present?
91
96
  end
92
97
 
93
98
  # Ingredient validation error messages
@@ -167,8 +172,11 @@ module Alchemy
167
172
 
168
173
  # Builds ingredients for this element as described in the +elements.yml+
169
174
  def build_ingredients
170
- self.ingredients = ingredient_definitions.map do |attributes|
171
- Ingredient.build(role: attributes[:role], element: self)
175
+ ingredient_definitions.each do |attributes|
176
+ ingredients.build(
177
+ role: attributes[:role],
178
+ type: Alchemy::Ingredient.normalize_type(attributes[:type]),
179
+ )
172
180
  end
173
181
  end
174
182
  end
@@ -46,7 +46,9 @@ module Alchemy
46
46
  # Length of characters after the text will be cut off.
47
47
  #
48
48
  def preview_text(maxlength = 60)
49
- preview_text_from_preview_content(maxlength) || preview_text_from_nested_elements(maxlength)
49
+ preview_text_from_preview_ingredient(maxlength) ||
50
+ preview_text_from_preview_content(maxlength) ||
51
+ preview_text_from_nested_elements(maxlength)
50
52
  end
51
53
 
52
54
  # Generates a preview text containing Element#display_name and Element#preview_text.
@@ -94,6 +96,17 @@ module Alchemy
94
96
  @_preview_content ||= contents.detect(&:preview_content?) || contents.first
95
97
  end
96
98
 
99
+ # The ingredient that's used for element's preview text.
100
+ #
101
+ # It tries to find one of element's ingredients that is defined +as_element_title+.
102
+ # Takes element's first ingredient if no ingredient is defined +as_element_title+.
103
+ #
104
+ # @return (Alchemy::Ingredient)
105
+ #
106
+ def preview_ingredient
107
+ @_preview_ingredient ||= ingredients.detect(&:preview_ingredient?) || ingredients.first
108
+ end
109
+
97
110
  private
98
111
 
99
112
  def preview_text_from_nested_elements(maxlength)
@@ -105,6 +118,10 @@ module Alchemy
105
118
  def preview_text_from_preview_content(maxlength)
106
119
  preview_content.try!(:preview_text, maxlength)
107
120
  end
121
+
122
+ def preview_text_from_preview_ingredient(maxlength)
123
+ preview_ingredient&.preview_text(maxlength)
124
+ end
108
125
  end
109
126
  end
110
127
  end
@@ -267,20 +267,6 @@ module Alchemy
267
267
  "alchemy/elements/#{name}"
268
268
  end
269
269
 
270
- # Returns the key that's taken for cache path.
271
- #
272
- # Uses the page's +published_at+ value that's updated when the user publishes the page.
273
- #
274
- # If the page is the current preview it uses the element's updated_at value as cache key.
275
- #
276
- def cache_key
277
- if Page.current_preview == page
278
- "alchemy/elements/#{id}-#{updated_at}"
279
- else
280
- "alchemy/elements/#{id}-#{page.published_at}"
281
- end
282
- end
283
-
284
270
  # A collection of element names that can be nested inside this element.
285
271
  def nestable_elements
286
272
  definition.fetch("nestable_elements", [])
@@ -6,12 +6,13 @@ module Alchemy
6
6
 
7
7
  include Hints
8
8
 
9
- self.abstract_class = true
10
9
  self.table_name = "alchemy_ingredients"
11
10
 
12
11
  belongs_to :element, touch: true, class_name: "Alchemy::Element", inverse_of: :ingredients
13
12
  belongs_to :related_object, polymorphic: true, optional: true
14
13
 
14
+ before_validation(on: :create) { self.value ||= default_value }
15
+
15
16
  validates :type, presence: true
16
17
  validates :role, presence: true
17
18
 
@@ -33,32 +34,6 @@ module Alchemy
33
34
  scope :videos, -> { where(type: "Alchemy::Ingredients::Video") }
34
35
 
35
36
  class << self
36
- # Builds concrete ingredient class as described in the +elements.yml+
37
- def build(attributes = {})
38
- element = attributes[:element]
39
- raise ArgumentError, "No element given. Please pass element in attributes." if element.nil?
40
- raise ArgumentError, "No role given. Please pass role in attributes." if attributes[:role].nil?
41
-
42
- definition = element.ingredient_definition_for(attributes[:role])
43
- if definition.nil?
44
- raise DefinitionError,
45
- "No definition found for #{attributes[:role]}. Please define #{attributes[:role]} on #{element[:name]}."
46
- end
47
-
48
- ingredient_class = Ingredient.ingredient_class_by_type(definition[:type])
49
- ingredient_class.new(
50
- type: Ingredient.normalize_type(definition[:type]),
51
- value: default_value(definition),
52
- role: definition[:role],
53
- element: element,
54
- )
55
- end
56
-
57
- # Creates concrete ingredient class as described in the +elements.yml+
58
- def create(attributes = {})
59
- build(attributes).tap(&:save)
60
- end
61
-
62
37
  # Defines getter and setter method aliases for related object
63
38
  #
64
39
  # @param [String|Symbol] The name of the alias
@@ -78,20 +53,6 @@ module Alchemy
78
53
  end
79
54
  end
80
55
 
81
- # Returns an ingredient class by type
82
- #
83
- # Raises ArgumentError if there is no such class in the
84
- # +Alchemy::Ingredients+ module namespace.
85
- #
86
- # If you add custom ingredient class,
87
- # put them in the +Alchemy::Ingredients+ module namespace
88
- #
89
- # @param [String] The ingredient class name to constantize
90
- # @return [Class]
91
- def ingredient_class_by_type(ingredient_type)
92
- Alchemy::Ingredients.const_get(ingredient_type.to_s.classify.demodulize)
93
- end
94
-
95
56
  # Modulize ingredient type
96
57
  #
97
58
  # Makes sure the passed ingredient type is in the +Alchemy::Ingredients+
@@ -112,22 +73,6 @@ module Alchemy
112
73
  default: Alchemy.t("ingredient_roles.#{role}", default: role.humanize),
113
74
  )
114
75
  end
115
-
116
- private
117
-
118
- # Returns the default value from ingredient definition
119
- #
120
- # If the value is a symbol it gets passed through i18n
121
- # inside the +alchemy.default_ingredient_texts+ scope
122
- def default_value(definition)
123
- default = definition[:default]
124
- case default
125
- when Symbol
126
- Alchemy.t(default, scope: :default_ingredient_texts)
127
- else
128
- default
129
- end
130
- end
131
76
  end
132
77
 
133
78
  # Compatibility method for access from element
@@ -173,11 +118,6 @@ module Alchemy
173
118
  value.to_s[0..maxlength - 1]
174
119
  end
175
120
 
176
- # Cross DB adapter data accessor that works
177
- def data
178
- @_data ||= (self[:data] || {}).with_indifferent_access
179
- end
180
-
181
121
  # The path to the view partial of the ingredient
182
122
  # @return [String]
183
123
  def to_partial_path
@@ -210,10 +150,29 @@ module Alchemy
210
150
  false
211
151
  end
212
152
 
153
+ # @return [Boolean]
154
+ def preview_ingredient?
155
+ !!definition[:as_element_title]
156
+ end
157
+
213
158
  private
214
159
 
215
160
  def hint_translation_attribute
216
161
  role
217
162
  end
163
+
164
+ # Returns the default value from ingredient definition
165
+ #
166
+ # If the value is a symbol it gets passed through i18n
167
+ # inside the +alchemy.default_ingredient_texts+ scope
168
+ def default_value
169
+ default = definition[:default]
170
+ case default
171
+ when Symbol
172
+ Alchemy.t(default, scope: :default_ingredient_texts)
173
+ else
174
+ default
175
+ end
176
+ end
218
177
  end
219
178
  end