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 ''}">
+
+
+ ${comment.comment_type or 'note'} +
+
+
- ${base.gravatar_with_user(comment.author.email, 20)} + ${base.gravatar_with_user(comment.author.email, 16)}
${h.age_component(comment.modified_at, time_is_local=True)} @@ -31,16 +37,14 @@ % if comment.pull_request: % 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 % else: % if comment.status_change: ${_('Status change on commit')}: - % else: - ${_('Comment on commit')} % endif % endif
@@ -185,6 +189,13 @@
  • ${_('Preview')}
  • +
  • + +
  • 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')}
  • +
  • + +