Skip to content

Commit 7170747

Browse files
committed
Add BetterTogether::Seed
1 parent 20d1abc commit 7170747

File tree

32 files changed

+955
-10
lines changed

32 files changed

+955
-10
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# frozen_string_literal: true
2+
3+
module BetterTogether
4+
# CRUD for Seed records
5+
class SeedsController < ApplicationController
6+
before_action :set_seed, only: %i[show edit update destroy]
7+
8+
# GET /seeds
9+
def index
10+
@seeds = Seed.all
11+
end
12+
13+
# GET /seeds/1
14+
def show; end
15+
16+
# GET /seeds/new
17+
def new
18+
@seed = Seed.new
19+
end
20+
21+
# GET /seeds/1/edit
22+
def edit; end
23+
24+
# POST /seeds
25+
def create
26+
@seed = Seed.new(seed_params)
27+
28+
if @seed.save
29+
redirect_to @seed, notice: 'Seed was successfully created.'
30+
else
31+
render :new, status: :unprocessable_entity
32+
end
33+
end
34+
35+
# PATCH/PUT /seeds/1
36+
def update
37+
if @seed.update(seed_params)
38+
redirect_to @seed, notice: 'Seed was successfully updated.', status: :see_other
39+
else
40+
render :edit, status: :unprocessable_entity
41+
end
42+
end
43+
44+
# DELETE /seeds/1
45+
def destroy
46+
@seed.destroy!
47+
redirect_to seeds_url, notice: 'Seed was successfully destroyed.', status: :see_other
48+
end
49+
50+
private
51+
52+
# Use callbacks to share common setup or constraints between actions.
53+
def set_seed
54+
@seed = Seed.find(params[:id])
55+
end
56+
57+
# Only allow a list of trusted parameters through.
58+
def seed_params
59+
params.fetch(:seed, {})
60+
end
61+
end
62+
end
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
3+
module BetterTogether
4+
module SeedsHelper # rubocop:todo Style/Documentation
5+
end
6+
end

app/models/better_together/application_record.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module BetterTogether
55
class ApplicationRecord < ActiveRecord::Base
66
self.abstract_class = true
77
include BetterTogetherId
8+
include Seedable
89

910
def self.extra_permitted_attributes
1011
[]

app/models/better_together/person.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def self.primary_community_delegation_attrs
1717
include Member
1818
include PrimaryCommunity
1919
include Privacy
20+
include Seedable
2021
include Viewable
2122

2223
include ::Storext.model

