-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* implement redirect to modal after edit button has been clicked * use yarn as manager * setup stimulus and hotwire * try to add modal * modal works * revert back to npm * clean up * clean up * clean up * use new crud controller * Add create functionality * first working version of create * start implementing show error * implement showing error messages when submiting invalid data * ingore unnecessary test for this feature * correct rubocop offenses * use different icon to create skill and correct template error * style modal * fix bug of reopening modal when cancel button was clicked * adjust styling of modal * only include children in category dropdown and change label of options * adjust styling of danger alerts * sort category options by the title of their parent --------- Co-authored-by: megli2 <[email protected]> Co-authored-by: Yanick Minder <[email protected]>
- Loading branch information
1 parent
a24e862
commit 0c61fab
Showing
27 changed files
with
773 additions
and
170 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,5 @@ | ||
@import "bootstrap"; | ||
@import "bootstrap"; | ||
|
||
.modal-dialog { | ||
max-width: 60%; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,130 +1,257 @@ | ||
# frozen_string_literal: true | ||
|
||
# A generic controller to display, create, update and destroy entries of a certain model class. | ||
# Abstract controller providing basic CRUD actions. | ||
# | ||
# Some enhancements were made to ease extensibility. | ||
# The current model entry is available in the view as an instance variable | ||
# named after the +model_class+ or in the helper method +entry+. | ||
# Several protected helper methods are there to be (optionally) overriden by | ||
# subclasses. | ||
# With the help of additional callbacks, it is possible to hook into the | ||
# action procedures without overriding the entire method. | ||
class CrudController < ListController | ||
class_attribute :permitted_attrs, :nested_models, :permitted_relationships | ||
|
||
# GET /users/1 | ||
def show(_options = {}) | ||
@entry = fetch_entry | ||
class_attribute :permitted_attrs | ||
|
||
# Defines before and after callback hooks for create, update, save and | ||
# destroy actions. | ||
define_model_callbacks :create, :update, :save, :destroy | ||
|
||
# Defines before callbacks for the render actions. A virtual callback | ||
# unifiying render_new and render_edit, called render_form, is defined | ||
# further down. | ||
define_render_callbacks :show, :new, :edit | ||
|
||
before_action :entry, only: %i[show new edit update destroy] | ||
|
||
helper_method :entry, :full_entry_label | ||
|
||
############## ACTIONS ############################################ | ||
|
||
# GET /entries/1 | ||
# GET /entries/1.json | ||
# | ||
# Show one entry of this model. | ||
def show; end | ||
|
||
# GET /entries/new | ||
# GET /entries/new.json | ||
# | ||
# Display a form to create a new entry of this model. | ||
def new | ||
assign_attributes if params[model_identifier] | ||
end | ||
|
||
# POST /users | ||
def create(options = {}) | ||
build_entry | ||
if entry.save | ||
render_entry({ status: :created, | ||
location: entry_url } | ||
.merge(options[:render_options] || {})) | ||
else | ||
render_errors | ||
# GET /entries/1/edit | ||
# | ||
# Display a form to edit an exisiting entry of this model. | ||
def edit; end | ||
|
||
# POST /entries | ||
# POST /entries.json | ||
# | ||
# Create a new entry of this model from the passed params. | ||
# There are before and after create callbacks to hook into the action. | ||
# | ||
# To customize the response for certain formats, you may overwrite | ||
# this action and call super with a block that gets the format and | ||
# success parameters. Calling a format action (e.g. format.html) | ||
# in the given block will take precedence over the one defined here. | ||
# | ||
# Specify a :location option if you wish to do a custom redirect. | ||
def create(**options, &block) | ||
model_class.transaction do | ||
assign_attributes | ||
created = with_callbacks(:create, :save) { entry.save } | ||
respond(created, | ||
**options.reverse_merge(status: :created, render_on_failure: :new), | ||
&block) | ||
raise ActiveRecord::Rollback unless created | ||
end | ||
end | ||
|
||
# PATCH/PUT /users/1 | ||
def update(options = {}) | ||
entry.attributes = model_params | ||
if entry.save | ||
render_entry(options[:render_options]) | ||
else | ||
render_errors | ||
# PUT /entries/1 | ||
# PUT /entries/1.json | ||
# | ||
# Update an existing entry of this model from the passed params. | ||
# There are before and after update callbacks to hook into the action. | ||
# | ||
# To customize the response for certain formats, you may overwrite | ||
# this action and call super with a block that gets the format and | ||
# success parameters. Calling a format action (e.g. format.html) | ||
# in the given block will take precedence over the one defined here. | ||
# | ||
# Specify a :location option if you wish to do a custom redirect. | ||
def update(**options, &block) | ||
model_class.transaction do | ||
assign_attributes | ||
updated = with_callbacks(:update, :save) { entry.save } | ||
respond(updated, | ||
**options.merge(status: :ok, render_on_failure: :edit), | ||
&block) | ||
raise ActiveRecord::Rollback unless updated | ||
end | ||
end | ||
|
||
# DELETE /users/1 | ||
def destroy(_options = {}) | ||
if entry.destroy | ||
head :no_content | ||
else | ||
render_errors | ||
# DELETE /entries/1 | ||
# DELETE /entries/1.json | ||
# | ||
# Destroy an existing entry of this model. | ||
# There are before and after destroy callbacks to hook into the action. | ||
# | ||
# To customize the response for certain formats, you may overwrite | ||
# this action and call super with a block that gets format and | ||
# success parameters. Calling a format action (e.g. format.html) | ||
# in the given block will take precedence over the one defined here. | ||
# | ||
# Specify a :location option if you wish to do a custom redirect. | ||
def destroy(**options, &block) | ||
model_class.transaction do | ||
destroyed = run_callbacks(:destroy) { entry.destroy } | ||
respond(destroyed, | ||
**options.merge(status: :no_content), | ||
&block) | ||
raise ActiveRecord::Rollback unless destroyed | ||
end | ||
end | ||
|
||
private | ||
|
||
############# CUSTOMIZABLE HELPER METHODS ############################## | ||
|
||
# Main accessor method for the handled model entry. | ||
def entry | ||
instance_variable_get(:"@#{ivar_name}") || | ||
instance_variable_set(:"@#{ivar_name}", fetch_entry) | ||
model_ivar_get || model_ivar_set(params[:id] ? find_entry : build_entry) | ||
end | ||
|
||
def fetch_entry | ||
model_scope.find(params.fetch(:id)) | ||
# Creates a new model entry. | ||
def build_entry | ||
model_scope.new | ||
end | ||
|
||
def build_entry | ||
instance_variable_set(:"@#{ivar_name}", model_scope.new(model_params)) | ||
# Sets an existing model entry from the given id. | ||
def find_entry | ||
model_scope.find(params[:id]) | ||
end | ||
|
||
# Assigns the attributes from the params to the model entry. | ||
def assign_attributes | ||
entry.attributes = model_params | ||
end | ||
|
||
def render_entry(options = {}) | ||
render({ json: entry, | ||
serializer: model_serializer, | ||
root: model_root_key } | ||
.merge(render_options) | ||
.merge(options || {})) | ||
# The form params for this model. | ||
def model_params | ||
params.require(model_identifier).permit(permitted_attrs) | ||
end | ||
|
||
def render_errors | ||
render json: entry, status: :unprocessable_entity, | ||
adapter: :json_api, serializer: ActiveModel::Serializer::ErrorSerializer | ||
# Path of the index page to return to. | ||
def index_path | ||
polymorphic_path(path_args(model_class), returning: true) | ||
end | ||
|
||
def entry_url | ||
send("#{self.class.name.underscore | ||
.gsub(/_controller$/, '') | ||
.gsub(/\//, '_').singularize}_url", entry) | ||
# Path of the show page. | ||
def show_path | ||
path_args(entry) | ||
end | ||
|
||
# Only allow a trusted parameter "white list" through. | ||
def model_params | ||
attrs = params[:data][:attributes].permit(permitted_attrs) | ||
attrs = map_relationships(attrs) | ||
AttributeDeserializer.new(attrs, nested_models: nested_models).run | ||
def respond(success, **options) | ||
respond_to do |format| | ||
yield(format, success) if block_given? | ||
if success | ||
render_on_success(format, **options) | ||
else | ||
render_on_error(format, **options) | ||
end | ||
end | ||
end | ||
|
||
def map_relationships(attrs) | ||
relationships = params[:data][:relationships] | ||
def render_on_success(format, **options) | ||
format.html { redirect_on_success(**options) } | ||
format.json { render_success_json(options[:status]) } | ||
end | ||
|
||
return attrs if relationships.blank? | ||
def render_on_error(format, **options) | ||
format.turbo_stream { render options[:render_on_failure], status: options[:status] } | ||
format.html { render_or_redirect_on_failure(**options) } | ||
format.json { render_failure_json } | ||
end | ||
|
||
relationships.each do |model_name, data| | ||
attrs[relationship_param_name(model_name)] = relationship_ids(data, model_name) | ||
# If the option :render_on_failure is given, render the corresponding | ||
# template, otherwise redirect. | ||
def render_or_redirect_on_failure(**options) | ||
if options[:render_on_failure] | ||
render options[:render_on_failure], status: options[:status] | ||
else | ||
redirect_on_failure(**options) | ||
end | ||
attrs | ||
end | ||
|
||
def relationship_param_name(model_name) | ||
permitted_param?("#{model_name}_id") ? "#{model_name}_id" : "#{model_name.singularize}_ids" | ||
# Perform a redirect after a successfull operation and set a flash notice. | ||
def redirect_on_success(**options) | ||
location = options[:location] || | ||
(entry.destroyed? ? index_path : show_path) | ||
flash[:notice] ||= flash_message(:success) | ||
redirect_to location | ||
end | ||
|
||
def relationship_ids(model_data, name) | ||
return unless model_data[:data] | ||
return model_data[:data][:id] unless model_data[:data].is_a?(Array) | ||
# Perform a redirect after a failed operation and set a flash alert. | ||
def redirect_on_failure(**options) | ||
location = options[:location] || | ||
request.env['HTTP_REFERER'].presence || | ||
index_path | ||
flash[:alert] ||= error_messages.presence || flash_message(:failure) | ||
redirect_to location | ||
end | ||
|
||
if permitted_relationship?(name) | ||
model_data[:data].collect do |e| | ||
e[:id] | ||
end | ||
# Render the show json with the given status or :no_content | ||
def render_success_json(status) | ||
if status == :no_content | ||
head :no_content | ||
else | ||
render :show, status: status, location: show_path | ||
end | ||
end | ||
|
||
def permitted_param?(attribute_name) | ||
permitted_attrs.map(&:to_s).include?(attribute_name) | ||
# Render a json with the errors. | ||
def render_failure_json | ||
render json: entry.errors, status: :unprocessable_entity | ||
end | ||
|
||
def permitted_relationship?(attribute_name) | ||
permitted_relationships.map(&:to_s).include?(attribute_name) | ||
# Get an I18n flash message. | ||
# Uses the key {controller_name}.{action_name}.flash.{state} | ||
# or crud.{action_name}.flash.{state} as fallback. | ||
def flash_message(state) | ||
scope = "#{action_name}.flash.#{state}" | ||
keys = [:"#{controller_name}.#{scope}_html", | ||
:"#{controller_name}.#{scope}", | ||
:"crud.#{scope}_html", | ||
:"crud.#{scope}"] | ||
I18n.t(keys.shift, model: full_entry_label, default: keys) | ||
end | ||
|
||
def ivar_name | ||
model_class.model_name.param_key | ||
# A label for the current entry, including the model name. | ||
def full_entry_label | ||
# rubocop:disable Rails/OutputSafety | ||
"#{models_label(plural: false)} <i>#{ERB::Util.h(entry)}</i>".html_safe | ||
# rubocop:enable Rails/OutputSafety | ||
end | ||
|
||
def get_asset_path(filename) | ||
manifest_file = Rails.application.assets_manifest.assets[filename] | ||
if manifest_file | ||
File.join(Rails.application.assets_manifest.directory, manifest_file) | ||
else | ||
Rails.application.assets&.[](filename)&.filename | ||
# Html safe error messages of the current entry. | ||
def error_messages | ||
# rubocop:disable Rails/OutputSafety | ||
escaped = entry.errors.full_messages.map { |m| ERB::Util.html_escape(m) } | ||
escaped.join('<br/>').html_safe | ||
# rubocop:enable Rails/OutputSafety | ||
end | ||
|
||
# Class methods for CrudActions. | ||
class << self | ||
# Convenience callback to apply a callback on both form actions | ||
# (new and edit). | ||
def before_render_form(*methods) | ||
before_render_new(*methods) | ||
before_render_edit(*methods) | ||
end | ||
end | ||
|
||
end |
Oops, something went wrong.