diff --git a/bullet_train-obfuscates_id/app/models/concerns/obfuscates_id.rb b/bullet_train-obfuscates_id/app/models/concerns/obfuscates_id.rb index 6feafa6f1..58a60a8d4 100644 --- a/bullet_train-obfuscates_id/app/models/concerns/obfuscates_id.rb +++ b/bullet_train-obfuscates_id/app/models/concerns/obfuscates_id.rb @@ -23,7 +23,11 @@ def decode_id(id) end def find(*ids) - super(*ids.map { |id| decode_id(id) }) + if respond_to?(:slug_attribute) + send("find_by_#{slug_attribute}".to_sym, ids.first) + else + super(*ids.map { |id| decode_id(id) }) + end end def relation @@ -46,6 +50,7 @@ def obfuscated_id end def to_param - obfuscated_id + # Even if the `slug` column exists, it might be empty (i.e. - Team). + (defined?(slug) && slug.present?) ? slug : obfuscated_id end end diff --git a/bullet_train-obfuscates_id/app/models/concerns/obfuscates_id/sluggable.rb b/bullet_train-obfuscates_id/app/models/concerns/obfuscates_id/sluggable.rb new file mode 100644 index 000000000..0e90d164a --- /dev/null +++ b/bullet_train-obfuscates_id/app/models/concerns/obfuscates_id/sluggable.rb @@ -0,0 +1,47 @@ +module ObfuscatesId + module Sluggable + extend ActiveSupport::Concern + + included do + before_validation :downcase_slug + after_validation :set_to_previous_value, on: :update + + if respond_to?(:slug_attribute) + validates slug_attribute, + uniqueness: true, + length: {minimum: 2, maximum: 30}, + format: {with: /\A[a-zA-Z0-9|-]+\Z/} + end + + validates_with ::RestrictedPathsValidator + + private + + def downcase_slug + send(attribute_name_setter, send(attribute_name).downcase) + end + + # Since Rails reloads the same invalid object into the form while displaying errors, + # we ensure the slug value being referenced to on reloading is the original value which + # has already been persisted to the database, not the invalid value from the form object. + def set_to_previous_value + if errors.any? + errors.each do |e| + if e.match?(attribute_name) + previous_slug_value = send("#{attribute_name}_was") + send(attribute_name_setter, previous_slug_value) + end + end + end + end + + def attribute_name + self.class.send(:slug_attribute) + end + + def attribute_name_setter + "#{attribute_name}=".to_sym + end + end + end +end diff --git a/bullet_train-obfuscates_id/app/validators/restricted_paths_validator.rb b/bullet_train-obfuscates_id/app/validators/restricted_paths_validator.rb new file mode 100644 index 000000000..9ee512327 --- /dev/null +++ b/bullet_train-obfuscates_id/app/validators/restricted_paths_validator.rb @@ -0,0 +1,7 @@ +class RestrictedPathsValidator < ActiveModel::Validator + def validate(record) + if record.class.restricted_paths.include?(record.slug) + record.errors.add record.class.slug_attribute, :restricted_path + end + end +end diff --git a/bullet_train-obfuscates_id/config/locales/en/errors.en.yml b/bullet_train-obfuscates_id/config/locales/en/errors.en.yml new file mode 100644 index 000000000..30e84a687 --- /dev/null +++ b/bullet_train-obfuscates_id/config/locales/en/errors.en.yml @@ -0,0 +1,4 @@ +en: + errors: + messages: + restricted_path: "is restricted, please try another path" diff --git a/bullet_train-super_scaffolding/lib/scaffolding.rb b/bullet_train-super_scaffolding/lib/scaffolding.rb index 928ce713a..2c0bfe5d2 100644 --- a/bullet_train-super_scaffolding/lib/scaffolding.rb +++ b/bullet_train-super_scaffolding/lib/scaffolding.rb @@ -24,6 +24,7 @@ def self.valid_attribute_type?(type) "options", "password_field", "phone_field", + "slug", "super_select", "text_area", "text_field", diff --git a/bullet_train-super_scaffolding/lib/scaffolding/attribute.rb b/bullet_train-super_scaffolding/lib/scaffolding/attribute.rb index 8f0cf764f..85bd071e7 100644 --- a/bullet_train-super_scaffolding/lib/scaffolding/attribute.rb +++ b/bullet_train-super_scaffolding/lib/scaffolding/attribute.rb @@ -126,6 +126,7 @@ def collection_name def partial_name return options[:attribute] if options[:attribute] + # TODO: We should put these in alphabetical order. case type when "trix_editor", "ckeditor" "html" @@ -169,6 +170,8 @@ def partial_name "number" when "address_field" "address" + when "slug" + "text" else raise "Invalid field type: #{type}." end diff --git a/bullet_train-super_scaffolding/lib/scaffolding/script.rb b/bullet_train-super_scaffolding/lib/scaffolding/script.rb index 2c2709194..a3e85b3b0 100644 --- a/bullet_train-super_scaffolding/lib/scaffolding/script.rb +++ b/bullet_train-super_scaffolding/lib/scaffolding/script.rb @@ -22,6 +22,7 @@ options: "string", password_field: "string", phone_field: "string", + slug: "string", super_select: "string", text_area: "text", text_field: "string", diff --git a/bullet_train-super_scaffolding/lib/scaffolding/transformer.rb b/bullet_train-super_scaffolding/lib/scaffolding/transformer.rb index 59865aa93..c76c06f89 100644 --- a/bullet_train-super_scaffolding/lib/scaffolding/transformer.rb +++ b/bullet_train-super_scaffolding/lib/scaffolding/transformer.rb @@ -623,7 +623,7 @@ def add_attributes_to_various_views(attributes, scaffolding_options = {}) # 'primary_key' => '', # 'references' => '', "string" => "text_field", - "text" => "text_area" + "text" => "text_area", # 'time' => '', # 'timestamp' => '', } @@ -1294,6 +1294,38 @@ def remove_#{attribute.name} if attribute.is_boolean? scaffold_add_line_to_file("./app/models/scaffolding/completely_concrete/tangible_thing.rb", "validates :#{attribute.name}, inclusion: [true, false]", VALIDATIONS_HOOK, prepend: true) end + when "slug" + slug_methods = <<~RUBY + def slug + #{attribute.name} + end + + def self.slug_attribute + :#{attribute.name} + end + + # Define which paths you don't want to show up when defining slugs. + def self.restricted_paths + [ + "admin", + "admins" + ] + end + RUBY + + scaffold_add_line_to_file( + "./app/models/scaffolding/completely_concrete/tangible_thing.rb", + "include Sluggable", + CONCERNS_HOOK, + prepend: true + ) + + scaffold_add_line_to_file( + "./app/models/scaffolding/completely_concrete/tangible_thing.rb", + slug_methods, + METHODS_HOOK, + prepend: true + ) end end diff --git a/bullet_train-themes/app/views/themes/base/fields/_slug.html.erb b/bullet_train-themes/app/views/themes/base/fields/_slug.html.erb new file mode 100644 index 000000000..33e785b3a --- /dev/null +++ b/bullet_train-themes/app/views/themes/base/fields/_slug.html.erb @@ -0,0 +1,8 @@ + +<% +form ||= current_fields_form +options ||= {} +other_options ||= {} +%> + +<%= render 'shared/fields/field', form: form, method: method, helper: :text_field, options: options, other_options: other_options %>