alchemy_cms 8.0.11 → 8.0.12
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.
- checksums.yaml +4 -4
- data/app/controllers/alchemy/admin/attachments_controller.rb +9 -0
- data/app/controllers/alchemy/admin/pictures_controller.rb +23 -2
- data/app/models/alchemy/attachment.rb +40 -0
- data/app/models/concerns/alchemy/relatable_resource.rb +14 -4
- data/app/views/alchemy/admin/attachments/_files_list.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/_picture.html.erb +1 -1
- data/lib/alchemy/configuration.rb +2 -0
- data/lib/alchemy/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d289053cf46130b3b2f6e4b32a27d82a2c0e09f1aad6ec8c1c89a394c44e3f0
|
|
4
|
+
data.tar.gz: 430300c6dc9d6cff90eaaea15f723e3ace2aa494fd87e0773416437307b3d1b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 383898d1e8f5e5b5abfc7139f8bcdb228d42acc9956bdb669ad197c81b11cc43c754c6c4f51d527e6f3202bf8cda100ee388f325f15905793c795198e7f411ae
|
|
7
|
+
data.tar.gz: f2a3c4f33af5260914e4b68665209d0f99854c8f10251878e49115043ebffdcbfa32fab4584d075f2229ac3775c09d0b41877e6b5c8eab00d34ef945cabaaeae
|
|
@@ -41,6 +41,15 @@ module Alchemy
|
|
|
41
41
|
.page(params[:page] || 1)
|
|
42
42
|
.per(items_per_page)
|
|
43
43
|
|
|
44
|
+
# Preload deletable ids for the current page in a single query so the
|
|
45
|
+
# view can decide which delete buttons to enable without calling
|
|
46
|
+
# +deletable?+ (a two-query check) per row. We pass the already-loaded
|
|
47
|
+
# ids (via +map(&:id)+) rather than the relation itself, because passing
|
|
48
|
+
# a paginated relation produces +IN (SELECT ... LIMIT n)+, which older
|
|
49
|
+
# MariaDB versions reject.
|
|
50
|
+
@deletable_attachment_ids =
|
|
51
|
+
Attachment.where(id: @attachments.map(&:id)).deletable.pluck(:id).to_set
|
|
52
|
+
|
|
44
53
|
if in_overlay?
|
|
45
54
|
archive_overlay
|
|
46
55
|
end
|
|
@@ -10,6 +10,8 @@ module Alchemy
|
|
|
10
10
|
|
|
11
11
|
helper "alchemy/admin/tags"
|
|
12
12
|
|
|
13
|
+
before_action :load_pictures, only: :index
|
|
14
|
+
|
|
13
15
|
before_action :load_resource,
|
|
14
16
|
only: [:edit, :update, :url, :destroy]
|
|
15
17
|
|
|
@@ -21,6 +23,12 @@ module Alchemy
|
|
|
21
23
|
@picture = Picture.find(params[:id])
|
|
22
24
|
end
|
|
23
25
|
|
|
26
|
+
# Preload deletable ids for the current page (index) or the current
|
|
27
|
+
# resource (update, which re-renders the +_picture+ partial via a
|
|
28
|
+
# turbo stream). One query replaces the per-row +deletable?+ check
|
|
29
|
+
# that would otherwise fire for every picture the view renders.
|
|
30
|
+
before_action :load_deletable_picture_ids, only: [:index, :update]
|
|
31
|
+
|
|
24
32
|
add_alchemy_filter :by_file_format, type: :select, options: ->(query) do
|
|
25
33
|
Alchemy::Picture.file_formats(query.result)
|
|
26
34
|
end
|
|
@@ -32,8 +40,6 @@ module Alchemy
|
|
|
32
40
|
helper_method :picture_offset
|
|
33
41
|
|
|
34
42
|
def index
|
|
35
|
-
@pictures = filtered_pictures(page: params[:page])
|
|
36
|
-
|
|
37
43
|
if in_overlay?
|
|
38
44
|
archive_overlay
|
|
39
45
|
end
|
|
@@ -162,6 +168,21 @@ module Alchemy
|
|
|
162
168
|
|
|
163
169
|
private
|
|
164
170
|
|
|
171
|
+
def load_pictures
|
|
172
|
+
@pictures = filtered_pictures(page: params[:page])
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Preload deletable ids in a single query so the view can decide which
|
|
176
|
+
# delete buttons to enable without calling +deletable?+ (a two-query
|
|
177
|
+
# check) per row. We pass already-loaded ids (via +map(&:id)+) rather
|
|
178
|
+
# than the relation itself, because passing a paginated relation
|
|
179
|
+
# produces +IN (SELECT ... LIMIT n)+, which older MariaDB versions
|
|
180
|
+
# reject.
|
|
181
|
+
def load_deletable_picture_ids
|
|
182
|
+
ids = @pictures ? @pictures.map(&:id) : [@picture.id]
|
|
183
|
+
@deletable_picture_ids = Picture.where(id: ids).deletable.pluck(:id).to_set
|
|
184
|
+
end
|
|
185
|
+
|
|
165
186
|
def picture_offset
|
|
166
187
|
((params[:page] || 1).to_i - 1) * items_per_page
|
|
167
188
|
end
|
|
@@ -40,6 +40,33 @@ module Alchemy
|
|
|
40
40
|
scope :recent, -> { where("#{table_name}.created_at > ?", Time.current - 24.hours).order(:created_at) }
|
|
41
41
|
scope :without_tag, -> { left_outer_joins(:taggings).where(gutentag_taggings: {id: nil}) }
|
|
42
42
|
|
|
43
|
+
# Override +Alchemy::RelatableResource#deletable+ to also exclude
|
|
44
|
+
# attachments referenced from +/attachment/:id/download+ URLs inside
|
|
45
|
+
# ingredient values (e.g. Richtext markup, Link ingredients, raw Html).
|
|
46
|
+
# Those URLs are written by the file tab of the link dialog and are
|
|
47
|
+
# not tracked via the polymorphic +related_object+ association, so the
|
|
48
|
+
# base scope cannot see them.
|
|
49
|
+
#
|
|
50
|
+
# Uses a correlated +NOT EXISTS+ subquery that builds the per-row LIKE
|
|
51
|
+
# pattern with +Arel::Nodes::Concat+, which compiles to +||+ on
|
|
52
|
+
# SQLite/PostgreSQL and +CONCAT()+ on MySQL.
|
|
53
|
+
scope :deletable, -> do
|
|
54
|
+
ingredients = Alchemy::Ingredient.arel_table
|
|
55
|
+
pattern = Arel::Nodes::Concat.new(
|
|
56
|
+
Arel::Nodes::Concat.new(
|
|
57
|
+
Arel::Nodes.build_quoted("%/attachment/"),
|
|
58
|
+
arel_table[:id]
|
|
59
|
+
),
|
|
60
|
+
Arel::Nodes.build_quoted("/download%")
|
|
61
|
+
)
|
|
62
|
+
referenced = ingredients
|
|
63
|
+
.project(1)
|
|
64
|
+
.where(ingredients[:value].matches(pattern))
|
|
65
|
+
|
|
66
|
+
where("#{table_name}.id NOT IN (#{RelatableResource::RELATED_INGREDIENTS_SUBQUERY})", type: name)
|
|
67
|
+
.where.not(referenced.exists)
|
|
68
|
+
end
|
|
69
|
+
|
|
43
70
|
# We need to define this method here to have it available in the validations below.
|
|
44
71
|
class << self
|
|
45
72
|
# The class used to generate URLs for attachments
|
|
@@ -112,6 +139,13 @@ module Alchemy
|
|
|
112
139
|
CGI.escape(file_name.gsub(/\.#{extension}$/, "").tr(".", " "))
|
|
113
140
|
end
|
|
114
141
|
|
|
142
|
+
# Override +Alchemy::RelatableResource#deletable?+ to also consider
|
|
143
|
+
# +/attachment/:id/download+ links inside ingredient values (e.g.
|
|
144
|
+
# Richtext markup, Link ingredients, raw Html).
|
|
145
|
+
def deletable?
|
|
146
|
+
super && !referenced_in_ingredient_value?
|
|
147
|
+
end
|
|
148
|
+
|
|
115
149
|
# Checks if the attachment is restricted, because it is attached on restricted pages only
|
|
116
150
|
def restricted?
|
|
117
151
|
pages.any? && pages.not_restricted.blank?
|
|
@@ -178,6 +212,12 @@ module Alchemy
|
|
|
178
212
|
end
|
|
179
213
|
end
|
|
180
214
|
|
|
215
|
+
def referenced_in_ingredient_value?
|
|
216
|
+
Alchemy::Ingredient
|
|
217
|
+
.where("value LIKE ?", "%/attachment/#{id}/download%")
|
|
218
|
+
.exists?
|
|
219
|
+
end
|
|
220
|
+
|
|
181
221
|
def set_name
|
|
182
222
|
self.name ||= convert_to_humanized_name(file_name, extension)
|
|
183
223
|
end
|
|
@@ -2,12 +2,22 @@ module Alchemy
|
|
|
2
2
|
module RelatableResource
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
|
+
# SQL subquery selecting +related_object_id+ values for ingredients that
|
|
6
|
+
# reference a given polymorphic type. Intended to be composed into a
|
|
7
|
+
# +NOT IN (...)+ clause by +deletable+ and any overrides in including
|
|
8
|
+
# classes. Takes one named bind :type - the polymorphic type name,
|
|
9
|
+
# typically the class name of the including model
|
|
10
|
+
# (e.g. +"Alchemy::Attachment"+).
|
|
11
|
+
RELATED_INGREDIENTS_SUBQUERY = <<~SQL.squish
|
|
12
|
+
SELECT related_object_id
|
|
13
|
+
FROM alchemy_ingredients
|
|
14
|
+
WHERE related_object_id IS NOT NULL
|
|
15
|
+
AND related_object_type = :type
|
|
16
|
+
SQL
|
|
17
|
+
|
|
5
18
|
included do
|
|
6
19
|
scope :deletable, -> do
|
|
7
|
-
where(
|
|
8
|
-
"#{table_name}.id NOT IN (SELECT related_object_id FROM alchemy_ingredients WHERE related_object_id IS NOT NULL AND related_object_type = ?)",
|
|
9
|
-
name
|
|
10
|
-
)
|
|
20
|
+
where("#{table_name}.id NOT IN (#{RELATED_INGREDIENTS_SUBQUERY})", type: name)
|
|
11
21
|
end
|
|
12
22
|
|
|
13
23
|
has_many :related_ingredients,
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
file_attribute: 'file' %>
|
|
62
62
|
<% end %>
|
|
63
63
|
<% table.with_action(:destroy) do |attachment| %>
|
|
64
|
-
<% if attachment.
|
|
64
|
+
<% if @deletable_attachment_ids.include?(attachment.id) %>
|
|
65
65
|
<sl-tooltip content="<%= Alchemy.t(:delete_file) %>">
|
|
66
66
|
<%= link_to_confirm_dialog render_icon(:minus),
|
|
67
67
|
Alchemy.t(:confirm_to_delete_file),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<span class="picture_tool select">
|
|
3
3
|
<%= check_box_tag "picture_ids[]", picture.id %>
|
|
4
4
|
</span>
|
|
5
|
-
<% if picture.
|
|
5
|
+
<% if @deletable_picture_ids.include?(picture.id) && can?(:destroy, picture) %>
|
|
6
6
|
<div class="picture_tool delete">
|
|
7
7
|
<sl-tooltip content="<%= Alchemy.t('Delete image') %>">
|
|
8
8
|
<%= link_to_confirm_dialog(
|
data/lib/alchemy/version.rb
CHANGED