inline_forms 8.1.47 → 8.1.53
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/.gitignore +3 -0
- data/CHANGELOG.md +149 -0
- data/Gemfile +3 -0
- data/README.rdoc +47 -0
- data/app/assets/stylesheets/inline_forms/inline_forms.scss +34 -0
- data/app/controllers/concerns/file_slots_concern.rb +211 -0
- data/app/controllers/inline_forms/file_trash_controller.rb +115 -0
- data/app/controllers/inline_forms_controller.rb +82 -16
- data/app/helpers/inline_forms_file_trash_helper.rb +131 -0
- data/app/helpers/inline_forms_helper.rb +5 -1
- data/app/models/concerns/inline_forms/stored_files.rb +350 -0
- data/app/models/inline_forms/current.rb +8 -0
- data/app/models/inline_forms/file_trash_sweep.rb +37 -0
- data/app/models/inline_forms/trashed_file.rb +205 -0
- data/app/views/inline_forms/_file_slot.html.erb +39 -0
- data/app/views/inline_forms/_header.html.erb +3 -0
- data/app/views/inline_forms/_model_top_bar.html.erb +3 -4
- data/app/views/inline_forms/_show.html.erb +6 -0
- data/app/views/inline_forms/_versions.html.erb +4 -4
- data/app/views/inline_forms/_versions_file_entry.html.erb +36 -0
- data/app/views/inline_forms/_versions_list.html.erb +11 -2
- data/app/views/inline_forms/file_field_frame.html.erb +22 -0
- data/app/views/inline_forms/file_trash/index.html.erb +112 -0
- data/app/views/inline_forms/file_trash_panel.html.erb +90 -0
- data/app/views/layouts/inline_forms.html.erb +5 -1
- data/lib/generators/inline_forms/file_trash_generator.rb +36 -0
- data/lib/generators/inline_forms/templates/create_inline_forms_file_trash.rb.erb +41 -0
- data/lib/generators/inline_forms_addto_generator.rb +47 -0
- data/lib/generators/inline_forms_generator.rb +19 -2
- data/lib/generators/templates/model.erb +4 -0
- data/lib/inline_forms/form_element_from_callee.rb +25 -0
- data/lib/inline_forms/form_elements/simple_file_field_helper.rb +30 -2
- data/lib/inline_forms/gem_files.rb +5 -0
- data/lib/inline_forms/version.rb +1 -1
- data/lib/inline_forms.rb +55 -0
- data/lib/locales/inline_forms.en.yml +87 -0
- data/lib/locales/inline_forms.nl.yml +87 -3
- data/lib/tasks/inline_forms_files.rake +61 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/controllers/documents_controller.rb +4 -0
- data/test/dummy/app/controllers/dossiers_controller.rb +4 -0
- data/test/dummy/app/models/ability.rb +15 -0
- data/test/dummy/app/models/document.rb +33 -0
- data/test/dummy/app/models/dossier.rb +38 -0
- data/test/dummy/config/application.rb +4 -3
- data/test/dummy/config/routes.rb +13 -0
- data/test/inline_forms_addto_generator_test.rb +47 -0
- data/test/inline_forms_generator_test.rb +24 -0
- data/test/integration/field_authorization_test.rb +131 -0
- data/test/integration/file_slots_test.rb +574 -0
- data/test/integration/layout_title_test.rb +56 -0
- data/test/integration/model_top_bar_test.rb +9 -0
- data/test/integration/revert_skipped_columns_test.rb +59 -0
- data/test/integration_test_helper.rb +62 -0
- metadata +34 -3
- data/.claude/settings.local.json +0 -9
- data/CLAUDE.md +0 -68
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
|
+
|
|
6
|
+
module InlineForms
|
|
7
|
+
module Generators
|
|
8
|
+
# `rails g inline_forms:file_trash`
|
|
9
|
+
#
|
|
10
|
+
# Writes the migration for the file trash of declared file slots
|
|
11
|
+
# (InlineForms::StoredFiles): inline_forms_trashed_files (one row per
|
|
12
|
+
# removed / replaced / uploaded file event, bytes until purged) and
|
|
13
|
+
# inline_forms_file_trash_sweeps (the retention sweep's heartbeat). The
|
|
14
|
+
# installer runs it for new apps; existing apps run it once by hand.
|
|
15
|
+
class FileTrashGenerator < Rails::Generators::Base
|
|
16
|
+
include Rails::Generators::Migration
|
|
17
|
+
|
|
18
|
+
source_root File.expand_path("templates", __dir__)
|
|
19
|
+
|
|
20
|
+
def self.next_migration_number(dirname)
|
|
21
|
+
# Collision-free against migrations generated in the same second
|
|
22
|
+
# (same policy as inline_forms_addto since 8.1.41).
|
|
23
|
+
now = Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
24
|
+
existing = Dir.glob(File.join(dirname, "*.rb")).map { |f| File.basename(f)[/\A\d+/] }.compact
|
|
25
|
+
[ now, *existing.map(&:to_s) ].max.then do |highest|
|
|
26
|
+
highest == now ? now : (highest.to_i + 1).to_s
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create_migration_file
|
|
31
|
+
migration_template "create_inline_forms_file_trash.rb.erb",
|
|
32
|
+
"db/migrate/create_inline_forms_file_trash.rb"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# File trash for declared file slots (inline_forms, InlineForms::StoredFiles).
|
|
2
|
+
class CreateInlineFormsFileTrash < ActiveRecord::Migration[8.1]
|
|
3
|
+
def change
|
|
4
|
+
# Trashed bytes can be as large as the slot's own column: LONGBLOB on
|
|
5
|
+
# MySQL (a plain BLOB stops at 64 KB); a plain binary elsewhere (SQLite
|
|
6
|
+
# and PostgreSQL have no size variants).
|
|
7
|
+
long_blob = connection.adapter_name.match?(/mysql|trilogy/i) ? { size: :long } : {}
|
|
8
|
+
|
|
9
|
+
create_table :inline_forms_trashed_files do |t|
|
|
10
|
+
t.string :record_type, null: false
|
|
11
|
+
t.bigint :record_id, null: false
|
|
12
|
+
t.string :attribute_name, null: false
|
|
13
|
+
t.string :filename
|
|
14
|
+
t.string :content_type
|
|
15
|
+
t.bigint :byte_size
|
|
16
|
+
t.string :checksum, limit: 64
|
|
17
|
+
t.binary :data, **long_blob
|
|
18
|
+
t.string :reason, null: false
|
|
19
|
+
t.boolean :redacted, null: false, default: false
|
|
20
|
+
t.bigint :trashed_by_id
|
|
21
|
+
t.datetime :trashed_at, null: false
|
|
22
|
+
t.bigint :restored_by_id
|
|
23
|
+
t.datetime :restored_at
|
|
24
|
+
t.bigint :purged_by_id
|
|
25
|
+
t.datetime :purged_at
|
|
26
|
+
t.datetime :purge_after
|
|
27
|
+
t.timestamps
|
|
28
|
+
end
|
|
29
|
+
add_index :inline_forms_trashed_files, [ :record_type, :record_id, :attribute_name ],
|
|
30
|
+
name: "index_inline_forms_trashed_files_on_slot"
|
|
31
|
+
add_index :inline_forms_trashed_files, [ :purged_at, :restored_at, :purge_after ],
|
|
32
|
+
name: "index_inline_forms_trashed_files_on_state"
|
|
33
|
+
|
|
34
|
+
create_table :inline_forms_file_trash_sweeps do |t|
|
|
35
|
+
t.datetime :ran_at, null: false
|
|
36
|
+
t.integer :purged_count, null: false, default: 0
|
|
37
|
+
t.text :error
|
|
38
|
+
t.timestamps
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -117,6 +117,12 @@ class InlineFormsAddtoGenerator < Rails::Generators::NamedBase
|
|
|
117
117
|
|
|
118
118
|
if attribute.column_type == :belongs_to
|
|
119
119
|
@migration_lines << " add_reference :#{table_name}, :#{attribute.name}, foreign_key: true\n"
|
|
120
|
+
elsif attribute.type == :simple_file_field
|
|
121
|
+
# Declared file slot: filename + bytes (LONGBLOB on MySQL) + content type.
|
|
122
|
+
columns = InlineForms.file_slot_columns(attribute.name)
|
|
123
|
+
@migration_lines << " add_column :#{table_name}, :#{columns[:filename]}, :string\n"
|
|
124
|
+
@migration_lines << " add_column :#{table_name}, :#{columns[:data]}, :binary, **(connection.adapter_name.match?(/mysql|trilogy/i) ? { size: :long } : {})\n"
|
|
125
|
+
@migration_lines << " add_column :#{table_name}, :#{columns[:content_type]}, :string\n"
|
|
120
126
|
else
|
|
121
127
|
commenter = attribute.attribute_type == :unknown ? "#" : " "
|
|
122
128
|
@migration_lines << "#{commenter} add_column :#{table_name}, :#{attribute.name}, :#{attribute.column_type}\n"
|
|
@@ -158,6 +164,8 @@ class InlineFormsAddtoGenerator < Rails::Generators::NamedBase
|
|
|
158
164
|
inject_class_line!(" has_and_belongs_to_many :#{attribute.name}\n")
|
|
159
165
|
when :rich_text
|
|
160
166
|
inject_class_line!(" has_rich_text :#{attribute.name}\n")
|
|
167
|
+
when :simple_file_field
|
|
168
|
+
add_file_slot!(attribute)
|
|
161
169
|
end
|
|
162
170
|
|
|
163
171
|
next unless attribute.attribute?
|
|
@@ -182,6 +190,45 @@ class InlineFormsAddtoGenerator < Rails::Generators::NamedBase
|
|
|
182
190
|
|
|
183
191
|
private
|
|
184
192
|
|
|
193
|
+
# A :simple_file_field is a declared file slot: the model gets
|
|
194
|
+
# InlineForms::StoredFiles + `inline_forms_file :attr`, and the resource
|
|
195
|
+
# block in routes.rb gets InlineForms.file_routes(self) (when it can be
|
|
196
|
+
# found; otherwise a reminder).
|
|
197
|
+
def add_file_slot!(attribute)
|
|
198
|
+
content = File.read(File.join(destination_root, model_file_path))
|
|
199
|
+
include_line = " include InlineForms::StoredFiles\n"
|
|
200
|
+
macro = " inline_forms_file :#{attribute.name}\n"
|
|
201
|
+
return if content.include?(macro)
|
|
202
|
+
|
|
203
|
+
if content.include?(include_line)
|
|
204
|
+
inject_into_file model_file_path, macro, after: include_line
|
|
205
|
+
else
|
|
206
|
+
# One injection: inject_into_class puts each line right under the
|
|
207
|
+
# class line, so two calls would land the macro above the include.
|
|
208
|
+
inject_class_line!(include_line + macro)
|
|
209
|
+
end
|
|
210
|
+
add_file_routes!
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def add_file_routes!
|
|
214
|
+
routes = "config/routes.rb"
|
|
215
|
+
path = File.join(destination_root, routes)
|
|
216
|
+
return unless File.exist?(path)
|
|
217
|
+
|
|
218
|
+
content = File.read(path)
|
|
219
|
+
opener = content[/^(\s*)resources :#{table_name} do\n/]
|
|
220
|
+
if opener.nil?
|
|
221
|
+
say_status :reminder, "add InlineForms.file_routes(self) inside `resources :#{table_name} do ... end` in #{routes}", :yellow
|
|
222
|
+
return
|
|
223
|
+
end
|
|
224
|
+
block = content[content.index(opener)..][/\A.*?^\s*end$/m]
|
|
225
|
+
return if block&.include?("InlineForms.file_routes")
|
|
226
|
+
|
|
227
|
+
indent = opener[/\A\s*/]
|
|
228
|
+
inject_into_file routes, "#{indent} InlineForms.file_routes(self)\n", after: opener
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
|
|
185
232
|
def model_file_name
|
|
186
233
|
name.underscore
|
|
187
234
|
end
|
|
@@ -66,6 +66,7 @@ module InlineForms
|
|
|
66
66
|
@order = "\n"
|
|
67
67
|
@list_scopes = ""
|
|
68
68
|
@carrierwave_mounters = "\n"
|
|
69
|
+
@stored_files = String.new
|
|
69
70
|
@inline_forms_attribute_list = String.new
|
|
70
71
|
|
|
71
72
|
for attribute in attributes
|
|
@@ -94,6 +95,12 @@ module InlineForms
|
|
|
94
95
|
if attribute.type == :rich_text
|
|
95
96
|
@has_rich_text << ' has_rich_text :' + attribute.name + "\n"
|
|
96
97
|
end
|
|
98
|
+
# Declared file slot: bytes + content type in the model's own table,
|
|
99
|
+
# replace/remove/undo through the file trash (InlineForms::StoredFiles).
|
|
100
|
+
if attribute.type == :simple_file_field
|
|
101
|
+
@stored_files << " include InlineForms::StoredFiles\n" if @stored_files.empty?
|
|
102
|
+
@stored_files << ' inline_forms_file :' + attribute.name + "\n"
|
|
103
|
+
end
|
|
97
104
|
if attribute.name == '_presentation'
|
|
98
105
|
@presentation << " def _presentation\n" +
|
|
99
106
|
" \"#{attribute.type.to_s}\"\n" +
|
|
@@ -142,10 +149,11 @@ module InlineForms
|
|
|
142
149
|
|
|
143
150
|
def generate_resource_route
|
|
144
151
|
if @flag_create_resource_route
|
|
152
|
+
file_routes = file_slot_attributes.any? ? "\n InlineForms.file_routes(self)" : ""
|
|
145
153
|
route <<-ROUTE.strip_heredoc
|
|
146
154
|
resources :#{resource_name} do
|
|
147
155
|
post 'revert', :on => :member
|
|
148
|
-
get 'list_versions', :on => :member
|
|
156
|
+
get 'list_versions', :on => :member#{file_routes}
|
|
149
157
|
end
|
|
150
158
|
ROUTE
|
|
151
159
|
end
|
|
@@ -156,7 +164,12 @@ module InlineForms
|
|
|
156
164
|
@columns = String.new
|
|
157
165
|
|
|
158
166
|
for attribute in attributes
|
|
159
|
-
if attribute.
|
|
167
|
+
if attribute.type == :simple_file_field
|
|
168
|
+
columns = InlineForms.file_slot_columns(attribute.name)
|
|
169
|
+
@columns << ' t.string :' + columns[:filename] + "\n"
|
|
170
|
+
@columns << ' t.binary :' + columns[:data] + ", **(connection.adapter_name.match?(/mysql|trilogy/i) ? { size: :long } : {})\n"
|
|
171
|
+
@columns << ' t.string :' + columns[:content_type] + "\n"
|
|
172
|
+
elsif attribute.column_type == :image
|
|
160
173
|
@columns << ' t.string :' + attribute.name + "_file_name\n"
|
|
161
174
|
@columns << ' t.string :' + attribute.name + "_content_type\n"
|
|
162
175
|
@columns << ' t.integer :' + attribute.name + "_file_size\n"
|
|
@@ -230,6 +243,10 @@ module InlineForms
|
|
|
230
243
|
|
|
231
244
|
|
|
232
245
|
private
|
|
246
|
+
def file_slot_attributes
|
|
247
|
+
attributes.select { |attribute| attribute.type == :simple_file_field }
|
|
248
|
+
end
|
|
249
|
+
|
|
233
250
|
def model_file_name
|
|
234
251
|
name.underscore
|
|
235
252
|
end
|
|
@@ -6,6 +6,10 @@ class <%= name %> < ApplicationRecord
|
|
|
6
6
|
<%= @habtm if @habtm.length > 1 -%>
|
|
7
7
|
<%= @has_rich_text if @has_rich_text.length > 1 -%>
|
|
8
8
|
<%= @has_attached_files if @has_attached_files.length > 1 -%>
|
|
9
|
+
<% unless @stored_files.empty? -%>
|
|
10
|
+
|
|
11
|
+
<%= @stored_files -%>
|
|
12
|
+
<% end -%>
|
|
9
13
|
<% if @list_scopes && !@list_scopes.empty? -%>
|
|
10
14
|
|
|
11
15
|
<%= @list_scopes -%>
|
|
@@ -8,4 +8,29 @@ module InlineForms
|
|
|
8
8
|
s = s.sub(/\Ablock in /, "")
|
|
9
9
|
s.delete_suffix("_show")
|
|
10
10
|
end
|
|
11
|
+
|
|
12
|
+
# Form elements whose +*_show+ helper delegates to another element's, so
|
|
13
|
+
# their edit link carries the delegate's name (+__callee__+ of the inner
|
|
14
|
+
# helper) as +form_element+ instead of the declared one.
|
|
15
|
+
FORM_ELEMENT_DELEGATES = {
|
|
16
|
+
plain_text_area: :plain_text,
|
|
17
|
+
text_area: :rich_text
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# The +inline_forms_attribute_list+ row for a single-field request, or nil
|
|
21
|
+
# when +attribute+/+form_element+ (both from params) do not name a row the
|
|
22
|
+
# UI could have linked to. The controller uses this as a whitelist before
|
|
23
|
+
# it dispatches +send("#{form_element}_update")+ or touches the attribute.
|
|
24
|
+
def self.attribute_list_row_for(attribute_list, attribute, form_element)
|
|
25
|
+
attribute = attribute.to_s
|
|
26
|
+
form_element = form_element.to_s
|
|
27
|
+
return nil if attribute.empty? || form_element.empty?
|
|
28
|
+
|
|
29
|
+
attribute_list.find do |listed_attribute, declared, *|
|
|
30
|
+
next false unless listed_attribute.to_s == attribute
|
|
31
|
+
|
|
32
|
+
declared.to_s == form_element ||
|
|
33
|
+
FORM_ELEMENT_DELEGATES[declared.to_s.to_sym].to_s == form_element
|
|
34
|
+
end
|
|
35
|
+
end
|
|
11
36
|
end
|
|
@@ -5,7 +5,15 @@ module InlineForms
|
|
|
5
5
|
module_eval(<<~'INLINE_FORMS_FORM_ELEMENT', __FILE__, __LINE__ + 1)
|
|
6
6
|
# -*- encoding : utf-8 -*-
|
|
7
7
|
|
|
8
|
+
# Two modes. A declared file slot (InlineForms::StoredFiles + routes from
|
|
9
|
+
# InlineForms.file_routes) renders the slot partial: download through the
|
|
10
|
+
# gem, replace / remove / undo icons by permission. Anything else keeps the
|
|
11
|
+
# legacy behaviour below: a link to the host's download route named in the
|
|
12
|
+
# values hash, and an edit link only while the field is empty.
|
|
8
13
|
def simple_file_field_show(object, attribute)
|
|
14
|
+
if inline_forms_file_slot_active?(object, attribute)
|
|
15
|
+
return render(partial: "inline_forms/file_slot", locals: { object: object, attribute: attribute })
|
|
16
|
+
end
|
|
9
17
|
o = object.send(attribute)
|
|
10
18
|
attributes = @inline_forms_attribute_list || object.inline_forms_attribute_list
|
|
11
19
|
values = attributes.assoc(attribute.to_sym)[2]
|
|
@@ -21,11 +29,31 @@ module InlineForms
|
|
|
21
29
|
end
|
|
22
30
|
|
|
23
31
|
def simple_file_field_edit(object, attribute)
|
|
24
|
-
file_field_tag attribute, :class => 'input_text_field'
|
|
32
|
+
field = file_field_tag attribute, :class => 'input_text_field'
|
|
33
|
+
return field unless object.class.respond_to?(:inline_forms_file_slot?) && object.class.inline_forms_file_slot?(attribute)
|
|
34
|
+
|
|
35
|
+
field = file_field_tag attribute, class: 'input_text_field', required: true
|
|
36
|
+
return field unless object.inline_forms_file_present?(attribute)
|
|
37
|
+
|
|
38
|
+
# Replacing: say which file goes, and where it goes.
|
|
39
|
+
hint = t("inline_forms.files.replace_hint", filename: object[attribute.to_s])
|
|
40
|
+
safe_join([ field, content_tag(:span, hint, class: "inline_forms-file-hint") ])
|
|
25
41
|
end
|
|
26
42
|
|
|
27
43
|
def simple_file_field_update(object, attribute)
|
|
28
|
-
|
|
44
|
+
value = params[attribute.to_sym]
|
|
45
|
+
if object.class.respond_to?(:inline_forms_file_slot?) && object.class.inline_forms_file_slot?(attribute)
|
|
46
|
+
# A declared slot only takes an uploaded file from a request (a crafted
|
|
47
|
+
# string would set a filename without bytes). Nothing chosen is a
|
|
48
|
+
# validation error, not a silent success.
|
|
49
|
+
if value.respond_to?(:original_filename)
|
|
50
|
+
object.send(attribute.to_s + '=', value)
|
|
51
|
+
else
|
|
52
|
+
object.inline_forms_file_missing!(attribute)
|
|
53
|
+
end
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
object.send(attribute.to_s + '=', value)
|
|
29
57
|
end
|
|
30
58
|
|
|
31
59
|
# You need to add a route to your routes.rb file:
|
|
@@ -23,8 +23,13 @@ module InlineFormsGemFiles
|
|
|
23
23
|
# ignores stuff/ is per-machine (present here, absent on the release box), so
|
|
24
24
|
# the token leaked into `gem build` on the release machine. Exclude it hard,
|
|
25
25
|
# independent of any ignore configuration.
|
|
26
|
+
#
|
|
27
|
+
# Same for local Claude Code files (untracked, per checkout): .claude/
|
|
28
|
+
# (launch.json, settings.local.json) and CLAUDE.md.
|
|
26
29
|
EXCLUDED_FILE_PREFIXES = %w[
|
|
27
30
|
stuff/
|
|
31
|
+
.claude/
|
|
32
|
+
CLAUDE.md
|
|
28
33
|
].freeze
|
|
29
34
|
|
|
30
35
|
module_function
|
data/lib/inline_forms/version.rb
CHANGED
data/lib/inline_forms.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
|
2
2
|
|
|
3
|
+
require "active_support/core_ext/module/attribute_accessors"
|
|
4
|
+
require "active_support/core_ext/numeric/time"
|
|
3
5
|
require "inline_forms/version"
|
|
4
6
|
require "inline_forms/attribute_list"
|
|
5
7
|
require "inline_forms/schema_intent"
|
|
@@ -18,6 +20,59 @@ require "inline_forms/turbo_tabs_builder"
|
|
|
18
20
|
module InlineForms
|
|
19
21
|
class PlainTextColumnMissingError < StandardError; end
|
|
20
22
|
|
|
23
|
+
# ---- File slots and the file trash (InlineForms::StoredFiles) ------------
|
|
24
|
+
#
|
|
25
|
+
# How long a removed/replaced file stays restorable in the trash before the
|
|
26
|
+
# sweep (`rake inline_forms:file_trash:purge_expired`) deletes its bytes.
|
|
27
|
+
# nil = never expire (purge by hand only).
|
|
28
|
+
mattr_accessor :file_trash_retention, default: 30.days
|
|
29
|
+
|
|
30
|
+
# Purging also blanks the stored filename (rendered as "removed file").
|
|
31
|
+
# Off by default: the filename is kept as part of the history.
|
|
32
|
+
mattr_accessor :file_trash_redact_on_purge, default: false
|
|
33
|
+
|
|
34
|
+
# Largest accepted upload for a declared file slot, in bytes; nil = no
|
|
35
|
+
# limit beyond the web server's. Enforced as a validation error.
|
|
36
|
+
mattr_accessor :max_file_size, default: nil
|
|
37
|
+
|
|
38
|
+
# Column names of a :simple_file_field slot, from its attribute: `filename`
|
|
39
|
+
# -> data / content_type; `<base>_filename` and any other `<base>` ->
|
|
40
|
+
# `<base>_data` / `<base>_content_type`. Shared by InlineForms::StoredFiles
|
|
41
|
+
# (its defaults) and the generators (the columns they create).
|
|
42
|
+
def self.file_slot_columns(attribute)
|
|
43
|
+
attribute = attribute.to_s
|
|
44
|
+
base = attribute == "filename" ? nil : attribute.delete_suffix("_filename")
|
|
45
|
+
{ filename: attribute, data: base ? "#{base}_data" : "data", content_type: base ? "#{base}_content_type" : "content_type" }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Member routes for a resource whose model declares file slots. Call it
|
|
49
|
+
# inside the resource block (order-independent, unlike a routing concern):
|
|
50
|
+
#
|
|
51
|
+
# resources :clients do
|
|
52
|
+
# post 'revert', on: :member
|
|
53
|
+
# get 'list_versions', on: :member
|
|
54
|
+
# InlineForms.file_routes(self)
|
|
55
|
+
# end
|
|
56
|
+
def self.file_routes(router)
|
|
57
|
+
router.member do
|
|
58
|
+
router.get "download_file"
|
|
59
|
+
router.post "remove_file"
|
|
60
|
+
router.post "restore_file"
|
|
61
|
+
router.post "purge_file"
|
|
62
|
+
router.get "trashed_file"
|
|
63
|
+
router.get "file_trash"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The cross-record trash page (superadmin). One line in routes.rb:
|
|
68
|
+
# InlineForms.draw_file_trash_routes(self)
|
|
69
|
+
def self.draw_file_trash_routes(router)
|
|
70
|
+
router.get "file_trash", to: "inline_forms/file_trash#index", as: :inline_forms_file_trash
|
|
71
|
+
router.post "file_trash/purge", to: "inline_forms/file_trash#purge", as: :inline_forms_file_trash_purge
|
|
72
|
+
router.post "file_trash/:id/restore", to: "inline_forms/file_trash#restore", as: :inline_forms_file_trash_restore
|
|
73
|
+
router.get "file_trash/:id/download", to: "inline_forms/file_trash#download", as: :inline_forms_file_trash_download
|
|
74
|
+
end
|
|
75
|
+
|
|
21
76
|
# DEFAULT_COLUMN_TYPES holds the standard ActiveRecord::Migration column types.
|
|
22
77
|
# This list provides compatability with the standard types, but we add our own
|
|
23
78
|
# later in 'Special Column Types'.
|
|
@@ -31,6 +31,89 @@ en:
|
|
|
31
31
|
trash: trash
|
|
32
32
|
list_versions: List versions
|
|
33
33
|
close_versions_list: Close versions list
|
|
34
|
+
files:
|
|
35
|
+
download: download
|
|
36
|
+
replace: replace file
|
|
37
|
+
remove: remove file
|
|
38
|
+
restore: restore
|
|
39
|
+
restore_file: restore file
|
|
40
|
+
purge: delete permanently
|
|
41
|
+
undo: undo
|
|
42
|
+
restore_previous: "restore removed file %{filename}"
|
|
43
|
+
replace_hint: "The current file %{filename} goes to the trash."
|
|
44
|
+
confirm_remove:
|
|
45
|
+
one: "Remove %{filename}? It goes to the trash and can be restored for 1 day."
|
|
46
|
+
other: "Remove %{filename}? It goes to the trash and can be restored for %{count} days."
|
|
47
|
+
confirm_remove_no_expiry: "Remove %{filename}? It goes to the trash and can be restored."
|
|
48
|
+
confirm_restore_swap: "The current file %{current} goes to the trash and %{filename} is put back. Continue?"
|
|
49
|
+
confirm_purge: "Delete %{filename} permanently? This cannot be undone."
|
|
50
|
+
notice_removed: "%{filename} removed"
|
|
51
|
+
notice_restored: File restored.
|
|
52
|
+
notice_purged: File deleted permanently.
|
|
53
|
+
notice_already_empty: There was no file to remove.
|
|
54
|
+
notice_not_restorable: This file can no longer be restored.
|
|
55
|
+
notice_not_purgeable: This file was already restored or deleted.
|
|
56
|
+
missing: This file has been removed.
|
|
57
|
+
trash_header:
|
|
58
|
+
zero: Removed files
|
|
59
|
+
one: Removed files (1)
|
|
60
|
+
other: Removed files (%{count})
|
|
61
|
+
open_trash: Show removed files
|
|
62
|
+
close_trash: Hide removed files
|
|
63
|
+
by_on: "by %{user} on %{at}"
|
|
64
|
+
by_system: the system
|
|
65
|
+
redacted_name: "[removed file]"
|
|
66
|
+
state_restored: "restored by %{user} on %{at}"
|
|
67
|
+
state_purged: "deleted permanently by %{user} on %{at}"
|
|
68
|
+
state_expired: expired, will be deleted
|
|
69
|
+
expired_pending:
|
|
70
|
+
one: 1 expired file waits for the nightly clean-up.
|
|
71
|
+
other: "%{count} expired files wait for the nightly clean-up."
|
|
72
|
+
expires_in:
|
|
73
|
+
one: "restorable until %{date} (1 day)"
|
|
74
|
+
other: "restorable until %{date} (%{count} days)"
|
|
75
|
+
reason:
|
|
76
|
+
removed: removed
|
|
77
|
+
replaced: replaced
|
|
78
|
+
record_destroyed: record deleted
|
|
79
|
+
orphaned: orphaned bytes
|
|
80
|
+
uploaded: uploaded
|
|
81
|
+
event:
|
|
82
|
+
uploaded: file uploaded
|
|
83
|
+
removed: file removed
|
|
84
|
+
replaced: file replaced
|
|
85
|
+
record_destroyed: file trashed (record deleted)
|
|
86
|
+
orphaned: orphaned file trashed
|
|
87
|
+
restored: file restored
|
|
88
|
+
purged: file deleted
|
|
89
|
+
global:
|
|
90
|
+
title: Removed files
|
|
91
|
+
model: Model
|
|
92
|
+
state: State
|
|
93
|
+
all: all
|
|
94
|
+
filter: filter
|
|
95
|
+
record: Record
|
|
96
|
+
record_gone: "%{model} #%{id} (deleted)"
|
|
97
|
+
slot: Field
|
|
98
|
+
file: File
|
|
99
|
+
trashed: Removed
|
|
100
|
+
actions: Actions
|
|
101
|
+
purge_selected: Delete selected permanently
|
|
102
|
+
confirm_purge_selected: Delete the selected files permanently? This cannot be undone.
|
|
103
|
+
purged_count:
|
|
104
|
+
one: 1 file deleted permanently.
|
|
105
|
+
other: "%{count} files deleted permanently."
|
|
106
|
+
nothing_selected: No files selected.
|
|
107
|
+
empty: No removed files.
|
|
108
|
+
sweep_last: "Last clean-up: %{at} (%{count} deleted)"
|
|
109
|
+
sweep_never: The nightly clean-up has never run.
|
|
110
|
+
sweep_overdue: The nightly clean-up has not run for more than 48 hours. Check the cron job.
|
|
111
|
+
sweep_failed: "Last clean-up failed: %{error}"
|
|
112
|
+
states:
|
|
113
|
+
trashed: in trash
|
|
114
|
+
restored: restored
|
|
115
|
+
purged: deleted
|
|
116
|
+
expired: expired
|
|
34
117
|
pending_migration:
|
|
35
118
|
notice: "pending migration \u2014 run rails db:migrate"
|
|
36
119
|
succes: success.
|
|
@@ -41,3 +124,7 @@ en:
|
|
|
41
124
|
client:
|
|
42
125
|
first_name: first name
|
|
43
126
|
last_name: last name
|
|
127
|
+
errors:
|
|
128
|
+
messages:
|
|
129
|
+
inline_forms_file_too_large: "is too large (maximum %{count} MB)"
|
|
130
|
+
inline_forms_file_missing: "no file chosen"
|
|
@@ -28,6 +28,89 @@ nl:
|
|
|
28
28
|
add_new: Maak een nieuw %{model}
|
|
29
29
|
undo: terughalen
|
|
30
30
|
trash: weggooien
|
|
31
|
+
files:
|
|
32
|
+
download: downloaden
|
|
33
|
+
replace: bestand vervangen
|
|
34
|
+
remove: bestand verwijderen
|
|
35
|
+
restore: terugzetten
|
|
36
|
+
restore_file: bestand terugzetten
|
|
37
|
+
purge: definitief verwijderen
|
|
38
|
+
undo: ongedaan maken
|
|
39
|
+
restore_previous: "verwijderd bestand %{filename} terugzetten"
|
|
40
|
+
replace_hint: "Het huidige bestand %{filename} gaat naar de prullenbak."
|
|
41
|
+
confirm_remove:
|
|
42
|
+
one: "%{filename} verwijderen? Het gaat naar de prullenbak en kan 1 dag worden teruggezet."
|
|
43
|
+
other: "%{filename} verwijderen? Het gaat naar de prullenbak en kan %{count} dagen worden teruggezet."
|
|
44
|
+
confirm_remove_no_expiry: "%{filename} verwijderen? Het gaat naar de prullenbak en kan worden teruggezet."
|
|
45
|
+
confirm_restore_swap: "Het huidige bestand %{current} gaat naar de prullenbak en %{filename} wordt teruggezet. Doorgaan?"
|
|
46
|
+
confirm_purge: "%{filename} definitief verwijderen? Dit kan niet ongedaan worden gemaakt."
|
|
47
|
+
notice_removed: "%{filename} verwijderd"
|
|
48
|
+
notice_restored: Bestand teruggezet.
|
|
49
|
+
notice_purged: Bestand definitief verwijderd.
|
|
50
|
+
notice_already_empty: Er was geen bestand om te verwijderen.
|
|
51
|
+
notice_not_restorable: Dit bestand kan niet meer worden teruggezet.
|
|
52
|
+
notice_not_purgeable: Dit bestand is al teruggezet of verwijderd.
|
|
53
|
+
missing: Dit bestand is verwijderd.
|
|
54
|
+
trash_header:
|
|
55
|
+
zero: Verwijderde bestanden
|
|
56
|
+
one: Verwijderde bestanden (1)
|
|
57
|
+
other: Verwijderde bestanden (%{count})
|
|
58
|
+
open_trash: Verwijderde bestanden tonen
|
|
59
|
+
close_trash: Verwijderde bestanden verbergen
|
|
60
|
+
by_on: "door %{user} op %{at}"
|
|
61
|
+
by_system: het systeem
|
|
62
|
+
redacted_name: "[verwijderd bestand]"
|
|
63
|
+
state_restored: "teruggezet door %{user} op %{at}"
|
|
64
|
+
state_purged: "definitief verwijderd door %{user} op %{at}"
|
|
65
|
+
state_expired: verlopen, wordt verwijderd
|
|
66
|
+
expired_pending:
|
|
67
|
+
one: 1 verlopen bestand wacht op de nachtelijke opruiming.
|
|
68
|
+
other: "%{count} verlopen bestanden wachten op de nachtelijke opruiming."
|
|
69
|
+
expires_in:
|
|
70
|
+
one: "terug te zetten tot %{date} (1 dag)"
|
|
71
|
+
other: "terug te zetten tot %{date} (%{count} dagen)"
|
|
72
|
+
reason:
|
|
73
|
+
removed: verwijderd
|
|
74
|
+
replaced: vervangen
|
|
75
|
+
record_destroyed: record verwijderd
|
|
76
|
+
orphaned: verweesde bytes
|
|
77
|
+
uploaded: geüpload
|
|
78
|
+
event:
|
|
79
|
+
uploaded: bestand geüpload
|
|
80
|
+
removed: bestand verwijderd
|
|
81
|
+
replaced: bestand vervangen
|
|
82
|
+
record_destroyed: bestand in prullenbak (record verwijderd)
|
|
83
|
+
orphaned: verweesd bestand in prullenbak
|
|
84
|
+
restored: bestand teruggezet
|
|
85
|
+
purged: bestand definitief verwijderd
|
|
86
|
+
global:
|
|
87
|
+
title: Verwijderde bestanden
|
|
88
|
+
model: Model
|
|
89
|
+
state: Status
|
|
90
|
+
all: alle
|
|
91
|
+
filter: filteren
|
|
92
|
+
record: Record
|
|
93
|
+
record_gone: "%{model} #%{id} (verwijderd)"
|
|
94
|
+
slot: Veld
|
|
95
|
+
file: Bestand
|
|
96
|
+
trashed: Verwijderd
|
|
97
|
+
actions: Acties
|
|
98
|
+
purge_selected: Selectie definitief verwijderen
|
|
99
|
+
confirm_purge_selected: De geselecteerde bestanden definitief verwijderen? Dit kan niet ongedaan worden gemaakt.
|
|
100
|
+
purged_count:
|
|
101
|
+
one: 1 bestand definitief verwijderd.
|
|
102
|
+
other: "%{count} bestanden definitief verwijderd."
|
|
103
|
+
nothing_selected: Geen bestanden geselecteerd.
|
|
104
|
+
empty: Geen verwijderde bestanden.
|
|
105
|
+
sweep_last: "Laatste opruiming: %{at} (%{count} verwijderd)"
|
|
106
|
+
sweep_never: De nachtelijke opruiming heeft nog nooit gedraaid.
|
|
107
|
+
sweep_overdue: De nachtelijke opruiming heeft al meer dan 48 uur niet gedraaid. Controleer de cron-taak.
|
|
108
|
+
sweep_failed: "Laatste opruiming mislukt: %{error}"
|
|
109
|
+
states:
|
|
110
|
+
trashed: in prullenbak
|
|
111
|
+
restored: teruggezet
|
|
112
|
+
purged: verwijderd
|
|
113
|
+
expired: verlopen
|
|
31
114
|
pending_migration:
|
|
32
115
|
notice: "migratie in afwachting \u2014 draai rails db:migrate"
|
|
33
116
|
succes: het ding is aangemaakt.
|
|
@@ -38,6 +121,7 @@ nl:
|
|
|
38
121
|
client:
|
|
39
122
|
first_name: voornaam
|
|
40
123
|
last_name: achternaam
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
124
|
+
errors:
|
|
125
|
+
messages:
|
|
126
|
+
inline_forms_file_too_large: "is te groot (maximaal %{count} MB)"
|
|
127
|
+
inline_forms_file_missing: "geen bestand gekozen"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# Declared file slots (InlineForms::StoredFiles) and the file trash.
|
|
4
|
+
namespace :inline_forms do
|
|
5
|
+
namespace :file_trash do
|
|
6
|
+
desc "Purge trashed files whose retention has passed (run daily from cron; exits non-zero on failure)"
|
|
7
|
+
task purge_expired: :environment do
|
|
8
|
+
purged = InlineForms::FileTrashSweep.run!
|
|
9
|
+
puts "inline_forms: file trash sweep purged #{purged} file(s)"
|
|
10
|
+
Rails.logger.info("inline_forms: file trash sweep purged #{purged} file(s)")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
namespace :files do
|
|
15
|
+
# Models that declare file slots. Eager-loads the app so every model is known.
|
|
16
|
+
def inline_forms_file_slot_models
|
|
17
|
+
Rails.application.eager_load!
|
|
18
|
+
ActiveRecord::Base.descendants.select do |klass|
|
|
19
|
+
!klass.abstract_class? && klass.respond_to?(:inline_forms_file_slots) &&
|
|
20
|
+
klass.inline_forms_file_slots.any? && klass.table_exists?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc "Report broken file slots (read-only): bytes without a filename, a filename without bytes"
|
|
25
|
+
task audit: :environment do
|
|
26
|
+
problems = 0
|
|
27
|
+
inline_forms_file_slot_models.each do |klass|
|
|
28
|
+
klass.inline_forms_file_slots.each do |attribute, slot|
|
|
29
|
+
scope = klass.unscoped
|
|
30
|
+
orphaned = scope.where.not(slot[:data] => nil).where(attribute => [ nil, "" ]).count
|
|
31
|
+
missing = scope.where(slot[:data] => nil).where.not(attribute => [ nil, "" ]).count
|
|
32
|
+
next if orphaned.zero? && missing.zero?
|
|
33
|
+
|
|
34
|
+
problems += orphaned + missing
|
|
35
|
+
puts "#{klass.name}##{attribute}: #{orphaned} with bytes but no filename (orphaned), " \
|
|
36
|
+
"#{missing} with a filename but no bytes (empty downloads)"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
puts(problems.zero? ? "inline_forms: all file slots consistent" :
|
|
40
|
+
"inline_forms: #{problems} inconsistent slot(s). Orphaned bytes: `rake inline_forms:files:trash_orphans CONFIRM=yes` " \
|
|
41
|
+
"moves them to the trash (purged by the retention sweep). Missing bytes cannot be repaired here; restore from a backup.")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
desc "Move orphaned bytes (bytes in a slot without a filename) into the file trash. Needs CONFIRM=yes"
|
|
45
|
+
task trash_orphans: :environment do
|
|
46
|
+
abort "inline_forms: refusing without CONFIRM=yes (run inline_forms:files:audit first)" unless ENV["CONFIRM"] == "yes"
|
|
47
|
+
|
|
48
|
+
moved = 0
|
|
49
|
+
inline_forms_file_slot_models.each do |klass|
|
|
50
|
+
klass.inline_forms_file_slots.each do |attribute, slot|
|
|
51
|
+
ids = klass.unscoped.where.not(slot[:data] => nil).where(attribute => [ nil, "" ]).pluck(klass.primary_key)
|
|
52
|
+
ids.each do |id|
|
|
53
|
+
record = klass.unscoped.find(id)
|
|
54
|
+
moved += 1 if record.inline_forms_remove_file!(attribute, by: nil, reason: :orphaned)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
puts "inline_forms: moved #{moved} orphaned file(s) to the trash"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -14,6 +14,11 @@ class ApplicationController < ActionController::Base
|
|
|
14
14
|
|
|
15
15
|
helper_method :current_user
|
|
16
16
|
|
|
17
|
+
# Generated apps redirect HTML with a notice; a bare 403 is easier to assert.
|
|
18
|
+
rescue_from CanCan::AccessDenied do
|
|
19
|
+
head :forbidden
|
|
20
|
+
end
|
|
21
|
+
|
|
17
22
|
def current_user
|
|
18
23
|
@current_user ||= DummyUser.new(1, "Dummy User")
|
|
19
24
|
end
|