app/models/better_together/seed.rb

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# frozen_string_literal: true
2+
3+
module BetterTogether
4+
# Allows for import and export of data in a structured and standardized way
5+
class Seed < ApplicationRecord # rubocop:todo Metrics/ClassLength
6+
self.table_name = 'better_together_seeds'
7+
self.inheritance_column = :type # Defensive for STI safety
8+
9+
include Creatable
10+
include Identifier
11+
include Privacy
12+
13+
DEFAULT_ROOT_KEY = 'better_together'
14+
15+
# 1) Make sure you have Active Storage set up in your app
16+
# This attaches a single YAML file to each seed record
17+
has_one_attached :yaml_file
18+
19+
# 2) Polymorphic association: optional
20+
belongs_to :seedable, polymorphic: true, optional: true
21+
22+
validates :type, :identifier, :version, :created_by, :seeded_at,
23+
:description, :origin, :payload, presence: true
24+
25+
after_create_commit :attach_yaml_file
26+
after_update_commit :attach_yaml_file
27+
28+
# -------------------------------------------------------------
29+
# Scopes
30+
# -------------------------------------------------------------
31+
scope :by_type, ->(type) { where(type: type) }
32+
scope :by_identifier, ->(identifier) { where(identifier: identifier) }
33+
scope :latest_first, -> { order(created_at: :desc) }
34+
scope :latest_version, ->(type, identifier) { by_type(type).by_identifier(identifier).latest_first.limit(1) }
35+
scope :latest, -> { latest_first.limit(1) }
36+
37+
# -------------------------------------------------------------
38+
# Accessor overrides for origin/payload => Indifferent Access
39+
# -------------------------------------------------------------
40+
def origin
41+
super&.with_indifferent_access || {}
42+
end
43+
44+
def payload
45+
super&.with_indifferent_access || {}
46+
end
47+
48+
# Helpers for nested origin data
49+
def contributors
50+
origin[:contributors] || []
51+
end
52+
53+
def platforms
54+
origin[:platforms] || []
55+
end
56+
57+
# -------------------------------------------------------------
58+
# plant = internal DB creation (used by import)
59+
# -------------------------------------------------------------
60+
def self.plant(type:, identifier:, version:, metadata:, content:) # rubocop:todo Metrics/MethodLength
61+
create!(
62+
type: type,
63+
identifier: identifier,
64+
version: version,
65+
created_by: metadata[:created_by],
66+
seeded_at: metadata[:created_at],
67+
description: metadata[:description],
68+
origin: metadata[:origin],
69+
payload: content,
70+
seedable_type: metadata[:seedable_type],
71+
seedable_id: metadata[:seedable_id]
72+
)
73+
end
74+
75+
# -------------------------------------------------------------
76+
# import = read a seed and store in DB
77+
# -------------------------------------------------------------
78+
def self.import(seed_data, root_key: DEFAULT_ROOT_KEY) # rubocop:todo Metrics/MethodLength
79+
data = seed_data.deep_symbolize_keys.fetch(root_key.to_sym)
80+
metadata = data.fetch(:seed)
81+
content = data.except(:version, :seed)
82+
83+
plant(
84+
type: metadata.fetch(:type),
85+
identifier: metadata.fetch(:identifier),
86+
version: data.fetch(:version),
87+
metadata: {
88+
created_by: metadata.fetch(:created_by),
89+
created_at: Time.iso8601(metadata.fetch(:created_at)),
90+
description: metadata.fetch(:description),
91+
origin: metadata.fetch(:origin),
92+
seedable_type: metadata[:seedable_type],
93+
seedable_id: metadata[:seedable_id]
94+
},
95+
content: content
96+
)
97+
end
98+
99+
# -------------------------------------------------------------
100+
# export = produce a structured hash including seedable info
101+
# -------------------------------------------------------------
102+
# rubocop:todo Metrics/MethodLength
103+
def export(root_key: DEFAULT_ROOT_KEY) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
104+
seed_obj = {
105+
type: type,
106+
identifier: identifier,
107+
created_by: created_by,
108+
created_at: seeded_at.iso8601,
109+
description: description,
110+
origin: origin.deep_symbolize_keys
111+
}
112+
113+
# If seedable_type or seedable_id is present, include them
114+
seed_obj[:seedable_type] = seedable_type if seedable_type.present?
115+
seed_obj[:seedable_id] = seedable_id if seedable_id.present?
116+
117+
{
118+
root_key => {
119+
version: version,
120+
seed: seed_obj,
121+
**payload.deep_symbolize_keys
122+
}
123+
}
124+
end
125+
# rubocop:enable Metrics/MethodLength
126+
127+
# Export as YAML
128+
def export_yaml(root_key: DEFAULT_ROOT_KEY)
129+
export(root_key: root_key).deep_stringify_keys.to_yaml
130+
end
131+
132+
# A recommended file name for the exported seed
133+
def versioned_file_name
134+
timestamp = seeded_at.utc.strftime('%Y%m%d%H%M%S')
135+
"#{type.demodulize.underscore}_#{identifier}_v#{version}_#{timestamp}.yml"
136+
end
137+
138+
# -------------------------------------------------------------
139+
# load_seed for file or named namespace
140+
# -------------------------------------------------------------
141+
def self.load_seed(source, root_key: DEFAULT_ROOT_KEY) # rubocop:todo Metrics/MethodLength
142+
# 1) Direct file path
143+
if File.exist?(source)
144+
begin
145+
seed_data = YAML.load_file(source)
146+
return import(seed_data, root_key: root_key)
147+
rescue StandardError => e
148+
raise "Error loading seed from file '#{source}': #{e.message}"
149+
end
150+
end
151+
152+
# 2) 'namespace' approach => config/seeds/#{source}.yml
153+
path = Rails.root.join('config', 'seeds', "#{source}.yml").to_s
154+
raise "Seed file not found for '#{source}' at path '#{path}'" unless File.exist?(path)
155+
156+
begin
157+
seed_data = YAML.load_file(path)
158+
import(seed_data, root_key: root_key)
159+
rescue StandardError => e
160+
raise "Error loading seed from namespace '#{source}' at path '#{path}': #{e.message}"
161+
end
162+
end
163+
164+
# -------------------------------------------------------------
165+
# Attach the exported YAML as an Active Storage file
166+
# -------------------------------------------------------------
167+
def attach_yaml_file
168+
yml_data = export_yaml
169+
yaml_file.attach(
170+
io: StringIO.new(yml_data),
171+
filename: versioned_file_name,
172+
content_type: 'text/yaml'
173+
)
174+
end
175+
end
176+
end

0 commit comments

Comments
 (0)