Olemis Lang avatar Olemis Lang committed 8702d4b

BH Multiproduct #115 : First draft implementation of product configuration ... (errors=27)

Ran 27 tests in 0.409s

Comments (0)

Files changed (2)

t115/t115_r1423431_product_envs_testing.diff

 # HG changeset patch
-# Parent c18792daf68245c5ca135a90b06ddf12457adb15
+# Parent 405ee3c3f238f6db6214078215265c406e6cffcb
 BH Theme #115 : Test cases for product environments
 
-diff -r c18792daf682 bloodhound_multiproduct/tests/env.py
+diff -r 405ee3c3f238 bloodhound_multiproduct/tests/config.py
 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
-+++ b/bloodhound_multiproduct/tests/env.py	Thu Dec 20 15:51:43 2012 -0500
-@@ -0,0 +1,111 @@
++++ b/bloodhound_multiproduct/tests/config.py	Sun Jan 06 19:49:32 2013 -0500
+@@ -0,0 +1,88 @@
 +
 +#  Licensed to the Apache Software Foundation (ASF) under one
 +#  or more contributor license agreements.  See the NOTICE file
 +#  specific language governing permissions and limitations
 +#  under the License.
 +
-+"""Tests for multiproduct/model.py"""
++"""Tests for Apache(TM) Bloodhound's product configuration objects"""
++
++from ConfigParser import ConfigParser
++from StringIO import StringIO
++import unittest
++
++from trac.config import Option, _to_utf8
++from trac.tests.config import ConfigurationTestCase
++
++from multiproduct.api import MultiProductSystem
++from multiproduct.config import Configuration
++from multiproduct.model import Product, ProductSetting
++from tests.env import MultiproductTestCase
++
++class ProductConfigTestCase(ConfigurationTestCase, MultiproductTestCase):
++    r"""Test cases for Trac configuration objects rewritten for product 
++    scope.
++    """
++    def setUp(self):
++        r"""Replace Trac environment with product environment
++        """
++        self.env = self._setup_test_env()
++        self._upgrade_mp(self.env)
++        self._setup_test_log(self.env)
++        self._load_product_from_data(self.env, self.default_product)
++        self._orig_registry = Option.registry
++        Option.registry = {}
++
++    def tearDown(self):
++        Option.registry = self._orig_registry
++        self.env = None
++
++    def _read(self, parents=None, product=None):
++        r"""Override superclass method by returning product-aware configuration
++        object retrieving settings from the database. Such objects will replace
++        instances of `trac.config.Configuration` used in inherited test cases.
++        """
++        if product is None:
++            product = self.default_product
++        return Configuration(self.env, product, parents)
++
++    def _write(self, lines, product=None):
++        r"""Override superclass method by writing configuration values
++        to the database rather than ini file in the filesystem.
++        """
++        if product is None:
++            product = self.default_product
++        fp = StringIO(('\n'.join(lines + [''])).encode('utf-8'))
++        parser = ConfigParser()
++        parser.readfp(fp, 'bh-product-test')
++        with self.env.db_transaction as db:
++            # Delete existing setting for target product , if any
++            for setting in ProductSetting.select(self.env, db, 
++                    {'product' : product}):
++                setting.delete()
++            # Insert new options
++            for section in parser.sections():
++                option_key = dict(section=_to_utf8(section), product=product)
++                for option, value in parser.items(section):
++                    option_key.update(dict(option=_to_utf8(option)))
++                    setting = ProductSetting(self.env, option_key)
++                    setting.value = _to_utf8(value)
++                    setting.insert()
++
++def suite():
++    return unittest.makeSuite(ProductConfigTestCase,'test')
++
++if __name__ == '__main__':
++    unittest.main(defaultTest='suite')
++
+diff -r 405ee3c3f238 bloodhound_multiproduct/tests/env.py
+--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
++++ b/bloodhound_multiproduct/tests/env.py	Sun Jan 06 19:49:32 2013 -0500
+@@ -0,0 +1,139 @@
++
++#  Licensed to the Apache Software Foundation (ASF) under one
++#  or more contributor license agreements.  See the NOTICE file
++#  distributed with this work for additional information
++#  regarding copyright ownership.  The ASF licenses this file
++#  to you under the Apache License, Version 2.0 (the
++#  "License"); you may not use this file except in compliance
++#  with the License.  You may obtain a copy of the License at
++#
++#   http://www.apache.org/licenses/LICENSE-2.0
++#
++#  Unless required by applicable law or agreed to in writing,
++#  software distributed under the License is distributed on an
++#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
++#  KIND, either express or implied.  See the License for the
++#  specific language governing permissions and limitations
++#  under the License.
++
++"""Tests for Apache(TM) Bloodhound's product environments"""
 +
 +import os.path
