inline_forms 8.1.47 → 8.1.55
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 +178 -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/devise/passwords/edit.html.erb +1 -1
- data/app/views/devise/passwords/new.html.erb +1 -1
- data/app/views/devise/shared/_header_and_errors.html.erb +9 -2
- 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 +37 -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/gem_package_modes_test.rb +86 -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 +36 -3
- data/.claude/settings.local.json +0 -9
- data/CLAUDE.md +0 -68
|
@@ -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
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Grants everything (the generated superadmin branch). A test narrows it by
|
|
4
|
+
# setting Ability.restrictions to a block of `cannot` rules, evaluated after
|
|
5
|
+
# the grant so they win; InlineFormsIntegrationTestCase resets it.
|
|
6
|
+
class Ability
|
|
7
|
+
include CanCan::Ability
|
|
8
|
+
|
|
9
|
+
cattr_accessor :restrictions
|
|
10
|
+
|
|
11
|
+
def initialize(_user)
|
|
12
|
+
can :manage, :all
|
|
13
|
+
instance_exec(&restrictions) if restrictions
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Declared file slots (InlineForms::StoredFiles), shaped like StProject's
|
|
4
|
+
# Client: two slots with the default column naming (`filename` -> data /
|
|
5
|
+
# content_type, `plan_filename` -> plan_data / plan_content_type). An
|
|
6
|
+
# ApplicationRecord subclass on purpose: has_paper_trail is inherited, so the
|
|
7
|
+
# slot columns' skip must be set on Document without leaking to Widget.
|
|
8
|
+
# See test/integration/file_slots_test.rb.
|
|
9
|
+
class Document < ApplicationRecord
|
|
10
|
+
include InlineForms::StoredFiles
|
|
11
|
+
|
|
12
|
+
inline_forms_file :filename
|
|
13
|
+
inline_forms_file :plan_filename
|
|
14
|
+
|
|
15
|
+
# StProject's Attachment/Incident/Zorgformulier validate the filename; a
|
|
16
|
+
# test switches this on to prove undo-of-destroy puts files back BEFORE save.
|
|
17
|
+
cattr_accessor :require_filename, default: false
|
|
18
|
+
validates :filename, presence: true, if: -> { self.class.require_filename }
|
|
19
|
+
|
|
20
|
+
validates :title, presence: true
|
|
21
|
+
|
|
22
|
+
def _presentation
|
|
23
|
+
"#{title}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def inline_forms_attribute_list
|
|
27
|
+
@inline_forms_attribute_list ||= [
|
|
28
|
+
[ :title, :text_field ],
|
|
29
|
+
[ :filename, :simple_file_field ],
|
|
30
|
+
[ :plan_filename, :simple_file_field ]
|
|
31
|
+
]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Models StProject's Client/Attachment: file bytes stored in a binary column
|
|
4
|
+
# that is excluded from PaperTrail (`skip:`), filename kept versioned. Reverting
|
|
5
|
+
# an unrelated edit must not touch `data`. See
|
|
6
|
+
# test/integration/revert_skipped_columns_test.rb.
|
|
7
|
+
#
|
|
8
|
+
# Not an ApplicationRecord: that already calls has_paper_trail, which PaperTrail
|
|
9
|
+
# allows only once per hierarchy. StProject likewise declares it per model, so
|
|
10
|
+
# this repeats the rest of the ApplicationRecord host contract inline.
|
|
11
|
+
class Dossier < ActiveRecord::Base
|
|
12
|
+
include InlineForms::Searchable
|
|
13
|
+
|
|
14
|
+
has_paper_trail on: [ :create, :update, :destroy ], skip: [ :data ]
|
|
15
|
+
|
|
16
|
+
attr_writer :inline_forms_attribute_list
|
|
17
|
+
|
|
18
|
+
self.per_page = 7
|
|
19
|
+
|
|
20
|
+
scope :inline_forms_list, -> { order(:name, :id) }
|
|
21
|
+
|
|
22
|
+
validates :name, presence: true
|
|
23
|
+
|
|
24
|
+
def self.not_accessible_through_html?
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def _presentation
|
|
29
|
+
"#{name}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def inline_forms_attribute_list
|
|
33
|
+
@inline_forms_attribute_list ||= [
|
|
34
|
+
[ :name, :text_field ],
|
|
35
|
+
[ :data_filename, :info ]
|
|
36
|
+
]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
# Minimal host app for the engine's integration tests (test/integration/).
|
|
4
4
|
# Boots the inline_forms engine against an in-memory SQLite database with the
|
|
5
|
-
# host-side gems the engine expects (will_paginate, paper_trail, turbo-rails
|
|
6
|
-
# but without Devise,
|
|
7
|
-
#
|
|
5
|
+
# host-side gems the engine expects (will_paginate, paper_trail, turbo-rails,
|
|
6
|
+
# cancancan) but without Devise, Sprockets or ActionText. CanCanCan is on so
|
|
7
|
+
# load_and_authorize_resource and the attribute-level checks run; the dummy
|
|
8
|
+
# Ability grants everything unless a test narrows it. Tests drive the
|
|
8
9
|
# UI through Turbo-Frame requests (the frame layout never renders the
|
|
9
10
|
# current_user header), and the asset-precompile initializer skips hosts
|
|
10
11
|
# without a pipeline.
|
data/test/dummy/config/routes.rb
CHANGED
|
@@ -25,6 +25,19 @@ Rails.application.routes.draw do
|
|
|
25
25
|
get "list_versions", on: :member
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
resources :dossiers do
|
|
29
|
+
post "revert", on: :member
|
|
30
|
+
get "list_versions", on: :member
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
resources :documents do
|
|
34
|
+
post "revert", on: :member
|
|
35
|
+
get "list_versions", on: :member
|
|
36
|
+
InlineForms.file_routes(self)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
InlineForms.draw_file_trash_routes(self)
|
|
40
|
+
|
|
28
41
|
get "stats", to: "stats#show"
|
|
29
42
|
|
|
30
43
|
# Schema-change GUI (opt-in in real installs; routed here for the engine's
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
4
|
+
|
|
5
|
+
require "minitest/autorun"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require "zlib"
|
|
8
|
+
require "inline_forms/gem_files"
|
|
9
|
+
|
|
10
|
+
# 8.1.53 shipped every file as 0660 (the checkout's group-only modes), so an
|
|
11
|
+
# installed gem was unreadable for anyone outside the installing user and
|
|
12
|
+
# group. Gem builds now package 0644 / 0755 whatever the on-disk modes.
|
|
13
|
+
class GemPackageModesTest < Minitest::Test
|
|
14
|
+
REPO_ROOT = File.expand_path("..", __dir__)
|
|
15
|
+
|
|
16
|
+
def packaged_modes(gem_path)
|
|
17
|
+
modes = {}
|
|
18
|
+
File.open(gem_path, "rb") do |io|
|
|
19
|
+
Gem::Package::TarReader.new(io).each do |entry|
|
|
20
|
+
next unless entry.full_name == "data.tar.gz"
|
|
21
|
+
|
|
22
|
+
Gem::Package::TarReader.new(Zlib::GzipReader.new(entry)).each do |file|
|
|
23
|
+
modes[file.full_name] = file.header.mode & 0o777
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
modes
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def build(spec, dir, gem_dir)
|
|
31
|
+
gem_path = File.join(gem_dir, "#{spec.full_name}.gem")
|
|
32
|
+
Dir.chdir(dir) do
|
|
33
|
+
package = Gem::Package.new(gem_path)
|
|
34
|
+
package.spec = spec
|
|
35
|
+
Gem::DefaultUserInteraction.use_ui(Gem::SilentUI.new) { package.build(true) }
|
|
36
|
+
end
|
|
37
|
+
gem_path
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_packaged_mode_keeps_only_the_owner_execute_bit
|
|
41
|
+
assert_equal 0o644, InlineFormsGemFiles.packaged_mode(0o100660)
|
|
42
|
+
assert_equal 0o644, InlineFormsGemFiles.packaged_mode(0o100600)
|
|
43
|
+
assert_equal 0o755, InlineFormsGemFiles.packaged_mode(0o100770)
|
|
44
|
+
assert_equal 0o755, InlineFormsGemFiles.packaged_mode(0o100700)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_group_only_files_are_packaged_world_readable
|
|
48
|
+
Dir.mktmpdir do |src|
|
|
49
|
+
Dir.mktmpdir do |out|
|
|
50
|
+
File.write(File.join(src, "plain.rb"), "# plain\n")
|
|
51
|
+
File.write(File.join(src, "tool"), "#!/bin/sh\n")
|
|
52
|
+
File.chmod(0o660, File.join(src, "plain.rb"))
|
|
53
|
+
File.chmod(0o700, File.join(src, "tool"))
|
|
54
|
+
|
|
55
|
+
spec = Gem::Specification.new do |s|
|
|
56
|
+
s.name = "modes_probe"
|
|
57
|
+
s.version = "0.0.1"
|
|
58
|
+
s.summary = "probe"
|
|
59
|
+
s.authors = ["probe"]
|
|
60
|
+
s.files = %w[plain.rb tool]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
modes = packaged_modes(build(spec, src, out))
|
|
64
|
+
|
|
65
|
+
assert_equal({ "plain.rb" => 0o644, "tool" => 0o755 }, modes)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_every_gemspec_in_the_repo_packages_world_readable_files
|
|
71
|
+
%w[inline_forms.gemspec inline_forms_installer.gemspec
|
|
72
|
+
inline_forms_schema_edit/inline_forms_schema_edit.gemspec].each do |gemspec|
|
|
73
|
+
path = File.join(REPO_ROOT, gemspec)
|
|
74
|
+
spec = Gem::Specification.load(path)
|
|
75
|
+
refute_nil spec, gemspec
|
|
76
|
+
|
|
77
|
+
Dir.mktmpdir do |out|
|
|
78
|
+
modes = packaged_modes(build(spec, File.dirname(path), out))
|
|
79
|
+
|
|
80
|
+
refute_empty modes, gemspec
|
|
81
|
+
unreadable = modes.reject { |_, mode| (mode & 0o444) == 0o444 }
|
|
82
|
+
assert_empty unreadable, "#{gemspec}: not world-readable in the .gem"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -386,6 +386,53 @@ class InlineFormsAddtoGeneratorTest < Minitest::Test
|
|
|
386
386
|
teardown_sqlite
|
|
387
387
|
end
|
|
388
388
|
|
|
389
|
+
def test_simple_file_field_adds_a_declared_slot_columns_and_routes
|
|
390
|
+
write_model("widget.rb", GENERATOR_SHAPED_MODEL)
|
|
391
|
+
mkdir_p("config")
|
|
392
|
+
File.write(File.join(@destination_root, "config/routes.rb"), <<~RUBY)
|
|
393
|
+
Rails.application.routes.draw do
|
|
394
|
+
resources :widgets do
|
|
395
|
+
post 'revert', :on => :member
|
|
396
|
+
get 'list_versions', :on => :member
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
RUBY
|
|
400
|
+
|
|
401
|
+
capture_io { run_generator("Widget", "manual:simple_file_field") }
|
|
402
|
+
capture_io { run_generator("Widget", "plan_filename:simple_file_field") }
|
|
403
|
+
|
|
404
|
+
model = read("app/models/widget.rb")
|
|
405
|
+
assert_equal(1, model.scan("include InlineForms::StoredFiles").size)
|
|
406
|
+
assert_includes(model, "inline_forms_file :manual")
|
|
407
|
+
assert_includes(model, "inline_forms_file :plan_filename")
|
|
408
|
+
assert_operator(model.index("include InlineForms::StoredFiles"), :<, model.index("inline_forms_file :manual"))
|
|
409
|
+
assert_operator(model.index("include InlineForms::StoredFiles"), :<, model.index("inline_forms_file :plan_filename"))
|
|
410
|
+
assert_includes(model, "[ :manual, :simple_file_field ]")
|
|
411
|
+
routes = read("config/routes.rb")
|
|
412
|
+
assert_equal(1, routes.scan("InlineForms.file_routes(self)").size)
|
|
413
|
+
assert_match(/resources :widgets do\n\s+InlineForms\.file_routes\(self\)/, routes)
|
|
414
|
+
|
|
415
|
+
migration_file = Dir.glob(File.join(@destination_root, "db/migrate/*_inline_forms_add_to_widgets_*.rb")).min
|
|
416
|
+
conn = apply_migration_to_memory_sqlite(migration_file, tables: %i[widgets])
|
|
417
|
+
%i[manual manual_data manual_content_type].each do |column|
|
|
418
|
+
assert(conn.column_exists?(:widgets, column), "#{column} should exist after migrate")
|
|
419
|
+
end
|
|
420
|
+
ensure
|
|
421
|
+
teardown_sqlite
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def test_simple_file_field_without_a_resource_block_reminds_about_routes
|
|
425
|
+
write_model("widget.rb", GENERATOR_SHAPED_MODEL)
|
|
426
|
+
mkdir_p("config")
|
|
427
|
+
File.write(File.join(@destination_root, "config/routes.rb"), "Rails.application.routes.draw do\n MODELS.each { |m| resources m }\nend\n")
|
|
428
|
+
|
|
429
|
+
stdout, = capture_io { run_generator("Widget", "manual:simple_file_field") }
|
|
430
|
+
|
|
431
|
+
assert_includes(read("app/models/widget.rb"), "inline_forms_file :manual")
|
|
432
|
+
refute_includes(read("config/routes.rb"), "file_routes")
|
|
433
|
+
assert_includes(stdout, "InlineForms.file_routes(self)")
|
|
434
|
+
end
|
|
435
|
+
|
|
389
436
|
private
|
|
390
437
|
|
|
391
438
|
def run_generator(*args)
|
|
@@ -171,6 +171,30 @@ class InlineFormsGeneratorTest < Minitest::Test
|
|
|
171
171
|
assert_includes(migration, "t.text :description")
|
|
172
172
|
end
|
|
173
173
|
|
|
174
|
+
def test_simple_file_field_declares_a_file_slot_with_its_columns_and_routes
|
|
175
|
+
run_generator("Folder", "title:string", "plan_filename:simple_file_field", "_enabled:yes")
|
|
176
|
+
|
|
177
|
+
model = read("app/models/folder.rb")
|
|
178
|
+
routes = read("config/routes.rb")
|
|
179
|
+
migration = read_single_migration_for("folders")
|
|
180
|
+
|
|
181
|
+
assert_includes(model, "include InlineForms::StoredFiles")
|
|
182
|
+
assert_includes(model, "inline_forms_file :plan_filename")
|
|
183
|
+
assert_operator(model.index("include InlineForms::StoredFiles"), :<, model.index("inline_forms_file :plan_filename"))
|
|
184
|
+
assert_includes(model, "[ :plan_filename, :simple_file_field ]")
|
|
185
|
+
assert_includes(migration, "t.string :plan_filename")
|
|
186
|
+
assert_includes(migration, "t.binary :plan_data")
|
|
187
|
+
assert_includes(migration, "t.string :plan_content_type")
|
|
188
|
+
assert_includes(routes, "InlineForms.file_routes(self)")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def test_no_file_routes_without_a_file_field
|
|
192
|
+
run_generator("Thing", "name:string", "_enabled:yes")
|
|
193
|
+
|
|
194
|
+
refute_includes(read("config/routes.rb"), "file_routes")
|
|
195
|
+
refute_includes(read("app/models/thing.rb"), "StoredFiles")
|
|
196
|
+
end
|
|
197
|
+
|
|
174
198
|
private
|
|
175
199
|
|
|
176
200
|
def build_destination_skeleton!
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../integration_test_helper"
|
|
4
|
+
|
|
5
|
+
# Server-side guard on the single-field actions (edit / update / show with an
|
|
6
|
+
# attribute). The UI only hides the edit link for an attribute the user may
|
|
7
|
+
# not update; before 8.1.48 a hand-made request still went through, because
|
|
8
|
+
# load_and_authorize_resource authorizes the record only (CanCanCan passes
|
|
9
|
+
# attribute-scoped rules when no attribute is given) and `form_element` was
|
|
10
|
+
# dispatched as `send("#{form_element}_update")` unchecked.
|
|
11
|
+
class FieldAuthorizationTest < InlineFormsIntegrationTestCase
|
|
12
|
+
setup do
|
|
13
|
+
@widget = Widget.create!(name: "Alpha", accent: "#112233")
|
|
14
|
+
@frame = "widget_#{@widget.id}_accent"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def patch_field(attribute, form_element, params = {})
|
|
18
|
+
patch widget_path(@widget, attribute: attribute, form_element: form_element,
|
|
19
|
+
update: "widget_#{@widget.id}_#{attribute}"),
|
|
20
|
+
params: params, headers: frame_headers("widget_#{@widget.id}_#{attribute}")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
test "the dummy runs with CanCanCan enabled" do
|
|
24
|
+
assert InlineFormsController.cancan_enabled?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
test "crafted PATCH on an attribute-scoped `cannot :update` is refused" do
|
|
28
|
+
Ability.restrictions = -> { cannot :update, Widget, [ :accent ] }
|
|
29
|
+
|
|
30
|
+
patch_field "accent", "color_field", accent: "#ff0000"
|
|
31
|
+
|
|
32
|
+
assert_response :forbidden
|
|
33
|
+
assert_equal "#112233", @widget.reload.accent
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
test "other attributes of the same record stay updatable" do
|
|
37
|
+
Ability.restrictions = -> { cannot :update, Widget, [ :accent ] }
|
|
38
|
+
|
|
39
|
+
patch_field "name", "text_field", name: "Renamed"
|
|
40
|
+
|
|
41
|
+
assert_response :success
|
|
42
|
+
assert_equal "Renamed", @widget.reload.name
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
test "an attribute-scoped `can :update` only covers the listed attributes" do
|
|
46
|
+
Ability.restrictions = lambda do
|
|
47
|
+
cannot :update, Widget
|
|
48
|
+
can :update, Widget, [ :name ]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
patch_field "accent", "color_field", accent: "#ff0000"
|
|
52
|
+
assert_response :forbidden
|
|
53
|
+
assert_equal "#112233", @widget.reload.accent
|
|
54
|
+
|
|
55
|
+
patch_field "name", "text_field", name: "Renamed"
|
|
56
|
+
assert_response :success
|
|
57
|
+
assert_equal "Renamed", @widget.reload.name
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
test "edit of a `cannot :update` attribute is refused" do
|
|
61
|
+
Ability.restrictions = -> { cannot :update, Widget, [ :accent ] }
|
|
62
|
+
|
|
63
|
+
get edit_widget_path(@widget, attribute: "accent", form_element: "color_field", update: @frame),
|
|
64
|
+
headers: frame_headers(@frame)
|
|
65
|
+
|
|
66
|
+
assert_response :forbidden
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
test "single-field show of a `cannot :read` attribute is refused" do
|
|
70
|
+
Ability.restrictions = -> { cannot :read, Widget, [ :accent ] }
|
|
71
|
+
|
|
72
|
+
get widget_path(@widget, attribute: "accent", form_element: "color_field", update: @frame),
|
|
73
|
+
headers: frame_headers(@frame)
|
|
74
|
+
|
|
75
|
+
assert_response :forbidden
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
test "a form_element that does not match the attribute's row is rejected" do
|
|
79
|
+
patch_field "accent", "text_field", accent: "not a color"
|
|
80
|
+
|
|
81
|
+
assert_response :bad_request
|
|
82
|
+
assert_equal "#112233", @widget.reload.accent
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
test "an unknown form_element is rejected before it is dispatched" do
|
|
86
|
+
patch_field "name", "nonexistent", name: "Renamed"
|
|
87
|
+
|
|
88
|
+
assert_response :bad_request
|
|
89
|
+
assert_equal "Alpha", @widget.reload.name
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
test "an attribute outside the attribute list is rejected" do
|
|
93
|
+
patch_field "kind_id", "text_field", kind_id: "42"
|
|
94
|
+
|
|
95
|
+
assert_response :bad_request
|
|
96
|
+
assert_nil @widget.reload.kind_id
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
test "missing attribute or form_element is rejected" do
|
|
100
|
+
patch widget_path(@widget, update: @frame), params: { name: "x" }, headers: frame_headers(@frame)
|
|
101
|
+
assert_response :bad_request
|
|
102
|
+
|
|
103
|
+
get edit_widget_path(@widget, attribute: "name", update: @frame), headers: frame_headers(@frame)
|
|
104
|
+
assert_response :bad_request
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
test "show cannot be used to send an arbitrary method to the record" do
|
|
108
|
+
get widget_path(@widget, attribute: "destroy", form_element: "has_one", update: @frame),
|
|
109
|
+
headers: frame_headers(@frame)
|
|
110
|
+
|
|
111
|
+
assert_response :bad_request
|
|
112
|
+
assert Widget.exists?(@widget.id)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
test "a delegating element's link name (plain_text_area -> plain_text) is accepted" do
|
|
116
|
+
patch_field "notes", "plain_text", notes: "Long enough notes"
|
|
117
|
+
|
|
118
|
+
assert_response :success
|
|
119
|
+
assert_equal "Long enough notes", @widget.reload.notes
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
test "attribute_list_row_for honours the declared element and its delegate only" do
|
|
123
|
+
list = Widget.new.inline_forms_attribute_list
|
|
124
|
+
|
|
125
|
+
assert_equal [ :notes, :plain_text_area ], InlineForms.attribute_list_row_for(list, "notes", "plain_text_area")
|
|
126
|
+
assert_equal [ :notes, :plain_text_area ], InlineForms.attribute_list_row_for(list, "notes", "plain_text")
|
|
127
|
+
assert_nil InlineForms.attribute_list_row_for(list, "notes", "rich_text")
|
|
128
|
+
assert_nil InlineForms.attribute_list_row_for(list, "name", nil)
|
|
129
|
+
assert_nil InlineForms.attribute_list_row_for(list, nil, "text_field")
|
|
130
|
+
end
|
|
131
|
+
end
|