You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bloodhound.apache.org by gj...@apache.org on 2012/10/16 22:06:19 UTC
svn commit: r1398968 [24/28] - in /incubator/bloodhound/trunk/trac: ./
contrib/ doc/ doc/api/ doc/utils/ sample-plugins/
sample-plugins/permissions/ sample-plugins/workflow/ trac/ trac/admin/
trac/admin/templates/ trac/admin/tests/ trac/db/ trac/db/tes...
Modified: incubator/bloodhound/trunk/trac/trac/ticket/templates/ticket_change.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/ticket/templates/ticket_change.html?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/ticket/templates/ticket_change.html (original)
+++ incubator/bloodhound/trunk/trac/trac/ticket/templates/ticket_change.html Tue Oct 16 20:06:09 2012
@@ -62,21 +62,21 @@ Arguments:
<form py:if="'cnum' in change and can_edit_comment" method="get" action="#comment:${cnum}">
<div class="inlinebuttons">
<input type="hidden" name="cnum_edit" value="${cnum}"/>
- <input type="submit" value="${_('Edit')}" title="${_('Edit comment %(cnum)s', cnum=cnum)}"/>
+ <input type="submit" value="${captioned_button('â', _('Edit'))}" title="${_('Edit comment %(cnum)s', cnum=cnum)}" />
</div>
</form>
<form py:if="'cnum' in change and can_append" id="reply-to-comment-${cnum}"
method="get" action="#comment">
<div class="inlinebuttons">
<input type="hidden" name="replyto" value="${cnum}"/>
- <input type="submit" value="${_('Reply')}" title="${_('Reply to comment %(cnum)s', cnum=cnum)}"/>
+ <input type="submit" value="${captioned_button('â³', _('Reply'))}" title="${_('Reply to comment %(cnum)s', cnum=cnum)}" />
</div>
</form>
</div>
<ul py:if="change.fields" class="changes">
<li py:for="field_name, field in sorted(change.fields.iteritems(), key=lambda item: item[1].label.lower())"
- class="${'trac-conflict' if preview and field_name in conflicts else None}">
- <strong>${field.label}</strong>
+ class="trac-field-${field_name}${' trac-conflict' if preview and field_name in conflicts else None}">
+ <strong class="trac-field-${field_name}">${field.label}</strong>
<py:choose>
<py:when test="field_name == 'attachment'"><i18n:msg params="name">
<a href="${href.attachment('ticket', ticket.id, field.new)}"><em>${field.new
@@ -94,6 +94,12 @@ Arguments:
<py:otherwise><i18n:msg params="value">
<em>${field.old}</em> deleted
</i18n:msg></py:otherwise>
+ <py:if test="preview and field.by == 'user'">
+ (<button py:with="old = ticket.get_default(field_name) if field.old is empty else field.old"
+ type="submit" name="revert_$field_name" class="trac-revert"
+ title="Revert this change">revert<div id="revert-$field_name">${
+ '0' if 'cc_update' in field else old}</div></button>)
+ </py:if>
</py:choose>
</li>
</ul>
Modified: incubator/bloodhound/trunk/trac/trac/ticket/tests/functional.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/ticket/tests/functional.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/ticket/tests/functional.py (original)
+++ incubator/bloodhound/trunk/trac/trac/ticket/tests/functional.py Tue Oct 16 20:06:09 2012
@@ -1,16 +1,11 @@
#!/usr/bin/python
+# -*- coding: utf-8 -*-
import os
import re
from datetime import datetime, timedelta
-try:
- import babel
- locale_en = babel.Locale('en_US')
-except ImportError:
- babel = None
- locale_en = None
-
+from trac.test import locale_en
from trac.tests.functional import *
from trac.util.datefmt import utc, localtz, format_date, format_datetime
@@ -200,6 +195,7 @@ class TestTicketQueryLinks(FunctionalTwi
tc.formvalue('query', '0_summary', 'TestTicketQueryLinks')
tc.submit('update')
query_url = b.get_url()
+ tc.find(r'\(%d matches\)' % count)
for i in range(count):
tc.find('TestTicketQueryLinks%s' % i)
@@ -1576,7 +1572,8 @@ class RegressionTestTicket8247(Functiona
tc.formvalue('milestone_table', 'sel', name)
tc.submit('remove')
tc.go(ticket_url)
- tc.find('<strong>Milestone</strong>[ \n\t]*<em>%s</em> deleted' % name)
+ tc.find('<strong class="trac-field-milestone">Milestone</strong>'
+ '[ \n\t]*<em>%s</em> deleted' % name)
tc.find('Changed <a.* ago</a> by admin')
tc.notfind('anonymous')
@@ -1607,7 +1604,7 @@ class RegressionTestTicket9084(Functiona
ticketid = self._tester.create_ticket()
self._tester.add_comment(ticketid)
self._tester.go_to_ticket(ticketid)
- tc.submit('Reply', formname='reply-to-comment-1')
+ tc.submit('2', formname='reply-to-comment-1') # '1' hidden, '2' submit
tc.formvalue('propertyform', 'comment', random_sentence(3))
tc.submit('Submit changes')
tc.notfind('AssertionError')
Modified: incubator/bloodhound/trunk/trac/trac/ticket/tests/model.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/ticket/tests/model.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/ticket/tests/model.py (original)
+++ incubator/bloodhound/trunk/trac/trac/ticket/tests/model.py Tue Oct 16 20:06:09 2012
@@ -154,6 +154,22 @@ class TicketTestCase(unittest.TestCase):
for change in ticket.get_changelog():
self.assertEqual(None, change[1])
+ def test_comment_with_whitespace_only_is_not_saved(self):
+ ticket = Ticket(self.env)
+ ticket.insert()
+
+ ticket.save_changes(comment='\n \n ')
+ self.assertEqual(0, len(ticket.get_changelog()))
+
+ def test_prop_whitespace_change_is_not_saved(self):
+ ticket = Ticket(self.env)
+ ticket.populate({'summary': 'ticket summary'})
+ ticket.insert()
+
+ ticket['summary'] = ' ticket summary '
+ ticket.save_changes()
+ self.assertEqual(0, len(ticket.get_changelog()))
+
def test_ticket_default_values(self):
"""
Verify that a ticket uses default values specified in the configuration
Modified: incubator/bloodhound/trunk/trac/trac/ticket/tests/query.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/ticket/tests/query.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/ticket/tests/query.py (original)
+++ incubator/bloodhound/trunk/trac/trac/ticket/tests/query.py Tue Oct 16 20:06:09 2012
@@ -1,15 +1,10 @@
-from trac.test import Mock, EnvironmentStub, MockPerm
+from trac.test import Mock, EnvironmentStub, MockPerm, locale_en
from trac.ticket.query import Query, QueryModule, TicketQueryMacro
from trac.util.datefmt import utc
from trac.web.chrome import web_context
from trac.web.href import Href
from trac.wiki.formatter import LinkFormatter
-try:
- from babel import Locale
-except ImportError:
- Locale = None
-
import unittest
import difflib
@@ -37,9 +32,8 @@ class QueryTestCase(unittest.TestCase):
def setUp(self):
self.env = EnvironmentStub(default_data=True)
- locale = Locale.parse('en_US') if Locale else None
self.req = Mock(href=self.env.href, authname='anonymous', tz=utc,
- locale=locale, lc_time=locale)
+ locale=locale_en, lc_time=locale_en)
def tearDown(self):
self.env.reset_db()
Modified: incubator/bloodhound/trunk/trac/trac/ticket/tests/report.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/ticket/tests/report.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/ticket/tests/report.py (original)
+++ incubator/bloodhound/trunk/trac/trac/ticket/tests/report.py Tue Oct 16 20:06:09 2012
@@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
+import doctest
+
from trac.db.mysql_backend import MySQLConnection
from trac.ticket.report import ReportModule
from trac.test import EnvironmentStub, Mock
from trac.web.api import Request, RequestDone
+import trac
import unittest
from StringIO import StringIO
@@ -98,7 +101,10 @@ class ReportTestCase(unittest.TestCase):
def suite():
- return unittest.makeSuite(ReportTestCase, 'test')
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocTestSuite(trac.ticket.report))
+ suite.addTest(unittest.makeSuite(ReportTestCase, 'test'))
+ return suite
if __name__ == '__main__':
- unittest.main()
+ unittest.main(defaultTest='suite')
Modified: incubator/bloodhound/trunk/trac/trac/ticket/tests/wikisyntax.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/ticket/tests/wikisyntax.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/ticket/tests/wikisyntax.py (original)
+++ incubator/bloodhound/trunk/trac/trac/ticket/tests/wikisyntax.py Tue Oct 16 20:06:09 2012
@@ -32,8 +32,8 @@ ticket:12-14,33
ticket:12,33?order=created
------------------------------
<p>
-<a href="/query?id=12-14%2C33" title="Tickets 12-14,33">ticket:12-14,33</a>
-<a href="/query?id=12%2C33&order=created" title="Tickets 12,33">ticket:12,33?order=created</a>
+<a href="/query?id=12-14%2C33" title="Tickets 12-14, 33">ticket:12-14,33</a>
+<a href="/query?id=12%2C33&order=created" title="Tickets 12, 33">ticket:12,33?order=created</a>
</p>
------------------------------
============================== ticket link shorthand form
@@ -50,8 +50,8 @@ ticket:12,33?order=created
#1,3,5,7
------------------------------
<p>
-<a href="/query?id=1-5%2C42" title="Tickets 1-5,42">#1-5,42</a>
-<a href="/query?id=1%2C3%2C5%2C7" title="Tickets 1,3,5,7">#1,3,5,7</a>
+<a href="/query?id=1-5%2C42" title="Tickets 1-5, 42">#1-5,42</a>
+<a href="/query?id=1%2C3%2C5%2C7" title="Tickets 1, 3, 5, 7">#1,3,5,7</a>
</p>
------------------------------
============================== ticket link shorthand form with long ranges (#10111 regression)
@@ -91,6 +91,21 @@ trac:#2041
<a class="ext-link" href="http://trac.edgewall.org/intertrac/%232041" title="#2041 in Trac's Trac"><span class="icon"></span>trac:#2041</a>
</p>
------------------------------
+============================== ticket syntax with unicode digits
+#â´Â²
+#1-âµ,42
+#1,³,5,7
+#T²â°â´Â¹
+#trac²â°â´Â¹
+------------------------------
+<p>
+#â´Â²
+<a class="new ticket" href="/ticket/1" title="This is the summary (new)">#1</a>-âµ,42
+<a class="new ticket" href="/ticket/1" title="This is the summary (new)">#1</a>,³,5,7
+#T²â°â´Â¹
+#trac²â°â´Â¹
+</p>
+------------------------------
""" # "
def ticket_setup(tc):
@@ -146,6 +161,17 @@ trac:report:1
<a class="ext-link" href="http://trac.edgewall.org/intertrac/report%3A1" title="report:1 in Trac's Trac"><span class="icon"></span>{trac 1}</a>
</p>
------------------------------
+============================== report syntax with unicode digits
+{â´Â²} !{â´Â²}
+{Tâ´Â²}
+{tracâ´Â²}
+------------------------------
+<p>
+{â´Â²} !{â´Â²}
+{Tâ´Â²}
+{tracâ´Â²}
+</p>
+------------------------------
""" # '
def report_setup(tc):
Modified: incubator/bloodhound/trunk/trac/trac/ticket/web_ui.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/ticket/web_ui.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/ticket/web_ui.py (original)
+++ incubator/bloodhound/trunk/trac/trac/ticket/web_ui.py Tue Oct 16 20:06:09 2012
@@ -29,27 +29,34 @@ from trac.attachment import AttachmentMo
from trac.config import BoolOption, Option, IntOption
from trac.core import *
from trac.mimeview.api import Mimeview, IContentConverter
-from trac.resource import Resource, ResourceNotFound, get_resource_url, \
- render_resource_link, get_resource_shortname
+from trac.resource import (
+ Resource, ResourceNotFound, get_resource_url, render_resource_link,
+ get_resource_shortname
+)
from trac.search import ISearchSource, search_to_sql, shorten_result
from trac.ticket.api import TicketSystem, ITicketManipulator
from trac.ticket.model import Milestone, Ticket, group_milestones
from trac.ticket.notification import TicketNotifyEmail
from trac.timeline.api import ITimelineEventProvider
from trac.util import as_bool, as_int, get_reporter_id
-from trac.util.datefmt import format_datetime, from_utimestamp, \
- to_utimestamp, utc
-from trac.util.text import exception_to_unicode, obfuscate_email_address, \
- shorten_line, to_unicode
+from trac.util.datefmt import (
+ format_datetime, from_utimestamp, to_utimestamp, utc
+)
+from trac.util.text import (
+ exception_to_unicode, empty, obfuscate_email_address, shorten_line,
+ to_unicode
+)
from trac.util.presentation import separated
from trac.util.translation import _, tag_, tagn_, N_, gettext, ngettext
from trac.versioncontrol.diff import get_diff_options, diff_blocks
-from trac.web import arg_list_to_args, parse_arg_list, IRequestHandler, \
- RequestDone
-from trac.web.chrome import (Chrome, INavigationContributor, ITemplateProvider,
- add_ctxtnav, add_link, add_notice, add_script,
- add_script_data, add_stylesheet, add_warning,
- auth_link, prevnext_nav, web_context)
+from trac.web import (
+ IRequestHandler, RequestDone, arg_list_to_args, parse_arg_list
+)
+from trac.web.chrome import (
+ Chrome, INavigationContributor, ITemplateProvider,
+ add_ctxtnav, add_link, add_notice, add_script, add_script_data,
+ add_stylesheet, add_warning, auth_link, prevnext_nav, web_context
+)
from trac.wiki.formatter import format_to, format_to_html, format_to_oneliner
@@ -106,7 +113,7 @@ class TicketModule(Component):
"""Delegate access to ticket default Options which were move to
TicketSystem.
- .. todo:: remove in 0.13
+ .. todo:: remove in 1.0
"""
if name.startswith('default_'):
if name not in self._warn_for_default_attr:
@@ -240,10 +247,10 @@ class TicketModule(Component):
ts_start = to_utimestamp(start)
ts_stop = to_utimestamp(stop)
- status_map = {'new': ('newticket', N_("created")),
- 'reopened': ('reopenedticket', N_("reopened")),
- 'closed': ('closedticket', N_("closed")),
- 'edit': ('editedticket', N_("updated"))}
+ status_map = {'new': ('newticket', 'created'),
+ 'reopened': ('reopenedticket', 'reopened'),
+ 'closed': ('closedticket', 'closed'),
+ 'edit': ('editedticket', 'updated')}
ticket_realm = Resource('ticket')
@@ -381,9 +388,15 @@ class TicketModule(Component):
elif field == 'title':
title = TicketSystem(self.env).format_summary(summary, status,
resolution, type)
- return tag_("Ticket %(ticketref)s (%(summary)s) %(verb)s",
+ message = {
+ 'created': N_("Ticket %(ticketref)s (%(summary)s) created"),
+ 'reopened': N_("Ticket %(ticketref)s (%(summary)s) reopened"),
+ 'closed': N_("Ticket %(ticketref)s (%(summary)s) closed"),
+ 'updated': N_("Ticket %(ticketref)s (%(summary)s) updated"),
+ }[verb]
+ return tag_(message,
ticketref=tag.em('#', ticket.id, title=title),
- summary=shorten_line(summary), verb=gettext(verb))
+ summary=shorten_line(summary))
elif field == 'description':
descr = message = ''
if status == 'new':
@@ -405,9 +418,9 @@ class TicketModule(Component):
description, comment, cid = event[3]
tickets = sorted(tickets)
if field == 'url':
- return context.href.query(id=','.join([str(t) for t in tickets]))
+ return context.href.query(id=','.join(str(t) for t in tickets))
elif field == 'title':
- ticketids = ','.join([str(t) for t in tickets])
+ ticketids = u',\u200b'.join(str(t) for t in tickets)
title = _("Tickets %(ticketids)s", ticketids=ticketids)
return tag_("Tickets %(ticketlist)s batch updated",
ticketlist=tag.em('#', ticketids, title=title))
@@ -469,11 +482,6 @@ class TicketModule(Component):
fields = self._prepare_fields(req, ticket)
- # setup default values for the new ticket
-
- for field in fields:
- ticket.values.setdefault(field['name'], field.get('value'))
-
# position 'owner' immediately before 'cc',
# if not already positioned after (?)
@@ -490,6 +498,8 @@ class TicketModule(Component):
del ticket.fields[curr_idx]
data['fields'] = fields
+ data['fields_map'] = dict((field['name'], i)
+ for i, field in enumerate(fields))
if req.get_header('X-Requested-With') == 'XMLHttpRequest':
data['preview_mode'] = True
@@ -544,7 +554,7 @@ class TicketModule(Component):
'reassign_owner': req.authname,
'resolve_resolution': None,
'start_time': ticket['changetime']})
- elif req.method == 'POST': # 'Preview' or 'Submit'
+ elif req.method == 'POST':
if 'cancel_comment' in req.args:
req.redirect(req.href.ticket(ticket.id))
elif 'edit_comment' in req.args:
@@ -598,7 +608,7 @@ class TicketModule(Component):
# validates and there were no problems with the workflow side of
# things.
valid = self._validate_ticket(req, ticket, not valid) and valid
- if 'preview' not in req.args:
+ if 'submit' in req.args:
if valid:
# redirected if successful
self._do_save(req, ticket, action)
@@ -709,13 +719,16 @@ class TicketModule(Component):
return {'comments_order': req.session.get('ticket_comments_order',
'oldest'),
'comments_only': req.session.get('ticket_comments_only',
- False)}
+ 'false')}
def _prepare_data(self, req, ticket, absurls=False):
return {'ticket': ticket, 'to_utimestamp': to_utimestamp,
- 'context': web_context(req, ticket.resource,
- absurls=absurls),
- 'preserve_newlines': self.must_preserve_newlines}
+ 'context': web_context(req, ticket.resource, absurls=absurls),
+ 'preserve_newlines': self.must_preserve_newlines,
+ 'emtpy': empty}
+
+ def _cc_list(self, cc):
+ return Chrome(self.env).cc_list(cc)
def _toggle_cc(self, req, cc):
"""Return an (action, recipient) tuple corresponding to a change
@@ -735,7 +748,7 @@ class TicketModule(Component):
entries.append(email)
add = []
remove = []
- cc_list = Chrome(self.env).cc_list(cc)
+ cc_list = self._cc_list(cc)
for entry in entries:
if entry in cc_list:
remove.append(entry)
@@ -750,8 +763,15 @@ class TicketModule(Component):
def _populate(self, req, ticket, plain_fields=False):
if not plain_fields:
- fields = dict([(k[6:], v) for k, v in req.args.iteritems()
- if k.startswith('field_')])
+ fields = dict((k[6:], v) for k, v in req.args.iteritems()
+ if k.startswith('field_')
+ and not 'revert_' + k[6:] in req.args)
+ # Handle revert of checkboxes (in particular, revert to 1)
+ for k in list(fields):
+ if k.startswith('checkbox_'):
+ k = k[9:]
+ if 'revert_' + k in req.args:
+ fields[k] = ticket[k]
else:
fields = req.args.copy()
# Prevent direct changes to protected fields (status and resolution are
@@ -761,7 +781,7 @@ class TicketModule(Component):
fields.pop('checkbox_' + each, None) # See Ticket.populate()
ticket.populate(fields)
# special case for updating the Cc: field
- if 'cc_update' in req.args:
+ if 'cc_update' in req.args and 'revert_cc' not in req.args:
cc_action, cc_entry, cc_list = self._toggle_cc(req, ticket['cc'])
if cc_action == 'remove':
cc_list.remove(cc_entry)
@@ -1381,10 +1401,11 @@ class TicketModule(Component):
def _query_link(self, req, name, value, text=None):
"""Return a link to /query with the appropriate name and value"""
- default_query = self.ticketlink_query.startswith('?') and \
- self.ticketlink_query[1:] or self.ticketlink_query
+ default_query = self.ticketlink_query.lstrip('?')
args = arg_list_to_args(parse_arg_list(default_query))
args[name] = value
+ if name == 'resolution':
+ args['status'] = 'closed'
return tag.a(text or value, href=req.href.query(args))
def _query_link_words(self, context, name, value):
@@ -1410,7 +1431,7 @@ class TicketModule(Component):
items.append(rendered)
return tag(items)
- def _prepare_fields(self, req, ticket):
+ def _prepare_fields(self, req, ticket, field_changes=None):
context = web_context(req, ticket.resource)
fields = []
owner_field = None
@@ -1453,18 +1474,33 @@ class TicketModule(Component):
field['rendered'] = self._query_link_words(context, name,
ticket[name])
elif name == 'cc':
+ cc_changed = field_changes is not None and 'cc' in field_changes
field['rendered'] = self._query_link_words(context, name,
ticket[name])
if ticket.exists and \
'TICKET_EDIT_CC' not in req.perm(ticket.resource):
cc = ticket._old.get('cc', ticket['cc'])
cc_action, cc_entry, cc_list = self._toggle_cc(req, cc)
+ cc_update = 'cc_update' in req.args \
+ and 'revert_cc' not in req.args
field['edit_label'] = {
'add': _("Add to Cc"),
'remove': _("Remove from Cc"),
'': _("Add/Remove from Cc")}[cc_action]
field['cc_entry'] = cc_entry or _("<Author field>")
- field['cc_update'] = 'cc_update' in req.args or None
+ field['cc_update'] = cc_update or None
+ if cc_changed:
+ field_changes['cc']['cc_update'] = cc_update
+ if cc_changed:
+ # normalize the new CC: list; also remove the
+ # change altogether if there's no real change
+ old_cc_list = self._cc_list(field_changes['cc']['old'])
+ new_cc_list = self._cc_list(field_changes['cc']['new']
+ .replace(' ', ','))
+ if new_cc_list == old_cc_list:
+ del field_changes['cc']
+ else:
+ field_changes['cc']['new'] = ','.join(new_cc_list)
# per type settings
if type_ in ('radio', 'select'):
@@ -1524,7 +1560,8 @@ class TicketModule(Component):
# -- Ticket fields
- fields = self._prepare_fields(req, ticket)
+ fields = self._prepare_fields(req, ticket, field_changes)
+ fields_map = dict((field['name'], i) for i, field in enumerate(fields))
# -- Ticket Change History
@@ -1582,6 +1619,13 @@ class TicketModule(Component):
# -- Workflow support
selected_action = req.args.get('action')
+
+ # retrieve close time from changes
+ closetime = None
+ for c in changes:
+ s = c['fields'].get('status')
+ if s:
+ closetime = c['date'] if s['new'] == 'closed' else None
# action_controls is an ordered list of "renders" tuples, where
# renders is a list of (action_key, label, widgets, hints) representing
@@ -1631,13 +1675,14 @@ class TicketModule(Component):
for user in 'reporter', 'owner':
if chrome.format_author(req, ticket[user]) == ticket[user]:
data['%s_link' % user] = self._query_link(req, user,
- ticket[user])
+ ticket[user])
data.update({
'context': context, 'conflicts': conflicts,
- 'fields': fields, 'changes': changes, 'replies': replies,
+ 'fields': fields, 'fields_map': fields_map,
+ 'changes': changes, 'replies': replies,
'attachments': AttachmentModule(self.env).attachment_data(context),
'action_controls': action_controls, 'action': selected_action,
- 'change_preview': change_preview,
+ 'change_preview': change_preview, 'closetime': closetime,
})
def rendered_changelog_entries(self, req, ticket, when=None):
@@ -1692,8 +1737,7 @@ class TicketModule(Component):
render_elt = lambda x: x
sep = ', '
if field == 'cc':
- chrome = Chrome(self.env)
- old_list, new_list = chrome.cc_list(old), chrome.cc_list(new)
+ old_list, new_list = self._cc_list(old), self._cc_list(new)
if not (Chrome(self.env).show_email_addresses or
'EMAIL_VIEW' in req.perm(resource_new or ticket.resource)):
render_elt = obfuscate_email_address
@@ -1729,8 +1773,8 @@ class TicketModule(Component):
"""Iterate on changelog entries, consolidating related changes
in a `dict` object.
- :since 0.13: the `db` parameter is no longer needed and will be removed
- in version 0.14
+ :since 1.0: the `db` parameter is no longer needed and will be removed
+ in version 1.1.1
"""
field_labels = TicketSystem(self.env).get_ticket_field_labels()
changelog = ticket.get_changelog(when=when)
Modified: incubator/bloodhound/trunk/trac/trac/timeline/api.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/timeline/api.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/timeline/api.py (original)
+++ incubator/bloodhound/trunk/trac/trac/timeline/api.py Tue Oct 16 20:06:09 2012
@@ -3,7 +3,7 @@
# Copyright (C) 2003-2009 Edgewall Software
# Copyright (C) 2003-2005 Jonas Borgström <jo...@edgewall.com>
# Copyright (C) 2004-2005 Christopher Lenz <cm...@gmx.de>
-# Copyright (C) 2005-2006 Christian Boos <cb...@neuf.fr>
+# Copyright (C) 2005-2006 Christian Boos <cb...@edgewall.org>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
Modified: incubator/bloodhound/trunk/trac/trac/timeline/templates/timeline.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/timeline/templates/timeline.html?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/timeline/templates/timeline.html (original)
+++ incubator/bloodhound/trunk/trac/trac/timeline/templates/timeline.html Tue Oct 16 20:06:09 2012
@@ -8,6 +8,11 @@
<xi:include href="layout.html" />
<head>
<title>Timeline</title>
+ <script type="text/javascript">/*<![CDATA[*/
+ jQuery(document).ready(function($) {
+ $("#fromdate").datepicker();
+ });
+ /*]]>*/</script>
</head>
<body>
@@ -17,7 +22,7 @@
<form id="prefs" method="get" action="">
<div i18n:msg="">
- <label>View changes from <input type="text" size="10" name="from" value="${format_date(fromdate)}" /></label> <br />
+ <label>View changes from <input type="text" id="fromdate" size="10" name="from" value="${format_date(fromdate)}" /></label> <br />
and <label><input type="text" size="3" name="daysback" value="$daysback" /> days back</label><br />
<label>done by <input type="text" size="16" name="authors" value="$authors" /></label>
</div>
Modified: incubator/bloodhound/trunk/trac/trac/timeline/web_ui.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/timeline/web_ui.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/timeline/web_ui.py (original)
+++ incubator/bloodhound/trunk/trac/trac/timeline/web_ui.py Tue Oct 16 20:06:09 2012
@@ -3,7 +3,7 @@
# Copyright (C) 2003-2009 Edgewall Software
# Copyright (C) 2003-2005 Jonas Borgström <jo...@edgewall.com>
# Copyright (C) 2004-2005 Christopher Lenz <cm...@gmx.de>
-# Copyright (C) 2005-2006 Christian Boos <cb...@neuf.fr>
+# Copyright (C) 2005-2006 Christian Boos <cb...@edgewall.org>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
@@ -211,8 +211,9 @@ class TimelineModule(Component):
data['context'] = rss_context
return 'timeline.rss', data, 'application/rss+xml'
else:
- req.session['timeline.daysback'] = daysback
- req.session['timeline.authors'] = authors
+ req.session.set('timeline.daysback', daysback,
+ self.default_daysback)
+ req.session.set('timeline.authors', authors, '')
# store lastvisit
if events and not revisit:
lastviewed = to_utimestamp(events[0]['date'])
@@ -229,6 +230,7 @@ class TimelineModule(Component):
format='rss')
add_link(req, 'alternate', auth_link(req, rss_href), _('RSS Feed'),
'application/rss+xml', 'rss')
+ Chrome(self.env).add_jquery_ui(req)
for filter_ in available_filters:
data['filters'].append({'name': filter_[0], 'label': filter_[1],
Modified: incubator/bloodhound/trunk/trac/trac/util/__init__.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/util/__init__.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/util/__init__.py (original)
+++ incubator/bloodhound/trunk/trac/trac/util/__init__.py Tue Oct 16 20:06:09 2012
@@ -3,7 +3,7 @@
# Copyright (C) 2003-2009 Edgewall Software
# Copyright (C) 2003-2006 Jonas Borgström <jo...@edgewall.com>
# Copyright (C) 2006 Matthew Good <tr...@matt-good.net>
-# Copyright (C) 2005-2006 Christian Boos <cb...@neuf.fr>
+# Copyright (C) 2005-2006 Christian Boos <cb...@edgewall.org>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
@@ -406,6 +406,7 @@ def fq_class_name(obj):
m, n = c.__module__, c.__name__
return n if m == '__builtin__' else '%s.%s' % (m, n)
+
def arity(f):
"""Return the number of arguments expected by the given function, unbound
or bound method.
@@ -524,7 +525,7 @@ def safe_repr(x):
without risking to trigger an exception (e.g. from a buggy
`x.__repr__`).
- .. versionadded :: 0.13
+ .. versionadded :: 1.0
"""
try:
return to_unicode(repr(x))
@@ -546,6 +547,23 @@ def get_doc(obj):
description = doc[1] if len(doc) > 1 else None
return (summary, description)
+
+_dont_import = frozenset(['__file__', '__name__', '__package__'])
+
+def import_namespace(globals_dict, module_name):
+ """Import the namespace of a module into a globals dict.
+
+ This function is used in stub modules to import all symbols defined in
+ another module into the global namespace of the stub, usually for
+ backward compatibility.
+ """
+ __import__(module_name)
+ module = sys.modules[module_name]
+ globals_dict.update(item for item in module.__dict__.iteritems()
+ if item[0] not in _dont_import)
+ globals_dict.pop('import_namespace', None)
+
+
# -- setuptools utils
def get_module_path(module):
@@ -787,9 +805,16 @@ class Ranges(object):
>>> str(Ranges("20-10", reorder=True))
'10-20'
+ As rendered ranges are often using u',\u200b' (comma + Zero-width
+ space) to enable wrapping, we also support reading such ranges, as
+ they can be copy/pasted back.
+
+ >>> str(Ranges(u'1,\u200b3,\u200b5,\u200b6,\u200b7,\u200b9'))
+ '1,3,5-7,9'
+
"""
- RE_STR = r"""\d+(?:[-:]\d+)?(?:,\d+(?:[-:]\d+)?)*"""
+ RE_STR = ur'[0-9]+(?:[-:][0-9]+)?(?:,\u200b?[0-9]+(?:[-:][0-9]+)?)*'
def __init__(self, r=None, reorder=False):
self.pairs = []
@@ -808,7 +833,7 @@ class Ranges(object):
return
p = self.pairs
if isinstance(r, basestring):
- r = r.split(',')
+ r = re.split(u',\u200b?', r)
for x in r:
try:
a, b = map(int, x.split('-', 1))
@@ -1059,3 +1084,4 @@ from trac.util.datefmt import pretty_tim
get_datetime_format_hint, http_date, \
parse_date
+__no_apidoc__ = 'compat presentation translation'
Modified: incubator/bloodhound/trunk/trac/trac/util/daemon.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/util/daemon.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/util/daemon.py (original)
+++ incubator/bloodhound/trunk/trac/trac/util/daemon.py Tue Oct 16 20:06:09 2012
@@ -22,7 +22,6 @@ import sys
def daemonize(pidfile=None, progname=None, stdin='/dev/null',
stdout='/dev/null', stderr='/dev/null', umask=022):
"""Fork a daemon process."""
-
if pidfile:
# Check whether the pid file already exists and refers to a still
# process running
@@ -32,7 +31,8 @@ def daemonize(pidfile=None, progname=Non
try:
pid = int(fileobj.read())
except ValueError:
- sys.exit('Invalid PID in file %s' % pidfile)
+ sys.exit('Invalid pid in file %s\nPlease remove it to '
+ 'proceed' % pidfile)
try: # signal the process to see if it is still running
os.kill(pid, 0)
@@ -45,7 +45,7 @@ def daemonize(pidfile=None, progname=Non
# The pid file must be writable
try:
- fileobj = open(pidfile, 'r+')
+ fileobj = open(pidfile, 'a+')
fileobj.close()
except IOError, e:
from trac.util.text import exception_to_unicode
Modified: incubator/bloodhound/trunk/trac/trac/util/datefmt.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/util/datefmt.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/util/datefmt.py (original)
+++ incubator/bloodhound/trunk/trac/trac/util/datefmt.py Tue Oct 16 20:06:09 2012
@@ -3,7 +3,7 @@
# Copyright (C) 2003-2009 Edgewall Software
# Copyright (C) 2003-2006 Jonas Borgström <jo...@edgewall.com>
# Copyright (C) 2006 Matthew Good <tr...@matt-good.net>
-# Copyright (C) 2005-2006 Christian Boos <cb...@neuf.fr>
+# Copyright (C) 2005-2006 Christian Boos <cb...@edgewall.org>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
@@ -26,12 +26,16 @@ from locale import getlocale, getpreferr
try:
import babel
- from babel.dates import format_datetime as babel_format_datetime, \
- format_date as babel_format_date, \
- format_time as babel_format_time, \
- get_datetime_format, get_date_format, \
- get_time_format, get_month_names, \
- get_period_names, get_day_names
+ from babel import Locale
+ from babel.core import LOCALE_ALIASES
+ from babel.dates import (
+ format_datetime as babel_format_datetime,
+ format_date as babel_format_date,
+ format_time as babel_format_time,
+ get_datetime_format, get_date_format,
+ get_time_format, get_month_names,
+ get_period_names, get_day_names
+ )
except ImportError:
babel = None
@@ -373,6 +377,17 @@ def get_timezone_list_jquery_ui(t=None):
if zone == '+0000' else zone[:-2] + ':' + zone[-2:]
for zone in sorted(zones, key=lambda tz: int(tz))]
+def get_first_week_day_jquery_ui(req):
+ """Get first week day for jQuery date picker"""
+ locale = req.lc_time
+ if locale == 'iso8601':
+ return 1 # Monday
+ if babel and locale:
+ if not locale.territory and locale.language in LOCALE_ALIASES:
+ locale = Locale.parse(LOCALE_ALIASES[locale.language])
+ return (locale.first_week_day + 1) % 7
+ return 0 # Sunday
+
def is_24_hours(locale):
"""Returns `True` for 24 hour time formats."""
if locale == 'iso8601':
@@ -396,7 +411,11 @@ def http_date(t=None):
_ISO_8601_RE = re.compile(r'''
(\d\d\d\d)(?:-?(\d\d)(?:-?(\d\d))?)? # date
- (?:T(\d\d)(?::?(\d\d)(?::?(\d\d))?)?)? # time
+ (?:
+ [T ]
+ (\d\d)(?::?(\d\d)(?::?(\d\d) # time
+ (?:[,.](\d{1,6}))?)?)? # microseconds
+ )?
(Z?(?:([-+])?(\d\d):?(\d\d)?)?)?$ # timezone
''', re.VERBOSE)
@@ -408,8 +427,9 @@ def _parse_date_iso8601(text, tzinfo):
years = g[0]
months = g[1] or '01'
days = g[2] or '01'
- hours, minutes, seconds = [x or '00' for x in g[3:6]]
- z, tzsign, tzhours, tzminutes = g[6:10]
+ hours, minutes, seconds, useconds = [x or '00' for x in g[3:7]]
+ useconds = (useconds + '000000')[:6]
+ z, tzsign, tzhours, tzminutes = g[7:11]
if z:
tz = timedelta(hours=int(tzhours or '0'),
minutes=int(tzminutes or '0')).seconds / 60
@@ -419,10 +439,9 @@ def _parse_date_iso8601(text, tzinfo):
tzinfo = FixedOffset(-tz if tzsign == '-' else tz,
'%s%s:%s' %
(tzsign, tzhours, tzminutes))
- tm = time.strptime('%s ' * 6 % (years, months, days,
- hours, minutes, seconds),
- '%Y %m %d %H %M %S ')
- t = tzinfo.localize(datetime(*tm[0:6]))
+ tm = [int(x) for x in (years, months, days,
+ hours, minutes, seconds, useconds)]
+ t = tzinfo.localize(datetime(*tm))
return tzinfo.normalize(t)
except ValueError:
pass
Modified: incubator/bloodhound/trunk/trac/trac/util/html.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/util/html.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/util/html.py (original)
+++ incubator/bloodhound/trunk/trac/trac/util/html.py Tue Oct 16 20:06:09 2012
@@ -71,47 +71,29 @@ class TracHTMLSanitizer(HTMLSanitizer):
self.safe_css = frozenset(safe_css)
# IE6 <http://heideri.ch/jso/#80>
- _EXPRESSION_SEARCH = re.compile(u"""
- [eE
- \uFF25 # FULLWIDTH LATIN CAPITAL LETTER E
- \uFF45 # FULLWIDTH LATIN SMALL LETTER E
- ]
- [xX
- \uFF38 # FULLWIDTH LATIN CAPITAL LETTER X
- \uFF58 # FULLWIDTH LATIN SMALL LETTER X
- ]
- [pP
- \uFF30 # FULLWIDTH LATIN CAPITAL LETTER P
- \uFF50 # FULLWIDTH LATIN SMALL LETTER P
- ]
- [rR
- \u0280 # LATIN LETTER SMALL CAPITAL R
- \uFF32 # FULLWIDTH LATIN CAPITAL LETTER R
- \uFF52 # FULLWIDTH LATIN SMALL LETTER R
- ]
- [eE
- \uFF25 # FULLWIDTH LATIN CAPITAL LETTER E
- \uFF45 # FULLWIDTH LATIN SMALL LETTER E
- ]
- [sS
- \uFF33 # FULLWIDTH LATIN CAPITAL LETTER S
- \uFF53 # FULLWIDTH LATIN SMALL LETTER S
- ]{2}
- [iI
- \u026A # LATIN LETTER SMALL CAPITAL I
- \uFF29 # FULLWIDTH LATIN CAPITAL LETTER I
- \uFF49 # FULLWIDTH LATIN SMALL LETTER I
- ]
- [oO
- \uFF2F # FULLWIDTH LATIN CAPITAL LETTER O
- \uFF4F # FULLWIDTH LATIN SMALL LETTER O
- ]
- [nN
- \u0274 # LATIN LETTER SMALL CAPITAL N
- \uFF2E # FULLWIDTH LATIN CAPITAL LETTER N
- \uFF4E # FULLWIDTH LATIN SMALL LETTER N
- ]
- """, re.VERBOSE).search
+ _EXPRESSION_SEARCH = re.compile(
+ u'[eE\uFF25\uFF45]' # FULLWIDTH LATIN CAPITAL LETTER E
+ # FULLWIDTH LATIN SMALL LETTER E
+ u'[xX\uFF38\uFF58]' # FULLWIDTH LATIN CAPITAL LETTER X
+ # FULLWIDTH LATIN SMALL LETTER X
+ u'[pP\uFF30\uFF50]' # FULLWIDTH LATIN CAPITAL LETTER P
+ # FULLWIDTH LATIN SMALL LETTER P
+ u'[rR\u0280\uFF32\uFF52]' # LATIN LETTER SMALL CAPITAL R
+ # FULLWIDTH LATIN CAPITAL LETTER R
+ # FULLWIDTH LATIN SMALL LETTER R
+ u'[eE\uFF25\uFF45]' # FULLWIDTH LATIN CAPITAL LETTER E
+ # FULLWIDTH LATIN SMALL LETTER E
+ u'[sS\uFF33\uFF53]{2}' # FULLWIDTH LATIN CAPITAL LETTER S
+ # FULLWIDTH LATIN SMALL LETTER S
+ u'[iI\u026A\uFF29\uFF49]' # LATIN LETTER SMALL CAPITAL I
+ # FULLWIDTH LATIN CAPITAL LETTER I
+ # FULLWIDTH LATIN SMALL LETTER I
+ u'[oO\uFF2F\uFF4F]' # FULLWIDTH LATIN CAPITAL LETTER O
+ # FULLWIDTH LATIN SMALL LETTER O
+ u'[nN\u0274\uFF2E\uFF4E]' # LATIN LETTER SMALL CAPITAL N
+ # FULLWIDTH LATIN CAPITAL LETTER N
+ # FULLWIDTH LATIN SMALL LETTER N
+ ).search
# IE6 <http://openmya.hacker.jp/hasegawa/security/expression.txt>
# 7) Particular bit of Unicode characters
@@ -184,7 +166,15 @@ class TracHTMLSanitizer(HTMLSanitizer):
def _repl(match):
t = match.group(1)
if t:
- return unichr(int(t, 16))
+ code = int(t, 16)
+ chr = unichr(code)
+ if code <= 0x1f:
+ # replace space character because IE ignores control
+ # characters
+ chr = ' '
+ elif chr == '\\':
+ chr = r'\\'
+ return chr
t = match.group(2)
if t == '\\':
return r'\\'
@@ -193,6 +183,14 @@ class TracHTMLSanitizer(HTMLSanitizer):
return self._UNICODE_ESCAPE(_repl,
self._NORMALIZE_NEWLINES('\n', text))
+ _CSS_COMMENTS = re.compile(r'/\*.*?\*/').sub
+
+ def _strip_css_comments(self, text):
+ """Replace comments with space character instead of superclass which
+ removes comments to avoid problems when nested comments.
+ """
+ return self._CSS_COMMENTS(' ', text)
+
class Deuglifier(object):
"""Help base class used for cleaning up HTML riddled with ``<FONT
Modified: incubator/bloodhound/trunk/trac/trac/util/presentation.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/util/presentation.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/util/presentation.py (original)
+++ incubator/bloodhound/trunk/trac/trac/util/presentation.py Tue Oct 16 20:06:09 2012
@@ -19,9 +19,14 @@ tasks such as grouping or pagination.
from math import ceil
import re
-__all__ = ['classes', 'first_last', 'group', 'istext', 'prepared_paginate',
- 'paginate', 'Paginator']
-
+__all__ = ['captioned_button', 'classes', 'first_last', 'group', 'istext',
+ 'prepared_paginate', 'paginate', 'Paginator']
+__no_apidoc__ = 'prepared_paginate'
+
+def captioned_button(req, symbol, text):
+ """Return symbol and text or only symbol, according to user preferences."""
+ return symbol if req.session.get('ui.use_symbols') \
+ else u'%s %s' % (symbol, text)
def classes(*args, **kwargs):
"""Helper function for dynamically assembling a list of CSS class names
@@ -190,6 +195,7 @@ def paginate(items, page=0, max_per_page
class Paginator(object):
+ """Pagination controller"""
def __init__(self, items, page=0, max_per_page=10, num_items=None):
if not page:
@@ -255,11 +261,11 @@ class Paginator(object):
from trac.util.translation import _
start, stop = self.span
total = self.num_items
- if start+1 == stop:
+ if start + 1 == stop:
return _("%(last)d of %(total)d", last=stop, total=total)
else:
return _("%(start)d - %(stop)d of %(total)d",
- start=self.span[0]+1, stop=self.span[1], total=total)
+ start=self.span[0] + 1, stop=self.span[1], total=total)
def separated(items, sep=','):
@@ -298,12 +304,12 @@ try:
return _js_quote_re.sub(replace, text)
except ImportError:
- from trac.util.text import javascript_quote
+ from trac.util.text import to_js_string
def to_json(value):
"""Encode `value` to JSON."""
if isinstance(value, basestring):
- return '"%s"' % javascript_quote(value)
+ return to_js_string(value)
elif value is None:
return 'null'
elif value is False:
Modified: incubator/bloodhound/trunk/trac/trac/util/tests/datefmt.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/util/tests/datefmt.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/util/tests/datefmt.py (original)
+++ incubator/bloodhound/trunk/trac/trac/util/tests/datefmt.py Tue Oct 16 20:06:09 2012
@@ -53,15 +53,19 @@ else:
def test_parse_date(self):
tz = datefmt.get_timezone('Europe/Zurich')
- t = datefmt.parse_date('2009-12-01T12:00:00', tz)
t_utc = datetime.datetime(2009, 12, 1, 11, 0, 0, 0, datefmt.utc)
- self.assertEqual(t_utc, t)
+ self.assertEqual(t_utc,
+ datefmt.parse_date('2009-12-01T12:00:00', tz))
+ self.assertEqual(t_utc,
+ datefmt.parse_date('2009-12-01 12:00:00', tz))
def test_parse_date_dst(self):
tz = datefmt.get_timezone('Europe/Zurich')
- t = datefmt.parse_date('2009-08-01T12:00:00', tz)
t_utc = datetime.datetime(2009, 8, 1, 10, 0, 0, 0, datefmt.utc)
- self.assertEqual(t_utc, t)
+ self.assertEqual(t_utc,
+ datefmt.parse_date('2009-08-01T12:00:00', tz))
+ self.assertEqual(t_utc,
+ datefmt.parse_date('2009-08-01 12:00:00', tz))
def test_parse_date_across_dst_boundary(self):
tz = datefmt.get_timezone('Europe/Zurich')
@@ -88,6 +92,7 @@ else:
expected = '2002-03-31 00:00:00 CET+0100'
self.assertEqual(expected, date.strftime(format))
+
class DateFormatTestCase(unittest.TestCase):
def test_to_datetime(self):
@@ -219,6 +224,36 @@ class ISO8601TestCase(unittest.TestCase)
self.assertEqual('2010-08-28T11:45:56+02:00',
datefmt.format_datetime(t, 'iso8601', tz, 'iso8601'))
+ def test_parse_date_offset(self):
+ t_utc = datetime.datetime(2009, 12, 1, 11, 0, 0, 0, datefmt.utc)
+ self.assertEqual(t_utc,
+ datefmt.parse_date('2009-12-01T11:00:00Z'))
+ self.assertEqual(t_utc,
+ datefmt.parse_date('2009-12-01T11:00:00+00:00'))
+ self.assertEqual(t_utc,
+ datefmt.parse_date('2009-12-01T11:00:00-00:00'))
+ self.assertEqual(t_utc,
+ datefmt.parse_date('2009-12-01T09:00:00-02:00'))
+ self.assertEqual(t_utc,
+ datefmt.parse_date('2009-12-01T11:30:00+00:30'))
+
+ def test_parse_date_usec(self):
+ tz = datefmt.get_timezone('GMT +1:00')
+ t_utc = datetime.datetime(2009, 12, 1, 11, 0, 0, 98765, datefmt.utc)
+ self.assertEqual(t_utc,
+ datefmt.parse_date('2009-12-01T12:00:00.098765', tz))
+ self.assertEqual(t_utc,
+ datefmt.parse_date('2009-12-01T12:00:00,098765', tz))
+ self.assertEqual(datetime.datetime(2009, 12, 1, 11, 0, 0, 98700,
+ datefmt.utc),
+ datefmt.parse_date('2009-12-01T12:00:00.0987', tz))
+ self.assertEqual(datetime.datetime(2009, 12, 1, 11, 0, 0, 90000,
+ datefmt.utc),
+ datefmt.parse_date('2009-12-01T12:00:00.09', tz))
+ self.assertEqual(datetime.datetime(2009, 12, 1, 11, 0, 0, 0,
+ datefmt.utc),
+ datefmt.parse_date('2009-12-01T12:00:00.0', tz))
+
def test_with_babel_format(self):
tz = datefmt.timezone('GMT +2:00')
t = datetime.datetime(2010, 8, 28, 11, 45, 56, 123456, tz)
Modified: incubator/bloodhound/trunk/trac/trac/util/tests/html.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/util/tests/html.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/util/tests/html.py (original)
+++ incubator/bloodhound/trunk/trac/trac/util/tests/html.py Tue Oct 16 20:06:09 2012
@@ -17,6 +17,18 @@ class TracHTMLSanitizerTestCase(unittest
encoding='utf-8')
self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
+ def test_expression_with_comments(self):
+ html = HTML(r'<div style="top:exp/**/ression(alert())">XSS</div>')
+ self.assertEqual('<div style="top:exp ression(alert())">XSS</div>',
+ unicode(html | TracHTMLSanitizer()))
+ html = HTML(r'<div style="top:exp//**/**/ression(alert())">XSS</div>')
+ self.assertEqual(
+ '<div style="top:exp/ **/ression(alert())">XSS</div>',
+ unicode(html | TracHTMLSanitizer()))
+ html = HTML(r'<div style="top:ex/*p*/ression(alert())">XSS</div>')
+ self.assertEqual('<div style="top:ex ression(alert())">XSS</div>',
+ unicode(html | TracHTMLSanitizer()))
+
def test_url_with_javascript(self):
html = HTML('<div style="background-image:url(javascript:alert())">'
'XSS</div>', encoding='utf-8')
@@ -31,6 +43,18 @@ class TracHTMLSanitizerTestCase(unittest
html = HTML(r'<div style="top:exp\72 ess\000069 on(alert())">'
r'XSS</div>', encoding='utf-8')
self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
+ # escaped backslash
+ html = HTML(r'<div style="top:exp\5c ression(alert())">XSS</div>')
+ self.assertEqual(r'<div style="top:exp\\ression(alert())">XSS</div>',
+ unicode(html | TracHTMLSanitizer()))
+ html = HTML(r'<div style="top:exp\5c 72 ession(alert())">XSS</div>')
+ self.assertEqual(r'<div style="top:exp\\72 ession(alert())">XSS</div>',
+ unicode(html | TracHTMLSanitizer()))
+ # escaped control characters
+ html = HTML(r'<div style="top:exp\000000res\1f sion(alert())">'
+ r'XSS</div>')
+ self.assertEqual('<div style="top:exp res sion(alert())">XSS</div>',
+ unicode(html | TracHTMLSanitizer()))
def test_backslash_without_hex(self):
html = HTML(r'<div style="top:e\xp\ression(alert())">XSS</div>',
Modified: incubator/bloodhound/trunk/trac/trac/util/tests/presentation.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/util/tests/presentation.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/util/tests/presentation.py (original)
+++ incubator/bloodhound/trunk/trac/trac/util/tests/presentation.py Tue Oct 16 20:06:09 2012
@@ -28,6 +28,8 @@ class ToJsonTestCase(unittest.TestCase):
self.assertEqual('null', presentation.to_json(None))
self.assertEqual('"String"', presentation.to_json('String'))
self.assertEqual(r'"a \" quote"', presentation.to_json('a " quote'))
+ self.assertEqual('''"a ' single quote"''',
+ presentation.to_json("a ' single quote"))
self.assertEqual(r'"\u003cb\u003e\u0026\u003c/b\u003e"',
presentation.to_json('<b>&</b>'))
@@ -35,11 +37,11 @@ class ToJsonTestCase(unittest.TestCase):
self.assertEqual('[1,2,[true,false]]',
presentation.to_json([1, 2, [True, False]]))
self.assertEqual(r'{"one":1,"other":[null,0],'
- r'"three":[3,"\u0026\u003c\u003e"],'
+ r'''"three":[3,"\u0026\u003c\u003e'"],'''
r'"two":2}',
presentation.to_json({"one": 1, "two": 2,
"other": [None, 0],
- "three": [3, "&<>"]}))
+ "three": [3, "&<>'"]}))
def suite():
Modified: incubator/bloodhound/trunk/trac/trac/util/tests/text.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/util/tests/text.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/util/tests/text.py (original)
+++ incubator/bloodhound/trunk/trac/trac/util/tests/text.py Tue Oct 16 20:06:09 2012
@@ -4,11 +4,12 @@ import unittest
from StringIO import StringIO
from trac.util.text import empty, expandtabs, fix_eol, javascript_quote, \
- normalize_whitespace, to_unicode, \
+ to_js_string, normalize_whitespace, to_unicode, \
text_width, print_table, unicode_quote, \
unicode_quote_plus, unicode_unquote, \
unicode_urlencode, wrap, quote_query_string, \
- unicode_to_base64, unicode_from_base64
+ unicode_to_base64, unicode_from_base64, stripws, \
+ levenshtein_distance
class ToUnicodeTestCase(unittest.TestCase):
@@ -65,6 +66,19 @@ class JavascriptQuoteTestCase(unittest.T
self.assertEqual(r'\u0026\u003c\u003e',
javascript_quote('&<>'))
+
+class ToJsStringTestCase(unittest.TestCase):
+ def test_(self):
+ self.assertEqual(r'"Quote \" in text"',
+ to_js_string('Quote " in text'))
+ self.assertEqual(r'''"\\\"\b\f\n\r\t'"''',
+ to_js_string('\\"\b\f\n\r\t\''))
+ self.assertEqual(r'"\u0002\u001e"',
+ to_js_string('\x02\x1e'))
+ self.assertEqual(r'"\u0026\u003c\u003e"',
+ to_js_string('&<>'))
+
+
class UnicodeQuoteTestCase(unittest.TestCase):
def test_unicode_quote(self):
self.assertEqual(u'the%20%C3%9C%20thing',
@@ -289,12 +303,38 @@ class UnicodeBase64TestCase(unittest.Tes
self.assertEqual(text, unicode_from_base64(text_base64_no_strip))
+class StripwsTestCase(unittest.TestCase):
+ def test_stripws(self):
+ self.assertEquals(u'stripws',
+ stripws(u' \u200b\t\u3000stripws \u200b\t\u2008'))
+ self.assertEquals(u'stripws \u3000\t',
+ stripws(u'\u200b\t\u2008 stripws \u3000\t',
+ trailing=False))
+ self.assertEquals(u' \t\u3000stripws',
+ stripws(u' \t\u3000stripws \u200b\t\u2008',
+ leading=False))
+ self.assertEquals(u' \t\u3000stripws \u200b\t\u2008',
+ stripws(u' \t\u3000stripws \u200b\t\u2008',
+ leading=False, trailing=False))
+
+
+
+class LevenshteinDistanceTestCase(unittest.TestCase):
+ def test_distance(self):
+ self.assertEqual(5, levenshtein_distance('kitten', 'sitting'))
+ self.assertEqual(1, levenshtein_distance('wii', 'wiki'))
+ self.assertEqual(2, levenshtein_distance('comfig', 'config'))
+ self.assertEqual(5, levenshtein_distance('update', 'upgrade'))
+ self.assertEqual(0, levenshtein_distance('milestone', 'milestone'))
+
+
def suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(ToUnicodeTestCase, 'test'))
suite.addTest(unittest.makeSuite(ExpandtabsTestCase, 'test'))
suite.addTest(unittest.makeSuite(UnicodeQuoteTestCase, 'test'))
suite.addTest(unittest.makeSuite(JavascriptQuoteTestCase, 'test'))
+ suite.addTest(unittest.makeSuite(ToJsStringTestCase, 'test'))
suite.addTest(unittest.makeSuite(QuoteQueryStringTestCase, 'test'))
suite.addTest(unittest.makeSuite(WhitespaceTestCase, 'test'))
suite.addTest(unittest.makeSuite(TextWidthTestCase, 'test'))
@@ -302,6 +342,8 @@ def suite():
suite.addTest(unittest.makeSuite(WrapTestCase, 'test'))
suite.addTest(unittest.makeSuite(FixEolTestCase, 'test'))
suite.addTest(unittest.makeSuite(UnicodeBase64TestCase, 'test'))
+ suite.addTest(unittest.makeSuite(StripwsTestCase, 'test'))
+ suite.addTest(unittest.makeSuite(LevenshteinDistanceTestCase, 'test'))
return suite
if __name__ == '__main__':
Modified: incubator/bloodhound/trunk/trac/trac/util/text.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/util/text.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/util/text.py (original)
+++ incubator/bloodhound/trunk/trac/trac/util/text.py Tue Oct 16 20:06:09 2012
@@ -3,7 +3,7 @@
# Copyright (C) 2003-2009 Edgewall Software
# Copyright (C) 2003-2004 Jonas Borgström <jo...@edgewall.com>
# Copyright (C) 2006 Matthew Good <tr...@matt-good.net>
-# Copyright (C) 2005-2006 Christian Boos <cb...@neuf.fr>
+# Copyright (C) 2005-2006 Christian Boos <cb...@edgewall.org>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
@@ -16,7 +16,7 @@
#
# Author: Jonas Borgström <jo...@edgewall.com>
# Matthew Good <tr...@matt-good.net>
-# Christian Boos <cb...@neuf.fr>
+# Christian Boos <cb...@edgewall.org>
import __builtin__
import locale
@@ -95,15 +95,36 @@ def path_to_unicode(path):
return unicode(path)
+_ws_leading_re = re.compile(ur'\A[\s\u200b]+', re.UNICODE)
+_ws_trailing_re = re.compile(ur'[\s\u200b]+\Z', re.UNICODE)
+
+def stripws(text, leading=True, trailing=True):
+ """Strips unicode white-spaces and ZWSPs from ``text``.
+
+ :param leading: strips leading spaces from ``text`` unless ``leading`` is
+ `False`.
+ :param trailing: strips trailing spaces from ``text`` unless ``trailing``
+ is `False`.
+ """
+ if leading:
+ text = _ws_leading_re.sub('', text)
+ if trailing:
+ text = _ws_trailing_re.sub('', text)
+ return text
+
+
_js_quote = {'\\': '\\\\', '"': '\\"', '\b': '\\b', '\f': '\\f',
'\n': '\\n', '\r': '\\r', '\t': '\\t', "'": "\\'"}
for i in range(0x20) + [ord(c) for c in '&<>']:
_js_quote.setdefault(chr(i), '\\u%04x' % i)
_js_quote_re = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t\'&<>]')
+_js_string_re = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t&<>]')
def javascript_quote(text):
- """Quote strings for inclusion in javascript"""
+ """Quote strings for inclusion in single or double quote delimited
+ Javascript strings
+ """
if not text:
return ''
def replace(match):
@@ -111,6 +132,17 @@ def javascript_quote(text):
return _js_quote_re.sub(replace, text)
+def to_js_string(text):
+ """Embed the given string in a double quote delimited Javascript string
+ (conform to the JSON spec)
+ """
+ if not text:
+ return ''
+ def replace(match):
+ return _js_quote[match.group(0)]
+ return '"%s"' % _js_string_re.sub(replace, text)
+
+
def unicode_quote(value, safe='/'):
"""A unicode aware version of `urllib.quote`
@@ -541,7 +573,7 @@ def normalize_whitespace(text, to_space=
def unquote_label(txt):
"""Remove (one level of) enclosing single or double quotes.
- .. versionadded :: 0.13
+ .. versionadded :: 1.0
"""
return txt[1:-1] if txt and txt[0] in "'\"" and txt[0] == txt[-1] else txt
@@ -626,3 +658,22 @@ def unicode_to_base64(text, strip_newlin
def unicode_from_base64(text):
"""Safe conversion of ``text`` to unicode based on utf-8 bytes."""
return text.decode('base64').decode('utf-8')
+
+
+def levenshtein_distance(lhs, rhs):
+ """Return the Levenshtein distance between two strings."""
+ if len(lhs) > len(rhs):
+ rhs, lhs = lhs, rhs
+ if not lhs:
+ return len(rhs)
+
+ prev = range(len(rhs) + 1)
+ for lidx, lch in enumerate(lhs):
+ curr = [lidx + 1]
+ for ridx, rch in enumerate(rhs):
+ cost = (lch != rch) * 2
+ curr.append(min(prev[ridx + 1] + 1, # deletion
+ curr[ridx] + 1, # insertion
+ prev[ridx] + cost)) # substitution
+ prev = curr
+ return prev[-1]
Modified: incubator/bloodhound/trunk/trac/trac/versioncontrol/api.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/versioncontrol/api.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/versioncontrol/api.py (original)
+++ incubator/bloodhound/trunk/trac/trac/versioncontrol/api.py Tue Oct 16 20:06:09 2012
@@ -663,7 +663,11 @@ class RepositoryManager(Component):
try:
changeset = repos.get_changeset(rev)
except NoSuchChangeset:
- continue
+ try:
+ repos.sync_changeset(rev)
+ changeset = repos.get_changeset(rev)
+ except NoSuchChangeset:
+ continue
self.log.debug("Event %s on %s for revision %s",
event, repos.reponame or '(default)', rev)
for listener in self.change_listeners:
@@ -1139,7 +1143,7 @@ class Changeset(object):
def get_tags(self):
"""Yield tags associated with this changeset.
- .. versionadded :: 0.13
+ .. versionadded :: 1.0
"""
return []
Modified: incubator/bloodhound/trunk/trac/trac/versioncontrol/cache.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/versioncontrol/cache.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/versioncontrol/cache.py (original)
+++ incubator/bloodhound/trunk/trac/trac/versioncontrol/cache.py Tue Oct 16 20:06:09 2012
@@ -30,6 +30,12 @@ _actionmap = {'A': Changeset.ADD, 'C': C
'D': Changeset.DELETE, 'E': Changeset.EDIT,
'M': Changeset.MOVE}
+def _invert_dict(d):
+ return dict(zip(d.values(), d.keys()))
+
+_inverted_kindmap = _invert_dict(_kindmap)
+_inverted_actionmap = _invert_dict(_actionmap)
+
CACHE_REPOSITORY_DIR = 'repository_dir'
CACHE_YOUNGEST_REV = 'youngest_rev'
@@ -89,10 +95,13 @@ class CachedRepository(Repository):
""", (self.id, srev)):
old_cset = Changeset(self.repos, cset.rev, message, author,
from_utimestamp(time))
- db("""UPDATE revision SET time=%s, author=%s, message=%s
- WHERE repos=%s AND rev=%s
- """, (to_utimestamp(cset.date), cset.author, cset.message,
- self.id, srev))
+ if old_cset:
+ db("""UPDATE revision SET time=%s, author=%s, message=%s
+ WHERE repos=%s AND rev=%s
+ """, (to_utimestamp(cset.date), cset.author,
+ cset.message, self.id, srev))
+ else:
+ self._insert_changeset(db, rev, cset)
return old_cset
@cached('_metadata_id')
@@ -215,69 +224,69 @@ class CachedRepository(Repository):
self.repos.clear(youngest_rev=youngest)
return
- # 1. prepare for resyncing
- # (there still might be a race condition at this point)
-
- kindmap = dict(zip(_kindmap.values(), _kindmap.keys()))
- actionmap = dict(zip(_actionmap.values(), _actionmap.keys()))
-
+ # prepare for resyncing (there might still be a race
+ # condition at this point)
while next_youngest is not None:
srev = self.db_rev(next_youngest)
-
+
with self.env.db_transaction as db:
-
- # 1.1 Attempt to resync the 'revision' table
self.log.info("Trying to sync revision [%s]",
next_youngest)
cset = self.repos.get_changeset(next_youngest)
try:
- db("""INSERT INTO revision
- (repos, rev, time, author, message)
- VALUES (%s, %s, %s, %s, %s)
- """, (self.id, srev, to_utimestamp(cset.date),
- cset.author, cset.message))
- except Exception, e: # *another* 1.1. resync attempt won
+ # steps 1. and 2.
+ self._insert_changeset(db, next_youngest, cset)
+ except Exception, e: # *another* 1.1. resync attempt won
self.log.warning('Revision %s already cached: %r',
next_youngest, e)
- # also potentially in progress, so keep ''previous''
- # notion of 'youngest'
+ # the other resync attempts is also
+ # potentially still in progress, so for our
+ # process/thread, keep ''previous'' notion of
+ # 'youngest'
self.repos.clear(youngest_rev=youngest)
# FIXME: This aborts a containing transaction
db.rollback()
return
-
- # 1.2. now *only* one process was able to get there
- # (i.e. there *shouldn't* be any race condition here)
-
- for path, kind, action, bpath, brev in cset.get_changes():
- self.log.debug("Caching node change in [%s]: %r",
- next_youngest,
- (path, kind, action, bpath, brev))
- kind = kindmap[kind]
- action = actionmap[action]
- db("""INSERT INTO node_change
- (repos, rev, path, node_type, change_type,
- base_path, base_rev)
- VALUES (%s, %s, %s, %s, %s, %s, %s)
- """, (self.id, srev, path, kind, action, bpath,
- brev))
-
- # 1.3. update 'youngest_rev' metadata
- # (minimize possibility of failures at point 0.)
- db("""UPDATE repository SET value=%s
- WHERE id=%s AND name=%s
- """, (str(next_youngest),
- self.id, CACHE_YOUNGEST_REV))
+
+ # 3. update 'youngest_rev' metadata (minimize
+ # possibility of failures at point 0.)
+ db("""
+ UPDATE repository SET value=%s WHERE id=%s AND name=%s
+ """, (str(next_youngest), self.id, CACHE_YOUNGEST_REV))
del self.metadata
- # 1.4. iterate (1.1 should always succeed now)
+ # 4. iterate (1. should always succeed now)
youngest = next_youngest
next_youngest = self.repos.next_rev(next_youngest)
- # 1.5. provide some feedback
+ # 5. provide some feedback
if feedback:
feedback(youngest)
+ def _insert_changeset(self, db, rev, cset):
+ srev = self.db_rev(rev)
+ # 1. Attempt to resync the 'revision' table. In case of
+ # concurrent syncs, only such insert into the `revision` table
+ # will succeed, the others will fail and raise an exception.
+ db("""
+ INSERT INTO revision (repos,rev,time,author,message)
+ VALUES (%s,%s,%s,%s,%s)
+ """, (self.id, srev, to_utimestamp(cset.date),
+ cset.author, cset.message))
+ # 2. now *only* one process was able to get there (i.e. there
+ # *shouldn't* be any race condition here)
+ for path, kind, action, bpath, brev in cset.get_changes():
+ self.log.debug("Caching node change in [%s]: %r", rev,
+ (path, kind, action, bpath, brev))
+ kind = _inverted_kindmap[kind]
+ action = _inverted_actionmap[action]
+ db("""
+ INSERT INTO node_change
+ (repos,rev,path,node_type,change_type,base_path,
+ base_rev)
+ VALUES (%s,%s,%s,%s,%s,%s,%s)
+ """, (self.id, srev, path, kind, action, bpath, brev))
+
def get_node(self, path, rev=None):
return self.repos.get_node(path, self.normalize_rev(rev))
Modified: incubator/bloodhound/trunk/trac/trac/versioncontrol/diff.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/trunk/trac/trac/versioncontrol/diff.py?rev=1398968&r1=1398967&r2=1398968&view=diff
==============================================================================
--- incubator/bloodhound/trunk/trac/trac/versioncontrol/diff.py (original)
+++ incubator/bloodhound/trunk/trac/trac/versioncontrol/diff.py Tue Oct 16 20:06:09 2012
@@ -177,7 +177,7 @@ def filter_ignorable_lines(hunks, fromli
def hdf_diff(*args, **kwargs):
- """:deprecated: use `diff_blocks` (will be removed in 0.14)"""
+ """:deprecated: use `diff_blocks` (will be removed in 1.1.1)"""
return diff_blocks(*args, **kwargs)
def diff_blocks(fromlines, tolines, context=None, tabwidth=8,
@@ -309,7 +309,7 @@ def get_diff_options(req):
pref = int(req.session.get('diff_' + name, default))
arg = int(name in req.args)
if 'update' in req.args and arg != pref:
- req.session['diff_' + name] = arg
+ req.session.set('diff_' + name, arg, default)
else:
arg = pref
return arg
@@ -317,7 +317,7 @@ def get_diff_options(req):
pref = req.session.get('diff_style', 'inline')
style = req.args.get('style', pref)
if 'update' in req.args and style != pref:
- req.session['diff_style'] = style
+ req.session.set('diff_style', style, 'inline')
data['style'] = style
pref = int(req.session.get('diff_contextlines', 2))
@@ -326,7 +326,7 @@ def get_diff_options(req):
except ValueError:
context = -1
if 'update' in req.args and context != pref:
- req.session['diff_contextlines'] = context
+ req.session.set('diff_contextlines', context, 2)
options_data['contextlines'] = context
arg = int(req.args.get('contextall', 0))