++import shutil
 +import tempfile
 +import unittest
 +
++from trac.test import EnvironmentStub
 +from trac.tests.env import EnvironmentTestCase
 +
 +from multiproduct.api import MultiProductSystem
 +from multiproduct.env import ProductEnvironment
 +from multiproduct.model import Product
 +
-+class ProductEnvTestCase(EnvironmentTestCase):
-+    r"""Test cases for Trac environments rewritten for product environments
++# FIXME: Subclass TestCase explictly ?
++class MultiproductTestCase(unittest.TestCase):
++    r"""Mixin providing access to multi-product testing extensions.
++
++    This class serves to the purpose of upgrading existing Trac test cases
++    with multi-product super-powers while still providing the foundations
++    to create product-specific subclasses.
 +    """
 +
-+    # Test setup
-+
-+    default_product = 'tp1'
-+
-+    def setUp(self):
-+        r"""Replace Trac environment with product environment
-+        """
-+        EnvironmentTestCase.setUp(self)
-+        self.global_env = self.env
-+        self._setup_test_log()
-+        self._upgrade_mp()
-+        self._load_product_from_data(self.default_product)
-+        try:
-+            self.env = ProductEnvironment(self.global_env, self.default_product)
-+        except :
-+            # All tests should fail if anything goes wrong
-+            self.global_env.log.exception('Error creating product environment')
-+            self.env = None
-+
-+    def tearDown(self):
-+        # Discard product environment
-+        self.env = self.global_env
-+
-+        EnvironmentTestCase.tearDown(self)
-+
 +    # Product data
 +
++    default_product = 'tp1'
 +    MAX_TEST_PRODUCT = 3
 +
 +    PRODUCT_DATA = dict(
 +            for i in xrange(1, MAX_TEST_PRODUCT)
 +        )
 +
-+    # Non-public methods
++    # Test setup
 +
-+    def _setup_test_log(self):
++    def _setup_test_env(self, create_folder=True, path=None):
++        r"""Prepare a new test environment . 
++        
++        Optionally set its path to a meaningful location (temp folder
++        if `path` is `None`).
++        """
++        self.env = env = EnvironmentStub(enable=['trac.*', 'multiproduct.*'])
++        if create_folder:
++            if path is None:
++                env.path = tempfile.mkdtemp('bh-product-tempenv')
++            else:
++                env.path = path
++        return env
++
++    def _setup_test_log(self, env):
 +        r"""Ensure test product with prefix is loaded
 +        """
 +        logdir = tempfile.gettempdir()
 +        logpath = os.path.join(logdir, 'trac-testing.log')
-+        config = self.global_env.config
++        config = env.config
 +        config.set('logging', 'log_file', logpath)
 +        config.set('logging', 'log_type', 'file')
 +        config.set('logging', 'log_level', 'DEBUG')
 +        config.save()
-+        self.global_env.setup_log()
-+        self.global_env.log.info('%s test case: %s %s',
++        env.setup_log()
++        env.log.info('%s test case: %s %s',
 +                '-' * 10, self.id(), '-' * 10)
 +
-+    def _load_product_from_data(self, prefix):
++    def _load_product_from_data(self, env, prefix):
 +        r"""Ensure test product with prefix is loaded
 +        """
 +        # TODO: Use fixtures implemented in #314
 +        product_data = self.PRODUCT_DATA[prefix]
-+        product = Product(self.global_env)
++        product = Product(env)
 +        product._data.update(product_data)
 +        product.insert()
 +
-+    def _upgrade_mp(self):
++    def _upgrade_mp(self, env):
 +        r"""Apply multi product upgrades
 +        """
-+        self.mpsystem = MultiProductSystem(self.global_env)
++        self.mpsystem = MultiProductSystem(env)
 +        try:
-+            self.mpsystem.upgrade_environment(self.global_env.db_transaction)
++            self.mpsystem.upgrade_environment(env.db_transaction)
 +        except OperationalError:
 +            # table remains but database version is deleted
 +            pass
-+        
++
++class ProductEnvTestCase(EnvironmentTestCase, MultiproductTestCase):
++    r"""Test cases for Trac environments rewritten for product environments
++    """
++
++    # Test setup
++
++    def setUp(self):
++        r"""Replace Trac environment with product environment
++        """
++        EnvironmentTestCase.setUp(self)
++        try:
++            self.global_env = self.env
++            self._setup_test_log(self.global_env)
++            self._upgrade_mp(self.global_env)
++            self._load_product_from_data(self.global_env, self.default_product)
++            try:
++                self.env = ProductEnvironment(self.global_env, self.default_product)
++            except :
++                # All tests should fail if anything goes wrong
++                self.global_env.log.exception('Error creating product environment')
++                self.env = None
++        except:
++            shutil.rmtree(self.env.path)
++            raise
++
++    def tearDown(self):
++        # Discard product environment
++        self.env = self.global_env
++
++        EnvironmentTestCase.tearDown(self)
++
 +def suite():
 +    return unittest.makeSuite(ProductEnvTestCase,'test')
 +

t115/t115_r1427886_product_config.diff

 # HG changeset patch
-# Parent 91a78951c15156dd7ed8b348bd9431b23f850c6c
+# Parent 56bf5ca5136d057fa2bd9600aab7aabc0ae76927
 BH Multiproduct #115 : Product-specific settings
 
-diff -r 91a78951c151 bloodhound_multiproduct/multiproduct/api.py
---- a/bloodhound_multiproduct/multiproduct/api.py	Thu Jan 03 01:16:05 2013 -0500
-+++ b/bloodhound_multiproduct/multiproduct/api.py	Thu Jan 03 02:38:12 2013 -0500
+diff -r 56bf5ca5136d bloodhound_dashboard/bhdashboard/model.py
+--- a/bloodhound_dashboard/bhdashboard/model.py	Sun Jan 06 12:53:47 2013 -0500
++++ b/bloodhound_dashboard/bhdashboard/model.py	Sun Jan 06 19:57:29 2013 -0500
+@@ -117,8 +117,9 @@
+                 self._update_from_row(row)
+                 break
+             else:
+-                raise ResourceNotFound('No %(object_name)s with %(where)s' %
+-                                sdata)
++                raise ResourceNotFound(
++                        ('No %(object_name)s with %(where)s' % sdata) 
++                                % tuple(values))
+     
+     def delete(self):
+         """Deletes the matching record from the database"""
+@@ -207,7 +208,7 @@
+             TicketSystem(self._env).reset_ticket_fields()
+     
+     @classmethod
+-    def select(cls, env, db=None, where=None):
++    def select(cls, env, db=None, where=None, limit=None):
+         """Query the database to get a set of records back"""
+         rows = []
+         fields = cls._meta['key_fields']+cls._meta['non_key_fields']
+@@ -218,7 +219,11 @@
+         wherestr, values = dict_to_kv_str(where)
+         if wherestr:
+             wherestr = ' WHERE ' + wherestr
+-        for row in env.db_query(sql + wherestr, values):
++        if limit is not None:
++            limitstr = ' LIMIT ' + str(int(limit))
++        else:
++            limitstr = ''
++        for row in env.db_query(sql + wherestr + limitstr, values):
+             # we won't know which class we need until called
+             model = cls.__new__(cls)
+             data = dict([(fields[i], row[i]) for i in range(len(fields))])
+diff -r 56bf5ca5136d bloodhound_multiproduct/multiproduct/api.py
+--- a/bloodhound_multiproduct/multiproduct/api.py	Sun Jan 06 12:53:47 2013 -0500
++++ b/bloodhound_multiproduct/multiproduct/api.py	Sun Jan 06 19:57:29 2013 -0500
 @@ -31,9 +31,9 @@
  from trac.util.translation import _, N_
  from trac.web.chrome import ITemplateProvider
      del modelcls
      
      def get_version(self):
-diff -r 91a78951c151 bloodhound_multiproduct/multiproduct/config.py
+diff -r 56bf5ca5136d bloodhound_multiproduct/multiproduct/config.py
 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
-+++ b/bloodhound_multiproduct/multiproduct/config.py	Thu Jan 03 02:38:12 2013 -0500
-@@ -0,0 +1,191 @@
++++ b/bloodhound_multiproduct/multiproduct/config.py	Sun Jan 06 19:57:29 2013 -0500
+@@ -0,0 +1,277 @@
 +
 +#  Licensed to the Apache Software Foundation (ASF) under one
 +#  or more contributor license agreements.  See the NOTICE file
 +
 +"""Configuration objects for Bloodhound product environments"""
 +
++__all__ = 'Configuration', 'Section'
++
 +import os.path
 +
-+from trac.config import ConfigurationError
++from trac.config import Configuration, ConfigurationError, Option, Section, \
++        _to_utf8, _use_default
++from trac.util.text import to_unicode
 +
 +from multiproduct.model import ProductSetting
 +
-+__metaclass__ = type
-+
-+class DefaultProductConfiguration:
++class Configuration(Configuration):
 +    """Product-aware settings repository equivalent to instances of
 +    `trac.config.Configuration` (and thus `ConfigParser` from the
 +    Python Standard Library) but retrieving configuration values 
 +        `trac.env.Environment` and product prefix.
 +
 +        Optionally it is possible to inherit settings from parent
