You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bloodhound.apache.org by ah...@apache.org on 2013/07/28 19:22:24 UTC
svn commit: r1507818 [1/3] - in
/bloodhound/branches/bep_0007_embeddable_objects: ./ bloodhound_dashboard/
bloodhound_dashboard/bhdashboard/ bloodhound_dashboard/bhdashboard/widgets/
bloodhound_dashboard/bhdashboard/widgets/templates/ bloodhound_multip...
Author: ahorincar
Date: Sun Jul 28 17:22:23 2013
New Revision: 1507818
URL: http://svn.apache.org/r1507818
Log:
Merged trunk to bep_007_embeddable_objects branch
Added:
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/hooks.py
- copied unchanged from r1507814, bloodhound/trunk/bloodhound_multiproduct/tests/hooks.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_relations/bhrelations/tests/base.py
- copied unchanged from r1507814, bloodhound/trunk/bloodhound_relations/bhrelations/tests/base.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_relations/bhrelations/utils.py
- copied unchanged from r1507814, bloodhound/trunk/bloodhound_relations/bhrelations/utils.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/templates/bh_register.html
- copied unchanged from r1507814, bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_register.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/templates/bh_reset_password.html
- copied unchanged from r1507814, bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_reset_password.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/templates/bh_user_table.html
- copied unchanged from r1507814, bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_user_table.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/templates/bh_verify_email.html
- copied unchanged from r1507814, bloodhound/trunk/bloodhound_theme/bhtheme/templates/bh_verify_email.html
Removed:
bloodhound/branches/bep_0007_embeddable_objects/DISCLAIMER
Modified:
bloodhound/branches/bep_0007_embeddable_objects/ (props changed)
bloodhound/branches/bep_0007_embeddable_objects/NOTICE
bloodhound/branches/bep_0007_embeddable_objects/RELEASE_NOTES
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/web_ui.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/product.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_grid.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_product.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_relations.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_timeline.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/timeline.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/setup.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/dbcursor.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/env.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/hooks.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/model.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/ticket/web_ui.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/web_ui.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/setup.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/__init__.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/cursor.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/env.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/model.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/upgrade.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/web_ui.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_relations/bhrelations/api.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_relations/bhrelations/templates/manage.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_relations/bhrelations/tests/api.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_relations/bhrelations/tests/notification.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_relations/bhrelations/tests/search.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_relations/bhrelations/tests/validation.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_relations/bhrelations/tests/web_ui.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_relations/bhrelations/web_ui.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_relations/bhrelations/widgets/relations.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_relations/setup.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_search/bhsearch/tests/__init__.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_search/bhsearch/tests/api.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_search/bhsearch/tests/base.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_search/bhsearch/tests/index_with_whoosh.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_search/bhsearch/tests/query_parser.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_search/bhsearch/tests/query_suggestion.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_search/bhsearch/tests/real_index_view.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_search/bhsearch/tests/search_resources/base.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_search/bhsearch/tests/search_resources/changeset_search.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_search/bhsearch/tests/search_resources/milestone_search.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_search/bhsearch/tests/search_resources/ticket_search.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_search/bhsearch/tests/search_resources/wiki_search.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_search/bhsearch/tests/security.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_search/bhsearch/tests/web_ui.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_search/bhsearch/tests/whoosh_backend.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_search/setup.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/htdocs/bloodhound.css
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/htdocs/js/theme.js
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/templates/bh_about.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/templates/bh_account_details.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/templates/bh_admin_products.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/templates/bh_admin_users.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/templates/bh_browser.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/templates/bh_login.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/templates/bh_milestone_edit.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/templates/bh_prefs_account.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/templates/bh_ticket.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/templates/bh_timeline.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/templates/bh_wiki_edit.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/templates/bloodhound_theme.html
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/bhtheme/theme.py
bloodhound/branches/bep_0007_embeddable_objects/bloodhound_theme/setup.py
bloodhound/branches/bep_0007_embeddable_objects/doc/Bloodhound logo.svg
bloodhound/branches/bep_0007_embeddable_objects/installer/bloodhound_setup.py
bloodhound/branches/bep_0007_embeddable_objects/trac/ (props changed)
bloodhound/branches/bep_0007_embeddable_objects/trac/trac/wiki/tests/wiki-tests.txt
Propchange: bloodhound/branches/bep_0007_embeddable_objects/
------------------------------------------------------------------------------
Merged /bloodhound/trunk:r1497828-1507814
Modified: bloodhound/branches/bep_0007_embeddable_objects/NOTICE
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/NOTICE?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/NOTICE (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/NOTICE Sun Jul 28 17:22:23 2013
@@ -27,4 +27,5 @@ developed by Trent Richardson
LRU and LFU cache decorators - licensed under the PSF License
This product includes code (LRU and LFU cache decorators) developed by
-Raymond Hettinger (http://code.activestate.com/recipes/498245-lru-and-lfu-cache-decorators/)
+Raymond Hettinger
+(http://code.activestate.com/recipes/498245-lru-and-lfu-cache-decorators/)
Modified: bloodhound/branches/bep_0007_embeddable_objects/RELEASE_NOTES
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/RELEASE_NOTES?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/RELEASE_NOTES (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/RELEASE_NOTES Sun Jul 28 17:22:23 2013
@@ -1,3 +1,23 @@
+0.6
+
+ * Added support for multiple products.
+ * Added support for ticket relations.
+ * Numerous improvements to the BloodhoundSearch module.
+ * Redesigned the Ticket page.
+ * Implemented Bootstrap templates for Timeline and AccountManagerPlugin.
+
+ * Not fixed for this release:
+ * No major outstanding issues
+
+0.5.3
+
+ * Removed reference to BloodhoundSearch docs in setup script, which was causing installation to fail.
+ * Updated installation document so that site-packages are inherited in the virtualenv.
+
+ * Not fixed for this release:
+ * No major outstanding issues
+
+
0.5.2
* Updated this file with 0.5.1 changes.
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/web_ui.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/web_ui.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/web_ui.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/web_ui.py Sun Jul 28 17:22:23 2013
@@ -224,7 +224,8 @@ class DashboardModule(Component):
'args': ['Timeline', None, {'args': {}}]
},
'products': {
- 'args': ['Product', None, {'args': {'max': 3}}]
+ 'args': ['Product', None, {'args': {'max': 3,
+ 'cols': 2}}]
},
}
}
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/product.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/product.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/product.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/product.py Sun Jul 28 17:22:23 2013
@@ -48,12 +48,11 @@ class ProductWidget(WidgetBase):
"""Return a dictionary containing arguments specification for
the widget with specified name.
"""
- return {
- 'max' : {
- 'desc' : """Limit the number of products displayed""",
- 'type' : int
- },
- }
+ return {'max' : {'desc' : """Limit the number of products displayed""",
+ 'type' : int},
+ 'cols' : {'desc' : """Number of columns""",
+ 'type' : int}
+ }
get_widget_params = pretty_wrapper(get_widget_params, check_widget_name)
@@ -116,8 +115,8 @@ class ProductWidget(WidgetBase):
data = {}
req = context.req
title = ''
- params = ('max', )
- max_, = self.bind_params(name, options, *params)
+ params = ('max', 'cols')
+ max_, cols = self.bind_params(name, options, *params)
if not isinstance(req.perm.env, ProductEnvironment):
for p in Product.select(self.env):
@@ -136,6 +135,9 @@ class ProductWidget(WidgetBase):
data.setdefault('product_list', []).append(p)
title = _('Products')
+ data['colseq'] = itertools.cycle(xrange(cols - 1, -1, -1)) if cols \
+ else itertools.repeat(1)
+
return 'widget_product.html', \
{
'title': title,
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_grid.html
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_grid.html?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_grid.html (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_grid.html Sun Jul 28 17:22:23 2013
@@ -92,7 +92,7 @@
<!--! for the ticket listing -->
<py:when test="col in ('ticket', 'id')">
<td class="ticket" py:attrs="td_attrs">
- <a title="View ${row.resource.realm}" href="$row.href">#$cell.value</a>
+ <a title="View ${row.resource.realm}" href="${row.href if row.href else url_of(row.resource)}">${shortname_of(row.resource)}</a>
</td>
</py:when>
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_product.html
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_product.html?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_product.html (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_product.html Sun Jul 28 17:22:23 2013
@@ -22,60 +22,63 @@
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude">
<div py:if="product_list" class="row" id="products">
- <div py:for="p in product_list" class="span4">
- <div class="well product-well">
- <h4>
- ☆ <a href="${req.href.products(p.prefix)}">$p.name ($p.prefix)</a>
- <py:if test="p.owner_link">
- <br />
- <small>owned by
- <a href="$p.owner_link">${authorinfo(p._data['owner']) if p._data['owner'] else _('(nobody)')}</a>
- </small>
- </py:if>
- </h4>
-
- <h5>Milestones</h5>
- <py:choose test="">
- <py:when test="p.milestones">
- <ul class="subset">
- <li py:for="m in p.milestones">
- <a href="$m.url">$m.name<py:if test="m.ticket_count is not None"> ($m.ticket_count)</py:if></a>
- </li>
- </ul>
- </py:when>
- <py:otherwise>
- (No milestones for this product)
- </py:otherwise>
- </py:choose>
-
- <h5>Components</h5>
- <py:choose test="">
- <py:when test="p.components">
- <ul class="subset">
- <li py:for="c in p.components">
- <a href="$c.url">$c.name<py:if test="c.ticket_count is not None"> ($c.ticket_count)</py:if></a>
- </li>
- </ul>
- </py:when>
- <py:otherwise>
- (No components for this product)
- </py:otherwise>
- </py:choose>
-
- <h5>Versions</h5>
- <py:choose test="">
- <py:when test="p.versions">
- <ul class="subset">
- <li py:for="v in p.versions">
- <a href="$v.url">$v.name<py:if test="v.ticket_count is not None"> ($v.ticket_count)</py:if></a>
- </li>
- </ul>
- </py:when>
- <py:otherwise>
- (No versions for this product)
- </py:otherwise>
- </py:choose>
+ <py:for each="i, p in zip(colseq, product_list)">
+ <div class="span4">
+ <div class="well product-well">
+ <h4>
+ ☆ <a href="${req.href.products(p.prefix)}">$p.name ($p.prefix)</a>
+ <py:if test="p.owner_link">
+ <br />
+ <small>owned by
+ <a href="$p.owner_link">${authorinfo(p._data['owner']) if p._data['owner'] else _('(nobody)')}</a>
+ </small>
+ </py:if>
+ </h4>
+
+ <h5>Milestones</h5>
+ <py:choose test="">
+ <py:when test="p.milestones">
+ <ul class="subset">
+ <li py:for="m in p.milestones">
+ <a href="$m.url">$m.name<py:if test="m.ticket_count is not None"> ($m.ticket_count)</py:if></a>
+ </li>
+ </ul>
+ </py:when>
+ <py:otherwise>
+ (No milestones for this product)
+ </py:otherwise>
+ </py:choose>
+
+ <h5>Components</h5>
+ <py:choose test="">
+ <py:when test="p.components">
+ <ul class="subset">
+ <li py:for="c in p.components">
+ <a href="$c.url">$c.name<py:if test="c.ticket_count is not None"> ($c.ticket_count)</py:if></a>
+ </li>
+ </ul>
+ </py:when>
+ <py:otherwise>
+ (No components for this product)
+ </py:otherwise>
+ </py:choose>
+
+ <h5>Versions</h5>
+ <py:choose test="">
+ <py:when test="p.versions">
+ <ul class="subset">
+ <li py:for="v in p.versions">
+ <a href="$v.url">$v.name<py:if test="v.ticket_count is not None"> ($v.ticket_count)</py:if></a>
+ </li>
+ </ul>
+ </py:when>
+ <py:otherwise>
+ (No versions for this product)
+ </py:otherwise>
+ </py:choose>
+ </div>
</div>
- </div>
+ <div class="clearfix" py:if="i == 0" />
+ </py:for>
</div>
</div>
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_relations.html
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_relations.html?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_relations.html (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_relations.html Sun Jul 28 17:22:23 2013
@@ -1,64 +1,64 @@
-<!--!
- Licensed to the Apache Software Foundation (ASF) under one
- or more contributor license agreements. See the NOTICE file
- distributed with this work for additional information
- regarding copyright ownership. The ASF licenses this file
- to you under the Apache License, Version 2.0 (the
- "License"); you may not use this file except in compliance
- with the License. You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing,
- software distributed under the License is distributed on an
- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- KIND, either express or implied. See the License for the
- specific language governing permissions and limitations
- under the License.
--->
-
-<div
- xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://genshi.edgewall.org/"
- xmlns:xi="http://www.w3.org/2001/XInclude">
-
- <py:choose test="">
- <py:when test="relations">
- <table class="table table-condensed table-bordered">
- <thead>
- <tr>
- <th>Type</th><th>Product</th><th>Ticket</th><th>Comment</th><th>Author</th><th class="hidden-phone">Changed</th>
- </tr>
- </thead>
-
- <tbody py:for="relgroup,items in relations.iteritems()">
- <tr py:for="item in items" class="relation">
- <td>${relgroup if items.index(item) == 0 else None}</td>
- <td>
- <a href="${href.products(item['destticket'].env.product.prefix)}">
- <span class="hidden-phone">${item['destticket'].env.product.name} (${item['destticket'].env.product.prefix})</span>
- <span class="visible-phone">${item['destticket'].env.product.prefix}</span>
- </a>
- </td>
- <td><a href="${item['desthref']}">#${item['destticket'].id}</a> - ${item['destticket'].summary}</td>
- <td>$item.comment</td>
- <td>$item.author</td>
- <td class="hidden-phone">${pretty_dateinfo(item.when)}</td>
- </tr>
- </tbody>
- </table>
- </py:when>
- <py:otherwise>
- <div class="alert alert-info">
- No defined relations for this ticket.
- </div>
- </py:otherwise>
- </py:choose>
-
- <div class="btn-group">
- <form method="get" action="${href.ticket(ticket.id, 'relations')}">
- <button type="submit" class="btn" id="manage-relations"><i class="icon-retweet"></i> Manage relations</button>
- </form>
- </div>
-</div>
-
+<!--!
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+<div
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+ <py:choose test="">
+ <py:when test="relations">
+ <table class="table table-condensed table-bordered">
+ <thead>
+ <tr>
+ <th>Type</th><th>Product</th><th>Ticket</th><th>Comment</th><th>Author</th><th class="hidden-phone">Changed</th>
+ </tr>
+ </thead>
+
+ <tbody py:for="relgroup,items in relations.iteritems()">
+ <tr py:for="item in items" class="relation">
+ <td>${relgroup if items.index(item) == 0 else None}</td>
+ <td>
+ <a href="${href.products(item['destticket'].env.product.prefix)}">
+ <span class="hidden-phone">${item['destticket'].env.product.name} (${item['destticket'].env.product.prefix})</span>
+ <span class="visible-phone">${item['destticket'].env.product.prefix}</span>
+ </a>
+ </td>
+ <td><a href="${item['desthref']}">#${item['destticket'].id}</a> - ${item['destticket'].summary}</td>
+ <td>$item.comment</td>
+ <td>$item.author</td>
+ <td class="hidden-phone">${pretty_dateinfo(item.when)}</td>
+ </tr>
+ </tbody>
+ </table>
+ </py:when>
+ <py:otherwise>
+ <div class="alert alert-info">
+ No defined relations for this ticket.
+ </div>
+ </py:otherwise>
+ </py:choose>
+
+ <div class="btn-group">
+ <form method="get" action="${href.ticket(ticket.id, 'relations')}">
+ <button type="submit" class="btn" id="manage-relations"><i class="icon-retweet"></i> Manage relations</button>
+ </form>
+ </div>
+</div>
+
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_timeline.html
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_timeline.html?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_timeline.html (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/templates/widget_timeline.html Sun Jul 28 17:22:23 2013
@@ -21,7 +21,9 @@
xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude"
- py:with="today = format_date(today); yesterday = format_date(yesterday)"
+ py:with="now = datetime.now(req.tz);
+ today = format_date(now);
+ yesterday = format_date(now - timedelta(days=1))"
py:choose="">
<table py:when="events"
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/timeline.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/timeline.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/timeline.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/bhdashboard/widgets/timeline.py Sun Jul 28 17:22:23 2013
@@ -39,7 +39,7 @@ from trac.ticket.api import TicketSystem
from trac.ticket.model import Ticket
from trac.ticket.web_ui import TicketModule
from trac.util.datefmt import utc
-from trac.util.translation import _
+from trac.util.translation import _, tag_
from trac.web.chrome import add_stylesheet
from bhdashboard.api import DateField, EnumField, ListField
@@ -50,6 +50,7 @@ from bhdashboard.util import WidgetBase,
__metaclass__ = type
+
class ITimelineEventsFilter(Interface):
"""Filter timeline events displayed in a rendering context
"""
@@ -71,11 +72,12 @@ class ITimelineEventsFilter(Interface):
`NotImplemented` if the filter doesn't care about it.
"""
+
class TimelineWidget(WidgetBase):
"""Display activity feed.
"""
default_count = IntOption('widget_activity', 'limit', 25,
- """Maximum number of items displayed by default""")
+ """Maximum number of items displayed by default""")
event_filters = ExtensionPoint(ITimelineEventsFilter)
@@ -101,36 +103,36 @@ class TimelineWidget(WidgetBase):
the widget with specified name.
"""
return {
- 'from' : {
- 'desc' : """Display events before this date""",
- 'type' : DateField(), # TODO: Custom datetime format
- },
- 'daysback' : {
- 'desc' : """Event time window""",
- 'type' : int,
- },
- 'precision' : {
- 'desc' : """Time precision""",
- 'type' : EnumField('second', 'minute', 'hour')
- },
- 'doneby' : {
- 'desc' : """Filter events related to user""",
- },
- 'filters' : {
- 'desc' : """Event filters""",
- 'type' : ListField()
- },
- 'max' : {
- 'desc' : """Limit the number of events displayed""",
- 'type' : int
- },
- 'realm' : {
- 'desc' : """Resource realm. Used to filter events""",
- },
- 'id' : {
- 'desc' : """Resource ID. Used to filter events""",
- },
- }
+ 'from': {
+ 'desc': """Display events before this date""",
+ 'type': DateField(), # TODO: Custom datetime format
+ },
+ 'daysback': {
+ 'desc': """Event time window""",
+ 'type': int,
+ },
+ 'precision': {
+ 'desc': """Time precision""",
+ 'type': EnumField('second', 'minute', 'hour')
+ },
+ 'doneby': {
+ 'desc': """Filter events related to user""",
+ },
+ 'filters': {
+ 'desc': """Event filters""",
+ 'type': ListField()
+ },
+ 'max': {
+ 'desc': """Limit the number of events displayed""",
+ 'type': int
+ },
+ 'realm': {
+ 'desc': """Resource realm. Used to filter events""",
+ },
+ 'id': {
+ 'desc': """Resource ID. Used to filter events""",
+ },
+ }
get_widget_params = pretty_wrapper(get_widget_params, check_widget_name)
def render_widget(self, name, context, options):
@@ -140,28 +142,34 @@ class TimelineWidget(WidgetBase):
req = context.req
try:
timemdl = self.env[TimelineModule]
- if timemdl is None :
+ admin_page = tag.a(_("administration page."),
+ title=_("Plugin Administration Page"),
+ href=req.href.admin('general/plugin'))
+ if timemdl is None:
return 'widget_alert.html', {
'title': _("Activity"),
'data': {
'msglabel': "Warning",
- 'msgbody': _("TimelineWidget is disabled because the "
- "Timeline component is not available. "
- "Is the component disabled?"),
+ 'msgbody':
+ tag_("The TimelineWidget is disabled because the "
+ "Timeline component is not available. "
+ "Is the component disabled? "
+ "You can enable from the %(page)s",
+ page=admin_page),
'dismiss': False,
}
}, context
- params = ('from', 'daysback', 'doneby', 'precision', 'filters', \
- 'max', 'realm', 'id')
+ params = ('from', 'daysback', 'doneby', 'precision', 'filters',
+ 'max', 'realm', 'id')
start, days, user, precision, filters, count, realm, rid = \
- self.bind_params(name, options, *params)
+ self.bind_params(name, options, *params)
if context.resource.realm == 'ticket':
if days is None:
# calculate a long enough time daysback
ticket = Ticket(self.env, context.resource.id)
- ticketage = datetime.now(utc) - ticket.time_created
- days = ticketage.days + 1
+ ticket_age = datetime.now(utc) - ticket.time_created
+ days = ticket_age.days + 1
if count is None:
# ignore short count for ticket feeds
count = 0
@@ -170,12 +178,12 @@ class TimelineWidget(WidgetBase):
fakereq = dummy_request(self.env, req.authname)
fakereq.args = {
- 'author' : user or '',
- 'daysback' : days or '',
- 'max' : count,
- 'precision' : precision,
- 'user' : user
- }
+ 'author': user or '',
+ 'daysback': days or '',
+ 'max': count,
+ 'precision': precision,
+ 'user': user
+ }
if filters:
fakereq.args.update(dict((k, True) for k in filters))
if start is not None:
@@ -191,12 +199,12 @@ class TimelineWidget(WidgetBase):
wcontext.req = req
else:
self.log.warning("TimelineWidget: Resource %s not found",
- resource)
+ resource)
# FIXME: Filter also if existence check is not conclusive ?
if resource_exists(self.env, wcontext.resource):
module = FilteredTimeline(self.env, wcontext)
- self.log.debug('Filtering timeline events for %s', \
- wcontext.resource)
+ self.log.debug('Filtering timeline events for %s',
+ wcontext.resource)
else:
module = timemdl
data = module.process_request(fakereq)[1]
@@ -207,23 +215,19 @@ class TimelineWidget(WidgetBase):
else:
merge_links(srcreq=fakereq, dstreq=req,
exclude=["stylesheet", "alternate"])
- data['today'] = today = datetime.now(req.tz)
- data['yesterday'] = today - timedelta(days=1)
if 'context' in data:
# Needed for abbreviated messages in widget events (#340)
wcontext.set_hints(**(data['context']._hints or {}))
data['context'] = wcontext
- return 'widget_timeline.html', \
- {
- 'title' : _('Activity'),
- 'data' : data,
- 'altlinks' : fakereq.chrome.get('links',
- {}).get('alternate')
- }, \
- context
+ return 'widget_timeline.html', {
+ 'title': _('Activity'),
+ 'data': data,
+ 'altlinks': fakereq.chrome.get('links', {}).get('alternate')
+ }, context
render_widget = pretty_wrapper(render_widget, check_widget_name)
+
class FilteredTimeline:
"""This is a class (not a component ;) aimed at overriding some parts of
TimelineModule without patching it in order to inject code needed to filter
@@ -250,7 +254,7 @@ class FilteredTimeline:
@property
def max_daysback(self):
return (-1 if self.context.resource.realm == 'ticket'
- else self._max_daysback)
+ else self._max_daysback)
@property
def event_providers(self):
@@ -268,11 +272,12 @@ class FilteredTimeline:
if isinstance(value, MethodType):
raise AttributeError()
except AttributeError:
- raise AttributeError("'%s' object has no attribute '%s'" % \
- (self.__class__.__name__, attrnm))
+ raise AttributeError("'%s' object has no attribute '%s'"
+ % (self.__class__.__name__, attrnm))
else:
return value
+
class TimelineFilterAdapter:
"""Wrapper class used to filter timeline event streams transparently.
Therefore it is compatible with `ITimelineEventProvider` interface
@@ -289,9 +294,9 @@ class TimelineFilterAdapter:
def get_timeline_filters(self, req):
gen = self.provider.get_timeline_filters(req)
- if (self.context.resource.realm == 'ticket' and
- isinstance(self.provider, TicketModule) and
- 'TICKET_VIEW' in req.perm):
+ if self.context.resource.realm == 'ticket' and \
+ isinstance(self.provider, TicketModule) and \
+ 'TICKET_VIEW' in req.perm:
# ensure ticket_details appears once if this is a query on a ticket
gen = list(gen)
if not [g for g in gen if g[0] == 'ticket_details']:
@@ -305,16 +310,16 @@ class TimelineFilterAdapter:
"""
filters_map = TimelineWidget(self.env).filters_map
evfilters = filters_map.get(self.provider.__class__.__name__, []) + \
- filters_map.get(None, [])
+ filters_map.get(None, [])
self.log.debug('Applying filters %s for %s against %s', evfilters,
- self.context.resource, self.provider)
+ self.context.resource, self.provider)
if evfilters:
for event in self.provider.get_timeline_events(
req, start, stop, filters):
match = False
for f in evfilters:
new_event = f.filter_event(self.context, self.provider,
- event, filters)
+ event, filters)
if new_event is None:
event = None
match = True
@@ -338,11 +343,12 @@ class TimelineFilterAdapter:
try:
value = getattr(self.provider, attrnm)
except AttributeError:
- raise AttributeError("'%s' object has no attribute '%s'" % \
- (self.__class__.__name__, attrnm))
+ raise AttributeError("'%s' object has no attribute '%s'"
+ % (self.__class__.__name__, attrnm))
else:
return value
+
class TicketFieldTimelineFilter(Component):
"""A class filtering ticket events related to a given resource
associated via ticket fields.
@@ -355,8 +361,8 @@ class TicketFieldTimelineFilter(Componen
"""
field_names = getattr(self, '_fields', None)
if field_names is None:
- self._fields = set(f['name'] \
- for f in TicketSystem(self.env).get_ticket_fields())
+ self._fields = set(f['name'] for f in
+ TicketSystem(self.env).get_ticket_fields())
return self._fields
# ITimelineEventsFilter methods
@@ -379,7 +385,7 @@ class TicketFieldTimelineFilter(Componen
ticket_ids = event[3][0]
except:
self.log.exception('Unknown ticket event %s ... [SKIP]',
- event)
+ event)
return None
if not isinstance(ticket_ids, list):
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/setup.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/setup.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/setup.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_dashboard/setup.py Sun Jul 28 17:22:23 2013
@@ -35,6 +35,7 @@ versions = [
(0, 4, 0),
(0, 5, 0),
(0, 6, 0),
+ (0, 7, 0),
]
latest = '.'.join(str(x) for x in versions[-1])
@@ -75,13 +76,6 @@ cats = [
"Topic :: Software Development :: Widget Sets"
]
-# Be compatible with older versions of Python
-from sys import version
-if version < '2.2.3':
- from distutils.dist import DistributionMetadata
- DistributionMetadata.classifiers = None
- DistributionMetadata.download_url = None
-
# Add the change log to the package description.
chglog = None
try:
@@ -136,7 +130,7 @@ setup(
description=DESC.split('\n', 1)[0],
author = "Apache Bloodhound",
license = "Apache License v2",
- url = "http://incubator.apache.org/bloodhound/",
+ url = "https://bloodhound.apache.org/",
requires = ['trac'],
tests_require = ['dutest>=0.2.4', 'TracXMLRPC'],
install_requires = [
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/dbcursor.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/dbcursor.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/dbcursor.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/dbcursor.py Sun Jul 28 17:22:23 2013
@@ -41,7 +41,7 @@ TRANSLATE_TABLES = ['system',
'permission',
'wiki',
'report',
- ]
+ ]
PRODUCT_COLUMN = 'product'
GLOBAL_PRODUCT = ''
@@ -533,6 +533,11 @@ class BloodhoundProductSQLTranslate(obje
ptoken = self._token_next(columns_token, ptoken)
last_token = ptoken
while ptoken:
+ if isinstance(ptoken, Types.IdentifierList):
+ if any(i.get_name() == 'product'
+ for i in ptoken.get_identifiers()
+ if isinstance(i, Types.Identifier)):
+ return True
last_token = ptoken
ptoken = self._token_next(columns_token, ptoken)
if not last_token or \
@@ -540,7 +545,7 @@ class BloodhoundProductSQLTranslate(obje
raise Exception("Invalid INSERT statement, unable to find column parenthesis end")
for keyword in [',', ' ', self._product_column]:
self._token_insert_before(columns_token, last_token, Types.Token(Tokens.Keyword, keyword))
- return
+ return False
def insert_extra_column_value(tablename, ptoken, before_token):
if tablename in self._translate_tables:
for keyword in [',', "'", self._product_prefix, "'"]:
@@ -548,6 +553,7 @@ class BloodhoundProductSQLTranslate(obje
return
tablename = None
table_name_token = self._token_next(parent, token)
+ has_product_column = False
if isinstance(table_name_token, Types.Function):
token = self._token_first(table_name_token)
if isinstance(token, Types.Identifier):
@@ -556,7 +562,7 @@ class BloodhoundProductSQLTranslate(obje
if columns_token.match(Tokens.Keyword, 'VALUES'):
token = columns_token
else:
- insert_extra_column(tablename, columns_token)
+ has_product_column = insert_extra_column(tablename, columns_token)
token = self._token_next(parent, table_name_token)
else:
tablename = table_name_token.value
@@ -564,9 +570,11 @@ class BloodhoundProductSQLTranslate(obje
if columns_token.match(Tokens.Keyword, 'VALUES'):
token = columns_token
else:
- insert_extra_column(tablename, columns_token)
+ has_product_column = insert_extra_column(tablename, columns_token)
token = self._token_next(parent, columns_token)
- if token.match(Tokens.Keyword, 'VALUES'):
+ if has_product_column:
+ pass # INSERT already has product, no translation needed
+ elif token.match(Tokens.Keyword, 'VALUES'):
separators = [',', '(', ')']
token = self._token_next(parent, token)
while token:
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/env.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/env.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/env.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/env.py Sun Jul 28 17:22:23 2013
@@ -405,7 +405,7 @@ class ProductEnvironment(Component, Comp
"""
return ''
- base_url = Option('trac', 'base_url', '',
+ _base_url = Option('trac', 'base_url', '',
"""Reference URL for the Trac deployment.
This is the base URL that will be used when producing
@@ -413,7 +413,14 @@ class ProductEnvironment(Component, Comp
context, like for example when inserting URLs pointing to Trac
resources in notification e-mails.""")
- base_url_for_redirect = BoolOption('trac', 'use_base_url_for_redirect',
+ @property
+ def base_url(self):
+ base_url = self._base_url
+ if base_url == self.parent.base_url:
+ return ''
+ return base_url
+
+ _base_url_for_redirect = BoolOption('trac', 'use_base_url_for_redirect',
False,
"""Optionally use `[trac] base_url` for redirects.
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/hooks.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/hooks.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/hooks.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/hooks.py Sun Jul 28 17:22:23 2013
@@ -30,7 +30,10 @@ from trac.web.href import Href
from trac.web.main import RequestWithSession
PRODUCT_RE = re.compile(r'^/products(?:/(?P<pid>[^/]*)(?P<pathinfo>.*))?')
-REDIRECT_DEFAULT_RE = re.compile(r'^/(?P<section>milestone|roadmap|query|report|newticket|ticket|qct|timeline|(raw-|zip-)?attachment|diff|batchmodify|search)(?P<pathinfo>.*)')
+REDIRECT_DEFAULT_RE = \
+ re.compile(r'^/(?P<section>milestone|roadmap|query|report|newticket|'
+ r'ticket|qct|timeline|diff|batchmodify|search|'
+ r'(raw-|zip-)?attachment/(ticket|milestone))(?P<pathinfo>.*)')
class MultiProductEnvironmentFactory(EnvironmentFactoryBase):
@@ -100,10 +103,11 @@ class ProductizedHref(Href):
self._global_href = global_href
def __call__(self, *args, **kwargs):
- if args:
+ if args and isinstance(args[0], basestring):
if args[0] in self.PATHS_NO_TRANSFORM or \
- (len(args) == 1 and args[0] == 'admin') or \
- filter(lambda x: args[0].startswith(x), self.STATIC_PREFIXES):
+ (len(args) == 1 and args[0] == 'admin') or \
+ filter(lambda x: args[0].startswith(x),
+ self.STATIC_PREFIXES):
return self._global_href(*args, **kwargs)
return self.super.__call__(*args, **kwargs)
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/model.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/model.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/model.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/model.py Sun Jul 28 17:22:23 2013
@@ -81,7 +81,11 @@ class Product(ModelBase):
@classmethod
def get_tickets(cls, env, product=''):
"""Retrieve all tickets associated with the product."""
- q = Query.from_string(env, 'product=%s' % product)
+ from multiproduct.ticket.query import ProductQuery
+ from multiproduct.env import ProductEnvironment
+ if not product and isinstance(env, ProductEnvironment):
+ product = env.product.prefix
+ q = ProductQuery.from_string(env, 'product=%s' % product)
return q.execute()
class ProductResourceMap(ModelBase):
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/ticket/web_ui.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/ticket/web_ui.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/ticket/web_ui.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/ticket/web_ui.py Sun Jul 28 17:22:23 2013
@@ -54,8 +54,7 @@ class ProductTicketModule(TicketModule):
productid = req.args.get('productid','')
if ticketid:
- if (req.path_info == '/products/' + productid + '/newticket' or
- req.path_info == '/products'):
+ if req.path_info in ('/newticket', '/products'):
raise TracError(_("id can't be set for a new ticket request."))
ticket = Ticket(self.env, ticketid)
if productid and ticket['product'] != productid:
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/web_ui.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/web_ui.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/web_ui.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/multiproduct/web_ui.py Sun Jul 28 17:22:23 2013
@@ -99,7 +99,6 @@ class ProductModule(Component):
def _render_list(self, req):
"""products list"""
- print "Rendering list"
products = [p for p in Product.select(self.env)
if 'PRODUCT_VIEW' in req.perm(Neighborhood('product',
p.prefix))]
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/setup.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/setup.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/setup.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/setup.py Sun Jul 28 17:22:23 2013
@@ -17,15 +17,17 @@
# under the License.
"""setup for multi product plugin"""
+import sys
+from pkg_resources import parse_version
from setuptools import setup
setup(
name = 'BloodhoundMultiProduct',
- version = '0.6.0',
+ version = '0.7.0',
description = "Multiproduct support for Apache(TM) Bloodhound.",
author = "Apache Bloodhound",
license = "Apache License v2",
- url = "http://incubator.apache.org/bloodhound/",
+ url = "https://bloodhound.apache.org/",
packages = ['multiproduct', 'multiproduct.ticket', 'tests',],
package_data = {'multiproduct' : ['templates/*.html',]},
entry_points = {'trac.plugins': [
@@ -37,5 +39,6 @@ setup(
'multiproduct.web_ui = multiproduct.web_ui',
],},
test_suite='tests.test_suite',
+ tests_require=['unittest2' if parse_version(sys.version) < parse_version('2.7') else '']
)
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/__init__.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/__init__.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/__init__.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/__init__.py Sun Jul 28 17:22:23 2013
@@ -79,4 +79,5 @@ class TestLoader(unittest.TestLoader):
def test_suite():
return TestLoader().discover_package('tests', pattern='*.py')
-
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/cursor.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/cursor.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/cursor.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/db/cursor.py Sun Jul 28 17:22:23 2013
@@ -987,7 +987,14 @@ data = {
"""create temporary table table_old as select * from table""",
"""create temporary table "PRODUCT_table_old" as select * from (SELECT * FROM "PRODUCT_table") AS table""",
)
- ]
+ ],
+ # insert with specified product (#601)
+ 'insert_with_product': [
+ (
+"""INSERT INTO ticket (summary, product) VALUES ('S', 'swlcu')""",
+"""INSERT INTO ticket (summary, product) VALUES ('S', 'swlcu')"""
+ ),
+ ],
}
@@ -1045,6 +1052,9 @@ class DbCursorTestCase(unittest.TestCase
def test_lowercase_tokens(self):
self._run_test('lowercase_tokens')
+ def test_insert_with_product(self):
+ self._run_test('insert_with_product')
+
if __name__ == '__main__':
unittest.main()
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/env.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/env.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/env.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/env.py Sun Jul 28 17:22:23 2013
@@ -42,6 +42,7 @@ from trac.tests.env import EnvironmentTe
from trac.ticket.report import ReportModule
from trac.ticket.web_ui import TicketModule
from trac.util.text import to_unicode
+from trac.web.href import Href
from multiproduct.api import MultiProductSystem
from multiproduct.env import ProductEnvironment
@@ -208,35 +209,38 @@ class MultiproductTestCase(unittest.Test
env._log_handler.close()
del env._log_handler
- def _load_product_from_data(self, env, prefix):
+ @classmethod
+ def _load_product_from_data(cls, env, prefix):
r"""Ensure test product with prefix is loaded
"""
# TODO: Use fixtures implemented in #314
- product_data = self.PRODUCT_DATA[prefix]
+ product_data = cls.PRODUCT_DATA[prefix]
prefix = to_unicode(prefix)
product = Product(env)
product._data.update(product_data)
product.insert()
- def _upgrade_mp(self, env):
+ @classmethod
+ def _upgrade_mp(cls, env):
r"""Apply multi product upgrades
"""
# Do not break wiki parser ( see #373 )
env.disable_component(TicketModule)
env.disable_component(ReportModule)
- self.mpsystem = MultiProductSystem(env)
+ mpsystem = MultiProductSystem(env)
try:
- self.mpsystem.upgrade_environment(env.db_transaction)
+ mpsystem.upgrade_environment(env.db_transaction)
except OperationalError:
# Database is upgraded, but database version was deleted.
# Complete the upgrade by inserting default product.
- self.mpsystem._insert_default_product(env.db_transaction)
+ mpsystem._insert_default_product(env.db_transaction)
# assume that the database schema has been upgraded, enable
# multi-product schema support in environment
env.enable_multiproduct_schema(True)
- def _load_default_data(self, env):
+ @classmethod
+ def _load_default_data(cls, env):
r"""Initialize environment with default data by respecting
values set in system table.
"""
@@ -555,9 +559,11 @@ class ProductEnvHrefTestCase(Multiproduc
def setUp(self):
self._mp_setup()
self.env.path = '/path/to/env'
+ self.env.abs_href = Href('http://globalenv.com/trac.cgi')
url_pattern = getattr(getattr(self, self._testMethodName).im_func,
'product_base_url', '')
self.env.config.set('multiproduct', 'product_base_url', url_pattern)
+ self.env.config.set('trac', 'base_url', 'http://globalenv.com/trac.cgi')
self.product_env = ProductEnvironment(self.env, self.default_product)
def tearDown(self):
@@ -582,21 +588,28 @@ class ProductEnvHrefTestCase(Multiproduc
def test_href_sibling_paths(self):
"""Test product base URL at sibling paths
"""
- self.assertEqual('http://example.org/trac.cgi/path/to/bloodhound/tp1',
+ self.assertEqual('http://globalenv.com/trac.cgi/path/to/bloodhound/tp1',
self.product_env.abs_href())
@product_base_url('/$(envname)s/$(prefix)s')
def test_href_inherit_sibling_paths(self):
"""Test product base URL at sibling paths inheriting configuration.
"""
- self.assertEqual('http://example.org/trac.cgi/env/tp1',
+ self.assertEqual('http://globalenv.com/trac.cgi/env/tp1',
+ self.product_env.abs_href())
+
+ @product_base_url('')
+ def test_href_default(self):
+ """Test product base URL is to a default
+ """
+ self.assertEqual('http://globalenv.com/trac.cgi/products/tp1',
self.product_env.abs_href())
@product_base_url('/products/$(prefix)s')
def test_href_embed(self):
"""Test default product base URL /products/prefix
"""
- self.assertEqual('http://example.org/trac.cgi/products/tp1',
+ self.assertEqual('http://globalenv.com/trac.cgi/products/tp1',
self.product_env.abs_href())
@product_base_url('http://$(envname)s.tld/bh/$(prefix)s')
@@ -605,6 +618,46 @@ class ProductEnvHrefTestCase(Multiproduc
"""
self.assertEqual('http://env.tld/bh/tp1', self.product_env.abs_href())
+ @product_base_url('http://$(prefix)s.$(envname)s.tld/')
+ def test_product_href_uses_multiproduct_product_base_url(self):
+ """Test that [multiproduct] product_base_url is used to compute
+ abs_href for the product environment when [trac] base_url for
+ the product environment is an empty string (the default).
+ """
+ # Global URLs
+ self.assertEqual('http://globalenv.com/trac.cgi', self.env.base_url)
+ self.assertEqual('http://globalenv.com/trac.cgi', self.env.abs_href())
+
+ # Product URLs
+ self.assertEqual('', self.product_env.base_url)
+ self.assertEqual('http://tp1.env.tld', self.product_env.abs_href())
+
+ @product_base_url('http://$(prefix)s.$(envname)s.tld/')
+ def test_product_href_uses_products_base_url(self):
+ """Test that [trac] base_url for the product environment is used to
+ compute abs_href for the product environment when [trac] base_url
+ for the product environment is different than [trac] base_url for
+ the global environment.
+ """
+ self.product_env.config.set('trac', 'base_url', 'http://productenv.com')
+ self.product_env.config.save()
+
+ self.assertEqual('http://productenv.com', self.product_env.base_url)
+ self.assertEqual('http://productenv.com', self.product_env.abs_href())
+
+ @product_base_url('http://$(prefix)s.$(envname)s.tld/')
+ def test_product_href_global_and_product_base_urls_same(self):
+ """Test that [multiproduct] product_base_url is used to compute
+ abs_href for the product environment when [trac] base_url is the same
+ for the product and global environment.
+ """
+ self.product_env.config.set('trac', 'base_url',
+ self.env.config.get('trac', 'base_url'))
+ self.product_env.config.save()
+
+ self.assertEqual('', self.product_env.base_url)
+ self.assertEqual('http://tp1.env.tld', self.product_env.abs_href())
+
product_base_url = staticmethod(product_base_url)
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/model.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/model.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/model.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/model.py Sun Jul 28 17:22:23 2013
@@ -30,11 +30,16 @@ from sqlite3 import OperationalError
from trac.test import EnvironmentStub
from trac.core import TracError
+from trac.ticket.model import Ticket
+from multiproduct.env import ProductEnvironment
from multiproduct.model import Product
+from bhdashboard.model import ModelBase
+
from multiproduct.api import MultiProductSystem
from trac.tests.resource import TestResourceChangeListener
+
class ProductTestCase(unittest.TestCase):
"""Unit tests covering the Product model"""
INITIAL_PREFIX = 'tp'
@@ -56,7 +61,8 @@ class ProductTestCase(unittest.TestCase)
self.default_data = {'prefix':self.INITIAL_PREFIX,
'name':self.INITIAL_NAME,
'description':self.INITIAL_DESCRIPTION}
-
+
+ self.global_env = self.env
self.product = Product(self.env)
self.product._data.update(self.default_data)
self.product.insert()
@@ -205,7 +211,6 @@ class ProductTestCase(unittest.TestCase)
"""ensure that that insert method works when _meta does not specify
unique fields when inserting more than one ProductResourceMap instances
"""
- from multiproduct.model import ModelBase
class TestModel(ModelBase):
"""A test model with no unique_fields"""
_meta = {'table_name': 'bloodhound_testmodel',
@@ -253,6 +258,33 @@ class ProductTestCase(unittest.TestCase)
self.assertIsInstance(self.listener.resource, Product)
self.assertEqual(self.INITIAL_PREFIX, self.prefix)
+ def test_get_tickets(self):
+ for pdata in (
+ {'prefix': 'p2', 'name':'product, too', 'description': ''},
+ {'prefix': 'p3', 'name':'strike three', 'description': ''},
+ ):
+ num_tickets = 5
+ product = Product(self.global_env)
+ product._data.update(pdata)
+ product.insert()
+ self.env = ProductEnvironment(self.global_env, product)
+ for i in range(num_tickets):
+ ticket = Ticket(self.env)
+ ticket['summary'] = 'hello ticket #%s-%d' % (product.prefix, i)
+ ticket['reporter'] = 'admin'
+ tid = ticket.insert()
+
+ # retrieve tickets using both global and product scope
+ tickets_from_global = [(t['product'], t['id']) for t in
+ Product.get_tickets(self.global_env, product.prefix)]
+ self.assertEqual(len(tickets_from_global), num_tickets)
+ tickets_from_product = [(t['product'], t['id']) for t in
+ Product.get_tickets(self.env)]
+ self.assertEqual(len(tickets_from_product), num_tickets)
+ # both lists should contain same elements
+ intersection = set(tickets_from_global) & set(tickets_from_product)
+ self.assertEqual(len(intersection), num_tickets)
+
def suite():
test_suite = unittest.TestSuite()
test_suite.addTest(unittest.makeSuite(ProductTestCase, 'test'))
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/upgrade.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/upgrade.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/upgrade.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/upgrade.py Sun Jul 28 17:22:23 2013
@@ -190,7 +190,7 @@ class EnvironmentUpgradeTestCase(unittes
% (table, len(rows), 6, rows))
for table in ('permission',):
# Permissions also hold rows for global product.
- rows = db("SELECT * FROM %s" % table)
+ rows = db("SELECT * FROM %s WHERE username='x'" % table)
self.assertEqual(
len(rows), 7,
"Wrong number of lines in %s (%d instead of %d)\n%s"
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/web_ui.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/web_ui.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/web_ui.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_multiproduct/tests/web_ui.py Sun Jul 28 17:22:23 2013
@@ -148,23 +148,6 @@ class ProductModuleTestCase(RequestHandl
"Unexpected product prefix")
self.assertEqual(self.expectedPathInfo, req.args['pathinfo'],
"Unexpected sub path")
-
- def test_product_pathinfo_warning(self):
- spy = self.global_env[TestRequestSpy]
- self.assertIsNot(None, spy)
-
- req = self._get_request_obj(self.global_env)
- req.authname = 'testuser'
- req.environ['PATH_INFO'] = '/products/PREFIX/some/path'
- self.expectedPrefix = 'PREFIX'
- self.expectedPathInfo = '/some/path'
- spy.testProcessing = lambda *args, **kwargs: None
-
- with self.assertRaises(HTTPNotFound) as test_cm:
- self._dispatch(req, self.global_env)
-
- self.assertEqual('Unable to render product page. Wrong setup ?',
- test_cm.exception.detail)
def test_product_list(self):
spy = self.global_env[TestRequestSpy]
@@ -216,13 +199,6 @@ class ProductModuleTestCase(RequestHandl
spy = self.global_env[TestRequestSpy]
self.assertIsNot(None, spy)
- # Missing product
- req = self._get_request_obj(self.global_env)
- req.authname = 'testuser'
- req.environ['PATH_INFO'] = '/products/missing'
-
- real_prefix = None
-
def assert_product_view(req, template, data, content_type):
self.assertEquals('product_view.html', template)
self.assertIs(None, content_type)
@@ -238,11 +214,6 @@ class ProductModuleTestCase(RequestHandl
spy.testProcessing = assert_product_view
- self.expectedPrefix = 'missing'
- self.expectedPathInfo = ''
- with self.assertRaises(RequestDone):
- self._dispatch(req, self.global_env)
-
# Existing product
req = self._get_request_obj(self.global_env)
req.authname = 'testuser'
@@ -254,6 +225,36 @@ class ProductModuleTestCase(RequestHandl
with self.assertRaises(RequestDone):
self._dispatch(req, self.global_env)
+ def test_missing_product(self):
+ spy = self.global_env[TestRequestSpy]
+ self.assertIsNot(None, spy)
+
+ mps = MultiProductSystem(self.global_env)
+ def assert_product_list(req, template, data, content_type):
+ self.assertEquals('product_list.html', template)
+ self.assertIs(None, content_type)
+ self.assertEquals([mps.default_product_prefix,
+ self.default_product],
+ [p.prefix for p in data.get('products')])
+ self.assertTrue('context' in data)
+ ctx = data['context']
+ self.assertEquals('product', ctx.resource.realm)
+ self.assertEquals(None, ctx.resource.id)
+
+ spy.testProcessing = assert_product_list
+
+ # Missing product
+ req = self._get_request_obj(self.global_env)
+ req.authname = 'testuser'
+ req.environ['PATH_INFO'] = '/products/missing'
+
+ self.expectedPrefix = 'missing'
+ self.expectedPathInfo = ''
+ with self.assertRaises(RequestDone):
+ self._dispatch(req, self.global_env)
+ self.assertEqual(1, len(req.chrome['warnings']))
+ self.assertEqual('Product missing not found',
+ req.chrome['warnings'][0].unescape())
def test_product_edit(self):
spy = self.global_env[TestRequestSpy]
Modified: bloodhound/branches/bep_0007_embeddable_objects/bloodhound_relations/bhrelations/api.py
URL: http://svn.apache.org/viewvc/bloodhound/branches/bep_0007_embeddable_objects/bloodhound_relations/bhrelations/api.py?rev=1507818&r1=1507817&r2=1507818&view=diff
==============================================================================
--- bloodhound/branches/bep_0007_embeddable_objects/bloodhound_relations/bhrelations/api.py (original)
+++ bloodhound/branches/bep_0007_embeddable_objects/bloodhound_relations/bhrelations/api.py Sun Jul 28 17:22:23 2013
@@ -17,17 +17,24 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+import itertools
+
+import re
from datetime import datetime
from pkg_resources import resource_filename
from bhrelations import db_default
from bhrelations.model import Relation
+from bhrelations.utils import unique
from multiproduct.api import ISupportMultiProductEnvironment
-from trac.config import OrderedExtensionsOption
+from multiproduct.model import Product
+from multiproduct.env import ProductEnvironment
+
+from trac.config import OrderedExtensionsOption, Option
from trac.core import (Component, implements, TracError, Interface,
ExtensionPoint)
from trac.env import IEnvironmentSetupParticipant
from trac.db import DatabaseManager
-from trac.resource import (ResourceSystem, Resource,
+from trac.resource import (ResourceSystem, Resource, ResourceNotFound,
get_resource_shortname, Neighborhood)
from trac.ticket import Ticket, ITicketManipulator, ITicketChangeListener
from trac.util.datefmt import utc, to_utimestamp
@@ -167,6 +174,12 @@ class RelationsSystem(Component):
regardless of their type."""
)
+ duplicate_relation_type = Option(
+ 'bhrelations',
+ 'duplicate_relation',
+ '',
+ "Relation type to be used with the resolve as duplicate workflow.")
+
def __init__(self):
links, labels, validators, blockers, copy_fields, exclusive = \
self._parse_config()
@@ -432,6 +445,9 @@ class ResourceIdSerializer(object):
#TODO: temporary workaround for the ticket specific behavior
#change it to generic resource behaviour
ticket = resource_instance
+ if ticket.id is None:
+ raise ValueError("Cannot get resource id for ticket "
+ "that does not exist yet.")
nbhprefix = ticket["product"]
resource_full_id = cls.RESOURCE_ID_DELIMITER.join(
@@ -443,28 +459,46 @@ class ResourceIdSerializer(object):
class TicketRelationsSpecifics(Component):
implements(ITicketManipulator, ITicketChangeListener)
- #ITicketChangeListener methods
+ def __init__(self):
+ self.rls = RelationsSystem(self.env)
+ #ITicketChangeListener methods
def ticket_created(self, ticket):
pass
def ticket_changed(self, ticket, comment, author, old_values):
- pass
+ if (
+ self._closed_as_duplicate(ticket) and
+ self.rls.duplicate_relation_type
+ ):
+ try:
+ self.rls.add(ticket, ticket.duplicate,
+ self.rls.duplicate_relation_type,
+ comment, author)
+ except TracError:
+ pass
+
+ def _closed_as_duplicate(self, ticket):
+ return (ticket['status'] == 'closed' and
+ ticket['resolution'] == 'duplicate')
def ticket_deleted(self, ticket):
- RelationsSystem(self.env).delete_resource_relations(ticket)
+ self.rls.delete_resource_relations(ticket)
#ITicketManipulator methods
-
def prepare_ticket(self, req, ticket, fields, actions):
pass
def validate_ticket(self, req, ticket):
- action = req.args.get('action')
- if action == 'resolve':
- rls = RelationsSystem(self.env)
- blockers = rls.find_blockers(
- ticket, self.is_blocker)
+ return itertools.chain(
+ self._check_blockers(req, ticket),
+ self._check_open_children(req, ticket),
+ self._check_duplicate_id(req, ticket),
+ )
+
+ def _check_blockers(self, req, ticket):
+ if req.args.get('action') == 'resolve':
+ blockers = self.rls.find_blockers(ticket, self.is_blocker)
if blockers:
blockers_str = ', '.join(
get_resource_shortname(self.env, blocker_ticket.resource)
@@ -474,14 +508,61 @@ class TicketRelationsSpecifics(Component
% blockers_str)
yield None, msg
- for relation in [r for r in rls.get_relations(ticket)
- if r['type'] == rls.CHILDREN_RELATION_TYPE]:
+ def _check_open_children(self, req, ticket):
+ if req.args.get('action') == 'resolve':
+ for relation in [r for r in self.rls.get_relations(ticket)
+ if r['type'] == self.rls.CHILDREN_RELATION_TYPE]:
ticket = self._create_ticket_by_full_id(relation['destination'])
if ticket['status'] != 'closed':
msg = ("Cannot resolve this ticket because it has open"
"child tickets.")
yield None, msg
+ def _check_duplicate_id(self, req, ticket):
+ if req.args.get('action') == 'resolve':
+ resolution = req.args.get('action_resolve_resolve_resolution')
+ if resolution == 'duplicate':
+ duplicate_id = req.args.get('duplicate_id')
+ if not duplicate_id:
+ yield None, "Duplicate ticket ID must be provided."
+
+ try:
+ duplicate_ticket = self.find_ticket(duplicate_id)
+ req.perm.require('TICKET_MODIFY',
+ Resource(duplicate_ticket.id))
+ ticket.duplicate = duplicate_ticket
+ except NoSuchTicketError:
+ yield None, "Invalid duplicate ticket ID."
+
+ def find_ticket(self, ticket_spec):
+ ticket = None
+ m = re.match(r'#?(?P<tid>\d+)', ticket_spec)
+ if m:
+ tid = m.group('tid')
+ try:
+ ticket = Ticket(self.env, tid)
+ except ResourceNotFound:
+ # ticket not found in current product, try all other products
+ for p in Product.select(self.env):
+ if p.prefix != self.env.product.prefix:
+ # TODO: check for PRODUCT_VIEW permissions
+ penv = ProductEnvironment(self.env.parent, p.prefix)
+ try:
+ ticket = Ticket(penv, tid)
+ except ResourceNotFound:
+ pass
+ else:
+ break
+
+ # ticket still not found, use fallback for <prefix>:ticket:<id> syntax
+ if ticket is None:
+ try:
+ resource = ResourceIdSerializer.get_resource_by_id(ticket_spec)
+ ticket = self._create_ticket_by_full_id(resource)
+ except:
+ raise NoSuchTicketError
+ return ticket
+
def is_blocker(self, resource):
ticket = self._create_ticket_by_full_id(resource)
if ticket['status'] != 'closed':
@@ -573,14 +654,10 @@ class TicketChangeRecordUpdater(Componen
new_value,
product))
-# Copied from trac/utils.py, ticket-links-trunk branch
-def unique(seq):
- """Yield unique elements from sequence of hashables, preserving order.
- (New in 0.13)
- """
- seen = set()
- return (x for x in seq if x not in seen and not seen.add(x))
-
class UnknownRelationType(ValueError):
pass
+
+
+class NoSuchTicketError(ValueError):
+ pass