Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions app/controllers/better_together/seeds_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

module BetterTogether
# CRUD for Seed records
class SeedsController < ApplicationController
before_action :set_seed, only: %i[show edit update destroy]

# GET /seeds
def index
@seeds = Seed.all
end

# GET /seeds/1
def show; end

# GET /seeds/new
def new
@seed = Seed.new
end

# GET /seeds/1/edit
def edit; end

# POST /seeds
def create
@seed = Seed.new(seed_params)

if @seed.save
redirect_to @seed, notice: 'Seed was successfully created.'
else
render :new, status: :unprocessable_entity
end
end

# PATCH/PUT /seeds/1
def update
if @seed.update(seed_params)
redirect_to @seed, notice: 'Seed was successfully updated.', status: :see_other

Check notice

Code scanning / Brakeman

Possible unprotected redirect. Note

Possible unprotected redirect.
else
render :edit, status: :unprocessable_entity
end
end

# DELETE /seeds/1
def destroy
@seed.destroy!
redirect_to seeds_url, notice: 'Seed was successfully destroyed.', status: :see_other
end

private

# Use callbacks to share common setup or constraints between actions.
def set_seed
@seed = Seed.find(params[:id])
end

# Only allow a list of trusted parameters through.
def seed_params
params.fetch(:seed, {})
end
end
end
6 changes: 6 additions & 0 deletions app/helpers/better_together/seeds_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

module BetterTogether
module SeedsHelper # rubocop:todo Style/Documentation
end
end
1 change: 1 addition & 0 deletions app/models/better_together/application_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module BetterTogether
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
include BetterTogetherId
include Seedable

def self.extra_permitted_attributes
[]
Expand Down
1 change: 1 addition & 0 deletions app/models/better_together/person.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def self.primary_community_delegation_attrs
include Member
include PrimaryCommunity
include Privacy
include Seedable
include Viewable
include Metrics::Viewable
include ::Storext.model
Expand Down
176 changes: 176 additions & 0 deletions app/models/better_together/seed.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# frozen_string_literal: true

module BetterTogether
# Allows for import and export of data in a structured and standardized way
class Seed < ApplicationRecord # rubocop:todo Metrics/ClassLength
self.table_name = 'better_together_seeds'
self.inheritance_column = :type # Defensive for STI safety

include Creatable
include Identifier
include Privacy

DEFAULT_ROOT_KEY = 'better_together'

# 1) Make sure you have Active Storage set up in your app
# This attaches a single YAML file to each seed record
has_one_attached :yaml_file

# 2) Polymorphic association: optional
belongs_to :seedable, polymorphic: true, optional: true

validates :type, :identifier, :version, :created_by, :seeded_at,
:description, :origin, :payload, presence: true

after_create_commit :attach_yaml_file
after_update_commit :attach_yaml_file

# -------------------------------------------------------------
# Scopes
# -------------------------------------------------------------
scope :by_type, ->(type) { where(type: type) }
scope :by_identifier, ->(identifier) { where(identifier: identifier) }
scope :latest_first, -> { order(created_at: :desc) }
scope :latest_version, ->(type, identifier) { by_type(type).by_identifier(identifier).latest_first.limit(1) }
scope :latest, -> { latest_first.limit(1) }

# -------------------------------------------------------------
# Accessor overrides for origin/payload => Indifferent Access
# -------------------------------------------------------------
def origin
super&.with_indifferent_access || {}
end

def payload
super&.with_indifferent_access || {}
end

# Helpers for nested origin data
def contributors
origin[:contributors] || []
end

def platforms
origin[:platforms] || []
end

# -------------------------------------------------------------
# plant = internal DB creation (used by import)
# -------------------------------------------------------------
def self.plant(type:, identifier:, version:, metadata:, content:) # rubocop:todo Metrics/MethodLength
create!(
type: type,
identifier: identifier,
version: version,
created_by: metadata[:created_by],
seeded_at: metadata[:created_at],
description: metadata[:description],
origin: metadata[:origin],
payload: content,
seedable_type: metadata[:seedable_type],
seedable_id: metadata[:seedable_id]
)
end

# -------------------------------------------------------------
# import = read a seed and store in DB
# -------------------------------------------------------------
def self.import(seed_data, root_key: DEFAULT_ROOT_KEY) # rubocop:todo Metrics/MethodLength
data = seed_data.deep_symbolize_keys.fetch(root_key.to_sym)
metadata = data.fetch(:seed)
content = data.except(:version, :seed)

