Skip to content

Commit 6f1474a

Browse files
authored
Merge pull request #1867 from codidact/0valt/1859/tag-drafts
Fix tag drafts not being deleted upon creation / update
2 parents 7f107ed + c01a33e commit 6f1474a

9 files changed

Lines changed: 184 additions & 50 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
module DraftManagement
2+
extend ActiveSupport::Concern
3+
4+
DRAFTABLE_FIELDS = [:body, :comment, :excerpt, :license, :saved_at, :tags, :tag_name, :title].freeze
5+
NESTED_DRAFTABLE_FIELDS = [:body, :saved_at].freeze
6+
TOP_LEVEL_DRAFTABLE_FIELDS = [:body, :comment, :excerpt, :license, :tag_name, :tags, :title].freeze
7+
DRAFT_MAX_AGE = 86_400 * 7
8+
9+
# saving by-field is kept for backwards compatibility with old drafts
10+
# @param user [User] user to save the draft for
11+
# @param path [String] draft path to save
12+
# @return [String] top level field key
13+
def do_save_draft(user, path)
14+
base_key = "saved_post.#{user.id}.#{path}"
15+
16+
TOP_LEVEL_DRAFTABLE_FIELDS.each do |key|
17+
next unless params.key?(key)
18+
19+
key_name = NESTED_DRAFTABLE_FIELDS.include?(key) ? base_key : "#{base_key}.#{key}"
20+
21+
if key == :tags
22+
valid_tags = params[key]&.select(&:present?)
23+
24+
RequestContext.redis.del(key_name)
25+
26+
if valid_tags.present?
27+
RequestContext.redis.sadd(key_name, valid_tags)
28+
end
29+
else
30+
RequestContext.redis.set(key_name, params[key])
31+
end
32+
33+
RequestContext.redis.expire(key_name, DRAFT_MAX_AGE)
34+
end
35+
36+
saved_at_key = "saved_post_at.#{user.id}.#{path}"
37+
RequestContext.redis.set(saved_at_key, DateTime.now.iso8601)
38+
RequestContext.redis.expire(saved_at_key, DRAFT_MAX_AGE)
39+
40+
base_key
41+
end
42+
43+
# Attempts to delete a draft for a given path
44+
# @param user [User] user to delete the draft for
45+
# @param path [String] draft path to delete
46+
# @return [Boolean] status of the operation
47+
def do_delete_draft(user, path)
48+
keys = DRAFTABLE_FIELDS.map do |key|
49+
pfx = key == :saved_at ? 'saved_post_at' : 'saved_post'
50+
base = "#{pfx}.#{user.id}.#{path}"
51+
NESTED_DRAFTABLE_FIELDS.include?(key) ? base : "#{base}.#{key}"
52+
end
53+
54+
RequestContext.redis.del(*keys)
55+
end
56+
end

app/controllers/posts_controller.rb

Lines changed: 7 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# rubocop:disable Metrics/ClassLength
22
# rubocop:disable Metrics/MethodLength
33
class PostsController < ApplicationController
4+
include DraftManagement
5+
46
before_action :authenticate_user!, except: [:document, :help_center, :show]
57
before_action :set_post, only: [:toggle_comments, :feature, :lock, :unlock]
68
before_action :set_scoped_post, only: [:change_category, :show, :edit, :update, :close, :reopen, :delete, :restore]
@@ -111,7 +113,7 @@ def create
111113
Rails.cache.delete "community_user/#{current_user.community_user.id}/metric/#{key}"
112114
end
113115

114-
do_draft_delete(URI(request.referer || '').path)
116+
do_delete_draft(current_user, URI(request.referer || '').path)
115117

116118
redirect_to helpers.generic_show_link(@post)
117119
else
@@ -261,7 +263,7 @@ def update
261263
PostHistory.redact(@post, current_user)
262264
end
263265
Rails.cache.delete "community_user/#{current_user.community_user.id}/metric/E"
264-
do_draft_delete(URI(request.referer || '').path)
266+
do_delete_draft(current_user, URI(request.referer || '').path)
265267
redirect_to post_path(@post)
266268
end
267269

@@ -300,7 +302,7 @@ def update
300302
message += " on '#{@post.parent.title}'"
301303
end
302304
@post.user.create_notification message, suggested_edit_url(edit, host: @post.community.host)
303-
do_draft_delete(URI(request.referer || '').path)
305+
do_delete_draft(current_user, URI(request.referer || '').path)
304306
redirect_to post_path(@post)
305307
else
306308
@post.errors.copy!(edit.errors)
@@ -631,41 +633,13 @@ def feature
631633
render json: { status: 'success', success: true }
632634
end
633635

