comments: add comments type into comments.

This commit is contained in:
Marcin Kuzminski 2017-01-11 14:46:29 +01:00
parent b52808ca95
commit 24ef7171f8
15 changed files with 285 additions and 79 deletions

View file

@ -51,7 +51,7 @@ PYRAMID_SETTINGS = {}
EXTENSIONS = {}
__version__ = ('.'.join((str(each) for each in VERSION[:3])))
__dbversion__ = 63 # defines current db version for migrations
__dbversion__ = 64 # defines current db version for migrations
__platform__ = platform.system()
__license__ = 'AGPLv3, and Commercial License'
__author__ = 'RhodeCode GmbH'

View file

@ -334,6 +334,8 @@ class ChangesetController(BaseRepoController):
commit_id = revision
status = request.POST.get('changeset_status', None)
text = request.POST.get('text')
comment_type = request.POST.get('comment_type')
if status:
text = text or (_('Status change %(transition_icon)s %(status)s')
% {'transition_icon': '>',
@ -355,7 +357,8 @@ class ChangesetController(BaseRepoController):
line_no=request.POST.get('line'),
status_change=(ChangesetStatus.get_status_lbl(status)
if status else None),
status_change_type=status
status_change_type=status,
comment_type=comment_type
)
c.inline_comment = True if comment.line_no else False

View file

@ -903,6 +903,7 @@ class PullrequestsController(BaseRepoController):
# as a changeset status, still we want to send it in one value.
status = request.POST.get('changeset_status', None)
text = request.POST.get('text')
comment_type = request.POST.get('comment_type')
if status and '_closed' in status:
close_pr = True
status = status.replace('_closed', '')
@ -934,7 +935,8 @@ class PullrequestsController(BaseRepoController):
if status and allowed_to_change_status else None),
status_change_type=(status
if status and allowed_to_change_status else None),
closing_pr=close_pr
closing_pr=close_pr,
comment_type=comment_type
)
if allowed_to_change_status:

View file

@ -56,7 +56,7 @@ from rhodecode.lib.utils2 import (
str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
from rhodecode.model import meta
from rhodecode.model.db import Repository, User
from rhodecode.model.db import Repository, User, ChangesetComment
from rhodecode.model.notification import NotificationModel
from rhodecode.model.scm import ScmModel
from rhodecode.model.settings import VcsSettingsModel, SettingsModel
@ -299,6 +299,7 @@ def attach_context_attributes(context, request):
context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
context.visual.default_renderer = rc_config.get(
'rhodecode_markup_renderer', 'rst')
context.visual.comment_types = ChangesetComment.COMMENT_TYPES
context.visual.rhodecode_support_url = \
rc_config.get('rhodecode_support_url') or url('rhodecode_support')

View file

@ -0,0 +1,30 @@
import logging
from sqlalchemy import Column, MetaData, Integer, Unicode, ForeignKey
from rhodecode.lib.dbmigrate.versions import _reset_base
log = logging.getLogger(__name__)
def upgrade(migrate_engine):
"""
Upgrade operations go here.
Don't create your own engine; bind migrate_engine to your metadata
"""
_reset_base(migrate_engine)
from rhodecode.lib.dbmigrate.schema import db_4_5_0_0 as db
# add comment type and link to resolve by id
comment_table = db.ChangesetComment.__table__
col1 = Column('comment_type', Unicode(128), nullable=True)
col1.create(table=comment_table)
col1 = Column('resolved_comment_id', Integer(),
ForeignKey('changeset_comments.comment_id'), nullable=True)
col1.create(table=comment_table)
def downgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine

View file

@ -44,6 +44,8 @@ from rhodecode.model.notification import NotificationModel
from rhodecode.model.meta import Session
from rhodecode.model.settings import VcsSettingsModel
from rhodecode.model.notification import EmailNotificationModel
from rhodecode.model.validation_schema.schemas import comment_schema
log = logging.getLogger(__name__)
@ -111,15 +113,32 @@ class CommentsModel(BaseModel):
if not renderer:
renderer = self._get_renderer()
repo = self._get_repo(repo)
user = self._get_user(user)
schema = comment_schema.CommentSchema()
validated_kwargs = schema.deserialize(dict(
comment_body=text,
comment_type=comment_type,
comment_file=f_path,
comment_line=line_no,
renderer_type=renderer,
status_change=status_change,
repo=repo,
user=user,
))
repo = self._get_repo(validated_kwargs['repo'])
user = self._get_user(validated_kwargs['user'])
comment = ChangesetComment()
comment.renderer = renderer
comment.renderer = validated_kwargs['renderer_type']
comment.text = validated_kwargs['comment_body']
comment.f_path = validated_kwargs['comment_file']
comment.line_no = validated_kwargs['comment_line']
comment.comment_type = validated_kwargs['comment_type']
comment.repo = repo
comment.author = user
comment.text = text
comment.f_path = f_path
comment.line_no = line_no
pull_request_id = pull_request

View file

@ -2896,6 +2896,9 @@ class ChangesetComment(Base, BaseModel):
)
COMMENT_OUTDATED = u'comment_outdated'
COMMENT_TYPE_NOTE = u'note'
COMMENT_TYPE_TODO = u'todo'
COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
@ -2912,6 +2915,9 @@ class ChangesetComment(Base, BaseModel):
renderer = Column('renderer', Unicode(64), nullable=True)
display_state = Column('display_state', Unicode(128), nullable=True)
comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
resolved_comment = relationship('ChangesetComment', remote_side=comment_id)
author = relationship('User', lazy='joined')
repo = relationship('Repository')
status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")