-+        configuration objects. Environment's configuration will 
++        Configuration objects. Environment's configuration will 
 +        always be added to parents list.
 +        """
 +        self.env = env
 +        self.parents = (parents or []) + [self.env.config]
 +        self.product = product
-+
-+    def __contains__(self, name):
-+        """Return whether the configuration contains a section of the given
-+        name.
-+        """
-+        raise NotImplementedError
++        self._sections = {}
 +
 +    def __getitem__(self, name):
-+        """Return the configuration section with the specified name."""
-+        raise NotImplementedError
-+
-+    def get(self, section, key, default=''):
-+        """Return the value of the specified option.
-+        
-+        Valid default input is a string. Returns a string.
++        """Return the configuration section with the specified name.
 +        """
-+        raise NotImplementedError
-+
-+    def getbool(self, section, key, default=''):
-+        """Return the specified option as boolean value.
-+        
-+        If the value of the option is one of "yes", "true", "enabled", "on",
-+        or "1", this method wll return `True`, otherwise `False`.
-+        
-+        Valid default input is a string or a bool. Returns a bool.
-+        
-+        (since Trac 0.9.3, "enabled" added in 0.11)
-+        """
-+        raise NotImplementedError
-+
-+    def getint(self, section, key, default=''):
-+        """Return the value of the specified option as integer.
-+        
-+        If the specified option can not be converted to an integer, a
-+        `ConfigurationError` exception is raised.
-+        
-+        Valid default input is a string or an int. Returns an int.
-+        
-+        (since Trac 0.10)
-+        """
-+        raise NotImplementedError
-+
-+    def getfloat(self, section, key, default=''):
-+        """Return the value of the specified option as float.
-+        
-+        If the specified option can not be converted to a float, a
-+        `ConfigurationError` exception is raised.
-+        
-+        Valid default input is a string, float or int. Returns a float.
-+        
-+        (since Trac 0.12)
-+        """
-+        raise NotImplementedError
-+
-+    def getlist(self, section, key, default='', sep=',', keep_empty=False):
-+        """Return a list of values that have been specified as a single
-+        comma-separated option.
-+        
-+        A different separator can be specified using the `sep` parameter. If
-+        the `keep_empty` parameter is set to `True`, empty elements are
-+        included in the list.
-+        
-+        Valid default input is a string or a list. Returns a string.
-+        
-+        (since Trac 0.10)
-+        """
-+        raise NotImplementedError
-+
-+    def getpath(self, section, key, default=''):
-+        """Return a configuration value as an absolute path.
-+        
-+        Relative paths are resolved relative to the location of this
-+        configuration file.
-+        
-+        Valid default input is a string. Returns a normalized path.
-+
-+        (enabled since Trac 0.11.5)
-+        """
-+        raise NotImplementedError
-+
-+    def set(self, section, key, value):
-+        """Change a configuration value.
-+        
-+        These changes are not persistent unless saved with `save()`.
-+        """
-+        raise NotImplementedError
-+
-+    def defaults(self, compmgr=None):
-+        """Returns a dictionary of the default configuration values
-+        (''since 0.10'').
-+        
-+        If `compmgr` is specified, return only options declared in components
-+        that are enabled in the given `ComponentManager`.
-+        """
-+        raise NotImplementedError
-+
-+    def options(self, section, compmgr=None):
-+        """Return a list of `(name, value)` tuples for every option in the
-+        specified section.
-+        
-+        This includes options that have default values that haven't been
-+        overridden. If `compmgr` is specified, only return default option
-+        values for components that are enabled in the given `ComponentManager`.
-+        """
-+        raise NotImplementedError
-+
-+    def remove(self, section, key):
-+        """Remove the specified option."""
-+        raise NotImplementedError
++        if name not in self._sections:
++            self._section[name] = Section(self, name)
++        return self._sections[name]
 +
 +    def sections(self, compmgr=None, defaults=True):
 +        """Return a list of section names.
-+        
++
 +        If `compmgr` is specified, only the section names corresponding to
 +        options declared in components that are enabled in the given
 +        `ComponentManager` are returned.
 +        """
-+        raise NotImplementedError
++        sections = set(to_unicode(s) \
++                for s in ProductSetting.get_sections(self.env, self.product))
++        for parent in self.parents:
++            sections.update(parent.sections(compmgr, defaults=False))
++        if defaults:
++            sections.update(self.defaults(compmgr))
++        return sorted(sections)
 +
 +    def has_option(self, section, option, defaults=True):
 +        """Returns True if option exists in section in either the project
 +        trac.ini or one of the parents, or is available through the Option
 +        registry.
-+        
++
 +        (since Trac 0.11)
 +        """
-+        raise NotImplementedError
++        if ProductSetting.exists(self.env, self.product, section, option):
++            return True
++        for parent in self.parents:
++            if parent.has_option(section, option, defaults=False):
++                return True
++        return defaults and (section, option) in Option.registry
 +
 +    def save(self):
-+        """Write the configuration options to the primary file."""
-+        raise NotImplementedError
++        """Nothing to do.
++
++        Notice: Opposite to Trac's Configuration objects Bloodhound's
++        product configuration objects commit changes to the database 
++        immediately. Thus there's no much to do in this method.
++        """
 +
 +    def parse_if_needed(self, force=False):
-+        raise NotImplementedError
++        """Just invalidate options cache.
++
++        Notice: Opposite to Trac's Configuration objects Bloodhound's
++        product configuration objects commit changes to the database 
++        immediately. Thus there's no much to do in this method.
++        """
++        for section in self.sections():
++            self[section]._cache.clear()
 +
 +    def touch(self):
-+        raise NotImplementedError
++        pass
 +
 +    def set_defaults(self, compmgr=None):
 +        """Retrieve all default values and store them explicitly in the
 +        configuration, so that they can be saved to file.
-+        
++
 +        Values already set in the configuration are not overridden.
 +        """
-+        raise NotImplementedError
++        for section, default_options in self.defaults(compmgr).items():
++            for name, value in default_options.items():
++                if not ProductSetting.exists(self.env, self.product,
++                        _to_utf8(section), _to_utf8(name)):
++                    if any(parent[section].contains(name, defaults=False)
++                           for parent in self.parents):
++                        value = None
++                    self.set(section, name, value)
 +
-diff -r 91a78951c151 bloodhound_multiproduct/multiproduct/model.py
---- a/bloodhound_multiproduct/multiproduct/model.py	Thu Jan 03 01:16:05 2013 -0500
-+++ b/bloodhound_multiproduct/multiproduct/model.py	Thu Jan 03 02:38:12 2013 -0500
-@@ -27,6 +27,10 @@
++class Section(Section):
++    """Proxy for a specific configuration section.
++
++    Objects of this class should not be instantiated directly.
++    """
++    __slots__ = ['config', 'name', 'overridden', '_cache']
++
++    def __init__(self, config, name):
++        self.config = config
++        self.name = name
++        self.overridden = {}
++        self._cache = {}
++
++    @property
++    def env(self):
++        return self.config.env
++
++    @property
++    def product(self):
++        return self.config.product
++
++    def contains(self, key, defaults=True):
++        if ProductSetting.exists(self.env, self.product, 
++                _to_utf8(self.name), _to_utf8(key)):
++            return True
++        for parent in self.config.parents:
++            if parent[self.name].contains(key, defaults=False):
++                return True
++        return defaults and Option.registry.has_key((self.name, key))
++
++    __contains__ = contains
++
++    def iterate(self, compmgr=None, defaults=True):
++        """Iterate over the options in this section.
++
++        If `compmgr` is specified, only return default option values for
++        components that are enabled in the given `ComponentManager`.
++        """
++        options = set()
++        name_str = _to_utf8(self.name)
++        for setting in ProductSetting.select(self.env,
++                where={'product':self.product, 'section':name_str}):
++            option = to_unicode(setting.option)
++            options.add(option.lower())
++            yield option
++        for parent in self.config.parents:
++            for option in parent[self.name].iterate(defaults=False):
++                loption = option.lower()
++                if loption not in options:
++                    options.add(loption)
++                    yield option
++        if defaults:
++            for section, option in Option.get_registry(compmgr).keys():
++                if section == self.name and option.lower() not in options:
++                    yield option
++
++    __iter__ = iterate
++
++    def __repr__(self):
++        return '<%s [%s , %s]>' % (self.__class__.__name__, \
++                self.product, self.name)
++
++    def get(self, key, default=''):
++        """Return the value of the specified option.
++
++        Valid default input is a string. Returns a string.
++        """
++        cached = self._cache.get(key, _use_default)
++        if cached is not _use_default:
++            return cached
++        name_str = _to_utf8(self.name)
++        key_str = _to_utf8(key)
++        settings = ProductSetting.select(self.env, 
++                where={'product':self.product, 'section':name_str,
++                        'option':key_str})
++        if len(settings) > 0:
++            value = settings[0].value
++        else:
++            for parent in self.config.parents:
++                value = parent[self.name].get(key, _use_default)
++                if value is not _use_default:
++                    break
++            else:
++                if default is not _use_default:
++                    option = Option.registry.get((self.name, key))
++                    value = option.default if option else _use_default
++                else:
++                    value = _use_default
++        if value is _use_default:
++            return default
++        if not value:
++            value = u''
++        elif isinstance(value, basestring):
++            value = to_unicode(value)
++        self._cache[key] = value
++        return value
++
++    def getpath(self, key, default=''):
++        """Return a configuration value as an absolute path.
++
++        Relative paths are resolved relative to the location of the
++        target global environment.
++
++        Valid default input is a string. Returns a normalized path.
++
++        (enabled since Trac 0.11.5)
++        """
++        path = self.get(key, default)
++        if not path:
++            return default
++        if not os.path.isabs(path):
++            path = os.path.join(self.env.path, path)
++        return os.path.normcase(os.path.realpath(path))
++
++    def remove(self, key):
++        """Delete a key from this section.
++
++        Like for `set()`, the changes won't persist until `save()` gets called.
++        """
++        option_key = {
++                'product' : self.product, 
++                'section' : _to_utf8(self.name),
++                'option' : _to_utf8(key),
++            }
++        setting = ProductSetting(self.env, keys=option_key)
++        if settings._exists:
++            self._cache.pop(key, None)
++            settings.delete()
++
++    def set(self, key, value):
++        """Change a configuration value.
++
++        These changes will be persistent right away.
++        """
++        self._cache.pop(key, None)
++        option_key = {
++                'product' : self.product, 
++                'section' : _to_utf8(self.name),
++                'option' : _to_utf8(key),
++            }
++        setting = ProductSetting(self.env, keys=option_key)
++
++        if setting._exists :
++            if value is None:
++                # Delete existing record from the database
++                self.overridden[key] = True
++                setting.delete()
++            else:
++                # Update existing record
++                setting.value = _to_utf8(value)
++                setting.update()
++        elif value is not None:
++            # Insert new record in the database
++            setting.value = _to_utf8(value)
++            setting.insert()
++
+diff -r 56bf5ca5136d bloodhound_multiproduct/multiproduct/model.py
+--- a/bloodhound_multiproduct/multiproduct/model.py	Sun Jan 06 12:53:47 2013 -0500
++++ b/bloodhound_multiproduct/multiproduct/model.py	Sun Jan 06 19:57:29 2013 -0500
+@@ -18,6 +18,7 @@
+ 
+ """Models to support multi-product"""
+ from datetime import datetime
++from itertools import izip
+ 
+ from trac.core import TracError
+ from trac.resource import Resource
+@@ -27,6 +28,10 @@
  
  from bhdashboard.model import ModelBase
  
  
  class Product(ModelBase):
      """The Product table"""