634-
# saving by-field is kept for backwards compatibility with old drafts
635636
def save_draft
636-
expiration_time = 86_400 * 7
637-
638-
base_key = "saved_post.#{current_user.id}.#{params[:path]}"
639-
640-
[:body, :comment, :excerpt, :license, :tag_name, :tags, :title].each do |key|
641-
next unless params.key?(key)
642-
643-
key_name = [:body, :saved_at].include?(key) ? base_key : "#{base_key}.#{key}"
644-
645-
if key == :tags
646-
valid_tags = params[key]&.select(&:present?)
647-
648-
RequestContext.redis.del(key_name)
649-
650-
if valid_tags.present?
651-
RequestContext.redis.sadd(key_name, valid_tags)
652-
end
653-
else
654-
RequestContext.redis.set(key_name, params[key])
655-
end
656-
657-
RequestContext.redis.expire(key_name, expiration_time)
658-
end
659-
660-
saved_at_key = "saved_post_at.#{current_user.id}.#{params[:path]}"
661-
RequestContext.redis.set(saved_at_key, DateTime.now.iso8601)
662-
RequestContext.redis.expire(saved_at_key, expiration_time)
663-
637+
base_key = do_save_draft(current_user, params[:path])
664638
render json: { status: 'success', success: true, key: base_key }
665639
end
666640

667641
def delete_draft
668-
do_draft_delete(params[:path])
642+
do_delete_draft(current_user, params[:path])
669643
render json: { status: 'success', success: true }
670644
end
671645

@@ -726,19 +700,6 @@ def edit_checks
726700
def unless_locked
727701
check_if_locked(@post)
728702
end
729-
730-
# Attempts to actually delete a post draft
731-
# @param path [String] draft path to delete
732-
# @return [Boolean] status of the operation
733-
def do_draft_delete(path)
734-
keys = [:body, :comment, :excerpt, :license, :saved_at, :tags, :tag_name, :title].map do |key|
735-
pfx = key == :saved_at ? 'saved_post_at' : 'saved_post'
736-
base = "#{pfx}.#{current_user.id}.#{path}"
737-
[:body, :saved_at].include?(key) ? base : "#{base}.#{key}"
738-
end
739-
740-
RequestContext.redis.del(*keys)
741-
end
742703
end
743704
# rubocop:enable Metrics/MethodLength
744705
# rubocop:enable Metrics/ClassLength

app/controllers/tags_controller.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
class TagsController < ApplicationController
2+
include DraftManagement
3+
24
before_action :authenticate_user!, only: [:new, :create, :edit, :update, :rename, :merge, :select_merge]
35
before_action :set_category, except: [:index]
46
before_action :set_tag, only: [:show, :edit, :update, :children, :rename, :merge, :select_merge, :nuke, :nuke_warning]
@@ -92,8 +94,11 @@ def new
9294
end
9395

9496
def create
95-
@tag = Tag.new(tag_params.merge(tag_set_id: @category.tag_set.id))
97+
create_params = tag_params.merge(tag_set_id: @category.tag_set.id)
98+
99+
@tag = Tag.new(create_params)
96100
if @tag.save
101+
do_delete_draft(current_user, URI(request.referer || '').path)
97102
redirect_to tag_path(id: @category.id, tag_id: @tag.id)
98103
else
99104
render :new, status: :bad_request
@@ -109,7 +114,11 @@ def update
109114
return unless check_your_privilege('edit_tags', nil, true)
110115

111116
wiki_md = params[:tag][:wiki_markdown]
112-
if @tag.update(tag_params.merge(wiki: wiki_md.present? ? helpers.render_markdown(wiki_md) : nil).except(:name))
117+
update_params = tag_params.merge(wiki: wiki_md.present? ? helpers.render_markdown(wiki_md) : nil)
118+
.except(:name)
119+
120+
if @tag.update(update_params)
121+
do_delete_draft(current_user, URI(request.referer || '').path)
113122
redirect_to tag_path(id: @category.id, tag_id: @tag.id)
114123
else
115124
render :edit, status: :bad_request
@@ -263,8 +272,7 @@ def exec_sql(sql_array)
263272
end
264273

265274
def verify_tag_editor
266-
unless user_signed_in? && (current_user.privilege?(:edit_tags) ||
267-
current_user.at_least_moderator?)
275+
unless user_signed_in? && current_user.can_edit_tags?
268276
respond_to do |format|
269277
format.html do
270278
render 'errors/not_found', layout: 'without_sidebar', status: :not_found

app/models/user.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ def can_delete?(target)
138138
privilege?('flag_curate') && !target.deleted?
139139
end
140140

141+
# Can the user edit tags?
142+
# @return [Boolean] check result
143+
def can_edit_tags?
144+
privilege?('edit_tags') || false
145+
end
146+
141147
# Can the user handle flags?
142148
# @return [Boolean] check result
143149
def can_handle_flags?

test/controllers/tags_controller_test.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,54 @@ class TagsControllerTest < ActionController::TestCase
9999
assert_not_nil assigns(:posts)
100100
end
101101