View file

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2017-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import os
import colander
from rhodecode.translation import _
from rhodecode.model.validation_schema import preparers
from rhodecode.model.validation_schema import types
@colander.deferred
def deferred_lifetime_validator(node, kw):
options = kw.get('lifetime_options', [])
return colander.All(
colander.Range(min=-1, max=60 * 24 * 30 * 12),
colander.OneOf([x for x in options]))
def unique_gist_validator(node, value):
from rhodecode.model.db import Gist
existing = Gist.get_by_access_id(value)
if existing:
msg = _(u'Gist with name {} already exists').format(value)
raise colander.Invalid(node, msg)
def filename_validator(node, value):
if value != os.path.basename(value):
msg = _(u'Filename {} cannot be inside a directory').format(value)
raise colander.Invalid(node, msg)
comment_types = ['note', 'todo']
class CommentSchema(colander.MappingSchema):
from rhodecode.model.db import ChangesetComment
comment_body = colander.SchemaNode(colander.String())
comment_type = colander.SchemaNode(
colander.String(),
validator=colander.OneOf(ChangesetComment.COMMENT_TYPES))
comment_file = colander.SchemaNode(colander.String(), missing=None)
comment_line = colander.SchemaNode(colander.String(), missing=None)
status_change = colander.SchemaNode(colander.String(), missing=None)
renderer_type = colander.SchemaNode(colander.String())
# do those ?
user = colander.SchemaNode(types.StrOrIntType())
repo = colander.SchemaNode(types.StrOrIntType())

View file

@ -186,3 +186,11 @@ class UserType(UserOrUserGroupType):
class UserGroupType(UserOrUserGroupType):
scopes = ('usergroup',)
class StrOrIntType(colander.String):
def deserialize(self, node, cstruct):
if isinstance(node, basestring):
return super(StrOrIntType, self).deserialize(node, cstruct)
else:
return colander.Integer().deserialize(node, cstruct)

View file

