diff --git a/rhodecode/__init__.py b/rhodecode/__init__.py
--- a/rhodecode/__init__.py
+++ b/rhodecode/__init__.py
@@ -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'
diff --git a/rhodecode/controllers/changeset.py b/rhodecode/controllers/changeset.py
--- a/rhodecode/controllers/changeset.py
+++ b/rhodecode/controllers/changeset.py
@@ -334,6 +334,8 @@ class ChangesetController(BaseRepoContro
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(BaseRepoContro
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
diff --git a/rhodecode/controllers/pullrequests.py b/rhodecode/controllers/pullrequests.py
--- a/rhodecode/controllers/pullrequests.py
+++ b/rhodecode/controllers/pullrequests.py
@@ -903,6 +903,7 @@ class PullrequestsController(BaseRepoCon
# 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(BaseRepoCon
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:
diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py
--- a/rhodecode/lib/base.py
+++ b/rhodecode/lib/base.py
@@ -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, r
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')
diff --git a/rhodecode/lib/dbmigrate/versions/064_version_4_6_0.py b/rhodecode/lib/dbmigrate/versions/064_version_4_6_0.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/lib/dbmigrate/versions/064_version_4_6_0.py
@@ -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
diff --git a/rhodecode/model/comment.py b/rhodecode/model/comment.py
--- a/rhodecode/model/comment.py
+++ b/rhodecode/model/comment.py
@@ -44,6 +44,8 @@ from rhodecode.model.notification import
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
diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py
--- a/rhodecode/model/db.py
+++ b/rhodecode/model/db.py
@@ -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")
diff --git a/rhodecode/model/validation_schema/schemas/comment_schema.py b/rhodecode/model/validation_schema/schemas/comment_schema.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/model/validation_schema/schemas/comment_schema.py
@@ -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 .
+#
+# 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())
diff --git a/rhodecode/model/validation_schema/types.py b/rhodecode/model/validation_schema/types.py
--- a/rhodecode/model/validation_schema/types.py
+++ b/rhodecode/model/validation_schema/types.py
@@ -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)
diff --git a/rhodecode/public/css/code-block.less b/rhodecode/public/css/code-block.less
--- a/rhodecode/public/css/code-block.less
+++ b/rhodecode/public/css/code-block.less
@@ -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 ****/
}
diff --git a/rhodecode/public/css/comments.less b/rhodecode/public/css/comments.less
--- a/rhodecode/public/css/comments.less
+++ b/rhodecode/public/css/comments.less
@@ -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;
diff --git a/rhodecode/public/css/main.less b/rhodecode/public/css/main.less
--- a/rhodecode/public/css/main.less
+++ b/rhodecode/public/css/main.less
@@ -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;
diff --git a/rhodecode/public/js/src/rhodecode/comments.js b/rhodecode/public/js/src/rhodecode/comments.js
--- a/rhodecode/public/js/src/rhodecode/comments.js
+++ b/rhodecode/public/js/src/rhodecode/comments.js
@@ -132,12 +132,13 @@ 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.cancelButton = this.withLineNo('#cancel-btn');
+ this.cmBox = this.withLineNo('#text');
+ this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
this.statusChange = '#change_status';
- this.cmBox = this.withLineNo('#text');
- this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
this.submitForm = formElement;
this.submitButton = $(this.submitForm).find('input[type="submit"]');
@@ -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() { /*
$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() { /*
// 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() { /*
'text': text,
'f_path': f_path,
'line': lineno,
+ 'comment_type': commentType,
'csrf_token': CSRF_TOKEN
};
var submitSuccessCallback = function(json_data) {
diff --git a/rhodecode/templates/changeset/changeset_file_comment.mako b/rhodecode/templates/changeset/changeset_file_comment.mako
--- a/rhodecode/templates/changeset/changeset_file_comment.mako
+++ b/rhodecode/templates/changeset/changeset_file_comment.mako
@@ -18,8 +18,14 @@
style="${'display: none;' if outdated_at_ver else ''}">
diff --git a/rhodecode/templates/codeblocks/diffs.mako b/rhodecode/templates/codeblocks/diffs.mako
--- a/rhodecode/templates/codeblocks/diffs.mako
+++ b/rhodecode/templates/codeblocks/diffs.mako
@@ -70,6 +70,13 @@ return h.url('', **new_args)
${_('Preview')}
+
+
+