102+
test ':create should require authentication' do
103+
try_create_tag(categories(:main), tag_sets(:main))
104+
105+
assert_redirected_to_sign_in
106+
end
107+
108+
test 'only users who can edit tags should be able to create them' do
109+
category = categories(:main)
110+
tag_set = tag_sets(:main)
111+
112+
users.each do |user|
113+
sign_in(user)
114+
115+
draft_path = create_tag_path(id: category.id)
116+
tag_name = "test-tag-#{user.id}"
117+
118+
@controller.stub('params', { tag_name: tag_name }) do
119+
@controller.do_save_draft(user, draft_path)
120+
end
121+
122+
try_create_tag(category, tag_set, name: tag_name)
123+
124+
if !@controller.helpers.user_signed_in?
125+
assert_redirected_to_sign_in
126+
elsif user.can_edit_tags?
127+
assert_response(:found, "Expected user '#{user.name}' to be able to create tags")
128+
assert_draft_deleted(user, draft_path, :tag_name)
129+
else
130+
assert_response(:not_found, "Expected user '#{user.name}' to not be able to create tags")
131+
end
132+
end
133+
end
134+
135+
test ':create should correctly create tags' do
136+
sign_in users(:tags_editor)
137+
138+
category = categories(:main)
139+
tag_set = tag_sets(:main)
140+
tag_name = 'test-tag'
141+
142+
try_create_tag(category, tag_set, name: tag_name)
143+
144+
@tag = assigns(:tag)
145+
146+
assert_not_nil @tag
147+
assert_redirected_to(tag_path(id: category.id, tag_id: @tag.id))
148+
end
149+
102150
test 'should deny edit to anonymous user' do
103151
get :edit, params: { id: categories(:main).id, tag_id: tags(:topic).id }
104152

@@ -234,6 +282,23 @@ class TagsControllerTest < ActionController::TestCase
234282

235283
private
236284

285+
# @param category [Category] category to create the tag in
286+
# @param tag_set [TagSet] tag set the tag belongs to
287+
def try_create_tag(tag_set, category, **opts)
288+
# this is needed for draft deletion until we switch from request.referer
289+
@request.set_header('HTTP_REFERER', create_tag_path(id: category.id))
290+
291+
post :create, params: {
292+
id: category.id,
293+
tag: {
294+
excerpt: 'Usage guidance goes here',
295+
name: 'test-tag',
296+
tag_set_id: tag_set.id,
297+
wiki_markdown: 'Extended tag description goes here'
298+
}.merge(opts)
299+
}
300+
end
301+
237302
# @param category [Category] category to rename the tag in
238303
# @param tag [Tag] tag to rename
239304
# @param name [String] new tag name

test/fixtures/community_users.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ sample_editor:
2626
is_moderator: false
2727
reputation: 501
2828

29+
sample_tags_editor:
30+
user: tags_editor
31+
community: sample
32+
is_admin: false
33+
is_moderator: false
34+
reputation: 42
35+
2936
sample_deleter:
3037
user: deleter
3138
community: sample

test/fixtures/user_abilities.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ e_eo:
1010
community_user: sample_editor
1111
ability: everyone
1212

13+
et_eo:
14+
community_user: sample_tags_editor
15+
ability: everyone
16+
1317
d_eo:
1418
community_user: sample_deleter
1519
ability: everyone
@@ -38,6 +42,10 @@ e_ur:
3842
community_user: sample_editor
3943
ability: unrestricted
4044

45+
et_ur:
46+
community_user: sample_tags_editor
47+
ability: unrestricted
48+
4149
d_ur:
4250
community_user: sample_deleter
4351
ability: unrestricted
@@ -78,6 +86,10 @@ d_et:
7886
community_user: sample_deleter
7987
ability: edit_tags
8088

89+
te_et:
90+
community_user: sample_tags_editor
91+
ability: edit_tags
92+
8193
b_eo:
8294
community_user: sample_basic_user
8395
ability: everyone

test/fixtures/users.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ editor:
4040
login_token_expires_at: 2000-01-01T00:00:00.000000Z
4141
confirmed_at: 2020-01-01T00:00:00.000000Z
4242

43+
tags_editor:
44+
email: editor@example.com
45+
encrypted_password: '$2a$11$roUHXKxecjyQ72Qn7DWs3.9eRCCoRn176kX/UNb/xiue3aGqf7xEW'
46+
sign_in_count: 1337
47+
username: tags-editor
48+
is_global_admin: false
49+
is_global_moderator: false
50+
confirmed_at: 2020-01-01T00:00:00.000000Z
51+
4352
deleter:
4453
email: delete@qpixel-test.net
4554
encrypted_password: '$2a$11$roUHXKxecjyQ72Qn7DWs3.9eRCCoRn176kX/UNb/xiue3aGqf7xEW'

test/test_helper.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,16 @@ def copy_abilities(community_id)
129129
end
130130
end
131131

132+
def assert_draft_deleted(user, path, *fields)
133+
base_key = "saved_post.#{user.id}.#{path}"
134+
135+
fields.each do |key|
136+
key_name = DraftManagement::NESTED_DRAFTABLE_FIELDS.include?(key) ? base_key : "#{base_key}.#{key}"
137+
assert_not(RequestContext.redis.exists?(key_name),
138+
"Expected '#{key_name}' draft to be deleted")
139+
end
140+
end
141+
132142
def assert_valid_json_response
133143
assert_nothing_raised do
134144
parsed = JSON.parse(response.body)

0 commit comments

Comments
 (0)