Files @ d5707598fd64
Branch filter:

Location: kallithea/kallithea/model/comment.py

Mads Kiilerich
db: fix unknown exception type in commit error handling

efce61aac33d was a blind fix. It failed because `from sqlalchemy import *`
doesn't import exc and the new except clause would thus fail. It also failed
because the session has to be rolled back after a commit failure.

Now, rework it to fix these issues.

Note that we are able to detect whether the commit failed for valid reasons ...
but we can't use that information to much ...
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# 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 General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
kallithea.model.comment
~~~~~~~~~~~~~~~~~~~~~~~

comments model for Kallithea

This file was forked by the Kallithea project in July 2014.
Original author and date, and relevant copyright and licensing information is below:
:created_on: Nov 11, 2011
:author: marcink
:copyright: (c) 2013 RhodeCode GmbH, and others.
:license: GPLv3, see LICENSE.md for more details.
"""

import logging

from pylons.i18n.translation import _
from collections import defaultdict

from kallithea.lib.utils2 import extract_mentioned_users, safe_unicode
from kallithea.lib import helpers as h
from kallithea.model import BaseModel
from kallithea.model.db import ChangesetComment, User, \
    Notification, PullRequest
from kallithea.model.notification import NotificationModel
from kallithea.model.meta import Session

log = logging.getLogger(__name__)


class ChangesetCommentsModel(BaseModel):

    cls = ChangesetComment

    def __get_changeset_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

    def _get_notification_data(self, repo, comment, user, comment_text,
                               line_no=None, revision=None, pull_request=None,
                               status_change=None, closing_pr=False):
        """
        :returns: tuple (subj,body,recipients,notification_type,email_kwargs)
        """
        # make notification
        body = comment_text  # text of the comment
        line = ''
        if line_no:
            line = _('on line %s') % line_no

        #changeset
        if revision:
            notification_type = Notification.TYPE_CHANGESET_COMMENT
            cs = repo.scm_instance.get_changeset(revision)
            desc = cs.short_id

            threading = ['%s-rev-%s@%s' % (repo.repo_name, revision, h.canonical_hostname())]
            if line_no: # TODO: url to file _and_ line number
                threading.append('%s-rev-%s-line-%s@%s' % (repo.repo_name, revision, line_no,
                                                           h.canonical_hostname()))
            comment_url = h.canonical_url('changeset_home',
                repo_name=repo.repo_name,
                revision=revision,
                anchor='comment-%s' % comment.comment_id)
            subj = safe_unicode(
                h.link_to('Re changeset: %(desc)s %(line)s' % \
                          {'desc': desc, 'line': line},
                          comment_url)
            )
            # get the current participants of this changeset
            recipients = ChangesetComment.get_users(revision=revision)
            # add changeset author if it's known locally
            cs_author = User.get_from_cs_author(cs.author)
            if not cs_author:
                #use repo owner if we cannot extract the author correctly
                cs_author = repo.user
            recipients += [cs_author]
            email_kwargs = {
                'status_change': status_change,
                'cs_comment_user': user.full_name_and_username,
                'cs_target_repo': h.canonical_url('summary_home', repo_name=repo.repo_name),
                'cs_comment_url': comment_url,
                'raw_id': revision,
                'message': cs.message,
                'repo_name': repo.repo_name,
                'short_id': h.short_id(revision),
                'branch': cs.branch,
                'comment_username': user.username,
                'threading': threading,
            }
        #pull request
        elif pull_request:
            notification_type = Notification.TYPE_PULL_REQUEST_COMMENT
            desc = comment.pull_request.title
            _org_ref_type, org_ref_name, _org_rev = comment.pull_request.org_ref.split(':')
            threading = ['%s-pr-%s@%s' % (pull_request.other_repo.repo_name,
                                          pull_request.pull_request_id,
                                          h.canonical_hostname())]
            if line_no: # TODO: url to file _and_ line number
                threading.append('%s-pr-%s-line-%s@%s' % (pull_request.other_repo.repo_name,
                                                          pull_request.pull_request_id, line_no,
                                                          h.canonical_hostname()))
            comment_url = pull_request.url(canonical=True,
                anchor='comment-%s' % comment.comment_id)
            subj = safe_unicode(
                h.link_to('Re pull request %(pr_nice_id)s: %(desc)s %(line)s' % \
                          {'desc': desc,
                           'pr_nice_id': comment.pull_request.nice_id(),
                           'line': line},
                          comment_url)
            )
            # get the current participants of this pull request
            recipients = ChangesetComment.get_users(pull_request_id=
                                                pull_request.pull_request_id)
            # add pull request author
            recipients += [pull_request.owner]

            # add the reviewers to notification
            recipients += [x.user for x in pull_request.reviewers]

            #set some variables for email notification
            email_kwargs = {
                'pr_title': pull_request.title,
                'pr_nice_id': pull_request.nice_id(),
                'status_change': status_change,
                'closing_pr': closing_pr,
                'pr_comment_url': comment_url,
                'pr_comment_user': user.full_name_and_username,
                'pr_target_repo': h.canonical_url('summary_home',
                                   repo_name=pull_request.other_repo.repo_name),
                'repo_name': pull_request.other_repo.repo_name,
                'ref': org_ref_name,
                'comment_username': user.username,
                'threading': threading,
            }

        return subj, body, recipients, notification_type, email_kwargs

    def create(self, text, repo, user, revision=None, pull_request=None,
               f_path=None, line_no=None, status_change=None, closing_pr=False,
               send_email=True):
        """
        Creates a new comment for either a changeset or a pull request.
        status_change and closing_pr is only for the optional email.

        Returns the created comment.
        """
        if not status_change and not text:
            log.warning('Missing text for comment, skipping...')
            return None

        repo = self._get_repo(repo)
        user = self._get_user(user)
        comment = ChangesetComment()
        comment.repo = repo
        comment.author = user
        comment.text = text
        comment.f_path = f_path
        comment.line_no = line_no

        if revision is not None:
            comment.revision = revision
        elif pull_request is not None:
            pull_request = self.__get_pull_request(pull_request)
            comment.pull_request = pull_request
        else:
            raise Exception('Please specify revision or pull_request_id')

        Session().add(comment)
        Session().flush()

        if send_email:
            (subj, body, recipients, notification_type,
             email_kwargs) = self._get_notification_data(
                                repo, comment, user,
                                comment_text=text,
                                line_no=line_no,
                                revision=revision,
                                pull_request=pull_request,
                                status_change=status_change,
                                closing_pr=closing_pr)
            email_kwargs['is_mention'] = False
            # create notification objects, and emails
            NotificationModel().create(
                created_by=user, subject=subj, body=body,
                recipients=recipients, type_=notification_type,
                email_kwargs=email_kwargs,
            )

            mention_recipients = set(self._extract_mentions(body))\
                                    .difference(recipients)
            if mention_recipients:
                email_kwargs['is_mention'] = True
                subj = _('[Mention]') + ' ' + subj
                NotificationModel().create(
                    created_by=user, subject=subj, body=body,
                    recipients=mention_recipients,
                    type_=notification_type,
                    email_kwargs=email_kwargs
                )

        return comment

    def delete(self, comment):
        comment = self.__get_changeset_comment(comment)
        Session().delete(comment)

        return comment

    def get_comments(self, repo_id, revision=None, pull_request=None):
        """
        Gets general comments for either revision or pull_request.

        Returns a list, ordered by creation date.
        """
        return self._get_comments(repo_id, revision=revision, pull_request=pull_request,
                                  inline=False)

    def get_inline_comments(self, repo_id, revision=None, pull_request=None):
        """
        Gets inline comments for either revision or pull_request.

        Returns a list of tuples with file path and list of comments per line number.
        """
        comments = self._get_comments(repo_id, revision=revision, pull_request=pull_request,
                                      inline=True)

        paths = defaultdict(lambda: defaultdict(list))
        for co in comments:
            paths[co.f_path][co.line_no].append(co)
        return paths.items()

    def _get_comments(self, repo_id, revision=None, pull_request=None, inline=False):
        """
        Gets comments for either revision or pull_request_id, either inline or general.
        """
        q = Session().query(ChangesetComment)

        if inline:
            q = q.filter(ChangesetComment.line_no != None)\
                .filter(ChangesetComment.f_path != None)
        else:
            q = q.filter(ChangesetComment.line_no == None)\
                .filter(ChangesetComment.f_path == None)

        if revision:
            q = q.filter(ChangesetComment.revision == revision)\
                .filter(ChangesetComment.repo_id == repo_id)
        elif pull_request:
            pull_request = self.__get_pull_request(pull_request)
            q = q.filter(ChangesetComment.pull_request == pull_request)
        else:
            raise Exception('Please specify either revision or pull_request')

        return q.order_by(ChangesetComment.created_on).all()