diff --git a/app/assets/javascripts/markdown.js b/app/assets/javascripts/markdown.js index b09b69a24..b31bd2d6b 100644 --- a/app/assets/javascripts/markdown.js +++ b/app/assets/javascripts/markdown.js @@ -15,6 +15,16 @@ $(() => { $field.val(prev.substring(0, $field[0].selectionStart) + text + prev.substring($field[0].selectionEnd)); }; + + $('.js-template').on('click', async ev => { + const $postField = $('.js-post-field'); + $tgt = $(ev.target); + const content = $(`#template-md-${$tgt.attr('data-template-id')}`).val(); + replaceSelection($postField, content); + $postField.trigger('markdown'); + $tgt.parents('.modal,.droppanel').removeClass('is-active'); + }); + $(document).on('click', '.js-markdown-tool', ev => { const $tgt = $(ev.target); const $button = $tgt.is('a') ? $tgt : $tgt.parents('a'); diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 44a2e1d21..ad5e5b2d2 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -133,7 +133,6 @@ hr { } .modal { - position: fixed !important; z-index: 8998; } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index efa5963ca..7a02fc1a8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -292,7 +292,7 @@ def pull_pinned_links_and_hot_questions def pull_categories @header_categories = Rails.cache.fetch('header_categories') do - Category.all.order(sequence: :asc, id: :asc) + Category.where.not(sequence: -1).order(sequence: :asc, id: :asc) end end diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 539b80458..b83d59915 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -161,7 +161,8 @@ def show def edit; end def update - before = { body: @post.body_markdown, title: @post.title, tags: @post.tags.to_a } + before = { body: @post.body_markdown, title: @post.title, tags: @post.tags.to_a, +template_post_type: @post.template_post_type } body_rendered = helpers.post_markdown(:post, :body_markdown) new_tags_cache = params[:post][:tags_cache]&.reject(&:empty?) @@ -184,7 +185,9 @@ def update PostHistory.post_edited(post, current_user, before: before[:body], after: @post.body_markdown, comment: params[:edit_comment], before_title: before[:title], after_title: @post.title, - before_tags: before[:tags], after_tags: @post.tags) + before_tags: before[:tags], after_tags: @post.tags, + before_template_post_type: before[:template_post_type], + after_template_post_type: @post.template_post_type) end flash[:success] = "#{helpers.pluralize(posts.to_a.size, 'post')} updated." redirect_to help_path(slug: @post.doc_slug) @@ -196,7 +199,9 @@ def update PostHistory.post_edited(@post, current_user, before: before[:body], after: @post.body_markdown, comment: params[:edit_comment], before_title: before[:title], after_title: @post.title, - before_tags: before[:tags], after_tags: @post.tags) + before_tags: before[:tags], after_tags: @post.tags, + before_template_post_type: before[:template_post_type], + after_template_post_type: @post.template_post_type) if params[:redact] # Hide all previous history @@ -541,7 +546,7 @@ def delete_draft def permitted [:post_type_id, :category_id, :parent_id, :title, :body_markdown, :license_id, - :doc_slug, :help_category, :help_ordering] + :doc_slug, :help_category, :help_ordering, :template_post_type_id] end def post_params diff --git a/app/models/post.rb b/app/models/post.rb index aecc13bd3..da1484eb8 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -14,6 +14,7 @@ class Post < ApplicationRecord belongs_to :license, optional: true belongs_to :close_reason, optional: true belongs_to :duplicate_post, class_name: 'Question', optional: true + belongs_to :template_post_type, class_name: 'PostType', optional: true has_and_belongs_to_many :tags, dependent: :destroy has_many :votes, dependent: :destroy has_many :comments, dependent: :destroy @@ -64,7 +65,7 @@ def self.search(term) # Double-define: initial definitions are less efficient, so if we have a record of the post type we'll # override them later with more efficient methods. - ['Question', 'Answer', 'PolicyDoc', 'HelpDoc', 'Article'].each do |pt| + ['Question', 'Answer', 'PolicyDoc', 'HelpDoc', 'Article', 'PostTemplate'].each do |pt| define_method "#{pt.underscore}?" do post_type_id == pt.constantize.post_type_id end diff --git a/app/models/post_history.rb b/app/models/post_history.rb index 73b2e715a..7970befee 100644 --- a/app/models/post_history.rb +++ b/app/models/post_history.rb @@ -1,5 +1,7 @@ class PostHistory < ApplicationRecord include PostRelated + belongs_to :before_template_post_type, class_name: 'PostType', optional: true + belongs_to :after_template_post_type, class_name: 'PostType', optional: true belongs_to :post_history_type belongs_to :user has_many :post_history_tags @@ -25,7 +27,8 @@ def self.method_missing(name, *args, **opts) end object, user = args - fields = [:before, :after, :comment, :before_title, :after_title, :before_tags, :after_tags, :hidden] + fields = [:before, :after, :comment, :before_title, :after_title, :before_tags, :after_tags, :hidden, + :before_template_post_type, :after_template_post_type] values = fields.to_h { |f| [f, nil] }.merge(opts) history_type_name = name.to_s @@ -37,7 +40,8 @@ def self.method_missing(name, *args, **opts) params = { post_history_type: history_type, user: user, post: object, community_id: object.community_id } { before: :before_state, after: :after_state, comment: :comment, before_title: :before_title, - after_title: :after_title, hidden: :hidden }.each do |arg, attr| + after_title: :after_title, hidden: :hidden, before_template_post_type: :before_template_post_type, + after_template_post_type: :after_template_post_type }.each do |arg, attr| next if values[arg].nil? params = params.merge(attr => values[arg]) diff --git a/app/models/post_template.rb b/app/models/post_template.rb new file mode 100644 index 000000000..72ce63757 --- /dev/null +++ b/app/models/post_template.rb @@ -0,0 +1,5 @@ +class PostTemplate < Post + def self.post_type_id + PostType.mapping['PostTemplate'] + end +end diff --git a/app/models/post_type.rb b/app/models/post_type.rb index 7f3e7570b..aa6edee1f 100644 --- a/app/models/post_type.rb +++ b/app/models/post_type.rb @@ -19,6 +19,10 @@ def reactions end end + def templates + Post.undeleted.where(post_type_id: PostTemplate.post_type_id).where(template_post_type: self).to_a + end + def self.mapping Rails.cache.fetch 'network/post_types/post_type_ids', include_community: false do PostType.all.to_h { |pt| [pt.name, pt.id] } diff --git a/app/views/categories/_form.html.erb b/app/views/categories/_form.html.erb index 064e2444d..aedf90433 100644 --- a/app/views/categories/_form.html.erb +++ b/app/views/categories/_form.html.erb @@ -141,7 +141,8 @@ <%= f.label :sequence, class: 'form-element' %> The order in which this category will appear in the header and on the category list page. Higher numbers - appear later. + appear later. Set to -1 to prevent the category from being listed. Note that users will still be able + to access it even when not listed; to change who can access it, change the minimum trust level instead. <%= f.number_field :sequence, class: 'form-element' %> diff --git a/app/views/post_history/post.html.erb b/app/views/post_history/post.html.erb index 8847e92e5..b82095af8 100644 --- a/app/views/post_history/post.html.erb +++ b/app/views/post_history/post.html.erb @@ -61,5 +61,10 @@ The detailed changes of this event are hidden because of a redaction.

<% end %> + <% if (event.before_template_post_type.present? || event.after_template_post_type.present?) && + event.before_template_post_type != event.after_template_post_type %> +

<%= t('posts.template_post_type_label') %>

+ <%= render 'diff', before: event.before_template_post_type.name, after: event.after_template_post_type.name, post: @post %> + <% end %> <% end %> diff --git a/app/views/posts/_form.html.erb b/app/views/posts/_form.html.erb index c228a80fb..f2067e0f9 100644 --- a/app/views/posts/_form.html.erb +++ b/app/views/posts/_form.html.erb @@ -55,7 +55,7 @@ <% end %> <%= render 'shared/body_field', f: f, field_name: :body_markdown, field_label: t('posts.body_label'), post: post, - cur_length: post.body_markdown&.length, min_length: min_body_length(category), max_length: max_body_length(category) %> + cur_length: post.body_markdown&.length, min_length: min_body_length(category), max_length: max_body_length(category), post_type: post_type %>

Unsupported HTML detected

@@ -176,6 +176,14 @@ <% end %> <% end %> + <% if post_type.id == PostTemplate.post_type_id %> +
+ <%= f.label :template_post_type_id, t('posts.template_post_type_label'), class: "form-element" %> + <%= f.select :template_post_type_id, options_for_select(PostType.all.map { |pt| [pt.name.underscore.humanize, pt.id] }, + selected: post.template_post_type_id), {}, { class: "form-element" } %> +
+ <% end %> + <% if edit_comment %>
diff --git a/app/views/posts/new.html.erb b/app/views/posts/new.html.erb index 94baae739..d8b4544e7 100644 --- a/app/views/posts/new.html.erb +++ b/app/views/posts/new.html.erb @@ -1,5 +1,5 @@

- New Post + New <%= @post_type.name.underscore.humanize.titleize %> <% if @category.present? %> in <%= @category.name %> <% end %> diff --git a/app/views/shared/_body_field.html.erb b/app/views/shared/_body_field.html.erb index 08b68f14d..5a8c53fe3 100644 --- a/app/views/shared/_body_field.html.erb +++ b/app/views/shared/_body_field.html.erb @@ -36,7 +36,7 @@ end[1] %> <% end %> - <%= render 'shared/markdown_tools' %> + <%= render 'shared/markdown_tools', post_type: post.try(:post_type) %> <%= f.text_area field_name, **({ class: classes, rows: 15, placeholder: 'Start typing your post...' }).merge(value), data: { character_count: ".js-character-count-post-body" } %> <%= render 'posts/mdhint', cur_length: cur_length, min_length: min_length, max_length: max_length %> diff --git a/app/views/shared/_markdown_tools.html.erb b/app/views/shared/_markdown_tools.html.erb index fdf2fd349..4094ea1de 100644 --- a/app/views/shared/_markdown_tools.html.erb +++ b/app/views/shared/_markdown_tools.html.erb @@ -22,6 +22,43 @@

+ <% templates = post_type&.templates || [] %> + <% if templates.any? %> +
+ + + +
+ <% templates.each do |template| %> + + <%= template.title %> + + + <% end %> +
+ <% templates.each do |template| %> + + <% end %> +
+ <% end %>
<%= md_button action: 'bold', label: 'Bold', class: 'is-icon-only' do %> diff --git a/app/views/tour/question2.html.erb b/app/views/tour/question2.html.erb index f5d4cd6db..5dec9f1a5 100644 --- a/app/views/tour/question2.html.erb +++ b/app/views/tour/question2.html.erb @@ -28,7 +28,7 @@
- <%= render 'shared/markdown_tools' %> + <%= render 'shared/markdown_tools', post_type: nil %> <%= render 'posts/mdhint' %>
diff --git a/config/locales/strings/en.posts.yml b/config/locales/strings/en.posts.yml index cab783491..302b99f25 100644 --- a/config/locales/strings/en.posts.yml +++ b/config/locales/strings/en.posts.yml @@ -52,6 +52,8 @@ en: Redact original content by hiding the previous versions from history? Use only for private information such as passwords or personally identifiable information. licence_label: > License + template_post_type_label: > + For Post Type unsaved_changes_confirmation: > Any unsaved changes will be lost. Are you sure? category_label: > diff --git a/db/migrate/20230801014134_add_template_post_type_to_posts.rb b/db/migrate/20230801014134_add_template_post_type_to_posts.rb new file mode 100644 index 000000000..5afd61976 --- /dev/null +++ b/db/migrate/20230801014134_add_template_post_type_to_posts.rb @@ -0,0 +1,5 @@ +class AddTemplatePostTypeToPosts < ActiveRecord::Migration[7.0] + def change + add_reference :posts, :template_post_type, foreign_key: { to_table: :post_types } + end +end diff --git a/db/migrate/20230813161623_add_before_and_after_template_post_type_to_post_histories.rb b/db/migrate/20230813161623_add_before_and_after_template_post_type_to_post_histories.rb new file mode 100644 index 000000000..a00b21b7b --- /dev/null +++ b/db/migrate/20230813161623_add_before_and_after_template_post_type_to_post_histories.rb @@ -0,0 +1,6 @@ +class AddBeforeAndAfterTemplatePostTypeToPostHistories < ActiveRecord::Migration[7.0] + def change + add_reference :post_histories, :before_template_post_type, foreign_key: { to_table: :post_types } + add_reference :post_histories, :after_template_post_type, foreign_key: { to_table: :post_types } + end +end diff --git a/db/schema.rb b/db/schema.rb index 7055af307..e0c274158 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -388,7 +388,11 @@ t.bigint "community_id" t.string "before_title" t.string "after_title" + t.bigint "before_template_post_type_id" + t.bigint "after_template_post_type_id" t.boolean "hidden", default: false, null: false + t.index ["after_template_post_type_id"], name: "index_post_histories_on_after_template_post_type_id" + t.index ["before_template_post_type_id"], name: "index_post_histories_on_before_template_post_type_id" t.index ["community_id"], name: "index_post_histories_on_community_id" t.index ["post_history_type_id"], name: "index_post_histories_on_post_history_type_id" t.index ["post_id"], name: "index_post_histories_on_post_type_and_post_id" @@ -474,6 +478,7 @@ t.bigint "locked_by_id" t.datetime "locked_at", precision: nil t.datetime "locked_until", precision: nil + t.bigint "template_post_type_id" t.index ["att_source"], name: "index_posts_on_att_source" t.index ["body_markdown"], name: "index_posts_on_body_markdown", type: :fulltext t.index ["category_id"], name: "index_posts_on_category_id" @@ -491,6 +496,7 @@ t.index ["post_type_id"], name: "index_posts_on_post_type_id" t.index ["score"], name: "index_posts_on_score" t.index ["tags_cache"], name: "index_posts_on_tags_cache" + t.index ["template_post_type_id"], name: "index_posts_on_template_post_type_id" t.index ["upvote_count"], name: "index_posts_on_upvote_count" t.index ["user_id"], name: "index_posts_on_user_id" end @@ -794,12 +800,15 @@ add_foreign_key "pinned_links", "communities" add_foreign_key "pinned_links", "posts" add_foreign_key "post_histories", "communities" + add_foreign_key "post_histories", "post_types", column: "after_template_post_type_id" + add_foreign_key "post_histories", "post_types", column: "before_template_post_type_id" add_foreign_key "post_history_tags", "post_histories" add_foreign_key "post_history_tags", "tags" add_foreign_key "post_types", "post_types", column: "answer_type_id" add_foreign_key "posts", "close_reasons" add_foreign_key "posts", "communities" add_foreign_key "posts", "licenses" + add_foreign_key "posts", "post_types", column: "template_post_type_id" add_foreign_key "posts", "posts", column: "duplicate_post_id" add_foreign_key "posts", "users", column: "locked_by_id" add_foreign_key "privileges", "communities" diff --git a/db/seeds/categories.yml b/db/seeds/categories.yml index d8c5a1f28..d164fa60c 100644 --- a/db/seeds/categories.yml +++ b/db/seeds/categories.yml @@ -1,4 +1,5 @@ - name: Q&A + sequence: 1 short_wiki: General Q&A on the topic of the site. display_post_types: - <%= PostType['Question'].id %> @@ -12,6 +13,7 @@ license_id: <%= License.unscoped.first.id %> - name: Meta + sequence: 2 short_wiki: Discussions and feedback about the site itself in Q&A format. display_post_types: - <%= PostType['Question'].id %> @@ -22,4 +24,21 @@ use_for_hot_posts: true use_for_advertisement: false color_code: bluegray - license_id: <%= License.unscoped.first.id %> \ No newline at end of file + license_id: <%= License.unscoped.first.id %> + +- name: Templates + sequence: -1 + short_wiki: Templates + # Default to moderators-only + min_trust_level: 4 + min_title_length: 1 + display_post_types: + - <%= PostType['PostTemplate'].id %> + post_type_ids: + - <%= PostType['PostTemplate'].id %> + tag_set_id: <%= TagSet.unscoped.where(name: 'Meta').first.id %> + use_for_hot_posts: false + use_for_advertisement: false + color_code: bluegray + # The PostTemplate actually has has_license: false, but just in case + license_id: <%= License.unscoped.where(name: 'CC0').first.id %> \ No newline at end of file diff --git a/db/seeds/post_types.yml b/db/seeds/post_types.yml index 55b553866..8f0dd23e8 100644 --- a/db/seeds/post_types.yml +++ b/db/seeds/post_types.yml @@ -74,4 +74,19 @@ is_top_level: true is_freely_editable: false has_reactions: false + has_only_specific_reactions: false + +- name: PostTemplate + description: ~ + has_answers: false + has_votes: false + has_tags: false + has_parent: false + has_category: true + has_license: false + is_public_editable: false + is_closeable: false + is_top_level: true + is_freely_editable: false + has_reactions: false has_only_specific_reactions: false \ No newline at end of file