diff --git a/app/assets/javascripts/discourse/app/models/category.js b/app/assets/javascripts/discourse/app/models/category.js index 9ed351759c2..4de28e476d4 100644 --- a/app/assets/javascripts/discourse/app/models/category.js +++ b/app/assets/javascripts/discourse/app/models/category.js @@ -204,6 +204,8 @@ const Category = RestModel.extend({ custom_fields: this.custom_fields, topic_template: this.topic_template, all_topics_wiki: this.all_topics_wiki, + allow_unlimited_owner_edits_on_first_post: this + .allow_unlimited_owner_edits_on_first_post, allowed_tags: this.allowed_tags, allowed_tag_groups: this.allowed_tag_groups, allow_global_tags: this.allow_global_tags, diff --git a/app/assets/javascripts/discourse/app/templates/components/edit-category-settings.hbs b/app/assets/javascripts/discourse/app/templates/components/edit-category-settings.hbs index bf8eac383dd..78c3414a364 100644 --- a/app/assets/javascripts/discourse/app/templates/components/edit-category-settings.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/edit-category-settings.hbs @@ -77,6 +77,13 @@ {{i18n "category.all_topics_wiki"}} + +
+ +
diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index b632d6b49b9..72508092ee5 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -296,6 +296,7 @@ class CategoriesController < ApplicationController :email_in_allow_strangers, :mailinglist_mirror, :all_topics_wiki, + :allow_unlimited_owner_edits_on_first_post, :parent_category_id, :auto_close_hours, :auto_close_based_on_last_post, diff --git a/app/models/category.rb b/app/models/category.rb index 7085aa30848..315a4fa2b35 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -957,67 +957,68 @@ end # # Table name: categories # -# id :integer not null, primary key -# name :string(50) not null -# color :string(6) default("0088CC"), not null -# topic_id :integer -# topic_count :integer default(0), not null -# created_at :datetime not null -# updated_at :datetime not null -# user_id :integer not null -# topics_year :integer default(0) -# topics_month :integer default(0) -# topics_week :integer default(0) -# slug :string not null -# description :text -# text_color :string(6) default("FFFFFF"), not null -# read_restricted :boolean default(FALSE), not null -# auto_close_hours :float -# post_count :integer default(0), not null -# latest_post_id :integer -# latest_topic_id :integer -# position :integer -# parent_category_id :integer -# posts_year :integer default(0) -# posts_month :integer default(0) -# posts_week :integer default(0) -# email_in :string -# email_in_allow_strangers :boolean default(FALSE) -# topics_day :integer default(0) -# posts_day :integer default(0) -# allow_badges :boolean default(TRUE), not null -# name_lower :string(50) not null -# auto_close_based_on_last_post :boolean default(FALSE) -# topic_template :text -# contains_messages :boolean -# sort_order :string -# sort_ascending :boolean -# uploaded_logo_id :integer -# uploaded_background_id :integer -# topic_featured_link_allowed :boolean default(TRUE) -# all_topics_wiki :boolean default(FALSE), not null -# show_subcategory_list :boolean default(FALSE) -# num_featured_topics :integer default(3) -# default_view :string(50) -# subcategory_list_style :string(50) default("rows_with_featured_topics") -# default_top_period :string(20) default("all") -# mailinglist_mirror :boolean default(FALSE), not null -# minimum_required_tags :integer default(0), not null -# navigate_to_first_post_after_read :boolean default(FALSE), not null -# search_priority :integer default(0) -# allow_global_tags :boolean default(FALSE), not null -# reviewable_by_group_id :integer -# required_tag_group_id :integer -# min_tags_from_required_group :integer default(1), not null -# read_only_banner :string -# default_list_filter :string(20) default("all") +# id :integer not null, primary key +# name :string(50) not null +# color :string(6) default("0088CC"), not null +# topic_id :integer +# topic_count :integer default(0), not null +# created_at :datetime not null +# updated_at :datetime not null +# user_id :integer not null +# topics_year :integer default(0) +# topics_month :integer default(0) +# topics_week :integer default(0) +# slug :string not null +# description :text +# text_color :string(6) default("FFFFFF"), not null +# read_restricted :boolean default(FALSE), not null +# auto_close_hours :float +# post_count :integer default(0), not null +# latest_post_id :integer +# latest_topic_id :integer +# position :integer +# parent_category_id :integer +# posts_year :integer default(0) +# posts_month :integer default(0) +# posts_week :integer default(0) +# email_in :string +# email_in_allow_strangers :boolean default(FALSE) +# topics_day :integer default(0) +# posts_day :integer default(0) +# allow_badges :boolean default(TRUE), not null +# name_lower :string(50) not null +# auto_close_based_on_last_post :boolean default(FALSE) +# topic_template :text +# contains_messages :boolean +# sort_order :string +# sort_ascending :boolean +# uploaded_logo_id :integer +# uploaded_background_id :integer +# topic_featured_link_allowed :boolean default(TRUE) +# all_topics_wiki :boolean default(FALSE), not null +# show_subcategory_list :boolean default(FALSE) +# num_featured_topics :integer default(3) +# default_view :string(50) +# subcategory_list_style :string(50) default("rows_with_featured_topics") +# default_top_period :string(20) default("all") +# mailinglist_mirror :boolean default(FALSE), not null +# minimum_required_tags :integer default(0), not null +# navigate_to_first_post_after_read :boolean default(FALSE), not null +# search_priority :integer default(0) +# allow_global_tags :boolean default(FALSE), not null +# reviewable_by_group_id :integer +# required_tag_group_id :integer +# min_tags_from_required_group :integer default(1), not null +# read_only_banner :string +# default_list_filter :string(20) default("all") +# allow_unlimited_owner_edits_on_first_post :boolean default(FALSE) # # Indexes # # index_categories_on_email_in (email_in) UNIQUE -# index_categories_on_forum_thread_count (topic_count) # index_categories_on_reviewable_by_group_id (reviewable_by_group_id) # index_categories_on_search_priority (search_priority) +# index_categories_on_topic_count (topic_count) # unique_index_categories_on_name (COALESCE(parent_category_id, '-1'::integer), name) UNIQUE -# unique_index_categories_on_slug (COALESCE(parent_category_id, '-1'::integer), slug) UNIQUE WHERE ((slug)::text <> ''::text) +# unique_index_categories_on_slug (COALESCE(parent_category_id, '-1'::integer), lower((slug)::text)) UNIQUE WHERE ((slug)::text <> ''::text) # diff --git a/app/models/topic.rb b/app/models/topic.rb index 18cb65fbc05..341839cf082 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -1418,6 +1418,10 @@ class Topic < ActiveRecord::Base category && category.read_restricted end + def category_allows_unlimited_owner_edits_on_first_post? + category && category.allow_unlimited_owner_edits_on_first_post? + end + def acting_user @acting_user || user end diff --git a/app/serializers/category_serializer.rb b/app/serializers/category_serializer.rb index e4428f2e3fe..4789df1cba9 100644 --- a/app/serializers/category_serializer.rb +++ b/app/serializers/category_serializer.rb @@ -12,6 +12,7 @@ class CategorySerializer < SiteCategorySerializer :email_in_allow_strangers, :mailinglist_mirror, :all_topics_wiki, + :allow_unlimited_owner_edits_on_first_post, :can_delete, :cannot_delete_reason, :is_special, diff --git a/app/services/post_action_notifier.rb b/app/services/post_action_notifier.rb index 056a58c48d1..881af4953f9 100644 --- a/app/services/post_action_notifier.rb +++ b/app/services/post_action_notifier.rb @@ -110,7 +110,10 @@ class PostActionNotifier user_ids << post.user_id end - if post.wiki && post.is_first_post? + # Notify all users watching the topic when the OP of a wiki topic is edited + # or if the topic category allows unlimited owner edits on the OP. + if post.is_first_post? && + (post.wiki? || post.topic.category_allows_unlimited_owner_edits_on_first_post?) user_ids.concat( TopicUser.watching(post.topic_id) .where.not(user_id: post_revision.user_id) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 7238838f18a..7b8e6156975 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3171,6 +3171,7 @@ en: num_featured_topics: "Number of topics shown on the categories page:" subcategory_num_featured_topics: "Number of featured topics on parent category's page:" all_topics_wiki: "Make new topics wikis by default" + allow_unlimited_owner_edits_on_first_post: "Allow unlimited owner edits on first post" subcategory_list_style: "Subcategory List Style:" sort_order: "Topic List Sort By:" default_view: "Default Topic List:" diff --git a/db/migrate/20210414013318_add_category_setting_allow_unlimited_owner_edits_op.rb b/db/migrate/20210414013318_add_category_setting_allow_unlimited_owner_edits_op.rb new file mode 100644 index 00000000000..85fd8238138 --- /dev/null +++ b/db/migrate/20210414013318_add_category_setting_allow_unlimited_owner_edits_op.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddCategorySettingAllowUnlimitedOwnerEditsOp < ActiveRecord::Migration[6.0] + def change + add_column :categories, :allow_unlimited_owner_edits_on_first_post, :boolean, default: false, null: false + end +end diff --git a/lib/guardian/post_guardian.rb b/lib/guardian/post_guardian.rb index 5ee297eec0d..791ce8b98f9 100644 --- a/lib/guardian/post_guardian.rb +++ b/lib/guardian/post_guardian.rb @@ -137,6 +137,7 @@ module PostGuardian return false end + # Editing a shared draft. return true if ( can_see_post?(post) && can_create_post?(post.topic) && @@ -165,6 +166,10 @@ module PostGuardian return true end + if post.is_first_post? && post.topic.category_allows_unlimited_owner_edits_on_first_post? + return true + end + return !post.edit_time_limit_expired?(@user) end diff --git a/spec/components/guardian/user_guardian_spec.rb b/spec/components/guardian/user_guardian_spec.rb index 9cd1af1a555..ef87af6399d 100644 --- a/spec/components/guardian/user_guardian_spec.rb +++ b/spec/components/guardian/user_guardian_spec.rb @@ -455,32 +455,4 @@ describe UserGuardian do expect(guardian.can_upload_user_card_background?(trust_level_1)).to eq(false) end end - - describe '#can_edit_post?' do - fab!(:category) { Fabricate(:category) } - - let(:topic) { Fabricate(:topic, category: category) } - let(:post_with_draft) { Fabricate(:post, topic: topic) } - - before do - SiteSetting.shared_drafts_category = category.id - SiteSetting.shared_drafts_min_trust_level = '2' - Fabricate(:shared_draft, topic: topic) - end - - it 'returns true if a shared draft exists' do - expect(Guardian.new(trust_level_2).can_edit_post?(post_with_draft)).to eq(true) - end - - it 'returns false if the user has a lower trust level' do - expect(Guardian.new(trust_level_1).can_edit_post?(post_with_draft)).to eq(false) - end - - it 'returns false if the draft is from a different category' do - topic.update!(category: Fabricate(:category)) - - expect(Guardian.new(trust_level_2).can_edit_post?(post_with_draft)).to eq(false) - end - - end end diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index da2b9b59725..d829accab93 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -1492,6 +1492,33 @@ describe Guardian do expect(Guardian.new(post.user).can_edit?(post)).to be_truthy end + context "shared drafts" do + fab!(:category) { Fabricate(:category) } + + let(:topic) { Fabricate(:topic, category: category) } + let(:post_with_draft) { Fabricate(:post, topic: topic) } + + before do + SiteSetting.shared_drafts_category = category.id + SiteSetting.shared_drafts_min_trust_level = '2' + Fabricate(:shared_draft, topic: topic) + end + + it 'returns true if a shared draft exists' do + expect(Guardian.new(trust_level_2).can_edit_post?(post_with_draft)).to eq(true) + end + + it 'returns false if the user has a lower trust level' do + expect(Guardian.new(trust_level_1).can_edit_post?(post_with_draft)).to eq(false) + end + + it 'returns false if the draft is from a different category' do + topic.update!(category: Fabricate(:category)) + + expect(Guardian.new(trust_level_2).can_edit_post?(post_with_draft)).to eq(false) + end + end + context 'category group moderation is enabled' do fab!(:cat_mod_user) { Fabricate(:user) } @@ -1511,8 +1538,10 @@ describe Guardian do end describe 'post edit time limits' do + context 'post is older than post_edit_time_limit' do - let(:old_post) { build(:post, topic: topic, user: topic.user, created_at: 6.minutes.ago) } + let(:topic) { Fabricate(:topic) } + let(:old_post) { Fabricate(:post, topic: topic, user: topic.user, created_at: 6.minutes.ago) } before do topic.user.update_columns(trust_level: 1) @@ -1539,6 +1568,31 @@ describe Guardian do old_post.wiki = true expect(Guardian.new(coding_horror).can_edit?(old_post)).to be_truthy end + + context "unlimited owner edits on first post" do + let(:owner) { old_post.user } + + it "returns true when the post topic's category allow_unlimited_owner_edits_on_first_post" do + old_post.topic.category.update(allow_unlimited_owner_edits_on_first_post: true) + expect(Guardian.new(owner).can_edit?(old_post)).to be_truthy + end + + it "returns false when the post topic's category does not allow_unlimited_owner_edits_on_first_post" do + old_post.topic.category.update(allow_unlimited_owner_edits_on_first_post: false) + expect(Guardian.new(owner).can_edit?(old_post)).to be_falsey + end + + it "returns false when the post topic's category allow_unlimited_owner_edits_on_first_post but the post is not the first in the topic" do + old_post.topic.category.update(allow_unlimited_owner_edits_on_first_post: true) + new_post = Fabricate(:post, user: owner, topic: old_post.topic, created_at: 6.minutes.ago) + expect(Guardian.new(owner).can_edit?(new_post)).to be_falsey + end + + it "returns false when someone other than owner is editing and category allow_unlimited_owner_edits_on_first_post" do + old_post.topic.category.update(allow_unlimited_owner_edits_on_first_post: false) + expect(Guardian.new(coding_horror).can_edit?(old_post)).to be_falsey + end + end end context 'post is older than tl2_post_edit_time_limit' do diff --git a/spec/requests/api/schemas/json/category_create_response.json b/spec/requests/api/schemas/json/category_create_response.json index b056f0365aa..87aca679ffa 100644 --- a/spec/requests/api/schemas/json/category_create_response.json +++ b/spec/requests/api/schemas/json/category_create_response.json @@ -131,6 +131,15 @@ "min_tags_from_required_group": { "type": "integer" }, + "allowed_tags": { + "type": "array" + }, + "allowed_tag_groups": { + "type": "array" + }, + "allow_global_tags": { + "type": "boolean" + }, "required_tag_group_name": { "type": [ "string", @@ -158,6 +167,9 @@ "auto_close_based_on_last_post": { "type": "boolean" }, + "allow_unlimited_owner_edits_on_first_post": { + "type": "boolean" + }, "group_permissions": { "type": "array", "items": [ @@ -261,6 +273,7 @@ "available_groups", "auto_close_hours", "auto_close_based_on_last_post", + "allow_unlimited_owner_edits_on_first_post", "group_permissions", "email_in", "email_in_allow_strangers", diff --git a/spec/requests/api/schemas/json/category_topics_response.json b/spec/requests/api/schemas/json/category_topics_response.json index 472616b35dc..77322166c9e 100644 --- a/spec/requests/api/schemas/json/category_topics_response.json +++ b/spec/requests/api/schemas/json/category_topics_response.json @@ -46,6 +46,9 @@ "per_page": { "type": "integer" }, + "top_tags": { + "type": "array" + }, "topics": { "type": "array", "items": [ diff --git a/spec/requests/api/schemas/json/category_update_response.json b/spec/requests/api/schemas/json/category_update_response.json index 0e5220d263f..94a8d50708f 100644 --- a/spec/requests/api/schemas/json/category_update_response.json +++ b/spec/requests/api/schemas/json/category_update_response.json @@ -134,6 +134,15 @@ "min_tags_from_required_group": { "type": "integer" }, + "allowed_tags": { + "type": "array" + }, + "allowed_tag_groups": { + "type": "array" + }, + "allow_global_tags": { + "type": "boolean" + }, "required_tag_group_name": { "type": [ "string", @@ -161,6 +170,9 @@ "auto_close_based_on_last_post": { "type": "boolean" }, + "allow_unlimited_owner_edits_on_first_post": { + "type": "boolean" + }, "group_permissions": { "type": "array", "items": [ @@ -264,6 +276,7 @@ "available_groups", "auto_close_hours", "auto_close_based_on_last_post", + "allow_unlimited_owner_edits_on_first_post", "group_permissions", "email_in", "email_in_allow_strangers", diff --git a/spec/services/post_action_notifier_spec.rb b/spec/services/post_action_notifier_spec.rb index 00d326ea22a..ed63971a3b0 100644 --- a/spec/services/post_action_notifier_spec.rb +++ b/spec/services/post_action_notifier_spec.rb @@ -23,7 +23,7 @@ describe PostActionNotifier do SiteSetting.editing_grace_period_max_diff = 1 post.update!(wiki: true) - user = post.user + owner = post.user user2 = Fabricate(:user) user3 = Fabricate(:user) @@ -42,7 +42,7 @@ describe PostActionNotifier do edited_notification_type = Notification.types[:edited] expect(Notification.exists?( - user: user, + user: owner, notification_type: edited_notification_type )).to eq(true) @@ -52,7 +52,7 @@ describe PostActionNotifier do )).to eq(true) expect do - post.revise(user, raw: "I made some changes to the wiki again!") + post.revise(owner, raw: "I made some changes to the wiki again!") end.to change { Notification.where(notification_type: edited_notification_type).count }.by(1) @@ -69,7 +69,62 @@ describe PostActionNotifier do }.by(1) expect(Notification.where( - user: user, + user: owner, + notification_type: edited_notification_type + ).count).to eq(2) + end + + it 'notifies watching users of revision when topic category allow_unlimited_owner_edits_on_first_post and first post in topic is edited' do + SiteSetting.editing_grace_period_max_diff = 1 + + post.topic.update(category: Fabricate(:category, allow_unlimited_owner_edits_on_first_post: true)) + owner = post.user + user2 = Fabricate(:user) + user3 = Fabricate(:user) + + TopicUser.change(user2.id, post.topic, + notification_level: TopicUser.notification_levels[:watching] + ) + + TopicUser.change(user3.id, post.topic, + notification_level: TopicUser.notification_levels[:tracking] + ) + + expect do + post.revise(Fabricate(:user), raw: "I made some changes to the first post!") + end.to change { Notification.count }.by(2) + + edited_notification_type = Notification.types[:edited] + + expect(Notification.exists?( + user: owner, + notification_type: edited_notification_type + )).to eq(true) + + expect(Notification.exists?( + user: user2, + notification_type: edited_notification_type + )).to eq(true) + + expect do + post.revise(owner, raw: "I made some changes to the first post again!") + end.to change { + Notification.where(notification_type: edited_notification_type).count + }.by(1) + + expect(Notification.where( + user: user2, + notification_type: edited_notification_type + ).count).to eq(2) + + expect do + post.revise(user2, raw: "I changed the first post totally") + end.to change { + Notification.where(notification_type: edited_notification_type).count + }.by(1) + + expect(Notification.where( + user: owner, notification_type: edited_notification_type ).count).to eq(2) end