Files
pillar/pillar/api/nodes/custom/comment.py
Tobias Johansson fbcce7a6d8 Vue Comments: Comments ported to Vue + DnD fileupload
* Drag and drop files to comment editor to add a file attachment
* Using Vue to render comments

Since comments now has attachments we need to update the schemas
./manage.py maintenance replace_pillar_node_type_schemas
2018-12-12 11:45:47 +01:00

194 lines
7.1 KiB
Python

"""PATCH support for comment nodes."""
import logging
from flask import current_app
import werkzeug.exceptions as wz_exceptions
from pillar.api.utils import authorization, authentication, jsonify, remove_private_keys
from . import register_patch_handler
log = logging.getLogger(__name__)
COMMENT_VOTING_OPS = {'upvote', 'downvote', 'revoke'}
VALID_COMMENT_OPERATIONS = COMMENT_VOTING_OPS.union({'edit'})
@register_patch_handler('comment')
def patch_comment(node_id, patch):
assert_is_valid_patch(node_id, patch)
user_id = authentication.current_user_id()
if patch['op'] in COMMENT_VOTING_OPS:
result, node = vote_comment(user_id, node_id, patch)
else:
assert patch['op'] == 'edit', 'Invalid patch operation %s' % patch['op']
result, node = edit_comment(user_id, node_id, patch)
return jsonify({'_status': 'OK',
'result': result,
'properties': node['properties']
})
def vote_comment(user_id, node_id, patch):
"""Performs a voting operation."""
# Find the node. Includes a query on the properties.ratings array so
# that we only get the current user's rating.
nodes_coll = current_app.data.driver.db['nodes']
node_query = {'_id': node_id,
'$or': [{'properties.ratings.$.user': {'$exists': False}},
{'properties.ratings.$.user': user_id}]}
node = nodes_coll.find_one(node_query,
projection={'properties': 1, 'user': 1})
if node is None:
log.warning('User %s wanted to patch non-existing node %s' % (user_id, node_id))
raise wz_exceptions.NotFound('Node %s not found' % node_id)
# We don't allow the user to down/upvote their own nodes.
if user_id == node['user']:
raise wz_exceptions.Forbidden('You cannot vote on your own node')
props = node['properties']
# Find the current rating (if any)
rating = next((rating for rating in props.get('ratings', ())
if rating.get('user') == user_id), None)
def revoke():
if not rating:
# No rating, this is a no-op.
return
label = 'positive' if rating.get('is_positive') else 'negative'
update = {'$pull': {'properties.ratings': rating},
'$inc': {'properties.rating_%s' % label: -1}}
return update
def upvote():
if rating and rating.get('is_positive'):
# There already was a positive rating, so this is a no-op.
return
update = {'$inc': {'properties.rating_positive': 1}}
if rating:
update['$inc']['properties.rating_negative'] = -1
update['$set'] = {'properties.ratings.$.is_positive': True}
else:
update['$push'] = {'properties.ratings': {
'user': user_id, 'is_positive': True,
}}
return update
def downvote():
if rating and not rating.get('is_positive'):
# There already was a negative rating, so this is a no-op.
return
update = {'$inc': {'properties.rating_negative': 1}}
if rating:
update['$inc']['properties.rating_positive'] = -1
update['$set'] = {'properties.ratings.$.is_positive': False}
else:
update['$push'] = {'properties.ratings': {
'user': user_id, 'is_positive': False,
}}
return update
actions = {
'upvote': upvote,
'downvote': downvote,
'revoke': revoke,
}
action = actions[patch['op']]
mongo_update = action()
nodes_coll = current_app.data.driver.db['nodes']
if mongo_update:
log.info('Running %s', mongo_update)
if rating:
result = nodes_coll.update_one({'_id': node_id, 'properties.ratings.user': user_id},
mongo_update)
else:
result = nodes_coll.update_one({'_id': node_id}, mongo_update)
else:
result = 'no-op'
# Fetch the new ratings, so the client can show these without querying again.
node = nodes_coll.find_one(node_id,
projection={'properties.rating_positive': 1,
'properties.rating_negative': 1})
return result, node
def edit_comment(user_id, node_id, patch):
"""Edits a single comment.
Doesn't do permission checking; users are allowed to edit their own
comment, and this is not something you want to revoke anyway. Admins
can edit all comments.
"""
# Find the node. We need to fetch some more info than we use here, so that
# we can pass this stuff to Eve's patch_internal; that way the validation &
# authorisation system has enough info to work.
nodes_coll = current_app.data.driver.db['nodes']
node = nodes_coll.find_one(node_id)
if node is None:
log.warning('User %s wanted to patch non-existing node %s' % (user_id, node_id))
raise wz_exceptions.NotFound('Node %s not found' % node_id)
if node['user'] != user_id and not authorization.user_has_role('admin'):
raise wz_exceptions.Forbidden('You can only edit your own comments.')
node = remove_private_keys(node)
node['properties']['content'] = patch['content']
node['properties']['attachments'] = patch.get('attachments', {})
# Use Eve to PUT this node, as that also updates the etag and we want to replace attachments.
r, _, _, status = current_app.put_internal('nodes',
node,
concurrency_check=False,
_id=node_id)
if status != 200:
log.error('Error %i editing comment %s for user %s: %s',
status, node_id, user_id, r)
raise wz_exceptions.InternalServerError('Internal error %i from Eve' % status)
else:
log.info('User %s edited comment %s', user_id, node_id)
# Fetch the new content, so the client can show these without querying again.
node = nodes_coll.find_one(node_id, projection={
'properties.content': 1,
'properties._content_html': 1,
})
return status, node
def assert_is_valid_patch(node_id, patch):
"""Raises an exception when the patch isn't valid."""
try:
op = patch['op']
except KeyError:
raise wz_exceptions.BadRequest("PATCH should have a key 'op' indicating the operation.")
if op not in VALID_COMMENT_OPERATIONS:
raise wz_exceptions.BadRequest('Operation should be one of %s',
', '.join(VALID_COMMENT_OPERATIONS))
if op not in COMMENT_VOTING_OPS:
# We can't check here, we need the node owner for that.
return
# See whether the user is allowed to patch
if authorization.user_matches_roles(current_app.config['ROLES_FOR_COMMENT_VOTING']):
log.debug('User is allowed to upvote/downvote comment')
return
# Access denied.
log.info('User %s wants to PATCH comment node %s, but is not allowed.',
authentication.current_user_id(), node_id)
raise wz_exceptions.Forbidden()