-
Notifications
You must be signed in to change notification settings - Fork 46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement slug
field partial
#764
base: main
Are you sure you want to change the base?
Conversation
@@ -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). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have a slug
column on Team
from a really long time ago:
module ObfuscatesId | ||
# We only want to ensure slugging is possible if a developer | ||
# can generate a `slug` field partial with our scaffolder. | ||
if defined?(BulletTrain::SuperScaffolding) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering changing this, it may not be too big of a deal if BulletTrain::SuperScaffolding
is defined or not (maybe I can just write a comment saying the the module is only intended for use with BulletTrain::SuperScaffolding
).
@gazayas, thanks for tackling this, it looks like a great start! I have kind of a mix of questions that I'm not sure how to answer and suggestions for this PR. Questions, which are kind of related:
Suggestions: If you do include Sluggable
def slug
foo
end
def self.slug_attribute
:foo
end
# Define which paths you don't want to show up when defining slugs.
def self.restricted_paths
[
"admin",
"admins"
]
end I'm wondering if we can condense all of this, so that it's something more like: include Sluggable
slug_field :foo I'm thinking that we probably want some kind of global "slugs to avoid" list that would be shared among all models by default, and that we probably don't want to have to list them on each and every model with a slug. So maybe in config.forbidden_slugs = ["admin", "admins"] And then if you wanted to add additional restrictions for a particular model you could do something like: include Sluggable
slug_field :foo, forbidden: ["root"] And if then we'd probably need some way to opt out of the global block list in a model: include Sluggable
slug_field :foo, forbidden: ["root"], use_global_forbidden_list: false And if you wanted to have a slug, but not use it inside of BT maybe you'd do something like: include Sluggable
slug_field :foo, use_in_urls: false Or maybe instead of opting-out of using it in urls you'd have to opt-in, which would make it clear which of multiple slugs you wanted to be used in urls: include Sluggable
slug_field :foo
slug_field :bar, use_in_urls: true I'm sure the naming of things could be greatly improved over what I've suggested here, I just wanted to get some thoughts down. What do you think? |
I think this is definitely the main question at hand, and if I'm understanding your idea of Field Partial or Magic Field1. Field PartialThis would keep the functionality of this PR (and I think implementing all the changes you proposed would improve the PR a lot), so developers could do the following:
The reason I phrased the second point that way is for how we would handle it if we went the 2. Magic FieldThis would tie a slug to a specific attribute on the model. We would then automatically generate the URL based on the attribute name. This would mean that developers wouldn't need to run something like
By adding this option, we would designate which attribute to sluggify. So, say you super scaffold a Project with this command, and then create a new Project with a title named Setting restricted paths in an initializerI think this is a good idea regardless if we make it a field partial or a magic field. I think the next step is to just decide which one we want to go with. |
Just kinda thinking out loud here, so apologies if this is a bit scatter shot.
I think that enabling this is very important. It's problematic to try to automatically and invisibly generate the slug based on some other attribute. Just a couple of scenarios to illustrate:
So, in that sense I think we definitely want it to be closer to the "field partial" model. I also think that it makes sense to let people name it how they like. "Slug" might make sense to some people, but other people might want to call it "URL fragment" or whatever. I thought some more about whether or not it makes sense to have multiple slugs on a model, and I'm not sure that it does (though I'm open to arguments). I kind of think that "slug" in this context basically means "a URL compatible identifier that's also human settable/readable which is used in URLs". I think that if you wanted to have two slugs on a model, that only one of them would meet that general definition, and the other would just be a text field with some validations. So I'm not sure that it makes sense to treat a non-URL slug as a Slug™. For instance, I had proposed that maybe there's a need to be able to do this: include Sluggable
slug_field :foo
slug_field :bar, use_in_urls: true But I think that it would probably be more straightforward, and more understandable when coming across the code in the future if it were this instead: include Sluggable
slug_field :bar
validates :foo, presence: true, uniqueness: true, format: {...} So, my leanings currently are that the slug feature should have the following qualities:
A nice bonus quality if it's relatively easy to do:
Like if a Maybe you could generate it by doing something like:
I don't really know what that would generate code-wise to let everything get hooked up properly. Maybe something like: include Sluggable
slug_field :bar, generate_from: :name Dunno. Maybe that's a little more than we want to bite off at this stage. |
class RestrictedPathsValidator < ActiveModel::Validator | ||
def validate(record) | ||
if record.class.restricted_paths.include?(record.slug) | ||
record.errors.add record.class.slug_attribute, :restricted_path |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we may also need to validate that the slug doesn't match any of the controller actions for this model. For instance if someone tried to use the slug new
for a team, then that would cause a collision for the path /teams/new
. We'd always want that to point to new_team_path
and not to team_path(slug: 'new')
.
This article suggests that something like TeamsController.action_methods
would give us a good list to validate against.
For our Account::TeamsController
the action_methods
method returns this big ol' list:
Account::TeamsController.action_methods
=>
#<Set:
{"team_params",
"switch_to",
"show",
"index",
"create",
"edit",
"destroy",
"update",
"new",
"ensure_onboarding_is_complete",
"assign_select_options",
"create_model_if_new",
"create_models_if_new",
"ensure_backing_models_on",
"assign_checkboxes",
"assign_date",
"assign_date_and_time",
"assign_boolean",
"load_team",
"managing_account?",
"ensure_onboarding_is_complete_and_set_next_step",
"set_last_seen_at",
"adding_team?",
"accepting_invitation?",
"adding_user_email?",
"adding_user_details?",
"switching_teams?",
"set_current_user",
"invited?",
"show_sign_up_options?",
"after_sign_up_path_for",
"enforce_invitation_only",
"only_allow_path",
"delegate_json_to_api",
"current_team",
"current_membership",
"current_locale",
"set_locale",
"layout_by_resource",
"set_sentry_context"}
>
That seems like too much. This StackOverflow answer suggests a method like this:
def actions_for_controller(controller_path)
route_defaults = Rails.application.routes.routes.map(&:defaults)
route_defaults = route_defaults.select { |x| x[:controller] == controller_path }
route_defaults.map { |x| x[:action] }.uniq
end
Which produces a much more reasonable list:
actions_for_controller('account/teams')
=> [
"index",
"create",
"new",
"edit",
"show",
"update",
"destroy",
"switch_to"
]
I'm not sure that we'd need to prevent member
actions like edit
and show
from being used as a slug, though allowing them might result in weird URLs like /teams/show/edit
(the edit
action for the team with a slug of show
).
Determining which controller to inspect for a given model may be difficult.
All of this sounds good to me. Let me recap to see if I'm processing everything correctly:
Seems like a cool feature to me, I think we could accomplish this with a simple stimulus controller. The dynamic forms/dependent fields feature came to mind, but that seems too heavy duty for the job. From a abstract/design standpoint though, if it SHOULD belong there I think it's worth looking into. |
That all sounds right to me @gazayas. The auto-fill thing does sound very similar to the dynamic-forms/dependent-fields thing. But I agree (after only very brief consideration) that it sounds a little heavy since there wouldn't need to be a round trip to the server. Though... as I'm typing that out I'm realizing that we would need to validate uniqueness (among other things) for the slug that we auto-generate, so... maybe it's closer to that pattern than I thought at first? |
Converting this to draft for now. |
Closes bullet-train-co/bullet_train#1232
Joint PR
slug
field partial bullet_train#1321TODO