diff --git a/app/controllers/sidebar_sections_controller.rb b/app/controllers/sidebar_sections_controller.rb index 73c7571724b..6990d20ca82 100644 --- a/app/controllers/sidebar_sections_controller.rb +++ b/app/controllers/sidebar_sections_controller.rb @@ -50,6 +50,18 @@ class SidebarSectionsController < ApplicationController render json: failed_json, status: 403 end + def reset + sidebar_section = SidebarSection.find_by(id: params[:id]) + raise Discourse::InvalidParameters if !sidebar_section + @guardian.ensure_can_edit!(sidebar_section) + + case sidebar_section.section_type + when "community" + sidebar_section.reset_community! + end + render_serialized(sidebar_section.reload, SidebarSectionSerializer) + end + def reorder sidebar_section = SidebarSection.find_by(id: reorder_params["sidebar_section_id"]) @guardian.ensure_can_edit!(sidebar_section) diff --git a/app/models/sidebar_section.rb b/app/models/sidebar_section.rb index ce6f5cc4621..dddbbdde720 100644 --- a/app/models/sidebar_section.rb +++ b/app/models/sidebar_section.rb @@ -26,6 +26,33 @@ class SidebarSection < ActiveRecord::Base scope :public_sections, -> { where("public") } enum :section_type, { community: 0 }, scopes: false, suffix: true + def reset_community! + ActiveRecord::Base.transaction do + self.update!(title: "Community") + self.sidebar_section_links.destroy_all + community_urls = + SidebarUrl::COMMUNITY_SECTION_LINKS.map do |url_data| + "('#{url_data[:name]}', '#{url_data[:path]}', '#{url_data[:icon]}', '#{url_data[:segment]}', false, now(), now())" + end + + result = DB.query <<~SQL + INSERT INTO sidebar_urls(name, value, icon, segment, external, created_at, updated_at) + VALUES #{community_urls.join(",")} + RETURNING sidebar_urls.id + SQL + + sidebar_section_links = + result.map.with_index do |url, index| + "(-1, #{url.id}, 'SidebarUrl', #{self.id}, #{index}, now(), now())" + end + + DB.query <<~SQL + INSERT INTO sidebar_section_links(user_id, linkable_id, linkable_type, sidebar_section_id, position, created_at, updated_at) + VALUES #{sidebar_section_links.join(",")} + SQL + end + end + private def set_system_user_for_public_section diff --git a/app/models/sidebar_url.rb b/app/models/sidebar_url.rb index a0a1292e9be..aa50a12fe90 100644 --- a/app/models/sidebar_url.rb +++ b/app/models/sidebar_url.rb @@ -1,20 +1,47 @@ # frozen_string_literal: true class SidebarUrl < ActiveRecord::Base + enum :segment, { primary: 0, secondary: 1 }, scopes: false, suffix: true + FULL_RELOAD_LINKS_REGEX = [%r{\A/my/[a-z_\-/]+\z}, %r{\A/safe-mode\z}] MAX_ICON_LENGTH = 40 MAX_NAME_LENGTH = 80 MAX_VALUE_LENGTH = 200 COMMUNITY_SECTION_LINKS = [ - { name: "Everything", path: "/latest", icon: "layer-group", segment: "primary" }, - { name: "My Posts", path: "/my/activity", icon: "user", segment: "primary" }, - { name: "Review", path: "/review", icon: "flag", segment: "primary" }, - { name: "Admin", path: "/admin", icon: "wrench", segment: "primary" }, - { name: "Users", path: "/u", icon: "users", segment: "secondary" }, - { name: "About", path: "/about", icon: "info-circle", segment: "secondary" }, - { name: "FAQ", path: "/faq", icon: "question-circle", segment: "secondary" }, - { name: "Groups", path: "/g", icon: "user-friends", segment: "secondary" }, - { name: "Badges", path: "/badges", icon: "certificate", segment: "secondary" }, + { + name: "Everything", + path: "/latest", + icon: "layer-group", + segment: SidebarUrl.segments["primary"], + }, + { + name: "My Posts", + path: "/my/activity", + icon: "user", + segment: SidebarUrl.segments["primary"], + }, + { name: "Review", path: "/review", icon: "flag", segment: SidebarUrl.segments["primary"] }, + { name: "Admin", path: "/admin", icon: "wrench", segment: SidebarUrl.segments["primary"] }, + { name: "Users", path: "/u", icon: "users", segment: SidebarUrl.segments["secondary"] }, + { + name: "About", + path: "/about", + icon: "info-circle", + segment: SidebarUrl.segments["secondary"], + }, + { + name: "FAQ", + path: "/faq", + icon: "question-circle", + segment: SidebarUrl.segments["secondary"], + }, + { name: "Groups", path: "/g", icon: "user-friends", segment: SidebarUrl.segments["secondary"] }, + { + name: "Badges", + path: "/badges", + icon: "certificate", + segment: SidebarUrl.segments["secondary"], + }, ] validates :icon, presence: true, length: { maximum: MAX_ICON_LENGTH } @@ -25,8 +52,6 @@ class SidebarUrl < ActiveRecord::Base before_validation :remove_internal_hostname, :set_external - enum :segment, { primary: 0, secondary: 1 }, scopes: false, suffix: true - def path_validator return true if !external? raise ActionController::RoutingError.new("Not Found") if value !~ Discourse::Utils::URI_REGEXP diff --git a/config/routes.rb b/config/routes.rb index 2c9ae21d87c..0e114877734 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1594,6 +1594,7 @@ Discourse::Application.routes.draw do resources :sidebar_sections, only: %i[index create update destroy] post "/sidebar_sections/reorder" => "sidebar_sections#reorder" + put "/sidebar_sections/reset/:id" => "sidebar_sections#reset" get "*url", to: "permalinks#show", constraints: PermalinkConstraint.new end diff --git a/lib/guardian/sidebar_guardian.rb b/lib/guardian/sidebar_guardian.rb index 5f55d4f465d..c7a85810a92 100644 --- a/lib/guardian/sidebar_guardian.rb +++ b/lib/guardian/sidebar_guardian.rb @@ -2,16 +2,17 @@ module SidebarGuardian def can_create_public_sidebar_section? - @user.staff? + @user.admin? end def can_edit_sidebar_section?(sidebar_section) - return @user.staff? if sidebar_section.public? + return @user.admin? if sidebar_section.public? + return @user.admin? if sidebar_section.section_type is_my_own?(sidebar_section) end def can_delete_sidebar_section?(sidebar_section) - return @user.staff? if sidebar_section.public? + return @user.admin? if sidebar_section.public? is_my_own?(sidebar_section) end end diff --git a/spec/models/sidebar_section_spec.rb b/spec/models/sidebar_section_spec.rb index 5db3cef5c28..7261efb0f48 100644 --- a/spec/models/sidebar_section_spec.rb +++ b/spec/models/sidebar_section_spec.rb @@ -3,10 +3,25 @@ RSpec.describe SidebarSection do fab!(:user) { Fabricate(:user) } fab!(:sidebar_section) { Fabricate(:sidebar_section, user: user) } + let(:community_section) do + SidebarSection.find_by(section_type: SidebarSection.section_types[:community]) + end it "uses system user for public sections" do expect(sidebar_section.user_id).to eq(user.id) sidebar_section.update!(public: true) expect(sidebar_section.user_id).to eq(Discourse.system_user.id) end + + it "resets Community section to the default state" do + community_section.update!(title: "test") + community_section.sidebar_section_links.first.linkable.update!(name: "everything edited") + community_section.sidebar_section_links.last.destroy! + + community_section.reset_community! + expect(community_section.reload.title).to eq("Community") + expect(community_section.sidebar_section_links.all.map { |link| link.linkable.name }).to eq( + ["Everything", "My Posts", "Review", "Admin", "Users", "About", "FAQ", "Groups", "Badges"], + ) + end end diff --git a/spec/requests/sidebar_sections_controller_spec.rb b/spec/requests/sidebar_sections_controller_spec.rb index d87caf53224..87f8d3ad6b4 100644 --- a/spec/requests/sidebar_sections_controller_spec.rb +++ b/spec/requests/sidebar_sections_controller_spec.rb @@ -3,6 +3,7 @@ RSpec.describe SidebarSectionsController do fab!(:user) { Fabricate(:user) } fab!(:admin) { Fabricate(:admin) } + fab!(:moderator) { Fabricate(:moderator) } describe "#index" do fab!(:sidebar_section) { Fabricate(:sidebar_section, title: "private section", user: user) } @@ -100,6 +101,20 @@ RSpec.describe SidebarSectionsController do expect(response.status).to eq(403) end + it "does not allow moderator to create public section" do + sign_in(moderator) + post "/sidebar_sections.json", + params: { + title: "custom section", + public: true, + links: [ + { icon: "link", name: "categories", value: "/categories" }, + { icon: "address-book", name: "tags", value: "/tags" }, + ], + } + expect(response.status).to eq(403) + end + it "allows admin to create public section" do sign_in(admin) post "/sidebar_sections.json", @@ -334,5 +349,43 @@ RSpec.describe SidebarSectionsController do expect(response.status).to eq(403) end + + it "doesn't allow moderator to delete public sidebar section" do + sign_in(moderator) + sidebar_section.update!(public: true) + delete "/sidebar_sections/#{sidebar_section.id}.json" + + expect(response.status).to eq(403) + end + end + + describe "#reset" do + let(:community_section) do + SidebarSection.find_by(section_type: SidebarSection.section_types[:community]) + end + let(:everything_link) { community_section.sidebar_section_links.first } + + it "doesn't allow user to reset community section" do + sign_in(user) + SidebarSection.any_instance.expects(:reset_community!).never + put "/sidebar_sections/reset/#{community_section.id}.json" + expect(response.status).to eq(403) + end + + it "doesn't allow staff to reset community section" do + sign_in(moderator) + SidebarSection.any_instance.expects(:reset_community!).never + put "/sidebar_sections/reset/#{community_section.id}.json" + expect(response.status).to eq(403) + end + + it "allows admins to reset community section to default" do + sign_in(admin) + SidebarSection.any_instance.expects(:reset_community!).once + put "/sidebar_sections/reset/#{community_section.id}.json" + expect(response.status).to eq(200) + expect(response.parsed_body["sidebar_section"]["id"]).to eq(community_section.id) + expect(response.parsed_body["sidebar_section"]["title"]).to eq(community_section.title) + end end end