@ -880,8 +880,6 @@ input.filediff-collapse-state {
display: inline;
}
@comment-padding: 5px;
/**** COMMENTS ****/
.filediff-menu {
@ -909,59 +907,6 @@ input.filediff-collapse-state {
}
}
.inline-comments {
border-radius: @border-radius;
.comment {
margin: 0;
border-radius: @border-radius;
}
.comment-outdated {
opacity: 0.5;
}
.comment-inline {
background: white;
padding: (@comment-padding + 3px) @comment-padding;
border: @comment-padding solid @grey6;
.text {
border: none;
}
.meta {
border-bottom: 1px solid @grey6;
padding-bottom: 10px;
}
}
.comment-selected {
border-left: 6px solid @comment-highlight-color;
}
.comment-inline-form {
padding: @comment-padding;
display: none;
}
.cb-comment-add-button {
margin: @comment-padding;
}
/* hide add comment button when form is open */
.comment-inline-form-open ~ .cb-comment-add-button {
display: none;
}
.comment-inline-form-open {
display: block;
}
/* hide add comment button when form but no comments */
.comment-inline-form:first-child + .cb-comment-add-button {
display: none;
}
/* hide add comment button when no comments or form */
.cb-comment-add-button:first-child {
display: none;
}
/* hide add comment button when only comment is being deleted */
.comment-deleting:first-child + .cb-comment-add-button {
display: none;
}
}
/**** END COMMENTS ****/
}

View file

