Post History
#2: Post edited
Please teach me to fish: how do I navigate the edit/suggested-edit code to apply the same error check to both?
- I was trying to fix what I thought was an [easy bug](https://github.com/codidact/qpixel/issues/1299): suggested edits can add restricted tags (the ones only moderators can apply), which creates a suggested edit that most reviewers can't approve. We already prevent you from adding those tags when you directly edit a post, so I figured -- how hard could it be to apply that same logic to *suggested* edits?
- Harder than I thought, apparently. I don't know if my mental block is due to insufficient Ruby/Rails knowledge, insufficient MVC clues, or something about the QPixel code in particular.
- Editing through either path leads to the same form, in `app/views/posts/edit.html.erb`. For direct saves, there's this check:
- ```ruby
- <% if @post.errors.any? %>
- <div class="notice is-danger is-filled">
- <p>The following errors prevented your post from being saved:</p>
- <ul>
- <% @post.errors.full_messages.each do |msg| %>
- <li><%= msg %></li>
- <% end %>
- </ul>
- </div>
- <% end %>
- ```
- Ok, I said -- how do the errors get there? This question led me to `app/controllers/posts_controller.rb` and specifically the `update` function. I'm having a little trouble following the logic, but the outline seems to be (in pseudocode):
```- if current user can edit
- if current user can push to network
- (lots of stuff we don't care about here)
- else (do an edit in a transaction)
- else // current user cannot edit
- create suggested edit and send notification to owner
- ```
- That last part, in a little more detail:
- ```ruby
- data = {
- post: @post,
- user: current_user,
- body: body_rendered == @post.body ? nil : body_rendered,
- title: params[:post][:title] == @post.title ? nil : params[:post][:title],
- tags_cache: new_tags_cache == @post.tags_cache ? @post.tags_cache : new_tags_cache,
- body_markdown: params[:post][:body_markdown] == @post.body_markdown ? nil : params[:post][:body_markdown],
- comment: params[:edit_comment],
- active: true, accepted: false
- }
- // NOTE the absence of any error checks here
- edit = SuggestedEdit.new(data)
- ```
- This seems to be the place where I should check for errors, same as we do for direct edits. The first block of code I quoted in this question is how we *report* errors to the user, but where do they get *checked* and *set*?
- I grepped for the text of the error message, which led me to `app/models/post.rb`, in a function called `moderator_tags`. A comment in the source code says "intended to be used for validation" -- sounds like just what I need! So then I grepped for that function name, looking for where it's called.
- That's where I got stuck:
- ```bash
- $ grep -r moderator_tags app/*
- app/controllers/tags_controller.rb: exec_sql([sql.gsub('$TABLENAME', 'categories_moderator_tags'), @primary.id, @subordinate.id])
- app/controllers/tags_controller.rb: tables = ['posts_tags', 'categories_moderator_tags', 'categories_required_tags', 'categories_topic_tags',
- app/controllers/tags_controller.rb~: exec_sql([sql.gsub('$TABLENAME', 'categories_moderator_tags'), @primary.id, @subordinate.id])
- app/controllers/tags_controller.rb~: tables = ['posts_tags', 'categories_moderator_tags', 'categories_required_tags', 'categories_topic_tags',
- app/controllers/admin_controller.rb: meta_category.moderator_tags << Tag.unscoped.where(community: @community, name: status_tags)
- app/models/category.rb: has_and_belongs_to_many :moderator_tags, class_name: 'Tag', join_table: 'categories_moderator_tags'
- app/models/post.rb: validate :moderator_tags, if: -> { post_type.has_tags && post_type.has_category && tags_cache_changed? }
- app/models/post.rb: def moderator_tags
- app/models/post.rb: mod_tags = category&.moderator_tags&.map(&:name)
- app/views/categories/_form.html.erb: <%= f.select :moderator_tag_ids, options_for_select(@category.moderator_tags.map { |t| [t.name, t.id] },
- ```
- What should I do next? How do I find how that validation is done for *regular* edits, so I can (I hope) apply it to the suggested-edit case and prevent an unapprovable suggestion from getting created?
- I was trying to fix what I thought was an [easy bug](https://github.com/codidact/qpixel/issues/1299): suggested edits can add restricted tags (the ones only moderators can apply), which creates a suggested edit that most reviewers can't approve. We already prevent you from adding those tags when you directly edit a post, so I figured -- how hard could it be to apply that same logic to *suggested* edits?
- Harder than I thought, apparently. I don't know if my mental block is due to insufficient Ruby/Rails knowledge, insufficient MVC clues, or something about the QPixel code in particular.
- Editing through either path leads to the same form, in `app/views/posts/edit.html.erb`. For direct saves, there's this check:
- ```ruby
- <% if @post.errors.any? %>
- <div class="notice is-danger is-filled">
- <p>The following errors prevented your post from being saved:</p>
- <ul>
- <% @post.errors.full_messages.each do |msg| %>
- <li><%= msg %></li>
- <% end %>
- </ul>
- </div>
- <% end %>
- ```
- Ok, I said -- how do the errors get there? This question led me to `app/controllers/posts_controller.rb` and specifically the `update` function. I'm having a little trouble following the logic, but the outline seems to be (in pseudocode):
- ```text
- if current user can edit
- if current user can push to network
- (lots of stuff we don't care about here)
- else (do an edit in a transaction)
- else // current user cannot edit
- create suggested edit and send notification to owner
- ```
- That last part, in a little more detail:
- ```ruby
- data = {
- post: @post,
- user: current_user,
- body: body_rendered == @post.body ? nil : body_rendered,
- title: params[:post][:title] == @post.title ? nil : params[:post][:title],
- tags_cache: new_tags_cache == @post.tags_cache ? @post.tags_cache : new_tags_cache,
- body_markdown: params[:post][:body_markdown] == @post.body_markdown ? nil : params[:post][:body_markdown],
- comment: params[:edit_comment],
- active: true, accepted: false
- }
- // NOTE the absence of any error checks here
- edit = SuggestedEdit.new(data)
- ```
- This seems to be the place where I should check for errors, same as we do for direct edits. The first block of code I quoted in this question is how we *report* errors to the user, but where do they get *checked* and *set*?
- I grepped for the text of the error message, which led me to `app/models/post.rb`, in a function called `moderator_tags`. A comment in the source code says "intended to be used for validation" -- sounds like just what I need! So then I grepped for that function name, looking for where it's called.
- That's where I got stuck:
- ```bash
- $ grep -r moderator_tags app/*
- ```
- ```text
- app/controllers/tags_controller.rb: exec_sql([sql.gsub('$TABLENAME', 'categories_moderator_tags'), @primary.id, @subordinate.id])
- app/controllers/tags_controller.rb: tables = ['posts_tags', 'categories_moderator_tags', 'categories_required_tags', 'categories_topic_tags',
- app/controllers/tags_controller.rb~: exec_sql([sql.gsub('$TABLENAME', 'categories_moderator_tags'), @primary.id, @subordinate.id])
- app/controllers/tags_controller.rb~: tables = ['posts_tags', 'categories_moderator_tags', 'categories_required_tags', 'categories_topic_tags',
- app/controllers/admin_controller.rb: meta_category.moderator_tags << Tag.unscoped.where(community: @community, name: status_tags)
- app/models/category.rb: has_and_belongs_to_many :moderator_tags, class_name: 'Tag', join_table: 'categories_moderator_tags'
- app/models/post.rb: validate :moderator_tags, if: -> { post_type.has_tags && post_type.has_category && tags_cache_changed? }
- app/models/post.rb: def moderator_tags
- app/models/post.rb: mod_tags = category&.moderator_tags&.map(&:name)
- app/views/categories/_form.html.erb: <%= f.select :moderator_tag_ids, options_for_select(@category.moderator_tags.map { |t| [t.name, t.id] },
- ```
- What should I do next? How do I find how that validation is done for *regular* edits, so I can (I hope) apply it to the suggested-edit case and prevent an unapprovable suggestion from getting created?
#1: Initial revision
Please teach me to fish: how do I navigate the edit/suggested-edit code to apply the same error check to both?
I was trying to fix what I thought was an [easy bug](https://github.com/codidact/qpixel/issues/1299): suggested edits can add restricted tags (the ones only moderators can apply), which creates a suggested edit that most reviewers can't approve. We already prevent you from adding those tags when you directly edit a post, so I figured -- how hard could it be to apply that same logic to *suggested* edits? Harder than I thought, apparently. I don't know if my mental block is due to insufficient Ruby/Rails knowledge, insufficient MVC clues, or something about the QPixel code in particular. Editing through either path leads to the same form, in `app/views/posts/edit.html.erb`. For direct saves, there's this check: ```ruby <% if @post.errors.any? %> <div class="notice is-danger is-filled"> <p>The following errors prevented your post from being saved:</p> <ul> <% @post.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> ``` Ok, I said -- how do the errors get there? This question led me to `app/controllers/posts_controller.rb` and specifically the `update` function. I'm having a little trouble following the logic, but the outline seems to be (in pseudocode): ``` if current user can edit if current user can push to network (lots of stuff we don't care about here) else (do an edit in a transaction) else // current user cannot edit create suggested edit and send notification to owner ``` That last part, in a little more detail: ```ruby data = { post: @post, user: current_user, body: body_rendered == @post.body ? nil : body_rendered, title: params[:post][:title] == @post.title ? nil : params[:post][:title], tags_cache: new_tags_cache == @post.tags_cache ? @post.tags_cache : new_tags_cache, body_markdown: params[:post][:body_markdown] == @post.body_markdown ? nil : params[:post][:body_markdown], comment: params[:edit_comment], active: true, accepted: false } // NOTE the absence of any error checks here edit = SuggestedEdit.new(data) ``` This seems to be the place where I should check for errors, same as we do for direct edits. The first block of code I quoted in this question is how we *report* errors to the user, but where do they get *checked* and *set*? I grepped for the text of the error message, which led me to `app/models/post.rb`, in a function called `moderator_tags`. A comment in the source code says "intended to be used for validation" -- sounds like just what I need! So then I grepped for that function name, looking for where it's called. That's where I got stuck: ```bash $ grep -r moderator_tags app/* app/controllers/tags_controller.rb: exec_sql([sql.gsub('$TABLENAME', 'categories_moderator_tags'), @primary.id, @subordinate.id]) app/controllers/tags_controller.rb: tables = ['posts_tags', 'categories_moderator_tags', 'categories_required_tags', 'categories_topic_tags', app/controllers/tags_controller.rb~: exec_sql([sql.gsub('$TABLENAME', 'categories_moderator_tags'), @primary.id, @subordinate.id]) app/controllers/tags_controller.rb~: tables = ['posts_tags', 'categories_moderator_tags', 'categories_required_tags', 'categories_topic_tags', app/controllers/admin_controller.rb: meta_category.moderator_tags << Tag.unscoped.where(community: @community, name: status_tags) app/models/category.rb: has_and_belongs_to_many :moderator_tags, class_name: 'Tag', join_table: 'categories_moderator_tags' app/models/post.rb: validate :moderator_tags, if: -> { post_type.has_tags && post_type.has_category && tags_cache_changed? } app/models/post.rb: def moderator_tags app/models/post.rb: mod_tags = category&.moderator_tags&.map(&:name) app/views/categories/_form.html.erb: <%= f.select :moderator_tag_ids, options_for_select(@category.moderator_tags.map { |t| [t.name, t.id] }, ``` What should I do next? How do I find how that validation is done for *regular* edits, so I can (I hope) apply it to the suggested-edit case and prevent an unapprovable suggestion from getting created?