// method.
//
// In this visualization, | is a debounced-function call and X is the actual
// callback execution:
//
// > Debounced with `at_begin` specified as false or unspecified:
// > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
// > X X
// >
// > Debounced with `at_begin` specified as true:
// > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
// > X X
//
// Usage:
//
// > var debounced = jQuery.debounce( delay, [ at_begin, ] callback );
// >
// > jQuery('selector').bind( 'someevent', debounced );
// > jQuery('selector').unbind( 'someevent', debounced );
//
// This also works in jQuery 1.4+:
//
// > jQuery('selector').bind( 'someevent', jQuery.debounce( delay, [ at_begin, ] callback ) );
// > jQuery('selector').unbind( 'someevent', callback );
//
// Arguments:
//
// delay - (Number) A zero-or-greater delay in milliseconds. For event
// callbacks, values around 100 or 250 (or even higher) are most useful.
// at_begin - (Boolean) Optional, defaults to false. If at_begin is false or
// unspecified, callback will only be executed `delay` milliseconds after
// the last debounced-function call. If at_begin is true, callback will be
// executed only at the first debounced-function call. (After the
// throttled-function has not been called for `delay` milliseconds, the
// internal counter is reset)
// callback - (Function) A function to be executed after delay milliseconds.
// The `this` context and all arguments are passed through, as-is, to
// `callback` when the debounced-function is executed.
//
// Returns:
//
// (Function) A new, debounced, function.
$.debounce = function( delay, at_begin, callback ) {
return callback === undefined
? jq_throttle( delay, at_begin, false )
: jq_throttle( delay, callback, at_begin !== false );
};
})(this);
trac-code-comments-plugin-master/code_comments/htdocs/code-comments-list.js 0000644 0001750 0001750 00000001711 12015430206 025623 0 ustar wmb wmb jQuery(document).ready(function($){
$('#send-to-ticket').click(function(e) {
e.preventDefault();
var ids = $('table.code-comments td.check input:checked' ).map(function(i, e) {return e.id.replace('checked-', '')}).get();
if (!ids.length) {
alert("Please select comments to include in the ticket.");
return;
}
window.location = $(this).attr('data-url') + '?ids=' + ids.join(',');
});
$check_all_checkbox = $('th.check input');
$all_checkboxes = $('td.check input')
$check_all_checkbox.click(function(){
$this = $(this);
var checked = $this.attr('checked');
$all_checkboxes.attr('checked', checked);
});
$all_checkboxes.click(function(){
var $this = $(this);
var all_checked = true;
if ( !$this.attr('checked') ) {
all_checked = false;
} else {
$all_checkboxes.each(function(){
if ( !$(this).attr('checked') ) {
all_checked = false;
}
});
}
$check_all_checkbox.attr('checked', all_checked);
});
});
trac-code-comments-plugin-master/code_comments/htdocs/sort/ 0002755 0001750 0001750 00000000000 12015430206 022550 5 ustar wmb wmb trac-code-comments-plugin-master/code_comments/htdocs/sort/sort.css 0000644 0001750 0001750 00000000502 12015430206 024244 0 ustar wmb wmb table.code-comments thead tr .header {
background-image: url(bg.gif);
background-repeat: no-repeat;
background-position: center right;
cursor: pointer;
}
table.code-comments thead tr .headerSortUp {
background-image: url(asc.gif);
}
table.code-comments thead tr .headerSortDown {
background-image: url(desc.gif);
} trac-code-comments-plugin-master/code_comments/htdocs/sort/asc.gif 0000644 0001750 0001750 00000000066 12015430206 024005 0 ustar wmb wmb GIF89a #-0! ,
ڛgk$- ; trac-code-comments-plugin-master/code_comments/htdocs/sort/bg.gif 0000644 0001750 0001750 00000000100 12015430206 023614 0 ustar wmb wmb GIF89a #-0! , bxT2W>e`U ; trac-code-comments-plugin-master/code_comments/htdocs/sort/desc.gif 0000644 0001750 0001750 00000000066 12015430206 024155 0 ustar wmb wmb GIF89a #-0! ,
ɭT2Y ; trac-code-comments-plugin-master/code_comments/web.py 0000644 0001750 0001750 00000026260 12015430206 021430 0 ustar wmb wmb import re
import copy
from trac.core import *
from trac.web.chrome import INavigationContributor, ITemplateProvider, add_script, add_script_data, add_stylesheet, add_notice, add_link
from trac.web.main import IRequestHandler, IRequestFilter
from trac.util import Markup
from trac.util.text import to_unicode
from trac.util.presentation import Paginator
from trac.versioncontrol.api import RepositoryManager
from code_comments.comments import Comments
from code_comments.comment import CommentJSONEncoder, format_to_html
try:
import json
except ImportError:
import simplejson as json
class CodeComments(Component):
implements(ITemplateProvider, IRequestFilter)
href = 'code-comments'
# ITemplateProvider methods
def get_templates_dirs(self):
return [self.get_template_dir()]
def get_template_dir(self):
from pkg_resources import resource_filename
return resource_filename(__name__, 'templates')
def get_htdocs_dirs(self):
from pkg_resources import resource_filename
return [('code-comments', resource_filename(__name__, 'htdocs'))]
# IRequestFilter methods
def pre_process_request(self, req, handler):
return handler
def post_process_request(self, req, template, data, content_type):
add_stylesheet(req, 'code-comments/code-comments.css')
return template, data, content_type
class MainNavigation(CodeComments):
implements(INavigationContributor)
# INavigationContributor methods
def get_active_navigation_item(self, req):
return self.href
def get_navigation_items(self, req):
if 'TRAC_ADMIN' in req.perm:
yield 'mainnav', 'code-comments', Markup('Code Comments' % (
req.href(self.href) ) )
class JSDataForRequests(CodeComments):
implements(IRequestFilter)
js_templates = ['top-comments-block', 'comment', 'add-comment-dialog', 'line-comment', 'comments-for-a-line',]
# IRequestFilter methods
def pre_process_request(self, req, handler):
return handler
def post_process_request(self, req, template, data, content_type):
if data is None:
return
js_data = {
'comments_rest_url': req.href(CommentsREST.href),
'formatting_help_url': req.href.wiki('WikiFormatting'),
'delete_url': req.href(DeleteCommentForm.href),
'preview_url': req.href(WikiPreview.href),
'templates': self.templates_js_data(),
'active_comment_id': req.args.get('codecomment'),
'username': req.authname,
'is_admin': 'TRAC_ADMIN' in req.perm,
}
original_return_value = template, data, content_type
if req.path_info.startswith('/changeset/'):
js_data.update(self.changeset_js_data(req, data))
elif req.path_info.startswith('/browser'):
js_data.update(self.browser_js_data(req, data))
elif re.match(r'/attachment/ticket/\d+/.*', req.path_info):
js_data.update(self.attachment_js_data(req, data))
else:
return original_return_value
add_script(req, 'code-comments/json2.js')
add_script(req, 'code-comments/underscore-min.js')
add_script(req, 'code-comments/backbone-min.js')
# jQuery UI includes: UI Core, Interactions, Button & Dialog Widgets, Core Effects, custom theme
add_script(req, 'code-comments/jquery-ui/jquery-ui.js')
add_stylesheet(req, 'code-comments/jquery-ui/trac-theme.css')
add_script(req, 'code-comments/jquery.ba-throttle-debounce.min.js')
add_script(req, 'code-comments/code-comments.js')
add_script_data(req, {'CodeComments': js_data})
return original_return_value
def templates_js_data(self):
data = {}
for name in self.js_templates:
# we want to use the name as JS identifier and we can't have dashes there
data[name.replace('-', '_')] = self.template_js_data(name)
return data
def changeset_js_data(self, req, data):
return {'page': 'changeset', 'revision': data['new_rev'], 'path': '', 'selectorToInsertBefore': 'div.diff:first'}
def browser_js_data(self, req, data):
return {'page': 'browser', 'revision': data['rev'], 'path': data['path'], 'selectorToInsertBefore': 'table#info'}
def attachment_js_data(self, req, data):
path = req.path_info.replace('/attachment/', 'attachment:/')
return {'page': 'attachment', 'revision': 0, 'path': path, 'selectorToInsertBefore': 'table#info'}
def template_js_data(self, name):
file_name = name + '.html'
return to_unicode(open(self.get_template_dir() + '/js/' + file_name).read())
class ListComments(CodeComments):
implements(IRequestHandler)
COMMENTS_PER_PAGE = 50
# IRequestHandler methods
def match_request(self, req):
return req.path_info == '/' + self.href
def process_request(self, req):
req.perm.require('TRAC_ADMIN')
self.data = {}
self.args = {}
self.req = req
self.per_page = int(req.args.get('per-page', self.COMMENTS_PER_PAGE))
self.page = int(req.args.get('page', 1))
self.order_by = req.args.get('orderby', 'id')
self.order = req.args.get('order', 'DESC')
self.add_path_and_author_filters()
self.comments = Comments(req, self.env);
self.data['comments'] = self.comments.search(self.args, self.order, self.per_page, self.page, self.order_by)
self.data['reponame'], repos, path = RepositoryManager(self.env).get_repository_by_path('/')
self.data['can_delete'] = 'TRAC_ADMIN' in req.perm
self.data['paginator'] = self.get_paginator()
self.data['current_sorting_method'] = self.order_by
self.data['current_order'] = self.order
self.data['sortable_headers'] = []
self.data.update(self.comments.get_filter_values())
self.prepare_sortable_headers()
return 'comments.html', self.data, None
def post_process_request(self, req, template, data, content_type):
add_stylesheet(req, 'code-comments/sort/sort.css')
add_script(req, 'code-comments/code-comments-list.js')
return template, data, content_type
def add_path_and_author_filters(self):
self.data['current_path_selection'] = '';
self.data['current_author_selection'] = '';
if self.req.args.get('filter-by-path'):
self.args['path__prefix'] = self.req.args['filter-by-path'];
self.data['current_path_selection'] = self.req.args['filter-by-path']
if self.req.args.get('filter-by-author'):
self.args['author'] = self.req.args['filter-by-author']
self.data['current_author_selection'] = self.req.args['filter-by-author']
def get_paginator(self):
def href_with_page(page):
args = copy.copy(self.req.args)
args['page'] = page
return self.req.href(self.href, args)
paginator = Paginator(self.data['comments'], self.page-1, self.per_page, Comments(self.req, self.env).count(self.args))
if paginator.has_next_page:
add_link(self.req, 'next', href_with_page(self.page + 1), 'Next Page')
if paginator.has_previous_page:
add_link(self.req, 'prev', href_with_page(self.page - 1), 'Previous Page')
shown_pages = paginator.get_shown_pages(page_index_count = 11)
links = [{'href': href_with_page(page), 'class': None, 'string': str(page), 'title': 'Page %d' % page}
for page in shown_pages]
paginator.shown_pages = links
paginator.current_page = {'href': None, 'class': 'current', 'string': str(paginator.page + 1), 'title': None}
return paginator
def prepare_sortable_headers(self):
displayed_sorting_methods = ('id', 'author', 'time', 'path', 'text')
displayed_sorting_method_names = ('ID', 'Author', 'Date', 'Path', 'Text')
query_args = self.req.args
if ( query_args.has_key('page') ):
del query_args['page']
for sorting_method, sorting_method_name in zip(displayed_sorting_methods, displayed_sorting_method_names):
query_args['orderby'] = sorting_method
html_class = 'header'
if self.order_by == sorting_method:
if 'ASC' == self.order:
query_args['order'] = 'DESC'
html_class += ' headerSortUp'
else:
query_args['order'] = 'ASC'
html_class += ' headerSortDown'
link = self.req.href(self.href, query_args)
self.data['sortable_headers'].append({ 'name': sorting_method_name, 'link': link, 'html_class': html_class })
class DeleteCommentForm(CodeComments):
implements(IRequestHandler)
href = CodeComments.href + '/delete'
# IRequestHandler methods
def match_request(self, req):
return req.path_info == '/' + self.href
def process_request(self, req):
req.perm.require('TRAC_ADMIN')
if 'GET' == req.method:
return self.form(req)
else:
return self.delete(req)
def form(self, req):
data = {}
referrer = req.get_header('Referer')
data['comment'] = Comments(req, self.env).by_id(req.args['id'])
data['return_to'] = referrer
return 'delete.html', data, None
def delete(self, req):
comment = Comments(req, self.env).by_id(req.args['id'])
comment.delete()
add_notice(req, 'Comment deleted.')
req.redirect(req.args['return_to'] or req.href())
class BundleCommentsRedirect(CodeComments):
implements(IRequestHandler)
href = CodeComments.href + '/create-ticket'
# IRequestHandler methods
def match_request(self, req):
return req.path_info == '/' + self.href
def process_request(self, req):
text = ''
for id in req.args['ids'].split(','):
comment = Comments(req, self.env).by_id(id)
text += """
[[CodeCommentLink(%(id)s)]]
%(comment_text)s
""".lstrip() % {'id': id, 'comment_text': comment.text}
req.redirect(req.href.newticket(description=text))
class CommentsREST(CodeComments):
implements(IRequestHandler)
href = CodeComments.href + '/comments'
# IRequestHandler methods
def match_request(self, req):
return req.path_info.startswith('/' + self.href)
def return_json(self, req, data, code=200):
req.send(json.dumps(data, cls=CommentJSONEncoder), 'application/json')
def process_request(self, req):
#TODO: catch errors
if '/' + self.href == req.path_info:
if 'GET' == req.method:
self.return_json(req, Comments(req, self.env).search(req.args))
if 'POST' == req.method:
comments = Comments(req, self.env)
id = comments.create(json.loads(req.read()))
self.return_json(req, comments.by_id(id))
class WikiPreview(CodeComments):
implements(IRequestHandler)
href = CodeComments.href + '/preview'
# IRequestHandler methods
def match_request(self, req):
return req.path_info.startswith('/' + self.href)
def process_request(self, req):
req.send(format_to_html(req, self.env, req.args.get('text', '')).encode('utf-8'))
trac-code-comments-plugin-master/code_comments/ticket_event_listener.py 0000644 0001750 0001750 00000004411 12015430206 025236 0 ustar wmb wmb from trac.core import *
from trac.ticket.api import ITicketChangeListener
import re
from code_comments.comment_macro import CodeCommentLinkMacro
class UpdateTicketCodeComments(Component):
"""Automatically stores relations to CodeComments whenever a ticket is saved or created
Note: This does not catch edits on replies right away but on the next change of the ticket or when adding a new reply
"""
implements(ITicketChangeListener)
def ticket_created(self, ticket):
self.update_relations(ticket)
def ticket_changed(self, ticket, comment, author, old_values):
self.update_relations(ticket)
def ticket_deleted(self, ticket):
self.update_relations(ticket)
def update_relations(self, ticket):
comment_ids = []
# (time, author, field, oldvalue, newvalue, permanent)
changes = ticket.get_changelog()
description = ticket['description']
comment_ids += re.findall(CodeCommentLinkMacro.re, description)
if changes:
for change in changes:
if change[2] == 'comment':
comment_ids += re.findall(CodeCommentLinkMacro.re, change[4])
comment_ids = set(comment_ids)
comment_ids_csv = ','.join(comment_ids)
existing_comments_query = "SELECT * FROM ticket_custom WHERE ticket = %s AND name = 'code_comment_relation'"
existing_comments = self.fetch(existing_comments_query, [ticket.id])
if existing_comments:
self.query("UPDATE ticket_custom SET value=%s WHERE ticket=%s AND name='code_comment_relation'", [comment_ids_csv, ticket.id])
else:
self.query("INSERT INTO ticket_custom (ticket, name, value) VALUES (%s, 'code_comment_relation', %s)", [ticket.id, comment_ids_csv])
def query(self, query, args = [], result_callback=None):
if result_callback is None:
result_callback = lambda db, cursor: True
result = {}
@self.env.with_transaction()
def insert_comment(db):
cursor = db.cursor()
cursor.execute(query, args)
result['result'] = result_callback(db, cursor)
return result['result']
def fetch(self, query, args = []):
return self.query(query, args, lambda db, cursor: cursor.fetchall())
trac-code-comments-plugin-master/code_comments/comment.py 0000644 0001750 0001750 00000013365 12015430206 022317 0 ustar wmb wmb import re
import locale
import trac.wiki.formatter
from trac.mimeview.api import Context
from time import strftime, localtime
from code_comments import db
from trac.util import Markup
try:
import json
except ImportError:
import simplejson as json
try:
import hashlib
md5_hexdigest = lambda s: hashlib.md5(s).hexdigest()
except ImportError:
import md5
md5_hexdigest = lambda s: md5.new(s).hexdigest()
VERSION = 1
class Comment:
columns = [column.name for column in db.schema['code_comments'].columns]
required = 'text', 'author'
_email_map = None
def __init__(self, req, env, data):
if isinstance(data, dict):
self.__dict__ = data
else:
self.__dict__ = dict(zip(self.columns, data))
self.env = env
self.req = req
if self._empty('version'):
self.version = VERSION
self.html = format_to_html(self.req, self.env, self.text)
email = self.email_map().get(self.author, 'baba@baba.net')
self.email_md5 = md5_hexdigest(email)
attachment_info = self.attachment_info()
self.is_comment_to_attachment = attachment_info['is']
self.attachment_ticket = attachment_info['ticket']
self.attachment_filename = attachment_info['filename']
self.is_comment_to_changeset = self.revision and not self.path
self.is_comment_to_file = self.revision and self.path
def _empty(self, column_name):
return not hasattr(self, column_name) or not getattr(self, column_name)
def email_map(self):
if Comment._email_map is None:
Comment._email_map = {}
for username, name, email in self.env.get_known_users():
if email:
Comment._email_map[username] = email
return Comment._email_map
def validate(self):
missing = [column_name for column_name in self.required if self._empty(column_name)]
if missing:
raise ValueError("Comment column(s) missing: %s" % ', '.join(missing))
def href(self):
if self.is_comment_to_file:
href = self.req.href.browser(self.path, rev=self.revision, codecomment=self.id)
elif self.is_comment_to_changeset:
href = self.req.href.changeset(self.revision, codecomment=self.id)
elif self.is_comment_to_attachment:
href = self.req.href('/attachment/ticket/%d/%s' % (self.attachment_ticket, self.attachment_filename), codecomment=self.id)
if self.line:
href += '#L' + str(self.line)
return href
def link_text(self):
if self.revision and not self.path:
return '[%s]' % self.revision
if self.path.startswith('attachment:'):
return self.attachment_link_text()
# except the two specials cases of changesets (revision-only)
# and arrachments (path-only), we must always have them both
assert self.path and self.revision
link_text = self.path + '@' + str(self.revision)
if self.line:
link_text += '#L' + str(self.line)
return link_text
def attachment_link_text(self):
return '#%s: %s' % (self.attachment_ticket, self.attachment_filename)
def trac_link(self):
if self.is_comment_to_attachment:
return '[%s %s]' % (self.req.href())
return 'source:' + self.link_text()
def attachment_info(self):
info = {'is': False, 'ticket': None, 'filename': None}
info['is'] = self.path.startswith('attachment:')
if not info['is']:
return info
match = re.match(r'attachment:/ticket/(\d+)/(.*)', self.path)
if not match:
return info
info['ticket'] = int(match.group(1))
info['filename'] = match.group(2)
return info
def path_link_tag(self):
return Markup('%s' % (self.href(), self.link_text()))
def formatted_date(self):
encoding = locale.getlocale()[1] if locale.getlocale()[1] else 'utf-8'
return strftime('%d %b %Y, %H:%M', localtime(self.time)).decode(encoding)
def get_ticket_relations(self):
relations = set()
db = self.env.get_db_cnx()
cursor = db.cursor()
query = """SELECT ticket FROM ticket_custom WHERE name = 'code_comment_relation' AND
(value LIKE '%(comment_id)d' OR
value LIKE '%(comment_id)d,%%' OR
value LIKE '%%,%(comment_id)d' OR value LIKE '%%,%(comment_id)d,%%')""" % {'comment_id': self.id}
result = {}
@self.env.with_transaction()
def get_ticket_ids(db):
cursor = db.cursor()
cursor.execute(query)
result['tickets'] = cursor.fetchall()
return set([int(row[0]) for row in result['tickets']])
def get_ticket_links(self):
relations = self.get_ticket_relations()
links = ['[[ticket:%s]]' % relation for relation in relations]
return format_to_html(self.req, self.env, ', '.join(links))
def delete(self):
@self.env.with_transaction()
def delete_comment(db):
cursor = db.cursor()
cursor.execute("DELETE FROM code_comments WHERE id=%s", [self.id])
class CommentJSONEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, Comment):
for_json = dict([(name, getattr(o, name)) for name in o.__dict__ if isinstance(getattr(o, name), (basestring, int, list, dict))])
for_json['formatted_date'] = o.formatted_date()
for_json['permalink'] = o.href()
return for_json
else:
return json.JSONEncoder.default(self, o)
def format_to_html(req, env, text):
context = Context.from_request(req)
return trac.wiki.formatter.format_to_html(env, context, text)
trac-code-comments-plugin-master/code_comments/templates/ 0002755 0001750 0001750 00000000000 12015430206 022273 5 ustar wmb wmb trac-code-comments-plugin-master/code_comments/templates/js/ 0002755 0001750 0001750 00000000000 12015430206 022707 5 ustar wmb wmb trac-code-comments-plugin-master/code_comments/templates/js/line-comment.html 0000644 0001750 0001750 00000000570 12015430206 026164 0 ustar wmb wmb
<%= html %>
by <%= author %> @ <%= formatted_date %>
•
∞
<% if (can_delete) { %>
•
Delete
<% } %>
|
trac-code-comments-plugin-master/code_comments/templates/js/top-comments-block.html 0000644 0001750 0001750 00000000111 12015430206 027301 0 ustar wmb wmb Comments