incubator-bloodhound-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From j...@apache.org
Subject svn commit: r1450762 [3/3] - in /incubator/bloodhound/branches/bep_0003_multiproduct: ./ bloodhound_dashboard/ bloodhound_dashboard/bhdashboard/htdocs/css/ bloodhound_dashboard/bhdashboard/htdocs/img/ bloodhound_dashboard/bhdashboard/htdocs/js/ bloodho...
Date Wed, 27 Feb 2013 12:52:43 GMT
Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/base.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/base.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/base.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/base.py Wed Feb 27 12:52:42 2013
@@ -19,8 +19,11 @@
 #  under the License.
 
 r"""Base classes for Bloodhound Search plugin."""
-from trac.core import Component
-from trac.config import BoolOption
+import re
+
+from bhsearch.api import ISearchWikiSyntaxFormatter
+from trac.core import Component, implements
+from trac.config import BoolOption, ExtensionOption
 
 class BaseIndexer(Component):
     """
@@ -29,6 +32,12 @@ class BaseIndexer(Component):
     silence_on_error = BoolOption('bhsearch', 'silence_on_error', "True",
         """If true, do not throw an exception during indexing a resource""")
 
+    wiki_formatter = ExtensionOption('bhsearch', 'wiki_syntax_formatter',
+        ISearchWikiSyntaxFormatter, 'SimpleSearchWikiSyntaxFormatter',
+        'Name of the component implementing wiki syntax to text formatter \
+        interface: ISearchWikiSyntaxFormatter.')
+
+
 
 class BaseSearchParticipant(Component):
     default_view = None
@@ -52,4 +61,26 @@ class BaseSearchParticipant(Component):
         return (not req or self.required_permission in req.perm)
 
     def get_participant_type(self):
-        return self.participant_type
\ No newline at end of file
+        return self.participant_type
+
+class SimpleSearchWikiSyntaxFormatter(Component):
+    """
+    This class provide very naive formatting of wiki syntax to text
+    appropriate for indexing and search result presentation.
+    A lot of things can be improved here.
+    """
+    implements(ISearchWikiSyntaxFormatter)
+
+    STRIP_CHARS = re.compile(r'([=#\'\"\*/])')
+    REPLACE_CHARS = re.compile(r'([=#\[\]\{\}|])')
+
+    WHITE_SPACE_RE = re.compile(r'([\s]+)')
+    def format(self, wiki_content):
+        if not wiki_content:
+            return wiki_content
+        intermediate = self.STRIP_CHARS.sub("", wiki_content)
+        intermediate = self.REPLACE_CHARS.sub(" ", intermediate)
+        result = self.WHITE_SPACE_RE.sub(" ", intermediate)
+        return result.strip()
+
+

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/milestone_search.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/milestone_search.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/milestone_search.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/milestone_search.py Wed Feb 27 12:52:42 2013
@@ -23,6 +23,7 @@ from bhsearch import BHSEARCH_CONFIG_SEC
 from bhsearch.api import (IIndexParticipant, BloodhoundSearchApi, IndexFields,
     ISearchParticipant)
 from bhsearch.search_resources.base import BaseIndexer, BaseSearchParticipant
+from bhsearch.search_resources.ticket_search import TicketIndexer
 from trac.ticket import IMilestoneChangeListener, Milestone
 from trac.config import ListOption, Option
 from trac.core import implements
@@ -64,11 +65,13 @@ class MilestoneIndexer(BaseIndexer):
                 raise
 
     def _rename_milestone(self, milestone, old_name):
-        #todo: reindex tickets that are referencing the renamed milestone
         try:
             doc = self.build_doc(milestone)
             search_api = BloodhoundSearchApi(self.env)
-            search_api.change_doc_id(doc, old_name)
+            with search_api.start_operation() as operation_context:
+                search_api.change_doc_id(doc, old_name, operation_context)
+                TicketIndexer(self.env).reindex_tickets(
+                    search_api, operation_context, milestone=milestone.name)
         except Exception, e:
             if self.silence_on_error:
                 self.log.error("Error occurs during renaming milestone from \

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/ticket_search.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/ticket_search.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/ticket_search.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/ticket_search.py Wed Feb 27 12:52:42 2013
@@ -24,7 +24,7 @@ from bhsearch.api import ISearchParticip
     IIndexParticipant, IndexFields
 from bhsearch.search_resources.base import BaseIndexer, BaseSearchParticipant
 from genshi.builder import tag
-from trac.ticket.api import ITicketChangeListener
+from trac.ticket.api import ITicketChangeListener, TicketSystem
 from trac.ticket import Ticket
 from trac.ticket.query import Query
 from trac.config import ListOption, Option
@@ -54,6 +54,10 @@ class TicketIndexer(BaseIndexer):
         'reporter': TicketFields.AUTHOR,
     }
 
+    def __init__(self):
+        self.fields = TicketSystem(self.env).get_ticket_fields()
+        self.text_area_fields = set(
+            f['name'] for f in self.fields if f['type'] =='textarea')
 
     #ITicketChangeListener methods
     def ticket_created(self, ticket):
@@ -77,14 +81,35 @@ class TicketIndexer(BaseIndexer):
             else:
                 raise
 
+    def reindex_tickets(self, search_api, operation_context, milestone=None):
+        for ticket in self._fetch_tickets(milestone):
+            self._index_ticket(ticket, search_api, operation_context)
+
+    def _fetch_tickets(self, milestone = None):
+#        with self.env.db_transaction as db:
+        for ticket_id in self._fetch_ids(milestone):
+            yield Ticket(self.env, ticket_id)
+
+    def _fetch_ids(self, milestone):
+        sql = "SELECT id FROM ticket"
+        args = []
+        conditions = []
+        if milestone:
+            args.append(milestone)
+            conditions.append("milestone=%s")
+        if conditions:
+            sql = sql + " WHERE " + " AND ".join(conditions)
+        for row in self.env.db_query(sql, args):
+            yield int(row[0])
+
+
     def _index_ticket(
-            self,
-            ticket,
-            ):
+            self, ticket, search_api = None, operation_context = None):
         try:
-            search_api = BloodhoundSearchApi(self.env)
+            if not search_api:
+                search_api = BloodhoundSearchApi(self.env)
             doc = self.build_doc(ticket)
-            search_api.add_doc(doc)
+            search_api.add_doc(doc, operation_context)
         except Exception, e:
             if self.silence_on_error:
                 self.log.error("Error occurs during ticket indexing. \
@@ -103,17 +128,18 @@ class TicketIndexer(BaseIndexer):
 
         for field, index_field in self.optional_fields.iteritems():
             if field in ticket.values:
-                doc[index_field] = ticket.values[field]
+                field_content = ticket.values[field]
+                if field in self.text_area_fields:
+                    field_content = self.wiki_formatter.format(field_content)
+                doc[index_field] = field_content
 
         doc[TicketFields.CHANGES] = u'\n\n'.join(
-            [x[4] for x in ticket.get_changelog() if x[2] == u'comment'])
+            [self.wiki_formatter.format(x[4]) for x in ticket.get_changelog()
+             if x[2] == u'comment'])
         return doc
 
     def get_entries_for_index(self):
-        #is there any better way to get all tickets?
-        query_records = self._load_ticket_ids()
-        for record in query_records:
-            ticket = Ticket(self.env, record["id"])
+        for ticket in self._fetch_tickets():
             yield self.build_doc(ticket)
 
     def _load_ticket_ids(self):

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/wiki_search.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/wiki_search.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/wiki_search.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/search_resources/wiki_search.py Wed Feb 27 12:52:42 2013
@@ -99,7 +99,7 @@ class WikiIndexer(BaseIndexer):
             IndexFields.TYPE: WIKI_TYPE,
             IndexFields.TIME: page.time,
             IndexFields.AUTHOR: page.author,
-            IndexFields.CONTENT: page.text,
+            IndexFields.CONTENT: self.wiki_formatter.format(page.text),
         }
         return doc
 

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/__init__.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/__init__.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/__init__.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/__init__.py Wed Feb 27 12:52:42 2013
@@ -21,7 +21,7 @@ import unittest
 from bhsearch.tests import (whoosh_backend, index_with_whoosh, web_ui,
                             api)
 from bhsearch.tests.search_resources import (ticket_search, wiki_search,
-                                             milestone_search)
+                                             milestone_search, base)
 
 def suite():
     test_suite = unittest.TestSuite()
@@ -32,6 +32,7 @@ def suite():
     test_suite.addTest(ticket_search.suite())
     test_suite.addTest(wiki_search.suite())
     test_suite.addTest(milestone_search.suite())
+    test_suite.addTest(base.suite())
     return test_suite
 
 if __name__ == '__main__':

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/base.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/base.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/base.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/base.py Wed Feb 27 12:52:42 2013
@@ -38,7 +38,7 @@ class BaseBloodhoundSearchTest(unittest.
 
     def setUp(self, enabled = None, create_req = False):
         if not enabled:
-            enabled = ['bhsearch.*']
+            enabled = ['trac.*', 'bhsearch.*']
         self.env = EnvironmentStub(enable=enabled)
         self.env.path = tempfile.mkdtemp('bhsearch-tempenv')
         self.env.config.set('bhsearch', 'silence_on_error', "False")
@@ -75,7 +75,8 @@ class BaseBloodhoundSearchTest(unittest.
     def insert_ticket(self, summary, **kw):
         """Helper for inserting a ticket into the database"""
         ticket = self.create_ticket(summary, **kw)
-        return ticket.insert()
+        ticket.insert()
+        return ticket
 
     def create_wiki(self, name, text,  **kw):
         page = WikiPage(self.env, name)
@@ -93,7 +94,8 @@ class BaseBloodhoundSearchTest(unittest.
         milestone = self.create_milestone(
             name = name,
             description = description)
-        return milestone.insert()
+        milestone.insert()
+        return milestone
 
     def create_milestone(self, name, description = None):
         milestone = Milestone(self.env)

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/index_with_whoosh.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/index_with_whoosh.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/index_with_whoosh.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/index_with_whoosh.py Wed Feb 27 12:52:42 2013
@@ -17,7 +17,6 @@
 #  KIND, either express or implied.  See the License for the
 #  specific language governing permissions and limitations
 #  under the License.
-from contextlib import closing
 
 import unittest
 import shutil
@@ -25,10 +24,7 @@ from bhsearch.api import BloodhoundSearc
 from bhsearch.search_resources.milestone_search import MilestoneIndexer
 from bhsearch.tests.base import BaseBloodhoundSearchTest
 from bhsearch.search_resources.ticket_search import TicketIndexer
-
 from bhsearch.whoosh_backend import WhooshBackend
-from trac.test import MockPerm
-from trac.web import Href
 
 class IndexWhooshTestCase(BaseBloodhoundSearchTest):
     def setUp(self):
@@ -116,59 +112,9 @@ class IndexWhooshTestCase(BaseBloodhound
         self.print_result(results)
         self.assertEqual(2, results.hits)
 
-class FormattingTestCase(BaseBloodhoundSearchTest):
-    def setUp(self):
-        super(FormattingTestCase, self).setUp(
-            ['trac.*', 'bhsearch.*'],
-            create_req=True,
-        )
-
-    def test_can_format_wiki_to_text(self):
-        wiki_content = """= Header #overview
-
-        '''bold''', ''italic'', '''''Wikipedia style'''''
-
-        {{{
-        code
-        }}}
-
-        [[PageOutline]]
-
-        || '''Table''' || sampe ||
-
-          - list
-          - sample
-
-        [[Image(mockup_tickets.png)]] [wiki:SomePage p1] [ticket:1 ticket one]
-        http://www.edgewall.com/,
-        [http://www.edgewall.com Edgewall Software]
-
-        [#point1]
-        """
-#        wiki_content = """
-#'''''one''''', '''''two''''', '''''three''''', '''''four'''''
-#        """
-
-        page = self.create_wiki("Dummy wiki", wiki_content)
-        from trac.mimeview.api import RenderingContext
-        context = RenderingContext(
-            page.resource,
-            href=Href('/'),
-            perm=MockPerm(),
-        )
-        context.req = None # 1.0 FIXME .req shouldn't be required by formatter
-#        result = format_to_oneliner(self.env, context, wiki_content)
-#        from trac.wiki.formatter import format_to_oneliner
-        from trac.web.chrome import  format_to_html
-        result = format_to_html(self.env, context, wiki_content)
-        print result
-#
-
-
 def suite():
     test_suite = unittest.TestSuite()
     test_suite.addTest(unittest.makeSuite(IndexWhooshTestCase, 'test'))
-    test_suite.addTest(unittest.makeSuite(FormattingTestCase, 'test'))
     return test_suite
 
 if __name__ == '__main__':

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/milestone_search.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/milestone_search.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/milestone_search.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/milestone_search.py Wed Feb 27 12:52:42 2013
@@ -121,6 +121,55 @@ class MilestoneIndexerEventsTestCase(Bas
         self.assertEqual(self.DUMMY_MILESTONE_NAME, doc["id"])
         self.assertEqual("milestone", doc["type"])
 
+    def test_that_tickets_updated_after_milestone_renaming(self):
+        #asser
+        INITIAL_MILESTONE = "initial_milestone"
+        RENAMED_MILESTONE = "renamed_name"
+        milestone = self.insert_milestone(INITIAL_MILESTONE)
+        self.insert_ticket("T1", milestone=INITIAL_MILESTONE)
+        self.insert_ticket("T2", milestone=INITIAL_MILESTONE)
+        #act
+        milestone.name = RENAMED_MILESTONE
+        milestone.update()
+        #assert
+        results = self.search_api.query("type:ticket")
+        self.print_result(results)
+        self.assertEqual(2, results.hits)
+        self.assertEqual(RENAMED_MILESTONE, results.docs[0]["milestone"])
+        self.assertEqual(RENAMED_MILESTONE, results.docs[1]["milestone"])
+
+    def test_that_tickets_updated_after_milestone_delete_no_retarget(self):
+        #asser
+        INITIAL_MILESTONE = "initial_milestone"
+        milestone = self.insert_milestone(INITIAL_MILESTONE)
+        self.insert_ticket("T1", milestone=INITIAL_MILESTONE)
+        self.insert_ticket("T2", milestone=INITIAL_MILESTONE)
+        #act
+        milestone.delete()
+        #assert
+        results = self.search_api.query("type:ticket")
+        self.print_result(results)
+        self.assertEqual(2, results.hits)
+        self.assertNotIn("milestone", results.docs[0])
+        self.assertNotIn("milestone", results.docs[1])
+
+    def test_that_tickets_updated_after_milestone_delete_with_retarget(self):
+        #asser
+        INITIAL_MILESTONE = "initial_milestone"
+        RETARGET_MILESTONE = "retarget_milestone"
+        milestone = self.insert_milestone(INITIAL_MILESTONE)
+        self.insert_milestone(RETARGET_MILESTONE)
+        self.insert_ticket("T1", milestone=INITIAL_MILESTONE)
+        self.insert_ticket("T2", milestone=INITIAL_MILESTONE)
+        #act
+        milestone.delete(retarget_to=RETARGET_MILESTONE)
+        #assert
+        results = self.search_api.query("type:ticket")
+        self.print_result(results)
+        self.assertEqual(2, results.hits)
+        self.assertEqual(RETARGET_MILESTONE, results.docs[0]["milestone"])
+        self.assertEqual(RETARGET_MILESTONE, results.docs[1]["milestone"])
+
 class MilestoneSearchParticipantTestCase(BaseBloodhoundSearchTest):
     def setUp(self):
         super(MilestoneSearchParticipantTestCase, self).setUp()

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/ticket_search.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/ticket_search.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/ticket_search.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/ticket_search.py Wed Feb 27 12:52:42 2013
@@ -18,20 +18,23 @@
 #  specific language governing permissions and limitations
 #  under the License.
 import unittest
+from bhsearch.api import BloodhoundSearchApi
 
 from bhsearch.tests.base import BaseBloodhoundSearchTest
 from bhsearch.search_resources.ticket_search import TicketIndexer
 
-class TicketIndexerSilenceOnExceptionTestCase(BaseBloodhoundSearchTest):
+class TicketIndexerTestCase(BaseBloodhoundSearchTest):
     def setUp(self):
-        super(TicketIndexerSilenceOnExceptionTestCase, self).setUp()
-        self.env.config.set('bhsearch', 'silence_on_error', "True")
+        super(TicketIndexerTestCase, self).setUp()
         self.ticket_indexer = TicketIndexer(self.env)
+        self.search_api = BloodhoundSearchApi(self.env)
+        self.env.config.set('bhsearch', 'silence_on_error', "False")
 
     def tearDown(self):
         pass
 
     def test_does_not_raise_exception_by_default(self):
+        self.env.config.set('bhsearch', 'silence_on_error', "True")
         self.ticket_indexer.ticket_created(None)
 
     def test_raise_exception_if_configured(self):
@@ -41,9 +44,17 @@ class TicketIndexerSilenceOnExceptionTes
             self.ticket_indexer.ticket_created,
             None)
 
+    def test_can_strip_wiki_syntax(self):
+        #act
+        self.insert_ticket("T1", description=" = Header")
+        #assert
+        results = self.search_api.query("*:*")
+        self.print_result(results)
+        self.assertEqual(1, results.hits)
+        self.assertEqual("Header", results.docs[0]["content"])
 
 def suite():
-    return unittest.makeSuite(TicketIndexerSilenceOnExceptionTestCase, 'test')
+    return unittest.makeSuite(TicketIndexerTestCase, 'test')
 
 if __name__ == '__main__':
     unittest.main()

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/wiki_search.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/wiki_search.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/wiki_search.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/search_resources/wiki_search.py Wed Feb 27 12:52:42 2013
@@ -128,6 +128,16 @@ class WikiIndexerEventsTestCase(BaseBloo
         self.assertEqual(1, results.hits)
         self.assertEqual("version1", results.docs[0]["content"])
 
+    def test_can_strip_wiki_formatting(self):
+        #arrange
+        self.insert_wiki(self.DUMMY_PAGE_NAME, " = Header")
+        #act
+        results = self.search_api.query("*:*")
+        #assert
+        self.print_result(results)
+        self.assertEqual(1, results.hits)
+        self.assertEqual("Header", results.docs[0]["content"])
+
 def suite():
     test_suite = unittest.TestSuite()
     test_suite.addTest(unittest.makeSuite(

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/web_ui.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/web_ui.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/web_ui.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/web_ui.py Wed Feb 27 12:52:42 2013
@@ -27,7 +27,6 @@ from bhsearch.web_ui import RequestParam
 from bhsearch.whoosh_backend import WhooshBackend
 
 from trac.test import Mock, MockPerm
-from trac.ticket import Ticket
 from trac.core import TracError
 from trac.util.datefmt import FixedOffset
 from trac.util import format_datetime
@@ -40,7 +39,6 @@ DEFAULT_DOCS_PER_PAGE = 10
 class WebUiTestCaseWithWhoosh(BaseBloodhoundSearchTest):
     def setUp(self):
         super(WebUiTestCaseWithWhoosh, self).setUp(
-            ['trac.*', 'bhsearch.*'],
             create_req=True,
         )
         self.req.redirect = self.redirect
@@ -84,8 +82,7 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
 
     def test_can_return_utc_time(self):
         #arrange
-        ticket_id = self.insert_ticket("bla")
-        ticket = Ticket(self.env, ticket_id)
+        ticket = self.insert_ticket("bla")
         ticket_time = ticket.time_changed
         #act
         self.req.args[RequestParameters.QUERY] = "*:*"
@@ -101,8 +98,7 @@ class WebUiTestCaseWithWhoosh(BaseBloodh
 
     def test_can_return_user_time(self):
         #arrange
-        ticket_id = self.insert_ticket("bla")
-        ticket = Ticket(self.env, ticket_id)
+        ticket = self.insert_ticket("bla")
         ticket_time = ticket.time_changed
         #act
         self.req.tz = FixedOffset(60, 'GMT +1:00')

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/whoosh_backend.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/whoosh_backend.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/whoosh_backend.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/tests/whoosh_backend.py Wed Feb 27 12:52:42 2013
@@ -451,6 +451,41 @@ class WhooshFunctionalityTestCase(unitte
             facets_result[name] = non_paged_results.groups(name)
         return facets_result
 
+    def test_can_auto_commit(self):
+        # pylint: disable=unused-argument
+
+        schema = Schema(
+                unique_id=ID(stored=True, unique=True),
+                type=ID(stored=True),
+                )
+
+        ix = index.create_in(self.index_dir, schema=schema)
+        with ix.writer() as w:
+            w.add_document(unique_id=u"1", type=u"type1")
+            w.add_document(unique_id=u"2", type=u"type2")
+
+        with ix.searcher() as s:
+            results = s.search(query.Every())
+            self.assertEquals(2, len(results))
+
+    def test_can_auto_cancel(self):
+        schema = Schema(
+                unique_id=ID(stored=True, unique=True),
+                type=ID(stored=True),
+                )
+
+        ix = index.create_in(self.index_dir, schema=schema)
+        try:
+            with ix.writer() as w:
+                w.add_document(unique_id=u"1", type=u"type1")
+                w.add_document(unique_id=u"2", type=u"type2")
+                raise Exception("some exception")
+        except Exception:
+            pass
+
+        with ix.searcher() as s:
+            results = s.search(query.Every())
+            self.assertEquals(0, len(results))
 
 class WhooshEmptyFacetErrorWorkaroundTestCase(BaseBloodhoundSearchTest):
     def setUp(self):

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/whoosh_backend.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/whoosh_backend.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/whoosh_backend.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/bhsearch/whoosh_backend.py Wed Feb 27 12:52:42 2013
@@ -76,17 +76,18 @@ class WhooshBackend(Component):
 
     #ISearchBackend methods
     def start_operation(self):
-        return dict(writer = self._create_writer())
+        return self._create_writer()
 
     def _create_writer(self):
         return AsyncWriter(self.index)
 
-    def add_doc(self, doc, writer=None):
+    def add_doc(self, doc, operation_context=None):
         """Add any type of  document index.
 
         The contents should be a dict with fields matching the search schema.
         The only required fields are type and id, everything else is optional.
         """
+        writer = operation_context
         is_local_writer = False
         if writer is None:
             is_local_writer = True
@@ -118,9 +119,10 @@ class WhooshBackend(Component):
             else:
                 doc[key] = self._to_whoosh_format(value)
 
-    def delete_doc(self, doc_type, doc_id, writer=None):
+    def delete_doc(self, doc_type, doc_id, operation_context=None):
         unique_id = self._create_unique_id(doc_type, doc_id)
         self.log.debug('Removing document from the index: %s', unique_id)
+        writer = operation_context
         is_local_writer = False
         if writer is None:
             is_local_writer = True
@@ -139,15 +141,6 @@ class WhooshBackend(Component):
         writer = AsyncWriter(self.index)
         writer.commit(optimize=True)
 
-    def commit(self, optimize, writer):
-        writer.commit(optimize=optimize)
-
-    def cancel(self, writer):
-        try:
-            writer.cancel()
-        except Exception, ex:
-            self.env.log.error("Error during writer cancellation: %s", ex)
-
     def recreate_index(self):
         self.log.info('Creating Whoosh index in %s' % self.index_dir)
         self._make_dir_if_not_exists()
@@ -380,7 +373,12 @@ class WhooshEmptyFacetErrorWorkaround(Co
                     if field == self.NULL_MARKER:
                         count_dict[None] = count
                         del count_dict[self.NULL_MARKER]
-        #we can fix query_result.docs later if needed
+
+        #fix query_result.docs
+        for doc in query_result.docs:
+            for field, value in doc.items():
+                if value == self.NULL_MARKER:
+                    del doc[field]
 
     #IQueryPreprocessor methods
     def query_pre_process(self, query_parameters):

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.cfg
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.cfg?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.cfg (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.cfg Wed Feb 27 12:52:42 2013
@@ -16,9 +16,9 @@
 #  under the License.
 
 [egg_info]
-tag_build = 
+tag_build = dev
 tag_date = 0
-tag_svn_revision = 0
+tag_svn_revision = true
 
 [sdist]
 formats = gztar,bztar,ztar,tar,zip

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_search/setup.py Wed Feb 27 12:52:42 2013
@@ -30,6 +30,7 @@ Add free text search and query functiona
 
 versions = [
     (0, 4, 0),
+    (0, 5, 0),
     ]
 
 latest = '.'.join(str(x) for x in versions[-1])

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/bloodhound.css
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/bloodhound.css?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/bloodhound.css (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/htdocs/bloodhound.css Wed Feb 27 12:52:42 2013
@@ -285,6 +285,8 @@ pre.wiki {
   display: none;
 }
 
+#trac-comment-editor textarea { width: 100%; }
+
 /* @end */
 
 /* @group Quick Ticket fieldset */

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_components.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_components.html?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_components.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_components.html Wed Feb 27 12:52:42 2013
@@ -32,19 +32,19 @@
   <body>
     <h2>Manage Components</h2>
 
-    <py:def function="owner_field(default_owner='')">
-      <div class="control-group">
-        <label class="control-label" for="owner">Owner: </label>
-        <div class="controls" py:choose="">
-          <select class="input-medium"
-              py:when="owners" size="1" id="owner" name="owner">
+    <py:def function="owner_field(default_owner='', inline=False)">
+      <div py:strip="inline" class="control-group">
+        <label class="control-label" for="owner">Owner:</label>
+        <div class="controls" py:choose="" py:strip="inline">
+          <select py:when="owners" id="owner"
+                  class="input-medium" size="1" name="owner">
             <option py:for="owner in owners"
                     selected="${owner==default_owner or None}" value="$owner">$owner</option>
             <option py:if="default_owner and default_owner not in owners"
                     selected="selected" value="$default_owner">$default_owner</option>
           </select>
           <input py:otherwise="" class="input-medium" type="text" id="owner"
-              name="owner" value="$default_owner" />
+                 name="owner" value="$default_owner" />
         </div>
       </div>
     </py:def>
@@ -55,9 +55,9 @@
         <fieldset>
           <legend>Modify Component:</legend>
           <div class="control-group">
-            <label class="control-label" for="name">Name:</label>
+            <label class="control-label" for="name">Name: </label>
             <div class="controls">
-              <input type="text" id="name" name="name" value="$component.name"/>
+              <input class="input-medium" type="text" id="name" name="name" value="$component.name"/>
             </div>
           </div>
           ${owner_field(component.owner)}
@@ -80,7 +80,7 @@
             </fieldset>
           </div>
           <div class="control-group">
-            <input class="btn" type="submit" name="cancel"
+            <input class="btn-link" type="submit" name="cancel"
                 value="${_('Cancel')}" />
             <input class="btn" type="submit" name="save"
                 value="${_('Save')}" />
@@ -90,9 +90,26 @@
 
       <py:otherwise>
         <div class="row">
-          <div class="span6">
+          <div class="span9">
+            <form id="addcomponent" class="well form-inline" method="post" action="">
+              <fieldset>
+                <legend>Add Component:</legend>
+                <div class="control-group">
+                  <input class="btn" type="submit" name="add"
+                         value="${_('Add')}"/>
+                  <label class="control-label" for="name">Name:</label>
+                  <input id="name" class="input-medium" type="text" name="name" />
+                  ${owner_field(inline=True)}
+                </div>
+              </fieldset>
+            </form>
+          </div>
+        </div>
+        <div class="row">
+          <div class="span9">
             <py:choose>
-              <form py:when="components" id="component_table" method="post" action="">
+              <form py:when="components" id="component_table"
+                    method="post" action="">
                 <table id="complist"
                     class="table table-striped table-condensed table-bordered">
                   <thead>
@@ -126,7 +143,7 @@
                   field from the user interface.
                 </p>
               </form>
-    
+
               <p py:otherwise="" class="help-block">
                 <span class="label label-warning">Warning</span>
                 As long as you don't add any items to the list, this field
@@ -134,18 +151,6 @@
               </p>
             </py:choose>
           </div>
-          <div class="span3">
-            <form class="well" id="addcomponent" method="post" action="">
-              <fieldset>
-                <legend>Add Component:</legend>
-                <label for="name">Name:</label>
-                <input class="input-medium" type="text" id="name" name="name" />
-                ${owner_field()}
-                <input class="btn" type="submit" name="add"
-                    value="${_('Add')}"/>
-              </fieldset>
-            </form>
-          </div>
         </div>
 
       </py:otherwise>

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_enums.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_enums.html?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_enums.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_enums.html Wed Feb 27 12:52:42 2013
@@ -35,7 +35,7 @@
     <py:choose test="view">
       <form py:when="'detail'" class="well form-horizontal" id="modenum" method="post" action="">
         <fieldset>
-          <legend i18n:msg="label_singular">Modify $label_singular</legend>
+          <legend i18n:msg="label_singular">Modify $label_singular:</legend>
           <div class="control-group">
             <label class="control-label" for="name">Name: </label>
             <div class="controls">
@@ -43,7 +43,7 @@
             </div>
           </div>
           <div class="btn-group">
-            <input class="btn" type="submit" name="cancel"
+            <input class="btn-link" type="submit" name="cancel"
                 value="${_('Cancel')}"/>
             <input class="btn" type="submit" name="save" value="${_('Save')}"/>
           </div>
@@ -52,11 +52,25 @@
 
       <py:otherwise>
         <div class="row">
-          <div class="span6">
+          <div class="span9">
+            <form id="addenum" class="well form-inline" method="post" action="">
+              <fieldset>
+                <legend i18n:msg="label_singular">Add $label_singular:</legend>
+                <input class="btn" type="submit" name="add"
+                       value="${_('Add')}"/>
+                <label class="control-label" for="name">Name:</label>
+                <input class="input-medium" type="text"
+                       name="name" id="name"/>
+              </fieldset>
+            </form>
+          </div>
+        </div>
+        <div class="row">
+          <div class="span9">
             <py:choose>
-              <form py:when="enums" id="enumtable" method="post" action="">
+              <form py:when="enums" id="enumtable" class="form-inline" method="post" action="">
                 <table id="enumlist"
-                    class="table table-striped table-condensed table-bordered">
+                       class="table table-striped table-condensed table-bordered">
                   <thead>
                     <tr><th class="sel"><i class="icon-check"></i></th>
                       <th>Name</th><th>Default</th><th>Order</th>
@@ -81,12 +95,6 @@
                     </tr>
                   </tbody>
                 </table>
-                <p class="help-block" py:if="type=='priority'" i18n:msg="">
-                  <span class="label label-info">Note:</span>
-                  The order of priorities determines the
-                  coloring of entries in the ticket queries and reports.
-                </p>
-                <br/>
                 <div class="form-inline">
                   <input class="btn" type="submit" name="remove"
                       value="${_('Remove selected items')}" />
@@ -94,8 +102,13 @@
                       value="${_('Apply changes')}" />
                 </div>
                 <br/>
+                <p class="help-block" py:if="type=='priority'" i18n:msg="">
+                  <span class="label label-info">Note</span>
+                  The order of priorities determines the
+                  coloring of entries in the ticket queries and reports.
+                </p>
                 <p class="help-block">
-                  <span class="label label-info">Notice</span>
+                  <span class="label label-info">Note</span>
                   You can remove all items from this list to completely hide this
                   field from the user interface.
                 </p>
@@ -108,17 +121,6 @@
               </p>
             </py:choose>
           </div>
-          <div class="span3">
-            <form class="well" id="addenum" method="post" action="">
-              <fieldset>
-                <legend i18n:msg="label_singular">Add $label_singular:</legend>
-                <label class="control-label" for="name">Name:</label>
-                <input class="input-medium" type="text" name="name" id="name"/>
-                <input class="btn" type="submit" name="add"
-                      value="${_('Add')}"/>
-              </fieldset>
-            </form>
-          </div>
         </div>
 
       </py:otherwise>

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_milestones.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_milestones.html?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_milestones.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_milestones.html Wed Feb 27 12:52:42 2013
@@ -39,7 +39,7 @@
     <h2>Milestone Management</h2>
 
     <py:choose test="view">
-      <form py:when="'detail'" class="well form-horizontal" method="post" 
+      <form py:when="'detail'" class="well form-horizontal" method="post"
           id="modifymilestone" action=""
           py:with="readonly = 'MILESTONE_MODIFY' not in req.perm or None">
         <fieldset>
@@ -70,7 +70,7 @@
               <input type="text" id="completeddate" name="completeddate"
                      size="${len(datetime_hint)}"
                      value="${format_datetime(milestone.completed)}" readonly="${readonly}"
-                     placeholder="${_('Format: %(datehint)s', datehint=datetime_hint)}"/>
+                     placeholder="${_('Format: %(datehint)s', datehint=datetime_hint)}" />
             </div>
             <script type="text/javascript">
               jQuery(document).ready(function($) {
@@ -98,7 +98,7 @@
             </fieldset>
           </div>
           <div class="control-group">
-            <input class="btn" type="submit" name="cancel"
+            <input class="btn-link" type="submit" name="cancel"
                 value="${_('Cancel')}"/>
             <input class="btn" type="submit" name="save"
                 value="${_('Save')}" disabled="${readonly}"/>
@@ -109,7 +109,7 @@
       <py:otherwise>
         <div py:if="'MILESTONE_CREATE' in req.perm" class="row">
           <div class="span9">
-            <form class="well form-horizontal" id="addmilestone" method="post" action="">
+            <form id="addmilestone" class="well form-horizontal" method="post" action="">
               <fieldset>
                 <legend>Add Milestone</legend>
                 <div class="control-group">
@@ -133,11 +133,6 @@
                   <div class="controls">
                     <input class="btn" type="submit" name="add" value="${_('Add')}" />
                   </div>
-
-                  <p class="help-block">
-                    <span class="label label-info">Hint</span>
-                    <i18n:msg params="datehint">Format: $datetime_hint</i18n:msg>
-                  </p>
                 </div>
               </fieldset>
             </form>
@@ -188,7 +183,7 @@
                       value="${_('Apply changes')}" />
                 </div>
                 <p class="help-block">
-                  <span class="label label-info">Notice</span>
+                  <span class="label label-info">Note</span>
                   You can remove all items from this list to completely hide this
                   field from the user interface.
                 </p>

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_plugins.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_plugins.html?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_plugins.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_plugins.html Wed Feb 27 12:52:42 2013
@@ -27,8 +27,6 @@
   <xi:include href="bh_admin.html" />
   <head>
     <title>Plugins</title>
-    <script type="text/javascript"
-        src="${href.chrome('dashboard/js/bootstrap-collapse.js')}"></script>
     <script type="text/javascript">/*<![CDATA[*/
     jQuery(document).ready(function($) {
       // Sets state of group toggler when component checkboxes are clicked
@@ -72,7 +70,6 @@
                 enctype="multipart/form-data" action="" py:otherwise="">
             <fieldset>
               <legend>Install Plugin:</legend>
-
               <label i18n:msg="" for="plugin_file">File:</label>
               <input class="input-medium" type="file" id="plugin_file" size="10"
                      name="plugin_file" disabled="${readonly or None}" />

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_products.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_products.html?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_products.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_products.html Wed Feb 27 12:52:42 2013
@@ -86,7 +86,7 @@
             </fieldset>
           </div>
           <div class="control-group">
-            <input class="btn" type="submit" name="cancel"
+            <input class="btn-link" type="submit" name="cancel"
                 value="${_('Cancel')}" />
             <input class="btn" type="submit" name="save"
                 value="${_('Save')}" />

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_repositories.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_repositories.html?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_repositories.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_repositories.html Wed Feb 27 12:52:42 2013
@@ -127,7 +127,7 @@
             </fieldset>
           </div>
           <div class="control-group">
-            <input class="btn" type="submit" name="cancel" value="${_('Cancel')}"/>
+            <input class="btn-link" type="submit" name="cancel" value="${_('Cancel')}"/>
             <input class="btn" py:if="info.editable" type="submit"
                    name="save" value="${_('Save')}"/>
           </div>

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_versions.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_versions.html?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_versions.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_admin_versions.html Wed Feb 27 12:52:42 2013
@@ -74,7 +74,7 @@
             </fieldset>
           </div>
           <div class="control-group">
-            <input class="btn" type="submit" name="cancel"
+            <input class="btn-link" type="submit" name="cancel"
                 value="${_('Cancel')}"/>
             <input class="btn" type="submit" name="save"
                 value="${_('Save')}"/>
@@ -84,7 +84,25 @@
 
       <py:otherwise>
         <div class="row">
-          <div class="span6">
+          <div class="span9">
+            <form class="well form-inline" id="addversion" method="post" action="">
+              <fieldset>
+                <legend>Add Version:</legend>
+                <input class="btn" type="submit" name="add"
+                       value="${_('Add')}" />
+                <label class="control-label" for="name">Name:</label>
+                <input class="input-medium" type="text" name="name"
+                       id="name" size="22" />
+                <label class="control-label" for="releaseddate">Released:</label>
+                <input type="text" id="releaseddate"
+                       name="time" size="${len(datetime_hint)}"
+                       value="${format_datetime()}" />
+              </fieldset>
+            </form>
+          </div>
+        </div>
+        <div class="row">
+          <div class="span9">
             <py:choose>
               <form py:when="versions" id="version_table" method="post" action="">
                 <table id="verlist"
@@ -114,7 +132,7 @@
                       value="${_('Apply changes')}" />
                 </div>
                 <p class="help-block">
-                  <span class="label label-info">Notice</span>
+                  <span class="label label-info">Note</span>
                   You can remove all items from this list to completely hide this
                   field from the user interface.
                 </p>
@@ -127,31 +145,7 @@
               </p>
             </py:choose>
           </div>
-          <div class="span3">
-            <form class="well" id="addversion" method="post" action="">
-              <fieldset>
-                <legend>Add Version:</legend>
-                <label for="name">Name:</label>
-                <input class="input-medium" type="text" name="name"
-                    id="name" size="22" />
-                <label for="releaseddate">Released:</label>
-                <input class="input-medium" type="text" id="releaseddate"
-                    name="time" size="${len(datetime_hint)}"
-                    placeholder="${_('Format: %(datehint)s', datehint=datetime_hint)}"
-                    value="${format_datetime()}" />
-                <p class="help-block">
-                  <span class="label label-info">Hint</span>
-                  <i18n:msg params="datehint">Format: $datetime_hint</i18n:msg>
-                </p>
-                <div class="control-group">
-                  <input class="btn" type="submit" name="add"
-                      value="${_('Add')}" />
-                </div>
-              </fieldset>
-            </form>
-          </div>
         </div>
-
       </py:otherwise>
     </py:choose>
   </body>

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_milestone_delete.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_milestone_delete.html?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_milestone_delete.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_milestone_delete.html Wed Feb 27 12:52:42 2013
@@ -61,7 +61,7 @@
           </div>
           <div class="buttons">
             <input type="submit" name="cancel" value="${_('Cancel')}" 
-                class="btn" />
+                class="btn-link" />
             <input type="submit" value="${_('Delete milestone')}" 
                 class="btn" />
           </div>

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_milestone_edit.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_milestone_edit.html?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_milestone_edit.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_milestone_edit.html Wed Feb 27 12:52:42 2013
@@ -148,7 +148,7 @@ ${milestone.description}</textarea></p>
           <input py:otherwise="" type="submit" value="${_('Add milestone')}" 
               class="btn" />
           <input type="submit" name="cancel" value="${_('Cancel')}" 
-              class="btn" />
+                 class="btn-link" />
         </div>
       </form>
 

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_report_delete.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_report_delete.html?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_report_delete.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_report_delete.html Wed Feb 27 12:52:42 2013
@@ -38,7 +38,7 @@
           <div class="buttons">
             <input type="hidden" name="id" value="$report.id"/>
             <input type="hidden" name="action" value="delete" />
-            <input type="submit" class="btn" name="cancel"
+            <input type="submit" class="btn-link" name="cancel"
                 value="${_('Cancel')}"/>
             <input type="submit" class="btn" 
                 value="${_('Delete report')}"/>

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_report_edit.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_report_edit.html?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_report_edit.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_report_edit.html Wed Feb 27 12:52:42 2013
@@ -76,8 +76,8 @@ $report.sql</textarea>
             </div>
             <div class="buttons control-group">
               <input class="btn" type="submit" value="${_('Save report')}"/>
-              <input class="btn" type="submit" name="cancel"
-                  value="${_('Cancel')}"/>
+              <input class="btn-link" type="submit" name="cancel"
+                     value="${_('Cancel')}"/>
             </div>
           </div>
         </form>

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_ticket_change.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_ticket_change.html?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_ticket_change.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_ticket_change.html Wed Feb 27 12:52:42 2013
@@ -134,12 +134,14 @@ Arguments:
 ${edited_comment if edited_comment is not None else change.comment}</textarea>
           <input type="hidden" name="cnum_edit" value="${cnum}"/>
         </div>
-        <div class="buttons">
-          <input type="submit" name="preview_comment" value="${_('Preview')}"
-                 title="${_('Preview changes to comment %(cnum)s', cnum=cnum)}"/>
-          <input type="submit" name="edit_comment" value="${_('Submit changes')}"
-                 title="${_('Submit changes to comment %(cnum)s', cnum=cnum)}"/>
-          <input type="submit" name="cancel_comment" value="${_('Cancel')}"
+        <div class="btn-toolbar buttons">
+          <div class="btn-group">
+            <input class="btn" type="submit" name="preview_comment" value="${_('Preview')}"
+                   title="${_('Preview changes to comment %(cnum)s', cnum=cnum)}"/>
+            <input class="btn" type="submit" name="edit_comment" value="${_('Submit changes')}"
+                   title="${_('Submit changes to comment %(cnum)s', cnum=cnum)}"/>
+          </div>
+          <input class="btn-link" type="submit" name="cancel_comment" value="${_('Cancel')}"
                  title="Cancel comment edit"/>
         </div>
       </form>

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_wiki_delete.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_wiki_delete.html?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_wiki_delete.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_wiki_delete.html Wed Feb 27 12:52:42 2013
@@ -116,7 +116,7 @@
             <strong>This is an irreversible operation.</strong>
           </p>
           <div class="control-group">
-            <input class="btn" type="submit" name="cancel" value="${_('Cancel')}" />
+            <input class="btn-link" type="submit" name="cancel" value="${_('Cancel')}" />
             <input class="btn" type="submit" value="${what == 'multiple' and _('Delete those versions')
                                           or what == 'single' and _('Delete this version')
                                           or _('Delete page')}" />

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_wiki_edit_form.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_wiki_edit_form.html?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_wiki_edit_form.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_wiki_edit_form.html Wed Feb 27 12:52:42 2013
@@ -103,7 +103,7 @@ $page.text</textarea>
         </py:otherwise>
       </div>
       <div class="btn-group">
-        <input class="btn" type="submit" name="cancel" value="${_('Cancel')}" />
+        <input class="btn-link" type="submit" name="cancel" value="${_('Cancel')}" />
       </div>
     </div>
   </form>

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_wiki_rename.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_wiki_rename.html?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_wiki_rename.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bh_wiki_rename.html Wed Feb 27 12:52:42 2013
@@ -49,7 +49,7 @@
           </label>
           <br/><br/>
           <div class="control-group">
-            <input class="btn" type="submit" name="cancel" value="${_('Cancel')}"/>
+            <input class="btn-link" type="submit" name="cancel" value="${_('Cancel')}"/>
             <input class="btn" type="submit" name="submit" value="${_('Rename page')}"/>
           </div>
         </form>

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bloodhound_theme.html
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bloodhound_theme.html?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bloodhound_theme.html (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/templates/bloodhound_theme.html Wed Feb 27 12:52:42 2013
@@ -40,20 +40,8 @@
 
     <script src="${href.chrome('theme/js/theme.js')}"
         type="text/javascript"></script>
-    <script src="${href.chrome('dashboard/js/bootstrap-transition.js')}"
+    <script src="${href.chrome('dashboard/js/bootstrap.js')}"
         type="text/javascript"></script>
-    <script src="${href.chrome('dashboard/js/bootstrap-alert.js')}"
-        type="text/javascript"></script>
-    <script src="${href.chrome('dashboard/js/bootstrap-dropdown.js')}"
-        type="text/javascript"></script>
-    <script src="${href.chrome('dashboard/js/bootstrap-tooltip.js')}"
-        type="text/javascript"></script>
-    <script src="${href.chrome('dashboard/js/bootstrap-popover.js')}"
-        type="text/javascript"></script> 
-    <script src="${href.chrome('dashboard/js/bootstrap-affix.js')}"
-        type="text/javascript"></script> 
-    <script src="${href.chrome('dashboard/js/bootstrap-button.js')}"
-        type="text/javascript"></script> 
 
   </head></py:match>
 
@@ -65,8 +53,10 @@
           <!--! logo -->
           <div id="logo" class="span4">
             <p>
-              <img src="${chrome.logo.src or href.chrome('theme/img/bh_logo.png')}"
-                  alt="Bloodhound Logo" />
+              <a href="${chrome.logo.link or href()}">
+                <img src="${chrome.logo.src or href.chrome('theme/img/bh_logo.png')}"
+                     alt="Bloodhound Logo" />
+              </a>
             </p>
           </div>
           <!--! top menu (login, logout...) -->

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/theme.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/theme.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/theme.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/bhtheme/theme.py Wed Feb 27 12:52:42 2013
@@ -149,7 +149,7 @@ class BloodhoundTheme(ThemeBase):
             footer_left_prefix = self.labels_footer_left_prefix,
             footer_left_postfix = self.labels_footer_left_postfix,
             footer_right = self.labels_footer_right,
-            application_version = ".".join(map(str, application_version)))
+            application_version = application_version)
 
     # ITemplateStreamFilter methods
 
@@ -307,7 +307,6 @@ class BloodhoundTheme(ThemeBase):
     def _modify_ticket(self, req, template, data, content_type, is_active):
         """Ticket modifications
         """
-        self._modify_scrollspy(req, template, data, content_type, is_active)
         self._modify_resource_breadcrumb(req, template, data, content_type,
                                          is_active)
 
@@ -323,11 +322,6 @@ class BloodhoundTheme(ThemeBase):
                 res = Resource(resname, data['ticket'][resname])
                 data['path_show_' + resname] = permname in req.perm(res)
 
-    def _modify_scrollspy(self, req, template, data, content_type, is_active):
-        """Insert Bootstrap scroll spy files.
-        """
-        add_script(req, 'dashboard/js/bootstrap-scrollspy.js')
-
     # INavigationContributor methods
 
     def get_active_navigation_item(self, req):
@@ -446,5 +440,4 @@ class QuickCreateTicketDialog(Component)
                                    "of ticket #%s: %s" % (t.id, e))
         return t.id
 
-application_version = tuple(int(i) for i in get_distribution('BloodhoundTheme')
-    .parsed_version if i.startswith('0'))
+application_version = get_distribution('BloodhoundTheme').version

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/setup.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/setup.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/setup.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_theme/setup.py Wed Feb 27 12:52:42 2013
@@ -23,7 +23,7 @@ import sys
 
 setup(
   name = 'BloodhoundTheme',
-  version = '0.4.0',
+  version = '0.5.0',
   description = "Theme for Apache(TM) Bloodhound.",
   author = "Apache Bloodhound",
   license = "Apache License v2",

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/installer/bloodhound_setup.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/installer/bloodhound_setup.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/installer/bloodhound_setup.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/installer/bloodhound_setup.py Wed Feb 27 12:52:42 2013
@@ -68,7 +68,7 @@ BASE_CONFIG = {'components': {'bhtheme.*
                               },
                'header_logo': {'src': '',},
                'mainnav': {'roadmap': 'disabled',
-                           'source': 'disabled',
+                           'search': 'disabled',
                            'timeline': 'disabled',
                            'browser.label': 'Source',
                            'tickets.label': 'Tickets',},

Propchange: incubator/bloodhound/branches/bep_0003_multiproduct/trac/
------------------------------------------------------------------------------
--- svn:ignore (original)
+++ svn:ignore Wed Feb 27 12:52:42 2013
@@ -1 +1,2 @@
 *.egg-info
+*.egg

Propchange: incubator/bloodhound/branches/bep_0003_multiproduct/trac/
------------------------------------------------------------------------------
  Merged /incubator/bloodhound/trunk/trac:r1446486-1450761

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/trac/trac/ticket/api.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/trac/trac/ticket/api.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/trac/trac/ticket/api.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/trac/trac/ticket/api.py Wed Feb 27 12:52:42 2013
@@ -156,6 +156,31 @@ class IMilestoneChangeListener(Interface
     def milestone_deleted(milestone):
         """Called when a milestone is deleted."""
 
+class IResourceChangeListener(Interface):
+    """Extension point interface for components that require notification
+    when resources are created, modified, or deleted.
+
+    'resource' instance of the a resource e.g. ticket, milestone etc.
+    'context' action context, may contain author, comment etc. Context
+    content depends on a resource type.
+    """
+
+    def resource_created(resource, context):
+        """
+        Called when a resource is created.
+        """
+
+    def resource_changed(resource, old_values, context):
+        """Called when a resource is modified.
+
+        `old_values` is a dictionary containing the previous values of the
+        resource properties that changed. Properties are specific for resource
+        type.
+        """
+
+    def resource_deleted(resource, context):
+        """Called when a resource is deleted."""
+
 class ITicketFieldProvider(Interface):
     """Extension point interface for components that provide fields for the
     ticket system."""
@@ -193,6 +218,7 @@ class TicketSystem(Component):
     ticket_field_providers = ExtensionPoint(ITicketFieldProvider)
     change_listeners = ExtensionPoint(ITicketChangeListener)
     milestone_change_listeners = ExtensionPoint(IMilestoneChangeListener)
+    resource_change_listeners = ExtensionPoint(IResourceChangeListener)
 
     ticket_custom_section = ConfigSection('ticket-custom',
         """In this section, you can define additional fields for tickets. See

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/trac/trac/ticket/model.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/trac/trac/ticket/model.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/trac/trac/ticket/model.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/trac/trac/ticket/model.py Wed Feb 27 12:52:42 2013
@@ -844,9 +844,12 @@ class Component(object):
         with self.env.db_transaction as db:
             self.env.log.info("Deleting component %s", self.name)
             db("DELETE FROM component WHERE name=%s", (self.name,))
-            self.name = self._old_name = None
             TicketSystem(self.env).reset_ticket_fields()
 
+        for listener in TicketSystem(self.env).resource_change_listeners:
+            listener.resource_deleted(self)
+        self.name = self._old_name = None
+
     def insert(self, db=None):
         """Insert a new component.
 
@@ -866,6 +869,9 @@ class Component(object):
             self._old_name = self.name
             TicketSystem(self.env).reset_ticket_fields()
 
+        for listener in TicketSystem(self.env).resource_change_listeners:
+            listener.resource_created(self)
+
     def update(self, db=None):
         """Update the component.
 
@@ -877,6 +883,7 @@ class Component(object):
         if not self.name:
             raise TracError(_("Invalid component name."))
 
+        old_name = self._old_name
         with self.env.db_transaction as db:
             self.env.log.info("Updating component '%s'", self.name)
             db("""UPDATE component SET name=%s,owner=%s, description=%s
@@ -890,6 +897,10 @@ class Component(object):
                 self._old_name = self.name
             TicketSystem(self.env).reset_ticket_fields()
 
+        old_values = dict(name=old_name)
+        for listener in TicketSystem(self.env).resource_change_listeners:
+            listener.resource_changed(self, old_values)
+
     @classmethod
     def select(cls, env, db=None):
         """

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/trac/trac/ticket/tests/model.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/trac/trac/ticket/tests/model.py?rev=1450762&r1=1450761&r2=1450762&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/trac/trac/ticket/tests/model.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/trac/trac/ticket/tests/model.py Wed Feb 27 12:52:42 2013
@@ -15,7 +15,8 @@ from trac.ticket.model import (
     Ticket, Component, Milestone, Priority, Type, Version
 )
 from trac.ticket.api import (
-    IMilestoneChangeListener, ITicketChangeListener, TicketSystem
+    IMilestoneChangeListener, ITicketChangeListener, TicketSystem,
+    IResourceChangeListener,
 )
 from trac.test import EnvironmentStub
 from trac.util.datefmt import from_utimestamp, to_utimestamp, utc
@@ -1005,6 +1006,27 @@ class MilestoneTestCase(unittest.TestCas
         self.assertEqual('deleted', listener.action)
         self.assertEqual(milestone, listener.milestone)
 
+class TestResourceChangeListener(core.Component):
+    implements(IResourceChangeListener)
+
+    def callback(self, action, resource, old_values = None):
+        pass
+
+    def resource_created(self, resource):
+        self.action = "created"
+        self.resource = resource
+        self.callback(self.action, self.resource)
+
+    def resource_changed(self, resource, old_values):
+        self.action = "changed"
+        self.resource = resource
+        self.old_values = old_values
+        self.callback(self.action, self.resource, old_values=self.old_values)
+
+    def resource_deleted(self, resource):
+        self.action = "deleted"
+        self.resource = resource
+        self.callback(self.action, self.resource)
 
 class ComponentTestCase(unittest.TestCase):
 
@@ -1041,6 +1063,49 @@ class ComponentTestCase(unittest.TestCas
         self.assertEqual([('Test', 'joe', None)], self.env.db_query(
             "SELECT name, owner, description FROM component WHERE name='Test'"))
 
+    def test_change_listener_created(self):
+        listener = TestResourceChangeListener(self.env)
+        self._create_component(name='Component 1')
+        self.assertEqual('created', listener.action)
+        self.assertIsInstance(listener.resource, Component)
+        self.assertEqual('Component 1', listener.resource.name)
+
+    def test_change_listener_changed(self):
+        listener = TestResourceChangeListener(self.env)
+        component = self._create_component(name='Component 1')
+        component.name = 'Component 2'
+        component.update()
+        self.assertEqual('changed', listener.action)
+        self.assertIsInstance(listener.resource, Component)
+        self.assertEqual('Component 2', listener.resource.name)
+        self.assertEqual("Component 1" ,listener.old_values["name"])
+
+    def test_change_listener_deleted(self):
+        listener = TestResourceChangeListener(self.env)
+
+        #component.name property is set to None during delete operation
+        #We need mechanism to remember component name
+        listener.callback = self.listener_callback
+
+        component = self._create_component(name='Component 1')
+        component.delete()
+        self.assertEqual('deleted', listener.action)
+        self.assertIsInstance(listener.resource, Component)
+        self.assertEqual('Component 1', self.resource_name)
+
+    def listener_callback(self, action, resource, old_values = None):
+        self.resource_name = resource.name
+
+    def _create_component(self, name, description = None, owner=None):
+        component = Component(self.env)
+        component.name = name
+        if description is None:
+            component.description = description
+        if owner is None:
+            component.owner = owner
+        component.insert()
+        return component
+
 class VersionTestCase(unittest.TestCase):
 
     def setUp(self):



Mime
View raw message