tessa 2.0 → 6.0.0.rc2
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/README.md +11 -18
- 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
|