Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New methods to access and update skip_content_moderation value #23432

Merged
merged 14 commits into from Jul 17, 2018

Conversation

Erin007
Copy link
Contributor

@Erin007 Erin007 commented Jun 29, 2018

To make the buttons in #23406 function so that project validators can enable or disable automated content moderation for a given project, this PR introduces methods to access and update the value of skip_content_moderation. Also included are tests for the new methods.

@@ -14,7 +14,7 @@ def initialize(storage_id)
@table = PEGASUS_DB[:storage_apps]
end

def create(value, ip:, type: nil, published_at: nil, remix_parent_id: nil)
def create(value, ip:, type: nil, published_at: nil, remix_parent_id: nil, skip_content_moderation: false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we ever need to create a storage_apps row with skip_content_moderation: true? If so, let's add tests for this case. If not, maybe we don't want it to be a parameter here.

@@ -138,6 +139,15 @@ def get_abuse(channel_id)
row[:abuse_score]
end

def get_skip_content_moderation_value(channel_id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #23406 you settled on disable-auto-moderation as the consistent language for this feature. Our server code should use the same language, unless there's a particular reason it's different.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idiomatic ruby nit: Name methods that return a boolean value as a query def auto_moderation_disabled?(channel_id)

raise NotFound, "channel `#{channel_id}` not found" unless row

update_content_moderation = @table.where(id: id).exclude(state: 'deleted').update({skip_content_moderation: should_skip})
raise NotFound, "channel `#{channel_id}` not found" if update_content_moderation == should_skip
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this works the way you describe it (there's no test for it) and I'm not sure we want to do it anyway.

Sequel::Dataset#update returns the number of rows updated, not the new value; therefore update_content_moderation == should_skip is probably never true becase it's comparing an integer to a boolean.

It's generally a good idea to make server APIs idempotent (I enjoyed this article on the subject). In this case, that means we want this method to do nothing and report success if the setting is already the desired value, rather than throw an error. This way, if a client requests this operation but the server's response gets lost on the way back, the client can safely re-send the request and not worry about causing an error.

# @raise [NotFound] if the channel does not exist or already has the desired
# value for skip_content_moderation
#
def change_content_moderation(channel_id, should_skip)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming nit: set instead of change. Since this is a boolean value it might be a non-issue, but with objects it matters. "Change" implies the subject may inherit some of its past state, while "set" suggests the value after the operation will be only the new information you passed it. It's like the difference between PUT and PATCH in HTTP.

row = @table.where(id: id).exclude(state: 'deleted').first
raise NotFound, "channel `#{channel_id}` not found" unless row

update_content_moderation = @table.where(id: id).exclude(state: 'deleted').update({skip_content_moderation: should_skip})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because update is safe if the project is not present and it returns the number of rows modified, you can implement this API with one query instead of two.

_owner, id = storage_decrypt_channel_id(channel_id)
rows_changed = @table.
  where(id: id).
  exclude(state: 'deleted').
  update({skip_content_moderation: should_skip})
raise NotFound, "channel `#{channel_id}` not found" unless rows_changed > 0

@davidsbailey
Copy link
Member

@Erin007 please tag me when you are ready for another look

@Erin007 Erin007 force-pushed the functional-content-mod-buttons branch from ab55b1c to a36f65f Compare July 12, 2018 17:39
@Erin007 Erin007 force-pushed the functional-content-mod-buttons branch from 7281052 to 35bb84d Compare July 12, 2018 23:15
@Erin007
Copy link
Contributor Author

Erin007 commented Jul 12, 2018

The buttons work! Thank you for your help @davidsbailey and @islemaster. This PR is ready for review.

@@ -303,6 +303,29 @@ def test_abuse

# Ideally we would also test that deleting abuse works when we're an admin
# but don't currently have a way to simulate admin from tests
# TODO (Erin B) confirm if this is accurate and update tests if needed.
# You might not need to be an admin, you might need project validator
# permissions.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to tackle this in a follow up PR.

Copy link
Contributor

@islemaster islemaster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see showProjectAdmin.js updated to respect the async nature of the API call. The rest are optional cleanup suggestions.

@@ -83,14 +83,16 @@ export default () => {
}

$('#disable-auto-moderation').click(function () {
alert("You clicked the disable automated image moderation button. It doesn't do anything yet because our moderation filter is turned off, but it will be functional soon. Thanks for your patience with this work-in-progress button!");
dashboard.project.disableAutoContentModeration();
alert("Our moderation filter is currently turned off for all projects, but your choice will be saved and applied when we re-enable the moderation service.");
$('#disable-auto-moderation').hide();
$('#moderation-explanation').hide();
$('#enable-auto-moderation').show();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if we didn't switch the displayed buttons until/unless the request to enable/disable moderation succeeds. Also a great opportunity to use async/await!

// in project.js
// Make these methods return a promise that resolves when the request succeeds, or rejects if it fails.
disableAutoContentModeration() {
  return new Promise((resolve, reject) => {
    channels.update(`${this.getCurrentId()}/disable-content-moderation`, (err) => {
      err ? reject(err) : resolve()
    });
  });
},

// in showProjectAdmin.js
// Make the click handler async, and await disableAutoContentModeration before changing the buttons.
$('#disable-auto-moderation').click(async function() {
  await dashboard.project.disableAutoContentModeration();
  alert("Our moderation filter...");
  $('#disable-auto-moderation').hide();
  $('#moderation-explanation').hide();
  $('#enable-auto-moderation').show();
});

bad_request
end
{skip_content_moderation: value}.to_json
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two blocks are identical except for the route name and the boolean value being set. Let's extract a helper method!

@@ -283,4 +317,8 @@ class ChannelsApi < Sinatra::Base
post %r{/v3/channels/([^/]+)/abuse/delete$} do |_id|
call(env.merge('REQUEST_METHOD' => 'DELETE', 'PATH_INFO' => File.dirname(request.path_info)))
end

def project_validator?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly worth a comment explaining this pass-through method is here so we can stub it in tests.

@@ -69,15 +69,15 @@ def has_permission?(permission)
end

# @param [Integer] section_id
# @returns [Boolean] true iff the current user is the owner of the given section.
# @returns [Boolean] true if the current user is the owner of the given section.
# Note: NOT always true for admins.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😆 This isn't a typo - it's shorthand for "if and only if".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But maybe you're right to correct that, given the note on the next line!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, I had no idea. Thanks!


post "/v3/channels/#{channel_id}/enable-content-moderation"
assert last_response.ok?
assert_equal false, JSON.parse(last_response.body)['skip_content_moderation']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Paranoid testing suggestion - double each of these tests, so you call disable-content-moderation twice in a row and enable-content-moderation twice in a row, verifying that each route is a no-op when the project is already in the desired state, and not just toggling the value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Erin007
Copy link
Contributor Author

Erin007 commented Jul 17, 2018

I updated the test coverage and implemented async await, anything else pressing before merging @islemaster?

Copy link
Contributor

@islemaster islemaster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants