Example #1
0
 def abs_href(self):
     """The application URL"""
     if not self.base_url:
         urlpattern = MultiProductSystem(self.parent).product_base_url
         if not urlpattern:
             self.log.warn("product_base_url option not set in "
                           "configuration, generated links may be "
                           "incorrect")
             urlpattern = 'products/$(prefix)s'
         envname = os.path.basename(self.parent.path)
         prefix = unicode_quote(self.product.prefix, safe="")
         name = unicode_quote(self.product.name, safe="")
         url = urlpattern.replace('$(', '%(') \
                         .replace('%(envname)s', envname) \
                         .replace('%(prefix)s', prefix) \
                         .replace('%(name)s', name)
         if urlsplit(url).netloc:
             #  Absolute URLs
             _abs_href = Href(url)
         else:
             # Relative URLs
             parent_href = Href(self.parent.abs_href(),
                                path_safe="/!~*'()%",
                                query_safe="!~*'()%")
             _abs_href = Href(parent_href(url))
     else:
         _abs_href = Href(self.base_url)
     return _abs_href
Example #2
0
 def abs_href(self):
     """The application URL"""
     if not self.base_url:
         urlpattern = MultiProductSystem(self.parent).product_base_url
         if not urlpattern:
             self.log.warn("product_base_url option not set in "
                           "configuration, generated links may be "
                           "incorrect")
             urlpattern = 'products/$(prefix)s'
         envname = os.path.basename(self.parent.path)
         prefix = unicode_quote(self.product.prefix, safe="")
         name = unicode_quote(self.product.name, safe="")
         url = urlpattern.replace('$(', '%(') \
                         .replace('%(envname)s', envname) \
                         .replace('%(prefix)s', prefix) \
                         .replace('%(name)s', name)
         if urlsplit(url).netloc:
             #  Absolute URLs
             _abs_href = Href(url)
         else:
             # Relative URLs
             parent_href = Href(self.parent.abs_href(),
                                path_safe="/!~*'()%",
                                query_safe="!~*'()%")
             _abs_href = Href(parent_href(url))
     else:
         _abs_href = Href(self.base_url)
     return _abs_href
Example #3
0
    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())
Example #4
0
 def _setup_multiproduct(self):
     try:
         MultiProductSystem(self.env)\
             .upgrade_environment(self.env.db_transaction)
     except self.env.db_exc.OperationalError:
         # table remains but content is deleted
         self._add_products('@')
     self.env.enable_multiproduct_schema()
Example #5
0
    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)

        mpsystem = MultiProductSystem(env)
        try:
            mpsystem.upgrade_environment(env.db_transaction)
        except OperationalError:
            # Database is upgraded, but database version was deleted.
            # Complete the upgrade by inserting default product.
            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)
Example #6
0
    def _default_product(self, envname=None):
        """Default product configured for a given environment

        @raise LookupError: if no environment matching `envname` can be found
        """
        if envname not in ('trac', None):
            raise LookupError('Unable to open environment ' + envname)
        env = self.get_trac_environment()
        return MultiProductSystem(env).default_product_prefix
Example #7
0
    def setup_config(self):
        """Load the configuration object.
        """
        import trac.config

        parent_path = MultiProductSystem(self.parent).product_config_parent
        if parent_path and os.path.isfile(parent_path):
            parents = [trac.config.Configuration(parent_path)]
        else:
            parents = [self.parent.config]
        self.config = Configuration(self.parent, self.product.prefix, parents)
        self.setup_log()
Example #8
0
    def setUp(self):
        self.env = EnvironmentStub(enable=['trac.*', 'multiproduct.*'])
        self.env.path = tempfile.mkdtemp(prefix='bh-product-tempenv-')

        self.mpsystem = MultiProductSystem(self.env)
        try:
            self.mpsystem.upgrade_environment(self.env.db_transaction)
        except self.env.db_exc.OperationalError:
            # table remains but database version is deleted
            pass

        self.listener = self._enable_resource_change_listener()
        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()
Example #9
0
    def test_can_upgrade_multi_product_from_v2(self):
        mp = MultiProductSystem(self.env)
        with self.env.db_direct_transaction as db:
            mp._add_column_product_to_ticket(db)
            mp._create_multiproduct_tables(db)
            mp._replace_product_on_ticket_with_product_prefix(db)
            mp._update_db_version(db, 2)

            db("""INSERT INTO bloodhound_product (prefix, name)
                       VALUES ('p1', 'Product 1')""")
            db("""INSERT INTO ticket (id, product)
                       VALUES (1, 'p1')""")
            db("""INSERT INTO ticket (id)
                       VALUES (2)""")

        self._enable_multiproduct()
        self.env.upgrade()

        with self.product('p1'):
            Ticket(self.env, 1)
        with self.product('@'):
            Ticket(self.env, 2)
Example #10
0
    def test_can_upgrade_multi_product_from_v2(self):
        mp = MultiProductSystem(self.env)
        with self.env.db_direct_transaction as db:
            mp._add_column_product_to_ticket(db)
            mp._create_multiproduct_tables(db)
            mp._replace_product_on_ticket_with_product_prefix(db)
            mp._update_db_version(db, 2)

            db("""INSERT INTO bloodhound_product (prefix, name)
                       VALUES ('p1', 'Product 1')""")
            db("""INSERT INTO ticket (id, product)
                       VALUES (1, 'p1')""")
            db("""INSERT INTO ticket (id)
                       VALUES (2)""")

        self._enable_multiproduct()
        self.env.upgrade()

        with self.product('p1'):
            Ticket(self.env, 1)
        with self.product('@'):
            Ticket(self.env, 2)
Example #11
0
    def open_environment(self, environ, env_path, global_env, use_cache=False):
        environ.setdefault('SCRIPT_NAME', '')  # bh:ticket:594

        env = pid = None
        path_info = environ.get('PATH_INFO')
        if not path_info:
            return env
        m = PRODUCT_RE.match(path_info)
        if m:
            pid = m.group('pid')

        def create_product_env(product_prefix, script_name, path_info):
            if not global_env._abs_href:
                # make sure global environment absolute href is set before
                # instantiating product environment. This would normally
                # happen from within trac.web.main.dispatch_request
                req = RequestWithSession(environ, None)
                global_env._abs_href = req.abs_href
            try:
                env = multiproduct.env.ProductEnvironment(
                    global_env, product_prefix)
            except LookupError:
                # bh:ticket:561 - Display product list and warning message
                env = global_env
            else:
                # shift WSGI environment to the left
                environ['SCRIPT_NAME'] = script_name
                environ['PATH_INFO'] = path_info
            return env

        if pid:
            env = create_product_env(
                pid, environ['SCRIPT_NAME'] + '/products/' + pid,
                m.group('pathinfo') or '')
        else:
            redirect = REDIRECT_DEFAULT_RE.match(path_info)
            if redirect:
                from multiproduct.api import MultiProductSystem
                default_product_prefix = \
                    MultiProductSystem(global_env).default_product_prefix
                env = create_product_env(default_product_prefix,
                                         environ['SCRIPT_NAME'],
                                         environ['PATH_INFO'])
        return env
Example #12
0
    def _upgrade_mp(cls, env):
        r"""Apply multi product upgrades
        """
        # Do not break wiki parser ( see #373 )
        EnvironmentStub.disable_component_in_config(env, TicketModule)
        EnvironmentStub.disable_component_in_config(env, ReportModule)

        mpsystem = MultiProductSystem(env)
        with env.db_transaction as db:
            try:
                mpsystem.upgrade_environment(db)
            except env.db_exc.OperationalError:
                # Database is upgraded, but database version was deleted.
                # Complete the upgrade by inserting default product.
                mpsystem._insert_default_product(db)
            finally:
                # Ensure that multiproduct DB version is set to latest value
                mpsystem._update_db_version(db, DB_VERSION)
        # assume that the database schema has been upgraded, enable
        # multi-product schema support in environment
        env.enable_multiproduct_schema(True)
