##// END OF EJS Templates
core: avoid using rhodecode.test packages inside main packages as tests are removed during build which can cause some problems in some edge case calls
core: avoid using rhodecode.test packages inside main packages as tests are removed during build which can cause some problems in some edge case calls

File last commit:

r5608:6d33e504 default
r5618:bdbdb63f default
Show More
comment.py
848 lines | 31.8 KiB | text/x-python | PythonLexer
core: updated copyright to 2024
r5608 # Copyright (C) 2011-2024 RhodeCode GmbH
project: added all source files and assets
r1 #
# 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/
"""
comments model for RhodeCode
"""
comments: save modification date for main comment on edit.
r4407 import datetime
project: added all source files and assets
r1
import logging
import traceback
import collections
events: expose permalink urls for different set of object....
r1788 from pyramid.threadlocal import get_current_registry, get_current_request
project: added all source files and assets
r1 from sqlalchemy.sql.expression import null
from sqlalchemy.sql.functions import coalesce
dan
hooks: added new hooks for comments on pull requests and commits....
r4305 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
audit-logs: implemented pull request and comment events.
r1807 from rhodecode.lib import audit_logger
comment-history: fixes/ui changes...
r4408 from rhodecode.lib.exceptions import CommentVersionMismatch
comments-api: make edit API resilent to bad version data.
r4443 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
project: added all source files and assets
r1 from rhodecode.model import BaseModel
from rhodecode.model.db import (
drafts: sidebar functionality
r4562 false, true,
comments: edit functionality added
r4401 ChangesetComment,
User,
Notification,
PullRequest,
AttributeDict,
ChangesetCommentHistory,
)
project: added all source files and assets
r1 from rhodecode.model.notification import NotificationModel
from rhodecode.model.meta import Session
from rhodecode.model.settings import VcsSettingsModel
from rhodecode.model.notification import EmailNotificationModel
comments: add comments type into comments.
r1324 from rhodecode.model.validation_schema.schemas import comment_schema
project: added all source files and assets
r1
log = logging.getLogger(__name__)
comments: renamed ChangesetCommentsModel to CommentsModel to reflect what it actually does....
r1323 class CommentsModel(BaseModel):
project: added all source files and assets
r1
cls = ChangesetComment
DIFF_CONTEXT_BEFORE = 3
DIFF_CONTEXT_AFTER = 3
def __get_commit_comment(self, changeset_comment):
return self._get_instance(ChangesetComment, changeset_comment)
def __get_pull_request(self, pull_request):
return self._get_instance(PullRequest, pull_request)
def _extract_mentions(self, s):
user_objects = []
for username in extract_mentioned_users(s):
user_obj = User.get_by_username(username, case_insensitive=True)
if user_obj:
user_objects.append(user_obj)
return user_objects
pylons: remove pylons as dependency...
r2351 def _get_renderer(self, global_renderer='rst', request=None):
request = request or get_current_request()
comments: remove usage of pylons global config.
r2005
project: added all source files and assets
r1 try:
comments: remove usage of pylons global config.
r2005 global_renderer = request.call_context.visual.default_renderer
project: added all source files and assets
r1 except AttributeError:
log.debug("Renderer not set, falling back "
"to default renderer '%s'", global_renderer)
except Exception:
log.error(traceback.format_exc())
return global_renderer
comments: use unified aggregation of comments counters....
r1332 def aggregate_comments(self, comments, versions, show_version, inline=False):
# group by versions, and count until, and display objects
comment_groups = collections.defaultdict(list)
pull-requests: overhaul of the UX by adding new sidebar...
r4482 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
comments: use unified aggregation of comments counters....
r1332
def yield_comments(pos):
modernize: python3 updates
r5096 yield from comment_groups[pos]
comments: use unified aggregation of comments counters....
r1332
comment_versions = collections.defaultdict(
lambda: collections.defaultdict(list))
prev_prvid = -1
# fake last entry with None, to aggregate on "latest" version which
# doesn't have an pull_request_version_id
for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
prvid = ver.pull_request_version_id
if prev_prvid == -1:
prev_prvid = prvid
for co in yield_comments(prvid):
comment_versions[prvid]['at'].append(co)
# save until
current = comment_versions[prvid]['at']
prev_until = comment_versions[prev_prvid]['until']
cur_until = prev_until + current
comment_versions[prvid]['until'].extend(cur_until)
# save outdated
if inline:
outdated = [x for x in cur_until
if x.outdated_at_version(show_version)]
else:
outdated = [x for x in cur_until
if x.older_than_version(show_version)]
display = [x for x in cur_until if x not in outdated]
comment_versions[prvid]['outdated'] = outdated
comment_versions[prvid]['display'] = display
prev_prvid = prvid
return comment_versions
api: added function to fetch comments for a repository.
r3435 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
qry = Session().query(ChangesetComment) \
.filter(ChangesetComment.repo == repo)
if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
qry = qry.filter(ChangesetComment.comment_type == comment_type)
if user:
user = self._get_user(user)
if user:
qry = qry.filter(ChangesetComment.user_id == user.user_id)
if commit_id:
qry = qry.filter(ChangesetComment.revision == commit_id)
qry = qry.order_by(ChangesetComment.created_on)
return qry.all()
comments: expose a function to fetch unresolved TODOs for repository
r3433 def get_repository_unresolved_todos(self, repo):
todos = Session().query(ChangesetComment) \
.filter(ChangesetComment.repo == repo) \
lint: use null() to compare to == None for linters to be happy
r5180 .filter(ChangesetComment.resolved_by == null()) \
comments: expose a function to fetch unresolved TODOs for repository
r3433 .filter(ChangesetComment.comment_type
== ChangesetComment.COMMENT_TYPE_TODO)
todos = todos.all()
return todos
comments: introduce new draft comments....
r4540 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
pull-request: introduced new merge-checks....
r1334
todos = Session().query(ChangesetComment) \
.filter(ChangesetComment.pull_request == pull_request) \
lint: use null() to compare to == None for linters to be happy
r5180 .filter(ChangesetComment.resolved_by == null()) \
pull-request: introduced new merge-checks....
r1334 .filter(ChangesetComment.comment_type
todos: all todos needs to be resolved for merge to happen....
r1342 == ChangesetComment.COMMENT_TYPE_TODO)
comments: introduce new draft comments....
r4540 if not include_drafts:
todos = todos.filter(ChangesetComment.draft == false())
todos: all todos needs to be resolved for merge to happen....
r1342 if not show_outdated:
todos = todos.filter(
coalesce(ChangesetComment.display_state, '') !=
ChangesetComment.COMMENT_OUTDATED)
todos = todos.all()
pull-request: introduced new merge-checks....
r1334
return todos
comments: introduce new draft comments....
r4540 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
comments: re-implemented diff and comments/todos in pull-requests.
r3884
todos = Session().query(ChangesetComment) \
.filter(ChangesetComment.pull_request == pull_request) \
.filter(ChangesetComment.resolved_by != None) \
.filter(ChangesetComment.comment_type
== ChangesetComment.COMMENT_TYPE_TODO)
comments: introduce new draft comments....
r4540 if not include_drafts:
todos = todos.filter(ChangesetComment.draft == false())
comments: re-implemented diff and comments/todos in pull-requests.
r3884 if not show_outdated:
todos = todos.filter(
coalesce(ChangesetComment.display_state, '') !=
ChangesetComment.COMMENT_OUTDATED)
todos = todos.all()
return todos
drafts: sidebar functionality
r4562 def get_pull_request_drafts(self, user_id, pull_request):
drafts = Session().query(ChangesetComment) \
.filter(ChangesetComment.pull_request == pull_request) \
.filter(ChangesetComment.user_id == user_id) \
.filter(ChangesetComment.draft == true())
return drafts.all()
comments: introduce new draft comments....
r4540 def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
commit-page: show unresolved TODOs on commit page below comments.
r1385
todos = Session().query(ChangesetComment) \
.filter(ChangesetComment.revision == commit_id) \
lint: use null() to compare to == None for linters to be happy
r5180 .filter(ChangesetComment.resolved_by == null()) \
commit-page: show unresolved TODOs on commit page below comments.
r1385 .filter(ChangesetComment.comment_type
== ChangesetComment.COMMENT_TYPE_TODO)
comments: introduce new draft comments....
r4540 if not include_drafts:
todos = todos.filter(ChangesetComment.draft == false())
commit-page: show unresolved TODOs on commit page below comments.
r1385 if not show_outdated:
todos = todos.filter(
coalesce(ChangesetComment.display_state, '') !=
ChangesetComment.COMMENT_OUTDATED)
todos = todos.all()
return todos
comments: introduce new draft comments....
r4540 def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
ui: new commits page....
r3882
todos = Session().query(ChangesetComment) \
.filter(ChangesetComment.revision == commit_id) \
.filter(ChangesetComment.resolved_by != None) \
.filter(ChangesetComment.comment_type
== ChangesetComment.COMMENT_TYPE_TODO)
comments: introduce new draft comments....
r4540 if not include_drafts:
todos = todos.filter(ChangesetComment.draft == false())
ui: new commits page....
r3882 if not show_outdated:
todos = todos.filter(
coalesce(ChangesetComment.display_state, '') !=
ChangesetComment.COMMENT_OUTDATED)
todos = todos.all()
return todos
comments: introduce new draft comments....
r4540 def get_commit_inline_comments(self, commit_id, include_drafts=True):
commits/pr pages various fixes....
r4485 inline_comments = Session().query(ChangesetComment) \
.filter(ChangesetComment.line_no != None) \
.filter(ChangesetComment.f_path != None) \
.filter(ChangesetComment.revision == commit_id)
comments: introduce new draft comments....
r4540
if not include_drafts:
inline_comments = inline_comments.filter(ChangesetComment.draft == false())
commits/pr pages various fixes....
r4485 inline_comments = inline_comments.all()
return inline_comments
audit-logs: store properly IP and user for certain comments types....
r2728 def _log_audit_action(self, action, action_data, auth_user, comment):
audit-logs: implemented pull request and comment events.
r1807 audit_logger.store(
action=action,
action_data=action_data,
audit-logs: store properly IP and user for certain comments types....
r2728 user=auth_user,
audit-logs: implemented pull request and comment events.
r1807 repo=comment.repo)
comments: changed signature of create method of ChangesetComment....
r1322 def create(self, text, repo, user, commit_id=None, pull_request=None,
comments: allow submitting id of comment which submitted comment resolved....
r1325 f_path=None, line_no=None, status_change=None,
comments: introduce new draft comments....
r4540 status_change_type=None, comment_type=None, is_draft=False,
comments: allow submitting id of comment which submitted comment resolved....
r1325 resolves_comment_id=None, closing_pr=False, send_email=True,
api: allow extra recipients for pr/commit comments api methods...
r4049 renderer=None, auth_user=None, extra_recipients=None):
project: added all source files and assets
r1 """
Creates new comment for commit or pull request.
IF status_change is not none this comment is associated with a
status change of commit or commit associated with pull request
:param text:
:param repo:
:param user:
comments: changed signature of create method of ChangesetComment....
r1322 :param commit_id:
project: added all source files and assets
r1 :param pull_request:
:param f_path:
:param line_no:
emails: added new tags to status sent...
r548 :param status_change: Label for status change
comments: changed signature of create method of ChangesetComment....
r1322 :param comment_type: Type of comment
comments: introduce new draft comments....
r4540 :param is_draft: is comment a draft only
api: allow extra recipients for pr/commit comments api methods...
r4049 :param resolves_comment_id: id of comment which this one will resolve
emails: added new tags to status sent...
r548 :param status_change_type: type of status change
project: added all source files and assets
r1 :param closing_pr:
:param send_email:
comments: changed signature of create method of ChangesetComment....
r1322 :param renderer: pick renderer for this comment
api: allow extra recipients for pr/commit comments api methods...
r4049 :param auth_user: current authenticated user calling this method
:param extra_recipients: list of extra users to be added to recipients
project: added all source files and assets
r1 """
audit-logs: store properly IP and user for certain comments types....
r2728
pylons: remove pylons as dependency...
r2351 request = get_current_request()
_ = request.translate
project: added all source files and assets
r1
if not renderer:
pylons: remove pylons as dependency...
r2351 renderer = self._get_renderer(request=request)
project: added all source files and assets
r1
comments: allow submitting id of comment which submitted comment resolved....
r1325 repo = self._get_repo(repo)
user = self._get_user(user)
comments: fix extracing auth_user from the passed in objects. Before if auth_user is empty we could relly on INT or STR passed in
r3026 auth_user = auth_user or user
comments: add comments type into comments.
r1324
schema = comment_schema.CommentSchema()
validated_kwargs = schema.deserialize(dict(
comment_body=text,
comment_type=comment_type,
comments: introduce new draft comments....
r4540 is_draft=is_draft,
comments: add comments type into comments.
r1324 comment_file=f_path,
comment_line=line_no,
renderer_type=renderer,
comments: allow submitting id of comment which submitted comment resolved....
r1325 status_change=status_change_type,
resolves_comment_id=resolves_comment_id,
repo=repo.repo_id,
user=user.user_id,
comments: add comments type into comments.
r1324 ))
models: update db.py with major changes for python3
r5071
comments: introduce new draft comments....
r4540 is_draft = validated_kwargs['is_draft']
comments: add comments type into comments.
r1324
project: added all source files and assets
r1 comment = ChangesetComment()
comments: add comments type into comments.
r1324 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']
comments: introduce new draft comments....
r4540 comment.draft = is_draft
comments: add comments type into comments.
r1324
project: added all source files and assets
r1 comment.repo = repo
comment.author = user
pull-request: verify resolve TODO comment needs to be bound to the same PR as we're calling
r2441 resolved_comment = self.__get_commit_comment(
comments: allow submitting id of comment which submitted comment resolved....
r1325 validated_kwargs['resolves_comment_id'])
comments: add ability to resolve todos from the side-bar.
r4633
pull-request: verify resolve TODO comment needs to be bound to the same PR as we're calling
r2441 # check if the comment actually belongs to this PR
if resolved_comment and resolved_comment.pull_request and \
resolved_comment.pull_request != pull_request:
comments[security]: make an additional check to forbid solving comments from other repo scope.
r3546 log.warning('Comment tried to resolved unrelated todo comment: %s',
resolved_comment)
pull-request: verify resolve TODO comment needs to be bound to the same PR as we're calling
r2441 # comment not bound to this pull request, forbid
resolved_comment = None
comments[security]: make an additional check to forbid solving comments from other repo scope.
r3546
elif resolved_comment and resolved_comment.repo and \
resolved_comment.repo != repo:
log.warning('Comment tried to resolved unrelated todo comment: %s',
resolved_comment)
# comment not bound to this repo, forbid
resolved_comment = None
comments: add ability to resolve todos from the side-bar.
r4633 if resolved_comment and resolved_comment.resolved_by:
# if this comment is already resolved, don't mark it again!
resolved_comment = None
pull-request: verify resolve TODO comment needs to be bound to the same PR as we're calling
r2441 comment.resolved_comment = resolved_comment
project: added all source files and assets
r1
pull_request_id = pull_request
commit_obj = None
pull_request_obj = None
if commit_id:
notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
# do a lookup, so we don't pass something bad here
commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
comment.revision = commit_obj.raw_id
elif pull_request_id:
notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
pull_request_obj = self.__get_pull_request(pull_request_id)
comment.pull_request = pull_request_obj
else:
raise Exception('Please specify commit or pull_request_id')
Session().add(comment)
Session().flush()
tests: fixed test suite for celery adoption
r5607
notifications: support real-time notifications with websockets via channelstream
r526 kwargs = {
'user': user,
'renderer_type': renderer,
'repo_name': repo.repo_name,
'status_change': status_change,
emails: added new tags to status sent...
r548 'status_change_type': status_change_type,
notifications: support real-time notifications with websockets via channelstream
r526 'comment_body': text,
'comment_file': f_path,
'comment_line': line_no,
dan
emails: added reply link to comment type emails...
r4050 'comment_type': comment_type or 'note',
'comment_id': comment.comment_id
notifications: support real-time notifications with websockets via channelstream
r526 }
project: added all source files and assets
r1
notifications: support real-time notifications with websockets via channelstream
r526 if commit_obj:
tests: fixed test suite for celery adoption
r5607 recipients = ChangesetComment.get_users(revision=commit_obj.raw_id)
notifications: support real-time notifications with websockets via channelstream
r526 # add commit author if it's in RhodeCode system
cs_author = User.get_from_cs_author(commit_obj.author)
if not cs_author:
# use repo owner if we cannot extract the author correctly
cs_author = repo.user
recipients += [cs_author]
project: added all source files and assets
r1
pylons: remove pylons as dependency...
r2351 commit_comment_url = self.get_url(comment, request=request)
tests: fixed test suite for celery adoption
r5607 commit_comment_reply_url = self.get_url(comment, request=request, anchor=f'comment-{comment.comment_id}/?/ReplyToComment')
project: added all source files and assets
r1
notifications: support real-time notifications with websockets via channelstream
r526 target_repo_url = h.link_to(
repo.repo_name,
repo-summary: re-implemented summary view as pyramid....
r1785 h.route_url('repo_summary', repo_name=repo.repo_name))
project: added all source files and assets
r1
tests: fixed test suite for celery adoption
r5607 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name, commit_id=commit_id)
emails: set References header for threading in mail user agents even with different subjects...
r4447
notifications: support real-time notifications with websockets via channelstream
r526 # commit specifics
kwargs.update({
'commit': commit_obj,
'commit_message': commit_obj.message,
dan
emails: updated emails design and data structure they provide....
r4038 'commit_target_repo_url': target_repo_url,
notifications: support real-time notifications with websockets via channelstream
r526 'commit_comment_url': commit_comment_url,
emails: set References header for threading in mail user agents even with different subjects...
r4447 'commit_comment_reply_url': commit_comment_reply_url,
'commit_url': commit_url,
'thread_ids': [commit_url, commit_comment_url],
notifications: support real-time notifications with websockets via channelstream
r526 })
project: added all source files and assets
r1
notifications: support real-time notifications with websockets via channelstream
r526 elif pull_request_obj:
# get the current participants of this pull request
recipients = ChangesetComment.get_users(
pull_request_id=pull_request_obj.pull_request_id)
# add pull request author
recipients += [pull_request_obj.author]
project: added all source files and assets
r1
notifications: support real-time notifications with websockets via channelstream
r526 # add the reviewers to notification
reviewers: use common function to obtain reviewers with filter of role.
r4514 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
project: added all source files and assets
r1
notifications: support real-time notifications with websockets via channelstream
r526 pr_target_repo = pull_request_obj.target_repo
pr_source_repo = pull_request_obj.source_repo
project: added all source files and assets
r1
dan
emails: added reply link to comment type emails...
r4050 pr_comment_url = self.get_url(comment, request=request)
pr_comment_reply_url = self.get_url(
comment, request=request,
modernize: python3 updates
r5096 anchor=f'comment-{comment.comment_id}/?/ReplyToComment')
project: added all source files and assets
r1
dan
emails: updated emails design and data structure they provide....
r4038 pr_url = h.route_url(
'pullrequest_show',
repo_name=pr_target_repo.repo_name,
pull_request_id=pull_request_obj.pull_request_id, )
notifications: support real-time notifications with websockets via channelstream
r526 # set some variables for email notification
repo-summary: re-implemented summary view as pyramid....
r1785 pr_target_repo_url = h.route_url(
'repo_summary', repo_name=pr_target_repo.repo_name)
project: added all source files and assets
r1
repo-summary: re-implemented summary view as pyramid....
r1785 pr_source_repo_url = h.route_url(
'repo_summary', repo_name=pr_source_repo.repo_name)
project: added all source files and assets
r1
notifications: support real-time notifications with websockets via channelstream
r526 # pull request specifics
kwargs.update({
'pull_request': pull_request_obj,
'pr_id': pull_request_obj.pull_request_id,
dan
emails: updated emails design and data structure they provide....
r4038 'pull_request_url': pr_url,
'pull_request_target_repo': pr_target_repo,
'pull_request_target_repo_url': pr_target_repo_url,
'pull_request_source_repo': pr_source_repo,
'pull_request_source_repo_url': pr_source_repo_url,
notifications: support real-time notifications with websockets via channelstream
r526 'pr_comment_url': pr_comment_url,
dan
emails: added reply link to comment type emails...
r4050 'pr_comment_reply_url': pr_comment_reply_url,
notifications: support real-time notifications with websockets via channelstream
r526 'pr_closing': closing_pr,
emails: set References header for threading in mail user agents even with different subjects...
r4447 'thread_ids': [pr_url, pr_comment_url],
notifications: support real-time notifications with websockets via channelstream
r526 })
api: allow extra recipients for pr/commit comments api methods...
r4049
notifications: support real-time notifications with websockets via channelstream
r526 if send_email:
reviewers: added observers as another way to define reviewers....
r4500 recipients += [self._get_user(u) for u in (extra_recipients or [])]
project: added all source files and assets
r1
mention_recipients = set(
self._extract_mentions(text)).difference(recipients)
# create notification objects, and emails
NotificationModel().create(
created_by=user,
notifications: skip double rendering just to generate email title/desc
r4560 notification_subject='', # Filled in based on the notification_type
notification_body='', # Filled in based on the notification_type
project: added all source files and assets
r1 notification_type=notification_type,
recipients=recipients,
mention_recipients=mention_recipients,
email_kwargs=kwargs,
)
audit-logs: implemented pull request and comment events.
r1807 Session().flush()
if comment.pull_request:
action = 'repo.pull_request.comment.create'
else:
action = 'repo.commit.comment.create'
comments: introduce new draft comments....
r4540 if not is_draft:
comment_data = comment.get_api_data()
self._log_audit_action(
action, {'data': comment_data}, auth_user, comment)
project: added all source files and assets
r1
return comment
comments: edit functionality added
r4401 def edit(self, comment_id, text, auth_user, version):
"""
Change existing comment for commit or pull request.
:param comment_id:
:param text:
:param auth_user: current authenticated user calling this method
:param version: last comment version
"""
if not text:
log.warning('Missing text for comment, skipping...')
return
comment = ChangesetComment.get(comment_id)
old_comment_text = comment.text
comment.text = text
comments: save modification date for main comment on edit.
r4407 comment.modified_at = datetime.datetime.now()
comments-api: make edit API resilent to bad version data.
r4443 version = safe_int(version)
comments: save modification date for main comment on edit.
r4407
comments-api: make edit API resilent to bad version data.
r4443 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
# would return 3 here
comments: edit functionality added
r4401 comment_version = ChangesetCommentHistory.get_version(comment_id)
comments-api: make edit API resilent to bad version data.
r4443
python3: fix usage of int/long
r4935 if isinstance(version, int) and (comment_version - version) != 1:
comments: edit functionality added
r4401 log.warning(
comment-history: fixes/ui changes...
r4408 'Version mismatch comment_version {} submitted {}, skipping'.format(
comments-api: make edit API resilent to bad version data.
r4443 comment_version-1, # -1 since note above
comment-history: fixes/ui changes...
r4408 version
comments: edit functionality added
r4401 )
)
comment-history: fixes/ui changes...
r4408 raise CommentVersionMismatch()
comments: edit functionality added
r4401 comment_history = ChangesetCommentHistory()
comment_history.comment_id = comment_id
comment_history.version = comment_version
comment_history.created_by_user_id = auth_user.user_id
comment_history.text = old_comment_text
# TODO add email notification
Session().add(comment_history)
Session().add(comment)
Session().flush()
if comment.pull_request:
action = 'repo.pull_request.comment.edit'
else:
action = 'repo.commit.comment.edit'
comment_data = comment.get_api_data()
comment_data['old_comment_text'] = old_comment_text
self._log_audit_action(
action, {'data': comment_data}, auth_user, comment)
return comment_history
audit-logs: store properly IP and user for certain comments types....
r2728 def delete(self, comment, auth_user):
project: added all source files and assets
r1 """
Deletes given comment
"""
comment = self.__get_commit_comment(comment)
audit-logs: implemented pull request and comment events.
r1807 old_data = comment.get_api_data()
project: added all source files and assets
r1 Session().delete(comment)
audit-logs: implemented pull request and comment events.
r1807 if comment.pull_request:
action = 'repo.pull_request.comment.delete'
else:
action = 'repo.commit.comment.delete'
self._log_audit_action(
audit-logs: store properly IP and user for certain comments types....
r2728 action, {'old_data': old_data}, auth_user, comment)
audit-logs: implemented pull request and comment events.
r1807
project: added all source files and assets
r1 return comment
comments: counts exclude now draft comments, they shouldn't be visible.
r4553 def get_all_comments(self, repo_id, revision=None, pull_request=None,
include_drafts=True, count_only=False):
project: added all source files and assets
r1 q = ChangesetComment.query()\
.filter(ChangesetComment.repo_id == repo_id)
if revision:
q = q.filter(ChangesetComment.revision == revision)
elif pull_request:
pull_request = self.__get_pull_request(pull_request)
pull-requests: use count only for comments related to display grids on my account and repo view.
r4506 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
project: added all source files and assets
r1 else:
raise Exception('Please specify commit or pull_request')
comments: counts exclude now draft comments, they shouldn't be visible.
r4553 if not include_drafts:
q = q.filter(ChangesetComment.draft == false())
project: added all source files and assets
r1 q = q.order_by(ChangesetComment.created_on)
pull-requests: use count only for comments related to display grids on my account and repo view.
r4506 if count_only:
return q.count()
project: added all source files and assets
r1 return q.all()
dan
emails: added reply link to comment type emails...
r4050 def get_url(self, comment, request=None, permalink=False, anchor=None):
events: expose permalink urls for different set of object....
r1788 if not request:
request = get_current_request()
dan
events: add an event for pull request comments with review status
r443 comment = self.__get_commit_comment(comment)
dan
emails: added reply link to comment type emails...
r4050 if anchor is None:
modernize: python3 updates
r5096 anchor = f'comment-{comment.comment_id}'
dan
emails: added reply link to comment type emails...
r4050
dan
events: add an event for pull request comments with review status
r443 if comment.pull_request:
events: expose permalink urls for different set of object....
r1788 pull_request = comment.pull_request
if permalink:
return request.route_url(
'pull_requests_global',
pull_request_id=pull_request.pull_request_id,
dan
emails: added reply link to comment type emails...
r4050 _anchor=anchor)
events: expose permalink urls for different set of object....
r1788 else:
events: properly refresh comment object to load it's relationship....
r2470 return request.route_url(
'pullrequest_show',
events: expose permalink urls for different set of object....
r1788 repo_name=safe_str(pull_request.target_repo.repo_name),
pull_request_id=pull_request.pull_request_id,
dan
emails: added reply link to comment type emails...
r4050 _anchor=anchor)
events: expose permalink urls for different set of object....
r1788
dan
events: add an event for pull request comments with review status
r443 else:
events: expose permalink urls for different set of object....
r1788 repo = comment.repo
commit_id = comment.revision
if permalink:
return request.route_url(
'repo_commit', repo_name=safe_str(repo.repo_id),
commit_id=commit_id,
dan
emails: added reply link to comment type emails...
r4050 _anchor=anchor)
events: expose permalink urls for different set of object....
r1788
else:
return request.route_url(
'repo_commit', repo_name=safe_str(repo.repo_name),
commit_id=commit_id,
dan
emails: added reply link to comment type emails...
r4050 _anchor=anchor)
dan
events: add an event for pull request comments with review status
r443
project: added all source files and assets
r1 def get_comments(self, repo_id, revision=None, pull_request=None):
"""
Gets main comments based on revision or pull_request_id
:param repo_id:
:param revision:
:param pull_request:
"""
q = ChangesetComment.query()\
.filter(ChangesetComment.repo_id == repo_id)\
lint: use null() to compare to == None for linters to be happy
r5180 .filter(ChangesetComment.line_no == null())\
.filter(ChangesetComment.f_path == null())
project: added all source files and assets
r1 if revision:
q = q.filter(ChangesetComment.revision == revision)
elif pull_request:
pull_request = self.__get_pull_request(pull_request)
q = q.filter(ChangesetComment.pull_request == pull_request)
else:
raise Exception('Please specify commit or pull_request')
q = q.order_by(ChangesetComment.created_on)
return q.all()
def get_inline_comments(self, repo_id, revision=None, pull_request=None):
q = self._get_inline_comments_query(repo_id, revision, pull_request)
return self._group_comments_by_path_and_line_number(q)
pull-requests: added observers, and fix few problems with versioned comments
r4481 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
pull-requests: overhaul of the UX by adding new sidebar...
r4482 version=None):
inline_comms = []
python3: removed usage of .iteritems()
r4932 for fname, per_line_comments in inline_comments.items():
for lno, comments in per_line_comments.items():
pull-requests: updated versioning support....
r1268 for comm in comments:
if not comm.outdated_at_version(version) and skip_outdated:
pull-requests: overhaul of the UX by adding new sidebar...
r4482 inline_comms.append(comm)
pull-requests: updated versioning support....
r1268
pull-requests: overhaul of the UX by adding new sidebar...
r4482 return inline_comms
inline-comments: added helper to properly count inline comments.
r1206
project: added all source files and assets
r1 def get_outdated_comments(self, repo_id, pull_request):
# TODO: johbo: Remove `repo_id`, it is not needed to find the comments
# of a pull request.
q = self._all_inline_comments_of_pull_request(pull_request)
q = q.filter(
ChangesetComment.display_state ==
ChangesetComment.COMMENT_OUTDATED
).order_by(ChangesetComment.comment_id.asc())
return self._group_comments_by_path_and_line_number(q)
def _get_inline_comments_query(self, repo_id, revision, pull_request):
# TODO: johbo: Split this into two methods: One for PR and one for
# commit.
if revision:
q = Session().query(ChangesetComment).filter(
ChangesetComment.repo_id == repo_id,
ChangesetComment.line_no != null(),
ChangesetComment.f_path != null(),
ChangesetComment.revision == revision)
elif pull_request:
pull_request = self.__get_pull_request(pull_request)
comments: renamed ChangesetCommentsModel to CommentsModel to reflect what it actually does....
r1323 if not CommentsModel.use_outdated_comments(pull_request):
project: added all source files and assets
r1 q = self._visible_inline_comments_of_pull_request(pull_request)
else:
q = self._all_inline_comments_of_pull_request(pull_request)
else:
raise Exception('Please specify commit or pull_request_id')
q = q.order_by(ChangesetComment.comment_id.asc())
return q
def _group_comments_by_path_and_line_number(self, q):
comments = q.all()
paths = collections.defaultdict(lambda: collections.defaultdict(list))
for co in comments:
paths[co.f_path][co.line_no].append(co)
return paths
@classmethod
def needed_extra_diff_context(cls):
return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
comments: renamed ChangesetCommentsModel to CommentsModel to reflect what it actually does....
r1323 if not CommentsModel.use_outdated_comments(pull_request):
project: added all source files and assets
r1 return
comments = self._visible_inline_comments_of_pull_request(pull_request)
comments_to_outdate = comments.all()
for comment in comments_to_outdate:
self._outdate_one_comment(comment, old_diff_data, new_diff_data)
def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
diff_line = _parse_comment_line_number(comment.line_no)
try:
old_context = old_diff_proc.get_context_of_line(
path=comment.f_path, diff_line=diff_line)
new_context = new_diff_proc.get_context_of_line(
path=comment.f_path, diff_line=diff_line)
except (diffs.LineNotInDiffException,
diffs.FileNotInDiffException):
comments: don't mark draft comments as outdated.
r4556 if not comment.draft:
comment.display_state = ChangesetComment.COMMENT_OUTDATED
project: added all source files and assets
r1 return
if old_context == new_context:
return
if self._should_relocate_diff_line(diff_line):
new_diff_lines = new_diff_proc.find_context(
path=comment.f_path, context=old_context,
offset=self.DIFF_CONTEXT_BEFORE)
comments: don't mark draft comments as outdated.
r4556 if not new_diff_lines and not comment.draft:
project: added all source files and assets
r1 comment.display_state = ChangesetComment.COMMENT_OUTDATED
else:
new_diff_line = self._choose_closest_diff_line(
diff_line, new_diff_lines)
comment.line_no = _diff_to_comment_line_number(new_diff_line)
else:
comments: don't mark draft comments as outdated.
r4556 if not comment.draft:
comment.display_state = ChangesetComment.COMMENT_OUTDATED
project: added all source files and assets
r1
def _should_relocate_diff_line(self, diff_line):
"""
Checks if relocation shall be tried for the given `diff_line`.
If a comment points into the first lines, then we can have a situation
that after an update another line has been added on top. In this case
we would find the context still and move the comment around. This
would be wrong.
"""
should_relocate = (
(diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
(diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
return should_relocate
def _choose_closest_diff_line(self, diff_line, new_diff_lines):
candidate = new_diff_lines[0]
best_delta = _diff_line_delta(diff_line, candidate)
for new_diff_line in new_diff_lines[1:]:
delta = _diff_line_delta(diff_line, new_diff_line)
if delta < best_delta:
candidate = new_diff_line
best_delta = delta
return candidate
def _visible_inline_comments_of_pull_request(self, pull_request):
comments = self._all_inline_comments_of_pull_request(pull_request)
comments = comments.filter(
coalesce(ChangesetComment.display_state, '') !=
ChangesetComment.COMMENT_OUTDATED)
return comments
def _all_inline_comments_of_pull_request(self, pull_request):
comments = Session().query(ChangesetComment)\
lint: use null() to compare to == None for linters to be happy
r5180 .filter(ChangesetComment.line_no != null())\
.filter(ChangesetComment.f_path != null())\
project: added all source files and assets
r1 .filter(ChangesetComment.pull_request == pull_request)
return comments
comments: use unified aggregation of comments counters....
r1332 def _all_general_comments_of_pull_request(self, pull_request):
comments = Session().query(ChangesetComment)\
lint: use null() to compare to == None for linters to be happy
r5180 .filter(ChangesetComment.line_no == null())\
.filter(ChangesetComment.f_path == null())\
comments: use unified aggregation of comments counters....
r1332 .filter(ChangesetComment.pull_request == pull_request)
comments: edit functionality added
r4401
comments: use unified aggregation of comments counters....
r1332 return comments
project: added all source files and assets
r1 @staticmethod
def use_outdated_comments(pull_request):
settings_model = VcsSettingsModel(repo=pull_request.target_repo)
settings = settings_model.get_general_settings()
return settings.get('rhodecode_use_outdated_comments', False)
dan
hooks: added new hooks for comments on pull requests and commits....
r4305 def trigger_commit_comment_hook(self, repo, user, action, data=None):
repo = self._get_repo(repo)
target_scm = repo.scm_instance()
if action == 'create':
trigger_hook = hooks_utils.trigger_comment_commit_hooks
elif action == 'edit':
comments: added new events for comment editing to handle them in integrations.
r4444 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
dan
hooks: added new hooks for comments on pull requests and commits....
r4305 else:
return
log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
repo, action, trigger_hook)
trigger_hook(
username=user.username,
repo_name=repo.repo_name,
repo_type=target_scm.alias,
repo=repo,
data=data)
project: added all source files and assets
r1
def _parse_comment_line_number(line_no):
modernize: python3 updates
r5096 r"""
project: added all source files and assets
r1 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
"""
old_line = None
new_line = None
if line_no.startswith('o'):
old_line = int(line_no[1:])
elif line_no.startswith('n'):
new_line = int(line_no[1:])
else:
raise ValueError("Comment lines have to start with either 'o' or 'n'.")
return diffs.DiffLineNumber(old_line, new_line)
def _diff_to_comment_line_number(diff_line):
if diff_line.new is not None:
modernize: python3 updates
r5096 return f'n{diff_line.new}'
project: added all source files and assets
r1 elif diff_line.old is not None:
modernize: python3 updates
r5096 return f'o{diff_line.old}'
return ''
project: added all source files and assets
r1
def _diff_line_delta(a, b):
if None not in (a.new, b.new):
return abs(a.new - b.new)
elif None not in (a.old, b.old):
return abs(a.old - b.old)
else:
raise ValueError(
modernize: python3 updates
r5096 f"Cannot compute delta between {a} and {b}")