plant(
type: metadata.fetch(:type),
identifier: metadata.fetch(:identifier),
version: data.fetch(:version),
metadata: {
created_by: metadata.fetch(:created_by),
created_at: Time.iso8601(metadata.fetch(:created_at)),
description: metadata.fetch(:description),
origin: metadata.fetch(:origin),
seedable_type: metadata[:seedable_type],
seedable_id: metadata[:seedable_id]
},
content: content
)
end

# -------------------------------------------------------------
# export = produce a structured hash including seedable info
# -------------------------------------------------------------
# rubocop:todo Metrics/MethodLength
def export(root_key: DEFAULT_ROOT_KEY) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
seed_obj = {
type: type,
identifier: identifier,
created_by: created_by,
created_at: seeded_at.iso8601,
description: description,
origin: origin.deep_symbolize_keys
}

# If seedable_type or seedable_id is present, include them
seed_obj[:seedable_type] = seedable_type if seedable_type.present?
seed_obj[:seedable_id] = seedable_id if seedable_id.present?

{
root_key => {
version: version,
seed: seed_obj,
**payload.deep_symbolize_keys
}
}
end
# rubocop:enable Metrics/MethodLength

# Export as YAML
def export_yaml(root_key: DEFAULT_ROOT_KEY)
export(root_key: root_key).deep_stringify_keys.to_yaml
end

# A recommended file name for the exported seed
def versioned_file_name
timestamp = seeded_at.utc.strftime('%Y%m%d%H%M%S')
"#{type.demodulize.underscore}_#{identifier}_v#{version}_#{timestamp}.yml"
end

# -------------------------------------------------------------
# load_seed for file or named namespace
# -------------------------------------------------------------
def self.load_seed(source, root_key: DEFAULT_ROOT_KEY) # rubocop:todo Metrics/MethodLength
# 1) Direct file path
if File.exist?(source)
begin
seed_data = YAML.load_file(source)
return import(seed_data, root_key: root_key)
rescue StandardError => e
raise "Error loading seed from file '#{source}': #{e.message}"
end
end

# 2) 'namespace' approach => config/seeds/#{source}.yml
path = Rails.root.join('config', 'seeds', "#{source}.yml").to_s
raise "Seed file not found for '#{source}' at path '#{path}'" unless File.exist?(path)

begin
seed_data = YAML.load_file(path)
import(seed_data, root_key: root_key)
rescue StandardError => e
raise "Error loading seed from namespace '#{source}' at path '#{path}': #{e.message}"
end
end

# -------------------------------------------------------------
# Attach the exported YAML as an Active Storage file
# -------------------------------------------------------------
def attach_yaml_file
yml_data = export_yaml
yaml_file.attach(
io: StringIO.new(yml_data),
filename: versioned_file_name,
content_type: 'text/yaml'
)
end
end
end
27 changes: 20 additions & 7 deletions app/models/better_together/wizard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# app/models/better_together/wizard.rb
module BetterTogether
# Ordered step defintions that the user must complete
# Ordered step definitions that the user must complete
class Wizard < ApplicationRecord
include Identifier
include Protected
Expand All @@ -19,13 +19,9 @@ class Wizard < ApplicationRecord
validates :max_completions, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :current_completions, numericality: { only_integer: true, greater_than_or_equal_to: 0 }

# Additional logic and methods as needed

def completed?
# TODO: Adjust for wizards with multiple possible completions
completed = wizard_steps.size == wizard_step_definitions.size &&
wizard_steps.ordered.all?(&:completed)

mark_completed if completed
current_completions.positive?
end
Expand All @@ -39,9 +35,26 @@ def mark_completed

self.current_completions += 1
self.last_completed_at = DateTime.now
self.first_completed_at = DateTime.now if first_completed_at.nil?

self.first_completed_at ||= DateTime.now
save
end

# -------------------------------------
# Overriding #plant for the Seedable concern
# -------------------------------------
def plant
# Pull in the default fields from the base Seedable (model_class, record_id, etc.)
super.merge(
name: name,
identifier: identifier,
description: description,
max_completions: max_completions,
current_completions: current_completions,
last_completed_at: last_completed_at,
first_completed_at: first_completed_at,
# Optionally embed your wizard_step_definitions so they're all in one seed
step_definitions: wizard_step_definitions.map(&:plant)
)
end
end
end
Loading
Loading