Example #13
0
 def setUp(self):
     self.env = EnvironmentStub(enable=['trac.*', 'multiproduct.*'])
     self.env.path = tempfile.mkdtemp('bh-product-tempenv')
     
     self.mpsystem = MultiProductSystem(self.env)
     try:
         self.mpsystem.upgrade_environment(self.env.db_transaction)
     except OperationalError:
         # table remains but database version is deleted
         pass
     
     self.listener = self._enable_resource_change_listener()
     self.default_data = {'prefix':self.INITIAL_PREFIX,
                          'name':self.INITIAL_NAME,
                          'description':self.INITIAL_DESCRIPTION}
     
     self.product = Product(self.env)
     self.product._data.update(self.default_data)
     self.product.insert()
Example #14
0
    def test_upgrade_copies_content_of_system_tables_to_all_products(self):
        mp = MultiProductSystem(self.env)
        with self.env.db_direct_transaction as db:
            mp._add_column_product_to_ticket(db)
            mp._create_multiproduct_tables(db)
            mp._update_db_version(db, 1)
            for i in range(1, 6):
                db("""INSERT INTO bloodhound_product (prefix, name)
                           VALUES ('p%d', 'Product 1')""" % i)
            for table in ('component', 'milestone', 'enum', 'version',
                          'permission', 'report'):
                db("""DELETE FROM %s""" % table)
            db("""INSERT INTO component (name) VALUES ('foobar')""")
            db("""INSERT INTO milestone (name) VALUES ('foobar')""")
            db("""INSERT INTO version (name) VALUES ('foobar')""")
            db("""INSERT INTO enum (type, name) VALUES ('a', 'b')""")
            db("""INSERT INTO permission VALUES ('x', 'TICKET_VIEW')""")
            db("""INSERT INTO report (title) VALUES ('x')""")

        self._enable_multiproduct()
        self.env.upgrade()

        with self.env.db_direct_transaction as db:
            for table in ('component', 'milestone', 'version', 'enum',
                          'report'):
                rows = db("SELECT * FROM %s" % table)
                self.assertEqual(
                    len(rows), 6,
                    "Wrong number of lines in %s (%d instead of %d)\n%s" %
                    (table, len(rows), 6, rows))
            for table in ('permission', ):
                # Permissions also hold rows for global product.
                rows = db("SELECT * FROM %s WHERE username='******'" % table)
                self.assertEqual(
                    len(rows), 7,
                    "Wrong number of lines in %s (%d instead of %d)\n%s" %
                    (table, len(rows), 7, rows))
Example #15
0
    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)

        mpsystem = MultiProductSystem(env)
        try:
            mpsystem.upgrade_environment(env.db_transaction)
        except OperationalError:
            # Database is upgraded, but database version was deleted.
            # Complete the upgrade by inserting default product.
            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)
Example #16
0
    def test_product_list(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'

        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
        with self.assertRaises(RequestDone):
            self._dispatch(req, self.global_env)
Example #17
0
    def test_upgrade_copies_content_of_system_tables_to_all_products(self):
        mp = MultiProductSystem(self.env)
        with self.env.db_direct_transaction as db:
            mp._add_column_product_to_ticket(db)
            mp._create_multiproduct_tables(db)
            mp._update_db_version(db, 1)
            for i in range(1, 6):
                db("""INSERT INTO bloodhound_product (prefix, name)
                           VALUES ('p%d', 'Product 1')""" % i)
            for table in ('component', 'milestone', 'enum', 'version',
                          'permission', 'report'):
                db("""DELETE FROM %s""" % table)
            db("""INSERT INTO component (name) VALUES ('foobar')""")
            db("""INSERT INTO milestone (name) VALUES ('foobar')""")
            db("""INSERT INTO version (name) VALUES ('foobar')""")
            db("""INSERT INTO enum (type, name) VALUES ('a', 'b')""")
            db("""INSERT INTO permission VALUES ('x', 'TICKET_VIEW')""")
            db("""INSERT INTO report (title) VALUES ('x')""")

        self._enable_multiproduct()
        self.env.upgrade()

        with self.env.db_direct_transaction as db:
            for table in ('component', 'milestone', 'version', 'enum',
                          'report'):
                rows = db("SELECT * FROM %s" % table)
                self.assertEqual(
                    len(rows), 6,
                    "Wrong number of lines in %s (%d instead of %d)\n%s"
                    % (table, len(rows), 6, rows))
            for table in ('permission',):
                # Permissions also hold rows for global product.
                rows = db("SELECT * FROM %s WHERE username='******'" % table)
                self.assertEqual(
                    len(rows), 7,
                    "Wrong number of lines in %s (%d instead of %d)\n%s"
                    % (table, len(rows), 7, rows))
Example #18
0
class ProductTestCase(unittest.TestCase):
    """Unit tests covering the Product model"""
    INITIAL_PREFIX = 'tp'
    INITIAL_NAME = 'test project'
    INITIAL_DESCRIPTION = 'a test project'

    def setUp(self):
        self.env = EnvironmentStub(enable=['trac.*', 'multiproduct.*'])
        self.env.path = tempfile.mkdtemp(prefix='bh-product-tempenv-')

        self.mpsystem = MultiProductSystem(self.env)
        try:
            self.mpsystem.upgrade_environment(self.env.db_transaction)
        except self.env.db_exc.OperationalError:
            # table remains but database version is deleted
            pass

        self.listener = self._enable_resource_change_listener()
        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()

    def tearDown(self):
        shutil.rmtree(self.env.path)
        self.env.reset_db()

    def _enable_resource_change_listener(self):
        listener = TestResourceChangeListener(self.env)
        listener.resource_type = Product
        listener.callback = self.listener_callback
        return listener

    def listener_callback(self, action, resource, context, old_values = None):
        # pylint: disable=unused-argument
        # pylint: disable=attribute-defined-outside-init
        self.prefix = resource.prefix
        self.name = resource.name
        self.description = resource.description

    def test_set_table_field(self):
        """tests that table.field style update works"""
        test = {'prefix': 'td',
                'name': 'test field access',
                'description': 'product to test field setting'}

        product = Product(self.env)

        # attempt to set the fields from the data
        product.prefix = test['prefix']
        product.name = test['name']
        product.description = test['description']

        self.assertEqual(product._data['prefix'], test['prefix'])
        self.assertEqual(product._data['name'], test['name'])
        self.assertEqual(product._data['description'], test['description'])

    def test_select(self):
        """tests that select can search Products by fields"""

        p2_data = {'prefix':'tp2',
                   'name':'test project 2',
                   'description':'a different test project'}
        p3_data = {'prefix':'tp3',
                   'name':'test project 3',
                   'description':'test project'}

        product2 = Product(self.env)
        product2._data.update(p2_data)
        product3 = Product(self.env)
        product3._data.update(p3_data)

        product2.insert()
        product3.insert()

        products = list(Product.select(self.env, where={'prefix':'tp'}))
        self.assertEqual(1, len(products))
        products = list(Product.select(self.env,
            where={'name':'test project'}))
        self.assertEqual(1, len(products))
        products = list(Product.select(self.env,
            where={'prefix':'tp3', 'name':'test project 3'}))
        self.assertEqual(1, len(products))

    def test_update(self):
        """tests that we can use update to push data to the database"""
        product = list(Product.select(self.env, where={'prefix':'tp'}))[0]
        self.assertEqual('test project', product._data['name'])

        new_data = {'prefix':'tp',
                    'name':'updated',
                    'description':'nothing'}
        product._data.update(new_data)
        product.update()

        comp_product = list(Product.select(self.env, where={'prefix':'tp'}))[0]
        self.assertEqual('updated', comp_product._data['name'])

    def test_update_key_change(self):
        """tests that we raise an error for attempting to update key fields"""
        bad_data = {'prefix':'tp0',
                    'name':'update',
                    'description':'nothing'}
        product = list(Product.select(self.env, where={'prefix':'tp'}))[0]
        product._data.update(bad_data)
        self.assertRaises(TracError, product.update)

    def test_insert(self):
        """test saving new Product"""
        data = {'prefix':'new', 'name':'new', 'description':'new'}
        product = Product(self.env)
        product._data.update(data)
        product.insert()

        check_products = list(Product.select(self.env, where={'prefix':'new'}))

        self.assertEqual(product._data['prefix'],
                         check_products[0]._data['prefix'])
        self.assertEqual(1, len(check_products))

    def test_insert_duplicate_key(self):
        """test attempted saving of Product with existing key fails"""
        dupe_key_data = {'prefix':'tp',
                         'name':'dupe',
                         'description':'dupe primary key'}
        product2 = Product(self.env)
        product2._data.update(dupe_key_data)
        self.assertRaises(TracError, product2.insert)

    def test_delete(self):
        """test that we are able to delete Products"""
        product = list(Product.select(self.env, where={'prefix':'tp'}))[0]
        product.delete()

        post = list(Product.select(self.env, where={'prefix':'tp'}))
        self.assertEqual(0, len(post))

    def test_delete_twice(self):
        """test that we error when deleting twice on the same key"""
        product = list(Product.select(self.env, where={'prefix':'tp'}))[0]
        product.delete()

        self.assertRaises(TracError, product.delete)

    def test_field_data_get(self):
        """tests that we can use table.field syntax to get to the field data"""
        prefix = self.default_data['prefix']
        name = self.default_data['name']
        description = self.default_data['description']
        product = list(Product.select(self.env, where={'prefix':prefix}))[0]
        self.assertEqual(prefix, product.prefix)
        self.assertEqual(name, product.name)
        self.assertEqual(description, product.description)

    def test_field_set(self):
        """tests that we can use table.field = something to set field data"""
        prefix = self.default_data['prefix']
        product = list(Product.select(self.env, where={'prefix':prefix}))[0]

        new_description = 'test change of description'
        product.description = new_description
        self.assertEqual(new_description, product.description)

    def test_missing_unique_fields(self):
        """ensure that that insert method works when _meta does not specify
        unique fields when inserting more than one ProductResourceMap instances
        """
        class TestModel(ModelBase):
            """A test model with no unique_fields"""
            _meta = {'table_name': 'bloodhound_testmodel',
                     'object_name': 'TestModelObject',
                     'key_fields': ['id',],
                     'non_key_fields': ['value'],
                     'unique_fields': [],}

        from trac.db import DatabaseManager
        schema = [TestModel._get_schema(), ]
        with self.env.db_transaction as db:
            db_connector, dummy = DatabaseManager(self.env)._get_connector()
            for table in schema:
                for statement in db_connector.to_sql(table):
                    db(statement)

        structure =  dict([(table.name, [col.name for col in table.columns])
                           for table in schema])
        tm1 = TestModel(self.env)
        tm1._data.update({'id':1, 'value':'value1'})
        tm1.insert()
        tm2 = TestModel(self.env)
        tm2._data.update({'id':2, 'value':'value2'})
        tm2.insert()

    def test_change_listener_created(self):
        self.assertEqual('created', self.listener.action)
        self.assertIsInstance(self.listener.resource, Product)
        self.assertEqual(self.INITIAL_PREFIX, self.prefix)
        self.assertEqual(self.INITIAL_NAME, self.name)
        self.assertEqual(self.INITIAL_DESCRIPTION, self.description)

    def test_change_listener_changed(self):
        CHANGED_NAME = "changed name"
        self.product.name = CHANGED_NAME
        self.product.update()
        self.assertEqual('changed', self.listener.action)
        self.assertIsInstance(self.listener.resource, Product)
        self.assertEqual(CHANGED_NAME, self.name)
        self.assertEqual({"name":self.INITIAL_NAME}, self.listener.old_values)

    def test_change_listener_deleted(self):
        self.product.delete()
        self.assertEqual('deleted', self.listener.action)
        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)
Example #19
0
class ProductTestCase(unittest.TestCase):
    """Unit tests covering the Product model"""
    INITIAL_PREFIX = 'tp'
    INITIAL_NAME = 'test project'
    INITIAL_DESCRIPTION = 'a test project'

    def setUp(self):
        self.env = EnvironmentStub(enable=['trac.*', 'multiproduct.*'])
        self.env.path = tempfile.mkdtemp(prefix='bh-product-tempenv-')

        self.mpsystem = MultiProductSystem(self.env)
        try:
            self.mpsystem.upgrade_environment(self.env.db_transaction)
        except self.env.db_exc.OperationalError:
            # table remains but database version is deleted
            pass

        self.listener = self._enable_resource_change_listener()
        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()

    def tearDown(self):
        shutil.rmtree(self.env.path)
        self.env.reset_db()

    def _enable_resource_change_listener(self):
        listener = TestResourceChangeListener(self.env)
        listener.resource_type = Product
        listener.callback = self.listener_callback
        return listener

    def listener_callback(self, action, resource, context, old_values=None):
        # pylint: disable=unused-argument
        # pylint: disable=attribute-defined-outside-init
        self.prefix = resource.prefix
        self.name = resource.name
        self.description = resource.description

    def test_set_table_field(self):
        """tests that table.field style update works"""
        test = {
            'prefix': 'td',
            'name': 'test field access',
            'description': 'product to test field setting'
        }

        product = Product(self.env)

        # attempt to set the fields from the data
        product.prefix = test['prefix']
        product.name = test['name']
        product.description = test['description']

        self.assertEqual(product._data['prefix'], test['prefix'])
        self.assertEqual(product._data['name'], test['name'])
        self.assertEqual(product._data['description'], test['description'])

    def test_select(self):
        """tests that select can search Products by fields"""

        p2_data = {
            'prefix': 'tp2',
            'name': 'test project 2',
            'description': 'a different test project'
        }
        p3_data = {
            'prefix': 'tp3',
            'name': 'test project 3',
            'description': 'test project'
        }

        product2 = Product(self.env)
        product2._data.update(p2_data)
        product3 = Product(self.env)
        product3._data.update(p3_data)

        product2.insert()
        product3.insert()

        products = list(Product.select(self.env, where={'prefix': 'tp'}))
        self.assertEqual(1, len(products))
        products = list(
            Product.select(self.env, where={'name': 'test project'}))
        self.assertEqual(1, len(products))
        products = list(
            Product.select(self.env,
                           where={
                               'prefix': 'tp3',
                               'name': 'test project 3'
                           }))
        self.assertEqual(1, len(products))

    def test_update(self):
        """tests that we can use update to push data to the database"""
        product = list(Product.select(self.env, where={'prefix': 'tp'}))[0]
        self.assertEqual('test project', product._data['name'])

        new_data = {
            'prefix': 'tp',
            'name': 'updated',
            'description': 'nothing'
        }
        product._data.update(new_data)
        product.update()

        comp_product = list(Product.select(self.env, where={'prefix':
                                                            'tp'}))[0]
        self.assertEqual('updated', comp_product._data['name'])

    def test_update_key_change(self):
        """tests that we raise an error for attempting to update key fields"""
        bad_data = {
            'prefix': 'tp0',
            'name': 'update',
            'description': 'nothing'
        }
        product = list(Product.select(self.env, where={'prefix': 'tp'}))[0]
        product._data.update(bad_data)
        self.assertRaises(TracError, product.update)

    def test_insert(self):
        """test saving new Product"""
        data = {'prefix': 'new', 'name': 'new', 'description': 'new'}
        product = Product(self.env)
        product._data.update(data)
        product.insert()

        check_products = list(Product.select(self.env, where={'prefix':
                                                              'new'}))

        self.assertEqual(product._data['prefix'],
                         check_products[0]._data['prefix'])
        self.assertEqual(1, len(check_products))

    def test_insert_duplicate_key(self):
        """test attempted saving of Product with existing key fails"""
        dupe_key_data = {
            'prefix': 'tp',
            'name': 'dupe',
            'description': 'dupe primary key'
        }
        product2 = Product(self.env)
        product2._data.update(dupe_key_data)
        self.assertRaises(TracError, product2.insert)

    def test_delete(self):
        """test that we are able to delete Products"""
        product = list(Product.select(self.env, where={'prefix': 'tp'}))[0]
        product.delete()

        post = list(Product.select(self.env, where={'prefix': 'tp'}))
        self.assertEqual(0, len(post))

    def test_delete_twice(self):
        """test that we error when deleting twice on the same key"""
        product = list(Product.select(self.env, where={'prefix': 'tp'}))[0]
        product.delete()

        self.assertRaises(TracError, product.delete)

    def test_field_data_get(self):
        """tests that we can use table.field syntax to get to the field data"""
        prefix = self.default_data['prefix']
        name = self.default_data['name']
        description = self.default_data['description']
        product = list(Product.select(self.env, where={'prefix': prefix}))[0]
        self.assertEqual(prefix, product.prefix)
        self.assertEqual(name, product.name)
        self.assertEqual(description, product.description)

    def test_field_set(self):
        """tests that we can use table.field = something to set field data"""
        prefix = self.default_data['prefix']
        product = list(Product.select(self.env, where={'prefix': prefix}))[0]

        new_description = 'test change of description'
        product.description = new_description
        self.assertEqual(new_description, product.description)

    def test_missing_unique_fields(self):
        """ensure that that insert method works when _meta does not specify
        unique fields when inserting more than one ProductResourceMap instances
        """
        class TestModel(ModelBase):
            """A test model with no unique_fields"""
            _meta = {
                'table_name': 'bloodhound_testmodel',
                'object_name': 'TestModelObject',
                'key_fields': [
                    'id',
                ],
                'non_key_fields': ['value'],
                'unique_fields': [],
            }

        from trac.db import DatabaseManager
        schema = [
            TestModel._get_schema(),
        ]
        with self.env.db_transaction as db:
            db_connector, dummy = DatabaseManager(self.env)._get_connector()
            for table in schema:
                for statement in db_connector.to_sql(table):
                    db(statement)

        structure = dict([(table.name, [col.name for col in table.columns])
                          for table in schema])
        tm1 = TestModel(self.env)
        tm1._data.update({'id': 1, 'value': 'value1'})
        tm1.insert()
        tm2 = TestModel(self.env)
        tm2._data.update({'id': 2, 'value': 'value2'})
        tm2.insert()

    def test_change_listener_created(self):
        self.assertEqual('created', self.listener.action)
        self.assertIsInstance(self.listener.resource, Product)
        self.assertEqual(self.INITIAL_PREFIX, self.prefix)
        self.assertEqual(self.INITIAL_NAME, self.name)
        self.assertEqual(self.INITIAL_DESCRIPTION, self.description)

    def test_change_listener_changed(self):
        CHANGED_NAME = "changed name"
        self.product.name = CHANGED_NAME
        self.product.update()
        self.assertEqual('changed', self.listener.action)
        self.assertIsInstance(self.listener.resource, Product)
        self.assertEqual(CHANGED_NAME, self.name)
        self.assertEqual({"name": self.INITIAL_NAME}, self.listener.old_values)

    def test_change_listener_deleted(self):
        self.product.delete()
        self.assertEqual('deleted', self.listener.action)
        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)