tessa 2.0 → 6.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -4
- data/app/assets/javascripts/tessa.esm.js +212 -173
- data/app/assets/javascripts/tessa.js +206 -167
- data/app/javascript/activestorage/file_checksum.ts +76 -0
- data/app/javascript/tessa/index.ts +264 -0
- data/app/javascript/tessa/types.ts +34 -0
- data/config/routes.rb +0 -1
- data/lib/tessa/simple_form/asset_input.rb +24 -25
- data/lib/tessa/version.rb +1 -1
- data/lib/tessa.rb +0 -80
- data/package.json +7 -2
- data/rollup.config.js +5 -5
- data/spec/dummy/app/models/single_asset_model.rb +1 -1
- data/spec/dummy/app/models/single_asset_model_form.rb +3 -5
- data/spec/dummy/bin/rails +2 -2
- data/spec/dummy/bin/rake +2 -2
- data/spec/dummy/bin/setup +14 -6
- data/spec/dummy/bin/yarn +9 -3
- data/spec/dummy/config/application.rb +11 -9
- data/spec/dummy/config/boot.rb +1 -1
- data/spec/dummy/config/environment.rb +1 -1
- data/spec/dummy/config/environments/development.rb +34 -5
- data/spec/dummy/config/environments/production.rb +49 -10
- data/spec/dummy/config/environments/test.rb +28 -12
- data/spec/dummy/config/initializers/backtrace_silencers.rb +4 -3
- data/spec/dummy/config/initializers/content_security_policy.rb +5 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +3 -1
- data/spec/dummy/config/initializers/new_framework_defaults_6_1.rb +67 -0
- data/spec/dummy/config/initializers/permissions_policy.rb +11 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +5 -0
- data/spec/dummy/config/locales/en.yml +1 -1
- data/spec/dummy/config/routes.rb +1 -1
- data/spec/dummy/config/storage.yml +31 -0
- data/spec/dummy/config.ru +2 -1
- data/spec/dummy/db/migrate/20230406194400_add_service_name_to_active_storage_blobs.active_storage.rb +22 -0
- data/spec/dummy/db/migrate/20230406194401_create_active_storage_variant_records.active_storage.rb +27 -0
- data/spec/dummy/db/schema.rb +15 -7
- data/tessa.gemspec +4 -5
- data/tsconfig.json +10 -0
- data/yarn.lock +74 -7
- metadata +36 -74
- data/app/javascript/activestorage/file_checksum.js +0 -53
- data/app/javascript/tessa/index.js.coffee +0 -183
- data/lib/tasks/tessa.rake +0 -18
- data/lib/tessa/active_storage/asset_wrapper.rb +0 -32
- data/lib/tessa/asset/failure.rb +0 -37
- data/lib/tessa/asset.rb +0 -47
- data/lib/tessa/asset_change.rb +0 -49
- data/lib/tessa/asset_change_set.rb +0 -49
- data/lib/tessa/config.rb +0 -16
- data/lib/tessa/controller_helpers.rb +0 -16
- data/lib/tessa/fake_connection.rb +0 -29
- data/lib/tessa/jobs/migrate_assets_job.rb +0 -222
- data/lib/tessa/model/dynamic_extensions.rb +0 -145
- data/lib/tessa/model/field.rb +0 -77
- data/lib/tessa/model.rb +0 -94
- data/lib/tessa/rack_upload_proxy.rb +0 -41
- data/lib/tessa/response_factory.rb +0 -15
- data/lib/tessa/upload/uploads_file.rb +0 -18
- data/lib/tessa/upload.rb +0 -31
- data/lib/tessa/view_helpers.rb +0 -23
- data/spec/dummy/app/models/multiple_asset_model.rb +0 -8
- data/spec/support/remote_call_macro.rb +0 -40
- data/spec/tessa/asset/failure_spec.rb +0 -48
- data/spec/tessa/asset_change_set_spec.rb +0 -198
- data/spec/tessa/asset_change_spec.rb +0 -86
- data/spec/tessa/asset_spec.rb +0 -196
- data/spec/tessa/config_spec.rb +0 -70
- data/spec/tessa/controller_helpers_spec.rb +0 -55
- data/spec/tessa/jobs/migrate_assets_job_spec.rb +0 -247
- data/spec/tessa/model_field_spec.rb +0 -72
- data/spec/tessa/model_spec.rb +0 -325
- data/spec/tessa/rack_upload_proxy_spec.rb +0 -83
- data/spec/tessa/upload/uploads_file_spec.rb +0 -72
- data/spec/tessa/upload_spec.rb +0 -125
- data/spec/tessa_spec.rb +0 -23
data/lib/tessa/asset/failure.rb
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
require 'delegate'
|
2
|
-
|
3
|
-
class Tessa::Asset::Failure < SimpleDelegator
|
4
|
-
attr_reader :message
|
5
|
-
|
6
|
-
def initialize(id:, message:)
|
7
|
-
@message = message
|
8
|
-
super(::Tessa::Asset.new(id: id))
|
9
|
-
end
|
10
|
-
|
11
|
-
def self.factory(id:, response:)
|
12
|
-
new(id: id, message: message_from_status(response.status))
|
13
|
-
end
|
14
|
-
|
15
|
-
def self.message_from_status(status)
|
16
|
-
case status.to_s
|
17
|
-
when /5\d{2}/
|
18
|
-
"The service is unavailable at this time."
|
19
|
-
when /4\d{2}/
|
20
|
-
"There was a problem retrieving the data for this asset."
|
21
|
-
else
|
22
|
-
"An error occurred."
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def failure?
|
27
|
-
true
|
28
|
-
end
|
29
|
-
|
30
|
-
def meta
|
31
|
-
{
|
32
|
-
name: "Not Found",
|
33
|
-
size: "0",
|
34
|
-
mime_type: "application/octet-stream"
|
35
|
-
}
|
36
|
-
end
|
37
|
-
end
|
data/lib/tessa/asset.rb
DELETED
@@ -1,47 +0,0 @@
|
|
1
|
-
module Tessa
|
2
|
-
class Asset
|
3
|
-
include Virtus.model
|
4
|
-
extend ResponseFactory
|
5
|
-
|
6
|
-
attribute :id, Integer
|
7
|
-
attribute :status, String
|
8
|
-
attribute :strategy, String
|
9
|
-
attribute :meta, Hash[Symbol => String]
|
10
|
-
attribute :public_url, String
|
11
|
-
attribute :private_url, String
|
12
|
-
attribute :private_download_url, String
|
13
|
-
attribute :delete_url, String
|
14
|
-
|
15
|
-
def complete!(connection: Tessa.config.connection)
|
16
|
-
Asset.new_from_response connection.patch("/assets/#{id}/completed")
|
17
|
-
end
|
18
|
-
|
19
|
-
def cancel!(connection: Tessa.config.connection)
|
20
|
-
Asset.new_from_response connection.patch("/assets/#{id}/cancelled")
|
21
|
-
end
|
22
|
-
|
23
|
-
def delete!(connection: Tessa.config.connection)
|
24
|
-
Asset.new_from_response connection.delete("/assets/#{id}")
|
25
|
-
end
|
26
|
-
|
27
|
-
def self.find(*ids,
|
28
|
-
connection: Tessa.config.connection)
|
29
|
-
new_from_response connection.get("/assets/#{ids.join(",")}")
|
30
|
-
end
|
31
|
-
|
32
|
-
def self.create(file:, **options)
|
33
|
-
default_options = {
|
34
|
-
size: File.size(file),
|
35
|
-
name: File.basename(file),
|
36
|
-
date: File.mtime(file),
|
37
|
-
}
|
38
|
-
Upload.create(default_options.merge(options)).upload_file(file)
|
39
|
-
end
|
40
|
-
|
41
|
-
def failure?
|
42
|
-
false
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
require 'tessa/asset/failure'
|
data/lib/tessa/asset_change.rb
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
module Tessa
|
2
|
-
class AssetChange
|
3
|
-
include Virtus.model
|
4
|
-
|
5
|
-
attribute :id, Integer
|
6
|
-
attribute :action, String
|
7
|
-
|
8
|
-
def initialize(args={})
|
9
|
-
case args
|
10
|
-
when Array
|
11
|
-
id, attributes = args
|
12
|
-
super attributes.merge(id: id)
|
13
|
-
else
|
14
|
-
super
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def apply
|
19
|
-
if add?
|
20
|
-
asset.complete!
|
21
|
-
elsif remove?
|
22
|
-
asset.delete!
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def hash
|
27
|
-
[id, action].hash
|
28
|
-
end
|
29
|
-
|
30
|
-
def eql?(b)
|
31
|
-
self.class == b.class &&
|
32
|
-
self.hash == b.hash
|
33
|
-
end
|
34
|
-
|
35
|
-
def add?
|
36
|
-
action == 'add'
|
37
|
-
end
|
38
|
-
|
39
|
-
def remove?
|
40
|
-
action == 'remove'
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
def asset
|
46
|
-
Tessa::Asset.new(id: id)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
@@ -1,49 +0,0 @@
|
|
1
|
-
module Tessa
|
2
|
-
class AssetChangeSet
|
3
|
-
include Virtus.model
|
4
|
-
|
5
|
-
attribute :changes, Array[AssetChange]
|
6
|
-
attribute :scoped_ids, Array[Integer]
|
7
|
-
|
8
|
-
def scoped_ids=(new_ids)
|
9
|
-
super new_ids.compact
|
10
|
-
end
|
11
|
-
|
12
|
-
def scoped_changes
|
13
|
-
changes.select { |change| scoped_ids.include?(change.id) }
|
14
|
-
end
|
15
|
-
|
16
|
-
def apply
|
17
|
-
scoped_changes.uniq.each(&:apply)
|
18
|
-
end
|
19
|
-
|
20
|
-
def +(b)
|
21
|
-
self.changes = (self.changes + b.changes).uniq
|
22
|
-
self.scoped_ids = (self.scoped_ids + b.scoped_ids).uniq
|
23
|
-
self
|
24
|
-
end
|
25
|
-
|
26
|
-
def add(value)
|
27
|
-
id = id_from_asset(value)
|
28
|
-
changes << AssetChange.new(id: id, action: "add")
|
29
|
-
scoped_ids << id
|
30
|
-
end
|
31
|
-
|
32
|
-
def remove(value)
|
33
|
-
id = id_from_asset(value)
|
34
|
-
changes << AssetChange.new(id: id, action: "remove")
|
35
|
-
scoped_ids << id
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
|
40
|
-
def id_from_asset(value)
|
41
|
-
case value
|
42
|
-
when Asset
|
43
|
-
value.id
|
44
|
-
when Fixnum
|
45
|
-
value
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
data/lib/tessa/config.rb
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
module Tessa
|
2
|
-
class Config
|
3
|
-
include Virtus.model
|
4
|
-
|
5
|
-
DEFAULT_STRATEGY = "default"
|
6
|
-
|
7
|
-
attribute :username, String, default: -> (*_) { ENV['TESSA_USERNAME'] }
|
8
|
-
attribute :password, String, default: -> (*_) { ENV['TESSA_PASSWORD'] }
|
9
|
-
attribute :url, String, default: -> (*_) { ENV['TESSA_URL'] }
|
10
|
-
attribute :strategy, String, default: -> (*_) { ENV['TESSA_STRATEGY'] || DEFAULT_STRATEGY }
|
11
|
-
|
12
|
-
def connection
|
13
|
-
@connection ||= Tessa::FakeConnection.new
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
@@ -1,16 +0,0 @@
|
|
1
|
-
module Tessa
|
2
|
-
module ControllerHelpers
|
3
|
-
|
4
|
-
def params_for_asset(changes)
|
5
|
-
Tessa::AssetChangeSet.new(
|
6
|
-
changes: changes,
|
7
|
-
scoped_ids: tessa_upload_asset_ids,
|
8
|
-
)
|
9
|
-
end
|
10
|
-
|
11
|
-
def tessa_upload_asset_ids
|
12
|
-
session[:tessa_upload_asset_ids] ||= []
|
13
|
-
end
|
14
|
-
|
15
|
-
end
|
16
|
-
end
|
@@ -1,29 +0,0 @@
|
|
1
|
-
module Tessa
|
2
|
-
# Since we no longer connect to the Tessa service, fake out the Tessa connection
|
3
|
-
# so that it always returns 503
|
4
|
-
class FakeConnection
|
5
|
-
|
6
|
-
[:get, :head, :put, :post, :patch, :delete].each do |method|
|
7
|
-
define_method(method) do |*args|
|
8
|
-
if defined?(Bugsnag)
|
9
|
-
Bugsnag.notify("Tessa::FakeConnection##{method} invoked")
|
10
|
-
end
|
11
|
-
Tessa::FakeConnection::Response.new()
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
class Response
|
16
|
-
def success?
|
17
|
-
false
|
18
|
-
end
|
19
|
-
|
20
|
-
def status
|
21
|
-
503
|
22
|
-
end
|
23
|
-
|
24
|
-
def body
|
25
|
-
'{ "error": "The Tessa connection is no longer implemented." }'
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
@@ -1,222 +0,0 @@
|
|
1
|
-
require 'open-uri'
|
2
|
-
|
3
|
-
class Tessa::MigrateAssetsJob < ActiveJob::Base
|
4
|
-
|
5
|
-
queue_as :low
|
6
|
-
|
7
|
-
def perform(*args)
|
8
|
-
options = args&.extract_options!
|
9
|
-
options = {
|
10
|
-
batch_size: 10,
|
11
|
-
interval: 10.minutes.to_i
|
12
|
-
}.merge!(options&.symbolize_keys || {})
|
13
|
-
|
14
|
-
interval = options[:interval].seconds
|
15
|
-
processing_state = args.first ? Marshal.load(args.first) : load_models_from_registry
|
16
|
-
|
17
|
-
if processing_state.fully_processed?
|
18
|
-
Rails.logger.info("Nothing to do - all models have transitioned to ActiveStorage")
|
19
|
-
return
|
20
|
-
end
|
21
|
-
|
22
|
-
processing_state.batch_count = 0
|
23
|
-
while processing_state.batch_count < options[:batch_size]
|
24
|
-
model_state = processing_state.next_model
|
25
|
-
|
26
|
-
process(processing_state, model_state, options)
|
27
|
-
|
28
|
-
break if processing_state.fully_processed?
|
29
|
-
end
|
30
|
-
|
31
|
-
if processing_state.fully_processed?
|
32
|
-
Rails.logger.info("Finished processing all Tessa assets")
|
33
|
-
else
|
34
|
-
remaining_batches = (processing_state.count / options[:batch_size].to_f).ceil
|
35
|
-
|
36
|
-
Rails.logger.info("Continuing processing in #{interval}, "\
|
37
|
-
"ETA #{(remaining_batches * interval).from_now}. "\
|
38
|
-
"Working on #{processing_state.next_model.next_field}")
|
39
|
-
|
40
|
-
processing_state.batch_count = 0
|
41
|
-
self.class.set(wait: interval)
|
42
|
-
.perform_later(Marshal.dump(processing_state), options)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
private
|
47
|
-
|
48
|
-
def process(processing_state, model_state, options)
|
49
|
-
while processing_state.batch_count < options[:batch_size]
|
50
|
-
field_state = model_state.next_field
|
51
|
-
|
52
|
-
process_field(processing_state, field_state, options)
|
53
|
-
|
54
|
-
return if model_state.fully_processed?
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def process_field(processing_state, field_state, options)
|
59
|
-
while processing_state.batch_count < options[:batch_size]
|
60
|
-
remaining = options[:batch_size] - processing_state.batch_count
|
61
|
-
|
62
|
-
next_batch = field_state.query
|
63
|
-
.offset(field_state.offset)
|
64
|
-
.limit(remaining)
|
65
|
-
|
66
|
-
next_batch.each_with_index do |record, idx|
|
67
|
-
begin
|
68
|
-
# Wait 1 second in between records to slow things down and let the
|
69
|
-
# system process for a bit
|
70
|
-
sleep 1 if idx > 0
|
71
|
-
|
72
|
-
reupload(record, field_state)
|
73
|
-
Rails.logger.info("#{record.class}#{record.id}##{field_state.field_name}: success")
|
74
|
-
field_state.success_count += 1
|
75
|
-
rescue StandardError => ex
|
76
|
-
Rails.logger.error("#{record.class}#{record.id}##{field_state.field_name}: error - #{ex}")
|
77
|
-
field_state.offset += 1
|
78
|
-
ensure
|
79
|
-
processing_state.batch_count += 1
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
return if field_state.fully_processed?
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def reupload(record, field_state)
|
88
|
-
if field_state.tessa_field.multiple?
|
89
|
-
reupload_multiple(record, field_state)
|
90
|
-
else
|
91
|
-
reupload_single(record, field_state)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
def reupload_single(record, field_state)
|
96
|
-
# models with ActiveStorage uploads have nil in the column, but if you call
|
97
|
-
# the method it hits the dynamic extensions and gives you the blob key
|
98
|
-
database_id = record.attributes["_tessa_#{field_state.tessa_field.id_field}"]
|
99
|
-
return unless database_id
|
100
|
-
|
101
|
-
asset = Tessa::Asset.find(database_id)
|
102
|
-
|
103
|
-
attachable = {
|
104
|
-
io: URI.open(asset.private_download_url),
|
105
|
-
filename: asset.meta[:name]
|
106
|
-
}
|
107
|
-
|
108
|
-
record.public_send("#{field_state.tessa_field.name}=", attachable)
|
109
|
-
record.save!
|
110
|
-
end
|
111
|
-
|
112
|
-
def reupload_multiple(record, field_state)
|
113
|
-
database_ids = record.attributes["_tessa_#{field_state.tessa_field.id_field}"]
|
114
|
-
return unless database_ids
|
115
|
-
|
116
|
-
assets = Tessa::Asset.find(database_ids)
|
117
|
-
|
118
|
-
attachables = assets.map do |asset|
|
119
|
-
{
|
120
|
-
io: URI.open(asset.private_download_url),
|
121
|
-
filename: asset.meta[:name]
|
122
|
-
}
|
123
|
-
end
|
124
|
-
|
125
|
-
record.public_send("#{field_state.tessa_field.name}=", attachables)
|
126
|
-
record.save!
|
127
|
-
end
|
128
|
-
|
129
|
-
def load_models_from_registry
|
130
|
-
# Initialize our Record Keeping object
|
131
|
-
ProcessingState.initialize_from_models
|
132
|
-
end
|
133
|
-
|
134
|
-
# Determines whether the migrate asset job is completed. If true, running the
|
135
|
-
# job again will not do anything.
|
136
|
-
def self.complete?
|
137
|
-
ProcessingState.initialize_from_models.fully_processed?
|
138
|
-
end
|
139
|
-
|
140
|
-
ProcessingState = Struct.new(:model_queue, :batch_count) do
|
141
|
-
def self.initialize_from_models(models = nil)
|
142
|
-
unless models
|
143
|
-
# Load all Tessa models that can have attachments (not form objects)
|
144
|
-
Rails.application.eager_load!
|
145
|
-
models = Tessa.model_registry.select { |m| m.respond_to?(:has_one_attached) }
|
146
|
-
end
|
147
|
-
|
148
|
-
new(
|
149
|
-
models.map do |model|
|
150
|
-
ModelProcessingState.initialize_from_model(model)
|
151
|
-
end,
|
152
|
-
0
|
153
|
-
)
|
154
|
-
end
|
155
|
-
|
156
|
-
def next_model
|
157
|
-
model_queue.detect { |i| !i.fully_processed? }
|
158
|
-
end
|
159
|
-
|
160
|
-
def fully_processed?
|
161
|
-
model_queue.all?(&:fully_processed?)
|
162
|
-
end
|
163
|
-
|
164
|
-
def count
|
165
|
-
model_queue.sum { |m| m.field_queue.sum { |f| f.count - f.offset } }
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
ModelProcessingState = Struct.new(:class_name, :field_queue) do
|
170
|
-
def self.initialize_from_model(model)
|
171
|
-
new(
|
172
|
-
model.name,
|
173
|
-
model.tessa_fields.map do |name, _|
|
174
|
-
FieldProcessingState.initialize_from_model(model, name)
|
175
|
-
end
|
176
|
-
)
|
177
|
-
end
|
178
|
-
|
179
|
-
def next_field
|
180
|
-
field_queue.detect { |i| !i.fully_processed? }
|
181
|
-
end
|
182
|
-
|
183
|
-
def model
|
184
|
-
@model ||= class_name.constantize
|
185
|
-
end
|
186
|
-
|
187
|
-
def fully_processed?
|
188
|
-
field_queue.all?(&:fully_processed?)
|
189
|
-
end
|
190
|
-
end
|
191
|
-
|
192
|
-
FieldProcessingState = Struct.new(:class_name, :field_name, :offset, :success_count) do
|
193
|
-
def self.initialize_from_model(model, field_name)
|
194
|
-
new(
|
195
|
-
model.name,
|
196
|
-
field_name,
|
197
|
-
0,
|
198
|
-
0
|
199
|
-
)
|
200
|
-
end
|
201
|
-
|
202
|
-
def model
|
203
|
-
@model ||= class_name.constantize
|
204
|
-
end
|
205
|
-
|
206
|
-
def tessa_field
|
207
|
-
model.tessa_fields[field_name]
|
208
|
-
end
|
209
|
-
|
210
|
-
def query
|
211
|
-
model.where.not(Hash[tessa_field.id_field, nil])
|
212
|
-
end
|
213
|
-
|
214
|
-
def count
|
215
|
-
query.count
|
216
|
-
end
|
217
|
-
|
218
|
-
def fully_processed?
|
219
|
-
offset >= count
|
220
|
-
end
|
221
|
-
end
|
222
|
-
end
|
@@ -1,145 +0,0 @@
|
|
1
|
-
require 'forwardable'
|
2
|
-
|
3
|
-
class Tessa::DynamicExtensions
|
4
|
-
extend Forwardable
|
5
|
-
|
6
|
-
attr_reader :field
|
7
|
-
|
8
|
-
def name
|
9
|
-
field.name
|
10
|
-
end
|
11
|
-
|
12
|
-
def initialize(field)
|
13
|
-
@field = field
|
14
|
-
end
|
15
|
-
|
16
|
-
class SingleRecord < ::Tessa::DynamicExtensions
|
17
|
-
def build(mod)
|
18
|
-
mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
|
19
|
-
def #{name}
|
20
|
-
# has_one_attached defines the getter using class_eval so we can't call
|
21
|
-
# super() here.
|
22
|
-
if #{name}_attachment.present?
|
23
|
-
return Tessa::ActiveStorage::AssetWrapper.new(#{name}_attachment)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def #{field.id_field}
|
28
|
-
# Use the attachment's key
|
29
|
-
return #{name}_attachment.key if #{name}_attachment.present?
|
30
|
-
end
|
31
|
-
|
32
|
-
def #{name}=(attachable)
|
33
|
-
# Every new upload is going to ActiveStorage
|
34
|
-
a = @active_storage_attached_#{name} ||=
|
35
|
-
::ActiveStorage::Attached::One.new("#{name}", self, dependent: :purge_later)
|
36
|
-
|
37
|
-
case attachable
|
38
|
-
when Tessa::AssetChangeSet
|
39
|
-
attachable.changes.select(&:remove?).each { a.detach }
|
40
|
-
attachable.changes.select(&:add?).each do |change|
|
41
|
-
next if #{field.id_field} == change.id
|
42
|
-
|
43
|
-
a.attach(change.id)
|
44
|
-
end
|
45
|
-
when nil
|
46
|
-
a.detach
|
47
|
-
else
|
48
|
-
a.attach(attachable)
|
49
|
-
end
|
50
|
-
|
51
|
-
# overwrite the tessa ID in the database
|
52
|
-
self.#{field.id_field} = nil
|
53
|
-
end
|
54
|
-
|
55
|
-
def attributes
|
56
|
-
super.merge({
|
57
|
-
'#{field.id_field}' => #{field.id_field},
|
58
|
-
'_tessa_#{field.id_field}' => super['#{field.id_field}']
|
59
|
-
})
|
60
|
-
end
|
61
|
-
CODE
|
62
|
-
mod
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
class MultipleRecord < ::Tessa::DynamicExtensions
|
67
|
-
def build(mod)
|
68
|
-
mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
|
69
|
-
def #{name}
|
70
|
-
field = self.class.tessa_fields["#{name}".to_sym]
|
71
|
-
tessa_ids = field.id(on: self) - #{name}_attachments.map(&:key)
|
72
|
-
|
73
|
-
@#{name} ||= [
|
74
|
-
*#{name}_attachments.map { |a| Tessa::ActiveStorage::AssetWrapper.new(a) },
|
75
|
-
]
|
76
|
-
end
|
77
|
-
|
78
|
-
def #{field.id_field}
|
79
|
-
[
|
80
|
-
# Use the attachment's key
|
81
|
-
*#{name}_attachments.map(&:key),
|
82
|
-
]
|
83
|
-
end
|
84
|
-
|
85
|
-
def #{name}=(attachables)
|
86
|
-
# Every new upload is going to ActiveStorage
|
87
|
-
a = @active_storage_attached_#{name} ||=
|
88
|
-
::ActiveStorage::Attached::Many.new("#{name}", self, dependent: :purge_later)
|
89
|
-
|
90
|
-
case attachables
|
91
|
-
when Tessa::AssetChangeSet
|
92
|
-
attachables.changes.select(&:remove?).each do |change|
|
93
|
-
if existing = #{name}_attachments.find { |a| a.key == change.id }
|
94
|
-
existing.destroy
|
95
|
-
else
|
96
|
-
ids = self.#{field.id_field}
|
97
|
-
ids&.delete(change.id.to_i)
|
98
|
-
self.#{field.id_field} = ids&.any? ? ids : nil
|
99
|
-
end
|
100
|
-
end
|
101
|
-
attachables.changes.select(&:add?).each do |change|
|
102
|
-
next if #{field.id_field}.include? change.id
|
103
|
-
|
104
|
-
a.attach(change.id)
|
105
|
-
end
|
106
|
-
when nil
|
107
|
-
a.detach
|
108
|
-
self.#{field.id_field} = nil
|
109
|
-
else
|
110
|
-
a.attach(*attachables)
|
111
|
-
self.#{field.id_field} = nil
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
def attributes
|
116
|
-
super.merge({
|
117
|
-
'#{field.id_field}' => #{field.id_field},
|
118
|
-
'_tessa_#{field.id_field}' => super['#{field.id_field}']
|
119
|
-
})
|
120
|
-
end
|
121
|
-
CODE
|
122
|
-
mod
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
class SingleFormObject < ::Tessa::DynamicExtensions
|
127
|
-
def build(mod)
|
128
|
-
mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
|
129
|
-
attr_accessor :#{name}_id
|
130
|
-
attr_accessor :#{name}
|
131
|
-
CODE
|
132
|
-
mod
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
class MultipleFormObject < ::Tessa::DynamicExtensions
|
137
|
-
def build(mod)
|
138
|
-
mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
|
139
|
-
attr_accessor :#{name}_ids
|
140
|
-
attr_accessor :#{name}
|
141
|
-
CODE
|
142
|
-
mod
|
143
|
-
end
|
144
|
-
end
|
145
|
-
end
|
data/lib/tessa/model/field.rb
DELETED
@@ -1,77 +0,0 @@
|
|
1
|
-
module Tessa
|
2
|
-
module Model
|
3
|
-
class Field
|
4
|
-
include Virtus.model
|
5
|
-
|
6
|
-
attribute :model
|
7
|
-
attribute :name, String
|
8
|
-
attribute :multiple, Boolean, default: false
|
9
|
-
attribute :id_field, String
|
10
|
-
|
11
|
-
def id_field
|
12
|
-
super || "#{name}#{default_id_field_suffix}"
|
13
|
-
end
|
14
|
-
|
15
|
-
def ids(on:)
|
16
|
-
[*id(on: on)]
|
17
|
-
end
|
18
|
-
|
19
|
-
def id(on:)
|
20
|
-
on.public_send(id_field)
|
21
|
-
end
|
22
|
-
|
23
|
-
def apply(set, on:)
|
24
|
-
ids = ids(on: on)
|
25
|
-
|
26
|
-
set.scoped_changes.each do |change|
|
27
|
-
if change.add?
|
28
|
-
ids << change.id
|
29
|
-
elsif change.remove?
|
30
|
-
ids.delete change.id
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
if multiple?
|
35
|
-
on.public_send(id_field_writer, ids)
|
36
|
-
else
|
37
|
-
on.public_send(id_field_writer, ids.first)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def change_set_for(value)
|
42
|
-
case value
|
43
|
-
when AssetChangeSet
|
44
|
-
value
|
45
|
-
when Array
|
46
|
-
value.map { |item| change_set_for(item) }.reduce(:+)
|
47
|
-
when Asset
|
48
|
-
AssetChangeSet.new.tap { |set| set.add(value) }
|
49
|
-
else
|
50
|
-
AssetChangeSet.new
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def difference_change_set(subtrahend_ids, on:)
|
55
|
-
AssetChangeSet.new.tap do |change_set|
|
56
|
-
(ids(on: on) - subtrahend_ids).each do |id|
|
57
|
-
change_set.remove(id)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
private
|
63
|
-
|
64
|
-
def id_field_writer
|
65
|
-
"#{id_field}="
|
66
|
-
end
|
67
|
-
|
68
|
-
def default_id_field_suffix
|
69
|
-
if multiple
|
70
|
-
"_ids"
|
71
|
-
else
|
72
|
-
"_id"
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|