@@ -343,12 +343,20 @@ def make_map(config):
conditions=dict(function=check_group))
rmap.connect('changeset_home', '/{repo_name:.*}/changeset/{revision}',
controller='changeset', revision='tip',
conditions=dict(function=check_repo))
rmap.connect('changeset_comment', '/{repo_name:.*}/changeset/{revision}/comment',
controller='changeset', revision='tip', action='comment',
rmap.connect('changeset_comment_delete', '/{repo_name:.*}/changeset/comment/{comment_id}/delete',
controller='changeset', action='delete_comment',
rmap.connect('raw_changeset_home',
'/{repo_name:.*}/raw-changeset/{revision}',
controller='changeset', action='raw_changeset',
revision='tip', conditions=dict(function=check_repo))
rmap.connect('summary_home', '/{repo_name:.*}/summary',
@@ -26,21 +26,25 @@
import logging
import traceback
from pylons import tmpl_context as c, url, request, response
from pylons.i18n.translation import _
from pylons.controllers.util import redirect
from pylons.decorators import jsonify
import rhodecode.lib.helpers as h
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
NotAnonymous
from rhodecode.lib.base import BaseRepoController, render
from rhodecode.lib.utils import EmptyChangeset
from rhodecode.lib.compat import OrderedDict
from rhodecode.model.db import ChangesetComment
from rhodecode.model.comment import ChangesetCommentsModel
from vcs.exceptions import RepositoryError, ChangesetError, \
ChangesetDoesNotExistError
from vcs.nodes import FileNode
from vcs.utils import diffs as differ
log = logging.getLogger(__name__)
@@ -63,13 +67,13 @@ class ChangesetController(BaseRepoContro
<td class="code"><pre>%s</pre></td>
</tr>
</table>''' % str
#get ranges of revisions if preset
rev_range = revision.split('...')[:2]
try:
if len(rev_range) == 2:
rev_start = rev_range[0]
rev_end = rev_range[1]
rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
end=rev_end)
@@ -89,12 +93,20 @@ class ChangesetController(BaseRepoContro
c.sum_added = 0
c.sum_removed = 0
c.lines_added = 0
c.lines_deleted = 0
c.cut_off = False # defines if cut off limit is reached
c.comments = []
for cs in c.cs_ranges:
c.comments.extend(ChangesetComment.query()\
.filter(ChangesetComment.repo_id == c.rhodecode_db_repo.repo_id)\
.filter(ChangesetComment.commit_id == cs.raw_id)\
.filter(ChangesetComment.line_no == None)\
.filter(ChangesetComment.f_path == None).all())
# Iterate over ranges (default changeset view is always one changeset)
for changeset in c.cs_ranges:
c.changes[changeset.raw_id] = []
changeset_parent = changeset.parents[0]
except IndexError:
@@ -249,6 +261,25 @@ class ChangesetController(BaseRepoContro
c.diffs = ''
for x in c.changes:
c.diffs += x[2]
return render('changeset/raw_changeset.html')
def comment(self, repo_name, revision):
ccmodel = ChangesetCommentsModel()
ccmodel.create(text=request.POST.get('text'),
repo_id=c.rhodecode_db_repo.repo_id,
user_id=c.rhodecode_user.user_id,
commit_id=revision, f_path=request.POST.get('f_path'),
line_no = request.POST.get('line'))
return redirect(h.url('changeset_home', repo_name=repo_name,
revision=revision))
@jsonify
@HasRepoPermissionAnyDecorator('hg.admin', 'repository.admin')
def delete_comment(self, comment_id):
ccmodel.delete(comment_id=comment_id)
return True
@@ -36,12 +36,14 @@ from webhelpers.html.tags import _set_in
convert_boolean_attrs, NotGiven
from vcs.utils.annotate import annotate_highlight
from rhodecode.lib.utils import repo_name_slug
from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
from rhodecode.lib.markup_renderer import MarkupRenderer
def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
"""
Reset button
_set_input_attrs(attrs, type, name, value)
_set_id_attr(attrs, id, name)
@@ -528,13 +530,13 @@ class RepoPage(Page):
# items_per_page if the last page is not full
self.first_item = max(0, (self.item_count) - (self.page *
items_per_page))
self.last_item = ((self.item_count - 1) - items_per_page *
(self.page - 1))
self.items = list(self.collection[self.first_item:self.last_item+1])
self.items = list(self.collection[self.first_item:self.last_item + 1])
# Links to previous and next page
if self.page > self.first_page:
self.previous_page = self.page - 1
else:
@@ -665,6 +667,9 @@ def urlify_text(text):
def url_func(match_obj):
url_full = match_obj.groups()[0]
return '<a href="%(url)s">%(url)s</a>' % ({'url':url_full})
return literal(url_pat.sub(url_func, text))
def rst(source):
return literal('<div class="rst-block">%s</div>' % MarkupRenderer.rst(source))
new file 100644
# -*- coding: utf-8 -*-
rhodecode.model.comment
~~~~~~~~~~~~~~~~~~~~~~~
comments model for RhodeCode
:created_on: Nov 11, 2011
:author: marcink
:copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
:license: GPLv3, see COPYING for more details.
# 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/>.
from rhodecode.model import BaseModel
class ChangesetCommentsModel(BaseModel):
def create(self, text, repo_id, user_id, commit_id, f_path=None,
line_no=None):
Creates new comment for changeset
:param text:
:param repo_id:
:param user_id:
:param commit_id:
:param f_path:
:param line_no:
comment = ChangesetComment()
comment.repo_id = repo_id
comment.user_id = user_id
comment.commit_id = commit_id
comment.text = text
comment.f_path = f_path
comment.line_no = line_no
self.sa.add(comment)
self.sa.commit()
return comment
def delete(self, comment_id):
Deletes given comment
:param comment_id:
comment = ChangesetComment.get(comment_id)
self.sa.delete(comment)
@@ -1092,12 +1092,28 @@ class CacheInvalidation(Base, BaseModel)
.filter(CacheInvalidation.cache_key == key).scalar()
inv_obj.cache_active = True
Session.add(inv_obj)
Session.commit()
class ChangesetComment(Base, BaseModel):
__tablename__ = 'changeset_comments'
__table_args__ = ({'extend_existing':True},)
comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
commit_id = Column('commit_id', String(100), nullable=False)
line_no = Column('line_no', Integer(), nullable=True)
f_path = Column('f_path', String(1000), nullable=True)
user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
text = Column('text', String(25000), nullable=False)
modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
author = relationship('User')
repo = relationship('Repository')
class DbMigrateVersion(Base, BaseModel):
__tablename__ = 'db_migrate_version'
__table_args__ = {'extend_existing':True}
repository_id = Column('repository_id', String(250), primary_key=True)
repository_path = Column('repository_path', Text)
version = Column('version', Integer)
@@ -5,12 +5,17 @@ div.diffblock {
background: #f8f8f8;
font-size: 100%;
line-height: 100%;
/* new */
line-height: 125%;
}
div.diffblock.margined{
margin: 0px 20px 0px 20px;
div.diffblock .code-header{
border-bottom: 1px solid #CCCCCC;
background: #EEEEEE;
padding:10px 0 10px 0;
div.diffblock .code-header div{
@@ -3132,6 +3132,204 @@ div.readme .readme_box pre {
overflow: auto;
-webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
/** RST STYLE **/
div.rst-block {
padding:0px;
div.rst-block h2 {
font-weight: normal;
background-color: #fafafa;
clear:both;
overflow:hidden;
margin:0;
padding:0 20px 10px;
div.rst-block h1, div.rst-block h2, div.rst-block h3, div.rst-block h4, div.rst-block h5, div.rst-block h6 {
border-bottom: 0 !important;
margin: 0 !important;
padding: 0 !important;
line-height: 1.5em !important;
div.rst-block h1:first-child {
padding-top: .25em !important;
div.rst-block h2, div.rst-block h3 {
margin: 1em 0 !important;
margin-top: 1.5em !important;
border-top: 4px solid #e0e0e0 !important;
padding-top: .5em !important;
div.rst-block p {
color: black !important;
div.rst-block ul {
list-style: disc !important;
margin: 1em 0 1em 2em !important;
div.rst-block ol {
list-style: decimal;
div.rst-block pre, code {
font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
div.rst-block code {
font-size: 12px !important;
background-color: ghostWhite !important;
color: #444 !important;
padding: 0 .2em !important;
border: 1px solid #dedede !important;
div.rst-block pre code {
background-color: #eee !important;
border: none !important;
div.rst-block pre {
margin: 1em 0;
font-size: 12px;
background-color: #eee;
border: 1px solid #ddd;
padding: 5px;
color: #444;
.comments {
padding:10px 20px;
.comments .comment {
margin-top: 10px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
.comments .comment .meta {
padding: 6px;
border-bottom: 1px solid #ddd;
.comments .comment .meta img {
vertical-align: middle;
.comments .comment .meta .user {
font-weight: bold;
.comments .comment .meta .date {
float: right;
.comments .comment .text {
margin-top: 7px;
padding-bottom: 13px;
.comments .comments-number{
padding:0px 0px 10px 0px;
.comment-form .clearfix{
background: #EEE;
padding: 10px;
div.comment-form {
margin-top: 20px;
.comment-form strong {
display: block;
margin-bottom: 15px;
.comment-form textarea {
width: 100%;
height: 100px;
font-family: 'Monaco', 'Courier', 'Courier New', monospace;
form.comment-form {
margin-left: 10px;
.comment-form-submit {
margin-top: 5px;
margin-left: 525px;
.file-comments {
display: none;
.comment-form .comment {
.comment-form .comment-help{
padding: 0px 0px 5px 0px;
color: #666;
.comment-form .comment-button{
padding-top:5px;
.add-another-button {
margin-bottom: 10px;
.comment .buttons {
@@ -105,13 +105,13 @@
</div>
%for change,filenode,diff,cs1,cs2,stat in c.changes:
%if change !='removed':
<div style="clear:both;height:10px"></div>
<div class="diffblock">
<div class="diffblock margined">
<div id="${h.repo_name_slug('C%s' % h.safe_unicode(filenode.path))}" class="code-header">
<div class="changeset_header">
<span class="changeset_file">
${h.link_to_if(change!='removed',h.safe_unicode(filenode.path),h.url('files_home',repo_name=c.repo_name,
revision=filenode.changeset.raw_id,f_path=h.safe_unicode(filenode.path)))}
</span>
@@ -131,9 +131,33 @@
%else:
${_('No changes in this file')}
%endif
%endfor
<%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
<div class="comments">
<div class="comments-number">${len(c.comments)} comment(s)</div>
%for co in c.comments:
${comment.comment_block(co)}
%if c.rhodecode_user.username != 'default':
<div class="comment-form">
${h.form(h.url('changeset_comment', repo_name=c.repo_name, revision=c.changeset.raw_id))}
<strong>Leave a comment</strong>
<div class="clearfix">
<div class="comment-help">${_('Comments parsed using RST syntax')}</div>
${h.textarea('text')}
<div class="comment-button">
${h.submit('save', _('Comment'), class_='ui-button')}
${h.end_form()}
</%def>
##usage:
## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
## ${comment.comment_block(co)}
##
<%def name="comment_block(co)">
<div class="comment" id="comment-${co.comment_id}">
<div class="meta">
<span class="user">
<img src="${h.gravatar_url(co.author.email, 20)}" />
${co.author.username}
<a href="${h.url.current(anchor='comment-%s' % co.comment_id)}"> ${_('commented on')} </a>
${h.short_id(co.commit_id)}
%if co.f_path:
${_(' in file ')}
${co.f_path}:L${co.line_no}
<span class="date">
${h.age(co.modified_at)}
<div class="text">
%if h.HasPermissionAny('hg.admin', 'repository.admin')() or co.author.user_id == c.rhodecode_user.user_id:
<div class="buttons">
<a href="javascript:void(0);" onClick="deleteComment(${co.comment_id})" class="">${_('Delete')}</a>
${h.rst(co.text)|n}
\ No newline at end of file
Status change: