diff --git a/alembic/versions/1ab5aa1e3054_start_associating_comments_with_.py b/alembic/versions/1ab5aa1e3054_start_associating_comments_with_.py new file mode 100644 index 0000000..fde6462 --- /dev/null +++ b/alembic/versions/1ab5aa1e3054_start_associating_comments_with_.py @@ -0,0 +1,28 @@ +"""Start associating comments with registered users and use user information +as a display name instead of arbitrary author names. + +Revision ID: 1ab5aa1e3054 +Revises: 2c00861f3ff1 +Create Date: 2015-09-01 18:05:35.598253 + +""" + +# revision identifiers, used by Alembic. +revision = '1ab5aa1e3054' +down_revision = '2c00861f3ff1' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + +def upgrade(): + op.drop_column('user_comments', 'author_name') + op.add_column('user_comments', + sa.Column('user_id', + sa.BigInteger, + sa.ForeignKey('users.id'))) + +def downgrade(): + op.drop_column('user_comments', 'user_id') + op.add_column('user_comments', sa.Column('author_name', sa.String()),) diff --git a/cycledash/api/comments.py b/cycledash/api/comments.py index addef63..2a09922 100644 --- a/cycledash/api/comments.py +++ b/cycledash/api/comments.py @@ -2,13 +2,15 @@ from collections import defaultdict from flask import jsonify, request from flask.ext.restful import abort, fields +from flask.ext.login import current_user from sqlalchemy import select, func, desc from voluptuous import Any, Required, Coerce, Schema from common.helpers import tables, to_epoch from cycledash import db from cycledash.helpers import (prepare_request_data, success_response, - abort_if_none_for, camelcase_dict) + abort_if_none_for, camelcase_dict, + desentisize_user) from cycledash.validations import Doc, to_epoch from . import Resource, validate_with, marshal_with @@ -20,8 +22,7 @@ Required("position"): Coerce(int), Required("reference"): basestring, Required("alternates"): basestring, - Required("comment_text"): basestring, - "author_name": basestring, + Required("comment_text"): basestring }) DeleteComment = Schema({ @@ -30,8 +31,7 @@ UpdateComment = Schema({ Required('last_modified'): Coerce(float), - "comment_text": basestring, - "author_name": basestring, + "comment_text": basestring }) CommentFields = Schema({ @@ -52,8 +52,8 @@ basestring, Doc('comment_text', 'The text of the comment.'): Any(basestring, None), - Doc('author_name', 'The name of the author of this comment.'): - Any(basestring, None), + Doc('user_id', 'The ID of the User this comment is associated with.'): + Any(long, None), Doc('created', 'The time at which the comment was created (in epoch time).'): Coerce(to_epoch), @@ -77,9 +77,11 @@ def get(self, run_id): @marshal_with(CommentFields) def post(self, run_id): """Create a comment.""" + print current_user with tables(db.engine, 'user_comments') as (con, comments): q = comments.insert().values( vcf_id=run_id, + user_id=current_user['id'], **request.validated_body ).returning(*comments.c) return dict(q.execute().fetchone()), 201 @@ -163,6 +165,7 @@ def _row_key(comment, table): comment['last_modified'] = to_epoch(comment['last_modified']) comment['created'] = to_epoch(comment['created']) comment = camelcase_dict(comment) + comment = userify_comment(comment) results_map[row_key].append(comment) return {'comments': results_map} @@ -174,8 +177,30 @@ def get_last_comments(n=5): q = select(cols.values()).order_by( desc(cols.created)).limit(n) comments = [camelcase_dict(dict(c)) for c in con.execute(q).fetchall()] - return epochify_comments(comments) + return userify_comments(epochify_comments(comments)) +def userify_comments(comments): + """Given comments with userIds, attaches the relevant user + info to the comments + """ + for comment in comments: + userify_comment(comment) + return comments + +def userify_comment(comment): + """Given a comment with userId, attaches the relevant user + info to the comment + """ + if 'userId' in comment: + with tables(db.engine, 'users') as (con, users): + q = select(users.c).where(users.c.id == comment['userId']) + user = q.execute().fetchone() + if user is not None: + user = desentisize_user(dict(user)) + comment['user'] = user + else: + comment['user'] = None + return comment def epochify_comments(comments): """Sets `lastModified` and `created` to be epoch time instead of iso8061.""" @@ -205,4 +230,5 @@ def _get_comment(comment_table, id=None, **query_kwargs): q = comment_table.select().where(comment_table.c.id == id) for colname, val in query_kwargs.items(): q = q.where(comment_table.c[colname] == val) - return dict(abort_if_none_for('comment')(q.execute().fetchone(), id)) + comment = q.execute().fetchone() + return dict(abort_if_none_for('comment')(comment, id)) diff --git a/cycledash/auth.py b/cycledash/auth.py index 79fd568..6c0a478 100644 --- a/cycledash/auth.py +++ b/cycledash/auth.py @@ -65,6 +65,24 @@ def is_anonymous(self): def get_id(self): return unicode(self['id']) +class AnonymousUser(dict): + def __init__(self): + self['id'] = None + self['username'] = u'Anonymous' + + def is_authenticated(self): + return False + + def is_active(self): + return False + + def is_anonymous(self): + return True + + def get_id(self): + return self['id'] + +login_manager.anonymous_user = AnonymousUser def wrap_user(user): """Wraps user record returned from the database in a class providing methods diff --git a/cycledash/helpers.py b/cycledash/helpers.py index ad59027..46ad609 100644 --- a/cycledash/helpers.py +++ b/cycledash/helpers.py @@ -195,6 +195,11 @@ def abort_if_none(obj, obj_id): return obj return abort_if_none +def desentisize_user(user): + """Removes sensitive user information before passing it to the frontend.""" + del user['password'] + del user['email'] + return user class CollisionError(Exception): pass diff --git a/cycledash/static/js/comments/components/comment.js b/cycledash/static/js/comments/components/comment.js index c08981f..6144793 100644 --- a/cycledash/static/js/comments/components/comment.js +++ b/cycledash/static/js/comments/components/comment.js @@ -59,8 +59,7 @@ var Comment = React.createClass({ // moment uses the local timezone by default (converting the // value, which starts as a UNIX timestamp, to that timezone) var relativeDate = moment.unix(comment.created).fromNow(); - var authorName = comment.authorName ? - comment.authorName.slice(0, 15) : 'Anonymous'; + var authorName = comment.userId ? comment.user.username : "Anonymous"; return (
  • {authorName}
    diff --git a/cycledash/static/js/examine/components/CommentBox.js b/cycledash/static/js/examine/components/CommentBox.js index 52faa4b..a337c23 100644 --- a/cycledash/static/js/examine/components/CommentBox.js +++ b/cycledash/static/js/examine/components/CommentBox.js @@ -11,10 +11,7 @@ var _ = require('underscore'), utils = require('../utils'), React = require('react/addons'), marked = require('marked'), - moment = require('moment'), - store = require('store'); - -// Currently used to write comment author names to local storage. + moment = require('moment'); /** * Use markdown for comments, and set appropriate flags to: @@ -39,9 +36,9 @@ var CommentBox = React.createClass({ igvLink: React.PropTypes.string, handleOpenViewer: React.PropTypes.func.isRequired, handleSetComment: React.PropTypes.func.isRequired, - handleDeleteComment: React.PropTypes.func.isRequired + handleDeleteComment: React.PropTypes.func.isRequired, + currentUser: React.PropTypes.object.isRequired }, - LOCAL_STORAGE_AUTHOR_KEY: 'CYCLEDASH_AUTHORNAME', getHandleDelete: function(comment) { var handleDeleteComment = this.props.handleDeleteComment; var record = this.props.record; @@ -55,11 +52,9 @@ var CommentBox = React.createClass({ getHandleSaveForUpdate: function(comment) { var handleSetComment = this.props.handleSetComment; var record = this.props.record; - return function(commentText, authorName) { + return function(commentText) { var newComment = _.clone(comment); newComment.commentText = commentText; - newComment.authorName = authorName; - handleSetComment(newComment, record); }; }, @@ -67,7 +62,9 @@ var CommentBox = React.createClass({ var handleSetComment = this.props.handleSetComment; var getGranularUnixSeconds = this.getGranularUnixSeconds; var record = this.props.record; - return function(commentText, authorName) { + var currentUser = this.props.currentUser; + + return function(commentText) { // Subtract the offset to get GMT (to match what's in the DB) var newComment = _.extend( _.pick(record, @@ -77,7 +74,8 @@ var CommentBox = React.createClass({ 'alternates', 'sample_name'), {'commentText': commentText, - 'authorName': authorName, + 'user': currentUser, + 'userId': currentUser.id, // Note: this is a temporary date that does not get persisted // to the DB. Instead, the DB creates its own date, but this // timestamp is used for distinguishing between comments in @@ -90,15 +88,6 @@ var CommentBox = React.createClass({ // moment does not appear to provide this functionality. return momentObject.valueOf() / 1000.0; }, - getLocalAuthorName: function() { - return store.enabled ? store.get(this.LOCAL_STORAGE_AUTHOR_KEY, '') : ''; - }, - saveLocalAuthorName: function(authorName) { - if (store.enabled && - store.get(this.LOCAL_STORAGE_AUTHOR_KEY, '') !== authorName) { - store.set(this.LOCAL_STORAGE_AUTHOR_KEY, authorName); - } - }, render: function() { var comments = this.props.record.comments; var commentNodes = _.sortBy(comments, 'created').map( @@ -114,12 +103,11 @@ var CommentBox = React.createClass({ moment(comment.created))); return ; }); @@ -136,9 +124,7 @@ var CommentBox = React.createClass({ key={utils.getRowKey(this.props.record) + 'newcomment'} handleSave={this.getHandleSaveForCreate()} startInEditState={true} - cancelable={false} - saveLocalAuthorName={this.saveLocalAuthorName} - authorName={this.getLocalAuthorName()}/> + cancelable={false}/> ); @@ -156,10 +142,9 @@ var VCFComment = React.createClass({ handleSave: React.PropTypes.func.isRequired, startInEditState: React.PropTypes.bool.isRequired, cancelable: React.PropTypes.bool.isRequired, - saveLocalAuthorName:React.PropTypes.func.isRequired, // Optional arguments. - authorName: React.PropTypes.string, + user: React.PropTypes.object, createdString: React.PropTypes.string, handleDelete: React.PropTypes.func, }, @@ -194,17 +179,10 @@ var VCFComment = React.createClass({ }, render: function() { var placeHolder = 'Enter your comment here'; - // Only use "Anonymous" in the viewer; the editor should just be - // blank in that case. - var authorNameOrAnonymous = this.props.authorName || 'Anonymous'; - var authorNameOrBlank = this.props.authorName || ''; - // handleDelete is optional, but not providing it requires the // edit view. var commentElement = (this.state.isEditing || !this.props.handleDelete) ? :
    - {this.props.authorName} + {authorName}
    @@ -289,15 +271,13 @@ var VCFCommentViewer = React.createClass({ }); /** - * VCFCommentEditor represents the active editing (or creating) of a + * VCFCommentEditor represents the active editing (or creating) of a * comment, and it has a separate state variable for updated text that * is not yet saved. */ var VCFCommentEditor = React.createClass({ propTypes: { commentText: React.PropTypes.string.isRequired, - authorName: React.PropTypes.string.isRequired, - saveLocalAuthorName: React.PropTypes.func.isRequired, placeHolder: React.PropTypes.string.isRequired, setCommentTextState: React.PropTypes.func.isRequired, setEditState: React.PropTypes.func.isRequired, @@ -307,18 +287,12 @@ var VCFCommentEditor = React.createClass({ }, handleSaveText: function() { // If non-blank text is entered that differs from what was originally - // in the editor (either text or author), save it. A new comment can + // in the editor, save it. A new comment can // never be blank, though. var newCommentText = this.state.newCommentText; - var newAuthorName = this.state.newAuthorName; if (newCommentText !== '') { - if ((newCommentText !== this.props.commentText) || - (newAuthorName !== '' && - newAuthorName !== this.props.authorName)) { - // Store the author name in local storage. - this.props.saveLocalAuthorName(newAuthorName); - - this.props.handleSave(newCommentText, newAuthorName); + if ((newCommentText !== this.props.commentText)) { + this.props.handleSave(newCommentText); this.props.setCommentTextState(newCommentText); this.props.setEditState(false); @@ -344,11 +318,7 @@ var VCFCommentEditor = React.createClass({ } }, getInitialState: function() { - return {newCommentText: this.props.commentText, - newAuthorName: this.props.authorName}; - }, - handleAuthorChange: function(event) { - this.setState({newAuthorName: event.target.value}); + return {newCommentText: this.props.commentText}; }, handleTextChange: function(event) { this.setState({newCommentText: event.target.value}); @@ -374,11 +344,6 @@ var VCFCommentEditor = React.createClass({ return (
    -