diff --git a/app/assets/javascripts/moderator.js b/app/assets/javascripts/moderator.js index ea6e95d50b44e39161f16adeded83eb984deeb15..3f92f61303a82955fc443bbb36d502e009865f39 100644 --- a/app/assets/javascripts/moderator.js +++ b/app/assets/javascripts/moderator.js @@ -1,5 +1,5 @@ $(() => { - $('.js-convert-to-comment, .js-toggle-comments').on('ajax:success', ev => { + $('.js-convert-to-comment, .js-toggle-comments, .js-feature-post').on('ajax:success', ev => { location.reload(); }); }); \ No newline at end of file diff --git a/app/assets/javascripts/pinned_links.coffee b/app/assets/javascripts/pinned_links.coffee new file mode 100644 index 0000000000000000000000000000000000000000..24f83d18bbd38c24c4f7c3c2fc360cd68e857a2a --- /dev/null +++ b/app/assets/javascripts/pinned_links.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index fe592dab45dde77b2483e2c92eca24a1b18c6ebb..48069d35470d677ea62d5352c7e6c5987f7f9a6f 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -182,4 +182,11 @@ img { .droppanel { z-index: 3; +} + +.widget .widget--body + .widget--header { + border-top: 1px solid #d0d9dd; +} +.widget.is-tertiary .widget--body + .widget--header { + border-top: 1px solid #9daeb7; } \ No newline at end of file diff --git a/app/assets/stylesheets/pinned_links.scss b/app/assets/stylesheets/pinned_links.scss new file mode 100644 index 0000000000000000000000000000000000000000..78805fa39484626dcffd98e19c94cc27c91d653f --- /dev/null +++ b/app/assets/stylesheets/pinned_links.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the PinnedLinks controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/controllers/advertisement_controller.rb b/app/controllers/advertisement_controller.rb index 754d2fdf8e923faf2ee131bc3be2e9ff80351d26..579f9aaf3241492f88ec66353641469cf87c6437 100644 --- a/app/controllers/advertisement_controller.rb +++ b/app/controllers/advertisement_controller.rb @@ -1,6 +1,7 @@ require 'rmagick' # Neccessary due to rmagick +# rubocop:disable Metrics/ClassLength # rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/BlockLength @@ -146,10 +147,29 @@ class AdvertisementController < ApplicationController end end + def specific_category + @category = Category.find(params[:id]) + @post = Rails.cache.fetch "community/#{RequestContext.community_id}/ca_random_category_post/#{params[:id]}", + expires_in: 5.minutes do + select_random_post(@category) + end + if @post.question? + question_ad(@post) + elsif @post.article? + article_ad(@post) + else + not_found + end + end + def random_question - @post = Rails.cache.fetch "community/#{RequestContext.community_id}/random_hot_post", expires_in: 5.minutes do - @hot_questions.sample + @post = Rails.cache.fetch "community/#{RequestContext.community_id}/ca_random_hot_post", expires_in: 5.minutes do + select_random_post end + if @post.nil? + return community + end + if @post.question? question_ad(@post) elsif @post.article? @@ -161,6 +181,17 @@ class AdvertisementController < ApplicationController private + def select_random_post(category = nil) + 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) + .where(post_type_id: Question.post_type_id) + .where(category: category) + .where('score > ?', SiteSetting['HotPostsScoreThreshold']) + .order('score DESC').limit(SiteSetting['HotQuestionsCount']).all.sample + end + def wrap_text(text, width, font_size) columns = (width * 2.0 / font_size).to_i # Source: http://viseztrance.com/2011/03/texts-over-multiple-lines-with-rmagick.html @@ -327,5 +358,6 @@ class AdvertisementController < ApplicationController end end # rubocop:enable Metrics/MethodLength +# rubocop:enable Metrics/ClassLength # rubocop:enable Metrics/AbcSize # rubocop:enable Metrics/BlockLength diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ad159bf37c2f6b0556456810697432cc49e0e2d0..67c1ca2dd9fc49b032df382b1b17e4c49d9f3a49 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -108,7 +108,7 @@ class ApplicationController < ActionController::Base setup_request_context || return setup_user - pull_hot_questions + pull_pinned_links_and_hot_questions pull_categories if user_signed_in? && (current_user.is_moderator || current_user.is_admin) @@ -150,12 +150,19 @@ class ApplicationController < ActionController::Base end end - def pull_hot_questions + def pull_pinned_links_and_hot_questions + @pinned_links = Rails.cache.fetch("#{RequestContext.community_id}/pinned_links", expires_in: 2.hours) do + Rack::MiniProfiler.step 'pinned_links: cache miss' do + PinnedLink.where(active: true).where('shown_before IS NULL OR shown_before > NOW()').all + end + 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) - .where(post_type_id: Question.post_type_id).includes(:category) - .order('score DESC').limit(SiteSetting['HotQuestionsCount']) + .where(post_type_id: Question.post_type_id) + .where(category: Category.where(use_for_hot_posts: true)) + .where('score > ?', SiteSetting['HotPostsScoreThreshold']) + .order('score DESC').limit(SiteSetting['HotQuestionsCount']).all end end end diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index c1b4f2a952cfa4aad7b4aa1bdca52cfeae5bf808..b9d0fa28f47dbce5cb1f0accdcc6fbbf8b24e594 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -92,6 +92,8 @@ class CategoriesController < ApplicationController def category_params params.require(:category).permit(:name, :short_wiki, :tag_set_id, :is_homepage, :min_trust_level, :button_text, :color_code, :min_view_trust_level, :license_id, :sequence, + :asking_guidance_override, :answering_guidance_override, + :use_for_hot_posts, :use_for_advertisement, display_post_types: [], post_type_ids: [], required_tag_ids: [], topic_tag_ids: [], moderator_tag_ids: []) end diff --git a/app/controllers/moderator_controller.rb b/app/controllers/moderator_controller.rb index 9affbebdf5291e87a6ed8431c5ffaf9c2324a914..d5d7fc8c898440dbb657064e16fb110277071d98 100644 --- a/app/controllers/moderator_controller.rb +++ b/app/controllers/moderator_controller.rb @@ -5,21 +5,8 @@ class ModeratorController < ApplicationController def index; end - def recently_deleted_questions - @questions = Question.unscoped.where(deleted: true).order('deleted_at DESC') - .paginate(page: params[:page], per_page: 50) - end - - def recently_deleted_answers - @answers = Answer.where(deleted: true).order('deleted_at DESC').paginate(page: params[:page], per_page: 50) - end - - def recently_undeleted_questions - @questions = Question.unscoped.where(deleted: false).where.not(deleted_at: nil) - .paginate(page: params[:page], per_page: 50) - end - - def recently_undeleted_answers - @answers = Answer.where(deleted: false).where.not(deleted_at: nil).paginate(page: params[:page], per_page: 50) + def recently_deleted_posts + @posts = Post.unscoped.where(community: @community, deleted: true).order('deleted_at DESC') + .paginate(page: params[:page], per_page: 50) end end diff --git a/app/controllers/pinned_links_controller.rb b/app/controllers/pinned_links_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..64b7139fffee33c729f4fadf796f6e2031db2e79 --- /dev/null +++ b/app/controllers/pinned_links_controller.rb @@ -0,0 +1,79 @@ +class PinnedLinksController < ApplicationController + before_action :verify_moderator + before_action :set_pinned_link, only: [:edit, :update] + + def index + links = if current_user.is_global_moderator && params[:global] == '2' + PinnedLink.unscoped + elsif current_user.is_global_moderator && params[:global] == '1' + PinnedLink.where(community: nil) + else + PinnedLink.where(community: @community) + end + @links = if params[:filter] == 'all' + links.all + elsif params[:filter] == 'inactive' + links.where(active: false).all + else + links.where(active: true).all + end + render layout: 'without_sidebar' + end + + def new + @link = PinnedLink.new + end + + def create + data = pinned_link_params + post = !data[:post_id].present? ? nil : Post.where(data[:post_id]).first + community = !data[:community].present? ? nil : Community.where(data[:community]).first + + @link = PinnedLink.create data.merge(post: post, community: community) + + attr = @link.attributes.map { |k, v| "#{k}: #{v}" }.join(' ') + AuditLog.moderator_audit(event_type: 'pinned_link_create', related: @link, user: current_user, + comment: "<<PinnedLink #{attr}>>") + + flash[:success] = 'Your pinned link has been created. Due to caching, it may take some time until it is shown.' + redirect_to pinned_links_path + end + + def edit + unless current_user.is_global_moderator + return not_found if @link.community.id != @community.id + end + end + + def update + unless current_user.is_global_moderator + return not_found if @link.community.id != @community.id + end + + before = @link.attributes.map { |k, v| "#{k}: #{v}" }.join(' ') + data = pinned_link_params + post = !data[:post_id].present? ? nil : Post.where(data[:post_id]).first + community = !data[:community].present? ? nil : Community.where(data[:community]).first + @link.update data.merge(post: post, community: community) + after = @link.attributes.map { |k, v| "#{k}: #{v}" }.join(' ') + AuditLog.moderator_audit(event_type: 'pinned_link_update', related: @link, user: current_user, + comment: "from <<PinnedLink #{before}>>\nto <<PinnedLink #{after}>>") + + flash[:success] = 'The pinned link has been updated. Due to caching, it may take some time until it is shown.' + redirect_to pinned_links_path + end + + private + + def set_pinned_link + @link = PinnedLink.find params[:id] + end + + def pinned_link_params + if current_user.is_global_moderator + params.require(:pinned_link).permit(:label, :link, :post, :active, :shown_before, :shown_after, :community) + else + params.require(:pinned_link).permit(:label, :link, :post, :active, :shown_before, :shown_after) + end + end +end diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index f8fac7e708d27a4bba723efa29a4a4890f5180e8..381789634f88254ee59e1029be8a65ad860f19f2 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -1,6 +1,6 @@ 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] + before_action :set_post, only: [:edit_help, :update_help, :toggle_comments, :feature] 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] @@ -166,6 +166,22 @@ class PostsController < ApplicationController render json: { success: true } end + def feature + data = { + label: @post.parent.nil? ? @post.title : @post.parent.title, + link: helpers.generic_show_link(@post), + post: @post, + active: true + } + @link = PinnedLink.create data + + attr = @link.attributes.map { |k, v| "#{k}: #{v}" }.join(' ') + AuditLog.moderator_audit(event_type: 'pinned_link_create', related: @link, user: current_user, + 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 } + end + def save_draft key = "saved_post.#{current_user.id}.#{params[:path]}" saved_at = "saved_post_at.#{current_user.id}.#{params[:path]}" diff --git a/app/helpers/pinned_links_helper.rb b/app/helpers/pinned_links_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..4bae07f413c33b863345765386726352c2383e76 --- /dev/null +++ b/app/helpers/pinned_links_helper.rb @@ -0,0 +1,2 @@ +module PinnedLinksHelper +end diff --git a/app/models/pinned_link.rb b/app/models/pinned_link.rb new file mode 100644 index 0000000000000000000000000000000000000000..03cd6abd79f2a70ebcef48d84cb5375075d09694 --- /dev/null +++ b/app/models/pinned_link.rb @@ -0,0 +1,4 @@ +class PinnedLink < ApplicationRecord + include MaybeCommunityRelated + belongs_to :post +end diff --git a/app/views/advertisement/index.html.erb b/app/views/advertisement/index.html.erb index 4a52c7940603fb0a59500ba8a59184bf493c2fa1..6152f28bf560dde23b45afa276ae2514a9ac97a2 100644 --- a/app/views/advertisement/index.html.erb +++ b/app/views/advertisement/index.html.erb @@ -34,20 +34,42 @@ <div class="grid--cell is-12 is-4-lg"> <div class="widget"> <div class="widget--header h-ta-center h-fw-bold"> - Posts + Random Posts </div> <a href="<%= random_question_ads_url %>"><img src="<%= random_question_ads_url %>" alt="Random posts ad"></a> <div class="widget--body"> - <p><strong>Random post</strong> (chosen from hot posts)</p> <p>Image link</p> <pre><code><%= random_question_ads_url %></code></pre> <p>Image HTML:</p> <pre><code><%= ('<img src="' + random_question_ads_url + '" alt="' + @community.name + ' questions">') %></code></pre> - <p><strong>Specific post</strong> (replace X with post ID)</p> + </div> + </div> + </div> + <div class="grid--cell is-12 is-4-lg"> + <div class="widget"> + <div class="widget--header h-ta-center h-fw-bold"> + Specific Post + </div> + <div class="widget--body"> <p>Image link</p> <pre><code><%= specific_question_ads_url('X') %></code></pre> <p>Image HTML:</p> <pre><code><%= ('<img src="' + specific_question_ads_url('X') + '" alt="(choose an alt text)">') %></code></pre> + <p><em>You need to replace X with the post ID</em></p> + </div> + </div> + </div> + <div class="grid--cell is-12 is-4-lg"> + <div class="widget"> + <div class="widget--header h-ta-center h-fw-bold"> + Random from Category + </div> + <div class="widget--body"> + <p>Image link</p> + <pre><code><%= specific_category_ads_url('X') %></code></pre> + <p>Image HTML:</p> + <pre><code><%= ('<img src="' + specific_category_ads_url('X') + '" alt="(choose an alt text)">') %></code></pre> + <p><em>You need to replace X with the category ID</em></p> </div> </div> </div> diff --git a/app/views/categories/_form.html.erb b/app/views/categories/_form.html.erb index daf36e9451107c67bb35e60f9855ea50362b142f..097316ac3075309ced3c8427658f061da9ac5cc8 100644 --- a/app/views/categories/_form.html.erb +++ b/app/views/categories/_form.html.erb @@ -90,6 +90,40 @@ <%= f.number_field :sequence, class: 'form-element' %> </div> + <div class="form-group"> + <%= f.label :asking_guidance_override, class: 'form-element' %> + <span class="form-caption"> + This field overrides the default asking guidance and is shown when a new post is created. Leave blank to use site-default. + </span> + <%= f.text_area :asking_guidance_override, class: 'form-element' %> + </div> + + <div class="form-group"> + <%= f.label :answering_guidance_override, class: 'form-element' %> + <span class="form-caption"> + This field overrides the default answering guidance and is shown when a new answer is created. Leave blank to use site-default. + </span> + <%= f.text_area :answering_guidance_override, class: 'form-element' %> + </div> + + <div class="form-group"> + <%= f.label :use_for_hot_posts, class: 'form-element' %> + <span class="form-caption"> + Whether the posts of this category are eligible to be selected as hot posts. + </span> + <%= f.select :use_for_hot_posts, options_for_select([['yes', true], ['no', false]], selected: @category.use_for_hot_posts), + {}, class: 'form-element' %> + </div> + + <div class="form-group"> + <%= f.label :use_for_advertisement, class: 'form-element' %> + <span class="form-caption"> + Whether the posts of this category are eligible to be selected as random advertisement. + </span> + <%= f.select :use_for_advertisement, options_for_select([['yes', true], ['no', false]], selected: @category.use_for_advertisement), + {}, class: 'form-element' %> + </div> + <div class="form-group js-category-tags-group"> <%= f.label :required_tag_ids, 'Required tags', class: 'form-element' %> <span class="form-caption js-tags-group-caption"> diff --git a/app/views/layouts/_sidebar.html.erb b/app/views/layouts/_sidebar.html.erb index 9c990d439daab610e200c883e1a3601daca0340c..ab409b348e2d49b7fcc76509de758688de13ccd6 100644 --- a/app/views/layouts/_sidebar.html.erb +++ b/app/views/layouts/_sidebar.html.erb @@ -12,19 +12,48 @@ <% end %> <% end %> - <% if Rails.env.development? || @hot_questions.to_a.size > 0 %> - <div class="widget has-margin-4"> - <h4 class="widget--header has-margin-0">Hot Posts</h4> + <% if Rails.env.development? || @hot_questions.to_a.size > 0 || @pinned_links.to_a.size > 0 %> + <div class="widget has-margin-4 is-tertiary"> + <% if Rails.env.development? || @pinned_links.to_a.size > 0 %> + <div class="widget--header">Featured</div> + <% @pinned_links.each do |pl| %> + <div class="widget--body"> + <% pl_link = pl.post.nil? ? pl.link : generic_share_link(pl.post) %> + <% pl_label = pl.post.nil? ? pl.label : (pl.post.parent.nil? ? pl.post.title : pl.post.parent.title) %> + <%= link_to pl_link, class: 'h-fw-bold' do %> + <%= pl_label %> + <% end %> + <% unless pl.shown_before.nil? %> + <div> + — + <% if !pl.shown_after.nil? %> + <% if pl.shown_after < DateTime.now %> + ends in <%= time_ago_in_words(pl.shown_before) %> + <% else %> + starts in <%= time_ago_in_words(pl.shown_after) %> + <% end %> + <% else %> + in <%= time_ago_in_words(pl.shown_before) %> + <% end %> + </div> + <% end %> + </div> + <% end %> + <% end %> + <% if Rails.env.development? || @hot_questions.to_a.size > 0 %> + <div class="widget--header">Hot Posts</div> <% @hot_questions.each do |hq| %> <div class="widget--body"> + <% unless hq.category.nil? %> + <%= hq.category.name %> + — + <% end %> <%= link_to question_path(hq) do %> - <% unless hq.category.nil? %> - <span class="badge is-tag is-master-tag"><%= hq.category.name %></span> - <% end %> <%= hq.title %> <% end %> </div> <% end %> + <% end %> </div> <% end %> @@ -64,27 +93,18 @@ <img src="/assets/codidact.png" alt="Codidact logo" class="codidact-logo" /> </div> <div class="widget--body"> +<% chat = SiteSetting['ChatLink'] %> <p> This site is part of the <a href="https://codidact.com">Codidact network</a>. We have other sites too — take a look! </p> + <% if chat.present? %> + <p> + You can also <%= link_to 'join us in chat', chat %>! + </p> + <% end %> <p> Want to advertise this site? <%= link_to 'Use our templates!', ads_path %> </p> </div> - </div> - - <% chat = SiteSetting['ChatLink'] %> - <% if chat.present? %> - <div class="widget has-margin-4"> - <div class="widget--header"> - Chat - </div> - <div class="widget--body"> - <p> - <%= link_to 'Join us in chat!', chat %> - </p> - </div> - </div> - <% end %> -</div> \ No newline at end of file + </div> \ No newline at end of file diff --git a/app/views/moderator/index.html.erb b/app/views/moderator/index.html.erb index f731147068533592fab1a5f6f82fa293a2da333e..9538270cb3ce2effa8f97ea146ad91abf24ae54e 100644 --- a/app/views/moderator/index.html.erb +++ b/app/views/moderator/index.html.erb @@ -1,28 +1,67 @@ <% content_for :title, "Moderator Dashboard" %> <h1>Moderator Dashboard</h1> -<p>This page serves as a repository of links to the various tools you have access to as an administrative user. While +<p>This page serves as a repository of links to the various tools you have access to as a moderator. While you use these, remember that the data in them is usually moderator-only, and should likely not be shared with anyone outside of that team unless you've ensured it's safe to share.</p> -<ul> - <li><a href="/mod/flags">Flag queue</a></li> - <li>Recent deletions: <a href="/mod/deleted/questions">questions</a>, <a href="/mod/deleted/answers">answers</a></li> - <li>Recent undeletions: <a href="/mod/undeleted/questions">questions</a>, <a href="/mod/undeleted/answers">answers</a></li> - <li><%= link_to 'Create a help center page', new_help_post_path %></li> - <li><%= link_to 'Reports', users_report_path %></li> - <li><%= link_to 'Edit close reasons', close_reasons_path %></li> -</ul> - -<h3>Safe to Share</h3> -<p>Although for the most part, the information available under the /mod routes is sensitive or confidential, there are - cases where it's beneficial to share the information. If you're going to do this, make sure it's safe to share. - In general terms, this means that the information you share:</p> -<ul> - <li>should not make it easier to circumvent the system - if you know details of systems that are generally not very - well-known, it's best to keep them secret so the system can't be circumvented</li> - <li>should not contain private details of <em>any</em> user</li> - <li>should not be discourteous to anyone - talk and speculation of bans and removals is inconsiderate to a user who - can't respond</li> -</ul> -<p>Apply common sense - think of what effect this information will have before you share it. If in doubt, check it with - another member of your team.</p> + +<div class="grid"> + <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <div class="widget"> + <div class="widget--body"> + <i class="fa fa-flag"></i> + <a href="/mod/flags">Flag Queue</a> + </div> + </div> + </div> + + <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <div class="widget"> + <div class="widget--body"> + <i class="fas fa-trash"></i> + <a href="/mod/deleted">Recent Deletions</a> + </div> + </div> + </div> + + <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <div class="widget"> + <div class="widget--body"> + <i class="fas fa-file-alt"></i> + <a href="<%= new_help_post_path %>">Create Help Page</a> + </div> + </div> + </div> + + <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <div class="widget"> + <div class="widget--body"> + <i class="fas fa-chart-line"></i> + <a href="<%= users_report_path %>">Reports</a> + </div> + </div> + </div> + + <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <div class="widget"> + <div class="widget--body"> + <i class="fas fa-hand-paper"></i> + <a href="<%= close_reasons_path %>">Close Reasons</a> + </div> + </div> + </div> + + <div class="grid--cell is-4-lg is-6-md is-12-sm"> + <div class="widget"> + <div class="widget--body"> + <i class="fas fa-info"></i> + <a href="<%= pinned_links_path %>">Featured Links</a> + </div> + </div> + </div> +</div> + +<% chat = SiteSetting['ChatLink'] %> +<% if chat.present? %> +<p>As a moderator, you should join our <a href="<%= chat %>">community chat server</a>. Ping a Codidact team member there and you'll receive access to a special moderator-only lounge, where you can discuss moderation questions with your fellow moderators.</p> +<% end %> \ No newline at end of file diff --git a/app/views/moderator/recently_deleted_answers.html.erb b/app/views/moderator/recently_deleted_answers.html.erb deleted file mode 100644 index 309158a07594475d2369a0c450a644add10c37f6..0000000000000000000000000000000000000000 --- a/app/views/moderator/recently_deleted_answers.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -<% content_for :title, "Recently Deleted Answers" %> - -<h1>Recently Deleted Answers</h1> -<div class="item-list"> - <% @answers.each do |a| %> - <%= render 'answers/answer', answer: a %> - <% end %> -</div> - -<%= will_paginate @answers, renderer: BootstrapPagination::Rails %> diff --git a/app/views/moderator/recently_deleted_posts.html.erb b/app/views/moderator/recently_deleted_posts.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..f95bf2482a214b97aa2788f69178a395da5ecdb0 --- /dev/null +++ b/app/views/moderator/recently_deleted_posts.html.erb @@ -0,0 +1,10 @@ +<% content_for :title, "Recently Deleted Posts" %> + +<h1>Recently Deleted Posts</h1> +<div class="item-list"> + <% @posts.each do |post| %> + <%= render 'posts/type_agnostic', post: post %> + <% end %> +</div> + +<%= will_paginate @posts, renderer: BootstrapPagination::Rails %> diff --git a/app/views/moderator/recently_deleted_questions.html.erb b/app/views/moderator/recently_deleted_questions.html.erb deleted file mode 100644 index 6eb638fdf8d7ad4cb3118a37ae2b97cc22d9f495..0000000000000000000000000000000000000000 --- a/app/views/moderator/recently_deleted_questions.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -<% content_for :title, "Recently Deleted Questions" %> - -<h1>Recently Deleted Questions</h1> -<div class="item-list"> - <% @questions.each do |q| %> - <%= render 'posts/list', post: q %> - <% end %> -</div> - -<%= will_paginate @questions, renderer: BootstrapPagination::Rails %> diff --git a/app/views/moderator/recently_undeleted_answers.html.erb b/app/views/moderator/recently_undeleted_answers.html.erb deleted file mode 100644 index 9045379d8f4adf5e5f4479e16649b8f7d30ae2c5..0000000000000000000000000000000000000000 --- a/app/views/moderator/recently_undeleted_answers.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -<% content_for :title, "Recently Undeleted Answers" %> - -<h1>Recently Undeleted Answers</h1> -<div class="item-list"> - <% @answers.each do |a| %> - <%= render 'answers/answer', answer: a %> - <% end %> -</div> - -<%= will_paginate @answers, renderer: BootstrapPagination::Rails %> diff --git a/app/views/moderator/recently_undeleted_questions.html.erb b/app/views/moderator/recently_undeleted_questions.html.erb deleted file mode 100644 index 387ff73e0b3c71a1c0d71b777e0678cf2b6be1e8..0000000000000000000000000000000000000000 --- a/app/views/moderator/recently_undeleted_questions.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -<% content_for :title, "Recently Undeleted Questions" %> - -<h1>Recently Undeleted Questions</h1> -<div class="item-list"> - <% @questions.each do |q| %> - <%= render 'posts/list', post: q %> - <% end %> -</div> - -<%= will_paginate @questions, renderer: BootstrapPagination::Rails %> diff --git a/app/views/pinned_links/_form.html.erb b/app/views/pinned_links/_form.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..1452dde029e30d3a19b24098d9eba4418db3bc3f --- /dev/null +++ b/app/views/pinned_links/_form.html.erb @@ -0,0 +1,52 @@ +<%= form_for @link, url: url do |f| %> + <div class="form-group has-padding-1"> + <%= f.label :label, "Shown label", class: "form-element" %> + <div class="form-caption">What is shown in the sidebar. This will be ignored when a post is set.</div> + <%= f.text_area :label, class: "form-element" %> + </div> + + <div class="form-group has-padding-1"> + <%= f.label :link, "Target link", class: "form-element" %> + <div class="form-caption">What the label is linked to in the sidebar. This will be ignored when a post is set.</div> + <%= f.text_field :link, class: "form-element" %> + </div> + + <% if current_user.is_global_moderator %> + <div class="form-group has-padding-1"> + <%= f.label :community, "Community", class: "form-element" %> + <div class="form-caption">Which sidebar this is shown in the sidebar. Global if blank.</div> + <% communities = Community.all %> + <% ocs = communities.map { |c| [c.name, c.id] } %> + <%= f.select :community, options_for_select(ocs, selected: @link.community&.id), + { include_blank: true }, class: 'form-element' %> + </div> + <% end %> + + <div class="form-group has-padding-1"> + <%= f.label :post_id, "ID of target post", class: "form-element" %> + <div class="form-caption">You can link to a post within this community. Will override label and link.</div> + <%= f.number_field :post_id, class: "form-element" %> + </div> + + <div class="form-group has-padding-1"> + <%= f.label :active, "Active?", class: "form-element" %> + <%= f.select :active, options_for_select([['yes', true], ['no', false]], selected: @link.active), + {}, class: 'form-element' %> + </div> + + <div class="form-group has-padding-1"> + <%= f.label :shown_before, "End date", class: "form-element" %> + <div class="form-caption">Link will show until this date. Will be shown in the sidebar.</div> + <%= f.date_field :shown_before, class: "form-element" %> + </div> + + <div class="form-group has-padding-1"> + <%= f.label :shown_after, "Start date", class: "form-element" %> + <div class="form-caption">Used to display in the sidebar when the event starts. Does not affect visibility - the event + will show immediately.</div> + <%= f.date_field :shown_after, class: "form-element" %> + </div> + + <%= f.submit "Update", class: "button is-filled" %> + <%= link_to "Cancel", pinned_links_path(global: params[:global]), class: "button" %> +<% end %> \ No newline at end of file diff --git a/app/views/pinned_links/edit.html.erb b/app/views/pinned_links/edit.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..eafe82cc755ce626ca40c61d50a86b24ced75215 --- /dev/null +++ b/app/views/pinned_links/edit.html.erb @@ -0,0 +1,14 @@ +<h1>Edit featured link</h1> + +<% if @link.errors.any? %> + <div class="notice is-danger"> + These errors prevented this link being saved: + <ul> + <% @link.errors.full_messages.each do |msg| %> + <li><%= msg %></li> + <% end %> + </ul> + </div> +<% end %> + +<%= render 'form', url: update_pinned_link_path(global: params[:global]) %> diff --git a/app/views/pinned_links/index.html.erb b/app/views/pinned_links/index.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..f95ebe133bf392335db55889f5e7f6ddad626614 --- /dev/null +++ b/app/views/pinned_links/index.html.erb @@ -0,0 +1,87 @@ +<h1>Featured Links</h1> +<p class="is-lead">Featured links allow you as a moderator to draw attention to issues or posts of importance to the community. Use them sparingly and only when needed, because they tend to get ignored if used too often.</p> + +<div class="grid has-margin-bottom-4"> + <% if current_user.is_global_moderator %> + <div class="grid--cell"> + <div class="button-list is-gutterless"> + <%= link_to "current community", query_url(global: '0', filter: params[:filter] || 'active'), + class: "button is-muted is-outlined #{(params[:global] == '0' || params[:global].nil?) ? 'is-active' : ''}" %> + <%= link_to "global", query_url(global: '1', filter: params[:filter] || 'active'), + class: "button is-muted is-outlined #{(params[:global] == '1') ? 'is-active' : ''}" %> + <%= link_to "everywhere", query_url(global: '2', filter: params[:filter] || 'active'), + class: "button is-muted is-outlined #{(params[:global] == '2') ? 'is-active' : ''}" %> + </div> + </div> + <% end %> + <div class="grid--cell"> + <div class="button-list is-gutterless"> + <%= link_to "active", query_url(global: params[:global] || '0', filter: 'active'), + class: "button is-muted is-outlined #{(params[:filter] == 'active' || params[:filter].nil?) ? 'is-active' : ''}" %> + <%= link_to "inactive", query_url(global: params[:global] || '0', filter: 'inactive'), + class: "button is-muted is-outlined #{(params[:filter] == 'inactive') ? 'is-active' : ''}" %> + <%= link_to "all", query_url(global: params[:global] || '0', filter: 'all'), + class: "button is-muted is-outlined #{(params[:filter] == 'all') ? 'is-active' : ''}" %> + </div> + </div> + <div class="grid--cell is-flexible"> + </div> + <div class="grid--cell"> + <div class="button-list is-gutterless"> + <a href="<%= new_pinned_link_path %>" class="button is-filled"><i class="fa fa-plus"></i> New</a> + </div> + </div> +</div> + +<table class="table is-with-hover"> + <tr> + <th>type</th> + <th>shown label</th> + <th>links to</th> + <th>begins</th> + <th>ends</th> + <th>active?</th> + <% if current_user.is_global_moderator %> + <th>community</th> + <% end %> + <th>actions</th> + </tr> + <% @links.each do |pl| %> + <% pl_link = pl.post.nil? ? pl.link : ('Post #' + pl.post.id.to_s) %> + <% pl_label = pl.post.nil? ? pl.label : (pl.post.parent.nil? ? pl.post.title : pl.post.parent.title) %> + <tr> + <td> + <% if pl.shown_before.nil? %> + <% if pl.post.nil? %> + link + <% else %> + post + <% end%> + <% else %> + event + <% end%> + </td> + <td> + <%= pl_label %> + </td> + <td> + <%= pl_link %> + </td> + <td> + <%= pl.shown_after ? pl.shown_after : raw('—') %> + </td> + <td> + <%= pl.shown_before ? pl.shown_before : raw('—') %> + </td> + <td> + <%= pl.active ? raw('<strong>yes</strong>') : 'no' %> + </td> + <% if current_user.is_global_moderator %> + <td><%= pl.community.nil? ? 'global' : pl.community.name %></td> + <% end %> + <td> + <a href="<%= edit_pinned_link_path(pl.id) %>" class="button is-outlined">edit</a> + </td> + </tr> + <% end %> +</table> \ No newline at end of file diff --git a/app/views/pinned_links/new.html.erb b/app/views/pinned_links/new.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..e719f8c89efe4496cf77e9d446676a4e1ef6279a --- /dev/null +++ b/app/views/pinned_links/new.html.erb @@ -0,0 +1,14 @@ +<h1>New featured link</h1> + +<% if @link.errors.any? %> + <div class="notice is-danger"> + These errors prevented this link being saved: + <ul> + <% @link.errors.full_messages.each do |msg| %> + <li><%= msg %></li> + <% end %> + </ul> + </div> +<% end %> + +<%= render 'form', url: create_pinned_link_path %> \ No newline at end of file diff --git a/app/views/posts/_post_tools.html.erb b/app/views/posts/_post_tools.html.erb index 3273a069cb1736afbf69fd0aeaf1dc1b753a310e..199e7291be1524697142da83049b69edebadbb9c 100644 --- a/app/views/posts/_post_tools.html.erb +++ b/app/views/posts/_post_tools.html.erb @@ -69,6 +69,17 @@ <% end %> </details> <% end %> + <% if moderator? %> + <details> + <summary>Feature post</summary> + <p> + You can feature this post by linking it in the sidebar. You can edit the link options later in the moderator tools. + </p> + <%= form_tag post_feature_url(post), remote: true, class: 'js-feature-post' do %> + <%= submit_tag 'Feature post', class: 'button is-filled' %> + <% end %> + </details> + <% end %> </div> </div> </div> \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 4d0ec2de22e79fe348167d2dfe3d8e280f2231c6..451a0cbe3167a822cf48c3d2d33f576be2e711a0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -44,10 +44,7 @@ Rails.application.routes.draw do end get 'mod', to: 'moderator#index', as: :moderator - get 'mod/deleted/questions', to: 'moderator#recently_deleted_questions', as: :recently_deleted_questions - get 'mod/deleted/answers', to: 'moderator#recently_deleted_answers', as: :recently_deleted_answers - get 'mod/undeleted/questions', to: 'moderator#recently_undeleted_questions', as: :recently_undeleted_questions - get 'mod/undeleted/answers', to: 'moderator#recently_undeleted_answers', as: :recently_undeleted_answers + get 'mod/deleted', to: 'moderator#recently_deleted_posts', as: :recently_deleted_posts get 'mod/flags', to: 'flags#queue', as: :flag_queue post 'mod/flags/:id/resolve', to: 'flags#resolve', as: :resolve_flag get 'mod/votes', to: 'suspicious_votes#index', as: :suspicious_votes @@ -55,6 +52,14 @@ Rails.application.routes.draw do get 'mod/votes/user/:id', to: 'suspicious_votes#user', as: :suspicious_votes_user delete 'mod/users/destroy/:id', to: 'users#destroy', as: :destroy_user + scope 'mod/featured' do + root to: 'pinned_links#index', as: :pinned_links + get 'new', to: 'pinned_links#new', as: :new_pinned_link + post 'new', to: 'pinned_links#create', as: :create_pinned_link + get ':id/edit', to: 'pinned_links#edit', as: :edit_pinned_link + 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 @@ -95,6 +100,7 @@ Rails.application.routes.draw do 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/feature', to: 'posts#feature', as: :post_feature get 'posts/suggested-edit/:id', to: 'suggested_edit#show', as: :suggested_edit @@ -217,6 +223,7 @@ Rails.application.routes.draw do get 'community.png', to: 'advertisement#community', as: :community_ads get 'posts/random.png', to: 'advertisement#random_question', as: :random_question_ads get 'posts/:id.png', to: 'advertisement#specific_question', as: :specific_question_ads + get 'category/:id.png', to: 'advertisement#specific_category', as: :specific_category_ads end get '403', to: 'errors#forbidden' diff --git a/db/migrate/20200727183756_create_pinned_links.rb b/db/migrate/20200727183756_create_pinned_links.rb new file mode 100644 index 0000000000000000000000000000000000000000..a7d390475892ecccca9668cae7499e6f08c34494 --- /dev/null +++ b/db/migrate/20200727183756_create_pinned_links.rb @@ -0,0 +1,14 @@ +class CreatePinnedLinks < ActiveRecord::Migration[5.2] + def change + create_table :pinned_links do |t| + t.references :community, null: true, foreign_key: true + t.string :label, null: true + t.string :link, null: true + t.references :post, null: true, foreign_key: true + t.boolean :active + t.datetime :shown_after, null: true + t.datetime :shown_before, null: true + t.timestamps + end + end +end diff --git a/db/migrate/20200728093322_add_eligible_for_hot_post_to_category.rb b/db/migrate/20200728093322_add_eligible_for_hot_post_to_category.rb new file mode 100644 index 0000000000000000000000000000000000000000..9fadf7741f3c05aca7cd04dc428d9f4748951142 --- /dev/null +++ b/db/migrate/20200728093322_add_eligible_for_hot_post_to_category.rb @@ -0,0 +1,6 @@ +class AddEligibleForHotPostToCategory < ActiveRecord::Migration[5.2] + def change + add_column :categories, :use_for_hot_posts, :boolean, default: true + add_column :categories, :use_for_advertisement, :boolean, default: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 8182de617f09dc27edf077780215d865e7f1e3b0..ed909ff11df7ca5b7690c626271a416982283ae1 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_07_21_185230) do +ActiveRecord::Schema.define(version: 2020_07_28_093322) do create_table "active_storage_attachments", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", force: :cascade do |t| t.string "name", null: false @@ -77,6 +77,8 @@ ActiveRecord::Schema.define(version: 2020_07_21_185230) do t.integer "min_view_trust_level" t.bigint "license_id" t.integer "sequence" + t.boolean "use_for_hot_posts", default: true + t.boolean "use_for_advertisement", default: true t.index ["community_id"], name: "index_categories_on_community_id" t.index ["license_id"], name: "index_categories_on_license_id" t.index ["sequence"], name: "index_categories_on_sequence" @@ -205,6 +207,20 @@ ActiveRecord::Schema.define(version: 2020_07_21_185230) do t.index ["user_id"], name: "index_notifications_on_user_id" end + create_table "pinned_links", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", force: :cascade do |t| + t.bigint "community_id" + t.string "label" + t.string "link" + t.bigint "post_id" + t.boolean "active" + t.datetime "shown_after" + t.datetime "shown_before" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["community_id"], name: "index_pinned_links_on_community_id" + t.index ["post_id"], name: "index_pinned_links_on_post_id" + end + create_table "post_histories", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", force: :cascade do |t| t.integer "post_history_type_id" t.integer "user_id" @@ -264,7 +280,7 @@ ActiveRecord::Schema.define(version: 2020_07_21_185230) do t.integer "post_type_id", null: false t.text "body_markdown" t.integer "answer_count", default: 0, null: false - t.datetime "last_activity", default: -> { "CURRENT_TIMESTAMP" }, null: false + t.datetime "last_activity", default: -> { "current_timestamp()" }, null: false t.string "att_source" t.string "att_license_name" t.string "att_license_link" @@ -501,6 +517,8 @@ ActiveRecord::Schema.define(version: 2020_07_21_185230) do add_foreign_key "error_logs", "users" add_foreign_key "flags", "communities" add_foreign_key "notifications", "communities" + add_foreign_key "pinned_links", "communities" + add_foreign_key "pinned_links", "posts" add_foreign_key "post_histories", "communities" add_foreign_key "post_history_tags", "post_histories" add_foreign_key "post_history_tags", "tags" diff --git a/db/seeds/categories.yml b/db/seeds/categories.yml index 3f379f31d0b3a2ae0354d3a8851a0d0fea6d4d72..17727fdf0efc870f9c76841587f9b2c8dd18df52 100644 --- a/db/seeds/categories.yml +++ b/db/seeds/categories.yml @@ -7,6 +7,8 @@ - <%= PostType['Answer'].id %> is_homepage: true tag_set_id: <%= TagSet.unscoped.where(name: 'Main').first %> + use_for_hot_posts: true + use_for_advertisement: true - name: Meta short_wiki: Discussions and feedback about the site itself in Q&A format. @@ -15,4 +17,6 @@ post_type_ids: - <%= PostType['Question'].id %> - <%= PostType['Answer'].id %> - tag_set_id: <%= TagSet.unscoped.where(name: 'Meta').first %> \ No newline at end of file + tag_set_id: <%= TagSet.unscoped.where(name: 'Meta').first %> + use_for_hot_posts: true + use_for_advertisement: false \ No newline at end of file diff --git a/db/seeds/site_settings.yml b/db/seeds/site_settings.yml index 93f3e3abbb5e1cc46736a99ea1c657983e61299e..0b2f716f9bbd684665a9a7048ae60d237d9ab3b3 100644 --- a/db/seeds/site_settings.yml +++ b/db/seeds/site_settings.yml @@ -291,3 +291,10 @@ category: SiteDetails description: > A slogan to be shown on the /ads/community.png page. + +- name: HotPostsScoreThreshold + value: 1 + value_type: integer + category: AdvancedSettings + description: > + The minimum score a question must have to qualify for selection for the Hot Posts sidebar and for being selected as random advertisement. \ No newline at end of file diff --git a/test/controllers/moderator_controller_test.rb b/test/controllers/moderator_controller_test.rb index 8528ecc9605b2dcb4e8145ca02f546bab84e9c5e..c26c6381107aebeff989b5e6de40f072b3bfa24c 100644 --- a/test/controllers/moderator_controller_test.rb +++ b/test/controllers/moderator_controller_test.rb @@ -11,8 +11,7 @@ class ModeratorControllerTest < ActionController::TestCase test 'should require authentication to access pages' do sign_out :user - [:index, :recently_deleted_answers, :recently_deleted_questions, :recently_undeleted_answers, - :recently_undeleted_questions].each do |path| + [:index, :recently_deleted_posts].each do |path| get path assert_response(404) end @@ -20,8 +19,7 @@ class ModeratorControllerTest < ActionController::TestCase test 'should require moderator status to access pages' do sign_in users(:standard_user) - [:index, :recently_deleted_answers, :recently_deleted_questions, :recently_undeleted_answers, - :recently_undeleted_questions].each do |path| + [:index, :recently_deleted_posts].each do |path| get path assert_response(404) end diff --git a/test/controllers/pinned_links_controller_test.rb b/test/controllers/pinned_links_controller_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..b7cb3d3d15993efc8ca563d798727df63861ccc7 --- /dev/null +++ b/test/controllers/pinned_links_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class PinnedLinksControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/fixtures/pinned_links.yml b/test/fixtures/pinned_links.yml new file mode 100644 index 0000000000000000000000000000000000000000..80aed36e30b2598726b55a90c65850a8f9aeb609 --- /dev/null +++ b/test/fixtures/pinned_links.yml @@ -0,0 +1,11 @@ +# 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 +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/models/pinned_link_test.rb b/test/models/pinned_link_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..66d150710199f79202dbbc2c268f97d8843c91b2 --- /dev/null +++ b/test/models/pinned_link_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class PinnedLinkTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end