diff --git a/.circleci/config.yml b/.circleci/config.yml index 3e59eb6372373313908432c21d28ac9ca144f914..a7e7aa2f66d129a940b4ecd0ce5aa4644281ca59 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,6 @@ version: 2.1 jobs: - build: + test: docker: - image: circleci/ruby:2.6.6-node - image: circleci/mysql:8.0.20 @@ -50,12 +50,49 @@ jobs: command: | git rev-parse $(git rev-parse --abbrev-ref HEAD) - run: - name: Rubocop + name: Coveralls token command: | - bundle exec rubocop + echo "repo_token: $COVERALLS_REPO_TOKEN" > .coveralls.yml - run: name: Test command: | bundle exec rails test - store_test_results: - path: "~/qpixel/test/reports" \ No newline at end of file + path: "~/qpixel/test/reports" + + rubocop: + docker: + - image: circleci/ruby:2.6.6-node + + working_directory: ~/qpixel + + steps: + - run: + name: Install packages + command: | + sudo apt-get -qq update + sudo apt-get -y install git libmariadb-dev libmagickwand-dev + - checkout + - restore_cache: + keys: + - qpixel-{{ checksum "Gemfile" }} + - qpixel- + - run: + name: Install Bundler & gems + command: | + gem install bundler + bundle install --path=~/gems + - save_cache: + key: qpixel-{{ checksum "Gemfile" }} + paths: + - ~/gems + - run: + name: Rubocop + command: | + bundle exec rubocop + +workflows: + test_lint: + jobs: + - test + - rubocop \ No newline at end of file diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index 8ac2189bd861fb80e3d77be946777a787d06f1cb..0000000000000000000000000000000000000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,46 +0,0 @@ ---- -engines: - brakeman: - enabled: true - bundler-audit: - enabled: true - csslint: - enabled: true - coffeelint: - enabled: true - duplication: - enabled: true - config: - languages: - ruby: - mass_threshold: 70 - javascript: - mass_threshold: 70 - eslint: - enabled: true - fixme: - enabled: true - rubocop: - enabled: true -ratings: - paths: - - Gemfile.lock - - "**.erb" - - "**.haml" - - "**.rb" - - "**.rhtml" - - "**.slim" - - "**.css" - - "**.coffee" - - "**.inc" - - "**.js" - - "**.jsx" - - "**.module" - - "**.php" - - "**.py" -exclude_paths: -- config/ -- db/ -- test/ -- vendor/ -- doc/ diff --git a/.csslintrc b/.csslintrc deleted file mode 100644 index aacba956e5bbede1c195ce554fcad3e200b24ff8..0000000000000000000000000000000000000000 --- a/.csslintrc +++ /dev/null @@ -1,2 +0,0 @@ ---exclude-exts=.min.css ---ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes diff --git a/.rubocop.yml b/.rubocop.yml index ad7500dc2509dcb8219df8afe28058129bc0af60..609087b50a5bbac7d28288da95b20035bcd23532 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -32,12 +32,14 @@ Metrics/AbcSize: Enabled: false Metrics/BlockLength: Max: 30 +Metrics/BlockNesting: + Max: 5 Metrics/ClassLength: Max: 300 Metrics/CyclomaticComplexity: Max: 30 Metrics/MethodLength: - Max: 40 + Max: 60 Metrics/ModuleLength: Max: 200 Metrics/PerceivedComplexity: diff --git a/Gemfile b/Gemfile index 5eba0d0af84ae98ecc5926f62c9ed358fd011aee..2a4ddc0cda16286149f558ca940cb5ccd08c8e78 100644 --- a/Gemfile +++ b/Gemfile @@ -47,6 +47,7 @@ gem 'whenever', '~> 1.0', require: false gem 'awesome_print', '~> 1.8' gem 'coveralls', '~> 0.8', require: false gem 'rubocop', '~> 1' +gem 'rubocop-rails', '~> 2.9' # MiniProfiler support, including stack traces & memory dumps, plus flamegraphs. gem 'flamegraph', '~> 0.9' diff --git a/Gemfile.lock b/Gemfile.lock index 9e4ec448c69eef4dfeda03a8b51b33a9c9203e98..41884b63dfc10bd48e5434135370aa40700e6c2c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -236,6 +236,10 @@ GEM unicode-display_width (>= 1.4.0, < 2.0) rubocop-ast (1.3.0) parser (>= 2.7.1.5) + rubocop-rails (2.9.0) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 0.90.0, < 2.0) ruby-enum (0.8.0) i18n ruby-progressbar (1.10.1) @@ -341,6 +345,7 @@ DEPENDENCIES rotp (~> 6.0) rqrcode (~> 1.1) rubocop (~> 1) + rubocop-rails (~> 2.9) ruby-progressbar (~> 1.10) sass-rails (~> 5.0) spring (~> 2.1) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 627f7c95585961bd5b1656301a2fe76d86f47636..5b5dc6f7da61799adce1fd059aa2b6cbecfe42c4 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -72,30 +72,31 @@ $(document).on('ready', function() { }); }); - $("a.close-dialog-link").on("click", (ev) => { + $('.close-dialog-link').on('click', (ev) => { ev.preventDefault(); const self = $(ev.target); console.log(self.parents(".post--body").find(".js-close-box").toggleClass("is-active")); }); - $("button.close-question").on("click", (ev) => { + + $('.js-close-question').on('click', (ev) => { ev.preventDefault(); const self = $(ev.target); - active_radio = self.parents(".js-close-box").find("input[type='radio'][name='close-reason']:checked"); + const active_radio = self.parents('.js-close-box').find("input[type='radio'][name='close-reason']:checked"); const data = { 'reason_id': active_radio.val(), - 'other_post': active_radio.parents(".widget--body").find(".js-close-other-post").val() + 'other_post': active_radio.parents('.widget--body').find('.js-close-other-post').val() // option will be silently discarded if no input element }; - if (data["other_post"]) { - if (data["other_post"].match(/\/[0-9]+$/)) { - data["other_post"] = data["other_post"].replace(/.*\/([0-9]+)$/, "$1"); + if (data['other_post']) { + if (data['other_post'].match(/\/[0-9]+$/)) { + data['other_post'] = data['other_post'].replace(/.*\/([0-9]+)$/, "$1"); } } $.ajax({ 'type': 'POST', - 'url': '/questions/' + self.data("post-id") + '/close', + 'url': '/posts/' + self.data('post-id') + '/close', 'data': data, 'target': self }) diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index a618fff16f4be724371704ef7887e8ed212afb91..80f1a12c0f7afea3c855b6938850d951de13035f 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -35,7 +35,7 @@ class AdminController < ApplicationController @ability.update("#{type}_score_threshold" => params[:threshold]) AuditLog.admin_audit(event_type: 'ability_threshold_update', related: @ability, user: current_user, comment: "#{params[:type]} score\nfrom <<#{pre}>>\nto <<#{params[:threshold]}>>") - render json: { status: 'OK', privilege: @ability }, status: 202 + render json: { status: 'OK', privilege: @ability }, status: :accepted end def admin_email; end diff --git a/app/controllers/advertisement_controller.rb b/app/controllers/advertisement_controller.rb index b5b62aca3bf0422e1deb94471c196783bac0b482..e884040c62ad5229fb1053cccad9f50b71a1681a 100644 --- a/app/controllers/advertisement_controller.rb +++ b/app/controllers/advertisement_controller.rb @@ -2,9 +2,9 @@ require 'rmagick' require 'open-uri' # Necessary due to rmagick -# rubocop:disable Metrics/ClassLength # rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/BlockLength +# rubocop:disable Metrics/ClassLength # noinspection RubyResolve, DuplicatedCode, RubyArgCount class AdvertisementController < ApplicationController include Magick @@ -197,7 +197,7 @@ class AdvertisementController < ApplicationController if category.nil? category = Category.where(use_for_advertisement: true) end - Post.undeleted.where(last_activity: (Rails.env.development? ? 365 : 7).days.ago..Time.now) + Post.undeleted.where(last_activity: (Rails.env.development? ? 365 : 7).days.ago..Time.zone.now) .where(post_type_id: Question.post_type_id) .where(category: category) .where('score > ?', SiteSetting['HotPostsScoreThreshold']) @@ -370,5 +370,5 @@ class AdvertisementController < ApplicationController end end # rubocop:enable Metrics/MethodLength -# rubocop:enable Metrics/ClassLength # rubocop:enable Metrics/BlockLength +# rubocop:enable Metrics/ClassLength diff --git a/app/controllers/answers_controller.rb b/app/controllers/answers_controller.rb index 6ee1aa676c2856aaa43cbcbf10f5dbfe30ae2667..a30ba3f64b51dff519403da0fb387232fd879511 100644 --- a/app/controllers/answers_controller.rb +++ b/app/controllers/answers_controller.rb @@ -1,166 +1,10 @@ # Web controller. Provides actions that relate to answers. Pretty much the standard set of resources, really - it's # questions that have a few more actions. class AnswersController < ApplicationController - before_action :authenticate_user!, only: [:new, :create, :edit, :update, :destroy, :undelete, :convert_to_comment] - before_action :set_answer, only: [:edit, :update, :destroy, :undelete, :convert_to_comment] + before_action :authenticate_user!, only: [:convert_to_comment] + before_action :set_answer, only: [:convert_to_comment] before_action :verify_moderator, only: [:convert_to_comment] - before_action :check_if_answer_locked, only: [:edit, :update, :destroy, :undelete, :convert_to_comment] - - def new - @answer = Answer.new - @question = Question.find params[:id] - end - - def create - @question = Question.find params[:id] - - @answer = Answer.new(answer_params.merge(parent: @question, user: current_user, score: 0, - body: helpers.post_markdown(:answer, :body_markdown), - last_activity: DateTime.now, last_activity_by: current_user, - category: @question.category)) - - recent_second_level_posts = Post.where(created_at: 24.hours.ago..Time.now, user: current_user) - .where(post_type_id: second_level_post_types).count - - max_slps = SiteSetting[if current_user.privilege?('unrestricted') - 'RL_SecondLevelPosts' - else - 'RL_NewUserSecondLevelPosts' - end] - - post_limit_msg = if current_user.privilege? 'unrestricted' - "You may only post #{max_slps} answers per day." - else - "You may only post #{max_slps} answers per day. " \ - 'Once you have some well-received posts, that limit will increase.' - end - - if recent_second_level_posts >= max_slps - @answer.errors.add :base, post_limit_msg - AuditLog.rate_limit_log(event_type: 'second_level_post', related: @question, user: current_user, - comment: "limit: #{max_slps}\n\npost:\n#{@answer.attributes_print}") - render :new, status: 400 - return - end - - unless current_user.id == @question.user.id - @question.user.create_notification("New answer to your question '#{@question.title.truncate(50)}'", - share_question_url(@question)) - end - if @answer.save - @question.update(last_activity: DateTime.now, last_activity_by: current_user) - unless current_user.id == @question.user.id - @question.user.create_notification("New answer to your question '#{@question.title.truncate(50)}'", - share_question_url(@question)) - end - redirect_to url_for(controller: :questions, action: :show, id: params[:id]) - else - render :new, status: 422 - end - end - - def edit; end - - def update - can_post_in_category = @answer.parent.category.present? && - (@answer.parent.category.min_trust_level || -1) <= current_user&.trust_level - unless current_user&.has_post_privilege?('edit_posts', @answer) && can_post_in_category - return update_as_suggested_edit - end - - if params[:answer][:body_markdown] == @answer.body_markdown - flash[:danger] = "No changes were saved because you didn't edit the post." - return redirect_to question_path(@answer.parent) - end - - before = @answer.body_markdown - if @answer.update(answer_params.merge(body: helpers.post_markdown(:answer, :body_markdown), - last_activity: DateTime.now, last_activity_by: current_user, - last_edited_at: DateTime.now, last_edited_by: current_user, - license_id: @answer.license_id)) - PostHistory.post_edited(@answer, current_user, before: before, - after: params[:answer][:body_markdown], comment: params[:edit_comment]) - redirect_to share_answer_path(qid: @answer.parent_id, id: @answer.id) - else - render :edit - end - end - - def update_as_suggested_edit - return if check_edits_limit! @answer - - if params[:answer][:body_markdown] == @answer.body_markdown - flash[:danger] = "No changes were saved because you didn't edit the post." - return redirect_to question_path(@answer.parent) - end - - updates = { - post: @answer, - user: current_user, - community: @answer.community, - body: helpers.post_markdown(:answer, :body_markdown), - body_markdown: params[:answer][:body_markdown] == @answer.body_markdown ? nil : params[:answer][:body_markdown], - comment: params[:edit_comment], - active: true, accepted: false, - decided_at: nil, decided_by: nil, - rejected_comment: nil - } - - @edit = SuggestedEdit.new(updates) - if @edit.save - @answer.user.create_notification("Edit suggested on your answer to #{@answer.parent.title.truncate(50)}", - share_answer_url(qid: @answer.parent_id, id: @answer.id)) - redirect_to share_answer_path(qid: @answer.parent_id, id: @answer.id) - else - @answer.errors = @edit.errors - render :edit - end - end - - def destroy - unless check_your_privilege('flag_curate', @answer, false) - flash[:danger] = helpers.ability_err_msg(:flag_curate, 'delete this answer') - redirect_to(question_path(@answer.parent)) && return - end - - if @answer.deleted - flash[:danger] = "Can't delete a deleted answer." - redirect_to(question_path(@answer.parent)) && return - end - - if @answer.update(deleted: true, deleted_at: DateTime.now, deleted_by: current_user, - last_activity: DateTime.now, last_activity_by: current_user) - PostHistory.post_deleted(@answer, current_user) - else - flash[:danger] = "Can't delete this answer right now. Try again later." - end - redirect_to question_path(@answer.parent) - end - - def undelete - unless check_your_privilege('flag_curate', @answer, false) - flash[:danger] = flash[:danger] = helpers.ability_err_msg(:flag_curate, 'undelete this answer') - redirect_to(question_path(@answer.parent)) && return - end - - unless @answer.deleted - flash[:danger] = "Can't undelete an undeleted answer." - redirect_to(question_path(@answer.parent)) && return - end - - if @answer.deleted_by.is_moderator && !current_user.is_moderator - flash[:danger] = 'You cannot undelete this post deleted by a moderator.' - redirect_to(question_path(@answer.parent)) && return - end - - if @answer.update(deleted: false, deleted_at: nil, deleted_by: nil, - last_activity: DateTime.now, last_activity_by: current_user) - PostHistory.post_undeleted(@answer, current_user) - else - flash[:danger] = "Can't undelete this answer right now. Try again later." - end - redirect_to question_path(@answer.parent) - end + before_action :check_if_answer_locked, only: [:convert_to_comment] def convert_to_comment text = @answer.body_markdown @@ -176,10 +20,6 @@ class AnswersController < ApplicationController private - def answer_params - params.require(:answer).permit(:body_markdown, :license_id) - end - def set_answer @answer = Answer.find params[:id] end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9db1e2e55d48ef37165e3ace224a52e8296dbba1..527bc7b615495d8f9bab081a5d83669dc19b6c0e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,6 +10,8 @@ class ApplicationController < ActionController::Base before_action :check_if_warning_or_suspension_pending before_action :stop_the_awful_troll + helper_method :top_level_post_types, :second_level_post_types + def upload redirect_to helpers.upload_remote_url(params[:key]) end @@ -28,13 +30,29 @@ class ApplicationController < ActionController::Base devise_parameter_sanitizer.permit(:account_update, keys: [:username, :profile, :website, :twitter]) end - def not_found - render 'errors/not_found', layout: 'without_sidebar', status: 404 + def not_found(**add) + respond_to do |format| + format.html do + render 'errors/not_found', layout: 'without_sidebar', status: :not_found + end + format.json do + render json: { status: 'failed', success: false, errors: ['not_found'] }.merge(add), status: :not_found + end + end + false end def verify_moderator if !user_signed_in? || !(current_user.is_moderator || current_user.is_admin) - render 'errors/not_found', layout: 'without_sidebar', status: 404 + respond_to do |format| + format.html do + render 'errors/not_found', layout: 'without_sidebar', status: :not_found + end + format.json do + render json: { status: 'failed', success: false, errors: ['not_found'] }, status: :not_found + end + end + return false end true @@ -42,7 +60,7 @@ class ApplicationController < ActionController::Base def verify_admin if !user_signed_in? || !current_user.is_admin - render 'errors/not_found', layout: 'without_sidebar', status: 404 + render 'errors/not_found', layout: 'without_sidebar', status: :not_found return false end true @@ -50,7 +68,7 @@ class ApplicationController < ActionController::Base def verify_global_admin if !user_signed_in? || !current_user.is_global_admin - render 'errors/not_found', layout: 'without_sidebar', status: 404 + render 'errors/not_found', layout: 'without_sidebar', status: :not_found return false end true @@ -58,7 +76,7 @@ class ApplicationController < ActionController::Base def verify_global_moderator if !user_signed_in? || !(current_user.is_global_moderator || current_user.is_global_admin) - render 'errors/not_found', layout: 'without_sidebar', status: 404 + render 'errors/not_found', layout: 'without_sidebar', status: :not_found return false end true @@ -67,7 +85,7 @@ class ApplicationController < ActionController::Base def check_your_privilege(name, post = nil, render_error = true) unless current_user&.privilege?(name) || (current_user&.has_post_privilege?(name, post) if post) @privilege = Ability.find_by(name: name) - render 'errors/forbidden', layout: 'without_sidebar', privilege_name: name, status: 403 if render_error + render 'errors/forbidden', layout: 'without_sidebar', privilege_name: name, status: :forbidden if render_error return false end true @@ -78,22 +96,26 @@ class ApplicationController < ActionController::Base if post.locked? respond_to do |format| - format.html { render 'errors/locked', layout: 'without_sidebar', status: 401 } - format.json { render json: { status: 'failed', message: 'Post is locked.' }, status: 401 } + format.html { render 'errors/locked', layout: 'without_sidebar', status: :unauthorized } + format.json { render json: { status: 'failed', message: 'Post is locked.' }, status: :unauthorized } end end end def top_level_post_types - [Question.post_type_id, Article.post_type_id] + Rails.cache.fetch 'top_level_post_types' do + PostType.where(is_top_level: true).select(:id).map(&:id) + end end def second_level_post_types - [Answer.post_type_id] + Rails.cache.fetch 'second_level_post_types' do + PostType.where(is_top_level: false, has_parent: true).select(:id).map(&:id) + end end def check_edits_limit!(post) - recent_edits = SuggestedEdit.where(created_at: 24.hours.ago..Time.now, user: current_user) \ + recent_edits = SuggestedEdit.where(created_at: 24.hours.ago..Time.zone.now, user: current_user) \ .where('active = TRUE OR accepted = FALSE').count max_edits = SiteSetting[if current_user.privilege?('unrestricted') @@ -113,7 +135,7 @@ class ApplicationController < ActionController::Base post.errors.add :base, edit_limit_msg AuditLog.rate_limit_log(event_type: 'suggested_edits', related: post, user: current_user, comment: "limit: #{max_edits}") - render :edit, status: 400 + render :edit, status: :bad_request return true end false @@ -184,8 +206,8 @@ class ApplicationController < ActionController::Base Rails.logger.info " Host #{host_name}, community ##{RequestContext.community_id} " \ "(#{RequestContext.community&.name})" - unless RequestContext.community.present? - render status: 422, plain: "No community record matching Host='#{host_name}'" + if RequestContext.community.blank? + render status: :unprocessable_entity, plain: "No community record matching Host='#{host_name}'" return false end @@ -210,7 +232,7 @@ class ApplicationController < ActionController::Base end @hot_questions = Rails.cache.fetch("#{RequestContext.community_id}/hot_questions", expires_in: 4.hours) do Rack::MiniProfiler.step 'hot_questions: cache miss' do - Post.undeleted.where(last_activity: (Rails.env.development? ? 365 : 7).days.ago..Time.now) + Post.undeleted.where(last_activity: (Rails.env.development? ? 365 : 7).days.ago..Time.zone.now) .where(post_type_id: [Question.post_type_id, Article.post_type_id]) .joins(:category).where(categories: { use_for_hot_posts: true }) .where('score >= ?', SiteSetting['HotPostsScoreThreshold']) diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb deleted file mode 100644 index 21debf8bbf969c23acfe8368eef4b02c14ea59ad..0000000000000000000000000000000000000000 --- a/app/controllers/articles_controller.rb +++ /dev/null @@ -1,157 +0,0 @@ -class ArticlesController < ApplicationController - before_action :set_article - before_action :check_article - before_action :check_if_article_locked, only: [:edit, :update, :destroy, :undelete, :close, :reopen] - - def show - if @article.deleted? - check_your_privilege('flag_curate', @article) # || return - end - end - - def share - redirect_to article_path(params[:id]) - end - - def edit; end - - def update - can_post_in_category = @article.category.present? && - (@article.category.min_trust_level || -1) <= current_user&.trust_level - unless current_user&.has_post_privilege?('edit_posts', @article) && can_post_in_category - return update_as_suggested_edit - end - - tags_cache = params[:article][:tags_cache]&.reject { |e| e.to_s.empty? } - after_tags = Tag.where(tag_set_id: @article.category.tag_set_id, name: tags_cache) - - if @article.tags == after_tags && @article.body_markdown == params[:article][:body_markdown] && - @article.title == params[:article][:title] - flash[:danger] = "No changes were saved because you didn't edit the post." - return redirect_to article_path(@article) - end - - body_rendered = helpers.post_markdown(:article, :body_markdown) - before = { body: @article.body_markdown, title: @article.title, tags: @article.tags } - if @article.update(article_params.merge(tags_cache: tags_cache, body: body_rendered, - last_activity: DateTime.now, last_activity_by: current_user, - last_edited_at: DateTime.now, last_edited_by: current_user)) - PostHistory.post_edited(@article, current_user, before: before[:body], - after: params[:article][:body_markdown], comment: params[:edit_comment], - before_title: before[:title], after_title: params[:article][:title], - before_tags: before[:tags], after_tags: after_tags) - redirect_to share_article_path(@article) - else - render :edit - end - end - - def update_as_suggested_edit - return if check_edits_limit! @article - - body_rendered = helpers.post_markdown(:article, :body_markdown) - new_tags_cache = params[:article][:tags_cache]&.reject(&:empty?) - - body_markdown = if params[:article][:body_markdown] != @article.body_markdown - params[:article][:body_markdown] - end - - if @article.tags_cache == new_tags_cache && @article.body_markdown == params[:article][:body_markdown] && - @article.title == params[:article][:title] - flash[:danger] = "No changes were saved because you didn't edit the post." - return redirect_to article_path(@article) - end - - updates = { - post: @article, - user: current_user, - community: @article.community, - body: body_rendered, - title: params[:article][:title] == @article.title ? nil : params[:article][:title], - tags_cache: new_tags_cache == @article.tags_cache ? @article.tags_cache : new_tags_cache, - body_markdown: body_markdown, - comment: params[:edit_comment], - active: true, accepted: false, - decided_at: nil, decided_by: nil, - rejected_comment: nil - } - - @edit = SuggestedEdit.new(updates) - if @edit.save - @article.user.create_notification("Edit suggested on your post #{@article.title.truncate(50)}", - article_url(@article)) - redirect_to share_article_path(@article) - else - @article.errors = @edit.errors - render :edit - end - end - - def destroy - unless check_your_privilege('flag_curate', @article, false) - flash[:danger] = helpers.ability_err_msg(:flag_curate, 'delete this article') - 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('flag_curate', @article, false) - flash[:danger] = helpers.ability_err_msg(:flag_curate, 'undelete this article') - 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.deleted_by.is_moderator && !current_user.is_moderator - flash[:danger] = 'You cannot undelete this post deleted by a moderator.' - 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?('flag_curate', @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 - - def check_if_article_locked - check_if_locked(@article) - end -end diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index 6d82fb3211794e75bdbcb6b5dcda7b32afc79614..84e61e08af8ae2c03ae2295854fea5ea31aaa42f 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -1,8 +1,8 @@ class CategoriesController < ApplicationController - before_action :authenticate_user!, except: [:index, :show, :homepage, :rss_feed] - before_action :verify_admin, except: [:index, :show, :homepage, :rss_feed] + before_action :authenticate_user!, except: [:index, :show, :homepage, :rss_feed, :post_types] + before_action :verify_admin, except: [:index, :show, :homepage, :rss_feed, :post_types] before_action :set_category, except: [:index, :homepage, :new, :create] - before_action :verify_view_access, except: [:index, :homepage, :new, :create] + before_action :verify_view_access, except: [:index, :homepage, :new, :create, :post_types] def index @categories = Category.all.order(:sequence, :name) @@ -83,6 +83,13 @@ class CategoriesController < ApplicationController set_list_posts end + def post_types + @post_types = @category.post_types.where(is_top_level: true) + if @post_types.count == 1 + redirect_to new_category_post_path(post_type: @post_types.first, category: @category) + end + end + private def set_category @@ -116,7 +123,7 @@ class CategoriesController < ApplicationController end def update_last_visit(category) - return unless current_user.present? + return if current_user.blank? key = "#{RequestContext.community_id}/#{current_user.id}/#{category.id}/last_visit" RequestContext.redis.set key, DateTime.now.to_s diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 80813a195811daebb06b99c9be834b39b3f869cb..c860421acbe25663985e10f22d26e9239392a46e 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -5,16 +5,17 @@ class CommentsController < ApplicationController before_action :check_privilege, only: [:update, :destroy, :undelete] before_action :check_if_target_post_locked, only: [:create] before_action :check_if_parent_post_locked, only: [:update, :destroy] + def create @post = Post.find(params[:comment][:post_id]) if @post.comments_disabled && !current_user.is_moderator && !current_user.is_admin - render json: { status: 'failed', message: 'Comments have been disabled on this post.' }, status: 403 + render json: { status: 'failed', message: 'Comments have been disabled on this post.' }, status: :forbidden return end @comment = Comment.new comment_params.merge(user: current_user) - recent_comments = Comment.where(created_at: 24.hours.ago..Time.now, user: current_user).where \ + recent_comments = Comment.where(created_at: 24.hours.ago..Time.zone.now, user: current_user).where \ .not(post: Post.includes(:parent).where(parents_posts: { user_id: current_user.id })) \ .where.not(post: Post.where(user_id: current_user.id)).count max_comments_per_day = SiteSetting[current_user.privilege?('unrestricted') ? 'RL_Comments' : 'RL_NewUserComments'] @@ -33,7 +34,7 @@ class CommentsController < ApplicationController AuditLog.rate_limit_log(event_type: 'comment', related: @comment, user: current_user, comment: "limit: #{max_comments_per_day}\n\comment:\n#{@comment.attributes_print}") - render json: { status: 'failed', message: comment_limit_msg }, status: 403 + render json: { status: 'failed', message: comment_limit_msg }, status: :forbidden return end @@ -53,7 +54,8 @@ class CommentsController < ApplicationController render json: { status: 'success', comment: render_to_string(partial: 'comments/comment', locals: { comment: @comment }) } else - render json: { status: 'failed', message: @comment.errors.full_messages.join(', ') }, status: 500 + render json: { status: 'failed', message: @comment.errors.full_messages.join(', ') }, + status: :internal_server_error end end @@ -68,7 +70,8 @@ class CommentsController < ApplicationController comment: render_to_string(partial: 'comments/comment', locals: { comment: @comment }) } else render json: { status: 'failed', - message: "Comment failed to save (#{@comment.errors.full_messages.join(', ')})" }, status: 500 + message: "Comment failed to save (#{@comment.errors.full_messages.join(', ')})" }, + status: :internal_server_error end end @@ -80,7 +83,7 @@ class CommentsController < ApplicationController end render json: { status: 'success' } else - render json: { status: 'failed' }, status: 500 + render json: { status: 'failed' }, status: :internal_server_error end end @@ -92,7 +95,7 @@ class CommentsController < ApplicationController end render json: { status: 'success' } else - render json: { status: 'failed' }, status: 500 + render json: { status: 'failed' }, status: :internal_server_error end end @@ -127,17 +130,15 @@ class CommentsController < ApplicationController def check_privilege unless current_user.is_moderator || current_user.is_admin || current_user == @comment.user - render template: 'errors/forbidden', status: 403 + render template: 'errors/forbidden', status: :forbidden end end def comment_link(comment) - if comment.post.question? - question_url(comment.post, anchor: "comment-#{comment.id}") - elsif comment.post.article? - article_url(comment.post, anchor: "comment-#{comment.id}") + if comment.post.parent_id.present? + post_url(comment.post.parent_id, anchor: "comment-#{comment.id}") else - question_url(comment.post.parent, anchor: "comment-#{comment.id}") + post_url(comment.post, anchor: "comment-#{comment.id}") end end diff --git a/app/controllers/flags_controller.rb b/app/controllers/flags_controller.rb index dc562faac620bc7047a2b4595bf9e43dc63260e8..b07e521d1b490076e1c748a34239474f214fd719 100644 --- a/app/controllers/flags_controller.rb +++ b/app/controllers/flags_controller.rb @@ -9,7 +9,7 @@ class FlagsController < ApplicationController PostFlagType.find params[:flag_type] end - recent_flags = Flag.where(created_at: 24.hours.ago..Time.now, user: current_user).count + recent_flags = Flag.where(created_at: 24.hours.ago..Time.zone.now, user: current_user).count max_flags_per_day = SiteSetting[current_user.privilege?('unrestricted') ? 'RL_Flags' : 'RL_NewUserFlags'] if recent_flags >= max_flags_per_day @@ -20,15 +20,15 @@ class FlagsController < ApplicationController AuditLog.rate_limit_log(event_type: 'flag', related: Post.find(params[:post_id]), user: current_user, comment: "limit: #{max_flags_per_day}\n\ntype:#{type}\ncomment:\n#{params[:reason].to_i}") - render json: { status: 'failed', message: flag_limit_msg }, status: 403 + render json: { status: 'failed', message: flag_limit_msg }, status: :forbidden return end @flag = Flag.new(post_flag_type: type, reason: params[:reason], post_id: params[:post_id], user: current_user) if @flag.save - render json: { status: 'success' }, status: 201 + render json: { status: 'success' }, status: :created else - render json: { status: 'failed', message: 'Flag failed to save.' }, status: 500 + render json: { status: 'failed', message: 'Flag failed to save.' }, status: :internal_server_error end end @@ -57,7 +57,7 @@ class FlagsController < ApplicationController AbilityQueue.add(@flag.user, "Flag Handled ##{@flag.id}") render json: { status: 'success' } else - render json: { status: 'failed', message: 'Failed to save new status.' }, status: 500 + render json: { status: 'failed', message: 'Failed to save new status.' }, status: :internal_server_error end end diff --git a/app/controllers/licenses_controller.rb b/app/controllers/licenses_controller.rb index 353841dd5c17ad5a329caa7a1548b7e16b18d9a1..fa25284e67e7d655827a5a91dfad3c63cab8d460 100644 --- a/app/controllers/licenses_controller.rb +++ b/app/controllers/licenses_controller.rb @@ -21,7 +21,7 @@ class LicensesController < ApplicationController comment: "<<License #{@license.attributes_print}>>") redirect_to licenses_path else - render :new, status: 400 + render :new, status: :bad_request end end @@ -37,7 +37,7 @@ class LicensesController < ApplicationController comment: "from <<License #{before}>>\nto <<License #{@license.attributes_print}>>") redirect_to licenses_path else - render :edit, status: 400 + render :edit, status: :bad_request end end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index bfa32e76493254246aa7036639bb0b714974a71d..02477f56a6d9e46a32a5d6f8d9677349a3d03ec0 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -16,8 +16,8 @@ class NotificationsController < ApplicationController unless @notification.user == current_user respond_to do |format| - format.html { render template: 'errors/forbidden', status: 403 } - format.json { render json: nil, status: 403 } + format.html { render template: 'errors/forbidden', status: :forbidden } + format.json { render json: nil, status: :forbidden } end return end diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index ee0a0d1dec9f788166e7e3392550a22040bdf4f7..d2aea372eb459ee27e5aac7564427b511381e229 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -1,106 +1,319 @@ +# rubocop:disable Metrics/ClassLength class PostsController < ApplicationController - before_action :authenticate_user!, except: [:document, :share_q, :share_a, :help_center] - before_action :set_post, only: [:edit_help, :update_help, :toggle_comments, :feature, :lock, :unlock] - before_action :set_scoped_post, only: [:change_category] - before_action :check_permissions, only: [:edit_help, :update_help] - before_action :verify_moderator, only: [:new_help, :create_help, :toggle_comments] + before_action :authenticate_user!, except: [:document, :help_center, :show] + before_action :set_post, only: [:toggle_comments, :feature, :lock, :unlock] + before_action :set_scoped_post, only: [:change_category, :show, :edit, :update, :close, :reopen, :delete, :restore] + before_action :verify_moderator, only: [:toggle_comments] + before_action :edit_checks, only: [:edit, :update] + before_action :unless_locked, only: [:edit, :update, :close, :reopen, :delete, :restore] def new - @category = Category.find(params[:category_id]) - @post = Post.new(category: @category, post_type_id: params[:post_type_id]) - if @category.min_trust_level.present? && @category.min_trust_level > current_user.trust_level - flash[:danger] = "You don't have a high enough trust level to post in the #{@category.name} category." + @post_type = PostType.find(params[:post_type]) + @category = params[:category].present? ? Category.find(params[:category]) : nil + @parent = Post.where(id: params[:parent]).first + @post = Post.new(category: @category, post_type: @post_type, parent: @parent) + + if @post_type.has_parent? && @parent.nil? + flash[:danger] = helpers.i18ns('posts.type_requires_parent', type: @post_type.name) redirect_back fallback_location: root_path + return + end + + if @post_type.has_category? && @category.nil? && @parent.nil? + flash[:danger] = helpers.i18ns('posts.type_requires_category', type: @post_type.name) + redirect_back fallback_location: root_path + return + end + + if ['HelpDoc', 'PolicyDoc'].include?(@post_type.name) + check_permissions + # return # uncomment if you add more code after this end end def create - @category = Category.find(params[:category_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.post_markdown(:post, :body_markdown))) - - if @category.min_trust_level.present? && @category.min_trust_level > current_user.trust_level - @post.errors.add(:base, "You don't have a high enough trust level to post in the #{@category.name} category.") - render :new, status: 403 + @post_type = PostType.find(params[:post][:post_type_id]) + @parent = Post.where(id: params[:parent]).first + @category = if @post_type.has_category + if params[:post][:category_id].present? + Category.find(params[:post][:category_id]) + elsif @parent.present? + @parent.category + end + end || nil + @post = Post.new(post_params.merge(user: current_user, body: helpers.post_markdown(:post, :body_markdown), + category: @category, post_type: @post_type, parent: @parent)) + + if @post_type.has_parent? && @parent.nil? + flash[:danger] = helpers.i18ns('posts.type_requires_parent', type: @post_type.name) + redirect_back fallback_location: root_path + return + end + + if @post_type.has_category? && @category.nil? && @parent.nil? + flash[:danger] = helpers.i18ns('posts.type_requires_category', type: @post_type.name) + redirect_back fallback_location: root_path return end - recent_top_level_posts = Post.where(created_at: 24.hours.ago..Time.now, user: current_user) \ - .where(post_type_id: top_level_post_types).count + if @category.present? && @category.min_trust_level.present? && @category.min_trust_level > current_user.trust_level + @post.errors.add(:base, helpers.i18ns('posts.category_low_trust_level', category: @category.name)) + render :new, status: :forbidden + return + end - max_posts = SiteSetting[current_user.privilege?('unrestricted') ? 'RL_TopLevelPosts' : 'RL_NewUserTopLevelPosts'] - post_limit_msg = if current_user.privilege? 'unrestricted' - "You may only post #{max_posts} top-level posts per day." - else - "You may only post #{max_posts} top-level posts (questions, articles) per day. " \ - 'Once you have some well-received posts, that limit will increase.' - end + if ['HelpDoc', 'PolicyDoc'].include?(@post_type.name) && !check_permissions + return + end - if recent_top_level_posts >= max_posts - @post.errors.add :base, post_limit_msg - AuditLog.rate_limit_log(event_type: 'top_level_post', related: @category, user: current_user, + level_name = @post_type.is_top_level? ? 'TopLevel' : 'SecondLevel' + level_type_ids = @post_type.is_top_level? ? top_level_post_types : second_level_post_types + recent_level_posts = Post.where(created_at: 24.hours.ago..Time.zone.now, user: current_user) + .where(post_type_id: level_type_ids).count + setting_name = current_user.privilege?('unrestricted') ? "RL_#{level_name}Posts" : "RL_NewUser#{level_name}Posts" + max_posts = SiteSetting[setting_name] + limit_msg = if current_user.privilege?('unrestricted') + helpers.i18ns('rate_limit.posts', count: max_posts, level: level_name.underscore.humanize.downcase) + else + helpers.i18ns('rate_limit.new_user_posts', + count: max_posts, level: level_name.underscore.humanize.downcase) + end + + if recent_level_posts >= max_posts + @post.errors.add :base, limit_msg + AuditLog.rate_limit_log(event_type: "#{level_name.underscore}_post", related: @category, user: current_user, comment: "limit: #{max_posts}\n\npost:\n#{@post.attributes_print}") - render :new, status: 400 + render :new, status: :forbidden return end if @post.save redirect_to helpers.generic_show_link(@post) else - render :new, status: 400 + render :new, status: :bad_request end end - def new_help - @post = Post.new + def show + if @post.parent_id.present? + return redirect_to post_path(@post.parent_id) + end + + if @post.deleted? && !current_user&.has_post_privilege?('flag_curate', @post) + return not_found + end + + @children = if current_user&.privilege?('flag_curate') + Post.where(parent_id: @post.id) + else + Post.where(parent_id: @post.id).undeleted + .or(Post.where(parent_id: @post.id, user_id: current_user&.id)) + end.includes(:votes, :user, :comments, :license, :post_type) + .user_sort({ term: params[:sort], default: Arel.sql('deleted ASC, score DESC, RAND()') }, + score: Arel.sql('deleted ASC, score DESC, RAND()'), age: :created_at) + .paginate(page: params[:page], per_page: 20) end - def create_help - setting_regex = /\${(?<setting_name>[^}]+)}/ - params[:post][:body_markdown] = params[:post][:body_markdown].gsub(setting_regex) do |_match| - setting_name = $LAST_MATCH_INFO&.send(:[], :setting_name) - if setting_name.nil? - '' + def edit; end + + def update + before = { body: @post.body_markdown, title: @post.title, tags: @post.tags } + after_tags = if @post_type.has_category? + Tag.where(tag_set_id: @post.category.tag_set_id, name: params[:post][:tags_cache]) + end + body_rendered = helpers.post_markdown(:post, :body_markdown) + new_tags_cache = params[:post][:tags_cache]&.reject(&:empty?) + + if edit_post_params.to_h.all? { |k, v| @post.send(k) == v } + flash[:danger] = "No changes were saved because you didn't edit the post." + return redirect_to post_path(@post) + end + + if current_user.privilege?('edit_posts') || current_user.is_moderator || current_user == @post.user + if @post.update(edit_post_params.merge(body: body_rendered, + last_edited_at: DateTime.now, last_edited_by: current_user, + last_activity: DateTime.now, last_activity_by: current_user)) + 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: after_tags) + redirect_to post_path(@post) + else + render :edit, status: :bad_request + end + else + new_user = !current_user.privilege?('unrestricted') + rate_limit = SiteSetting["RL_#{new_user ? 'NewUser' : ''}SuggestedEdits"] + recent_edits = SuggestedEdit.where(user: current_user, active: true).where('created_at > ?', 24.hours.ago).count + if recent_edits >= rate_limit + key = new_user ? 'rate_limit.new_user_suggested_edits' : 'rate_limit.suggested_edits' + msg = helpers.i18ns key, count: rate_limit + @post.errors.add :base, msg + render :edit, status: :forbidden else - SiteSetting[setting_name] || '(No such setting)' + 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 + } + edit = SuggestedEdit.new(data) + if edit.save + message = "Edit suggested on your #{@post_type.name.underscore.humanize.downcase}" + if @post_type.has_parent + message += " on '#{@post.parent.title}'" + end + @post.user.create_notification message, suggested_edit_path(edit) + redirect_to post_path(@post) + else + @post.errors = edit.errors + render :edit, status: :bad_request + end end end - @post = Post.new(new_post_params.merge(body: helpers.post_markdown(:post, :body_markdown), - user: User.find(-1))) + end - if @post.policy_doc? && !current_user&.is_admin - @post.errors.add(:base, 'You must be an administrator to create a policy document.') - render :new_help, status: 403 + def close + unless check_your_privilege('flag_close', nil, false) + render json: { status: 'failed', message: helpers.ability_err_msg(:flag_close, 'close this post') }, + status: :forbidden return end - if @post.save - redirect_to policy_path(slug: @post.doc_slug) + if @post.closed + render json: { status: 'failed', message: 'Cannot close a closed post.' }, status: :bad_request + return + end + + reason = CloseReason.find_by id: params[:reason_id] + if reason.nil? + render json: { status: 'failed', message: 'Close reason not found.' }, status: :not_found + return + end + + if reason.requires_other_post + other = Post.find_by(id: params[:other_post]) + if other.nil? || !top_level_post_types.include?(other.post_type_id) + render json: { status: 'failed', message: 'Invalid input for other post.' }, status: :bad_request + return + end + + duplicate_of = Question.find(params[:other_post]) + else + duplicate_of = nil + end + + if @post.update(closed: true, closed_by: current_user, closed_at: DateTime.now, last_activity: DateTime.now, + last_activity_by: current_user, close_reason: reason, duplicate_post: duplicate_of) + PostHistory.question_closed(@post, current_user) + render json: { status: 'success' } else - render :new_help, status: 500 + render json: { status: 'failed', message: "Can't close this question right now. Try again later.", + errors: @post.errors.full_messages } end end - def edit_help; end + def reopen + unless check_your_privilege('flag_close', nil, false) + flash[:danger] = helpers.ability_err_msg(:flag_close, 'reopen this post') + redirect_to post_path(@post) + return + end - def update_help - setting_regex = /\${(?<setting_name>[^}]+)}/ - params[:post][:body_markdown] = params[:post][:body_markdown].gsub(setting_regex) do |_match| - setting_name = $LAST_MATCH_INFO&.send(:[], :setting_name) - if setting_name.nil? - '' - else - SiteSetting[setting_name] || '(No such setting)' + unless @post.closed + flash[:danger] = 'Cannot reopen an open post.' + redirect_to post_path(@post) + return + end + + if @post.update(closed: false, closed_by: current_user, closed_at: Time.zone.now, + last_activity: DateTime.now, last_activity_by: current_user, + close_reason: nil, duplicate_post: nil) + PostHistory.question_reopened(@post, current_user) + else + flash[:danger] = "Can't reopen this post right now. Try again later." + end + redirect_to post_path(@post) + end + + def delete + unless check_your_privilege('flag_curate', @post, false) + flash[:danger] = helpers.ability_err_msg(:flag_curate, 'delete this post') + redirect_to post_path(@post) + return + end + + if @post.children.any? { |a| a.score >= 0.5 } + flash[:danger] = 'This post cannot be deleted because it has responses.' + redirect_to post_path(@post) + return + end + + if @post.deleted + flash[:danger] = "Can't delete a deleted post." + redirect_to post_path(@post) + return + end + + if @post.update(deleted: true, deleted_at: DateTime.now, deleted_by: current_user, + last_activity: DateTime.now, last_activity_by: current_user) + PostHistory.post_deleted(@post, current_user) + if @post.children.any? + @post.children.update_all(deleted: true, deleted_at: DateTime.now, deleted_by_id: current_user.id, + last_activity: DateTime.now, last_activity_by_id: current_user.id) + histories = @post.children.map do |c| + { post_history_type: PostHistoryType.find_by(name: 'post_deleted'), user: current_user, post: c, + community: RequestContext.community } + end + PostHistory.create(histories) end + else + flash[:danger] = "Can't delete this post right now. Try again later." + end + + redirect_to post_path(@post) + end + + def restore + unless check_your_privilege('flag_curate', @post, false) + flash[:danger] = helpers.ability_err_msg(:flag_curate, 'restore this post') + redirect_to post_path(@post) + return + end + + unless @post.deleted + flash[:danger] = "Can't restore an undeleted post." + redirect_to post_path(@post) + return + end + + if @post.deleted_by.is_moderator && !current_user.is_moderator + flash[:danger] = 'You cannot restore this post deleted by a moderator.' + redirect_to post_path(@post) + return end - PostHistory.post_edited(@post, current_user, before: @post.body_markdown, after: params[:post][:body_markdown]) - if @post.update(help_post_params.merge(body: helpers.post_markdown(:post, :body_markdown), - last_activity: DateTime.now, last_activity_by: current_user)) - redirect_to policy_path(slug: @post.doc_slug) + + deleted_at = @post.deleted_at + if @post.update(deleted: false, deleted_at: nil, deleted_by: nil, + last_activity: DateTime.now, last_activity_by: current_user) + PostHistory.post_undeleted(@post, current_user) + restore_children = @post.children.where('deleted_at >= ?', deleted_at) + restore_children.update_all(deleted: true, deleted_at: DateTime.now, deleted_by_id: current_user.id, + last_activity: DateTime.now, last_activity_by_id: current_user.id) + histories = restore_children.map do |c| + { post_history_type: PostHistoryType.find_by(name: 'post_undeleted'), user: current_user, post: c, + community: RequestContext.community } + end + PostHistory.create(histories) else - render :edit_help, status: 500 + flash[:danger] = "Can't restore this post right now. Try again later." end + + redirect_to post_path(@post) end def document @@ -119,7 +332,7 @@ class PostsController < ApplicationController content_types = ActiveStorage::Variant::WEB_IMAGE_CONTENT_TYPES extensions = content_types.map { |ct| ct.gsub('image/', '') } unless helpers.valid_image?(params[:file]) - render json: { error: "Images must be one of #{extensions.join(', ')}" }, status: 400 + render json: { error: "Images must be one of #{extensions.join(', ')}" }, status: :bad_request return end @blob = ActiveStorage::Blob.create_after_upload!(io: params[:file], filename: params[:file].original_filename, @@ -127,14 +340,6 @@ class PostsController < ApplicationController render json: { link: uploaded_url(@blob.key) } end - def share_q - redirect_to question_path(id: params[:id]) - end - - def share_a - redirect_to question_path(id: params[:qid], anchor: "answer-#{params[:id]}") - end - def help_center @posts = Post.where(post_type_id: [PolicyDoc.post_type_id, HelpDoc.post_type_id]) .or(Post.unscoped.where(post_type_id: [PolicyDoc.post_type_id, HelpDoc.post_type_id], @@ -142,19 +347,19 @@ class PostsController < ApplicationController .where(Arel.sql("posts.help_category IS NULL OR posts.help_category != '$Disabled'")) .order(:help_ordering, :title) .group_by(&:post_type_id) - .transform_values { |posts| posts.group_by { |p| p.help_category.present? ? p.help_category : nil } } + .transform_values { |posts| posts.group_by { |p| p.help_category.presence } } end def change_category @target = Category.find params[:target_id] unless helpers.can_change_category(current_user, @target) - render json: { success: false, errors: ["You don't have permission to make that change."] }, status: 403 + render json: { success: false, errors: ["You don't have permission to make that change."] }, status: :forbidden return end unless @target.post_type_ids.include? @post.post_type_id render json: { success: false, errors: ["This post type is not allowed in the #{@target.name} category."] }, - status: 409 + status: :conflict return end @@ -172,15 +377,11 @@ class PostsController < ApplicationController end def toggle_comments - @post.comments_disabled = !@post.comments_disabled - @post.save + @post.update(comments_disabled: !@post.comments_disabled) if @post.comments_disabled && params[:delete_all_comments] - @post.comments.undeleted.map do |c| - c.deleted = true - c.save - end + @post.comments.update_all(deleted: true) end - render json: { success: true } + render json: { status: 'success', success: true } end def lock @@ -201,33 +402,38 @@ class PostsController < ApplicationController @post.update locked: true, locked_by: current_user, locked_at: DateTime.now, locked_until: end_date - render json: { success: true } + render json: { status: 'success', success: true } end def unlock - return not_found unless current_user.privilege? 'flag_curate' - return not_found unless @post.locked? - return not_found if @post.locked_until.nil? && !current_user.is_moderator + return not_found(errors: ['no_privilege']) unless current_user.privilege? 'flag_curate' + return not_found(errors: ['not_locked']) unless @post.locked? + if @post.locked_by.is_moderator && !current_user.is_moderator + return not_found(errors: ['locked_by_mod']) + end @post.update locked: false, locked_by: nil, locked_at: nil, locked_until: nil - render json: { success: true } + render json: { status: 'success', success: true } end def feature + return not_found(errors: ['no_privilege']) unless current_user.is_moderator + data = { label: @post.parent.nil? ? @post.title : @post.parent.title, link: helpers.generic_show_link(@post), post: @post, - active: true + active: true, + community: RequestContext.community } @link = PinnedLink.create data attr = @link.attributes_print AuditLog.moderator_audit(event_type: 'pinned_link_create', related: @link, user: current_user, - comment: "<<PinnedLink #{attr}>>\n(using moderator tools on post)") + comment: "<<PinnedLink #{attr}>>\n(using moderator tools on post)") flash[:success] = 'Post has been featured. Due to caching, it may take some time until the changes apply.' - render json: { success: true } + render json: { status: 'success', success: true } end def save_draft @@ -237,28 +443,32 @@ class PostsController < ApplicationController RequestContext.redis.set saved_at, DateTime.now.iso8601 RequestContext.redis.expire key, 86_400 * 7 RequestContext.redis.expire saved_at, 86_400 * 7 - render json: { success: true, key: key } + render json: { status: 'success', success: true, key: key } end def delete_draft key = "saved_post.#{current_user.id}.#{params[:path]}" saved_at = "saved_post_at.#{current_user.id}.#{params[:path]}" RequestContext.redis.del key, saved_at - render json: { success: true } + render json: { status: 'success', success: true } end private - def new_post_params - params.require(:post).permit(:post_type_id, :title, :doc_slug, :help_category, :body_markdown, :help_ordering) + def permitted + [:post_type_id, :category_id, :parent_id, :title, :body_markdown, :license_id, + :doc_slug, :help_category, :help_ordering] end - def help_post_params - params.require(:post).permit(:title, :help_category, :body_markdown, :help_ordering) + def post_params + p = params.require(:post).permit(*permitted, tags_cache: []) + p[:tags_cache] = p[:tags_cache]&.reject { |t| t.empty? } + p end - def post_params - p = params.require(:post).permit(:title, :body_markdown, :post_type_id, :license_id, tags_cache: []) + def edit_post_params + p = params.require(:post).permit(*(permitted - [:license_id, :post_type_id, :category_id, :parent_id]), + tags_cache: []) p[:tags_cache] = p[:tags_cache]&.reject { |t| t.empty? } p end @@ -280,4 +490,25 @@ class PostsController < ApplicationController not_found end end + + def edit_checks + @category = @post.category + @parent = @post.parent + @post_type = @post.post_type + + if @post_type.has_parent? && @parent.nil? + flash[:danger] = helpers.i18ns('posts.type_requires_parent', type: @post_type.name) + redirect_back fallback_location: root_path + end + + if !@post_type.is_public_editable && !(@post.user == current_user || current_user.is_moderator) + flash[:danger] = helpers.i18ns('posts.not_public_editable') + redirect_back fallback_location: root_path + end + end + + def unless_locked + check_if_locked(@post) + end end +# rubocop:enable Metrics/ClassLength diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index fd24ccf1c2d719cf99a995213aeddd5bd7a7ac4f..1b57bce8494fefe93112ac946b66ba7069e37e18 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -1,52 +1,6 @@ # Web controller. Provides actions that relate to questions - this is essentially the standard set of resources, plus a # couple for the extra question lists (such as listing by tag). class QuestionsController < ApplicationController - before_action :authenticate_user!, only: [:new, :new_meta, :create, :edit, :update, :destroy, :undelete, - :close, :reopen] - before_action :set_question, only: [:show, :edit, :update, :destroy, :undelete, :close, :reopen] - before_action :check_if_question_locked, only: [:edit, :update, :destroy, :undelete, :close, :reopen] - - def index - sort_params = { activity: :last_activity, age: :created_at, score: :score } - sort_param = sort_params[params[:sort]&.to_sym] || :last_activity - @questions = Question.list_includes.main.undeleted.order(sort_param => :desc) - .paginate(page: params[:page], per_page: 25) - end - - def meta - sort_params = { activity: :last_activity, age: :created_at, score: :score } - sort_param = sort_params[params[:sort]&.to_sym] || :last_activity - @questions = Question.list_includes.meta.undeleted.order(sort_param => :desc) - .paginate(page: params[:page], per_page: 25) - end - - def show - if @question.deleted? - check_your_privilege('flag_curate', @question) || return - end - - @answers = if current_user&.privilege?('flag_curate') - Answer.where(parent_id: @question.id) - else - Answer.where(parent_id: @question.id).undeleted - .or(Answer.where(parent_id: @question.id, user_id: current_user&.id)) - end.user_sort({ term: params[:sort], default: Arel.sql('deleted ASC, score DESC, RAND()') }, - score: Arel.sql('deleted ASC, score DESC, RAND()'), age: :created_at) - .paginate(page: params[:page], per_page: 20) - .includes(:votes, :user, :comments, :license) - - @close_reasons = CloseReason.active - end - - def tagged - @tag = Tag.find_by name: params[:tag], tag_set_id: params[:tag_set] - if @tag.nil? - not_found - return - end - @questions = @tag.posts.list_includes.undeleted.order('updated_at DESC').paginate(page: params[:page], per_page: 50) - end - def lottery ids = Rails.cache.fetch 'lottery_questions', expires_in: 24.hours do # noinspection RailsParamDefResolve @@ -57,129 +11,6 @@ class QuestionsController < ApplicationController @questions = Question.list_includes.where(id: ids).paginate(page: params[:page], per_page: 25) end - def edit; end - - def update - can_post_in_category = @question.category.present? && - (@question.category.min_trust_level || -1) <= current_user&.trust_level - unless current_user&.has_post_privilege?('edit_posts', @question) && can_post_in_category - return update_as_suggested_edit - end - - tags_cache = params[:question][:tags_cache]&.reject { |e| e.to_s.empty? } - after_tags = Tag.where(tag_set_id: @question.category.tag_set_id, name: tags_cache) - - if @question.tags == after_tags && @question.body_markdown == params[:question][:body_markdown] && - @question.title == params[:question][:title] - flash[:danger] = "No changes were saved because you didn't edit the post." - return redirect_to question_path(@question) - end - - body_rendered = helpers.post_markdown(:question, :body_markdown) - before = { body: @question.body_markdown, title: @question.title, tags: @question.tags } - if @question.update(question_params.merge(tags_cache: tags_cache, body: body_rendered, - last_activity: DateTime.now, last_activity_by: current_user, - last_edited_at: DateTime.now, last_edited_by: current_user)) - PostHistory.post_edited(@question, current_user, before: before[:body], - after: params[:question][:body_markdown], comment: params[:edit_comment], - before_title: before[:title], after_title: params[:question][:title], - before_tags: before[:tags], after_tags: after_tags) - redirect_to share_question_path(@question) - else - render :edit - end - end - - def update_as_suggested_edit - return if check_edits_limit! @question - - body_rendered = helpers.post_markdown(:question, :body_markdown) - new_tags_cache = params[:question][:tags_cache]&.reject(&:empty?) - - body_markdown = if params[:question][:body_markdown] != @question.body_markdown - params[:question][:body_markdown] - end - - if @question.tags_cache == new_tags_cache && @question.body_markdown == params[:question][:body_markdown] && - @question.title == params[:question][:title] - flash[:danger] = "No changes were saved because you didn't edit the post." - return redirect_to question_path(@question) - end - - updates = { - post: @question, - user: current_user, - community: @question.community, - body: body_rendered, - title: params[:question][:title] == @question.title ? nil : params[:question][:title], - tags_cache: new_tags_cache == @question.tags_cache ? @question.tags_cache : new_tags_cache, - body_markdown: body_markdown, - comment: params[:edit_comment], - active: true, accepted: false, - decided_at: nil, decided_by: nil, - rejected_comment: nil - } - - @edit = SuggestedEdit.new(updates) - if @edit.save - @question.user.create_notification("Edit suggested on your post #{@question.title.truncate(50)}", - question_url(@question)) - redirect_to share_question_path(@question) - else - @question.errors = @edit.errors - render :edit - end - end - - def destroy - unless check_your_privilege('flag_curate', @question, false) - flash[:danger] = helpers.ability_err_msg(:flag_curate, 'delete this question') - redirect_to(question_path(@question)) && return - end - - if @question.answer_count.positive? && @question.answers.any? { |a| a.score >= 0.5 } - flash[:danger] = 'This question cannot be deleted because it has answers.' - redirect_to(question_path(@question)) && return - end - if @question.deleted - flash[:danger] = "Can't delete a deleted question." - redirect_to(question_path(@question)) && return - end - - if @question.update(deleted: true, deleted_at: DateTime.now, deleted_by: current_user, - last_activity: DateTime.now, last_activity_by: current_user) - PostHistory.post_deleted(@question, current_user) - else - flash[:danger] = "Can't delete this question right now. Try again later." - end - redirect_to url_for(controller: :questions, action: :show, id: @question.id) - end - - def undelete - unless check_your_privilege('flag_curate', @question, false) - flash[:danger] = helpers.ability_err_msg(:flag_curate, 'undelete this question') - redirect_to(question_path(@question)) && return - end - - unless @question.deleted - flash[:danger] = "Can't undelete an undeleted question." - redirect_to(question_path(@question)) && return - end - - if @question.deleted_by.is_moderator && !current_user.is_moderator - flash[:danger] = 'You cannot undelete this post deleted by a moderator.' - redirect_to(question_path(@question)) && return - end - - if @question.update(deleted: false, deleted_at: nil, deleted_by: nil, - last_activity: DateTime.now, last_activity_by: current_user) - PostHistory.post_undeleted(@question, current_user) - else - flash[:danger] = "Can't undelete this question right now. Try again later." - end - redirect_to url_for(controller: :questions, action: :show, id: @question.id) - end - def feed @questions = Rails.cache.fetch('questions_rss', expires_in: 5.minutes) do Question.all.order(created_at: :desc).limit(25) @@ -188,89 +19,4 @@ class QuestionsController < ApplicationController format.rss { render layout: false } end end - - def close - unless check_your_privilege('flag_close', nil, false) - render(json: { status: 'failed', message: helpers.ability_err_msg(:flag_close, 'close this question') }, - status: 403) - return - end - - if @question.closed - render(json: { status: 'failed', message: 'Cannot close a closed question.' }, status: 400) - return - end - - reason = CloseReason.find_by id: params[:reason_id] - if reason.nil? - render(json: { status: 'failed', message: 'Close reason not found.' }, status: 404) - return - end - - if reason.requires_other_post - unless Question.exists? params[:other_post] - render(json: { status: 'failed', message: 'Invalid input for other post.' }, status: 400) - return - end - - duplicate_of = Question.find(params[:other_post]) - else - duplicate_of = nil - end - - if @question.update(closed: true, closed_by: current_user, closed_at: DateTime.now, last_activity: DateTime.now, - last_activity_by: current_user, close_reason: reason, duplicate_post: duplicate_of) - PostHistory.question_closed(@question, current_user) - render json: { status: 'success' } - else - render json: { status: 'failed', message: "Can't close this question right now. Try again later.", - errors: @question.errors.full_messages } - end - end - - def reopen - unless check_your_privilege('flag_close', nil, false) - flash[:danger] = helpers.ability_err_msg(:flag_close, 'reopen this question') - redirect_to(question_path(@question)) && return - end - - unless @question.closed - flash[:danger] = 'Cannot reopen an open question.' - redirect_to(question_path(@question)) && return - end - - if @question.update(closed: false, closed_by: current_user, closed_at: Time.now, - last_activity: DateTime.now, last_activity_by: current_user, - close_reason: nil, duplicate_post: nil) - PostHistory.question_reopened(@question, current_user) - else - flash[:danger] = "Can't reopen this question right now. Try again later." - end - redirect_to question_path(@question) - end - - private - - def question_params - params.require(:question).permit(:body_markdown, :title, :tags_cache) - end - - def set_question - @question = Question.find params[:id] - rescue - if current_user&.privilege?('flag_curate') - @question ||= Question.unscoped.find params[:id] - end - if @question.nil? - not_found - return - end - unless @question.post_type_id == Question.post_type_id - not_found - end - end - - def check_if_question_locked - check_if_locked(@question) - end end diff --git a/app/controllers/site_settings_controller.rb b/app/controllers/site_settings_controller.rb index 47c2e8187a1ee33aac9776fc8c7a7ba1c4c7df90..b24331ade46b26ca5ea87a12505425458fc6493c 100644 --- a/app/controllers/site_settings_controller.rb +++ b/app/controllers/site_settings_controller.rb @@ -27,7 +27,7 @@ class SiteSettingsController < ApplicationController end def update - if !params[:community_id].present? && !current_user.is_global_admin + if params[:community_id].blank? && !current_user.is_global_admin not_found return end diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb index 37db8c4bffa978e8094cd1aa9ba28c6c61b1c6a4..5e867048a2511f3d1b0ace5b5bb3346897fd4dfa 100644 --- a/app/controllers/subscriptions_controller.rb +++ b/app/controllers/subscriptions_controller.rb @@ -12,9 +12,9 @@ class SubscriptionsController < ApplicationController @subscription = Subscription.new sub_params.merge(user: current_user) if @subscription.save flash[:success] = 'Your subscription was saved successfully.' - redirect_to params[:return_to].present? ? params[:return_to] : root_path + redirect_to params[:return_to].presence || root_path else - render :error, status: 500 + render :error, status: :internal_server_error end end @@ -28,10 +28,11 @@ class SubscriptionsController < ApplicationController if @subscription.update(enabled: params[:enabled] || false) render json: { status: 'success', subscription: @subscription } else - render json: { status: 'failed' }, status: 500 + render json: { status: 'failed' }, status: :internal_server_error end else - render json: { status: 'failed', message: 'You do not have permission to update this subscription.' }, status: 403 + render json: { status: 'failed', message: 'You do not have permission to update this subscription.' }, + status: :forbidden end end @@ -41,10 +42,11 @@ class SubscriptionsController < ApplicationController if @subscription.destroy render json: { status: 'success' } else - render json: { status: 'failed' }, status: 500 + render json: { status: 'failed' }, status: :internal_server_error end else - render json: { status: 'failed', message: 'You do not have permission to remove this subscription.' }, status: 403 + render json: { status: 'failed', message: 'You do not have permission to remove this subscription.' }, + status: :forbidden end end diff --git a/app/controllers/suggested_edit_controller.rb b/app/controllers/suggested_edit_controller.rb index 4ce929743580fdd4df9786f91441cc90f8df07bc..4e169347537d6a7f855d6fd5514226438a84452d 100644 --- a/app/controllers/suggested_edit_controller.rb +++ b/app/controllers/suggested_edit_controller.rb @@ -7,22 +7,20 @@ class SuggestedEditController < ApplicationController def approve unless @edit.active? - render json: { status: 'error', message: 'This edit has already been reviewed.' }, status: 409 + render json: { status: 'error', message: 'This edit has already been reviewed.' }, status: :conflict return end @post = @edit.post unless check_your_privilege('edit_posts', @post, false) render(json: { status: 'error', message: helpers.ability_err_msg(:edit_posts, 'review suggested edits') }, - status: 400) + status: :bad_request) return end - opts = { before: @post.body_markdown, after: @edit.body_markdown, comment: params[:edit_comment] } - if @post.question? || @post.article? - opts.merge(before_title: @post.title, after_title: @edit.title, before_tags: @post.tags, after_tags: @edit.tags) - end + opts = { before: @post.body_markdown, after: @edit.body_markdown, comment: params[:edit_comment], + before_title: @post.title, after_title: @edit.title, before_tags: @post.tags, after_tags: @edit.tags } before = { before_body: @post.body, before_body_markdown: @post.body_markdown, before_tags_cache: @post.tags_cache, before_tags: @post.tags.to_a, before_title: @post.title } @@ -33,26 +31,15 @@ class SuggestedEditController < ApplicationController PostHistory.post_edited(@post, @edit.user, **opts) flash[:success] = 'Edit approved successfully.' AbilityQueue.add(@edit.user, "Suggested Edit Approved ##{@edit.id}") - if @post.question? - render(json: { status: 'success', redirect_url: url_for(controller: :posts, action: :share_q, id: @post.id) }) - - elsif @post.answer? - render(json: { status: 'success', redirect_url: url_for(controller: :posts, action: :share_a, - qid: @post.parent.id, id: @post.id) }) - elsif @post.article? - render(json: { status: 'success', redirect_url: url_for(controller: :articles, action: :share, id: @post.id) }) - else - render(json: { status: 'error', message: 'Could not approve suggested edit.' }, status: 400) - end + render json: { status: 'success', redirect_url: post_path(@post) } else - render json: { status: 'error', - message: @post.errors.full_messages.join(', ') }, status: 400 + render json: { status: 'error', message: @post.errors.full_messages.join(', ') }, status: :bad_request end end def reject unless @edit.active? - render json: { status: 'error', message: 'This edit has already been reviewed.' }, status: 409 + render json: { status: 'error', message: 'This edit has already been reviewed.' }, status: :conflict return end @@ -60,7 +47,7 @@ class SuggestedEditController < ApplicationController unless check_your_privilege('edit_posts', @post, false) render(json: { status: 'error', redirect_url: helpers.ability_err_msg(:edit_posts, 'review suggested edits') }, - status: 400) + status: :bad_request) return end @@ -71,18 +58,10 @@ class SuggestedEditController < ApplicationController decided_by: current_user, updated_at: now) flash[:success] = 'Edit rejected successfully.' AbilityQueue.add(@edit.user, "Suggested Edit Rejected ##{@edit.id}") - if @post.question? - render(json: { status: 'success', redirect_url: url_for(controller: :posts, action: :share_q, - id: @post.id) }) - elsif @post.answer? - render(json: { status: 'success', redirect_url: url_for(controller: :posts, action: :share_a, - qid: @post.parent.id, id: @post.id) }) - elsif @post.article? - render(json: { status: 'success', redirect_url: url_for(controller: :articles, action: :share, - id: @post.id) }) - end + render json: { status: 'success', redirect_url: helpers.generic_share_link(@post) } else - render(json: { status: 'error', redirect_url: 'Cannot reject this suggested edit... Strange.' }, status: 400) + render json: { status: 'error', redirect_url: 'Cannot reject this suggested edit... Strange.' }, + status: :bad_request end end @@ -93,26 +72,15 @@ class SuggestedEditController < ApplicationController end def applied_details - if @post.question? || @post.article? - { - title: @edit.title, - tags_cache: @edit.tags_cache&.reject(&:empty?), - body: @edit.body, - body_markdown: @edit.body_markdown, - last_activity: DateTime.now, - last_activity_by: @edit.user, - last_edited_at: DateTime.now, - last_edited_by_id: @edit.user - }.compact - elsif @post.answer? - { - body: @edit.body, - body_markdown: @edit.body_markdown, - last_activity: DateTime.now, - last_activity_by: @edit.user, - last_edited_at: DateTime.now, - last_edited_by_id: @edit.user - }.compact - end + { + title: @edit.title, + tags_cache: @edit.tags_cache&.reject(&:empty?), + body: @edit.body, + body_markdown: @edit.body_markdown, + last_activity: DateTime.now, + last_activity_by: @edit.user, + last_edited_at: DateTime.now, + last_edited_by_id: @edit.user + }.compact end end diff --git a/app/controllers/suspicious_votes_controller.rb b/app/controllers/suspicious_votes_controller.rb index e4fa8db4eaf60b3ffef6c2ad150567b09065bff0..7092843a81e11a35706f26dcf94afd9d89f62e01 100644 --- a/app/controllers/suspicious_votes_controller.rb +++ b/app/controllers/suspicious_votes_controller.rb @@ -15,12 +15,12 @@ class SuspiciousVotesController < ApplicationController def investigated @sv = SuspiciousVote.find params[:id] @sv.was_investigated = true - @sv.investigated_at = Time.now + @sv.investigated_at = Time.zone.now @sv.investigated_by = current_user.id if @sv.save render json: { status: 'success' } else - render json: { status: 'failed' }, status: 500 + render json: { status: 'failed' }, status: :internal_server_error end end end diff --git a/app/controllers/tag_sets_controller.rb b/app/controllers/tag_sets_controller.rb index cfe1cbaa7a6c534d018018e96f8934a9a691ab63..dc23904ec2869814b2505628038f71e7fc96faed 100644 --- a/app/controllers/tag_sets_controller.rb +++ b/app/controllers/tag_sets_controller.rb @@ -29,7 +29,7 @@ class TagSetsController < ApplicationController AuditLog.admin_audit(event_type: 'tag_set_update', related: @tag_set, user: current_user, comment: "from <<TagSet #{before}>>\nto <<TagSet #{@tag_set.attributes_print}>>") else - render json: { tag_set: @tag_set, status: 'failed' }, status: 500 + render json: { tag_set: @tag_set, status: 'failed' }, status: :internal_server_error end end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 1d4cb52e4bc460bf95fc594e5bd90dee0fbcbeca..7cb4674ecd5bdd2538d48fbc6f95697b4d86face 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -66,7 +66,7 @@ class TagsController < ApplicationController if @tag.update(tag_params.merge(wiki: wiki_md.present? ? helpers.render_markdown(wiki_md) : nil)) redirect_to tag_path(id: @category.id, tag_id: @tag.id) else - render :edit, status: 400 + render :edit, status: :bad_request end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 95bdd30af9487d2c2616702ed2abcea0478c3890..f494965c3700e45e5bfb2f0a533fb8ef5355e847 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,4 +1,5 @@ require 'net/http' + # rubocop:disable Metrics/ClassLength class UsersController < ApplicationController include Devise::Controllers::Rememberable @@ -128,12 +129,14 @@ class UsersController < ApplicationController def destroy if @user.votes.count > 100 - render(json: { status: 'failed', message: 'Users with more than 100 votes cannot be destroyed.' }, status: 422) + render json: { status: 'failed', message: 'Users with more than 100 votes cannot be destroyed.' }, + status: :unprocessable_entity return end if @user.is_admin || @user.is_moderator - render(json: { status: 'failed', message: 'Admins and moderators cannot be destroyed.' }, status: 422) + render json: { status: 'failed', message: 'Admins and moderators cannot be destroyed.' }, + status: :unprocessable_entity return end @@ -149,13 +152,14 @@ class UsersController < ApplicationController else render json: { status: 'failed', message: 'Call to <code>@user.destroy!</code> failed; ask a DBA or dev to destroy.' }, - status: 500 + status: :internal_server_error end end def soft_delete if @user.is_admin || @user.is_moderator - render(json: { status: 'failed', message: 'Admins and moderators cannot be deleted.' }, status: 422) + render json: { status: 'failed', message: 'Admins and moderators cannot be deleted.' }, + status: :unprocessable_entity return end @@ -178,7 +182,7 @@ class UsersController < ApplicationController before = @user.attributes_print unless @user.destroy - render(json: { status: 'failed', message: "Failed to destroy UID #{@user.id}" }, status: 500) + render(json: { status: 'failed', message: "Failed to destroy UID #{@user.id}" }, status: :internal_server_error) return end AuditLog.moderator_audit(event_type: 'user_delete', user: current_user, comment: "<<User #{before}>>") @@ -230,7 +234,7 @@ class UsersController < ApplicationController permission_map = { mod: :is_admin, admin: :is_global_admin, mod_global: :is_global_admin, admin_global: :is_global_admin, staff: :staff } unless role_map.keys.include?(params[:role].underscore.to_sym) - render json: { status: 'error', message: "Role not found: #{params[:role]}" }, status: 400 + render json: { status: 'error', message: "Role not found: #{params[:role]}" }, status: :bad_request end key = params[:role].underscore.to_sym diff --git a/app/controllers/votes_controller.rb b/app/controllers/votes_controller.rb index 0fb93435d3f84f62bff9889dafb082634088e83a..cad1790c7bc9684b9c2b4dbe4dd7fc6909b3403e 100644 --- a/app/controllers/votes_controller.rb +++ b/app/controllers/votes_controller.rb @@ -7,10 +7,10 @@ class VotesController < ApplicationController post = Post.find(params[:post_id]) if post.user == current_user && !SiteSetting['AllowSelfVotes'] - render(json: { status: 'failed', message: 'You may not vote on your own posts.' }, status: 403) && return + render(json: { status: 'failed', message: 'You may not vote on your own posts.' }, status: :forbidden) && return end - recent_votes = Vote.where(created_at: 24.hours.ago..Time.now, user: current_user) \ + recent_votes = Vote.where(created_at: 24.hours.ago..Time.zone.now, user: current_user) \ .where.not(post: Post.includes(:parent).where(parents_posts: { user_id: current_user.id })).count max_votes_per_day = SiteSetting[current_user.privilege?('unrestricted') ? 'RL_Votes' : 'RL_NewUserVotes'] @@ -22,7 +22,7 @@ class VotesController < ApplicationController AuditLog.rate_limit_log(event_type: 'vote', related: post, user: current_user, comment: "limit: #{max_votes_per_day}\n\nvote:\n#{params[:vote_type].to_i}") - render json: { status: 'failed', message: vote_limit_msg }, status: 403 + render json: { status: 'failed', message: vote_limit_msg }, status: :forbidden return end @@ -30,7 +30,7 @@ class VotesController < ApplicationController vote = post.votes.create(user: current_user, vote_type: params[:vote_type].to_i, recv_user: post.user) if vote.errors.any? - render json: { status: 'failed', message: vote.errors.full_messages.join('. ') }, status: 403 + render json: { status: 'failed', message: vote.errors.full_messages.join('. ') }, status: :forbidden return end @@ -48,14 +48,15 @@ class VotesController < ApplicationController post = vote.post if vote.user != current_user - render(json: { status: 'failed', message: 'You are not authorized to remove this vote.' }, status: 403) && return + render json: { status: 'failed', message: 'You are not authorized to remove this vote.' }, status: :forbidden + return end if vote.destroy AbilityQueue.add(post.user, "Vote Change on ##{post.id}") render json: { status: 'OK', upvotes: post.upvote_count, downvotes: post.downvote_count } else - render json: { status: 'failed', message: vote.errors.full_messages.join('. ') }, status: 403 + render json: { status: 'failed', message: vote.errors.full_messages.join('. ') }, status: :forbidden end end @@ -63,7 +64,7 @@ class VotesController < ApplicationController def auth_for_voting unless user_signed_in? - render json: { status: 'failed', message: 'You must be logged in to vote.' }, status: 403 + render json: { status: 'failed', message: 'You must be logged in to vote.' }, status: :forbidden end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 613247e784a93476b60c47d22a8bb0469646bd87..d62eceb6397dc2ba1d564c41ef51663fa1f7c9bc 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -75,39 +75,31 @@ module ApplicationHelper 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) + if second_level_post_types.include?(post.post_type_id) + post_url(post, anchor: "answer-#{post.id}") else - '#' + post_url(post) 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 + edit_post_url(post) 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) + if top_level_post_types.include? post.post_type_id + post_url(post) + elsif second_level_post_types.include?(post.post_type_id) + post_url(post.parent, anchor: "answer-#{post.id}") else - '#' + case post.post_type_id + when HelpDoc.post_type_id + help_path(post.doc_slug) + when PolicyDoc.post_type_id + policy_path(post.doc_slug) + else + '#' + end end end @@ -132,4 +124,12 @@ module ApplicationHelper false end end + + def i18ns(key, **subs) + s = I18n.t key + subs.each do |f, r| + s = s.gsub ":#{f}", r.to_s + end + s + end end diff --git a/app/helpers/categories_helper.rb b/app/helpers/categories_helper.rb index cfb31edf2eab4ec8428d8149fc71958cc64e9572..4ce1d55e6a29ee3a901aabd31b84fbb76389e386 100644 --- a/app/helpers/categories_helper.rb +++ b/app/helpers/categories_helper.rb @@ -6,14 +6,14 @@ module CategoriesHelper end def expandable? - (defined?(@category) && !current_page?(new_category_path)) || + (defined?(@category) && !@category.nil? && !current_page?(new_category_path)) || (defined?(@post) && !@post.category.nil?) || (defined?(@question) && !@question.category.nil?) || (defined?(@article) && !@article.category.nil?) end def current_category - @current_category ||= if defined? @category + @current_category ||= if defined?(@category) && !@category.nil? @category elsif defined?(@post) && !@post.category.nil? @post.category diff --git a/app/helpers/posts_helper.rb b/app/helpers/posts_helper.rb index c4cfdc80edab611e74b1fdeb745b79bfe7eb0b1d..23c8f7d06fb38a38b2d2af95209b09146df91fde 100644 --- a/app/helpers/posts_helper.rb +++ b/app/helpers/posts_helper.rb @@ -1,10 +1,6 @@ module PostsHelper def post_markdown(scope, field_name) - if params['__html'].present? - params['__html'] - else - render_markdown(params[scope][field_name]) - end + params['__html'].presence || render_markdown(params[scope][field_name]) end class PostScrubber < Rails::Html::PermitScrubber diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index d1b6d9e526260cd6b510bdc195104e62ffa6e8bc..5e66a022f919d9772bc81af24495186d067abcee 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -26,33 +26,33 @@ module SearchHelper next unless value.match?(valid_value[:numeric]) operator, val = numeric_value_sql value - ["score #{operator.present? ? operator : '='} ?", val.to_f] + ["score #{operator.presence || '='} ?", val.to_f] when 'created' next unless value.match?(valid_value[:date]) operator, val, timeframe = date_value_sql value - ["created_at #{operator.present? ? operator : '='} DATE_SUB(CURRENT_TIMESTAMP, INTERVAL ? #{timeframe})", + ["created_at #{operator.presence || '='} DATE_SUB(CURRENT_TIMESTAMP, INTERVAL ? #{timeframe})", val.to_i] when 'user' next unless value.match?(valid_value[:numeric]) operator, val = numeric_value_sql value - ["user_id #{operator.present? ? operator : '='} ?", val.to_i] + ["user_id #{operator.presence || '='} ?", val.to_i] when 'upvotes' next unless value.match?(valid_value[:numeric]) operator, val = numeric_value_sql value - ["upvotes #{operator.present? ? operator : '='} ?", val.to_i] + ["upvotes #{operator.presence || '='} ?", val.to_i] when 'downvotes' next unless value.match?(valid_value[:numeric]) operator, val = numeric_value_sql value - ["downvotes #{operator.present? ? operator : '='} ?", val.to_i] + ["downvotes #{operator.presence || '='} ?", val.to_i] when 'votes' next unless value.match?(valid_value[:numeric]) operator, val = numeric_value_sql value - ["(upvotes - downvotes) #{operator.present? ? operator : '='}", val.to_i] + ["(upvotes - downvotes) #{operator.presence || '='}", val.to_i] end end.compact diff --git a/app/models/application_record.rb b/app/models/application_record.rb index fad87a7012bb6a7dc89ca1b43fb383bc2c751c8d..223980ca78ed9546cde975922dc8a40329045986 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -94,7 +94,7 @@ module UserSortable default = term_opts[:default] || :created_at requested = term_opts[:term] direction = term_opts[:direction] || :desc - if requested.nil? || !field_mappings.include?(requested.to_sym) + if requested.nil? || field_mappings.exclude?(requested.to_sym) $active_search_param = default default.is_a?(Symbol) ? order(default => direction) : order(default) else diff --git a/app/models/audit_log.rb b/app/models/audit_log.rb index 2d32dde131146c5199a3a35d476feabc56b3a01f..04bc34f34a0e8254fc8a13ec6ef9ad1f20596a06 100644 --- a/app/models/audit_log.rb +++ b/app/models/audit_log.rb @@ -1,8 +1,8 @@ class AuditLog < ApplicationRecord include CommunityRelated - belongs_to :related, polymorphic: true, required: false - belongs_to :user, required: false + belongs_to :related, polymorphic: true, optional: true + belongs_to :user, optional: true class << self [:admin_audit, :moderator_audit, :action_audit, :user_annotation, :user_history, :block_log, diff --git a/app/models/community_user.rb b/app/models/community_user.rb index 61d3a72617906531988f4f9389ebfbcf8317eb8f..897672e95f4752708767345416f3ec2c5c07f551 100644 --- a/app/models/community_user.rb +++ b/app/models/community_user.rb @@ -96,6 +96,7 @@ class CommunityUser < ApplicationRecord # If not sandbox mode, create new privilege entry grant_privilege(internal_id) unless sandbox + recalc_trust_level unless sandbox true end @@ -112,4 +113,24 @@ class CommunityUser < ApplicationRecord def prevent_ulysses_case recalc_privileges end + + def trust_level + attributes['trust_level'] || recalc_trust_level + end + + def recalc_trust_level + trust = if user.staff? + 5 + elsif is_moderator || user.is_global_moderator || is_admin || user.is_global_admin + 4 + elsif privilege?('flag_close') || privilege?('edit_posts') + 3 + elsif privilege?('unrestricted') + 2 + else + 1 + end + update(trust_level: trust) + trust + end end diff --git a/app/models/concerns/maybe_community_related.rb b/app/models/concerns/maybe_community_related.rb index adf7d8fd3c7e0cafe5f767798cb32dd0dcef4ffe..065687269f276155b3d76f7dd8c78cdbf93ecd21 100644 --- a/app/models/concerns/maybe_community_related.rb +++ b/app/models/concerns/maybe_community_related.rb @@ -2,7 +2,7 @@ module MaybeCommunityRelated extend ActiveSupport::Concern included do - belongs_to :community, required: false + belongs_to :community, optional: true default_scope { where(community_id: RequestContext.community_id).or(where(community_id: nil)) } end end diff --git a/app/models/error_log.rb b/app/models/error_log.rb index 0e666979a01cda0356a110806dfea472323651cb..17980399038f1697450d31e688e1dd26afc5aefb 100644 --- a/app/models/error_log.rb +++ b/app/models/error_log.rb @@ -1,4 +1,4 @@ class ErrorLog < ApplicationRecord - belongs_to :community, required: false - belongs_to :user, required: false + belongs_to :community, optional: true + belongs_to :user, optional: true end diff --git a/app/models/flag.rb b/app/models/flag.rb index 0367f7aefad40348f9fcecd5e9ab5ec52399004d..eac0d2554a31eace707610a289fba2756d53d2f2 100644 --- a/app/models/flag.rb +++ b/app/models/flag.rb @@ -2,7 +2,7 @@ class Flag < ApplicationRecord include PostRelated belongs_to :user - belongs_to :handled_by, class_name: 'User', required: false + belongs_to :handled_by, class_name: 'User', optional: true belongs_to :post_flag_type scope :handled, -> { where.not(status: nil) } diff --git a/app/models/notification.rb b/app/models/notification.rb index 0898eb785b2493f494d722061cdffdefb70fdcaa..387b94ed34b1037bc44a3ce8a32e5a57e8b4674e 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -2,7 +2,5 @@ class Notification < ApplicationRecord include CommunityRelated belongs_to :user - def community_name - community.name - end + delegate :name, to: :community, prefix: true end diff --git a/app/models/post.rb b/app/models/post.rb index e36f2f5b7eef989ccee3a630a456ef81e5031254..98e9edbbcc9d84c676b775f0f93b8407d1cc6df7 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -3,14 +3,16 @@ class Post < ApplicationRecord belongs_to :user belongs_to :post_type - belongs_to :parent, class_name: 'Post', required: false - belongs_to :closed_by, class_name: 'User', required: false - belongs_to :deleted_by, class_name: 'User', required: false - belongs_to :last_activity_by, class_name: 'User', required: false - belongs_to :locked_by, class_name: 'User', required: false - belongs_to :last_edited_by, class_name: 'User', required: false - belongs_to :category, required: false - belongs_to :license, required: false + belongs_to :parent, class_name: 'Post', optional: true + belongs_to :closed_by, class_name: 'User', optional: true + belongs_to :deleted_by, class_name: 'User', optional: true + belongs_to :last_activity_by, class_name: 'User', optional: true + belongs_to :locked_by, class_name: 'User', optional: true + belongs_to :last_edited_by, class_name: 'User', optional: true + belongs_to :category, optional: true + belongs_to :license, optional: true + belongs_to :close_reason, optional: true + belongs_to :duplicate_post, class_name: 'Question', optional: true has_and_belongs_to_many :tags, dependent: :destroy has_many :votes, dependent: :destroy has_many :comments, dependent: :destroy @@ -41,14 +43,14 @@ class Post < ApplicationRecord scope :qa_only, -> { where(post_type_id: [Question.post_type_id, Answer.post_type_id, Article.post_type_id]) } scope :list_includes, -> { includes(:user, :tags, user: :avatar_attachment) } + before_validation :update_tag_associations, if: -> { question? || article? } + after_create :create_initial_revision + after_create :add_license_if_nil after_save :check_attribution_notice after_save :modify_author_reputation after_save :copy_last_activity_to_parent after_save :break_description_cache 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 after_save :recalc_score def self.search(term) @@ -63,7 +65,7 @@ class Post < ApplicationRecord end end - PostType.all.each do |pt| + PostType.all.find_each do |pt| define_method "#{pt.name.underscore}?" do post_type_id == pt.id end diff --git a/app/models/question.rb b/app/models/question.rb index b0a347e7e0a0e11e29982e6d6f56a1ecb1cec626..85d63310c74ff41449674029f02bad3f8df25607 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -4,9 +4,6 @@ class Question < Post scope :meta, -> { joins(:category).where(categories: { name: 'Meta' }) } scope :main, -> { joins(:category).where(categories: { name: 'Main' }) } - belongs_to :close_reason, optional: true - belongs_to :duplicate_post, class_name: 'Question', optional: true - def self.post_type_id PostType.mapping['Question'] end diff --git a/app/models/subscription.rb b/app/models/subscription.rb index c6f6c5844502fcbfdbe220deee0bf35d5b0dd94b..bb7447f134a4da7bf8777f4d594369fb4f2d3117 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -37,11 +37,11 @@ class Subscription < ApplicationRecord def qualifier_presence return unless ['tag', 'user', 'category'].include? type - if type == 'tag' && (!qualifier.present? || Tag.find_by(name: qualifier).nil?) + if type == 'tag' && (qualifier.blank? || Tag.find_by(name: qualifier).nil?) errors.add(:qualifier, 'must provide a valid tag name for tag subscriptions') - elsif type == 'user' && (!qualifier.present? || User.find_by(id: qualifier).nil?) + elsif type == 'user' && (qualifier.blank? || User.find_by(id: qualifier).nil?) errors.add(:qualifier, 'must provide a valid user ID for user subscriptions') - elsif type == 'category' && (!qualifier.present? || Category.find_by(id: qualifier).nil?) + elsif type == 'category' && (qualifier.blank? || Category.find_by(id: qualifier).nil?) errors.add(:qualifier, 'must provide a valid category ID for category subscriptions') end end diff --git a/app/models/suspicious_vote.rb b/app/models/suspicious_vote.rb index a2961002022c4d180975a7932c75b59c599881a2..27416bebad40de37920ed05589767d274a720d8b 100644 --- a/app/models/suspicious_vote.rb +++ b/app/models/suspicious_vote.rb @@ -1,6 +1,6 @@ class SuspiciousVote < ApplicationRecord - belongs_to :from_user, foreign_key: 'from_user_id', class_name: 'User' - belongs_to :to_user, foreign_key: 'to_user_id', class_name: 'User' + belongs_to :from_user, class_name: 'User' + belongs_to :to_user, class_name: 'User' validates :from_user, uniqueness: { scope: [:to_user] } @@ -9,7 +9,7 @@ class SuspiciousVote < ApplicationRecord end def self.check_for_vote_fraud - User.all.each do |u| + User.all.find_each do |u| votes = u.votes.group(:recv_user_id).count(:recv_user_id) total = u.votes.count votes.each do |recv_id, cnt| diff --git a/app/models/tag.rb b/app/models/tag.rb index 191a1f78b7b5299d6e663a81ae950eae9e7be755..670c16ff4eaa48571ca52d2f753695fdeb3b71f6 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -38,7 +38,7 @@ class Tag < ApplicationRecord private def parent_not_self - return unless parent_id.present? + return if parent_id.blank? if parent_id == id errors.add(:base, 'A tag cannot be its own parent.') @@ -46,7 +46,7 @@ class Tag < ApplicationRecord end def parent_not_own_child - return unless parent_id.present? + return if parent_id.blank? if all_children.include? parent_id errors.add(:base, "The #{parent.name} tag is already a child of this tag.") diff --git a/app/models/user.rb b/app/models/user.rb index 9db3fc30ae9c5933221e407fb3be63799345032a..a2679c6042320bd17f932e6ac7e8abe5ceb3a292 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -19,11 +19,11 @@ class User < ApplicationRecord has_many :suggested_edits, dependent: :nullify has_many :suggested_edits_decided, class_name: 'SuggestedEdit', foreign_key: 'decided_by_id', dependent: :nullify has_many :audit_logs, dependent: :nullify - has_many :audit_logs_related, class_name: 'AuditLog', foreign_key: 'related_id', dependent: :nullify, as: :related + has_many :audit_logs_related, class_name: 'AuditLog', dependent: :nullify, as: :related has_many :mod_warning_author, class_name: 'ModWarning', foreign_key: 'author_id', dependent: :nullify validates :username, presence: true, length: { minimum: 3, maximum: 50 } - validates :login_token, uniqueness: { allow_nil: true, allow_blank: true } + validates :login_token, uniqueness: { allow_blank: true } validate :no_links_in_username validate :username_not_fake_admin validate :no_blank_unicode_in_username @@ -31,7 +31,7 @@ class User < ApplicationRecord validate :is_not_blocklisted validate :email_not_bad_pattern - delegate :reputation, :reputation=, :privilege?, :privilege, to: :community_user + delegate :reputation, :reputation=, :privilege?, :privilege, :trust_level, to: :community_user after_create :send_welcome_tour_message @@ -83,31 +83,10 @@ class User < ApplicationRecord is_global_admin || community_user&.is_admin || false end - def trust_level - attributes['trust_level'] || recalc_trust_level - end - def rtl_safe_username "#{username}\u202D" end - def recalc_trust_level - # Temporary hack until we have some things to actually calculate based on. - trust = if staff? - 5 - elsif is_moderator || is_global_moderator || is_admin || is_global_admin - 4 - elsif privilege?('flag_close') || privilege?('edit_posts') - 3 - elsif privilege?('unrestricted') - 2 - else - 1 - end - update(trust_level: trust) - trust - end - def username_not_fake_admin admin_badge = SiteSetting['AdminBadgeCharacter'] mod_badge = SiteSetting['ModBadgeCharacter'] diff --git a/app/models/vote.rb b/app/models/vote.rb index 90b7e2cfca252c4474840efab97900d9ad1e6066..b0f6e4f471c641992ab2350bd6e72f04ab57bfa7 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -2,14 +2,14 @@ # association), and to a user. class Vote < ApplicationRecord include PostRelated - belongs_to :user, required: true - belongs_to :recv_user, class_name: 'User', required: true + belongs_to :user, optional: false + belongs_to :recv_user, class_name: 'User', optional: false after_create :apply_rep_change + after_create :add_counter before_destroy :check_valid before_destroy :reverse_rep_change - after_create :add_counter after_destroy :remove_counter validates :vote_type, inclusion: [1, -1] diff --git a/app/views/answers/_answer.html.erb b/app/views/answers/_answer.html.erb deleted file mode 100644 index 57af4919a16676cf0867f6418e4b4d1fb297a126..0000000000000000000000000000000000000000 --- a/app/views/answers/_answer.html.erb +++ /dev/null @@ -1,14 +0,0 @@ -<div class="question"> - <div class="q-info-box"> - <p class="large-text center"><%= answer.score %></p> - <p>score</p> - </div> - <div class="q-info-box"> - <p><a href="/questions/<%= answer.parent.id %>"><%= answer.parent.title %></a></p> - <p> - Answered <span title="<%= answer.created_at %>" data-livestamp="<%= answer.created_at.to_time.iso8601 %>"><%= answer.created_at %></span> - by <a dir="ltr" href="/users/<%= answer.user.id %>"><%= answer.user.rtl_safe_username %></a> - </p> - </div> - <span class="clearfix"></span> -</div> diff --git a/app/views/answers/_new.html.erb b/app/views/answers/_new.html.erb deleted file mode 100644 index 76ad4eaba24cd12198cec94b7925ab21f0e90a57..0000000000000000000000000000000000000000 --- a/app/views/answers/_new.html.erb +++ /dev/null @@ -1,47 +0,0 @@ -<h2>Your Answer</h2> -<% if answer.errors.any? %> - <div class="notice is-danger"> - <p>The following <%= "error".pluralize(answer.errors.count) %> prevented the answer from being posted:</p> - <ul> - <% answer.errors.full_messages.each do |e| %> - <li><%= e %></li> - <% end %> - </ul> - </div> -<% end %> - -<%= render 'posts/markdown_script' %> -<%= render 'posts/image_upload' %> - -<%= form_for answer, url: create_answer_path do |f| %> - <%= render 'shared/body_field', f: f, field_name: :body_markdown, field_label: 'Body', post: answer %> - - <div class="form-group"> - <%= f.label :license_id, 'License', class: 'form-element' %> - <% cat = defined?(@category) ? @category : @question.category %> - <span class="form-caption"> - <% site_default = License.site_default %> - <% category_default = cat.license %> - <% if site_default.present? %> - site default: <a href="javascript:void(0)" class="js-license-autofill" data-license-id="<%= site_default.id %>"> - <%= site_default.name %> - </a> - <% end %> - <% if site_default.present? && category_default.present? %> - · - <% end %> - <% if category_default.present? %> - category default: <a href="javascript:void(0)" class="js-license-autofill" data-license-id="<%= category_default.id %>"> - <%= category_default.name %> - </a> - <% end %> - </span> - <%= f.select :license_id, options_for_select(License.enabled.default_order(cat).map { |l| [l.name, l.id] }), - {}, class: 'form-element' %> - </div> - - <div class="post-preview"></div> - <div class="field"> - <%= f.submit "Post Answer", class: "button is-filled" %> - </div> -<% end %> diff --git a/app/views/answers/edit.html.erb b/app/views/answers/edit.html.erb deleted file mode 100644 index 6a7757331ea6d9f4c4534935a556f65db2dbf7f4..0000000000000000000000000000000000000000 --- a/app/views/answers/edit.html.erb +++ /dev/null @@ -1,47 +0,0 @@ -<% content_for :title, "Edit Answer" %> - -<h1><%= check_your_post_privilege(@answer, 'edit_posts') ? "Edit Answer" : "Suggest Edit for Answer" %></h1> -<% if @answer.errors.any? %> - <div class="notice is-danger"> - <p>The following <%= "error".pluralize(@answer.errors.count) %> prevented the answer from being updated:</p> - <ul> - <% @answer.errors.full_messages.each do |e| %> - <li><%= e %></li> - <% end %> - </ul> - </div> -<% end %> - -<%= render 'posts/markdown_script' %> -<%= render 'posts/image_upload' %> - -<%= form_for @answer, url: { controller: :answers, action: :update } do |f| %> - <%= render 'shared/body_field', f: f, field_name: :body_markdown, field_label: 'Body', post: @answer %> - <div class="post-preview"></div> - <div class="form-group"> - <%= label_tag :edit_comment, 'Edit comment', class: "form-element" %> - <div class="form-caption"> - Describe—if necessary—what you are changing and why you are making this edit. - </div> - <%= text_field_tag :edit_comment, params[:edit_comment], class: 'form-element' %> - </div> - <div class="form-group"> - <%= f.submit (check_your_post_privilege(@answer, 'edit_posts') ? "Save changes" : "Suggest changes"), class: "button is-filled" %> - <%= link_to 'Cancel', question_path(@answer.parent), class: 'button is-outlined is-muted' %> - </div> -<% end %> - -<h2>Question</h2> -<div class="has-padding-6 has-border-style-solid has-border-width-1 has-border-color-tertiary-050" style="max-height: 500px; overflow: auto;"> -<%= render 'posts/expanded', post: @answer.parent %> -</div> - -<% content_for :sidebar do %> - <div class="widget has-margin-4"> - <h4 class="widget--header has-margin-0">Hints and Tips</h4> - <div class="widget--body"> - <% guidance = @answer.category.answering_guidance_override || SiteSetting['AnsweringGuidance'] %> - <%= raw(sanitize(render_markdown(guidance), scrubber: scrubber)) %> - </div> - </div> -<% end %> diff --git a/app/views/answers/new.html.erb b/app/views/answers/new.html.erb deleted file mode 100644 index 578e61015b553eca671ef855d6682fd1aa653074..0000000000000000000000000000000000000000 --- a/app/views/answers/new.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= render 'posts/expanded', post: @question, answer: @answer %> -<hr/> -<%= render 'new', answer: @answer, parent: @question %> \ No newline at end of file diff --git a/app/views/articles/_form.html.erb b/app/views/articles/_form.html.erb deleted file mode 100644 index 594316ee5b7a93ac6810684bf5a2f845178e27eb..0000000000000000000000000000000000000000 --- a/app/views/articles/_form.html.erb +++ /dev/null @@ -1,48 +0,0 @@ -<%= render 'posts/markdown_script' %> - -<% if @article.errors.any? %> - <div class="notice is-danger is-filled"> - The following errors prevented this post from being saved: - <ul> - <% @article.errors.full_messages.each do |msg| %> - <li><%= msg %></li> - <% end %> - </ul> - </div> -<% end %> - -<%= render 'posts/image_upload' %> - -<%= form_for @article, url: edit_article_path(@article) do |f| %> - <div class="form-group"> - <%= f.label :title, "Title your post:", class: "form-element" %> - <%= f.text_field :title, class: "form-element" %> - </div> - - <%= render 'shared/body_field', f: f, field_name: :body_markdown, field_label: 'Body', post: @article %> - - <div class="post-preview"></div> - - <div class="form-group"> - <%= f.label :tags_cache, "Tags", class: "form-element" %> - <div class="form-caption"> - Tags help to categorize posts. Separate them by space. Use hyphens for multiple-word tags. - </div> - <%= f.select :tags_cache, options_for_select(@article.tags_cache.map { |t| [t, t] }, selected: @article.tags_cache), - { include_blank: true }, multiple: true, class: "form-element js-tag-select", - data: { tag_set: @article.category.tag_set.id } %> - </div> - - <div class="form-group"> - <%= label_tag :edit_comment, 'Edit comment', class: "form-element" %> - <div class="form-caption"> - Describe—if necessary—what you are changing and why you are making this edit. - </div> - <%= text_field_tag :edit_comment, params[:edit_comment], class: 'form-element' %> - </div> - - <div class="form-group"> - <%= f.submit check_your_post_privilege(@article, 'edit_posts') ? "Save changes" : "Suggest changes", class: "button is-filled" %> - <%= link_to 'Cancel', article_path(@article), class: 'button is-outlined is-muted' %> - </div> -<% end %> diff --git a/app/views/articles/edit.html.erb b/app/views/articles/edit.html.erb deleted file mode 100644 index 6727a85a707a9f1bfb920d6498c26629bb9bfc8f..0000000000000000000000000000000000000000 --- a/app/views/articles/edit.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= 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 deleted file mode 100644 index ffbd774949cb4b6869468649a2c380dcff20517b..0000000000000000000000000000000000000000 --- a/app/views/articles/show.html.erb +++ /dev/null @@ -1,19 +0,0 @@ -<% 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 %> - -<% content_for :twitter_card_meta do %> - <meta name="twitter:card" content="summary" /> - <% if @article.user.twitter.present? %> - <meta name="twitter:creator" content="@<%= @article.user.twitter %>" /> - <% end %> - <meta property="og:url" content="<%= question_url(@article) %>" /> - <meta property="og:title" content="<%= @article.title %>" /> - <meta property="og:description" content="<%= @article.body_plain[0..150].strip %>..." /> - <meta property="og:image" content="<%= "https://#{RequestContext.community.host}#{SiteSetting['SiteLogoPath']}" %>" /> -<% end %> - -<%= render 'posts/expanded', post: @article %> \ No newline at end of file diff --git a/app/views/categories/post_types.html.erb b/app/views/categories/post_types.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..3b594b3277a8fe848b1f1f16c7c46cea115e9f3c --- /dev/null +++ b/app/views/categories/post_types.html.erb @@ -0,0 +1,10 @@ +<h1>What kind of post?</h1> +<p class="has-font-size-larger has-color-tertiary-500"> + This category has more than one type of post available. Pick a post type to get started. +</p> + +<% @post_types.each do |pt| %> + <h3><%= link_to pt.name.underscore.humanize, new_category_post_path(post_type: pt, category: @category) %></h3> + <p class="has-color-tertiary-500"><%= pt.description %></p> + <hr/> +<% end %> diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index 5dbee75d0fa07eab46e124be2e488b6a862fe32b..33fe278446f5cbd9ef258fe32f2ed387fb7bdd34 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -126,10 +126,8 @@ class: "category-header--nav-item #{active?(current_cat) && controller_name != 'tags' ? 'is-active' : ''}" %> <%= link_to 'Tags', category_tags_path(current_cat), class: "category-header--nav-item #{active?(current_cat) && controller_name == 'tags' ? 'is-active' : ''}" %> - <%# There will eventually be a Tags link here too, once we have a page that lists tags per-tag set. %> <div class="category-header--nav-separator"></div> - <% ptid = current_cat.display_post_types.reject { |e| e.to_s.empty? }.first || Question.post_type_id %> - <%= link_to create_post_path(category_id: current_cat.id, post_type_id: ptid), + <%= link_to category_post_types_path(current_cat.id), class: 'category-header--nav-item is-button' do %> <%= current_cat.button_text.present? ? current_cat.button_text : 'Create Post' %> <% end %> diff --git a/app/views/moderator/index.html.erb b/app/views/moderator/index.html.erb index 95fa1c29850cf915aebff5162be14b7343c88837..26b11ae4a1a27377ffd583b9e90a325d79b50acf 100644 --- a/app/views/moderator/index.html.erb +++ b/app/views/moderator/index.html.erb @@ -28,7 +28,7 @@ <div class="widget"> <div class="widget--body"> <i class="fas fa-file-alt"></i> - <%= link_to 'Create Help Page', new_help_post_path %> + <%= link_to 'Create Help Page', new_post_path(post_type: HelpDoc.post_type_id) %> </div> </div> </div> diff --git a/app/views/posts/_article_list.html.erb b/app/views/posts/_article_list.html.erb index 607dd804a9dbd013d34a47e2128e7d2015adffb6..3d945a0d2695a0c16326469e82c73d838aafe0d9 100644 --- a/app/views/posts/_article_list.html.erb +++ b/app/views/posts/_article_list.html.erb @@ -19,7 +19,7 @@ <% if @show_category_tag %> <span class="badge is-tag is-filled"><%= defined?(@category) ? @category.name : post.category.name %></span> <% end %> - <%= link_to post.title, share_article_path(post), 'data-ckb-item-link' => '' %> + <%= link_to post.title, generic_share_link(post), 'data-ckb-item-link' => '' %> </div> <% if @last_activity %> <p class="has-color-tertiary-600 has-float-right post-list--meta"> diff --git a/app/views/posts/_expanded.html.erb b/app/views/posts/_expanded.html.erb index f6c43b3cd45556115d4b494d68ce38e82fef8d17..1f39eb34844393aacc75703b6de2bbe647af621d 100644 --- a/app/views/posts/_expanded.html.erb +++ b/app/views/posts/_expanded.html.erb @@ -1,52 +1,61 @@ +<%# + Full post view, containing all details and interactions. + Variables: + post : the Post instance to display +%> + +<% category ||= defined?(@category) ? @category : post.category %> +<% post_type ||= defined?(@post_type) ? @post_type : post.post_type %> + <% 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? %> <div class="post <%= post.meta? ? 'is-meta' : '' %> <%= is_top_level ? '' : 'has-border-bottom-style-solid has-border-bottom-width-1 has-border-color-tertiary-100' %>" data-post-id="<%= post.id %>" id="<%= (is_question ? 'question-' : 'answer-') + post.id.to_s %>" data-ckb-list-item data-ckb-item-type="post" data-ckb-post-id="<%= post.id %>"> <% if is_top_level %> <h1 class="post--title has-border-top-width-4 has-border-top-style-solid has-border-color-<%= post.meta? ? 'tertiary' : 'primary' %>-400 has-padding-2"> <span class="post--title-text"> <%= post.title %> - <%= is_question && post.closed ? "[closed]" : "" %> + <%= post_type.is_closeable && post.closed ? "[closed]" : "" %> </span> - <% 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) %> + <%= post_type_badge(post_type.name) %> <% end %> </h1> <% end %> <div class="post--container <%= 'deleted-content' if post.deleted? %> grid is-nowrap"> - <div> - <div class="post--votes has-text-align-center" title="<%= post.score %>"> - <% existing_vote = my_vote(post) %> - <% unless post.locked? %> - <button class="vote-button button is-icon-only-button <%= (existing_vote&.vote_type == 1) ? 'is-active' : '' %>" - data-vote-type="1" data-vote-id="<%= existing_vote&.id %>" id="post-<%= post.id %>-up" aria-label="Upvote"> - <svg width="2em" height="1.33em" viewbox="0 0 100 50"> - <path d="M50,0 L100,50 L0,50 Z" fill="currentColor" /> - </svg> - </button> - <% end %> - <div class="score has-font-size-subheading js-upvote-count"> - +<%= post.upvote_count %> - </div> - <div class="score has-font-size-subheading js-downvote-count"> - −<%= post.downvote_count %> + <% if post_type.has_votes %> + <div> + <div class="post--votes has-text-align-center" title="<%= post.score %>"> + <% existing_vote = my_vote(post) %> + <% unless post.locked? %> + <button class="vote-button button is-icon-only-button <%= (existing_vote&.vote_type == 1) ? 'is-active' : '' %>" + data-vote-type="1" data-vote-id="<%= existing_vote&.id %>" id="post-<%= post.id %>-up" aria-label="Upvote"> + <svg width="2em" height="1.33em" viewbox="0 0 100 50"> + <path d="M50,0 L100,50 L0,50 Z" fill="currentColor" /> + </svg> + </button> + <% end %> + <div class="score has-font-size-subheading js-upvote-count"> + +<%= post.upvote_count %> + </div> + <div class="score has-font-size-subheading js-downvote-count"> + −<%= post.downvote_count %> + </div> + <% unless post.locked? %> + <button class="vote-button button is-icon-only-button <%= (existing_vote&.vote_type == -1) ? 'is-active' : '' %>" <%= (post.locked?) ? 'disabled' : '' %> + data-vote-type="-1" data-vote-id="<%= existing_vote&.id %>" id="post-<%= post.id %>-up" aria-label="Downvote"> + <svg width="2em" height="1.33em" viewbox="0 0 100 50"> + <path d="M0,0 L100,0 L50,50 Z" fill="currentColor" /> + </svg> + </button> + <% end %> </div> - <% unless post.locked? %> - <button class="vote-button button is-icon-only-button <%= (existing_vote&.vote_type == -1) ? 'is-active' : '' %>" <%= (post.locked?) ? 'disabled' : '' %> - data-vote-type="-1" data-vote-id="<%= existing_vote&.id %>" id="post-<%= post.id %>-up" aria-label="Downvote"> - <svg width="2em" height="1.33em" viewbox="0 0 100 50"> - <path d="M0,0 L100,0 L50,50 Z" fill="currentColor" /> - </svg> - </button> - <% end %> </div> - </div> + <% end %> <div class="post--content has-padding-2"> - <% if is_question && post.closed %> + <% if post_type.is_closeable && post.closed %> <div class="notice has-margin-2"> <p class="has-font-weight-normal"> <strong>Closed</strong> @@ -59,7 +68,7 @@ <p> <%= post.close_reason.description.gsub "$SiteName", SiteSetting['SiteName'] %> <% if post.close_reason.requires_other_post %> - See: <%= link_to post.duplicate_post.title, question_path(post.duplicate_post), class: "has-font-size-larger" %> + See: <%= link_to post.duplicate_post.title, post_path(post.duplicate_post), class: "has-font-size-larger" %> <% end %> </p> <% end %> @@ -100,7 +109,7 @@ </div> <% end %> - <% if post.pending_suggested_edit? %> + <% if post_type.is_public_editable && post.pending_suggested_edit? %> <% if check_your_post_privilege(post, 'edit_posts') %> <div class="notice h-p-2"> <p class="h-m-0"><i class="fa fa-pencil-alt h-m-l-2 h-c-red-600"></i> There is a <strong>pending suggested edit</strong> on this post. <a href="<%= suggested_edit_url(post.pending_suggested_edit.id) %>" class="button is-outlined is-muted is-small">Review changes</a></p> @@ -121,7 +130,7 @@ <div class="post--meta has-margin-bottom-4"> <%= render 'users/post_usercard', post: post %> <div> - <% if has_tags %> + <% if post_type.has_tags %> <div class="post--tags has-padding-2"> <% required_ids = post.category&.required_tag_ids %> <% moderator_ids = post.category&.moderator_tag_ids %> @@ -135,7 +144,7 @@ <% end %> </div> <% end %> - <% if post.license.present? || been_edited %> + <% if (post_type.has_license && post.license.present?) || been_edited %> <div class="post--licensing--edited has-margin-left-2"> <% if post.license.present? %> <i class="fas fa-balance-scale" title="License"></i> @@ -161,15 +170,15 @@ <div class="post--actions"> <div class="tools"> - <%= link_to generic_share_link(post), class: 'tools--item js-permalink' do %> - <i class="fa fa-link"></i> - <span class="js-text">Permalink</span> - <% end %> - <%= link_to post_history_path(post), class: 'tools--item' do %> - <i class="fa fa-history"></i> - History - <% end %> - <% unless post.locked? %> + <%= link_to generic_share_link(post), class: 'tools--item js-permalink' do %> + <i class="fa fa-link"></i> + <span class="js-text">Permalink</span> + <% end %> + <%= link_to post_history_path(post), class: 'tools--item' do %> + <i class="fa fa-history"></i> + History + <% end %> + <% unless post.locked? || !post_type.is_public_editable %> <% if check_your_post_privilege(post, 'edit_post') %> <% if post.pending_suggested_edit? %> <%= link_to suggested_edit_url(post.pending_suggested_edit.id), class: 'tools--item is-danger is-filled' do %> @@ -177,7 +186,7 @@ Review suggested edit <% end %> <% else %> - <%= link_to generic_edit_link(post), class: 'tools--item' do %> + <%= link_to edit_post_path(post), class: 'tools--item' do %> <i class="fa fa-pencil-alt"></i> Edit <% end %> @@ -186,28 +195,28 @@ <% if post.pending_suggested_edit? %> <span class="tools--item">suggested edit pending...</span> <% else %> - <%= link_to generic_edit_link(post), class: 'tools--item' do %> + <%= link_to edit_post_path(post), class: 'tools--item' do %> <i class="fa fa-pencil-alt"></i> Suggest edit <% end %> <% end %> <% end %> <% end %> - <% unless current_user.nil? %> + <% if user_signed_in? %> <a href="#" class="flag-dialog-link tools--item"> <i class="fa fa-flag"></i> Flag </a> <% end %> - <% unless post.locked? %> + <% unless post.locked? || !post_type.is_closeable %> <% if check_your_privilege('flag_close') %> - <% if is_question && !post.closed %> + <% if !post.closed %> <a href="#" class="close-dialog-link tools--item"> <i class="fa fa-lock"></i> Close </a> - <% elsif is_question && post.closed %> - <%= link_to reopen_question_path(post), method: :post, class: 'reopen-question tools--item' do %> + <% elsif post.closed %> + <%= link_to reopen_post_path(post), method: :post, class: 'reopen-question tools--item' do %> <i class="fa fa-unlock"></i> Reopen <% end %> @@ -217,14 +226,14 @@ <% if check_your_post_privilege(post, 'flag_curate') %> <% unless post.locked? %> <% if !post.deleted %> - <%= link_to 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: "tools--item is-danger" do %> + <%= link_to delete_post_path(post), method: :post, + data: { confirm: 'Are you sure you want to delete this post?' }, class: "tools--item is-danger" do %> <i class="fa fa-trash"></i> Delete <% end %> <% else %> - <%= link_to url_for(controller: post.post_type.name.pluralize.downcase.to_sym, action: :undelete, id: post.id), - method: :post, data: { confirm: 'Restore this post, making it visible to regular users?' }, class: "tools--item is-danger is-filled" do %> + <%= link_to restore_post_path(post), method: :post, + data: { confirm: 'Restore this post, making it visible to regular users?' }, class: "tools--item is-danger is-filled" do %> <i class="fa fa-undo"></i> Restore <% end %> @@ -236,13 +245,13 @@ <i class="fa fa-wrench"></i> Tools </a> - <% flags_count = if current_user.is_moderator - post.flags - else - post.flags.not_confidential - end.where(handled_by_id: nil).count %> + <% flags_count = if current_user&.is_moderator + post.flags + else + post.flags.not_confidential + end.where(handled_by_id: nil).count %> - <% own_flags_count = if current_user.is_moderator + <% own_flags_count = if current_user&.is_moderator 0 else post.flags.not_confidential.where(user: current_user, handled_by_id: nil).count @@ -251,7 +260,7 @@ <% if flags_count > 0 %> <a href="#" class="show-all-flags-dialog-link tools--item"> <i class="fa fa-exclamation-triangle"></i> - Show <%= pluralize(flags_count - own_flags_count, "flag") %> + Show <%= pluralize(flags_count - own_flags_count, 'flag') %> </a> <% end %> <% end %> @@ -288,9 +297,9 @@ <div class="grid--cell is-flexible"> <label class="form-element has-margin-0" for="flag-reason-other_<%= post.id %>"> other reason - <div class="form-caption"> + <span class="form-caption"> Please elaborate in the details field below. - </div> + </span> </label> </div> </div> @@ -310,11 +319,11 @@ </div> </div> - <% if is_question %> + <% if post_type.is_closeable %> <div class="post--action-dialog js-close-box"> <div class="widget"> <div class="widget--header">Why should this post be closed?</div> - <% (@close_reasons || CloseReason.all).each do |reason| %> + <% CloseReason.active.each do |reason| %> <div class="widget--body"> <div class="grid"> <div class="grid--cell"> @@ -335,7 +344,7 @@ </div> <% end %> <div class="widget--footer"> - <button class="close-question button is-filled is-muted" data-post-type="<%= is_question ? 'Question' : 'Answer' %>" data-post-id="<%= post.id %>"> + <button class="js-close-question button is-filled is-muted" data-post-id="<%= post.id %>"> Close this post </button> </div> @@ -378,7 +387,6 @@ <% end %> <div class="post--status"> - <% if post.att_source.present? %> <div class="notice has-margin-2"> <p> @@ -389,8 +397,8 @@ </p> </div> <% end %> - </div> - <div class="post--comments has-padding-4"> + </div> + <div class="post--comments has-padding-4"> <h4 class="has-margin-0"> <%= pluralize(post.comments.undeleted.size, 'comment') %> <span class="has-color-tertiary-500"><%= (moderator? && post.comments.deleted.size != 0) ? '(deleted: ' + post.comments.deleted.size.to_s + ')' : '' %></span> diff --git a/app/views/posts/_form.html.erb b/app/views/posts/_form.html.erb index 91758e9ce08e8a349f07ca4df125c84ed6b919a1..c274a537fc6334d5a4c7c6554b7edbaffbc363ef 100644 --- a/app/views/posts/_form.html.erb +++ b/app/views/posts/_form.html.erb @@ -1,81 +1,96 @@ -<% with_post_type ||= false %> +<%# + Post create/edit form covering all post types. + Variables: + post : a Post instance to work on (either via .new or .find - doesn't matter if it has content or not) + submit_path : the URL to submit the form to (must accept POST requests) + post_type : a PostType instance describing the requested post type + category : a Category instance, required if the post type has a category, indicating which category to post in + parent : a Post instance containing the parent for this post (required if the post type has a parent) + inline_parent : optional, default true: display an inline copy of the parent? (if present) + type_summary : optional, default true: display a summary of the requested post type? + edit_comment : optional, default false: include a field for an edit comment? +%> + +<% + # defaults + category ||= defined?(category) ? category : nil + parent ||= defined?(parent) ? parent : nil + inline_parent ||= defined?(inline_parent) ? inline_parent : true + type_summary ||= defined?(type_summary) ? type_summary : true + edit_comment ||= defined?(edit_comment) ? edit_comment : false +%> <% content_for :head do %> <%= render 'posts/markdown_script' %> <% end %> -<div class="notice is-info"> - <p><strong>Posting Tips</strong></p> - <div class="has-font-size-caption"> - <% guidance = @category.asking_guidance_override %> - <%= raw(sanitize(render_markdown(guidance.present? ? guidance : SiteSetting['AskingGuidance']), scrubber: scrubber)) %> - </div> -</div> - -<% if @post.errors.any? %> - <div class="notice is-danger is-filled"> - <p>The following errors prevented your post being saved:</p> - <ul> - <% @post.errors.full_messages.each do |msg| %> - <li><%= msg %></li> - <% end %> - </ul> - </div> -<% end %> - <%= render 'posts/image_upload' %> -<%= form_for @post, url: submit_path, html: { class: 'has-margin-top-4' } do |f| %> - <%= f.hidden_field :category_id %> +<%= form_for post, url: submit_path, html: { class: 'has-margin-top-4' } do |f| %> + <% if post.id.nil? %> + <%= f.hidden_field :post_type_id, value: post.post_type_id || post_type.id %> + <% end %> - <% if with_post_type %> - <div class="form-group"> - <%= f.label :post_type_id, 'Post type', class: 'form-element' %> - <span class="form-caption">What kind of post is this? Questions can have answers; articles only have comments.</span> - <% 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' %> - </div> - <% else %> - <%= f.hidden_field :post_type_id %> + <% if post_type.has_category? && post.id.nil? %> + <%= f.hidden_field :category_id, value: post.category_id || category&.id || parent&.category_id %> + <% end %> + + <% if post_type.has_parent? && post.id.nil? %> + <%= f.hidden_field :parent_id, value: post.parent_id || parent&.id %> + <% end %> + + <% if type_summary %> + <p> + <span class="has-font-size-larger"> + You're writing a new <strong><%= post_type.name %></strong> + <% if post_type.has_category? && category.present? %> + in <strong><%= category.name %></strong> + <% end %> + </span><br/> + <span class="has-font-size-smaller has-color-tertiary-700"> + <%= post_type.description %> + </span> + </p> <% end %> - <%= render 'shared/body_field', f: f, field_name: :body_markdown, field_label: 'Body', post: @post %> + <%= render 'shared/body_field', f: f, field_name: :body_markdown, field_label: 'Body', post: post %> <div class="post-preview"></div> - <div class="form-group"> - <%= f.label :title, 'Summarize your post with a title:', class: 'form-element' %> - <%= f.text_field :title, class: 'form-element' %> - </div> + <% unless post_type.has_parent? %> + <div class="form-group"> + <%= f.label :title, 'Summarize your post with a title:', class: 'form-element' %> + <%= f.text_field :title, class: 'form-element' %> + </div> + <% end %> - <div class="form-group"> - <%= f.label :tags_cache, 'Tags (at least one):', class: 'form-element' %> - <% required_tags = @category.required_tags.to_a %> - <% unless required_tags.empty? %> - <span class="form-caption"> - Requires at least one of - <% required_tags.each do |tag| %> - <a class="badge is-tag is-filled js-add-required-tag" href="javascript:void(0)" data-tag-id="<%= tag.id %>" - data-tag-name="<%= tag.name %>"> - <%= tag.name %> - </a> - <% end %> - </span> - <% end %> - <%= f.select :tags_cache, options_for_select(@post.tags_cache.map { |t| [t, t] }, selected: @post.tags_cache), - { include_blank: true }, multiple: true, class: "form-element js-tag-select", - data: { tag_set: @category.tag_set_id } %> - </div> + <% if post_type.has_tags? && category.present? %> + <div class="form-group"> + <%= f.label :tags_cache, 'Tags (at least one):', class: 'form-element' %> + <% required_tags = category.required_tags.to_a %> + <% unless required_tags.empty? %> + <span class="form-caption"> + Requires at least one of + <% required_tags.each do |tag| %> + <a class="badge is-tag is-filled js-add-required-tag" href="javascript:void(0)" data-tag-id="<%= tag.id %>" + data-tag-name="<%= tag.name %>"> + <%= tag.name %> + </a> + <% end %> + </span> + <% end %> + <%= f.select :tags_cache, options_for_select(post.tags_cache.map { |t| [t, t] }, selected: post.tags_cache), + { include_blank: true }, multiple: true, class: "form-element js-tag-select", + data: { tag_set: category.tag_set_id } %> + </div> + <% end %> - <% unless @post.id.present? %> + <% if post_type.has_category? && category.present? && post_type.has_license? && !post.id.present? %> <div class="form-group"> <%= f.label :license_id, 'License', class: 'form-element' %> <span class="form-caption"> <% site_default = License.site_default %> - <% category_default = @category.license %> + <% category_default = category.license %> <% if site_default.present? %> site default: <a href="javascript:void(0)" class="js-license-autofill" data-license-id="<%= site_default.id %>"> <%= site_default.name %> @@ -90,13 +105,58 @@ </a> <% end %> </span> - <%= f.select :license_id, options_for_select(License.enabled.default_order(@category).map { |l| [l.name, l.id] }, - selected: @post.license_id), {}, class: 'form-element' %> + <%= f.select :license_id, options_for_select(License.enabled.default_order(category).map { |l| [l.name, l.id] }, + selected: post.license_id), {}, class: 'form-element' %> + </div> + <% end %> + + <% if [HelpDoc.post_type_id, PolicyDoc.post_type_id].include? post_type.id %> + <div class="form-group"> + <%= f.label :help_category, 'Category', class: 'form-element' %> + <span class="form-caption"> + Name a category under which to display this post in the help center. + </span> + <%= f.text_field :help_category, class: 'form-element' %> + </div> + + <div class="form-group"> + <%= f.label :help_ordering, 'Order', class: 'form-element' %> + <span class="form-caption"> + Control where this post appears in the list of help articles. Higher values appear later in the list. + </span> + <%= f.number_field :help_ordering, class: 'form-element' %> + </div> + + <div class="form-group"> + <%= f.label :doc_slug, 'URL slug', class: 'form-element' %> + <span class="form-caption">In a URL of "https://yoursite.codidact.com/help/topic", the "topic" is the slug.</span> + <%= f.text_field :doc_slug, class: 'form-element' %> + </div> + <% end %> + + <% if edit_comment %> + <hr/> + <div class="form-group"> + <%= label_tag :edit_comment, 'Edit comment', class: 'form-element' %> + <%= text_field_tag :edit_comment, nil, class: 'form-element' %> </div> <% end %> <div class="actions"> - <%= f.submit "Save Post in #{@category.name}", class: 'button is-filled' %> - <%= link_to 'Cancel', category_path(@category), class: 'button is-muted is-outlined' %> + <% if post_type.has_category && category.present? %> + <%= f.submit "Save Post in #{category.name}", class: 'button is-filled' %> + <%= link_to 'Cancel', category_path(category), class: 'button is-muted is-outlined is-danger', + data: { confirm: 'Any unsaved changes will be lost. Are you sure?' } %> + <% else %> + <%= f.submit 'Save Post', class: 'button is-filled' %> + <%= link_to 'Cancel', :back, class: 'button is-muted is-outlined is-danger', + data: { confirm: 'Any unsaved changes will be lost. Are you sure?' } %> + <% end %> </div> -<% end %> \ No newline at end of file +<% end %> + +<% if inline_parent && defined?(parent) && parent.present? %> + <h2>Responding to:</h2> + + <%= render 'posts/expanded', post: parent %> +<% end %> diff --git a/app/views/posts/document.html.erb b/app/views/posts/document.html.erb index 00e5560a2ee51cad47a051b13a3bd162c0063532..a5cb84a167ba3c82b7ae75a9110be610f0b9a887 100644 --- a/app/views/posts/document.html.erb +++ b/app/views/posts/document.html.erb @@ -3,7 +3,7 @@ <% end %> <% unless @post.nil? %> <% if (moderator? && @post.post_type_id == HelpDoc.post_type_id) || (admin? && @post.post_type_id == PolicyDoc.post_type_id) %> - <%= link_to 'edit', edit_help_post_path(@post), class: "button is-outlined is-muted" %> + <%= link_to 'edit', edit_post_path(@post), class: "button is-outlined is-muted" %> <% end %> <% end %> diff --git a/app/views/posts/edit.html.erb b/app/views/posts/edit.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..4195ace2658236a6300a76f77fc78b2052643ceb --- /dev/null +++ b/app/views/posts/edit.html.erb @@ -0,0 +1,30 @@ +<h1> + Edit <%= @post_type.name.underscore.humanize.titleize %> + <% if @category.present? %> + in <%= @category.name %> + <% end %> +</h1> + +<% if @category.present? %> + <div class="notice is-info"> + <p><strong>Posting Tips</strong></p> + <div class="has-font-size-caption"> + <% guidance = @category.asking_guidance_override %> + <%= raw(sanitize(render_markdown(guidance.present? ? guidance : SiteSetting['AskingGuidance']), scrubber: scrubber)) %> + </div> + </div> +<% end %> + +<% if @post.errors.any? %> + <div class="notice is-danger is-filled"> + <p>The following errors prevented your post being saved:</p> + <ul> + <% @post.errors.full_messages.each do |msg| %> + <li><%= msg %></li> + <% end %> + </ul> + </div> +<% end %> + +<%= render 'form', post: @post, post_type: @post_type, category: @category, submit_path: request.path, + edit_comment: true, parent: @post.parent, type_summary: false %> \ No newline at end of file diff --git a/app/views/posts/edit_help.html.erb b/app/views/posts/edit_help.html.erb deleted file mode 100644 index 1122a1799bcacc0a265c803b9a1483fee671438f..0000000000000000000000000000000000000000 --- a/app/views/posts/edit_help.html.erb +++ /dev/null @@ -1,39 +0,0 @@ -<% content_for :title, "Editing '#{@post.title.truncate(50)}'" %> - -<% if @post.errors.any? %> - <div class="notice is-danger"> - These errors prevented this post being saved: - <ul> - <% @post.errors.full_messages.each do |msg| %> - <li><%= msg %></li> - <% end %> - </ul> - </div> -<% end %> - -<h1>Edit <%= @post.policy_doc? ? 'Policy' : (@post.help_doc? ? 'Help Article' : 'Post') %></h1> -<%= form_for @post, url: update_help_post_path(@post) do |f| %> - <div class="form-group"> - <%= f.label :title, "Title your post:", class: "form-element" %> - <%= f.text_field :title, class: "form-element" %> - </div> - <div class="form-group"> - <%= f.label :body_markdown, 'Body', class: "form-element" %> - <%= f.text_area :body_markdown, { class: "form-element is-large post-field", rows: 15 } %> - </div> - <div class="form-group"> - <%= f.label :help_category, 'Category', class: 'form-element' %> - <span class="form-caption"> - Name a category under which to display this post in the help center. - </span> - <%= f.text_field :help_category, class: 'form-element' %> - </div> - <div class="form-group"> - <%= f.label :help_ordering, 'Order', class: 'form-element' %> - <span class="form-caption"> - Control where this post appears in the list of help articles. Higher values appear later in the list. - </span> - <%= f.number_field :help_ordering, class: 'form-element' %> - </div> - <%= f.submit "Update", class: "button is-filled is-very-large" %> -<% end %> diff --git a/app/views/posts/new.html.erb b/app/views/posts/new.html.erb index 1db9ccf3fdc9292ecaef382020eb94e25d15532c..c7a1a1fbfecfbb11c5c5754aaa9ba7515df2ecfb 100644 --- a/app/views/posts/new.html.erb +++ b/app/views/posts/new.html.erb @@ -1,5 +1,30 @@ -<h1 class="has-margin-bottom-2">New Post in <%= @category.name %></h1> -<p class="has-color-tertiary-500">Not where you meant to post? See <%= link_to 'Categories', categories_path %></p> +<h1 class="has-margin-bottom-2"> + New Post + <% if @category.present? %> + in <%= @category.name %> + <% end %> +</h1> +<% if @category.present? %> + <p class="has-color-tertiary-500">Not where you meant to post? See <%= link_to 'Categories', categories_path %></p> -<%= 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 + <div class="notice is-info"> + <p><strong>Posting Tips</strong></p> + <div class="has-font-size-caption"> + <% guidance = @category.asking_guidance_override %> + <%= raw(sanitize(render_markdown(guidance.present? ? guidance : SiteSetting['AskingGuidance']), scrubber: scrubber)) %> + </div> + </div> +<% end %> + +<% if @post.errors.any? %> + <div class="notice is-danger is-filled"> + <p>The following errors prevented your post being saved:</p> + <ul> + <% @post.errors.full_messages.each do |msg| %> + <li><%= msg %></li> + <% end %> + </ul> + </div> +<% end %> + +<%= render 'form', post: @post, post_type: @post_type, category: @category, submit_path: request.path %> \ No newline at end of file diff --git a/app/views/posts/new_help.html.erb b/app/views/posts/new_help.html.erb deleted file mode 100644 index a65f5646e2e9fd1f34e36a58b936b6324a62d387..0000000000000000000000000000000000000000 --- a/app/views/posts/new_help.html.erb +++ /dev/null @@ -1,55 +0,0 @@ -<% content_for :title, "New Policy Document" %> - -<% if @post.errors.any? %> - <div class="notice is-danger"> - These errors prevented this post being saved: - <ul> - <% @post.errors.full_messages.each do |msg| %> - <li><%= msg %></li> - <% end %> - </ul> - </div> -<% end %> - -<h1>New Policy Document</h1> -<p> - If you're a moderator, you may use this page to create help documents that you can link to from /help/<document>. - If you're an administrator, you can also create policy documents, including legal documents. -</p> -<%= form_for @post, url: create_help_post_path do |f| %> - <div class="form-group"> - <%= f.label :post_type_id, 'Post type', class: "form-element" %> - <%= f.select :post_type_id, options_for_select(current_user.is_admin ? - [['Policy', PolicyDoc.post_type_id], ['Help', HelpDoc.post_type_id]] : - [['Help', HelpDoc.post_type_id]]), - { include_blank: true }, class: 'form-element' %> - </div> - <div class="form-group"> - <%= f.label :doc_slug, 'URL slug', class: "form-element" %> - <span class="form-caption">In a URL of "https://yoursite.codidact.com/help/topic", the "topic" is the slug.</span> - <%= f.text_field :doc_slug, class: 'form-element' %> - </div> - <div class="form-group"> - <%= f.label :title, "Title your post:", class: "form-element" %> - <%= f.text_field :title, class: "form-element" %> - </div> - <div class="form-group"> - <%= f.label :body_markdown, 'Body', class: "form-element" %> - <%= f.text_area :body_markdown, { class: "form-element post-field", rows: 15 } %> - </div> - <div class="form-group"> - <%= f.label :help_category, 'Category', class: 'form-element' %> - <span class="form-caption"> - Name a category under which to display this post in the help center. - </span> - <%= f.text_field :help_category, class: 'form-element' %> - </div> - <div class="form-group"> - <%= f.label :help_ordering, 'Order', class: 'form-element' %> - <span class="form-caption"> - Control where this post appears in the list of help articles. Higher values appear later in the list. - </span> - <%= f.number_field :help_ordering, class: 'form-element' %> - </div> - <%= f.submit "Create", class: "button is-filled is-very-large" %> -<% end %> diff --git a/app/views/posts/show.html.erb b/app/views/posts/show.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..91fc1a3c34a0872af3e45aa922073fcf87be644a --- /dev/null +++ b/app/views/posts/show.html.erb @@ -0,0 +1,52 @@ +<% content_for :title, @post.title.truncate(50) %> +<% content_for :description do %> + <% Rails.cache.fetch "posts/#{@post.id}/description" do %> + <%= @post.body_plain[0..74].strip %>... + <%= @post.children.any? ? @post.children.first.body_plain[0..74].strip : '' %> + <% end %> +<% end %> + +<% content_for :twitter_card_meta do %> + <meta name="twitter:card" content="summary" /> + <% if @post.user.twitter.present? %> + <meta name="twitter:creator" content="@<%= @post.user.twitter %>" /> + <% end %> + <meta property="og:url" content="<%= post_url(@post) %>" /> + <meta property="og:title" content="<%= @post.title %>" /> + <meta property="og:description" content="<%= @post.body_plain[0..150].strip %>..." /> + <meta property="og:image" content="<%= "https://#{RequestContext.community.host}#{SiteSetting['SiteLogoPath']}" %>" /> +<% end %> + +<%= render 'posts/expanded', post: @post %> + +<% if @post.post_type.has_answers %> + <h2><%= pluralize(@post.children.where(deleted: false).count, 'answer') %></h2> + + <div class="button-list is-gutterless has-float-right"> + <a href="<%= query_url(sort: 'score') %>" class="button is-muted is-outlined <%= params[:sort].nil? || params[:sort] == 'score' ? 'is-active' : '' %>">Score</a> + <a href="<%= query_url(sort: 'age') %>" class="button is-muted is-outlined <%= params[:sort] == 'age' ? 'is-active' : '' %>">Active</a> + </div> + + <div class="has-clear-clear"></div> + + <% @children.each do |answer| %> + <%= render 'posts/expanded', post: answer %> + <% end %> + + <div class="text-center"> + <%= will_paginate @children, renderer: BootstrapPagination::Rails %> + </div> + + <% if user_signed_in? && !@post.closed %> + <h2>Your Answer</h2> + <%= render 'posts/form', post: Post.new(post_type_id: Answer.post_type_id, category: @post.category, + parent: @post), type_summary: false, + category: @post.category, post_type: PostType['Answer'], inline_parent: false, + submit_path: create_response_path(post_type: PostType['Answer'], parent: @post) %> + <% elsif !@post.closed %> + <p class="has-margin-top-4"></p> + <%= link_to new_user_registration_path, class: 'button is-filled is-success h-m-t-4' do %> + Sign up to answer this question » + <% end %> + <% end %> +<% end %> diff --git a/app/views/questions/_form.html.erb b/app/views/questions/_form.html.erb deleted file mode 100644 index 9b750e5cda2e870130533fccd0ae1b5e5e8ee54f..0000000000000000000000000000000000000000 --- a/app/views/questions/_form.html.erb +++ /dev/null @@ -1,52 +0,0 @@ -<% is_edit ||= false %> - -<%= render 'posts/markdown_script' %> - -<% if @question.errors.any? %> - <div class="notice is-danger is-filled"> - The following errors prevented this post from being saved: - <ul> - <% @question.errors.full_messages.each do |msg| %> - <li><%= msg %></li> - <% end %> - </ul> - </div> -<% end %> - -<%= render 'posts/image_upload' %> - -<%= form_for @question, url: is_edit ? edit_question_path(@question) : create_question_path do |f| %> - <div class="form-group"> - <%= f.label :title, "Title your question:", class: "form-element" %> - <%= f.text_field :title, class: "form-element" %> - </div> - - <%= render 'shared/body_field', f: f, field_name: :body_markdown, field_label: 'Body', post: @question %> - - <div class="post-preview"></div> - - <div class="form-group"> - <%= f.label :tags_cache, "Tags", class: "form-element" %> - <div class="form-caption"> - Tags help to categorize questions. Separate them by space. Use hyphens for multiple-word tags. - </div> - <%= f.select :tags_cache, options_for_select(@question.tags_cache.map { |t| [t, t] }, selected: @question.tags_cache), - { include_blank: true }, multiple: true, class: "form-element js-tag-select", - data: { tag_set: @question.category.tag_set.id } %> - </div> - - <% if is_edit %> - <div class="form-group"> - <%= label_tag :edit_comment, 'Edit comment', class: "form-element" %> - <div class="form-caption"> - Describe—if necessary—what you are changing and why you are making this edit. - </div> - <%= text_field_tag :edit_comment, params[:edit_comment], class: 'form-element' %> - </div> - <% end %> - - <div class="form-group"> - <%= f.submit is_edit ? (check_your_post_privilege(@question, 'edit_posts') ? "Save changes" : "Suggest changes") : 'Ask Question', class: "button is-filled" %> - <%= link_to 'Cancel', is_edit ? question_path(@question) : category_path(@question.category), class: 'button is-outlined is-muted' %> - </div> -<% end %> diff --git a/app/views/questions/edit.html.erb b/app/views/questions/edit.html.erb deleted file mode 100644 index 11c572979cc528cad38e7dacba10f9f715f42f74..0000000000000000000000000000000000000000 --- a/app/views/questions/edit.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -<% content_for :title, "Editing '#{@question.title.truncate(50)}'" %> - -<h1><%= check_your_post_privilege(@question, 'edit_posts') ? "Edit Question" : "Suggest Edit for Question" %></h1> -<%= render 'form', is_edit: true %> - -<% if SiteSetting['AskingGuidance'] %> - <% content_for :sidebar do %> - <div class="widget has-margin-4"> - <h4 class="widget--header has-margin-0">Hints and Tips</h4> - <div class="widget--body"> - <% guidance = @question.category.asking_guidance_override || SiteSetting['AskingGuidance'] %> - <%= raw(sanitize(render_markdown(guidance), scrubber: scrubber)) %> - </div> - </div> - <% end %> -<% end %> diff --git a/app/views/questions/index.html.erb b/app/views/questions/index.html.erb deleted file mode 100644 index 62a3f553378f8b25701a4ecd1b71865d0c28e065..0000000000000000000000000000000000000000 --- a/app/views/questions/index.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -<% content_for :title, "Questions" %> -<% content_for :head do %> - <link rel="alternate" type="application/rss+xml" title="Questions RSS Feed" href="/questions/feed.rss"> -<% end %> -<% content_for :sidebar do %> - -<div class="has-margin-4 widget has-border-style-none has-shadow-0"> - <div class="widget--body has-text-align-right"> - - <%= link_to new_question_path, class: 'button is-outlined is-very-large' do %> - Ask Question - <% end %> - - </div> -</div> -<% end %> - -<h1>Questions</h1> - -<%= render 'list' %> diff --git a/app/views/questions/lottery.html.erb b/app/views/questions/lottery.html.erb index 89bc45a4f863e353ed4a1cca620312a430ba718f..4fc8d95197ef988e9efd8edd2e7e8e1fdf31e952 100644 --- a/app/views/questions/lottery.html.erb +++ b/app/views/questions/lottery.html.erb @@ -1,16 +1,4 @@ -<% content_for :title, "Questions Lottery" %> -<% content_for :sidebar do %> - -<div class="has-margin-4 widget has-border-style-none has-shadow-0"> - <div class="widget--body has-text-align-right"> - - <%= link_to new_question_path, class: 'button is-filled is-very-large is-outlined' do %> - Ask Question - <% end %> - - </div> -</div> -<% end %> +<% content_for :title, 'Questions Lottery' %> <h1>Questions</h1> diff --git a/app/views/questions/meta.html.erb b/app/views/questions/meta.html.erb deleted file mode 100644 index 203948500c42ebabc7c24e7ef18ceaf9f7e0b201..0000000000000000000000000000000000000000 --- a/app/views/questions/meta.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -<% content_for :title, "Meta Questions" %> -<% content_for :sidebar do %> - -<div class="has-margin-4 widget has-border-style-none has-shadow-0"> - <div class="widget--body has-text-align-right"> - - <%= link_to new_meta_question_path, class: 'button is-outlined is-muted is-very-large' do %> - Ask Meta Question - <% end %> - - </div> -</div> -<% end %> - -<h1>Meta Questions</h1> - -<%= render 'list' %> diff --git a/app/views/questions/new.html.erb b/app/views/questions/new.html.erb deleted file mode 100644 index a9484f626b8c25ce48b2500264e1ed18d21aed37..0000000000000000000000000000000000000000 --- a/app/views/questions/new.html.erb +++ /dev/null @@ -1,33 +0,0 @@ -<% content_for :title, "Ask a Question" %> - -<h1>Ask a Question</h1> - -<div class="notice"> - <i class="fas fa-info-circle"></i> You're asking a new question on the <strong>main site</strong>, which is - for questions on the topic of the site. If you want to post feedback or start a discussion about the site itself, - you'll need to <%= link_to 'ask a Meta question instead', new_meta_question_path %>. -</div> - -<% if @question.errors.any? %> - <div class="notice is-danger"> - <p>The following <%= "error".pluralize(@question.errors.count) %> prevented the question from being posted:</p> - <ul> - <% @question.errors.full_messages.each do |e| %> - <li><%= e %></li> - <% end %> - </ul> - </div> -<% end %> - -<%= render 'form' %> - -<% if SiteSetting['AskingGuidance'] %> - <% content_for :sidebar do %> - <div class="widget has-margin-4"> - <h4 class="widget--header has-margin-0">Hints and Tips</h4> - <div class="widget--body"> - <%= raw(sanitize(render_markdown(SiteSetting['AskingGuidance']), scrubber: scrubber)) %> - </div> - </div> - <% end %> -<% end %> diff --git a/app/views/questions/new_meta.html.erb b/app/views/questions/new_meta.html.erb deleted file mode 100644 index 4a7918e72ab38f70c970943c56622a51c7aa111d..0000000000000000000000000000000000000000 --- a/app/views/questions/new_meta.html.erb +++ /dev/null @@ -1,35 +0,0 @@ -<% content_for :title, "Ask a Meta Question" %> - -<h1>Ask a Meta Question</h1> - -<div class="notice"> - <i class="fas fa-info-circle"></i> You're asking a new question on <strong>Meta</strong>, our area for feedback - and discussions about this site itself. If you're intending to ask a topical question on the main site, you're in - the wrong place - you can do that <%= link_to 'here instead', new_question_path %>. -</div> - -<% if @question.errors.any? %> - <div class="notice is-error"> - <p>The following <%= "error".pluralize(@question.errors.count) %> prevented the question from being posted:</p> - <ul> - <% @question.errors.full_messages.each do |e| %> - <li><%= e %></li> - <% end %> - </ul> - </div> -<% end %> - -<%= render 'posts/image_upload' %> - -<%= render 'form', meta?: true %> - -<% if SiteSetting['AskingGuidance'] %> - <% content_for :sidebar do %> - <div class="widget has-margin-4"> - <h4 class="widget--header has-margin-0">Hints and Tips</h4> - <div class="widget--body"> - <%= raw(sanitize(render_markdown(SiteSetting['AskingGuidance']), scrubber: scrubber)) %> - </div> - </div> - <% end %> -<% end %> diff --git a/app/views/questions/show.html.erb b/app/views/questions/show.html.erb deleted file mode 100644 index 34eacd7ba04d6a7bcd68dbfcd888f135c47deb19..0000000000000000000000000000000000000000 --- a/app/views/questions/show.html.erb +++ /dev/null @@ -1,46 +0,0 @@ -<% content_for :title, @question.title.truncate(50) %> -<% content_for :description do %> - <% Rails.cache.fetch "posts/#{@question.id}/description" do %> - <%= @question.body_plain[0..74].strip %>... - <%= @answers.any? ? @answers.first.body_plain[0..74].strip : '' %> - <% end %> -<% end %> - -<% content_for :twitter_card_meta do %> - <meta name="twitter:card" content="summary" /> - <% if @question.user.twitter.present? %> - <meta name="twitter:creator" content="@<%= @question.user.twitter %>" /> - <% end %> - <meta property="og:url" content="<%= question_url(@question) %>" /> - <meta property="og:title" content="<%= @question.title %>" /> - <meta property="og:description" content="<%= @question.body_plain[0..150].strip %>..." /> - <meta property="og:image" content="<%= "https://#{RequestContext.community.host}#{SiteSetting['SiteLogoPath']}" %>" /> -<% end %> - -<%= render 'posts/expanded', post: @question %> - -<h2><%= pluralize(@question.answers.where(deleted: false).count, "answer") %></h2> - -<div class="button-list is-gutterless has-float-right"> - <a href="<%= query_url(sort: 'score') %>" class="button is-muted is-outlined <%= params[:sort].nil? || params[:sort] == 'score' ? 'is-active' : '' %>">Score</a> - <a href="<%= query_url(sort: 'age') %>" class="button is-muted is-outlined <%= params[:sort] == 'age' ? 'is-active' : '' %>">Active</a> -</div> - -<div class="has-clear-clear"></div> - -<% @answers.each do |answer| %> - <%= render 'posts/expanded', post: answer %> -<% end %> - -<div class="text-center"> - <%= will_paginate @answers, renderer: BootstrapPagination::Rails %> -</div> - -<% if user_signed_in? && !@question.closed %> - <%= render 'answers/new', answer: Answer.new, parent: @question %> -<% elsif !@question.closed %> - <p class="has-margin-top-4"></p> - <%= link_to new_user_registration_path, class: 'button is-filled is-success h-m-t-4' do %> - Sign up to answer this question » - <% end %> -<% end %> diff --git a/app/views/questions/tagged.html.erb b/app/views/questions/tagged.html.erb deleted file mode 100644 index 2aa7fe81eff2e4dc180ee7291349292ada569157..0000000000000000000000000000000000000000 --- a/app/views/questions/tagged.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -<% content_for :title, "Tag: #{params[:tag]}" %> - -<h3>Questions tagged <%= params[:tag] %></h3> - -<div class="item-list"> - <% @questions.each do |question| %> - <%= render 'posts/type_agnostic', post: question %> - <% end %> -</div><br/> - -<%= will_paginate @questions, renderer: BootstrapPagination::Rails %> - -<div> - <%= link_to 'Subscribe to questions in this tag', - new_subscription_path(type: 'tag', qualifier: params[:tag], return_to: request.path) %> -</div> diff --git a/app/views/subscription_mailer/subscription.html.erb b/app/views/subscription_mailer/subscription.html.erb index 399de797299d9a6a517bf3eabc2dc48404aa7c3b..c3ee467ea001fd78ded14b372d7b9bd7a62bb0dc 100644 --- a/app/views/subscription_mailer/subscription.html.erb +++ b/app/views/subscription_mailer/subscription.html.erb @@ -8,7 +8,7 @@ <% @questions.each do |question| %> <h3 class="question-title"> score <%= " +-"[question.score <=> 0] + question.score.to_s %>: - <%= link_to question.title, question_url(question, host: @subscription.community.host) %> + <%= link_to question.title, post_url(question, host: @subscription.community.host) %> </h3> <p> <%= question.body.first(150).gsub(/<\/?[^>]+>/, '') %> diff --git a/app/views/subscription_mailer/subscription.text.erb b/app/views/subscription_mailer/subscription.text.erb index 7c2ddbea5114beec015504edc1324969b7b976ca..c18d661a9263488dc70f87c9c2e76e27fea5c1c3 100644 --- a/app/views/subscription_mailer/subscription.text.erb +++ b/app/views/subscription_mailer/subscription.text.erb @@ -3,7 +3,7 @@ <% @questions.each do |question| %> score <%= " +-"[question.score <=> 0] + question.score.to_s %>: <%= question.title %> -<%= question_url(question, host: @subscription.community.host) %> +<%= post_url(question, host: @subscription.community.host) %> <%= question.body.first(150).gsub(/<\/?[^>]+>/, '') %><%= question.body.length > 150 ? '...' : '' %> — "<%= question.user.rtl_safe_username %>" (<%= user_url(question.user, host: @subscription.community.host) %>), diff --git a/app/views/subscriptions/new.html.erb b/app/views/subscriptions/new.html.erb index 9229f92c2410f34be58267eb3b9da112a50cb20d..3f4b7a895e86cb1b2b68fe3736513e990ad5218b 100644 --- a/app/views/subscriptions/new.html.erb +++ b/app/views/subscriptions/new.html.erb @@ -17,7 +17,7 @@ </p> <p>If you came here from a link and have filled in the details below, this may be a bug. Please let us know on - <%= link_to 'Meta', new_meta_question_path %>.</p> + <%= link_to 'Meta', 'https://meta.codidact.com/' %>.</p> </div> <% end %> diff --git a/codeclimate-config.patch b/codeclimate-config.patch deleted file mode 100644 index 503502907f2c530999296434fc6ec33cdcdb2d71..0000000000000000000000000000000000000000 --- a/codeclimate-config.patch +++ /dev/null @@ -1,1576 +0,0 @@ -diff --git a/.codeclimate.yml b/.codeclimate.yml -index e69de29..81df173 100644 ---- a/.codeclimate.yml -+++ b/.codeclimate.yml -@@ -0,0 +1,45 @@ -+--- -+engines: -+ brakeman: -+ enabled: true -+ bundler-audit: -+ enabled: true -+ csslint: -+ enabled: true -+ coffeelint: -+ enabled: true -+ duplication: -+ enabled: true -+ config: -+ languages: -+ - ruby -+ - javascript -+ - python -+ - php -+ eslint: -+ enabled: true -+ fixme: -+ enabled: true -+ rubocop: -+ enabled: true -+ratings: -+ paths: -+ - Gemfile.lock -+ - "**.erb" -+ - "**.haml" -+ - "**.rb" -+ - "**.rhtml" -+ - "**.slim" -+ - "**.css" -+ - "**.coffee" -+ - "**.inc" -+ - "**.js" -+ - "**.jsx" -+ - "**.module" -+ - "**.php" -+ - "**.py" -+exclude_paths: -+- config/ -+- db/ -+- test/ -+- vendor/ -diff --git a/.csslintrc b/.csslintrc -index e69de29..aacba95 100644 ---- a/.csslintrc -+++ b/.csslintrc -@@ -0,0 +1,2 @@ -+--exclude-exts=.min.css -+--ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes -diff --git a/.eslintignore b/.eslintignore -index e69de29..96212a3 100644 ---- a/.eslintignore -+++ b/.eslintignore -@@ -0,0 +1 @@ -+**/*{.,-}min.js -diff --git a/.eslintrc b/.eslintrc -index e69de29..9faa375 100644 ---- a/.eslintrc -+++ b/.eslintrc -@@ -0,0 +1,213 @@ -+ecmaFeatures: -+ modules: true -+ jsx: true -+ -+env: -+ amd: true -+ browser: true -+ es6: true -+ jquery: true -+ node: true -+ -+# http://eslint.org/docs/rules/ -+rules: -+ # Possible Errors -+ comma-dangle: [2, never] -+ no-cond-assign: 2 -+ no-console: 0 -+ no-constant-condition: 2 -+ no-control-regex: 2 -+ no-debugger: 2 -+ no-dupe-args: 2 -+ no-dupe-keys: 2 -+ no-duplicate-case: 2 -+ no-empty: 2 -+ no-empty-character-class: 2 -+ no-ex-assign: 2 -+ no-extra-boolean-cast: 2 -+ no-extra-parens: 0 -+ no-extra-semi: 2 -+ no-func-assign: 2 -+ no-inner-declarations: [2, functions] -+ no-invalid-regexp: 2 -+ no-irregular-whitespace: 2 -+ no-negated-in-lhs: 2 -+ no-obj-calls: 2 -+ no-regex-spaces: 2 -+ no-sparse-arrays: 2 -+ no-unexpected-multiline: 2 -+ no-unreachable: 2 -+ use-isnan: 2 -+ valid-jsdoc: 0 -+ valid-typeof: 2 -+ -+ # Best Practices -+ accessor-pairs: 2 -+ block-scoped-var: 0 -+ complexity: [2, 6] -+ consistent-return: 0 -+ curly: 0 -+ default-case: 0 -+ dot-location: 0 -+ dot-notation: 0 -+ eqeqeq: 2 -+ guard-for-in: 2 -+ no-alert: 2 -+ no-caller: 2 -+ no-case-declarations: 2 -+ no-div-regex: 2 -+ no-else-return: 0 -+ no-empty-label: 2 -+ no-empty-pattern: 2 -+ no-eq-null: 2 -+ no-eval: 2 -+ no-extend-native: 2 -+ no-extra-bind: 2 -+ no-fallthrough: 2 -+ no-floating-decimal: 0 -+ no-implicit-coercion: 0 -+ no-implied-eval: 2 -+ no-invalid-this: 0 -+ no-iterator: 2 -+ no-labels: 0 -+ no-lone-blocks: 2 -+ no-loop-func: 2 -+ no-magic-number: 0 -+ no-multi-spaces: 0 -+ no-multi-str: 0 -+ no-native-reassign: 2 -+ no-new-func: 2 -+ no-new-wrappers: 2 -+ no-new: 2 -+ no-octal-escape: 2 -+ no-octal: 2 -+ no-proto: 2 -+ no-redeclare: 2 -+ no-return-assign: 2 -+ no-script-url: 2 -+ no-self-compare: 2 -+ no-sequences: 0 -+ no-throw-literal: 0 -+ no-unused-expressions: 2 -+ no-useless-call: 2 -+ no-useless-concat: 2 -+ no-void: 2 -+ no-warning-comments: 0 -+ no-with: 2 -+ radix: 2 -+ vars-on-top: 0 -+ wrap-iife: 2 -+ yoda: 0 -+ -+ # Strict -+ strict: 0 -+ -+ # Variables -+ init-declarations: 0 -+ no-catch-shadow: 2 -+ no-delete-var: 2 -+ no-label-var: 2 -+ no-shadow-restricted-names: 2 -+ no-shadow: 0 -+ no-undef-init: 2 -+ no-undef: 0 -+ no-undefined: 0 -+ no-unused-vars: 0 -+ no-use-before-define: 0 -+ -+ # Node.js and CommonJS -+ callback-return: 2 -+ global-require: 2 -+ handle-callback-err: 2 -+ no-mixed-requires: 0 -+ no-new-require: 0 -+ no-path-concat: 2 -+ no-process-exit: 2 -+ no-restricted-modules: 0 -+ no-sync: 0 -+ -+ # Stylistic Issues -+ array-bracket-spacing: 0 -+ block-spacing: 0 -+ brace-style: 0 -+ camelcase: 0 -+ comma-spacing: 0 -+ comma-style: 0 -+ computed-property-spacing: 0 -+ consistent-this: 0 -+ eol-last: 0 -+ func-names: 0 -+ func-style: 0 -+ id-length: 0 -+ id-match: 0 -+ indent: 0 -+ jsx-quotes: 0 -+ key-spacing: 0 -+ linebreak-style: 0 -+ lines-around-comment: 0 -+ max-depth: 0 -+ max-len: 0 -+ max-nested-callbacks: 0 -+ max-params: 0 -+ max-statements: [2, 30] -+ new-cap: 0 -+ new-parens: 0 -+ newline-after-var: 0 -+ no-array-constructor: 0 -+ no-bitwise: 0 -+ no-continue: 0 -+ no-inline-comments: 0 -+ no-lonely-if: 0 -+ no-mixed-spaces-and-tabs: 0 -+ no-multiple-empty-lines: 0 -+ no-negated-condition: 0 -+ no-nested-ternary: 0 -+ no-new-object: 0 -+ no-plusplus: 0 -+ no-restricted-syntax: 0 -+ no-spaced-func: 0 -+ no-ternary: 0 -+ no-trailing-spaces: 0 -+ no-underscore-dangle: 0 -+ no-unneeded-ternary: 0 -+ object-curly-spacing: 0 -+ one-var: 0 -+ operator-assignment: 0 -+ operator-linebreak: 0 -+ padded-blocks: 0 -+ quote-props: 0 -+ quotes: 0 -+ require-jsdoc: 0 -+ semi-spacing: 0 -+ semi: 0 -+ sort-vars: 0 -+ space-after-keywords: 0 -+ space-before-blocks: 0 -+ space-before-function-paren: 0 -+ space-before-keywords: 0 -+ space-in-parens: 0 -+ space-infix-ops: 0 -+ space-return-throw-case: 0 -+ space-unary-ops: 0 -+ spaced-comment: 0 -+ wrap-regex: 0 -+ -+ # ECMAScript 6 -+ arrow-body-style: 0 -+ arrow-parens: 0 -+ arrow-spacing: 0 -+ constructor-super: 0 -+ generator-star-spacing: 0 -+ no-arrow-condition: 0 -+ no-class-assign: 0 -+ no-const-assign: 0 -+ no-dupe-class-members: 0 -+ no-this-before-super: 0 -+ no-var: 0 -+ object-shorthand: 0 -+ prefer-arrow-callback: 0 -+ prefer-const: 0 -+ prefer-reflect: 0 -+ prefer-spread: 0 -+ prefer-template: 0 -+ require-yield: 0 -diff --git a/.rubocop.yml b/.rubocop.yml -index e69de29..3f1d222 100644 ---- a/.rubocop.yml -+++ b/.rubocop.yml -@@ -0,0 +1,1156 @@ -+AllCops: -+ DisabledByDefault: true -+ -+#################### Lint ################################ -+ -+Lint/AmbiguousOperator: -+ Description: >- -+ Checks for ambiguous operators in the first argument of a -+ method invocation without parentheses. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-as-args' -+ Enabled: true -+ -+Lint/AmbiguousRegexpLiteral: -+ Description: >- -+ Checks for ambiguous regexp literals in the first argument of -+ a method invocation without parenthesis. -+ Enabled: true -+ -+Lint/AssignmentInCondition: -+ Description: "Don't use assignment in conditions." -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition' -+ Enabled: true -+ -+Lint/BlockAlignment: -+ Description: 'Align block ends correctly.' -+ Enabled: true -+ -+Lint/CircularArgumentReference: -+ Description: "Don't refer to the keyword argument in the default value." -+ Enabled: true -+ -+Lint/ConditionPosition: -+ Description: >- -+ Checks for condition placed in a confusing position relative to -+ the keyword. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#same-line-condition' -+ Enabled: true -+ -+Lint/Debugger: -+ Description: 'Check for debugger calls.' -+ Enabled: true -+ -+Lint/DefEndAlignment: -+ Description: 'Align ends corresponding to defs correctly.' -+ Enabled: true -+ -+Lint/DeprecatedClassMethods: -+ Description: 'Check for deprecated class method calls.' -+ Enabled: true -+ -+Lint/DuplicateMethods: -+ Description: 'Check for duplicate methods calls.' -+ Enabled: true -+ -+Lint/EachWithObjectArgument: -+ Description: 'Check for immutable argument given to each_with_object.' -+ Enabled: true -+ -+Lint/ElseLayout: -+ Description: 'Check for odd code arrangement in an else block.' -+ Enabled: true -+ -+Lint/EmptyEnsure: -+ Description: 'Checks for empty ensure block.' -+ Enabled: true -+ -+Lint/EmptyInterpolation: -+ Description: 'Checks for empty string interpolation.' -+ Enabled: true -+ -+Lint/EndAlignment: -+ Description: 'Align ends correctly.' -+ Enabled: true -+ -+Lint/EndInMethod: -+ Description: 'END blocks should not be placed inside method definitions.' -+ Enabled: true -+ -+Lint/EnsureReturn: -+ Description: 'Do not use return in an ensure block.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-return-ensure' -+ Enabled: true -+ -+Lint/Eval: -+ Description: 'The use of eval represents a serious security risk.' -+ Enabled: true -+ -+Lint/FormatParameterMismatch: -+ Description: 'The number of parameters to format/sprint must match the fields.' -+ Enabled: true -+ -+Lint/HandleExceptions: -+ Description: "Don't suppress exception." -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions' -+ Enabled: true -+ -+Lint/InvalidCharacterLiteral: -+ Description: >- -+ Checks for invalid character literals with a non-escaped -+ whitespace character. -+ Enabled: true -+ -+Lint/LiteralInCondition: -+ Description: 'Checks of literals used in conditions.' -+ Enabled: true -+ -+Lint/LiteralInInterpolation: -+ Description: 'Checks for literals used in interpolation.' -+ Enabled: true -+ -+Lint/Loop: -+ Description: >- -+ Use Kernel#loop with break rather than begin/end/until or -+ begin/end/while for post-loop tests. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#loop-with-break' -+ Enabled: true -+ -+Lint/NestedMethodDefinition: -+ Description: 'Do not use nested method definitions.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-methods' -+ Enabled: true -+ -+Lint/NonLocalExitFromIterator: -+ Description: 'Do not use return in iterator to cause non-local exit.' -+ Enabled: true -+ -+Lint/ParenthesesAsGroupedExpression: -+ Description: >- -+ Checks for method calls with a space before the opening -+ parenthesis. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' -+ Enabled: true -+ -+Lint/RequireParentheses: -+ Description: >- -+ Use parentheses in the method call to avoid confusion -+ about precedence. -+ Enabled: true -+ -+Lint/RescueException: -+ Description: 'Avoid rescuing the Exception class.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-blind-rescues' -+ Enabled: true -+ -+Lint/ShadowingOuterLocalVariable: -+ Description: >- -+ Do not use the same name as outer local variable -+ for block arguments or block local variables. -+ Enabled: true -+ -+Lint/StringConversionInInterpolation: -+ Description: 'Checks for Object#to_s usage in string interpolation.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-to-s' -+ Enabled: true -+ -+Lint/UnderscorePrefixedVariableName: -+ Description: 'Do not use prefix `_` for a variable that is used.' -+ Enabled: true -+ -+Lint/UnneededDisable: -+ Description: >- -+ Checks for rubocop:disable comments that can be removed. -+ Note: this cop is not disabled when disabling all cops. -+ It must be explicitly disabled. -+ Enabled: true -+ -+Lint/UnusedBlockArgument: -+ Description: 'Checks for unused block arguments.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' -+ Enabled: true -+ -+Lint/UnusedMethodArgument: -+ Description: 'Checks for unused method arguments.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' -+ Enabled: true -+ -+Lint/UnreachableCode: -+ Description: 'Unreachable code.' -+ Enabled: true -+ -+Lint/UselessAccessModifier: -+ Description: 'Checks for useless access modifiers.' -+ Enabled: true -+ -+Lint/UselessAssignment: -+ Description: 'Checks for useless assignment to a local variable.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' -+ Enabled: true -+ -+Lint/UselessComparison: -+ Description: 'Checks for comparison of something with itself.' -+ Enabled: true -+ -+Lint/UselessElseWithoutRescue: -+ Description: 'Checks for useless `else` in `begin..end` without `rescue`.' -+ Enabled: true -+ -+Lint/UselessSetterCall: -+ Description: 'Checks for useless setter call to a local variable.' -+ Enabled: true -+ -+Lint/Void: -+ Description: 'Possible use of operator/literal/variable in void context.' -+ Enabled: true -+ -+###################### Metrics #################################### -+ -+Metrics/AbcSize: -+ Description: >- -+ A calculated magnitude based on number of assignments, -+ branches, and conditions. -+ Reference: 'http://c2.com/cgi/wiki?AbcMetric' -+ Enabled: false -+ Max: 20 -+ -+Metrics/BlockNesting: -+ Description: 'Avoid excessive block nesting' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count' -+ Enabled: true -+ Max: 4 -+ -+Metrics/ClassLength: -+ Description: 'Avoid classes longer than 250 lines of code.' -+ Enabled: true -+ Max: 250 -+ -+Metrics/CyclomaticComplexity: -+ Description: >- -+ A complexity metric that is strongly correlated to the number -+ of test cases needed to validate a method. -+ Enabled: true -+ -+Metrics/LineLength: -+ Description: 'Limit lines to 80 characters.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits' -+ Enabled: false -+ -+Metrics/MethodLength: -+ Description: 'Avoid methods longer than 30 lines of code.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods' -+ Enabled: true -+ Max: 30 -+ -+Metrics/ModuleLength: -+ Description: 'Avoid modules longer than 250 lines of code.' -+ Enabled: true -+ Max: 250 -+ -+Metrics/ParameterLists: -+ Description: 'Avoid parameter lists longer than three or four parameters.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params' -+ Enabled: true -+ -+Metrics/PerceivedComplexity: -+ Description: >- -+ A complexity metric geared towards measuring complexity for a -+ human reader. -+ Enabled: false -+ -+##################### Performance ############################# -+ -+Performance/Count: -+ Description: >- -+ Use `count` instead of `select...size`, `reject...size`, -+ `select...count`, `reject...count`, `select...length`, -+ and `reject...length`. -+ Enabled: true -+ -+Performance/Detect: -+ Description: >- -+ Use `detect` instead of `select.first`, `find_all.first`, -+ `select.last`, and `find_all.last`. -+ Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerabledetect-vs-enumerableselectfirst-code' -+ Enabled: true -+ -+Performance/FlatMap: -+ Description: >- -+ Use `Enumerable#flat_map` -+ instead of `Enumerable#map...Array#flatten(1)` -+ or `Enumberable#collect..Array#flatten(1)` -+ Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablemaparrayflatten-vs-enumerableflat_map-code' -+ Enabled: true -+ EnabledForFlattenWithoutParams: false -+ # If enabled, this cop will warn about usages of -+ # `flatten` being called without any parameters. -+ # This can be dangerous since `flat_map` will only flatten 1 level, and -+ # `flatten` without any parameters can flatten multiple levels. -+ -+Performance/ReverseEach: -+ Description: 'Use `reverse_each` instead of `reverse.each`.' -+ Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablereverseeach-vs-enumerablereverse_each-code' -+ Enabled: true -+ -+Performance/Sample: -+ Description: >- -+ Use `sample` instead of `shuffle.first`, -+ `shuffle.last`, and `shuffle[Fixnum]`. -+ Reference: 'https://github.com/JuanitoFatas/fast-ruby#arrayshufflefirst-vs-arraysample-code' -+ Enabled: true -+ -+Performance/Size: -+ Description: >- -+ Use `size` instead of `count` for counting -+ the number of elements in `Array` and `Hash`. -+ Reference: 'https://github.com/JuanitoFatas/fast-ruby#arraycount-vs-arraysize-code' -+ Enabled: true -+ -+Performance/StringReplacement: -+ Description: >- -+ Use `tr` instead of `gsub` when you are replacing the same -+ number of characters. Use `delete` instead of `gsub` when -+ you are deleting characters. -+ Reference: 'https://github.com/JuanitoFatas/fast-ruby#stringgsub-vs-stringtr-code' -+ Enabled: true -+ -+##################### Rails ################################## -+ -+Rails/ActionFilter: -+ Description: 'Enforces consistent use of action filter methods.' -+ Enabled: false -+ -+Rails/Date: -+ Description: >- -+ Checks the correct usage of date aware methods, -+ such as Date.today, Date.current etc. -+ Enabled: false -+ -+Rails/Delegate: -+ Description: 'Prefer delegate method for delegations.' -+ Enabled: false -+ -+Rails/FindBy: -+ Description: 'Prefer find_by over where.first.' -+ Enabled: false -+ -+Rails/FindEach: -+ Description: 'Prefer all.find_each over all.find.' -+ Enabled: false -+ -+Rails/HasAndBelongsToMany: -+ Description: 'Prefer has_many :through to has_and_belongs_to_many.' -+ Enabled: false -+ -+Rails/Output: -+ Description: 'Checks for calls to puts, print, etc.' -+ Enabled: false -+ -+Rails/ReadWriteAttribute: -+ Description: >- -+ Checks for read_attribute(:attr) and -+ write_attribute(:attr, val). -+ Enabled: false -+ -+Rails/ScopeArgs: -+ Description: 'Checks the arguments of ActiveRecord scopes.' -+ Enabled: false -+ -+Rails/TimeZone: -+ Description: 'Checks the correct usage of time zone aware methods.' -+ StyleGuide: 'https://github.com/bbatsov/rails-style-guide#time' -+ Reference: 'http://danilenko.org/2012/7/6/rails_timezones' -+ Enabled: false -+ -+Rails/Validation: -+ Description: 'Use validates :attribute, hash of validations.' -+ Enabled: false -+ -+################## Style ################################# -+ -+Style/AccessModifierIndentation: -+ Description: Check indentation of private/protected visibility modifiers. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-public-private-protected' -+ Enabled: false -+ -+Style/AccessorMethodName: -+ Description: Check the naming of accessor methods for get_/set_. -+ Enabled: false -+ -+Style/Alias: -+ Description: 'Use alias_method instead of alias.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#alias-method' -+ Enabled: false -+ -+Style/AlignArray: -+ Description: >- -+ Align the elements of an array literal if they span more than -+ one line. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#align-multiline-arrays' -+ Enabled: false -+ -+Style/AlignHash: -+ Description: >- -+ Align the elements of a hash literal if they span more than -+ one line. -+ Enabled: false -+ -+Style/AlignParameters: -+ Description: >- -+ Align the parameters of a method call if they span more -+ than one line. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-double-indent' -+ Enabled: false -+ -+Style/AndOr: -+ Description: 'Use &&/|| instead of and/or.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-and-or-or' -+ Enabled: false -+ -+Style/ArrayJoin: -+ Description: 'Use Array#join instead of Array#*.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#array-join' -+ Enabled: false -+ -+Style/AsciiComments: -+ Description: 'Use only ascii symbols in comments.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-comments' -+ Enabled: false -+ -+Style/AsciiIdentifiers: -+ Description: 'Use only ascii symbols in identifiers.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-identifiers' -+ Enabled: false -+ -+Style/Attr: -+ Description: 'Checks for uses of Module#attr.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr' -+ Enabled: false -+ -+Style/BeginBlock: -+ Description: 'Avoid the use of BEGIN blocks.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-BEGIN-blocks' -+ Enabled: false -+ -+Style/BarePercentLiterals: -+ Description: 'Checks if usage of %() or %Q() matches configuration.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q-shorthand' -+ Enabled: false -+ -+Style/BlockComments: -+ Description: 'Do not use block comments.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-block-comments' -+ Enabled: false -+ -+Style/BlockEndNewline: -+ Description: 'Put end statement of multiline block on its own line.' -+ Enabled: false -+ -+Style/BlockDelimiters: -+ Description: >- -+ Avoid using {...} for multi-line blocks (multiline chaining is -+ always ugly). -+ Prefer {...} over do...end for single-line blocks. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' -+ Enabled: false -+ -+Style/BracesAroundHashParameters: -+ Description: 'Enforce braces style around hash parameters.' -+ Enabled: false -+ -+Style/CaseEquality: -+ Description: 'Avoid explicit use of the case equality operator(===).' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-case-equality' -+ Enabled: false -+ -+Style/CaseIndentation: -+ Description: 'Indentation of when in a case/when/[else/]end.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#indent-when-to-case' -+ Enabled: false -+ -+Style/CharacterLiteral: -+ Description: 'Checks for uses of character literals.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-character-literals' -+ Enabled: false -+ -+Style/ClassAndModuleCamelCase: -+ Description: 'Use CamelCase for classes and modules.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#camelcase-classes' -+ Enabled: false -+ -+Style/ClassAndModuleChildren: -+ Description: 'Checks style of children classes and modules.' -+ Enabled: false -+ -+Style/ClassCheck: -+ Description: 'Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.' -+ Enabled: false -+ -+Style/ClassMethods: -+ Description: 'Use self when defining module/class methods.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#def-self-class-methods' -+ Enabled: false -+ -+Style/ClassVars: -+ Description: 'Avoid the use of class variables.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-class-vars' -+ Enabled: false -+ -+Style/ClosingParenthesisIndentation: -+ Description: 'Checks the indentation of hanging closing parentheses.' -+ Enabled: false -+ -+Style/ColonMethodCall: -+ Description: 'Do not use :: for method call.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#double-colons' -+ Enabled: false -+ -+Style/CommandLiteral: -+ Description: 'Use `` or %x around command literals.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-x' -+ Enabled: false -+ -+Style/CommentAnnotation: -+ Description: 'Checks formatting of annotation comments.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#annotate-keywords' -+ Enabled: false -+ -+Style/CommentIndentation: -+ Description: 'Indentation of comments.' -+ Enabled: false -+ -+Style/ConstantName: -+ Description: 'Constants should use SCREAMING_SNAKE_CASE.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#screaming-snake-case' -+ Enabled: false -+ -+Style/DefWithParentheses: -+ Description: 'Use def with parentheses when there are arguments.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens' -+ Enabled: false -+ -+Style/DeprecatedHashMethods: -+ Description: 'Checks for use of deprecated Hash methods.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-key' -+ Enabled: false -+ -+Style/Documentation: -+ Description: 'Document classes and non-namespace modules.' -+ Enabled: false -+ -+Style/DotPosition: -+ Description: 'Checks the position of the dot in multi-line method calls.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains' -+ Enabled: false -+ -+Style/DoubleNegation: -+ Description: 'Checks for uses of double negation (!!).' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-bang-bang' -+ Enabled: false -+ -+Style/EachWithObject: -+ Description: 'Prefer `each_with_object` over `inject` or `reduce`.' -+ Enabled: false -+ -+Style/ElseAlignment: -+ Description: 'Align elses and elsifs correctly.' -+ Enabled: false -+ -+Style/EmptyElse: -+ Description: 'Avoid empty else-clauses.' -+ Enabled: false -+ -+Style/EmptyLineBetweenDefs: -+ Description: 'Use empty lines between defs.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#empty-lines-between-methods' -+ Enabled: false -+ -+Style/EmptyLines: -+ Description: "Don't use several empty lines in a row." -+ Enabled: false -+ -+Style/EmptyLinesAroundAccessModifier: -+ Description: "Keep blank lines around access modifiers." -+ Enabled: false -+ -+Style/EmptyLinesAroundBlockBody: -+ Description: "Keeps track of empty lines around block bodies." -+ Enabled: false -+ -+Style/EmptyLinesAroundClassBody: -+ Description: "Keeps track of empty lines around class bodies." -+ Enabled: false -+ -+Style/EmptyLinesAroundModuleBody: -+ Description: "Keeps track of empty lines around module bodies." -+ Enabled: false -+ -+Style/EmptyLinesAroundMethodBody: -+ Description: "Keeps track of empty lines around method bodies." -+ Enabled: false -+ -+Style/EmptyLiteral: -+ Description: 'Prefer literals to Array.new/Hash.new/String.new.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#literal-array-hash' -+ Enabled: false -+ -+Style/EndBlock: -+ Description: 'Avoid the use of END blocks.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-END-blocks' -+ Enabled: false -+ -+Style/EndOfLine: -+ Description: 'Use Unix-style line endings.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#crlf' -+ Enabled: false -+ -+Style/EvenOdd: -+ Description: 'Favor the use of Fixnum#even? && Fixnum#odd?' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' -+ Enabled: false -+ -+Style/ExtraSpacing: -+ Description: 'Do not use unnecessary spacing.' -+ Enabled: false -+ -+Style/FileName: -+ Description: 'Use snake_case for source file names.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files' -+ Enabled: false -+ -+Style/InitialIndentation: -+ Description: >- -+ Checks the indentation of the first non-blank non-comment line in a file. -+ Enabled: false -+ -+Style/FirstParameterIndentation: -+ Description: 'Checks the indentation of the first parameter in a method call.' -+ Enabled: false -+ -+Style/FlipFlop: -+ Description: 'Checks for flip flops' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops' -+ Enabled: false -+ -+Style/For: -+ Description: 'Checks use of for or each in multiline loops.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-for-loops' -+ Enabled: false -+ -+Style/FormatString: -+ Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#sprintf' -+ Enabled: false -+ -+Style/GlobalVars: -+ Description: 'Do not introduce global variables.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#instance-vars' -+ Reference: 'http://www.zenspider.com/Languages/Ruby/QuickRef.html' -+ Enabled: false -+ -+Style/GuardClause: -+ Description: 'Check for conditionals that can be replaced with guard clauses' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals' -+ Enabled: false -+ -+Style/HashSyntax: -+ Description: >- -+ Prefer Ruby 1.9 hash syntax { a: 1, b: 2 } over 1.8 syntax -+ { a: 1, b: 2 }. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-literals' -+ Enabled: false -+ -+Style/IfUnlessModifier: -+ Description: >- -+ Favor modifier if/unless usage when you have a -+ single-line body. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier' -+ Enabled: false -+ -+Style/IfWithSemicolon: -+ Description: 'Do not use if x; .... Use the ternary operator instead.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon-ifs' -+ Enabled: false -+ -+Style/IndentationConsistency: -+ Description: 'Keep indentation straight.' -+ Enabled: false -+ -+Style/IndentationWidth: -+ Description: 'Use 2 spaces for indentation.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation' -+ Enabled: false -+ -+Style/IndentArray: -+ Description: >- -+ Checks the indentation of the first element in an array -+ literal. -+ Enabled: false -+ -+Style/IndentHash: -+ Description: 'Checks the indentation of the first key in a hash literal.' -+ Enabled: false -+ -+Style/InfiniteLoop: -+ Description: 'Use Kernel#loop for infinite loops.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#infinite-loop' -+ Enabled: false -+ -+Style/Lambda: -+ Description: 'Use the new lambda literal syntax for single-line blocks.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#lambda-multi-line' -+ Enabled: false -+ -+Style/LambdaCall: -+ Description: 'Use lambda.call(...) instead of lambda.(...).' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc-call' -+ Enabled: false -+ -+Style/LeadingCommentSpace: -+ Description: 'Comments should start with a space.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space' -+ Enabled: false -+ -+Style/LineEndConcatenation: -+ Description: >- -+ Use \ instead of + or << to concatenate two string literals at -+ line end. -+ Enabled: false -+ -+Style/MethodCallParentheses: -+ Description: 'Do not use parentheses for method calls with no arguments.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens' -+ Enabled: false -+ -+Style/MethodDefParentheses: -+ Description: >- -+ Checks if the method definitions have or don't have -+ parentheses. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens' -+ Enabled: false -+ -+Style/MethodName: -+ Description: 'Use the configured style when naming methods.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars' -+ Enabled: false -+ -+Style/ModuleFunction: -+ Description: 'Checks for usage of `extend self` in modules.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#module-function' -+ Enabled: false -+ -+Style/MultilineBlockChain: -+ Description: 'Avoid multi-line chains of blocks.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' -+ Enabled: false -+ -+Style/MultilineBlockLayout: -+ Description: 'Ensures newlines after multiline block do statements.' -+ Enabled: false -+ -+Style/MultilineIfThen: -+ Description: 'Do not use then for multi-line if/unless.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-then' -+ Enabled: false -+ -+Style/MultilineOperationIndentation: -+ Description: >- -+ Checks indentation of binary operations that span more than -+ one line. -+ Enabled: false -+ -+Style/MultilineTernaryOperator: -+ Description: >- -+ Avoid multi-line ?: (the ternary operator); -+ use if/unless instead. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-ternary' -+ Enabled: false -+ -+Style/NegatedIf: -+ Description: >- -+ Favor unless over if for negative conditions -+ (or control flow or). -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#unless-for-negatives' -+ Enabled: false -+ -+Style/NegatedWhile: -+ Description: 'Favor until over while for negative conditions.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#until-for-negatives' -+ Enabled: false -+ -+Style/NestedTernaryOperator: -+ Description: 'Use one expression per branch in a ternary operator.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-ternary' -+ Enabled: false -+ -+Style/Next: -+ Description: 'Use `next` to skip iteration instead of a condition at the end.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals' -+ Enabled: false -+ -+Style/NilComparison: -+ Description: 'Prefer x.nil? to x == nil.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' -+ Enabled: false -+ -+Style/NonNilCheck: -+ Description: 'Checks for redundant nil checks.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-non-nil-checks' -+ Enabled: false -+ -+Style/Not: -+ Description: 'Use ! instead of not.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bang-not-not' -+ Enabled: false -+ -+Style/NumericLiterals: -+ Description: >- -+ Add underscores to large numeric literals to improve their -+ readability. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscores-in-numerics' -+ Enabled: false -+ -+Style/OneLineConditional: -+ Description: >- -+ Favor the ternary operator(?:) over -+ if/then/else/end constructs. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator' -+ Enabled: false -+ -+Style/OpMethod: -+ Description: 'When defining binary operators, name the argument other.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg' -+ Enabled: false -+ -+Style/OptionalArguments: -+ Description: >- -+ Checks for optional arguments that do not appear at the end -+ of the argument list -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#optional-arguments' -+ Enabled: false -+ -+Style/ParallelAssignment: -+ Description: >- -+ Check for simple usages of parallel assignment. -+ It will only warn when the number of variables -+ matches on both sides of the assignment. -+ This also provides performance benefits -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parallel-assignment' -+ Enabled: false -+ -+Style/ParenthesesAroundCondition: -+ Description: >- -+ Don't use parentheses around the condition of an -+ if/unless/while. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-parens-if' -+ Enabled: false -+ -+Style/PercentLiteralDelimiters: -+ Description: 'Use `%`-literal delimiters consistently' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-literal-braces' -+ Enabled: false -+ -+Style/PercentQLiterals: -+ Description: 'Checks if uses of %Q/%q match the configured preference.' -+ Enabled: false -+ -+Style/PerlBackrefs: -+ Description: 'Avoid Perl-style regex back references.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers' -+ Enabled: false -+ -+Style/PredicateName: -+ Description: 'Check the names of predicate methods.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark' -+ Enabled: false -+ -+Style/Proc: -+ Description: 'Use proc instead of Proc.new.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc' -+ Enabled: false -+ -+Style/RaiseArgs: -+ Description: 'Checks the arguments passed to raise/fail.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#exception-class-messages' -+ Enabled: false -+ -+Style/RedundantBegin: -+ Description: "Don't use begin blocks when they are not needed." -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#begin-implicit' -+ Enabled: false -+ -+Style/RedundantException: -+ Description: "Checks for an obsolete RuntimeException argument in raise/fail." -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-runtimeerror' -+ Enabled: false -+ -+Style/RedundantReturn: -+ Description: "Don't use return where it's not required." -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-return' -+ Enabled: false -+ -+Style/RedundantSelf: -+ Description: "Don't use self where it's not needed." -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-self-unless-required' -+ Enabled: false -+ -+Style/RegexpLiteral: -+ Description: 'Use / or %r around regular expressions.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-r' -+ Enabled: false -+ -+Style/RescueEnsureAlignment: -+ Description: 'Align rescues and ensures correctly.' -+ Enabled: false -+ -+Style/RescueModifier: -+ Description: 'Avoid using rescue in its modifier form.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-rescue-modifiers' -+ Enabled: false -+ -+Style/SelfAssignment: -+ Description: >- -+ Checks for places where self-assignment shorthand should have -+ been used. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#self-assignment' -+ Enabled: false -+ -+Style/Semicolon: -+ Description: "Don't use semicolons to terminate expressions." -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon' -+ Enabled: false -+ -+Style/SignalException: -+ Description: 'Checks for proper usage of fail and raise.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#fail-method' -+ Enabled: false -+ -+Style/SingleLineBlockParams: -+ Description: 'Enforces the names of some block params.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#reduce-blocks' -+ Enabled: false -+ -+Style/SingleLineMethods: -+ Description: 'Avoid single-line methods.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-single-line-methods' -+ Enabled: false -+ -+Style/SpaceBeforeFirstArg: -+ Description: >- -+ Checks that exactly one space is used between a method name -+ and the first argument for method calls without parentheses. -+ Enabled: true -+ -+Style/SpaceAfterColon: -+ Description: 'Use spaces after colons.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' -+ Enabled: false -+ -+Style/SpaceAfterComma: -+ Description: 'Use spaces after commas.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' -+ Enabled: false -+ -+Style/SpaceAroundKeyword: -+ Description: 'Use spaces around keywords.' -+ Enabled: false -+ -+Style/SpaceAfterMethodName: -+ Description: >- -+ Do not put a space between a method name and the opening -+ parenthesis in a method definition. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' -+ Enabled: false -+ -+Style/SpaceAfterNot: -+ Description: Tracks redundant space after the ! operator. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-bang' -+ Enabled: false -+ -+Style/SpaceAfterSemicolon: -+ Description: 'Use spaces after semicolons.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' -+ Enabled: false -+ -+Style/SpaceBeforeBlockBraces: -+ Description: >- -+ Checks that the left block brace has or doesn't have space -+ before it. -+ Enabled: false -+ -+Style/SpaceBeforeComma: -+ Description: 'No spaces before commas.' -+ Enabled: false -+ -+Style/SpaceBeforeComment: -+ Description: >- -+ Checks for missing space between code and a comment on the -+ same line. -+ Enabled: false -+ -+Style/SpaceBeforeSemicolon: -+ Description: 'No spaces before semicolons.' -+ Enabled: false -+ -+Style/SpaceInsideBlockBraces: -+ Description: >- -+ Checks that block braces have or don't have surrounding space. -+ For blocks taking parameters, checks that the left brace has -+ or doesn't have trailing space. -+ Enabled: false -+ -+Style/SpaceAroundBlockParameters: -+ Description: 'Checks the spacing inside and after block parameters pipes.' -+ Enabled: false -+ -+Style/SpaceAroundEqualsInParameterDefault: -+ Description: >- -+ Checks that the equals signs in parameter default assignments -+ have or don't have surrounding space depending on -+ configuration. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-around-equals' -+ Enabled: false -+ -+Style/SpaceAroundOperators: -+ Description: 'Use a single space around operators.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' -+ Enabled: false -+ -+Style/SpaceInsideBrackets: -+ Description: 'No spaces after [ or before ].' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces' -+ Enabled: false -+ -+Style/SpaceInsideHashLiteralBraces: -+ Description: "Use spaces inside hash literal braces - or don't." -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-operators' -+ Enabled: false -+ -+Style/SpaceInsideParens: -+ Description: 'No spaces after ( or before ).' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces' -+ Enabled: false -+ -+Style/SpaceInsideRangeLiteral: -+ Description: 'No spaces inside range literals.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-space-inside-range-literals' -+ Enabled: false -+ -+Style/SpaceInsideStringInterpolation: -+ Description: 'Checks for padding/surrounding spaces inside string interpolation.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#string-interpolation' -+ Enabled: false -+ -+Style/SpecialGlobalVars: -+ Description: 'Avoid Perl-style global variables.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms' -+ Enabled: false -+ -+Style/StringLiterals: -+ Description: 'Checks if uses of quotes match the configured preference.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-string-literals' -+ Enabled: false -+ -+Style/StringLiteralsInInterpolation: -+ Description: >- -+ Checks if uses of quotes inside expressions in interpolated -+ strings match the configured preference. -+ Enabled: false -+ -+Style/StructInheritance: -+ Description: 'Checks for inheritance from Struct.new.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-extend-struct-new' -+ Enabled: false -+ -+Style/SymbolLiteral: -+ Description: 'Use plain symbols instead of string symbols when possible.' -+ Enabled: false -+ -+Style/SymbolProc: -+ Description: 'Use symbols as procs instead of blocks when possible.' -+ Enabled: false -+ -+Style/Tab: -+ Description: 'No hard tabs.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation' -+ Enabled: false -+ -+Style/TrailingBlankLines: -+ Description: 'Checks trailing blank lines and final newline.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#newline-eof' -+ Enabled: false -+ -+Style/TrailingCommaInArguments: -+ Description: 'Checks for trailing comma in parameter lists.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-params-comma' -+ Enabled: false -+ -+Style/TrailingCommaInLiteral: -+ Description: 'Checks for trailing comma in literals.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' -+ Enabled: false -+ -+Style/TrailingWhitespace: -+ Description: 'Avoid trailing whitespace.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-whitespace' -+ Enabled: false -+ -+Style/TrivialAccessors: -+ Description: 'Prefer attr_* methods to trivial readers/writers.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family' -+ Enabled: false -+ -+Style/UnlessElse: -+ Description: >- -+ Do not use unless with else. Rewrite these with the positive -+ case first. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-else-with-unless' -+ Enabled: false -+ -+Style/UnneededCapitalW: -+ Description: 'Checks for %W when interpolation is not needed.' -+ Enabled: false -+ -+Style/UnneededPercentQ: -+ Description: 'Checks for %q/%Q when single quotes or double quotes would do.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q' -+ Enabled: false -+ -+Style/TrailingUnderscoreVariable: -+ Description: >- -+ Checks for the usage of unneeded trailing underscores at the -+ end of parallel variable assignment. -+ Enabled: false -+ -+Style/VariableInterpolation: -+ Description: >- -+ Don't interpolate global, instance and class variables -+ directly in strings. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#curlies-interpolate' -+ Enabled: false -+ -+Style/VariableName: -+ Description: 'Use the configured style when naming variables.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars' -+ Enabled: false -+ -+Style/WhenThen: -+ Description: 'Use when x then ... for one-line cases.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#one-line-cases' -+ Enabled: false -+ -+Style/WhileUntilDo: -+ Description: 'Checks for redundant do after while or until.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-multiline-while-do' -+ Enabled: false -+ -+Style/WhileUntilModifier: -+ Description: >- -+ Favor modifier while/until usage when you have a -+ single-line body. -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#while-as-a-modifier' -+ Enabled: false -+ -+Style/WordArray: -+ Description: 'Use %w or %W for arrays of words.' -+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-w' -+ Enabled: false -diff --git a/coffeelint.json b/coffeelint.json -index e69de29..86422e3 100644 ---- a/coffeelint.json -+++ b/coffeelint.json -@@ -0,0 +1,129 @@ -+{ -+ "arrow_spacing": { -+ "level": "ignore" -+ }, -+ "braces_spacing": { -+ "level": "ignore", -+ "spaces": 0, -+ "empty_object_spaces": 0 -+ }, -+ "camel_case_classes": { -+ "level": "error" -+ }, -+ "coffeescript_error": { -+ "level": "error" -+ }, -+ "colon_assignment_spacing": { -+ "level": "ignore", -+ "spacing": { -+ "left": 0, -+ "right": 0 -+ } -+ }, -+ "cyclomatic_complexity": { -+ "value": 10, -+ "level": "ignore" -+ }, -+ "duplicate_key": { -+ "level": "error" -+ }, -+ "empty_constructor_needs_parens": { -+ "level": "ignore" -+ }, -+ "ensure_comprehensions": { -+ "level": "warn" -+ }, -+ "eol_last": { -+ "level": "ignore" -+ }, -+ "indentation": { -+ "value": 2, -+ "level": "error" -+ }, -+ "line_endings": { -+ "level": "ignore", -+ "value": "unix" -+ }, -+ "max_line_length": { -+ "value": 80, -+ "level": "error", -+ "limitComments": true -+ }, -+ "missing_fat_arrows": { -+ "level": "ignore", -+ "is_strict": false -+ }, -+ "newlines_after_classes": { -+ "value": 3, -+ "level": "ignore" -+ }, -+ "no_backticks": { -+ "level": "error" -+ }, -+ "no_debugger": { -+ "level": "warn", -+ "console": false -+ }, -+ "no_empty_functions": { -+ "level": "ignore" -+ }, -+ "no_empty_param_list": { -+ "level": "ignore" -+ }, -+ "no_implicit_braces": { -+ "level": "ignore", -+ "strict": true -+ }, -+ "no_implicit_parens": { -+ "strict": true, -+ "level": "ignore" -+ }, -+ "no_interpolation_in_single_quotes": { -+ "level": "ignore" -+ }, -+ "no_plusplus": { -+ "level": "ignore" -+ }, -+ "no_stand_alone_at": { -+ "level": "ignore" -+ }, -+ "no_tabs": { -+ "level": "error" -+ }, -+ "no_this": { -+ "level": "ignore" -+ }, -+ "no_throwing_strings": { -+ "level": "error" -+ }, -+ "no_trailing_semicolons": { -+ "level": "error" -+ }, -+ "no_trailing_whitespace": { -+ "level": "error", -+ "allowed_in_comments": false, -+ "allowed_in_empty_lines": true -+ }, -+ "no_unnecessary_double_quotes": { -+ "level": "ignore" -+ }, -+ "no_unnecessary_fat_arrows": { -+ "level": "warn" -+ }, -+ "non_empty_constructor_needs_parens": { -+ "level": "ignore" -+ }, -+ "prefer_english_operator": { -+ "level": "ignore", -+ "doubleNotLevel": "ignore" -+ }, -+ "space_operators": { -+ "level": "ignore" -+ }, -+ "spacing_after_comma": { -+ "level": "ignore" -+ }, -+ "transform_messes_up_line_numbers": { -+ "level": "warn" -+ } -+} diff --git a/coffeelint.json b/coffeelint.json deleted file mode 100644 index 86422e323d1f407c05c048bb35d67e433101e758..0000000000000000000000000000000000000000 --- a/coffeelint.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "arrow_spacing": { - "level": "ignore" - }, - "braces_spacing": { - "level": "ignore", - "spaces": 0, - "empty_object_spaces": 0 - }, - "camel_case_classes": { - "level": "error" - }, - "coffeescript_error": { - "level": "error" - }, - "colon_assignment_spacing": { - "level": "ignore", - "spacing": { - "left": 0, - "right": 0 - } - }, - "cyclomatic_complexity": { - "value": 10, - "level": "ignore" - }, - "duplicate_key": { - "level": "error" - }, - "empty_constructor_needs_parens": { - "level": "ignore" - }, - "ensure_comprehensions": { - "level": "warn" - }, - "eol_last": { - "level": "ignore" - }, - "indentation": { - "value": 2, - "level": "error" - }, - "line_endings": { - "level": "ignore", - "value": "unix" - }, - "max_line_length": { - "value": 80, - "level": "error", - "limitComments": true - }, - "missing_fat_arrows": { - "level": "ignore", - "is_strict": false - }, - "newlines_after_classes": { - "value": 3, - "level": "ignore" - }, - "no_backticks": { - "level": "error" - }, - "no_debugger": { - "level": "warn", - "console": false - }, - "no_empty_functions": { - "level": "ignore" - }, - "no_empty_param_list": { - "level": "ignore" - }, - "no_implicit_braces": { - "level": "ignore", - "strict": true - }, - "no_implicit_parens": { - "strict": true, - "level": "ignore" - }, - "no_interpolation_in_single_quotes": { - "level": "ignore" - }, - "no_plusplus": { - "level": "ignore" - }, - "no_stand_alone_at": { - "level": "ignore" - }, - "no_tabs": { - "level": "error" - }, - "no_this": { - "level": "ignore" - }, - "no_throwing_strings": { - "level": "error" - }, - "no_trailing_semicolons": { - "level": "error" - }, - "no_trailing_whitespace": { - "level": "error", - "allowed_in_comments": false, - "allowed_in_empty_lines": true - }, - "no_unnecessary_double_quotes": { - "level": "ignore" - }, - "no_unnecessary_fat_arrows": { - "level": "warn" - }, - "non_empty_constructor_needs_parens": { - "level": "ignore" - }, - "prefer_english_operator": { - "level": "ignore", - "doubleNotLevel": "ignore" - }, - "space_operators": { - "level": "ignore" - }, - "spacing_after_comma": { - "level": "ignore" - }, - "transform_messes_up_line_numbers": { - "level": "warn" - } -} diff --git a/config/locales/strings/en.posts.yml b/config/locales/strings/en.posts.yml new file mode 100644 index 0000000000000000000000000000000000000000..541765be70799157868d796cb7c8fb62901da136 --- /dev/null +++ b/config/locales/strings/en.posts.yml @@ -0,0 +1,10 @@ +en: + posts: + category_low_trust_level: > + You don't have a high enough trust level to post in the :name category. + type_requires_category: > + You can't create a :type without a category. + type_requires_parent: > + You can't create a :type without a parent post. + not_public_editable: > + This type of post can only be edited by its author and the site moderators. \ No newline at end of file diff --git a/config/locales/strings/en.rate_limit.yml b/config/locales/strings/en.rate_limit.yml new file mode 100644 index 0000000000000000000000000000000000000000..c9ada65be06a98875031d4634deccff3eaf631fd --- /dev/null +++ b/config/locales/strings/en.rate_limit.yml @@ -0,0 +1,10 @@ +rate_limit: + new_user_posts: > + You may only post :count :level posts per day. Once you have some well-received posts, your limit will increase. + posts: > + You may only post :count :level posts per day. + new_user_suggested_edits: > + You may only suggest :count edits per day. Once you have some well-received contributions, your limit will increase. + suggested_edits: > + You may only suggest :count edits per day. Once you have enough well-received suggestions, you will earn the ability + to edit without review. \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 32f7fb19007f7020751834aca9b7ee556a0d66da..968af0e61643732befd039716aca7365fe327d7c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -62,54 +62,43 @@ Rails.application.routes.draw do patch ':id/edit', to: 'pinned_links#update', as: :update_pinned_link end - get 'questions', to: 'questions#index', as: :questions get 'questions/lottery', to: 'questions#lottery', as: :questions_lottery - get 'meta', to: 'questions#meta', as: :meta get 'questions/feed', to: 'questions#feed', as: :question_feed - get 'questions/ask', to: 'questions#new', as: :new_question - get 'meta/ask', to: 'questions#new_meta', as: :new_meta_question - post 'questions/ask', to: 'questions#create', as: :create_question - get 'questions/tagged/:tag_set/:tag', to: 'questions#tagged', as: :questions_tagged - get 'questions/:id', to: 'questions#show', as: :question - get 'questions/:id/edit', to: 'questions#edit', as: :edit_question - patch 'questions/:id/edit', to: 'questions#update', as: :update_question - delete 'questions/:id/delete', to: 'questions#destroy', as: :delete_question - post 'questions/:id/undelete', to: 'questions#undelete', as: :undelete_question - 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 - post ':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 - post 'posts/save-draft', to: 'posts#save_draft', as: :save_draft - post 'posts/delete-draft', to: 'posts#delete_draft', as: :delete_draft - - get 'posts/:id/edit', to: 'posts#edit', as: :edit_post - patch 'posts/:id/edit', to: 'posts#update', as: :update_post - - get 'posts/new-help', to: 'posts#new_help', as: :new_help_post - post 'posts/new-help', to: 'posts#create_help', as: :create_help_post - get 'posts/:id/edit-help', to: 'posts#edit_help', as: :edit_help_post - patch 'posts/:id/edit-help', to: 'posts#update_help', as: :update_help_post - post 'posts/:id/category', to: 'posts#change_category', as: :change_category - post 'posts/:id/toggle_comments', to: 'posts#toggle_comments', as: :post_comments_allowance_toggle - post 'posts/:id/lock', to: 'posts#lock', as: :post_lock - post 'posts/:id/unlock', to: 'posts#unlock', as: :post_unlock - post 'posts/:id/feature', to: 'posts#feature', as: :post_feature - - - get 'posts/suggested-edit/:id', to: 'suggested_edit#show', as: :suggested_edit - post 'posts/suggested-edit/:id/approve', to: 'suggested_edit#approve', as: :suggested_edit_approve - post 'posts/suggested-edit/:id/reject', to: 'suggested_edit#reject', as: :suggested_edit_reject + scope 'posts' do + get 'new/:post_type', to: 'posts#new', as: :new_post + get 'new/:post_type/respond/:parent', to: 'posts#new', as: :new_response + get 'new/:post_type/:category', to: 'posts#new', as: :new_category_post + post 'new/:post_type', to: 'posts#create', as: :create_post + post 'new/:post_type/respond/:parent', to: 'posts#create', as: :create_response + post 'new/:post_type/:category', to: 'posts#create', as: :create_category_post + + get ':id', to: 'posts#show', as: :post + + get ':id/history', to: 'post_history#post', as: :post_history + get 'search', to: 'search#search', as: :search + post 'upload', to: 'posts#upload', as: :upload + post 'save-draft', to: 'posts#save_draft', as: :save_draft + post 'delete-draft', to: 'posts#delete_draft', as: :delete_draft + + get ':id/edit', to: 'posts#edit', as: :edit_post + patch ':id/edit', to: 'posts#update', as: :update_post + + post ':id/close', to: 'posts#close', as: :close_post + post ':id/reopen', to: 'posts#reopen', as: :reopen_post + post ':id/delete', to: 'posts#delete', as: :delete_post + post ':id/restore', to: 'posts#restore', as: :restore_post + + post ':id/category', to: 'posts#change_category', as: :change_category + post ':id/toggle_comments', to: 'posts#toggle_comments', as: :post_comments_allowance_toggle + post ':id/lock', to: 'posts#lock', as: :post_lock + post ':id/unlock', to: 'posts#unlock', as: :post_unlock + post ':id/feature', to: 'posts#feature', as: :post_feature + + get 'suggested-edit/:id', to: 'suggested_edit#show', as: :suggested_edit + post 'suggested-edit/:id/approve', to: 'suggested_edit#approve', as: :suggested_edit_approve + post 'suggested-edit/:id/reject', to: 'suggested_edit#reject', as: :suggested_edit_reject + end get 'policy/:slug', to: 'posts#document', as: :policy get 'help/:slug', to: 'posts#document', as: :help @@ -156,12 +145,6 @@ Rails.application.routes.draw do post 'votes/new', to: 'votes#create', as: :create_vote delete 'votes/:id', to: 'votes#destroy', as: :destroy_vote - get 'questions/:id/answer', to: 'answers#new', as: :new_answer - post 'questions/:id/answer', to: 'answers#create', as: :create_answer - get 'answers/:id/edit', to: 'answers#edit', as: :edit_answer - patch 'answers/:id/edit', to: 'answers#update', as: :update_answer - delete 'answers/:id/delete', to: 'answers#destroy', as: :delete_answer - post 'answers/:id/delete', to: 'answers#undelete', as: :undelete_answer post 'answers/:id/convert', to: 'answers#convert_to_comment', as: :convert_to_comment post 'flags/new', to: 'flags#new', as: :new_flag @@ -173,10 +156,6 @@ Rails.application.routes.draw do delete 'comments/:id/delete', to: 'comments#destroy', as: :delete_comment patch 'comments/:id/delete', to: 'comments#undelete', as: :undelete_comment - 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 get 'subscriptions', to: 'subscriptions#index', as: :subscriptions @@ -192,24 +171,23 @@ Rails.application.routes.draw do get 'help', to: 'posts#help_center', as: :help_center scope 'categories' do - root to: 'categories#index', as: :categories - get 'new', to: 'categories#new', as: :new_category - post 'new', to: 'categories#create', as: :create_category - get ':category_id/posts/new/:post_type_id', to: 'posts#new', as: :new_post - post ':category_id/posts/new/:post_type_id', to: 'posts#create', as: :create_post - get ':id', to: 'categories#show', as: :category - get ':id/edit', to: 'categories#edit', as: :edit_category - post ':id/edit', to: 'categories#update', as: :update_category - delete ':id', to: 'categories#destroy', as: :destroy_category - get ':id/feed', to: 'categories#rss_feed', as: :category_feed - get ':id/tags', to: 'tags#category', as: :category_tags - get ':id/tags/:tag_id', to: 'tags#show', as: :tag - get ':id/tags/:tag_id/children', to: 'tags#children', as: :tag_children - get ':id/tags/:tag_id/edit', to: 'tags#edit', as: :edit_tag - patch ':id/tags/:tag_id/edit', to: 'tags#update', as: :update_tag - post ':id/tags/:tag_id/rename', to: 'tags#rename', as: :rename_tag - get ':id/tags/:tag_id/merge', to: 'tags#select_merge', as: :select_tag_merge - post ':id/tags/:tag_id/merge', to: 'tags#merge', as: :merge_tag + root to: 'categories#index', as: :categories + get 'new', to: 'categories#new', as: :new_category + post 'new', to: 'categories#create', as: :create_category + get ':id', to: 'categories#show', as: :category + get ':id/edit', to: 'categories#edit', as: :edit_category + post ':id/edit', to: 'categories#update', as: :update_category + delete ':id', to: 'categories#destroy', as: :destroy_category + get ':id/types', to: 'categories#post_types', as: :category_post_types + get ':id/feed', to: 'categories#rss_feed', as: :category_feed + get ':id/tags', to: 'tags#category', as: :category_tags + get ':id/tags/:tag_id', to: 'tags#show', as: :tag + get ':id/tags/:tag_id/children', to: 'tags#children', as: :tag_children + get ':id/tags/:tag_id/edit', to: 'tags#edit', as: :edit_tag + patch ':id/tags/:tag_id/edit', to: 'tags#update', as: :update_tag + post ':id/tags/:tag_id/rename', to: 'tags#rename', as: :rename_tag + get ':id/tags/:tag_id/merge', to: 'tags#select_merge', as: :select_tag_merge + post ':id/tags/:tag_id/merge', to: 'tags#merge', as: :merge_tag end get 'warning', to: 'mod_warning#current', as: :current_mod_warning diff --git a/db/migrate/20201204170746_add_columns_to_post_type.rb b/db/migrate/20201204170746_add_columns_to_post_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..560bb722ba8fcabb37f13d02e17aa0a6791bed62 --- /dev/null +++ b/db/migrate/20201204170746_add_columns_to_post_type.rb @@ -0,0 +1,32 @@ +class AddColumnsToPostType < ActiveRecord::Migration[5.2] + def change + change_table :post_types do |t| + t.text :description + t.boolean :has_answers, null: false, default: false + t.boolean :has_votes, null: false, default: false + t.boolean :has_tags, null: false, default: false + t.boolean :has_parent, null: false, default: false + t.boolean :has_category, null: false, default: false + t.boolean :has_license, null: false, default: false + t.boolean :is_public_editable, null: false, default: false + t.boolean :is_closeable, null: false, default: false + t.boolean :is_top_level, null: false, default: false + end + + data = { + 'Question' => { has_answers: true, has_votes: true, has_tags: true, has_parent: false, has_category: true, + has_license: true, is_public_editable: true, is_closeable: true, is_top_level: true }, + 'Answer' => { has_answers: false, has_votes: true, has_tags: false, has_parent: true, has_category: true, + has_license: true, is_public_editable: true, is_closeable: false, is_top_level: false }, + 'HelpDoc' => { has_answers: false, has_votes: false, has_tags: false, has_parent: false, has_category: false, + has_license: false, is_public_editable: false, is_closeable: false, is_top_level: false }, + 'PolicyDoc' => { has_answers: false, has_votes: false, has_tags: false, has_parent: false, has_category: false, + has_license: false, is_public_editable: false, is_closeable: false, is_top_level: false }, + 'Article' => { has_answers: false, has_votes: true, has_tags: true, has_parent: false, has_category: true, + has_license: true, is_public_editable: false, is_closeable: false, is_top_level: true } + } + PostType.unscoped.all.each do |pt| + pt.update(data[pt.name]) + end + end +end diff --git a/db/migrate/20201212235514_move_trust_level_to_community_user.rb b/db/migrate/20201212235514_move_trust_level_to_community_user.rb new file mode 100644 index 0000000000000000000000000000000000000000..25d86d7b35190a3c1c0d13b51da6cddf70109b3a --- /dev/null +++ b/db/migrate/20201212235514_move_trust_level_to_community_user.rb @@ -0,0 +1,6 @@ +class MoveTrustLevelToCommunityUser < ActiveRecord::Migration[5.2] + def change + add_column :community_users, :trust_level, :integer + remove_column :users, :trust_level + end +end diff --git a/db/schema.rb b/db/schema.rb index 78e6e236ef452b49305057f932a819d23445c563..8c4c796443e9b058652f5aeab6014b1a8c892de0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_11_23_132854) do +ActiveRecord::Schema.define(version: 2020_12_12_235514) do create_table "abilities", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci", force: :cascade do |t| t.bigint "community_id" @@ -170,6 +170,7 @@ ActiveRecord::Schema.define(version: 2020_11_23_132854) do t.boolean "is_suspended" t.datetime "suspension_end" t.string "suspension_public_comment" + t.integer "trust_level" t.index ["community_id"], name: "index_community_users_on_community_id" t.index ["user_id"], name: "index_community_users_on_user_id" end @@ -298,6 +299,16 @@ ActiveRecord::Schema.define(version: 2020_11_23_132854) do create_table "post_types", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", force: :cascade do |t| t.string "name" + t.text "description" + t.boolean "has_answers", default: false, null: false + t.boolean "has_votes", default: false, null: false + t.boolean "has_tags", default: false, null: false + t.boolean "has_parent", default: false, null: false + t.boolean "has_category", default: false, null: false + t.boolean "has_license", default: false, null: false + t.boolean "is_public_editable", default: false, null: false + t.boolean "is_closeable", default: false, null: false + t.boolean "is_top_level", default: false, null: false t.index ["name"], name: "index_post_types_on_name" end @@ -337,7 +348,7 @@ ActiveRecord::Schema.define(version: 2020_11_23_132854) do t.boolean "comments_disabled" t.datetime "last_edited_at" t.bigint "last_edited_by_id" - t.boolean "locked" + t.boolean "locked", default: false, null: false t.bigint "locked_by_id" t.datetime "locked_at" t.datetime "locked_until" @@ -521,7 +532,6 @@ ActiveRecord::Schema.define(version: 2020_11_23_132854) do t.text "profile_markdown" t.integer "se_acct_id" t.boolean "transferred_content", default: false - t.integer "trust_level" t.string "login_token" t.datetime "login_token_expires_at" t.string "two_factor_token" diff --git a/test/controllers/answers_controller_test.rb b/test/controllers/answers_controller_test.rb index fd5c2aaa540ce1cbd4176a2c6054c2cc38e9e68f..b5a1ade00e1d7e619cdc6820e74f6430a31f031e 100644 --- a/test/controllers/answers_controller_test.rb +++ b/test/controllers/answers_controller_test.rb @@ -3,173 +3,6 @@ require 'test_helper' class AnswersControllerTest < ActionController::TestCase include Devise::Test::ControllerHelpers - test 'should get new answer page' do - sign_in users(:standard_user) - get :new, params: { id: posts(:question_one).id } - assert_response(200) - assert_not_nil assigns(:answer) - assert_not_nil assigns(:question) - end - - test 'should create new answer' do - sign_in users(:standard_user) - post :create, params: { answer: { body_markdown: 'ABCDEF ABCDEF ABCDEF ABCDEF GH', license_id: licenses(:cc_by_sa).id }, - id: posts(:question_one).id } - assert_not_nil assigns(:answer) - assert_not_nil assigns(:question) - assert_response(302) - end - - test 'should get edit answer page' do - sign_in users(:editor) - get :edit, params: { id: posts(:answer_one).id } - assert_response(200) - assert_not_nil assigns(:answer) - end - - test 'should update existing answer' do - sign_in users(:editor) - patch :update, params: { answer: { body_markdown: 'ABCDEF GHIJKL MNOPQR STUVWX YZ' }, id: posts(:answer_one).id } - assert_not_nil assigns(:answer) - assert_response(302) - end - - test 'should mark answer deleted' do - sign_in users(:deleter) - delete :destroy, params: { id: posts(:answer_one).id } - assert_not_nil assigns(:answer) - assert_equal true, assigns(:answer).deleted - assert_not_nil assigns(:answer).deleted_at - assert_response(302) - end - - test 'should mark answer undeleted' do - sign_in users(:deleter) - delete :undelete, params: { id: posts(:deleted_answer).id } - assert_not_nil assigns(:answer) - assert_equal false, assigns(:answer).deleted - assert_nil assigns(:answer).deleted_at - assert_response(302) - end - - test 'should require authentication to get new page' do - sign_out :user - get :new, params: { id: posts(:question_one).id } - assert_response(302) - end - - test 'should require authentication to create answer' do - sign_out :user - post :create, params: { id: posts(:question_one).id } - assert_response(302) - end - - test 'should require authentication to get edit page' do - sign_out :user - get :edit, params: { id: posts(:answer_one).id } - assert_response(302) - end - - test 'should require authentication to update answer' do - sign_out :user - patch :update, params: { id: posts(:answer_one).id } - assert_response(302) - end - - test 'should require authentication to delete' do - sign_out :user - delete :destroy, params: { id: posts(:answer_one).id } - assert_response(302) - end - - test 'should require authentication to undelete' do - sign_out :user - delete :undelete, params: { id: posts(:answer_one).id } - assert_response(302) - end - - test 'should require above standard privileges to delete' do - sign_in users(:standard_user) - delete :destroy, params: { id: posts(:answer_two).id } - assert_response(302) - assert_not_nil flash[:danger] - assert_equal false, assigns(:answer).deleted - end - - test 'should require above standard privileges to undelete' do - sign_in users(:standard_user) - delete :undelete, params: { id: posts(:deleted_answer).id } - assert_response(302) - assert_not_nil flash[:danger] - assert_equal true, assigns(:answer).deleted - end - - test 'should require above edit privileges to delete' do - sign_in users(:editor) - delete :destroy, params: { id: posts(:answer_one).id } - assert_response(302) - assert_not_nil flash[:danger] - assert_equal false, assigns(:answer).deleted - end - - test 'should require above edit privileges to undelete' do - sign_in users(:editor) - delete :undelete, params: { id: posts(:deleted_answer).id } - assert_response(302) - assert_not_nil flash[:danger] - assert_equal true, assigns(:answer).deleted - end - - test 'should allow author to get edit page' do - sign_in users(:standard_user) - get :edit, params: { id: posts(:answer_one).id } - assert_not_nil assigns(:answer) - assert_response(200) - end - - test 'should allow author to update answer' do - sign_in users(:standard_user) - patch :update, params: { answer: { body_markdown: 'ABCDEF GHIJKL MNOPQR STUVWX YZ' }, id: posts(:answer_one).id } - assert_not_nil assigns(:answer) - assert_response(302) - end - - test 'should allow author to delete answer' do - sign_in users(:standard_user) - delete :destroy, params: { id: posts(:answer_one).id } - assert_not_nil assigns(:answer) - assert_nil flash[:danger] - assert_response(302) - end - - test 'should allow author to undelete answer' do - sign_in users(:closer) - delete :undelete, params: { id: posts(:deleted_answer).id } - assert_not_nil assigns(:answer) - assert_nil flash[:danger] - assert_response(302) - end - - test 'should block short answers' do - sign_in users(:standard_user) - post :create, params: { answer: { body_markdown: 'ABCDEF' }, id: posts(:question_one).id } - assert_response(422) - end - - test 'should block whitespace answers' do - sign_in users(:standard_user) - post :create, params: { answer: { body_markdown: ' ' * 31, license_id: licenses(:cc_by_sa).id }, - id: posts(:question_one).id } - assert_response(422) - end - - test 'should block long answers' do - sign_in users(:standard_user) - post :create, params: { answer: { body_markdown: 'A' * (3e4 + 1), license_id: licenses(:cc_by_sa).id }, - id: posts(:question_one).id } - assert_response(422) - end - test 'should convert to comment' do sign_in users(:moderator) pre_count = posts(:question_one).comments.count diff --git a/test/controllers/articles_controller_test.rb b/test/controllers/articles_controller_test.rb index 07a6bacfc1155cef057d030412fddcc001c3d200..87c9519803a319d90a480b54985ebba54c2db525 100644 --- a/test/controllers/articles_controller_test.rb +++ b/test/controllers/articles_controller_test.rb @@ -2,57 +2,4 @@ require 'test_helper' class ArticlesControllerTest < ActionController::TestCase include Devise::Test::ControllerHelpers - include ApplicationTestHelper - - 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_array_equal ['discussion', 'support'], assigns(:article).tags_cache - assert_array_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/mod_warning_controller_test.rb b/test/controllers/mod_warning_controller_test.rb index d839d8c6a2d46184539ea93e5e3aac23db6bc42b..e2a0c2a1022707f3bd355039287f49631def0fb7 100644 --- a/test/controllers/mod_warning_controller_test.rb +++ b/test/controllers/mod_warning_controller_test.rb @@ -38,7 +38,7 @@ class ModWarningControllerTest < ActionController::TestCase @warning.update(active: true) post :approve, params: { approve_checkbox: true } @warning.reload - assert !@warning.active + assert_not @warning.active end test 'suspended user should not be able to accept pending suspension' do @@ -56,6 +56,6 @@ class ModWarningControllerTest < ActionController::TestCase @warning.update(active: true) post :approve, params: { approve_checkbox: true } @warning.reload - assert !@warning.active + assert_not @warning.active end end diff --git a/test/controllers/posts_controller_test.rb b/test/controllers/posts_controller_test.rb index 46e135e585c73093260847bdb06c53896514d979..f59de979b46d35a966f937339cd2a65274036a39 100644 --- a/test/controllers/posts_controller_test.rb +++ b/test/controllers/posts_controller_test.rb @@ -3,214 +3,901 @@ require 'test_helper' class PostsControllerTest < ActionController::TestCase include Devise::Test::ControllerHelpers - test 'should require authentication to get new_help' do - get :new_help - assert_response 302 + # Help + + test 'can get help center' do + get :help_center + assert_response 200 + assert_not_nil assigns(:posts) end - test 'should require authentication to get edit_help' do - get :edit_help, params: { id: posts(:policy_doc).id } - assert_response 302 + test 'can get help article' do + get :document, params: { slug: posts(:help_article).doc_slug } + assert_response 200 + assert_not_nil assigns(:post) end - test 'should deny regular users access to new_help' do - sign_in users(:standard_user) - get :new_help + test 'moderator can get mod help article' do + sign_in users(:moderator) + get :document, params: { slug: posts(:mod_help_article).doc_slug } + assert_response 200 + assert_not_nil assigns(:post) + end + + test 'moderator help requires authentication' do + get :document, params: { slug: posts(:mod_help_article).doc_slug } assert_response 404 + assert_not_nil assigns(:post) end - test 'should deny regular users access to edit_help' do + test 'regular user cannot get mod help' do sign_in users(:standard_user) - get :edit_help, params: { id: posts(:policy_doc).id } + get :document, params: { slug: posts(:mod_help_article).doc_slug } assert_response 404 + assert_not_nil assigns(:post) end - test 'should allow moderators access to new_help' do + test 'cannot get disabled help article' do sign_in users(:moderator) - get :new_help + get :document, params: { slug: posts(:disabled_help_article).doc_slug } + assert_response 404 + assert_not_nil assigns(:post) + end + + # Change category + + test 'should change category' do + sign_in users(:deleter) + post :change_category, params: { id: posts(:article_one).id, target_id: categories(:articles_only).id } assert_response 200 + assert_not_nil assigns(:post) + assert_not_nil assigns(:target) + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal categories(:articles_only).id, assigns(:post).category_id + end + + test 'should deny change category to unprivileged' do + sign_in users(:standard_user) + post :change_category, params: { id: posts(:article_one).id, target_id: categories(:articles_only).id } + assert_response 403 + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal ["You don't have permission to make that change."], JSON.parse(response.body)['errors'] end - test 'should allow moderators access to edit_help' do + test 'should refuse to change category of wrong post type' do + sign_in users(:deleter) + post :change_category, params: { id: posts(:question_one).id, target_id: categories(:articles_only).id } + assert_response 409 + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal ["This post type is not allowed in the #{categories(:articles_only).name} category."], + JSON.parse(response.body)['errors'] + end + + # New + + test 'should get new' do sign_in users(:moderator) - get :edit_help, params: { id: posts(:help_doc).id } + get :new, params: { post_type: post_types(:help_doc).id } + assert_nil flash[:danger] assert_response 200 - assert_not_nil assigns(:post) + + get :new, params: { post_type: post_types(:answer).id, parent: posts(:question_one).id } + assert_nil flash[:danger] + assert_response 200 + + get :new, params: { post_type: post_types(:question).id, category: categories(:main).id } + assert_nil flash[:danger] + assert_response 200 + end + + test 'new requires authentication' do + get :new, params: { post_type: post_types(:help_doc).id } + assert_redirected_to new_user_session_path + get :new, params: { post_type: post_types(:answer).id, parent: posts(:question_one).id } + assert_redirected_to new_user_session_path + get :new, params: { post_type: post_types(:question).id, category: categories(:main).id } + assert_redirected_to new_user_session_path end - test 'should disallow regular users to create help doc' do + test 'new rejects category post type without category' do sign_in users(:standard_user) - post :create_help, params: { post: { post_type_id: HelpDoc.post_type_id, body_markdown: 'ABCDEF GHIJKL MNOPQR STUVWX YZ', - title: 'ABCDEF GHIJKL MNOPQR', doc_slug: 'help-doc' } } - assert_response 404 + get :new, params: { post_type: post_types(:question).id } + assert_response 302 + assert_redirected_to root_path + assert_not_nil flash[:danger] + end + + test 'new rejects parented post type without parent' do + sign_in users(:standard_user) + get :new, params: { post_type: post_types(:answer).id } + assert_response 302 + assert_redirected_to root_path + assert_not_nil flash[:danger] end - test 'should allow moderators to create help doc' do + # Create + + test 'can create help post' do sign_in users(:moderator) - post :create_help, params: { post: { post_type_id: HelpDoc.post_type_id, body_markdown: 'ABCDEF GHIJKL MNOPQR STUVWX YZ', - title: 'ABCDEF GHIJKL MNOPQR', doc_slug: 'help-doc' } } + post :create, params: { post_type: post_types(:help_doc).id, + post: { post_type_id: post_types(:help_doc).id, title: sample.title, doc_slug: 'topic', + body_markdown: sample.body_markdown, help_category: 'A', help_ordering: '99' } } + assert_response 302 + assert_not_nil assigns(:post).id + assert_redirected_to help_path(assigns(:post).doc_slug) + end + + test 'can create category post' do + sign_in users(:standard_user) + post :create, params: { post_type: post_types(:question).id, category: categories(:main).id, + post: { post_type_id: post_types(:question).id, title: sample.title, + body_markdown: sample.body_markdown, category_id: categories(:main).id, + tags_cache: sample.tags_cache } } + assert_response 302 + assert_not_nil assigns(:post).id + assert_redirected_to post_path(assigns(:post)) + end + + test 'can create answer' do + sign_in users(:standard_user) + post :create, params: { post_type: post_types(:answer).id, parent: posts(:question_one).id, + post: { post_type_id: post_types(:answer).id, title: sample.title, + body_markdown: sample.body_markdown, parent_id: posts(:question_one).id } } assert_response 302 - assert_not_nil assigns(:post) assert_not_nil assigns(:post).id + assert_redirected_to post_path(posts(:question_one).id, anchor: "answer-#{assigns(:post).id}") + end + + test 'create requires authentication' do + post :create, params: { post_type: post_types(:question).id, category: categories(:main).id, + post: { post_type_id: post_types(:question).id, title: sample.title, + body_markdown: sample.body_markdown, category_id: categories(:main).id, + tags_cache: sample.tags_cache } } + assert_response 302 + assert_redirected_to new_user_session_path + end + + test 'standard users cannot create help posts' do + sign_in users(:standard_user) + post :create, params: { post_type: post_types(:help_doc).id, + post: { post_type_id: post_types(:help_doc).id, title: sample.title, doc_slug: 'topic', + body_markdown: sample.body_markdown, help_category: 'A', help_ordering: '99' } } + assert_response 404 end - test 'should disallow moderators to create policy doc' do + test 'moderators cannot create policy posts' do sign_in users(:moderator) - post :create_help, params: { post: { post_type_id: PolicyDoc.post_type_id, body_markdown: 'ABCDEF GHIJKL MNOPQR STUVWX YZ', - title: 'ABCDEF GHIJKL MNOPQR', doc_slug: 'policy-doc' } } + post :create, params: { post_type: post_types(:policy_doc).id, + post: { post_type_id: post_types(:policy_doc).id, title: sample.title, doc_slug: 'topic', + body_markdown: sample.body_markdown, help_category: 'A', help_ordering: '99' } } + assert_response 404 + end + + test 'category post type rejects without category' do + sign_in users(:standard_user) + post :create, params: { post_type: post_types(:question).id, + post: { post_type_id: post_types(:question).id, title: sample.title, + body_markdown: sample.body_markdown, tags_cache: sample.tags_cache } } + assert_response 302 + assert_redirected_to root_path + assert_not_nil flash[:danger] + assert_nil assigns(:post).id + end + + test 'category post type checks required trust level' do + sign_in users(:standard_user) + post :create, params: { post_type: post_types(:question).id, category: categories(:high_trust).id, + post: { post_type_id: post_types(:question).id, title: sample.title, + body_markdown: sample.body_markdown, category_id: categories(:high_trust).id, + tags_cache: sample.tags_cache } } assert_response 403 assert_nil assigns(:post).id - assert_equal true, assigns(:post).errors.any? + assert_not_empty assigns(:post).errors.full_messages end - test 'should allow admins to create policy doc' do - sign_in users(:admin) - post :create_help, params: { post: { post_type_id: PolicyDoc.post_type_id, body_markdown: 'ABCDEF GHIJKL MNOPQR STUVWX YZ', - title: 'ABCDEF GHIJKL MNOPQR', doc_slug: 'policy-doc' } } + test 'parented post type rejects without parent' do + sign_in users(:standard_user) + post :create, params: { post_type: post_types(:answer).id, + post: { post_type_id: post_types(:answer).id, title: sample.title, + body_markdown: sample.body_markdown } } assert_response 302 + assert_redirected_to root_path + assert_not_nil flash[:danger] + assert_nil assigns(:post).id + end + + test 'create ensures community user is created' do + user = users(:no_community_user) + before = CommunityUser.where(user: user, community: communities(:sample)).count + + sign_in user + post :create, params: { post_type: post_types(:question).id, category: categories(:main).id, + post: { post_type_id: post_types(:question).id, title: sample.title, + body_markdown: sample.body_markdown, category_id: categories(:main).id, + tags_cache: sample.tags_cache } } + + after = CommunityUser.where(user: user, community: communities(:sample)).count + assert_equal before + 1, after, 'No CommunityUser record was created' + end + + # Show + + test 'anonymous user can get show' do + get :show, params: { id: posts(:question_one).id } + assert_response 200 assert_not_nil assigns(:post) - assert_not_nil assigns(:post).id + assert_not_nil assigns(:children) + assert_not assigns(:children).any?(&:deleted), 'Anonymous user can see deleted answers' end - test 'should disallow regular users to edit help doc' do + test 'standard user can get show' do sign_in users(:standard_user) - patch :update_help, params: { id: posts(:help_doc).id, - post: { body_markdown: 'ABCDEF GHIJKL MNOPQR STUVWX YZ', title: 'ABCDEF GHIJKL MNOPQR' } } - assert_response 404 + get :show, params: { id: posts(:question_one).id } + assert_response 200 + assert_not_nil assigns(:post) + assert_not_nil assigns(:children) + assert_not assigns(:children).any?(&:deleted), 'Anonymous user can see deleted answers' end - test 'should allow moderators to edit help doc' do - sign_in users(:moderator) - patch :update_help, params: { id: posts(:help_doc).id, - post: { body_markdown: 'ABCDEF GHIJKL MNOPQR STUVWX YZ', title: 'ABCDEF GHIJKL MNOPQR' } } + test 'privileged user can see deleted post' do + sign_in users(:deleter) + get :show, params: { id: posts(:deleted).id } + assert_response 200 + assert_not_nil assigns(:post) + assert_not_nil assigns(:children) + end + + test 'privileged user can see deleted answers' do + sign_in users(:deleter) + get :show, params: { id: posts(:question_one).id } + assert_response 200 + assert_not_nil assigns(:post) + assert_not_nil assigns(:children) + assert assigns(:children).any?(&:deleted), 'Privileged user cannot see deleted answers' + end + + test 'show redirects parented to parent post' do + get :show, params: { id: posts(:answer_one).id } assert_response 302 + assert_redirected_to post_path(posts(:answer_one).parent_id) + end + + # Edit + + test 'can get edit' do + sign_in users(:standard_user) + get :edit, params: { id: posts(:question_one).id } + assert_response 200 assert_not_nil assigns(:post) - assert_not_nil assigns(:post).id end - test 'should disallow moderators to edit policy doc' do - sign_in users(:moderator) - patch :update_help, params: { id: posts(:policy_doc).id, - post: { body_markdown: 'ABCDEF GHIJKL MNOPQR STUVWX YZ', title: 'ABCDEF GHIJKL MNOPQR' } } - assert_response 404 + test 'edit requires authentication' do + get :edit, params: { id: posts(:question_one).id } + assert_response 302 + assert_redirected_to new_user_session_path + end + + test 'cannot edit locked post' do + sign_in users(:standard_user) + get :edit, params: { id: posts(:locked).id } + assert_response 401 end - test 'should allow admins to edit policy doc' do - sign_in users(:admin) - patch :update_help, params: { id: posts(:policy_doc).id, - post: { body_markdown: 'ABCDEF GHIJKL MNOPQR STUVWX YZ', title: 'ABCDEF GHIJKL MNOPQR' } } + test 'cannot edit non-public post without permissions' do + sign_in users(:standard_user) + get :edit, params: { id: posts(:blog_post).id } assert_response 302 + assert_redirected_to root_path + assert_not_nil flash[:danger] + end + + test 'author can edit non-public post' do + sign_in users(:closer) + get :edit, params: { id: posts(:blog_post).id } + assert_response 200 assert_not_nil assigns(:post) - assert_not_nil assigns(:post).id end - test 'should successfully get help center' do - get :help_center + test 'moderator can edit non-public post' do + sign_in users(:moderator) + get :edit, params: { id: posts(:blog_post).id } assert_response 200 - assert_not_nil assigns(:posts) + assert_not_nil assigns(:post) end - test 'question permalink should correctly redirect' do - get :share_q, params: { id: posts(:question_one).id } + # Update + + test 'can update post' do + sign_in users(:standard_user) + before_history = PostHistory.where(post: posts(:question_one)).count + patch :update, params: { id: posts(:question_one).id, + post: { title: sample.edit.title, body_markdown: sample.edit.body_markdown, + tags_cache: sample.edit.tags_cache } } + after_history = PostHistory.where(post: posts(:question_one)).count assert_response 302 - assert_redirected_to question_path(posts(:question_one)) + assert_redirected_to post_path(posts(:question_one)) + assert_not_nil assigns(:post) + assert_equal sample.edit.body_markdown, assigns(:post).body_markdown + assert_equal before_history + 1, after_history, 'No PostHistory event created on author update' end - test 'answer permalink should correctly redirect' do - get :share_a, params: { qid: posts(:question_one).id, id: posts(:answer_one).id } + test 'moderators can update post' do + sign_in users(:moderator) + before_history = PostHistory.where(post: posts(:question_one)).count + patch :update, params: { id: posts(:question_one).id, + post: { title: sample.edit.title, body_markdown: sample.edit.body_markdown, + tags_cache: sample.edit.tags_cache } } + after_history = PostHistory.where(post: posts(:question_one)).count assert_response 302 - assert_redirected_to question_path(id: posts(:question_one).id, anchor: "answer-#{posts(:answer_one).id}") + assert_redirected_to post_path(posts(:question_one)) + assert_not_nil assigns(:post) + assert_equal sample.edit.body_markdown, assigns(:post).body_markdown + assert_equal before_history + 1, after_history, 'No PostHistory event created on moderator update' end - test 'should require sign in to write post' do - get :new, params: { category_id: categories(:main).id, post_type_id: post_types(:question).id } + test 'update requires authentication' do + patch :update, params: { id: posts(:question_one).id, + post: { title: sample.edit.title, body_markdown: sample.edit.body_markdown, + tags_cache: sample.edit.tags_cache } } assert_response 302 assert_redirected_to new_user_session_path end - test 'should require sign in to create post' do - post :create, params: { category_id: categories(:main).id, post_type_id: post_types(:question).id, - post: { body_markdown: 'ABCD EFGH IJKL MNOP QRST UVWX YZ', title: 'ABCD EFGH IJKL M', - tags_cache: ['discussion', 'support', 'bug', 'feature-request'], - license_id: licenses(:cc_by_sa).id } } + test 'update by unprivileged user generates suggested edit' do + sign_in users(:closer) + before_history = PostHistory.where(post: posts(:question_one)).count + before_edits = SuggestedEdit.where(post: posts(:question_one)).count + before_body = posts(:question_one).body_markdown + patch :update, params: { id: posts(:question_one).id, + post: { title: sample.edit.title, body_markdown: sample.edit.body_markdown, + tags_cache: sample.edit.tags_cache } } + after_history = PostHistory.where(post: posts(:question_one)).count + after_edits = SuggestedEdit.where(post: posts(:question_one)).count assert_response 302 - assert_redirected_to new_user_session_path + assert_redirected_to post_path(posts(:question_one)) + assert_not_nil assigns(:post) + assert_equal before_body, assigns(:post).body_markdown, 'Suggested edit incorrectly applied immediately' + assert_equal before_history, after_history, 'PostHistory event incorrectly created on unprivileged update' + assert_equal before_edits + 1, after_edits, 'No SuggestedEdit created on unprivileged update' + end + + test 'update rejects no change edit' do + sign_in users(:standard_user) + post = posts(:question_one) + before_history = PostHistory.where(post: post).count + patch :update, params: { id: post.id, + post: { title: post.title, body_markdown: post.body_markdown, + tags_cache: post.tags_cache } } + after_history = PostHistory.where(post: posts(:question_one)).count + assert_response 302 + assert_redirected_to post_path(posts(:question_one)) + assert_not_nil assigns(:post) + assert_not_nil flash[:danger] + assert_equal before_history, after_history, 'PostHistory event incorrectly created on no-change update' end - test 'should allow signed in user to write post' do + test 'cannot update locked post' do sign_in users(:standard_user) - get :new, params: { category_id: categories(:main).id, post_type_id: post_types(:question).id } + before_history = PostHistory.where(post: posts(:locked)).count + patch :update, params: { id: posts(:locked).id, + post: { title: sample.edit.title, body_markdown: sample.edit.body_markdown, + tags_cache: sample.edit.tags_cache } } + after_history = PostHistory.where(post: posts(:locked)).count + assert_response 401 + assert_equal before_history, after_history, 'PostHistory event incorrectly created on update' + end + + # Close + + test 'can close question' do + sign_in users(:closer) + before_history = PostHistory.where(post: posts(:question_one)).count + post :close, params: { id: posts(:question_one).id, reason_id: close_reasons(:not_good).id } + after_history = PostHistory.where(post: posts(:question_one)).count assert_response 200 assert_not_nil assigns(:post) - assert_equal categories(:main).id, assigns(:post).category_id - assert_equal post_types(:question).id, assigns(:post).post_type_id + assert_equal before_history + 1, after_history, 'PostHistory event not created on closure' + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal 'success', JSON.parse(response.body)['status'] + end + + test 'close requires authentication' do + post :close, params: { id: posts(:question_one).id, reason_id: close_reasons(:not_good).id } + assert_response 302 + assert_redirected_to new_user_session_path + end + + test 'unprivileged user cannot close' do + sign_in users(:standard_user) + before_history = PostHistory.where(post: posts(:question_one)).count + post :close, params: { id: posts(:question_one).id, reason_id: close_reasons(:not_good).id } + after_history = PostHistory.where(post: posts(:question_one)).count + assert_response 403 + assert_not_nil assigns(:post) + assert_equal before_history, after_history, 'PostHistory event incorrectly created on closure' + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal 'failed', JSON.parse(response.body)['status'] + end + + test 'cannot close a closed post' do + sign_in users(:closer) + before_history = PostHistory.where(post: posts(:closed)).count + post :close, params: { id: posts(:closed).id, reason_id: close_reasons(:not_good).id } + after_history = PostHistory.where(post: posts(:closed)).count + assert_response 400 + assert_not_nil assigns(:post) + assert_equal before_history, after_history, 'PostHistory event incorrectly created on closure' + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal 'failed', JSON.parse(response.body)['status'] + end + + test 'close rejects nonexistent close reason' do + sign_in users(:closer) + before_history = PostHistory.where(post: posts(:question_one)).count + post :close, params: { id: posts(:question_one).id, reason_id: -999 } + after_history = PostHistory.where(post: posts(:question_one)).count + assert_response 404 + assert_not_nil assigns(:post) + assert_equal before_history, after_history, 'PostHistory event incorrectly created on closure' + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal 'failed', JSON.parse(response.body)['status'] + end + + test 'close ensures other post exists if reason requires it' do + sign_in users(:closer) + before_history = PostHistory.where(post: posts(:question_one)).count + post :close, params: { id: posts(:question_one).id, reason_id: close_reasons(:duplicate) } + after_history = PostHistory.where(post: posts(:question_one)).count + assert_response 400 + assert_not_nil assigns(:post) + assert_equal before_history, after_history, 'PostHistory event incorrectly created on closure' + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal 'failed', JSON.parse(response.body)['status'] + end + + test 'cannot close a locked post' do + sign_in users(:closer) + before_history = PostHistory.where(post: posts(:locked)).count + post :close, params: { id: posts(:locked).id, reason_id: close_reasons(:not_good).id } + after_history = PostHistory.where(post: posts(:locked)).count + assert_response 401 + assert_equal before_history, after_history, 'PostHistory event incorrectly created on close' end - test 'should allow signed in user to create post' do + # Reopen + + test 'can reopen question' do + sign_in users(:closer) + before_history = PostHistory.where(post: posts(:closed)).count + post :reopen, params: { id: posts(:closed).id } + after_history = PostHistory.where(post: posts(:closed)).count + assert_response 302 + assert_redirected_to post_path(posts(:closed)) + assert_nil flash[:danger] + assert_equal before_history + 1, after_history, 'PostHistory event not created on reopen' + end + + test 'reopen requires authentication' do + post :reopen, params: { id: posts(:closed).id } + assert_response 302 + assert_redirected_to new_user_session_path + end + + test 'unprivileged user cannot reopen' do sign_in users(:standard_user) - post :create, params: { category_id: categories(:main).id, post_type_id: post_types(:question).id, - post: { body_markdown: 'ABCD EFGH IJKL MNOP QRST UVWX YZ', title: 'ABCD EFGH IJKL M', - tags_cache: ['discussion', 'support', 'bug', 'feature-request'], - license_id: licenses(:cc_by_sa).id } } + before_history = PostHistory.where(post: posts(:closed)).count + post :reopen, params: { id: posts(:closed).id } + after_history = PostHistory.where(post: posts(:closed)).count + assert_response 302 + assert_redirected_to post_path(posts(:closed)) + assert_not_nil flash[:danger] + assert_equal before_history, after_history, 'PostHistory event incorrectly created on reopen' + end + + test 'cannot reopen an open post' do + sign_in users(:closer) + before_history = PostHistory.where(post: posts(:question_one)).count + post :reopen, params: { id: posts(:question_one).id } + after_history = PostHistory.where(post: posts(:question_one)).count + assert_response 302 + assert_redirected_to post_path(posts(:question_one)) + assert_not_nil flash[:danger] + assert_equal before_history, after_history, 'PostHistory event incorrectly created on reopen' + end + + test 'cannot reopen a locked post' do + sign_in users(:closer) + before_history = PostHistory.where(post: posts(:locked)).count + post :reopen, params: { id: posts(:locked).id } + after_history = PostHistory.where(post: posts(:locked)).count + assert_response 401 + assert_equal before_history, after_history, 'PostHistory event incorrectly created on reopen' + end + + # Delete + + test 'can delete post' do + sign_in users(:deleter) + before_history = PostHistory.where(post: posts(:question_two)).count + post :delete, params: { id: posts(:question_two).id } + after_history = PostHistory.where(post: posts(:question_two)).count + assert_response 302 + assert_redirected_to post_path(assigns(:post)) + assert_nil flash[:danger] + assert_equal before_history + 1, after_history, 'PostHistory event not created on deletion' + end + + test 'delete requires authentication' do + post :delete, params: { id: posts(:question_one).id } + assert_response 302 + assert_redirected_to new_user_session_path + end + + test 'unprivileged user cannot delete' do + sign_in users(:closer) + before_history = PostHistory.where(post: posts(:question_one)).count + post :delete, params: { id: posts(:question_one).id } + after_history = PostHistory.where(post: posts(:question_one)).count + assert_response 302 + assert_redirected_to post_path(assigns(:post)) + assert_not_nil flash[:danger] + assert_equal before_history, after_history, 'PostHistory event incorrectly created on deletion' + end + + test 'cannot delete a post with responses' do + sign_in users(:deleter) + before_history = PostHistory.where(post: posts(:question_one)).count + post :delete, params: { id: posts(:question_one).id } + after_history = PostHistory.where(post: posts(:question_one)).count + assert_response 302 + assert_redirected_to post_path(assigns(:post)) + assert_not_nil flash[:danger] + assert_equal before_history, after_history, 'PostHistory event incorrectly created on deletion' + end + + test 'cannot delete a deleted post' do + sign_in users(:deleter) + before_history = PostHistory.where(post: posts(:deleted)).count + post :delete, params: { id: posts(:deleted).id } + after_history = PostHistory.where(post: posts(:deleted)).count + assert_response 302 + assert_redirected_to post_path(assigns(:post)) + assert_not_nil flash[:danger] + assert_equal before_history, after_history, 'PostHistory event incorrectly created on deletion' + end + + test 'cannot delete a locked post' do + sign_in users(:deleter) + before_history = PostHistory.where(post: posts(:locked)).count + post :delete, params: { id: posts(:locked).id } + after_history = PostHistory.where(post: posts(:locked)).count + assert_response 401 + assert_equal before_history, after_history, 'PostHistory event incorrectly created on deletion' + end + + test 'delete ensures all children are deleted' do + sign_in users(:deleter) + before_history = PostHistory.where(post_id: posts(:bad_answers).children.map(&:id)).count + post :delete, params: { id: posts(:bad_answers).id } + after_history = PostHistory.where(post_id: posts(:bad_answers).children.map(&:id)).count + assert_response 302 + assert_redirected_to post_path(assigns(:post)) + assert_nil flash[:danger] + assert assigns(:post).children.all?(&:deleted), 'Answers not deleted with question' + assert_equal before_history + posts(:bad_answers).children.count, after_history, + 'Answer PostHistory events not created on question deletion' + end + + # Restore + + test 'can restore post' do + sign_in users(:deleter) + before_history = PostHistory.where(post: posts(:deleted)).count + post :restore, params: { id: posts(:deleted).id } + after_history = PostHistory.where(post: posts(:deleted)).count + assert_response 302 + assert_redirected_to post_path(assigns(:post)) + assert_nil flash[:danger] + assert_equal before_history + 1, after_history, 'PostHistory event not created on deletion' + end + + test 'restore requires authentication' do + post :restore, params: { id: posts(:deleted).id } assert_response 302 + assert_redirected_to new_user_session_path + end + + test 'unprivileged user cannot restore' do + sign_in users(:closer) + before_history = PostHistory.where(post: posts(:deleted)).count + post :restore, params: { id: posts(:deleted).id } + after_history = PostHistory.where(post: posts(:deleted)).count + assert_response 302 + assert_redirected_to post_path(assigns(:post)) + assert_not_nil flash[:danger] + assert_equal before_history, after_history, 'PostHistory event incorrectly created on deletion' + end + + test 'cannot restore a post deleted by a moderator' do + sign_in users(:deleter) + before_history = PostHistory.where(post: posts(:deleted_mod)).count + post :restore, params: { id: posts(:deleted_mod).id } + after_history = PostHistory.where(post: posts(:deleted_mod)).count + assert_response 302 + assert_redirected_to post_path(assigns(:post)) + assert_not_nil flash[:danger] + assert_equal before_history, after_history, 'PostHistory event incorrectly created on deletion' + end + + test 'cannot restore a restored post' do + sign_in users(:deleter) + before_history = PostHistory.where(post: posts(:question_one)).count + post :restore, params: { id: posts(:question_one).id } + after_history = PostHistory.where(post: posts(:question_one)).count + assert_response 302 + assert_redirected_to post_path(assigns(:post)) + assert_not_nil flash[:danger] + assert_equal before_history, after_history, 'PostHistory event incorrectly created on deletion' + end + + test 'cannot restore a locked post' do + sign_in users(:deleter) + before_history = PostHistory.where(post: posts(:locked)).count + post :restore, params: { id: posts(:locked).id } + after_history = PostHistory.where(post: posts(:locked)).count + assert_response 401 + assert_equal before_history, after_history, 'PostHistory event incorrectly created on deletion' + end + + test 'restore brings back all answers deleted after question' do + sign_in users(:deleter) + deleted_at = posts(:deleted).deleted_at + children = posts(:deleted).children.where('deleted_at >= ?', deleted_at) + children_count = children.count + before_history = PostHistory.where(post_id: children.where('deleted_at >= ?', deleted_at)).count + post :restore, params: { id: posts(:deleted).id } + after_history = PostHistory.where(post_id: children.where('deleted_at >= ?', deleted_at)).count + assert_response 302 + assert_redirected_to post_path(assigns(:post)) + assert_nil flash[:danger] + assert_equal before_history + children_count, after_history, + 'Answer PostHistory events not created on question restore' + end + + # Toggle comments + + test 'can toggle comments' do + sign_in users(:moderator) + post :toggle_comments, params: { id: posts(:question_one).id } + assert_response 200 assert_not_nil assigns(:post) - assert_empty assigns(:post).errors.full_messages - assert_redirected_to question_path(assigns(:post)) + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal 'success', JSON.parse(response.body)['status'] + assert assigns(:post).comments_disabled + end + + test 'toggle comments requires authentication' do + post :toggle_comments, params: { id: posts(:question_one).id } + assert_response 302 + assert_redirected_to new_user_session_path end - test 'should ensure community_user is created while posting' do + test 'regular users cannot toggle comments' do sign_in users(:standard_user) - post :create, params: { category_id: categories(:main).id, post_type_id: post_types(:question).id, - post: { body_markdown: 'ABCD EFGH IJKL MNOP QRST UVWX YZ', title: 'ABCD EFGH IJKL M', - tags_cache: ['discussion', 'support', 'bug', 'feature-request'], - license_id: licenses(:cc_by_sa).id } } + post :toggle_comments, params: { id: posts(:question_one).id } + assert_response 404 + assert_not_nil assigns(:post) + assert_not assigns(:post).comments_disabled + end + + test 'specifying delete all results in comments being deleted' do + sign_in users(:moderator) + post :toggle_comments, params: { id: posts(:question_one).id, delete_all_comments: true } + assert_response 200 + assert_not_nil assigns(:post) + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal 'success', JSON.parse(response.body)['status'] + assert assigns(:post).comments_disabled + assert assigns(:post).comments.all?(&:deleted?) + end + + # Lock + + test 'can lock post' do + sign_in users(:deleter) + post :lock, params: { id: posts(:question_one).id, format: :json } + assert_response 200 assert_not_nil assigns(:post) - assert_not_nil assigns(:post).user - assert_not_nil assigns(:post).user.community_user + assert assigns(:post).locked_until <= 7.days.from_now + assert assigns(:post).locked_until >= 7.days.from_now - 1.minute + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal 'success', JSON.parse(response.body)['status'] end - test 'should prevent user with insufficient trust level posting when category requires higher' do + test 'lock requires authentication' do + post :lock, params: { id: posts(:question_one).id } + assert_response 302 + assert_redirected_to new_user_session_path + end + + test 'unprivileged user cannot lock' do sign_in users(:standard_user) - post :create, params: { category_id: categories(:high_trust).id, post_type_id: post_types(:question).id, - post: { body_markdown: 'ABCD EFGH IJKL MNOP QRST UVWX YZ', title: 'ABCD EFGH IJKL M', - tags_cache: ['discussion', 'support', 'bug', 'feature-request'], - license_id: licenses(:cc_by_sa).id } } - assert_response 403 + post :lock, params: { id: posts(:question_one).id, format: :json } + assert_response 404 + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal 'failed', JSON.parse(response.body)['status'] + end + + test 'cannot lock locked post' do + sign_in users(:deleter) + post :lock, params: { id: posts(:locked).id, format: :json } + assert_response 404 + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal 'failed', JSON.parse(response.body)['status'] + end + + test 'cannot lock longer than 30 days' do + sign_in users(:deleter) + post :lock, params: { id: posts(:question_one).id, length: 60, format: :json } + assert_response 200 assert_not_nil assigns(:post) - assert_equal true, assigns(:post).errors.any? - assert_equal true, assigns(:post).errors.full_messages[0].start_with?("You don't have a high enough trust level") + assert assigns(:post).locked_until <= 30.days.from_now + assert assigns(:post).locked_until >= 30.days.from_now - 1.minute + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal 'success', JSON.parse(response.body)['status'] end - test 'should change category' do + test 'moderator can lock longer than 30 days' do + sign_in users(:moderator) + post :lock, params: { id: posts(:question_one).id, length: 60, format: :json } + assert_response 200 + assert_not_nil assigns(:post) + assert assigns(:post).locked_until <= 60.days.from_now + assert assigns(:post).locked_until >= 60.days.from_now - 1.minute + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal 'success', JSON.parse(response.body)['status'] + end + + test 'moderator can lock indefinitely' do + sign_in users(:moderator) + post :lock, params: { id: posts(:question_one).id, format: :json } + assert_response 200 + assert_not_nil assigns(:post) + assert_nil assigns(:post).locked_until + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal 'success', JSON.parse(response.body)['status'] + end + + # Unlock + + test 'can unlock post' do sign_in users(:deleter) - post :change_category, params: { id: posts(:article_one).id, target_id: categories(:articles_only).id } + posts(:locked).update(locked_until: 2.days.from_now) + post :unlock, params: { id: posts(:locked).id, format: :json } assert_response 200 assert_not_nil assigns(:post) - assert_not_nil assigns(:target) assert_nothing_raised do JSON.parse(response.body) end - assert_equal categories(:articles_only).id, assigns(:post).category_id + assert_equal 'success', JSON.parse(response.body)['status'] end - test 'should deny change category to unprivileged' do + test 'unlock requires authentication' do + post :unlock, params: { id: posts(:locked).id } + assert_response 302 + assert_redirected_to new_user_session_path + end + + test 'unprivileged user cannot unlock' do sign_in users(:standard_user) - post :change_category, params: { id: posts(:article_one).id, target_id: categories(:articles_only).id } - assert_response 403 + post :unlock, params: { id: posts(:locked).id, format: :json } + assert_response 404 assert_nothing_raised do JSON.parse(response.body) end - assert_equal ["You don't have permission to make that change."], JSON.parse(response.body)['errors'] + assert_equal 'failed', JSON.parse(response.body)['status'] end - test 'should refuse to change category of wrong post type' do + test 'cannot unlock unlocked post' do sign_in users(:deleter) - post :change_category, params: { id: posts(:question_one).id, target_id: categories(:articles_only).id } - assert_response 409 + post :unlock, params: { id: posts(:question_one).id, format: :json } + assert_response 404 assert_nothing_raised do JSON.parse(response.body) end - assert_equal ["This post type is not allowed in the #{categories(:articles_only).name} category."], - JSON.parse(response.body)['errors'] + assert_equal 'failed', JSON.parse(response.body)['status'] + end + + test 'cannot unlock post locked by moderator' do + sign_in users(:deleter) + posts(:locked_mod).update(locked_until: 2.days.from_now) + post :unlock, params: { id: posts(:locked_mod).id, format: :json } + assert_response 404 + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal 'failed', JSON.parse(response.body)['status'] + assert_equal ['locked_by_mod'], JSON.parse(response.body)['errors'] + end + + # Feature + + test 'can feature post' do + sign_in users(:moderator) + before_audits = AuditLog.count + post :feature, params: { id: posts(:question_one).id } + assert_response 200 + assert_not_nil assigns(:post) + assert_not_nil assigns(:link).id + assert_equal before_audits + 1, AuditLog.count, 'AuditLog not created on post feature' + end + + test 'feature requires authentication' do + post :feature, params: { id: posts(:question_one).id } + assert_response 302 + assert_redirected_to new_user_session_path + end + + test 'regular user cannot feature' do + sign_in users(:deleter) + post :feature, params: { id: posts(:question_one).id, format: :json } + assert_response 404 + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal ['no_privilege'], JSON.parse(response.body)['errors'] + end + + # Save draft + + test 'can save draft' do + sign_in users(:standard_user) + post :save_draft, params: { path: 'test', post: 'test' } + assert_response 200 + assert_nothing_raised do + JSON.parse(response.body) + end + assert_equal "saved_post.#{users(:standard_user).id}.test", JSON.parse(response.body)['key'] + assert_equal 'test', RequestContext.redis.get(JSON.parse(response.body)['key']) + end + + # Delete draft + + test 'can delete draft' do + sign_in users(:standard_user) + post :delete_draft, params: { path: 'test' } + assert_response 200 end end diff --git a/test/controllers/questions_controller_test.rb b/test/controllers/questions_controller_test.rb index 0cf44210f05184eb5a142c79e0fdb2cbefcb722b..5164c708ef072ffa37b48bdd24dfc1f81e27968a 100644 --- a/test/controllers/questions_controller_test.rb +++ b/test/controllers/questions_controller_test.rb @@ -3,185 +3,4 @@ require 'test_helper' class QuestionsControllerTest < ActionController::TestCase include Devise::Test::ControllerHelpers include ApplicationTestHelper - - test 'should get index' do - get :index - assert_not_nil assigns(:questions) - assert_equal Question.undeleted.count, assigns(:questions).size - assert_response(200) - end - - test 'should get show question page' do - get :show, params: { id: posts(:question_one).id } - assert_not_nil assigns(:question) - assert_not_nil assigns(:answers) - assert_response(200) - end - - test 'should get show question page with deleted question' do - sign_in users(:deleter) - get :show, params: { id: posts(:deleted).id } - assert_not_nil assigns(:question) - assert_not_nil assigns(:answers) - assert_response(200) - end - - test 'should get show question page with closed question' do - sign_in users(:closer) - get :show, params: { id: posts(:closed).id } - assert_response 200 - assert_not_nil assigns(:question) - assert_not_nil assigns(:answers) - end - - test 'should get tagged page' do - get :tagged, params: { tag: 'discussion', tag_set: tag_sets(:main).id } - assert_not_nil assigns(:questions) - assert_response(200) - end - - test 'should get edit question page' do - sign_in users(:editor) - get :edit, params: { id: posts(:question_one).id } - assert_not_nil assigns(:question) - assert_response(200) - end - - test 'should update existing question' do - sign_in users(:editor) - patch :update, params: { id: posts(:question_one).id, question: { title: 'ABCDEF GHIJKL MNOPQR', - body_markdown: 'ABCDEF GHIJKL MNOPQR STUVWX YZ', - tags_cache: ['discussion', 'support'] } } - assert_not_nil assigns(:question) - assert_array_equal ['discussion', 'support'], assigns(:question).tags_cache - assert_array_equal ['discussion', 'support'], assigns(:question).tags.map(&:name) - assert_response(302) - end - - test 'should mark question deleted' do - sign_in users(:deleter) - delete :destroy, params: { id: posts(:question_one).id } - assert_not_nil assigns(:question) - assert_equal true, assigns(:question).deleted - assert_response(302) - end - - test 'should mark question undeleted' do - sign_in users(:deleter) - delete :undelete, params: { id: posts(:question_one).id } - assert_not_nil assigns(:question) - assert_equal false, assigns(:question).deleted - assert_response(302) - end - - test 'should require authentication to get edit question page' do - sign_out :user - get :edit, params: { id: posts(:question_one).id } - assert_response(302) - end - - test 'should require authentication to update existing question' do - sign_out :user - patch :update, params: { id: posts(:question_one).id } - assert_response(302) - end - - test 'should require authentication to mark question deleted' do - sign_out :user - delete :destroy, params: { id: posts(:question_one).id } - assert_response(302) - end - - test 'should require authentication to undelete question' do - sign_out :user - delete :undelete, params: { id: posts(:question_one).id } - assert_response(302) - end - - test 'should require delete privileges to mark question deleted' do - sign_in users(:editor) - delete :destroy, params: { id: posts(:question_one).id } - assert_response(302) - assert_not_nil flash[:danger] - end - - test 'should require delete privileges to undelete question' do - sign_in users(:editor) - delete :undelete, params: { id: posts(:question_one).id } - assert_response(302) - assert_not_nil flash[:danger] - end - - test 'should require viewdeleted privileges to view deleted question' do - sign_in users(:editor) - get :show, params: { id: posts(:deleted).id } - assert_response(403) - end - - test 'should close question' do - sign_in users(:closer) - patch :close, params: { id: posts(:question_one).id, reason_id: close_reasons(:not_good).id } - assert_not_nil assigns(:question) - assert_equal true, assigns(:question).closed - assert_nothing_raised do - JSON.parse(response.body) - end - assert_equal 'success', JSON.parse(response.body)['status'] - assert_response(200) - end - - test 'should reopen question' do - sign_in users(:closer) - patch :reopen, params: { id: posts(:closed).id } - assert_not_nil assigns(:question) - assert_equal false, assigns(:question).closed - assert_response(302) - end - - test 'should require authentication to close question' do - sign_out :user - patch :close, params: { id: posts(:question_one).id } - assert_response(302) - end - - test 'should require authentication to reopen question' do - sign_out :user - patch :reopen, params: { id: posts(:closed).id } - assert_response(302) - end - - test 'should require privileges to close question' do - sign_in users(:standard_user) - patch :close, params: { id: posts(:question_one).id } - assert_equal false, assigns(:question).closed - assert_response(403) - end - - test 'should require privileges to reopen question' do - sign_in users(:standard_user) - patch :reopen, params: { id: posts(:closed).id } - assert_equal true, assigns(:question).closed - assert_response(302) - end - - test 'should prevent closed questions being closed' do - sign_in users(:closer) - patch :close, params: { id: posts(:closed).id } - assert_equal true, assigns(:question).closed - assert_response(400) - end - - test 'should prevent open questions being reopened' do - sign_in users(:closer) - patch :reopen, params: { id: posts(:question_one).id } - assert_equal false, assigns(:question).closed - 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/controllers/subscriptions_controller_test.rb b/test/controllers/subscriptions_controller_test.rb index d9afb8f497e183453b4e25b42180316836c9f11c..898730b31091b4f05089e73d285b8878fb784713 100644 --- a/test/controllers/subscriptions_controller_test.rb +++ b/test/controllers/subscriptions_controller_test.rb @@ -20,8 +20,8 @@ class SubscriptionsControllerTest < ActionController::TestCase get :index assert_response 200 assert_not_nil assigns(:subscriptions) - assert !assigns(:subscriptions).empty?, - '@subscriptions instance variable expected size > 0, got <= 0' + assert_not assigns(:subscriptions).empty?, + '@subscriptions instance variable expected size > 0, got <= 0' end test 'should get new when logged in' do diff --git a/test/fixtures/categories.yml b/test/fixtures/categories.yml index 8110d53a851b764b5d80d5ec6e3c5183dbd92703..5669e36b4f47099b43bdd50227c02333a0817546 100644 --- a/test/fixtures/categories.yml +++ b/test/fixtures/categories.yml @@ -36,7 +36,7 @@ high_trust: - question - answer tag_set: main - min_trust_level: 6 + min_trust_level: 3 license: cc_by_sa admin_only: diff --git a/test/fixtures/post_types.yml b/test/fixtures/post_types.yml index 71c578bf15cd658dd8bb260ec97eaa0f725e874c..dea8f1923fecae1422e2a244f1099156f06cd0ef 100644 --- a/test/fixtures/post_types.yml +++ b/test/fixtures/post_types.yml @@ -1,14 +1,77 @@ question: name: Question + description: ~ + has_answers: true + has_votes: true + has_tags: true + has_parent: false + has_category: true + has_license: true + is_public_editable: true + is_closeable: true + is_top_level: true answer: name: Answer + description: ~ + has_answers: false + has_votes: true + has_tags: false + has_parent: true + has_category: true + has_license: true + is_public_editable: true + is_closeable: false + is_top_level: false article: name: Article + description: ~ + has_answers: false + has_votes: true + has_tags: true + has_parent: false + has_category: true + has_license: true + is_public_editable: true + is_closeable: false + is_top_level: true policy_doc: name: PolicyDoc + description: ~ + has_answers: false + has_votes: false + has_tags: false + has_parent: false + has_category: false + has_license: false + is_public_editable: false + is_closeable: false + is_top_level: false help_doc: - name: HelpDoc \ No newline at end of file + name: HelpDoc + description: ~ + has_answers: false + has_votes: false + has_tags: false + has_parent: false + has_category: false + has_license: false + is_public_editable: false + is_closeable: false + is_top_level: false + +blog_post: + name: BlogPost + description: ~ + has_answers: false + has_votes: true + has_tags: true + has_parent: false + has_category: true + has_license: true + is_public_editable: false + is_closeable: false + is_top_level: true \ No newline at end of file diff --git a/test/fixtures/posts.yml b/test/fixtures/posts.yml index 9781f1877fdf599b79e799c2944f771ba5626923..54e50a21a3d00b8259dea8ec0b6219eff0a82e25 100644 --- a/test/fixtures/posts.yml +++ b/test/fixtures/posts.yml @@ -40,6 +40,27 @@ question_two: upvote_count: 0 downvote_count: 0 +bad_answers: + post_type: question + 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.5 + user: standard_user + community: sample + category: main + license: cc_by_sa + upvote_count: 0 + downvote_count: 0 + deleted: post_type: question title: Q3D ZY XWVUTS RQPONM LKJIHG FEDCBA @@ -64,6 +85,30 @@ deleted: upvote_count: 0 downvote_count: 0 +deleted_mod: + post_type: question + title: Q3D ZY XWVUTS RQPONM LKJIHG FEDCBA + body: ZY XWVUTS RQPONM LKJIHG FEDCBA ZY XWVUTS RQPONM LKJIHG FEDCBA + body_markdown: ZY XWVUTS RQPONM LKJIHG FEDCBA ZY XWVUTS RQPONM LKJIHG FEDCBA + tags_cache: + - discussion + - support + - bug + tags: + - discussion + - support + - bug + score: 0.5 + user: standard_user + deleted: true + deleted_at: 2019-01-01T00:00:00.000000Z + deleted_by: moderator + community: sample + category: main + license: cc_by_sa + upvote_count: 0 + downvote_count: 0 + closed: post_type: question title: Q4C ABCDEF GHIJKL MNOPQR STUVWX YZ @@ -88,6 +133,56 @@ closed: upvote_count: 0 downvote_count: 0 +locked: + post_type: question + title: Q4C ABCDEF GHIJKL MNOPQR STUVWX YZ + body: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ + body_markdown: ZY XWVUTS RQPONM LKJIHG FEDCBA ZY XWVUTS RQPONM LKJIHG FEDCBA + tags_cache: + - discussion + - support + - bug + tags: + - discussion + - support + - bug + score: 0.5 + locked: true + locked_by: deleter + locked_at: 2019-01-01T00:00:00.000000Z + locked_until: 2020-01-01T00:00:00.000000Z + user: standard_user + community: sample + category: main + license: cc_by_sa + upvote_count: 0 + downvote_count: 0 + +locked_mod: + post_type: question + title: LM ABCDEF GHIJKL MNOPQR STUVWX YZ + body: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ + body_markdown: ZY XWVUTS RQPONM LKJIHG FEDCBA ZY XWVUTS RQPONM LKJIHG FEDCBA + tags_cache: + - discussion + - support + - bug + tags: + - discussion + - support + - bug + score: 0.5 + locked: true + locked_by: moderator + locked_at: 2019-01-01T00:00:00.000000Z + locked_until: 2020-01-01T00:00:00.000000Z + user: standard_user + community: sample + category: main + license: cc_by_sa + upvote_count: 0 + downvote_count: 0 + answer_one: post_type: answer body: A1 ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ @@ -114,6 +209,19 @@ answer_two: upvote_count: 0 downvote_count: 0 +bad_answer: + post_type: answer + body: A2 ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ + body_markdown: ZY XWVUTS RQPONM LKJIHG FEDCBA ZY XWVUTS RQPONM LKJIHG FEDCBA + score: 0.4 + parent: question_one + user: editor + community: sample + category: main + license: cc_by_sa + upvote_count: 0 + downvote_count: 1 + really_old_answer: post_type: answer body: A3RO ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ @@ -208,3 +316,52 @@ deleted_article: deleted_by: deleter upvote_count: 0 downvote_count: 0 + +help_article: + post_type: help_doc + body: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ + body_markdown: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ + user: system + community: sample + help_category: Site Information + help_ordering: 99 + doc_slug: sample + +mod_help_article: + post_type: help_doc + body: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ + body_markdown: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ + user: system + community: sample + help_category: $Moderator + help_ordering: 99 + doc_slug: sample-mod + +disabled_help_article: + post_type: help_doc + body: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ + body_markdown: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ + user: system + community: sample + help_category: $Disabled + help_ordering: 99 + doc_slug: sample-disable + +blog_post: + post_type: blog_post + title: B1 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 + user: closer + community: sample + category: main + license: cc_by_sa + diff --git a/test/fixtures/user_abilities.yml b/test/fixtures/user_abilities.yml index 0cbcccbe2b9316ab8bac1b363e6b4ef1c6f15eb7..6d4b9dd5ec1b1e896aaf5a0ec2ea92fd31b15781 100644 --- a/test/fixtures/user_abilities.yml +++ b/test/fixtures/user_abilities.yml @@ -1,10 +1,3 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html - -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# - stu_eo: community_user: sample_standard_user ability: everyone diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index 3bbd83aa9950307b2ccc8e82ff07b76c03ef3ac4..b3b65427bca634104cb3728508140c486d7cfec1 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -75,3 +75,22 @@ global_admin: is_global_admin: true is_global_moderator: false confirmed_at: 2020-01-01T00:00:00.000000Z + +no_community_user: + email: no_community_user@qpixel-test.net + encrypted_password: abcdefghijklmnopqrstuvwxyz + sign_in_count: 1337 + username: no_community_user + is_global_admin: false + is_global_moderator: false + confirmed_at: 2020-01-01T00:00:00.000000Z + +system: + id: -99 + email: system@qpixel-test.net + encrypted_password: abcdefghijklmnopqrstuvwxyz + sign_in_count: 1337 + username: system + is_global_admin: true + is_global_moderator: true + confirmed_at: 2020-01-01T00:00:00.000000Z diff --git a/test/models/site_setting_test.rb b/test/models/site_setting_test.rb index e13bd026f76bcddb0919ee5ffef5009c3e62a670..053c540094dd6efda9c8172f4d6b14bbd80d3c3e 100644 --- a/test/models/site_setting_test.rb +++ b/test/models/site_setting_test.rb @@ -34,18 +34,18 @@ class SiteSettingTest < ActiveSupport::TestCase test 'community settings are in the default scope' do SiteSetting.create(community_id: RequestContext.community, name: 'test', value: 'true', value_type: 'string') - assert SiteSetting.where(name: 'test').exists? + assert SiteSetting.exists?(name: 'test') end test 'global settings are in the default scope' do SiteSetting.create(community_id: nil, name: 'test', value: 'true', value_type: 'string') - assert SiteSetting.where(name: 'test').exists? + assert SiteSetting.exists?(name: 'test') end test 'external community settings are not in the default scope' do other_community = Community.create(host: 'other', name: 'other') SiteSetting.create(community_id: other_community, name: 'test', value: 'true', value_type: 'string') - assert SiteSetting.where(name: 'test').exists? + assert SiteSetting.exists?(name: 'test') end test 'community setting takes precedence over global setting' do diff --git a/test/models/subscription_test.rb b/test/models/subscription_test.rb index 7fdeec1c2650d48e46d2c5e32a985f0afbfbcaa5..da38c75449f337a8600a03c913bfef4c3004c9ea 100644 --- a/test/models/subscription_test.rb +++ b/test/models/subscription_test.rb @@ -10,14 +10,14 @@ class SubscriptionTest < ActiveSupport::TestCase test 'subscription to all should return some questions' do questions = subscriptions(:all).questions assert_not_nil questions - assert !questions.empty?, 'No questions returned' + assert_not questions.empty?, 'No questions returned' assert questions.size <= 100, 'Too many questions returned' end test 'tag subscription should return only tag questions' do questions = subscriptions(:tag).questions assert_not_nil questions - assert !questions.empty?, 'No questions returned' + assert_not questions.empty?, 'No questions returned' assert questions.size <= 100, 'Too many questions returned' questions.each do |question| assert question.tags.map(&:name).include?(subscriptions(:tag).qualifier), @@ -28,7 +28,7 @@ class SubscriptionTest < ActiveSupport::TestCase test 'user subscription should return only user questions' do questions = subscriptions(:user).questions assert_not_nil questions - assert !questions.empty?, 'No questions returned' + assert_not questions.empty?, 'No questions returned' assert questions.size <= 100, 'Too many questions returned' questions.each do |question| assert question.user_id == subscriptions(:user).qualifier.to_i, diff --git a/test/test_helper.rb b/test/test_helper.rb index 6c11d6e003367fb0fd967d6e849a47a0dfcd8b55..cab55a4fef2de24cf658f5dd684db4eb615fb07e 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -35,6 +35,21 @@ class ActiveSupport::TestCase Ability.create(a.attributes.merge(community_id: community_id, id: nil)) end end + + def sample + OpenStruct.new( + title: 'This is a sample title', + body_markdown: 'This is a sample post with some **Markdown** and [a link](/).', + body: '<p>This is a sample post with some <b>Markdown</b> and <a href="/">a link</a></p>', + tags_cache: ['discussion', 'posts', 'tags'], + edit: OpenStruct.new( + title: 'This is another sample title', + body_markdown: 'This is a sample post with some more **Markdown** and [a link](/).', + body: '<p>This is a sample post with some more <b>Markdown</b> and <a href="/">a link</a></p>', + tags_cache: ['discussion', 'posts', 'tags', 'edits'] + ) + ) + end end class ActionController::TestCase