alchemy_cms 6.0.0.b1 → 6.0.0.b2

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/CHANGELOG.md +21 -0
  3. data/alchemy_cms.gemspec +1 -1
  4. data/app/assets/stylesheets/alchemy/archive.scss +14 -0
  5. data/app/controllers/alchemy/admin/attachments_controller.rb +3 -3
  6. data/app/controllers/alchemy/admin/pages_controller.rb +1 -9
  7. data/app/controllers/alchemy/admin/pictures_controller.rb +21 -8
  8. data/app/controllers/alchemy/admin/resources_controller.rb +84 -10
  9. data/app/controllers/alchemy/api/elements_controller.rb +12 -8
  10. data/app/controllers/alchemy/api/pages_controller.rb +4 -2
  11. data/app/models/alchemy/attachment.rb +24 -7
  12. data/app/models/alchemy/element/presenters.rb +18 -1
  13. data/app/models/alchemy/ingredient.rb +5 -0
  14. data/app/models/alchemy/page.rb +10 -1
  15. data/app/models/alchemy/page/page_scopes.rb +4 -0
  16. data/app/models/alchemy/picture.rb +14 -38
  17. data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +1 -1
  18. data/app/views/alchemy/admin/attachments/index.html.erb +2 -3
  19. data/app/views/alchemy/admin/pages/_toolbar.html.erb +1 -1
  20. data/app/views/alchemy/admin/pages/index.html.erb +2 -9
  21. data/app/views/alchemy/admin/partials/_search_form.html.erb +9 -0
  22. data/app/views/alchemy/admin/pictures/_archive.html.erb +1 -1
  23. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -1
  24. data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +4 -2
  25. data/app/views/alchemy/admin/pictures/index.html.erb +8 -3
  26. data/app/views/alchemy/admin/resources/_filter.html.erb +12 -0
  27. data/app/views/alchemy/admin/resources/_filter_bar.html.erb +14 -17
  28. data/app/views/alchemy/admin/resources/_form.html.erb +2 -0
  29. data/app/views/alchemy/admin/resources/_table_header.html.erb +15 -0
  30. data/app/views/alchemy/admin/resources/index.html.erb +3 -11
  31. data/config/locales/alchemy.en.yml +85 -49
  32. data/lib/alchemy/forms/builder.rb +21 -1
  33. data/lib/alchemy/resource_filter.rb +40 -0
  34. data/lib/alchemy/resources_helper.rb +1 -16
  35. data/lib/alchemy/test_support/shared_ingredient_examples.rb +21 -3
  36. data/lib/alchemy/upgrader/tasks/ingredients_migrator.rb +14 -6
  37. data/lib/alchemy/version.rb +1 -1
  38. data/lib/alchemy_cms.rb +1 -0
  39. data/lib/generators/alchemy/menus/templates/node.html.erb +1 -1
  40. data/lib/generators/alchemy/menus/templates/node.html.haml +1 -1
  41. data/lib/generators/alchemy/menus/templates/node.html.slim +1 -1
  42. data/lib/generators/alchemy/menus/templates/wrapper.html.erb +1 -1
  43. data/lib/generators/alchemy/menus/templates/wrapper.html.haml +1 -1
  44. data/lib/generators/alchemy/menus/templates/wrapper.html.slim +1 -1
  45. data/package.json +1 -1
  46. metadata +10 -9
  47. data/app/views/alchemy/admin/attachments/_filter_bar.html.erb +0 -29
  48. data/app/views/alchemy/admin/pictures/_filter_bar.html.erb +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f491f19d3f42a56aa03e0a82724e1473413c926f9bb9a823f909726415bb844
4
- data.tar.gz: 42c4841d037f5d351305b4e2c97fdc9298a4b886d51b666436c712f1c9c7ce80
3
+ metadata.gz: 50e6a89657c457ab3061bdc6d2cff1eba7ab25856249071e1bbe8c7684e18055
4
+ data.tar.gz: 282dd5e51249513e14309c4d98a09822b2fd7d038f8494f3313e00bc1026f8df
5
5
  SHA512:
