diff --git a/app/assets/javascripts/articles.coffee b/app/assets/javascripts/articles.coffee
new file mode 100644
index 000000000..24f83d18b
--- /dev/null
+++ b/app/assets/javascripts/articles.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/stylesheets/articles.scss b/app/assets/stylesheets/articles.scss
new file mode 100644
index 000000000..e77f17a9e
--- /dev/null
+++ b/app/assets/stylesheets/articles.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the Articles controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/assets/stylesheets/posts.scss b/app/assets/stylesheets/posts.scss
index 53e70c898..ed8df6017 100644
--- a/app/assets/stylesheets/posts.scss
+++ b/app/assets/stylesheets/posts.scss
@@ -75,3 +75,12 @@ h1 .badge.is-tag.is-master-tag {
margin-bottom: 0;
}
}
+
+.post--title {
+ display: flex;
+ align-items: center;
+
+ > .badge {
+ margin-left: 0.5em;
+ }
+}
diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb
new file mode 100644
index 000000000..b4253e185
--- /dev/null
+++ b/app/controllers/articles_controller.rb
@@ -0,0 +1,92 @@
+class ArticlesController < ApplicationController
+ before_action :set_article
+ before_action :check_article
+
+ def show
+ if @article.deleted?
+ check_your_privilege('ViewDeleted', @article) # || return
+ end
+ end
+
+ def share
+ redirect_to article_path(params[:id])
+ end
+
+ def edit
+ check_your_privilege('Edit', @article)
+ end
+
+ def update
+ return unless check_your_privilege('Edit', @article)
+
+ PostHistory.post_edited(@article, current_user, before: @article.body_markdown,
+ after: params[:article][:body_markdown], comment: params[:edit_comment])
+ body_rendered = helpers.render_markdown(params[:article][:body_markdown])
+ if @article.update(article_params.merge(tags_cache: params[:article][:tags_cache]&.reject { |e| e.to_s.empty? },
+ body: body_rendered, last_activity: DateTime.now,
+ last_activity_by: current_user))
+ redirect_to article_path(@article)
+ else
+ render :edit
+ end
+ end
+
+ def destroy
+ unless check_your_privilege('Delete', @article, false)
+ flash[:danger] = 'You must have the Delete privilege to delete posts.'
+ redirect_to article_path(@article) && return
+ end
+
+ if @article.deleted
+ flash[:danger] = "Can't delete a deleted post."
+ redirect_to article_path(@article) && return
+ end
+
+ if @article.update(deleted: true, deleted_at: DateTime.now, deleted_by: current_user,
+ last_activity: DateTime.now, last_activity_by: current_user)
+ PostHistory.post_deleted(@article, current_user)
+ else
+ flash[:danger] = "Can't delete this post right now. Try again later."
+ end
+ redirect_to article_path(@article)
+ end
+
+ def undelete
+ unless check_your_privilege('Delete', @article, false)
+ flash[:danger] = 'You must have the Delete privilege to undelete posts.'
+ redirect_to article_path(@article) && return
+ end
+
+ unless @article.deleted
+ flash[:danger] = "Can't undelete an undeleted post."
+ redirect_to article_path(@article) && return
+ end
+
+ if @article.update(deleted: false, deleted_at: nil, deleted_by: nil,
+ last_activity: DateTime.now, last_activity_by: current_user)
+ PostHistory.post_undeleted(@article, current_user)
+ else
+ flash[:danger] = "Can't undelete this article right now. Try again later."
+ end
+ redirect_to article_path(@article)
+ end
+
+ private
+
+ def set_article
+ @article = Article.find params[:id]
+ if @article.deleted && !current_user&.has_post_privilege?('ViewDeleted', @article)
+ not_found
+ end
+ end
+
+ def check_article
+ unless @article.post_type_id == Article.post_type_id
+ not_found
+ end
+ end
+
+ def article_params
+ params.require(:article).permit(:body_markdown, :title, :tags_cache)
+ end
+end
diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb
index 1ff980842..915a2c916 100644
--- a/app/controllers/comments_controller.rb
+++ b/app/controllers/comments_controller.rb
@@ -88,21 +88,10 @@ def check_privilege
def comment_link(comment)
if comment.post.question?
question_path(comment.post, anchor: "comment-#{comment.id}")
+ elsif comment.post.article?
+ article_path(comment.post, anchor: "comment-#{comment.id}")
else
question_path(comment.post.parent, anchor: "comment-#{comment.id}")
end
end
end
-
-# Provides a custom HTML sanitization interface to use for cleaning up the HTML in questions.
-class CommentScrubber < Rails::Html::PermitScrubber
- def initialize
- super
- self.tags = %w[a b i em strong strike del code]
- self.attributes = %w[href title]
- end
-
- def skip_node?(node)
- node.text?
- end
-end
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index ed4091c9e..0970ba6e3 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -15,7 +15,8 @@ def new
def create
@category = Category.find(params[:category_id])
- @post = Post.new(post_params.merge(category: @category, user: current_user, post_type_id: params[:post_type_id],
+ @post = Post.new(post_params.merge(category: @category, user: current_user,
+ post_type_id: params[:post][:post_type_id] || params[:post_type_id],
body: helpers.render_markdown(params[:post][:body_markdown])))
if @category.min_trust_level.present? && @category.min_trust_level > current_user.trust_level
@@ -25,7 +26,7 @@ def create
end
if @post.save
- redirect_to question_path(@post)
+ redirect_to helpers.generic_show_link(@post)
else
render :new, status: 400
end
diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb
index cf9ce5ba2..8d478f8be 100644
--- a/app/controllers/questions_controller.rb
+++ b/app/controllers/questions_controller.rb
@@ -65,7 +65,7 @@ def update
PostHistory.post_edited(@question, current_user, before: @question.body_markdown,
after: params[:question][:body_markdown], comment: params[:edit_comment])
body_rendered = helpers.render_markdown(params[:question][:body_markdown])
- if @question.update(question_params.merge(tags_cache: params[:question][:tags_cache]&.reject(&:empty?),
+ if @question.update(question_params.merge(tags_cache: params[:question][:tags_cache]&.reject { |e| e.to_s.empty? },
body: body_rendered, last_activity: DateTime.now,
last_activity_by: current_user))
redirect_to url_for(controller: :questions, action: :show, id: @question.id)
@@ -191,11 +191,15 @@ def question_params
def set_question
@question = Question.find params[:id]
rescue
- if current_user.has_privilege?('ViewDeleted')
+ if current_user&.has_privilege?('ViewDeleted')
@question ||= Question.unscoped.find params[:id]
end
if @question.nil?
- render template: 'errors/not_found', status: 404
+ not_found
+ return
+ end
+ unless @question.post_type_id == Question.post_type_id
+ not_found
end
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 78fdcce45..ef7d04e6b 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -75,4 +75,41 @@ def strip_markdown(markdown)
markdown
end
+
+ def generic_share_link(post)
+ case post.post_type_id
+ when Question.post_type_id
+ share_question_url(post)
+ when Answer.post_type_id
+ share_answer_url(qid: post.parent_id, id: post.id)
+ when Article.post_type_id
+ share_article_url(post)
+ else
+ '#'
+ end
+ end
+
+ def generic_edit_link(post)
+ case post.post_type_id
+ when Question.post_type_id
+ edit_question_url(post)
+ when Answer.post_type_id
+ edit_answer_url(post)
+ when Article.post_type_id
+ edit_article_url(post)
+ else
+ '#'
+ end
+ end
+
+ def generic_show_link(post)
+ case post.post_type_id
+ when Question.post_type_id
+ question_url(post)
+ when Article.post_type_id
+ article_url(post)
+ else
+ '#'
+ end
+ end
end
diff --git a/app/helpers/articles_helper.rb b/app/helpers/articles_helper.rb
new file mode 100644
index 000000000..296827759
--- /dev/null
+++ b/app/helpers/articles_helper.rb
@@ -0,0 +1,2 @@
+module ArticlesHelper
+end
diff --git a/app/helpers/categories_helper.rb b/app/helpers/categories_helper.rb
index bb87154d1..cfb31edf2 100644
--- a/app/helpers/categories_helper.rb
+++ b/app/helpers/categories_helper.rb
@@ -8,7 +8,8 @@ def active?(category)
def expandable?
(defined?(@category) && !current_page?(new_category_path)) ||
(defined?(@post) && !@post.category.nil?) ||
- (defined?(@question) && !@question.category.nil?)
+ (defined?(@question) && !@question.category.nil?) ||
+ (defined?(@article) && !@article.category.nil?)
end
def current_category
@@ -18,6 +19,8 @@ def current_category
@post.category
elsif defined?(@question) && !@question.category.nil?
@question.category
+ elsif defined?(@article) && !@article.category.nil?
+ @article.category
end
end
end
diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb
index 0ec9ca5f2..1feb966f4 100644
--- a/app/helpers/comments_helper.rb
+++ b/app/helpers/comments_helper.rb
@@ -1,2 +1,14 @@
module CommentsHelper
end
+
+class CommentScrubber < Rails::Html::PermitScrubber
+ def initialize
+ super
+ self.tags = %w[a b i em strong strike del code]
+ self.attributes = %w[href title]
+ end
+
+ def skip_node?(node)
+ node.text?
+ end
+end
diff --git a/app/helpers/post_types_helper.rb b/app/helpers/post_types_helper.rb
new file mode 100644
index 000000000..ddd6bc365
--- /dev/null
+++ b/app/helpers/post_types_helper.rb
@@ -0,0 +1,12 @@
+module PostTypesHelper
+ def post_type_badge(type)
+ icon_class = {
+ 'Question' => 'fas fa-question',
+ 'Article' => 'fas fa-newspaper'
+ }[type]
+ tag.span class: 'badge is-tag is-filled is-muted' do
+ tag.i(class: icon_class) + ' ' +
+ tag.span(type)
+ end
+ end
+end
diff --git a/app/models/article.rb b/app/models/article.rb
new file mode 100644
index 000000000..f5cb2f4d5
--- /dev/null
+++ b/app/models/article.rb
@@ -0,0 +1,7 @@
+class Article < Post
+ default_scope { where(post_type_id: Article.post_type_id) }
+
+ def self.post_type_id
+ PostType.mapping['Article']
+ end
+end
diff --git a/app/models/post.rb b/app/models/post.rb
index 036a49311..601c7189e 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -23,15 +23,15 @@ class Post < ApplicationRecord
validates :body, presence: true, length: { minimum: 30, maximum: 30_000 }
validates :doc_slug, uniqueness: { scope: [:community_id] }, if: -> { doc_slug.present? }
- validates :title, :body, :tags_cache, presence: true, if: :question?
- validate :tags_in_tag_set, if: :question?
- validate :maximum_tags, if: :question?
- validate :maximum_tag_length, if: :question?
- validate :no_spaces_in_tags, if: :question?
- validate :stripped_minimum, if: :question?
+ validates :title, :body, :tags_cache, presence: true, if: -> { question? || article? }
+ validate :tags_in_tag_set, if: -> { question? || article? }
+ validate :maximum_tags, if: -> { question? || article? }
+ validate :maximum_tag_length, if: -> { question? || article? }
+ validate :no_spaces_in_tags, if: -> { question? || article? }
+ validate :stripped_minimum, if: -> { question? || article? }
validate :category_allows_post_type
validate :license_available
- validate :required_tags?, if: -> { post_type_id == Question.post_type_id }
+ validate :required_tags?, if: -> { question? || article? }
scope :undeleted, -> { where(deleted: false) }
scope :deleted, -> { where(deleted: true) }
@@ -42,8 +42,8 @@ class Post < ApplicationRecord
after_save :modify_author_reputation
after_save :copy_last_activity_to_parent
after_save :break_description_cache
- after_save :update_category_activity, if: :question?
- before_validation :update_tag_associations, if: :question?
+ after_save :update_category_activity, if: -> { question? || article? }
+ before_validation :update_tag_associations, if: -> { question? || article? }
after_create :create_initial_revision
after_create :add_license_if_nil
@@ -53,7 +53,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'].each do |pt|
+ ['Question', 'Answer', 'PolicyDoc', 'HelpDoc', 'Article'].each do |pt|
define_method "#{pt.underscore}?" do
post_type_id == pt.constantize.post_type_id
end
diff --git a/app/models/vote.rb b/app/models/vote.rb
index fe5eb01fa..a94767d0b 100644
--- a/app/models/vote.rb
+++ b/app/models/vote.rb
@@ -17,7 +17,8 @@ class Vote < ApplicationRecord
def self.total_rep_change(col)
col = col.includes(:post)
settings = SiteSetting.where(name: ['QuestionUpVoteRep', 'QuestionDownVoteRep',
- 'AnswerUpVoteRep', 'AnswerDownVoteRep'])
+ 'AnswerUpVoteRep', 'AnswerDownVoteRep',
+ 'ArticleUpVoteRep', 'ArticleDownVoteRep'])
.map { |ss| [ss.name, ss.value] }.to_h
rep_changes = PostType.mapping.map do |k, v|
vote_types = { 1 => 'Up', -1 => 'Down' }
@@ -45,7 +46,9 @@ def rep_change(direction)
[post_type_ids['Question'], 1] => 'QuestionUpVoteRep',
[post_type_ids['Question'], -1] => 'QuestionDownVoteRep',
[post_type_ids['Answer'], 1] => 'AnswerUpVoteRep',
- [post_type_ids['Answer'], -1] => 'AnswerDownVoteRep'
+ [post_type_ids['Answer'], -1] => 'AnswerDownVoteRep',
+ [post_type_ids['Article'], 1] => 'ArticleUpVoteRep',
+ [post_type_ids['Article'], -1] => 'ArticleDownVoteRep'
}
rep_change = SiteSetting[setting_names[[post.post_type_id, vote_type]]] || 0
recv_user.update!(reputation: recv_user.reputation + direction * rep_change)
diff --git a/app/views/articles/_form.html.erb b/app/views/articles/_form.html.erb
new file mode 100644
index 000000000..bb9878bde
--- /dev/null
+++ b/app/views/articles/_form.html.erb
@@ -0,0 +1,47 @@
+<%= render 'posts/markdown_script' %>
+
+<% if @article.errors.any? %>
+
+ The following errors prevented this post from being saved:
+
+ <% @article.errors.full_messages.each do |msg| %>
+ - <%= msg %>
+ <% end %>
+
+
+<% end %>
+
+<%= render 'posts/image_upload' %>
+
+<%= form_for @article, url: edit_article_path(@article) do |f| %>
+
+ <%= f.label :title, "Title your post:", class: "form-element" %>
+ <%= f.text_field :title, class: "form-element" %>
+
+
+ <%= render 'shared/body_field', f: f, field_name: :body_markdown, field_label: 'Body' %>
+
+
+
+
+
+
+
+
+ <%= f.submit 'Save', class: "button is-filled" %>
+
+<% end %>
diff --git a/app/views/articles/edit.html.erb b/app/views/articles/edit.html.erb
new file mode 100644
index 000000000..6727a85a7
--- /dev/null
+++ b/app/views/articles/edit.html.erb
@@ -0,0 +1 @@
+<%= render 'form', is_edit: true %>
\ No newline at end of file
diff --git a/app/views/articles/show.html.erb b/app/views/articles/show.html.erb
new file mode 100644
index 000000000..0e822e298
--- /dev/null
+++ b/app/views/articles/show.html.erb
@@ -0,0 +1,8 @@
+<% content_for :title, @article.title.truncate(50) %>
+<% content_for :description do %>
+ <% Rails.cache.fetch "posts/#{@article.id}/description" do %>
+ <%= @article.body_plain[0..74].strip %>...
+ <% end %>
+<% end %>
+
+<%= render 'posts/expanded', post: @article %>
\ No newline at end of file
diff --git a/app/views/posts/_article_list.html.erb b/app/views/posts/_article_list.html.erb
new file mode 100644
index 000000000..237edc9cf
--- /dev/null
+++ b/app/views/posts/_article_list.html.erb
@@ -0,0 +1,30 @@
+<% active_user = post.last_activity_by || post.user %>
+
+
+ <%= post.score %>
+ score
+
+
+
+ <%= link_to post.title, share_article_path(post) %>
+
+
+ last activity <%= time_ago_in_words(post.last_activity) %> ago by <%= link_to active_user.username, user_path(active_user) %>
+
+
+ <% category = defined?(@category) ? @category : post.category %>
+ <% if category.display_post_types.reject { |e| e.to_s.empty? }.size > 1 %>
+ <%= post_type_badge(post.post_type.name) %>
+ <% end %>
+ <% tag_set = post.tag_set %>
+ <% required_ids = category&.required_tag_ids %>
+ <% topic_ids = category&.topic_tag_ids %>
+ <% category_sort_tags(post.tags, required_ids, topic_ids).each do |tag| %>
+ <% required = required_ids&.include? tag.id %>
+ <% topic = topic_ids&.include? tag.id %>
+ <%= link_to tag.name, questions_tagged_path(tag_set: tag_set.id, tag: tag.name),
+ class: "badge is-tag #{required ? 'is-filled' : ''} #{topic ? 'is-outlined' : ''}" %>
+ <% end %>
+
+
+
diff --git a/app/views/posts/_expanded.html.erb b/app/views/posts/_expanded.html.erb
index 7c40682cd..7e6c076f6 100644
--- a/app/views/posts/_expanded.html.erb
+++ b/app/views/posts/_expanded.html.erb
@@ -1,13 +1,16 @@
<% is_question = post.post_type_id == Question.post_type_id %>
+<% is_top_level = post.parent_id.nil? %>
+<% has_tags = is_top_level && !post.tag_ids.empty? %>
-
- <% if is_question %>
+
+ <% if is_top_level %>
- <% if post.meta? %>
- meta
- <% end %>
<%= post.title %>
- <%= post.closed ? "[closed]" : "" %>
+ <%= is_question && post.closed ? "[closed]" : "" %>
+ <% category = defined?(@category) ? @category : post.category %>
+ <% if category.display_post_types.reject { |e| e.to_s.empty? }.size > 1 %>
+ <%= post_type_badge(post.post_type.name) %>
+ <% end %>
<% end %>
@@ -95,7 +98,7 @@
- <% if is_question %>
+ <% if has_tags %>
<% tag_set = post.tag_set %>
<% required_ids = post.category&.required_tag_ids %>
@@ -121,18 +124,18 @@
<%= link_to 'history', post_history_path(post) %> ·
- <%= link_to 'edit', is_question ? edit_question_path(post) : edit_answer_path(post) %> ·
- <%= link_to 'permalink', is_question ? share_question_path(post) : share_answer_path(qid: post.parent_id, id: post.id) %> ·
+ <%= link_to 'edit', generic_edit_link(post) %> ·
+ <%= link_to 'permalink', generic_share_link(post) %> ·
<% if is_question && !post.closed %>
close ·
<% elsif is_question && post.closed %>
<%= link_to 'reopen', reopen_question_path(post), method: :post, class: 'reopen-question' %> ·
<% end %>
<% if !post.deleted %>
- <%= link_to 'delete', url_for(controller: is_question ? :questions : :answers, action: :destroy, id: post.id),
+ <%= link_to 'delete', url_for(controller: post.post_type.name.pluralize.downcase.to_sym, action: :destroy, id: post.id),
method: :delete, data: { confirm: 'Are you sure you want to delete this post?' }, class: "is-red" %>
<% else %>
- <%= link_to 'undelete', url_for(controller: is_question ? :questions : :answers, action: :undelete, id: post.id),
+ <%= link_to 'undelete', url_for(controller: post.post_type.name.pluralize.downcase.to_sym, action: :undelete, id: post.id),
method: :post, data: { confirm: 'Undelete this question, making it visible to regular users?' }, class: "is-red" %>
<% end %> ·
flag
diff --git a/app/views/posts/_form.html.erb b/app/views/posts/_form.html.erb
index 7bc27bc87..6e71b2d99 100644
--- a/app/views/posts/_form.html.erb
+++ b/app/views/posts/_form.html.erb
@@ -1,3 +1,5 @@
+<% with_post_type ||= false %>
+
<% content_for :head do %>
<%= render 'posts/markdown_script' %>
<% end %>
@@ -25,7 +27,20 @@
<%= form_for @post, url: submit_path, html: { class: 'has-margin-top-4' } do |f| %>
<%= f.hidden_field :category_id %>
- <%= f.hidden_field :post_type_id %>
+
+ <% if with_post_type %>
+
+ <%= f.label :post_type_id, 'Post type', class: 'form-element' %>
+ What kind of post is this? Questions can have answers; articles only have comments.
+ <% ids = @category.display_post_types.reject { |e| e.to_s.empty? } %>
+ <% post_types = PostType.where(id: ids) %>
+ <% opts = post_types.map { |pt| [pt.name, pt.id] } %>
+ <%= f.select :post_type_id, options_for_select(opts, selected: @post.post_type_id),
+ { include_blank: true }, class: 'form-element' %>
+
+ <% else %>
+ <%= f.hidden_field :post_type_id %>
+ <% end %>
<%= render 'shared/body_field', f: f, field_name: :body_markdown, field_label: 'Body' %>
diff --git a/app/views/posts/_list.html.erb b/app/views/posts/_list.html.erb
index 57312cf0c..e72067a30 100644
--- a/app/views/posts/_list.html.erb
+++ b/app/views/posts/_list.html.erb
@@ -1,5 +1,5 @@
<% is_question = post.post_type_id == Question.post_type_id %>
-<% is_meta = (is_question && post.meta?) || (!is_question && post.parent.meta?) %>
+<% is_meta = (is_question && post.meta?) || (!is_question && post.parent&.meta?) %>
<% active_user = post.last_activity_by || post.user %>
@@ -25,10 +25,14 @@
last activity <%= time_ago_in_words(post.last_activity) %> ago by <%= link_to active_user.username, user_path(active_user) %>
+ <% category = defined?(@category) ? @category : post.category %>
+ <% if category.display_post_types.reject { |e| e.to_s.empty? }.size > 1 %>
+ <%= post_type_badge(post.post_type.name) %>
+ <% end %>
<% if is_question %>
<% tag_set = post.tag_set %>
- <% required_ids = defined?(@category) ? @category&.required_tag_ids : post.category&.required_tag_ids %>
- <% topic_ids = defined?(@category) ? @category&.topic_tag_ids : post.category&.topic_tag_ids %>
+ <% required_ids = category&.required_tag_ids %>
+ <% topic_ids = category&.topic_tag_ids %>
<% category_sort_tags(post.tags, required_ids, topic_ids).each do |tag| %>
<% required = required_ids&.include? tag.id %>
<% topic = topic_ids&.include? tag.id %>
diff --git a/app/views/posts/_type_agnostic.html.erb b/app/views/posts/_type_agnostic.html.erb
index 9fb034ba7..2ac0c70f6 100644
--- a/app/views/posts/_type_agnostic.html.erb
+++ b/app/views/posts/_type_agnostic.html.erb
@@ -3,7 +3,8 @@
'Question' => 'posts/list',
'Answer' => 'posts/list',
'PolicyDoc' => 'posts/document',
- 'HelpDoc' => 'posts/document'
+ 'HelpDoc' => 'posts/document',
+ 'Article' => 'posts/article_list'
}
%>
<%= render post_types_views[post.post_type.name], post: post %>
\ No newline at end of file
diff --git a/app/views/posts/new.html.erb b/app/views/posts/new.html.erb
index 09a9d727b..1db9ccf3f 100644
--- a/app/views/posts/new.html.erb
+++ b/app/views/posts/new.html.erb
@@ -1,4 +1,5 @@
New Post in <%= @category.name %>
Not where you meant to post? See <%= link_to 'Categories', categories_path %>
-<%= render 'form', submit_path: create_post_path(@category.id) %>
\ No newline at end of file
+<%= render 'form', with_post_type: @category.display_post_types.reject { |e| e.to_s.empty? }.size > 1,
+ submit_path: create_post_path(@category.id) %>
\ No newline at end of file
diff --git a/app/views/questions/tagged.html.erb b/app/views/questions/tagged.html.erb
index 63bfaafc4..2aa7fe81e 100644
--- a/app/views/questions/tagged.html.erb
+++ b/app/views/questions/tagged.html.erb
@@ -4,7 +4,7 @@
<% @questions.each do |question| %>
- <%= render 'posts/list', post: question %>
+ <%= render 'posts/type_agnostic', post: question %>
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index b7efba506..af9ed4624 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -68,6 +68,14 @@
post 'questions/:id/close', to: 'questions#close', as: :close_question
post 'questions/:id/reopen', to: 'questions#reopen', as: :reopen_question
+ scope 'articles' do
+ get ':id', to: 'articles#show', as: :article
+ get ':id/edit', to: 'articles#edit', as: :edit_article
+ patch ':id/edit', to: 'articles#update', as: :update_article
+ delete ':id/delete', to: 'articles#destroy', as: :destroy_article
+ delete ':id/undelete', to: 'articles#undelete', as: :undelete_article
+ end
+
get 'posts/:id/history', to: 'post_history#post', as: :post_history
get 'posts/search', to: 'search#search', as: :search
post 'posts/upload', to: 'posts#upload', as: :upload
@@ -135,6 +143,7 @@
get 'q/:id', to: 'posts#share_q', as: :share_question
get 'a/:qid/:id', to: 'posts#share_a', as: :share_answer
+ get 'ar/:id', to: 'articles#share', as: :share_article
get 'subscriptions/new/:type', to: 'subscriptions#new', as: :new_subscription
post 'subscriptions/new', to: 'subscriptions#create', as: :create_subscription
diff --git a/db/seeds/post_types.yml b/db/seeds/post_types.yml
index c957d9cfd..ae17f34ee 100644
--- a/db/seeds/post_types.yml
+++ b/db/seeds/post_types.yml
@@ -1,4 +1,5 @@
- name: Question
- name: Answer
- name: PolicyDoc
-- name: HelpDoc
\ No newline at end of file
+- name: HelpDoc
+- name: Article
\ No newline at end of file
diff --git a/db/seeds/site_settings.yml b/db/seeds/site_settings.yml
index 145846f65..bcf88b9f4 100644
--- a/db/seeds/site_settings.yml
+++ b/db/seeds/site_settings.yml
@@ -54,6 +54,20 @@
description: >
The reputation change to the post's author when an answer is downvoted.
+- name: ArticleUpVoteRep
+ value: 10
+ value_type: integer
+ category: ReputationAndVoting
+ description: >
+ The reputation change to the post's author when an article is upvoted.
+
+- name: ArticleDownVoteRep
+ value: -2
+ value_type: integer
+ category: ReputationAndVoting
+ description: >
+ The reputation change to the post's author when an article is downvoted.
+
- name: AllowSelfVotes
value: false
value_type: boolean
diff --git a/test/controllers/articles_controller_test.rb b/test/controllers/articles_controller_test.rb
new file mode 100644
index 000000000..f431f0687
--- /dev/null
+++ b/test/controllers/articles_controller_test.rb
@@ -0,0 +1,57 @@
+require 'test_helper'
+
+class ArticlesControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+
+ test 'should get show article page' do
+ get :show, params: { id: posts(:article_one).id }
+ assert_not_nil assigns(:article)
+ assert_response(200)
+ end
+
+ test 'should get show article page with deleted article' do
+ sign_in users(:deleter)
+ get :show, params: { id: posts(:deleted_article).id }
+ assert_not_nil assigns(:article)
+ assert_response(200)
+ end
+
+ test 'should prevent unprivileged user seeing deleted post' do
+ get :show, params: { id: posts(:deleted_article).id }
+ assert_response 404
+ end
+
+ test 'should get edit article page' do
+ sign_in users(:editor)
+ get :edit, params: { id: posts(:article_one).id }
+ assert_not_nil assigns(:article)
+ assert_response(200)
+ end
+
+ test 'should update existing article' do
+ sign_in users(:editor)
+ patch :update, params: { id: posts(:article_one).id, article: { title: 'ABCDEF GHIJKL MNOPQR',
+ body_markdown: 'ABCDEF GHIJKL MNOPQR STUVWX YZ',
+ tags_cache: ['discussion', 'support'] } }
+ assert_not_nil assigns(:article)
+ assert_equal ['discussion', 'support'], assigns(:article).tags_cache
+ assert_equal ['discussion', 'support'], assigns(:article).tags.map(&:name)
+ assert_response(302)
+ end
+
+ test 'should mark article deleted' do
+ sign_in users(:deleter)
+ delete :destroy, params: { id: posts(:article_one).id }
+ assert_not_nil assigns(:article)
+ assert_equal true, assigns(:article).deleted
+ assert_response(302)
+ end
+
+ test 'should mark article undeleted' do
+ sign_in users(:deleter)
+ delete :undelete, params: { id: posts(:deleted_article).id }
+ assert_not_nil assigns(:article)
+ assert_equal false, assigns(:article).deleted
+ assert_response(302)
+ end
+end
diff --git a/test/controllers/moderator_controller_test.rb b/test/controllers/moderator_controller_test.rb
index 9350bc885..8528ecc96 100644
--- a/test/controllers/moderator_controller_test.rb
+++ b/test/controllers/moderator_controller_test.rb
@@ -9,46 +9,6 @@ class ModeratorControllerTest < ActionController::TestCase
assert_response(200)
end
- test 'should get recently deleted questions' do
- sign_in users(:moderator)
- get :recently_deleted_questions
- assert_not_nil assigns(:questions)
- assigns(:questions).each do |question|
- assert_equal true, question.deleted
- end
- assert_response(200)
- end
-
- test 'should get recently deleted answers' do
- sign_in users(:moderator)
- get :recently_deleted_answers
- assert_not_nil assigns(:answers)
- assigns(:answers).each do |answer|
- assert_equal true, answer.deleted
- end
- assert_response(200)
- end
-
- test 'should get recently undeleted questions' do
- sign_in users(:moderator)
- get :recently_undeleted_questions
- assert_not_nil assigns(:questions)
- assigns(:questions).each do |question|
- assert_equal false, question.deleted
- end
- assert_response(200)
- end
-
- test 'should get recently undeleted answers' do
- sign_in users(:moderator)
- get :recently_undeleted_answers
- assert_not_nil assigns(:answers)
- assigns(:answers).each do |answer|
- assert_equal false, answer.deleted
- end
- assert_response(200)
- end
-
test 'should require authentication to access pages' do
sign_out :user
[:index, :recently_deleted_answers, :recently_deleted_questions, :recently_undeleted_answers,
diff --git a/test/controllers/questions_controller_test.rb b/test/controllers/questions_controller_test.rb
index 32cf644c5..9ca6af0f0 100644
--- a/test/controllers/questions_controller_test.rb
+++ b/test/controllers/questions_controller_test.rb
@@ -189,4 +189,10 @@ class QuestionsControllerTest < ActionController::TestCase
assert_not_nil flash[:danger]
assert_response(302)
end
+
+ test 'should prevent using questions routes for articles' do
+ sign_in users(:deleter) # deliberate; catch ViewDeleted using unscoped
+ get :show, params: { id: posts(:article_one).id }
+ assert_response 404
+ end
end
diff --git a/test/fixtures/categories.yml b/test/fixtures/categories.yml
index 53bae5c6f..df944e28a 100644
--- a/test/fixtures/categories.yml
+++ b/test/fixtures/categories.yml
@@ -6,9 +6,11 @@ main:
short_wiki: Main Q&A
display_post_types:
- <%= Question.post_type_id %>
+ - <%= Article.post_type_id %>
post_types:
- question
- answer
+ - article
tag_set: main
license: cc_by_sa
diff --git a/test/fixtures/post_types.yml b/test/fixtures/post_types.yml
index 4736a9e20..71c578bf1 100644
--- a/test/fixtures/post_types.yml
+++ b/test/fixtures/post_types.yml
@@ -4,6 +4,9 @@ question:
answer:
name: Answer
+article:
+ name: Article
+
policy_doc:
name: PolicyDoc
diff --git a/test/fixtures/posts.yml b/test/fixtures/posts.yml
index e8b53a4dd..3039cfa85 100644
--- a/test/fixtures/posts.yml
+++ b/test/fixtures/posts.yml
@@ -147,3 +147,44 @@ help_doc:
user: admin
community: sample
license: cc_by_sa
+
+article_one:
+ post_type: article
+ title: Q1 ABCDEF GHIJKL MNOPQR STUVWX YZ
+ body: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ
+ body_markdown: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ
+ tags_cache:
+ - discussion
+ - support
+ - bug
+ tags:
+ - discussion
+ - support
+ - bug
+ score: 0
+ user: standard_user
+ community: sample
+ category: main
+ license: cc_by_sa
+
+deleted_article:
+ post_type: article
+ title: Q1 ABCDEF GHIJKL MNOPQR STUVWX YZ
+ body: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ
+ body_markdown: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ
+ tags_cache:
+ - discussion
+ - support
+ - bug
+ tags:
+ - discussion
+ - support
+ - bug
+ score: 0
+ user: standard_user
+ community: sample
+ category: main
+ license: cc_by_sa
+ deleted: true
+ deleted_at: 2019-01-01T00:00:00.000000Z
+ deleted_by: admin