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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/CHANGELOG.md +149 -0
  4. data/Gemfile +3 -0
  5. data/README.rdoc +47 -0
  6. data/app/assets/stylesheets/inline_forms/inline_forms.scss +34 -0
  7. data/app/controllers/concerns/file_slots_concern.rb +211 -0
  8. data/app/controllers/inline_forms/file_trash_controller.rb +115 -0
  9. data/app/controllers/inline_forms_controller.rb +82 -16
  10. data/app/helpers/inline_forms_file_trash_helper.rb +131 -0
  11. data/app/helpers/inline_forms_helper.rb +5 -1
  12. data/app/models/concerns/inline_forms/stored_files.rb +350 -0
  13. data/app/models/inline_forms/current.rb +8 -0
  14. data/app/models/inline_forms/file_trash_sweep.rb +37 -0
  15. data/app/models/inline_forms/trashed_file.rb +205 -0
  16. data/app/views/inline_forms/_file_slot.html.erb +39 -0
  17. data/app/views/inline_forms/_header.html.erb +3 -0
  18. data/app/views/inline_forms/_model_top_bar.html.erb +3 -4
  19. data/app/views/inline_forms/_show.html.erb +6 -0
  20. data/app/views/inline_forms/_versions.html.erb +4 -4
  21. data/app/views/inline_forms/_versions_file_entry.html.erb +36 -0
  22. data/app/views/inline_forms/_versions_list.html.erb +11 -2
  23. data/app/views/inline_forms/file_field_frame.html.erb +22 -0
  24. data/app/views/inline_forms/file_trash/index.html.erb +112 -0
  25. data/app/views/inline_forms/file_trash_panel.html.erb +90 -0
  26. data/app/views/layouts/inline_forms.html.erb +5 -1
  27. data/lib/generators/inline_forms/file_trash_generator.rb +36 -0
  28. data/lib/generators/inline_forms/templates/create_inline_forms_file_trash.rb.erb +41 -0
  29. data/lib/generators/inline_forms_addto_generator.rb +47 -0
  30. data/lib/generators/inline_forms_generator.rb +19 -2
  31. data/lib/generators/templates/model.erb +4 -0
  32. data/lib/inline_forms/form_element_from_callee.rb +25 -0
  33. data/lib/inline_forms/form_elements/simple_file_field_helper.rb +30 -2
  34. data/lib/inline_forms/gem_files.rb +5 -0
  35. data/lib/inline_forms/version.rb +1 -1
  36. data/lib/inline_forms.rb +55 -0
  37. data/lib/locales/inline_forms.en.yml +87 -0
  38. data/lib/locales/inline_forms.nl.yml +87 -3
  39. data/lib/tasks/inline_forms_files.rake +61 -0
  40. data/test/dummy/app/controllers/application_controller.rb +5 -0
  41. data/test/dummy/app/controllers/documents_controller.rb +4 -0
  42. data/test/dummy/app/controllers/dossiers_controller.rb +4 -0
  43. data/test/dummy/app/models/ability.rb +15 -0
  44. data/test/dummy/app/models/document.rb +33 -0
  45. data/test/dummy/app/models/dossier.rb +38 -0
  46. data/test/dummy/config/application.rb +4 -3
  47. data/test/dummy/config/routes.rb +13 -0
  48. data/test/inline_forms_addto_generator_test.rb +47 -0
  49. data/test/inline_forms_generator_test.rb +24 -0
  50. data/test/integration/field_authorization_test.rb +131 -0
  51. data/test/integration/file_slots_test.rb +574 -0
  52. data/test/integration/layout_title_test.rb +56 -0
  53. data/test/integration/model_top_bar_test.rb +9 -0
  54. data/test/integration/revert_skipped_columns_test.rb +59 -0
  55. data/test/integration_test_helper.rb +62 -0
  56. metadata +34 -3
  57. data/.claude/settings.local.json +0 -9
  58. 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.column_type == :image
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
- object.send(attribute.to_s + '=', params[attribute.to_sym])
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
@@ -1,5 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
3
  module InlineForms
4
- VERSION = "8.1.47"
4
+ VERSION = "8.1.53"
5
5
  end
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
- application_name: naam van de applicatie
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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DocumentsController < InlineFormsController
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DossiersController < InlineFormsController
4
+ end