6
- metadata.gz: fbb3af41e4d5a9715507f87d38d87f0dea4ceb9c62750721587ce14a7e7aa32f89b7e044fac5b14ab68781539f80b16297cce668b0bef76eb96b099c157b751c
7
- data.tar.gz: fdd0443e75620528d6434eeb13fb675db04f3a91911b92f66122df83c963a737509a99d968ddd48189195340ea00e06310919d3ba201906291088c66ee5ec455
6
+ metadata.gz: 54fc70c8b8ba0305d79d3e6309d4dab7a0a050a3aad1d06014fd3f38f9af34f81970a68358a7c3c222d2d740896dccb905920e6a67ee43bfb226d925916b67e6
7
+ data.tar.gz: 5d5c5ea2794e1f3c2e84a6b99f4eeb07fe284e98dca598c93361ed5f977b3b4b0e966372f2bba1c32f6b5bf21f8f94ec6cb68bd7b375f6bfb899b295bc724e11
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## 6.0.0.b2 (2021-08-05)
2
+
3
+ ### Features
4
+
5
+ - Add datepicker simple form input [#2162](https://github.com/AlchemyCMS/alchemy_cms/pull/2162) ([tvdeyen](https://github.com/tvdeyen))
6
+ - Feature flexible resource filters [#2091](https://github.com/AlchemyCMS/alchemy_cms/pull/2091) ([robinboening](https://github.com/robinboening))
7
+
8
+ ### Changes
9
+
10
+ - Require alchemy/version [#2159](https://github.com/AlchemyCMS/alchemy_cms/pull/2159) ([tvdeyen](https://github.com/tvdeyen))
11
+ - Make ingredient examples usable without elements.yml [#2158](https://github.com/AlchemyCMS/alchemy_cms/pull/2158) ([tvdeyen](https://github.com/tvdeyen))
12
+ - Fix ingredient migrator (again) [#2155](https://github.com/AlchemyCMS/alchemy_cms/pull/2155) ([tvdeyen](https://github.com/tvdeyen))
13
+ - Respect additional essence attributes during ingredients migration [#2154](https://github.com/AlchemyCMS/alchemy_cms/pull/2154) ([tvdeyen](https://github.com/tvdeyen))
14
+ - Improve cache key defaults for menus [#2153](https://github.com/AlchemyCMS/alchemy_cms/pull/2153) ([oneiros](https://github.com/oneiros))
15
+ - Make element preview text work with ingredients [#2152](https://github.com/AlchemyCMS/alchemy_cms/pull/2152) ([tvdeyen](https://github.com/tvdeyen))
16
+ - Do not leak all records for guest users in API controllers [#2145](https://github.com/AlchemyCMS/alchemy_cms/pull/2145) ([tvdeyen](https://github.com/tvdeyen))
17
+
18
+ ### Misc
19
+
20
+ - [ruby - main] Upgrade shoulda-matchers to version 5.0.0 [#2161](https://github.com/AlchemyCMS/alchemy_cms/pull/2161) ([depfu](https://github.com/apps/depfu))
21
+
1
22
  ## 6.0.0.b1 (2021-07-05)
2
23
 
3
24
  ### Features
data/alchemy_cms.gemspec CHANGED
@@ -65,7 +65,7 @@ Gem::Specification.new do |gem|
65
65
  gem.add_development_dependency "simplecov", ["~> 0.20"]
66
66
  gem.add_development_dependency "webdrivers", ["~> 4.0"]
67
67
  gem.add_development_dependency "webmock", ["~> 3.3"]
68
- gem.add_development_dependency "shoulda-matchers", ["~> 4.0"]
68
+ gem.add_development_dependency "shoulda-matchers", ["~> 5.0"]
69
69
  gem.add_development_dependency "timecop", ["~> 0.9"]
70
70
 
71
71
  gem.post_install_message = <<~MSG
@@ -2,6 +2,20 @@
2
2
  padding: 2 * $default-padding;
3
3
  }
4
4
 
5
+ .applied-filter {
6
+ display: inline-block;
7
+ padding: 2px 6px;
8
+ border-radius: $default-border-radius;
9
+ border: 1px solid $default-border-color;
10
+ white-space: nowrap;
11
+ font-weight: normal;
12
+
13
+ .dismiss-filter {
14
+ position: relative;
15
+ top: -1px;
16
+ }
17
+ }
18
+
5
19
  .resources-table-wrapper {
6
20
  padding-bottom: 60px;
7
21
 
@@ -21,8 +21,8 @@ module Alchemy
21
21
  @attachments = @attachments.tagged_with(search_filter_params[:tagged_with])
22
22
  end
23
23
 
24
- if search_filter_params[:file_type].present?
25
- @attachments = @attachments.with_file_type(search_filter_params[:file_type])
24
+ if search_filter_params[:filter].present?
25
+ @attachments = apply_filters(@attachments)
26
26
  end
27
27
 
28
28
  @attachments = @attachments
@@ -77,8 +77,8 @@ module Alchemy
77
77
  def search_filter_params
78
78
  @_search_filter_params ||= params.except(*COMMON_SEARCH_FILTER_EXCLUDES + [:attachment]).permit(
79
79
  *common_search_filter_includes + [
80
- :file_type,
81
80
  :form_field_id,
81
+ :content_id,
82
82
  ]
83
83
  )
84
84
  end
@@ -47,11 +47,7 @@ module Alchemy
47
47
  end
48
48
 
49
49
  if search_filter_params[:filter].present?
50
- items = items.public_send(sanitized_filter_params)
51
- end
52
-
53
- if search_filter_params[:page_layout].present?
54
- items = items.where(page_layout: search_filter_params[:page_layout])
50
+ items = apply_filters(items)
55
51
  end
56
52
 
57
53
  items = items.page(params[:page] || 1).per(items_per_page)
@@ -244,10 +240,6 @@ module Alchemy
244
240
  @_resource_handler ||= Alchemy::Resource.new(controller_path, alchemy_module, Alchemy::Page)
245
241
  end
246
242
 
247
- def common_search_filter_includes
248
- super.push(:page_layout, :view)
249
- end
250
-
251
243
  def set_view
252
244
  @view = params[:view] || session[:alchemy_pages_view] || "tree"
253
245
  session[:alchemy_pages_view] = @view
@@ -21,12 +21,7 @@ module Alchemy
21
21
 
22
22
  def index
23
23
  @query = Picture.ransack(search_filter_params[:q])
24
- @pictures = Picture.search_by(
25
- search_filter_params,
26
- @query,
27
- items_per_page,
28
- )
29
- @pictures = @pictures.includes(:thumbs)
24
+ @pictures = filtered_pictures.includes(:thumbs)
30
25
 
31
26
  if in_overlay?
32
27
  archive_overlay
@@ -34,9 +29,11 @@ module Alchemy
34
29
  end
35
30
 
36
31
  def show
37
- @previous = @picture.previous(params)
38
- @next = @picture.next(params)
32
+ @query = Picture.ransack(params[:q])
33
+ @previous = filtered_pictures.where("name < ?", @picture.name).last
34
+ @next = filtered_pictures.where("name > ?", @picture.name).first
39
35
  @assignments = @picture.essence_pictures.joins(content: { element: :page })
36
+
40
37
  render action: "show"
41
38
  end
42
39
 
@@ -130,6 +127,22 @@ module Alchemy
130
127
  redirect_to_index
131
128
  end
132
129
 
130
+ def filtered_pictures
131
+ pictures = @query.result
132
+
133
+ if params[:tagged_with].present?
134
+ pictures = pictures.tagged_with(params[:tagged_with])
135
+ end
136
+
137
+ if search_filter_params[:filter].present?
138
+ pictures = apply_filters(pictures)
139
+ end
140
+
141
+ pictures = pictures.page(params[:page] || 1).per(items_per_page)
142
+
143
+ pictures.order(:name)
144
+ end
145
+
133
146
  def items_per_page
134
147
  if in_overlay?
135
148
  case @size
@@ -3,6 +3,7 @@
3
3
  require "csv"
4
4
  require "alchemy/resource"
5
5
  require "alchemy/resources_helper"
6
+ require "alchemy/resource_filter"
6
7
 
7
8
  module Alchemy
8
9
  module Admin
@@ -13,7 +14,8 @@ module Alchemy
13
14
 
14
15
  helper Alchemy::ResourcesHelper, TagsHelper
15
16
  helper_method :resource_handler, :search_filter_params,
16
- :items_per_page, :items_per_page_options
17
+ :items_per_page, :items_per_page_options, :resource_has_filters,
18
+ :resource_filters_for_select
17
19
 
18
20
  before_action :load_resource,
19
21
  only: [:show, :edit, :update, :destroy]
@@ -34,7 +36,7 @@ module Alchemy
34
36
  end
35
37
 
36
38
  if search_filter_params[:filter].present?
37
- items = items.public_send(sanitized_filter_params)
39
+ items = apply_filters(items)
38
40
  end
39
41
 
40
42
  respond_to do |format|
@@ -90,8 +92,79 @@ module Alchemy
90
92
  @_resource_handler ||= Alchemy::Resource.new(controller_path, alchemy_module)
91
93
  end
92
94
 
95
+ def resource_has_filters
96
+ resource_model.respond_to?(:alchemy_resource_filters)
97
+ end
98
+
99
+ def resource_has_deprecated_filters
100
+ resource_model.alchemy_resource_filters.any? { |f| !f.is_a?(Hash) }
101
+ end
102
+
103
+ def resource_filters
104
+ return unless resource_has_filters
105
+
106
+ @_resource_filters ||= deprecated_resource_filters || resource_model.alchemy_resource_filters
107
+ end
108
+
109
+ def resource_filters_for_select
110
+ resource_filters.map do |filter|
111
+ ResourceFilter.new(filter, resource_handler.resource_name)
112
+ end
113
+ end
114
+
93
115
  protected
94
116
 
117
+ def deprecated_resource_filters
118
+ if resource_has_deprecated_filters
119
+ Alchemy::Deprecation.warn(
120
+ "#{resource_model}.alchemy_resource_filters is using a legacy data structure. " \
121
+ "Please use an Array of Hashes instead. i.e. [{ name: 'foo', values: ['bar', 'baz'] }, ...] " \
122
+ "where values are scopes. With Alchemy 6.1 only the new structure will be supported."
123
+ )
124
+
125
+ @_resource_filters ||= [
126
+ {
127
+ name: :misc,
128
+ values: resource_model.alchemy_resource_filters,
129
+ },
130
+ ]
131
+ end
132
+ end
133
+
134
+ def apply_filters(items)
135
+ sanitize_filter_params!
136
+
137
+ search_filter_params[:filter].each do |filter|
138
+ if argument_scope_filter?(filter)
139
+ items = items.public_send(filter[0], filter[1])
140
+ elsif simple_scope_filter?(filter)
141
+ items = items.public_send(filter[1])
142
+ else
143
+ raise "Can't apply filter #{filter[0]}. Either the name or the values must be defined as class methods / scopes on the model."
144
+ end
145
+ end
146
+
147
+ items
148
+ end
149
+
150
+ def simple_scope_filter?(filter)
151
+ resource_model.respond_to?(filter[1])
152
+ end
153
+
154
+ def argument_scope_filter?(filter)
155
+ resource_model.respond_to?(filter[0])
156
+ end
157
+
158
+ def sanitize_filter_params!
159
+ search_filter_params[:filter].reject! do |_, v|
160
+ eligible_resource_filter_values.exclude?(v)
161
+ end
162
+ end
163
+
164
+ def eligible_resource_filter_values
165
+ resource_filters.map(&:values).flatten
166
+ end
167
+
95
168
  # Returns a translated +flash[:notice]+.
96
169
  # The key should look like "Modelname successfully created|updated|destroyed."
97
170
  def flash_notice_for_resource_action(action = params[:action])
@@ -136,27 +209,28 @@ module Alchemy
136
209
  params.require(resource_handler.namespaced_resource_name).permit!
137
210
  end
138
211
 
139
- def sanitized_filter_params
140
- resource_model.alchemy_resource_filters.detect do |filter|
141
- filter == search_filter_params[:filter]
142
- end || :all
143
- end
144
-
145
212
  def search_filter_params
146
213
  @_search_filter_params ||= params.except(*COMMON_SEARCH_FILTER_EXCLUDES).permit(*common_search_filter_includes).to_h
147
214
  end
148
215
 
149
216
  def common_search_filter_includes
150
- [
217
+ search_filters = [
151
218
  { q: [
152
219
  resource_handler.search_field_name,
153
220
  :s,
154
221
  ] },
155
222
  :tagged_with,
156
- :filter,
157
223
  :page,
158
224
  :per_page,
159
225
  ]
226
+
227
+ if resource_has_filters
228
+ search_filters << {
229
+ filter: resource_filters.map { |f| f[:name] },
230
+ }
231
+ end
232
+
233
+ search_filters
160
234
  end
161
235
 
162
236
  def items_per_page
@@ -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
@@ -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
@@ -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