-@@ -37,12 +41,12 @@
+@@ -37,12 +42,12 @@
              'no_change_fields':['prefix',],
              'unique_fields':['name'],
              }
      def delete(self, resources_to=None):
          """ override the delete method so that we can move references to this
          object to a new product """
-@@ -59,7 +63,7 @@
+@@ -59,7 +64,7 @@
          for prm in ProductResourceMap.select(self._env, where=where):
              prm._data['product_id'] = resources_to
              prm.update()
      def _update_relations(self, db=None, author=None):
          """Extra actions due to update"""
          # tickets need to be updated
-@@ -71,7 +75,7 @@
+@@ -71,7 +76,7 @@
              for t in Product.get_tickets(self._env, self._data['prefix']):
                  ticket = Ticket(self._env, t['id'], db)
                  ticket.save_changes(author, comment, now)
      @classmethod
      def get_tickets(cls, env, product=''):
          """Retrieve all tickets associated with the product."""
-@@ -88,7 +92,7 @@
+@@ -88,7 +93,7 @@
              'unique_fields':[],
              'auto_inc':['id'],
              }
      def reparent_resource(self, product=None):
          """a specific function to update a record when it is to move product"""
          if product is not None:
-@@ -101,3 +105,17 @@
+@@ -101,3 +106,39 @@
          self._data['product_id'] = product
          self.update()
  
 +# -------------------------------------------
 +
 +class ProductSetting(ModelBase):
-+    """The Product configuration table"""
++    """The Product configuration table
++    """
 +    _meta = {'table_name':'bloodhound_productconfig',
 +            'object_name':'ProductSetting',
 +            'key_fields':['product', 'section', 'option'],
 +            'unique_fields':[],
 +            }
 +
++    @classmethod
++    def exists(cls, env, product, section=None, option=None, db=None):
++        """Determine whether there are configuration values for
++        product, section, option .
++        """
++        if product is None:
++            raise ValueError("Product prefix required")
++        l = locals()
++        option_subkey = ([c, l[c]] for c in ('product', 'section', 'option'))
++        option_subkey = dict(c for c in option_subkey if c[1] is not None)
++        return len(cls.select(env, db, where=option_subkey, limit=1)) > 0
++
++    @classmethod
++    def get_sections(cls, env, product):
++        """Retrieve configuration sections defined for a product
++        """
++        # FIXME: Maybe something more ORM-ish should be added in ModelBase
++        return [row[0] for row in env.db_query("""SELECT DISTINCT section 
++                FROM bloodhound_productconfig WHERE product = %s""", 
++                (product,)) ]
++
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.