@ -47,6 +47,33 @@ tr.inline-comments div {
visibility: hidden;
}
.comment-label {
float: left;
padding: 0.4em 0.4em;
margin: 2px 5px 0px -10px;
display: inline-block;
min-height: 0;
text-align: center;
font-size: 10px;
line-height: .8em;
font-family: @text-italic;
background: #fff none;
color: @grey4;
border: 1px solid @grey4;
white-space: nowrap;
text-transform: uppercase;
&.todo {
color: @color5;
font-family: @text-bold-italic;
}
}
.comment {
&.comment-general {
@ -60,15 +87,19 @@ tr.inline-comments div {
.rc-user {
min-width: 0;
margin: -2px .5em 0 0;
margin: 0px .5em 0 0;
.user {
display: inline;
}
}
.meta {
position: relative;
width: 100%;
margin: 0 0 .5em 0;
border-bottom: 1px solid @grey5;
padding: 8px 0px;
margin: -5px 0px;
line-height: 24px;
&:hover .permalink {
visibility: visible;
@ -87,10 +118,10 @@ tr.inline-comments div {
}
.author-general img {
top: -3px;
top: 3px;
}
.author-inline img {
top: -3px;
top: 3px;
}
.status-change,
@ -182,7 +213,7 @@ tr.inline-comments div {
}
.pr-version-inline {
float: left;
margin: 1px 4px;
margin: 0px 4px;
}
.pr-version-num {
font-size: 10px;
@ -190,6 +221,64 @@ tr.inline-comments div {
}
@comment-padding: 5px;
.inline-comments {
border-radius: @border-radius;
.comment {
margin: 0;
border-radius: @border-radius;
}
.comment-outdated {
opacity: 0.5;
}
.comment-inline {
background: white;
padding: @comment-padding @comment-padding;
border: @comment-padding solid @grey6;
.text {
border: none;
}
.meta {
border-bottom: 1px solid @grey6;
margin: -5px 0px;
line-height: 24px;
}
}
.comment-selected {
border-left: 6px solid @comment-highlight-color;
}
.comment-inline-form {
padding: @comment-padding;
display: none;
}
.cb-comment-add-button {
margin: @comment-padding;
}
/* hide add comment button when form is open */
.comment-inline-form-open ~ .cb-comment-add-button {
display: none;
}
.comment-inline-form-open {
display: block;
}
/* hide add comment button when form but no comments */
.comment-inline-form:first-child + .cb-comment-add-button {
display: none;
}
/* hide add comment button when no comments or form */
.cb-comment-add-button:first-child {
display: none;
}
/* hide add comment button when only comment is being deleted */
.comment-deleting:first-child + .cb-comment-add-button {
display: none;
}
}
.show-outdated-comments {
display: inline;
color: @rcblue;
@ -300,6 +389,12 @@ form.comment-form {
}
}
.comment-type {
margin: 0px;
border-radius: inherit;
border-color: @grey6;
}
.preview-box {
min-height: 105px;
margin-bottom: 15px;

View file

@ -688,6 +688,7 @@ label {
padding: 0;
line-height: 1em;
border: 1px solid @grey4;
box-sizing: content-box;
&.gravatar-large {
margin: -0.5em .25em -0.5em 0;

View file

@ -132,13 +132,14 @@ var CommentForm = (function() {
this.editButton = this.withLineNo('#edit-btn');
this.editContainer = this.withLineNo('#edit-container');
this.cancelButton = this.withLineNo('#cancel-btn');
this.commentType = this.withLineNo('#comment_type');
this.statusChange = '#change_status';
this.cmBox = this.withLineNo('#text');
this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
this.statusChange = '#change_status';
this.submitForm = formElement;
this.submitButton = $(this.submitForm).find('input[type="submit"]');
this.submitButtonText = this.submitButton.val();
@ -172,7 +173,9 @@ var CommentForm = (function() {
this.getCommentStatus = function() {
return $(this.submitForm).find(this.statusChange).val();
};
this.getCommentType = function() {
return $(this.submitForm).find(this.commentType).val();
};
this.isAllowedToSubmit = function() {
return !$(this.submitButton).prop('disabled');
};
@ -256,6 +259,7 @@ var CommentForm = (function() {
this.handleFormSubmit = function() {
var text = self.cm.getValue();
var status = self.getCommentStatus();
var commentType = self.getCommentType();
if (text === "" && !status) {
return;
@ -268,6 +272,7 @@ var CommentForm = (function() {
var postData = {
'text': text,
'changeset_status': status,
'comment_type': commentType,
'csrf_token': CSRF_TOKEN
};
@ -536,6 +541,7 @@ var CommentsController = function() { /* comments controller */
$filediff.removeClass('hide-comments');
var f_path = $filediff.attr('data-f-path');
var lineno = self.getLineNumber(node);
tmpl = tmpl.format(f_path, lineno);
$form = $(tmpl);
@ -557,6 +563,7 @@ var CommentsController = function() { /* comments controller */
// set a CUSTOM submit handler for inline comments.
commentForm.setHandleFormSubmit(function(o) {
var text = commentForm.cm.getValue();
var commentType = commentForm.getCommentType();
if (text === "") {
return;
@ -579,6 +586,7 @@ var CommentsController = function() { /* comments controller */
'text': text,
'f_path': f_path,
'line': lineno,
'comment_type': commentType,
'csrf_token': CSRF_TOKEN
};
var submitSuccessCallback = function(json_data) {

View file

@ -18,8 +18,14 @@
style="${'display: none;' if outdated_at_ver else ''}">
<div class="meta">
<div class="comment-type-label tooltip">
<div class="comment-label ${comment.comment_type or 'note'}">
${comment.comment_type or 'note'}
</div>
</div>
<div class="author ${'author-inline' if inline else 'author-general'}">
${base.gravatar_with_user(comment.author.email, 20)}
${base.gravatar_with_user(comment.author.email, 16)}
</div>
<div class="date">
${h.age_component(comment.modified_at, time_is_local=True)}
@ -31,16 +37,14 @@
% if comment.pull_request:
<a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
% if comment.status_change:
${_('Vote on pull request #%s') % comment.pull_request.pull_request_id}:
${_('pull request #%s') % comment.pull_request.pull_request_id}:
% else:
${_('Comment on pull request #%s') % comment.pull_request.pull_request_id}
${_('pull request #%s') % comment.pull_request.pull_request_id}
% endif
</a>
% else:
% if comment.status_change:
${_('Status change on commit')}:
% else:
${_('Comment on commit')}
% endif
% endif
</div>
@ -185,6 +189,13 @@
<li class="">
<a href="#preview-btn" tabindex="-1" id="preview-btn">${_('Preview')}</a>
</li>
<li class="pull-right">
<select class="comment-type" id="comment_type" name="comment_type">
% for val in c.visual.comment_types:
<option value="${val}">${val.upper()}</option>
% endfor
</select>
</li>
</ul>
</div>

View file

@ -70,6 +70,13 @@ return h.url('', **new_args)
<li class="">
<a href="#preview-btn" tabindex="-1" id="preview-btn_{1}">${_('Preview')}</a>
</li>
<li class="pull-right">
<select class="comment-type" id="comment_type_{1}" name="comment_type">
% for val in c.visual.comment_types:
<option value="${val}">${val.upper()}</option>
% endfor
</select>
</li>
</ul>
</div>