incubator-bloodhound-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From j...@apache.org
Subject svn commit: r1430768 - in /incubator/bloodhound/branches/bep_0003_multiproduct: bloodhound_dashboard/bhdashboard/ bloodhound_multiproduct/multiproduct/ bloodhound_multiproduct/tests/
Date Wed, 09 Jan 2013 10:24:23 GMT
Author: jure
Date: Wed Jan  9 10:24:22 2013
New Revision: 1430768

URL: http://svn.apache.org/viewvc?rev=1430768&view=rev
Log:
Product specific environment & configuration, ticket #115 (from Olemis)


Added:
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/config.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/config.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/env.py
Modified:
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/model.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/api.py
    incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/model.py

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/model.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/model.py?rev=1430768&r1=1430767&r2=1430768&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/model.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_dashboard/bhdashboard/model.py Wed Jan  9 10:24:22 2013
@@ -118,8 +118,9 @@ class ModelBase(object):
                 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"""
@@ -145,14 +146,15 @@ class ModelBase(object):
                                       for k in self._meta['key_fields']]))):
             sdata = {'keys':','.join(["%s='%s'" % (k, self._data[k])
                                      for k in self._meta['key_fields']])}
-        elif len(self.select(self._env, where =
+        elif self._meta['unique_fields'] and len(self.select(self._env, where =
                                 dict([(k,self._data[k])
                                       for k in self._meta['unique_fields']]))):
             sdata = {'keys':','.join(["%s='%s'" % (k, self._data[k])
                                      for k in self._meta['unique_fields']])}
         if sdata:
             sdata.update(self._meta)
-            raise TracError('%(object_name)s %(keys)s already exists' %
+            sdata['values'] = self._data
+            raise TracError('%(object_name)s %(keys)s already exists %(values)s' %
                             sdata)
             
         for key in self._meta['key_fields']:
@@ -208,7 +210,7 @@ class ModelBase(object):
             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']
@@ -219,7 +221,11 @@ class ModelBase(object):
         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))])

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/api.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/api.py?rev=1430768&r1=1430767&r2=1430768&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/api.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/api.py Wed Jan  9 10:24:22 2013
@@ -31,7 +31,7 @@ from trac.ticket.api import ITicketField
 from trac.util.translation import _, N_
 from trac.web.chrome import ITemplateProvider
 
-from multiproduct.model import Product, ProductResourceMap
+from multiproduct.model import Product, ProductResourceMap, ProductSetting
 
 DB_VERSION = 3
 DB_SYSTEM_KEY = 'bloodhound_multi_product_version'
@@ -43,9 +43,9 @@ class MultiProductSystem(Component):
     implements(IEnvironmentSetupParticipant, ITemplateProvider,
             IPermissionRequestor, ITicketFieldProvider, IResourceManager)
     
-    SCHEMA = [mcls._get_schema() for mcls in (Product, ProductResourceMap)]
-    del mcls
-    
+    SCHEMA = [mcls._get_schema() \
+              for mcls in (Product, ProductResourceMap, ProductSetting)]
+
     def get_version(self):
         """Finds the current version of the bloodhound database schema"""
         rows = self.env.db_query("""

Added: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/config.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/config.py?rev=1430768&view=auto
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/config.py (added)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/config.py Wed Jan  9 10:24:22 2013
@@ -0,0 +1,311 @@
+
+#  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.
+
+"""Configuration objects for Bloodhound product environments"""
+
+__all__ = 'Configuration', 'Section'
+
+import os.path
+
+from trac.config import Configuration, ConfigurationError, Option, Section, \
+        _use_default
+from trac.resource import ResourceNotFound
+from trac.util.text import to_unicode
+
+from multiproduct.model import ProductSetting
+
+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 
+    from the database.
+    """
+    def __init__(self, env, product, parents=None):
+        """Initialize configuration object with an instance of 
+        `trac.env.Environment` and product prefix.
+
+        Optionally it is possible to inherit settings from parent
+        Configuration objects. Environment's configuration will not
+        be added to parents list.
+        """
+        self.env = env
+        self.product = to_unicode(product)
+        self._sections = {}
+        self._setup_parents(parents)
+
+    def __getitem__(self, name):
+        """Return the configuration section with the specified name.
+        """
+        if name not in self._sections:
+            self._sections[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.
+        """
+        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)
+        """
+        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):
+        """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):
+        """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):
+        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.
+        """
+        for section, default_options in self.defaults(compmgr).items():
+            for name, value in default_options.items():
+                if not ProductSetting.exists(self.env, self.product,
+                        section, name):
+                    if any(parent[section].contains(name, defaults=False)
+                           for parent in self.parents):
+                        value = None
+                    self.set(section, name, value)
+
+    # Helper methods
+
+    def _setup_parents(self, parents=None):
+        """Inherit configuration from parent `Configuration` instances.
+        If there's a value set to 'file' option in 'inherit' section then
+        it will be considered as a list of paths to .ini files
+        that will be added to parents list as well.
+        """
+        from trac import config
+        self.parents = (parents or [])
+        for filename in self.get('inherit', 'file').split(','):
+            filename = Section._normalize_path(filename.strip(), self.env)
+            self.parents.append(config.Configuration(filename))
+
+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 = to_unicode(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):
+        key = to_unicode(key)
+        if ProductSetting.exists(self.env, self.product, self.name, 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 = 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 = self.name
+        key_str = to_unicode(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 `conf` subfolder 
+        of the target global environment. This approach is consistent
+        with TracIni path resolution.
+
+        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
+        return self._normalize_path(path, self.env)
+
+    def remove(self, key):
+        """Delete a key from this section.
+
+        Like for `set()`, the changes won't persist until `save()` gets called.
+        """
+        key_str = to_unicode(key)
+        option_key = {
+                'product' : self.product, 
+                'section' : self.name,
+                'option' : key_str,
+            }
+        try:
+            setting = ProductSetting(self.env, keys=option_key)
+        except ResourceNotFound:
+            self.env.log.warning("No record for product option %s", option_key)
+        else:
+            self._cache.pop(key, None)
+            setting.delete()
+            self.env.log.info("Removing product option %s", option_key)
+
+    def set(self, key, value):
+        """Change a configuration value.
+
+        These changes will be persistent right away.
+        """
+        key_str = to_unicode(key)
+        value_str = to_unicode(value)
+        self._cache.pop(key_str, None)
+        option_key = {
+                'product' : self.product, 
+                'section' : self.name,
+                'option' : key_str,
+            }
+        try:
+            setting = ProductSetting(self.env, option_key)
+        except ResourceNotFound:
+            if value is not None:
+                # Insert new record in the database
+                setting = ProductSetting(self.env)
+                setting._data.update(option_key)
+                setting._data['value'] = value_str
+                self.env.log.debug('Writing option %s', setting._data)
+                setting.insert()
+        else:
+            if value is None:
+                # Delete existing record from the database
+                # FIXME : Why bother with setting overriden
+                self.overridden[key] = True
+                setting.delete()
+            else:
+                # Update existing record
+                setting.value = value
+                setting.update()
+
+    # Helper methods
+
+    @staticmethod
+    def _normalize_path(path, env):
+        if not os.path.isabs(path):
+            path = os.path.join(env.path, 'conf', path)
+        return os.path.normcase(os.path.realpath(path))
+

Added: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py?rev=1430768&view=auto
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py (added)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/env.py Wed Jan  9 10:24:22 2013
@@ -0,0 +1,476 @@
+
+#  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.
+
+"""Bloodhound product environment and related APIs"""
+
+import os.path
+
+from trac.config import ConfigSection, Option
+from trac.core import Component, ComponentManager, ExtensionPoint, \
+        implements, TracError
+from trac.env import Environment, ISystemInfoProvider
+from trac.util import get_pkginfo, lazy
+from trac.util.compat import sha1
+
+from multiproduct.model import Product
+
+class ProductEnvironment(Component, ComponentManager):
+    """Bloodhound product-aware environment manager.
+
+    Bloodhound encapsulates access to product resources stored inside a
+    Trac environment via product environments. They are compatible lightweight
+    irepresentations of top level environment. 
+
+    Product environments contain among other things:
+
+    * a configuration file, 
+    * product-aware clones of the wiki and ticket attachments files,
+
+    Product environments do not have:
+
+    * product-specific templates and plugins,
+    * a separate database
+    * active participation in database upgrades and other setup tasks
+
+    See https://issues.apache.org/bloodhound/wiki/Proposals/BEP-0003
+    """
+
+    implements(ISystemInfoProvider)
+
+    @property
+    def system_info_providers(self):
+        r"""System info will still be determined by the global environment.
+        """
+        return self.env.system_info_providers
+
+    @property
+    def setup_participants(self):
+        """Setup participants list for product environments will always
+        be empty based on the fact that upgrades will only be handled by
+        the global environment.
+        """
+        return ()
+
+    components_section = ConfigSection('components',
+        """This section is used to enable or disable components
+        provided by plugins, as well as by Trac itself.
+
+        See also: TracIni , TracPlugins
+        """)
+
+    @property
+    def shared_plugins_dir():
+        """Product environments may not add plugins.
+        """
+        return ''
+
+    # TODO: Estimate product base URL considering global base URL, pattern, ...
+    base_url = ''
+
+    # TODO: Estimate product base URL considering global base URL, pattern, ...
+    base_url_for_redirect = ''
+
+    @property
+    def secure_cookies(self):
+        """Restrict cookies to HTTPS connections.
+        """
+        return self.env.secure_cookies
+
+    @property
+    def project_name(self):
+        """Name of the product.
+        """
+        return self.product.name
+
+    @property
+    def project_description(self):
+        """Short description of the product.
+        """
+        return self.product.description
+
+    @property
+    def project_url(self):
+        """URL of the main project web site, usually the website in
+        which the `base_url` resides. This is used in notification
+        e-mails.
+        """
+        return self.env.project_url
+
+    project_admin = Option('project', 'admin', '',
+        """E-Mail address of the product's leader / administrator.""")
+
+    @property
+    def project_admin_trac_url(self):
+        """Base URL of a Trac instance where errors in this Trac
+        should be reported.
+        """
+        return self.env.project_admin_trac_url
+
+    # FIXME: Should products have different values i.e. config option ?
+    @property
+    def project_footer(self):
+        """Page footer text (right-aligned).
+        """
+        return self.env.project_footer
+
+    project_icon = Option('project', 'icon', 'common/trac.ico',
+        """URL of the icon of the product.""")
+
+    log_type = Option('logging', 'log_type', 'inherit',
+        """Logging facility to use.
+
+        Should be one of (`inherit`, `none`, `file`, `stderr`, 
+        `syslog`, `winlog`).""")
+
+    log_file = Option('logging', 'log_file', 'trac.log',
+        """If `log_type` is `file`, this should be a path to the
+        log-file.  Relative paths are resolved relative to the `log`
+        directory of the environment.""")
+
+    log_level = Option('logging', 'log_level', 'DEBUG',
+        """Level of verbosity in log.
+
+        Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""")
+
+    log_format = Option('logging', 'log_format', None,
+        """Custom logging format.
+
+        If nothing is set, the following will be used:
+
+        Trac[$(module)s] $(levelname)s: $(message)s
+
+        In addition to regular key names supported by the Python
+        logger library (see
+        http://docs.python.org/library/logging.html), one could use:
+
+        - $(path)s     the path for the current environment
+        - $(basename)s the last path component of the current environment
+        - $(project)s  the project name
+
+        Note the usage of `$(...)s` instead of `%(...)s` as the latter form
+        would be interpreted by the ConfigParser itself.
+
+        Example:
+        `($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s`
+
+        ''(since 0.10.5)''""")
+
+    def __init__(self, env, product):
+        """Initialize the product environment.
+
+        :param env:     the global Trac environment
+        :param product: product prefix or an instance of
+                        multiproduct.model.Product
+        """
+        ComponentManager.__init__(self)
+
+        if isinstance(product, Product):
+            if product._env is not env:
+                raise ValueError("Product's environment mismatch")
+        elif isinstance(product, basestring):
+            products = Product.select(env, where={'prefix': product})
+            if len(products) == 1 :
+                product = products[0]
+            else:
+                env.log.debug("Products for '%s' : %s",
+                        product, products)
+                raise LookupError("Missing product %s" % (product,))
+
+        self.env = env
+        self.product = product
+        self.systeminfo = []
+        self._href = self._abs_href = None
+
+        self.setup_config()
+
+    # ISystemInfoProvider methods
+
+    def get_system_info(self):
+        return self.env.get_system_info()
+
+    # Same as parent environment's . Avoid duplicated code
+    component_activated = Environment.component_activated.im_func
+    _component_name = Environment._component_name.im_func
+    _component_rules = Environment._component_rules
+    enable_component = Environment.enable_component.im_func
+    get_known_users = Environment.get_known_users.im_func
+    get_systeminfo = Environment.get_system_info.im_func
+    get_repository = Environment.get_repository.im_func
+    is_component_enabled = Environment.is_component_enabled.im_func
+
+    def get_db_cnx(self):
+        """Return a database connection from the connection pool
+
+        :deprecated: Use :meth:`db_transaction` or :meth:`db_query` instead
+
+        `db_transaction` for obtaining the `db` database connection
+        which can be used for performing any query
+        (SELECT/INSERT/UPDATE/DELETE)::
+
+           with env.db_transaction as db:
+               ...
+
+
+        `db_query` for obtaining a `db` database connection which can
+        be used for performing SELECT queries only::
+
+           with env.db_query as db:
+               ...
+        """
+        # TODO: Install database schema proxy with limited scope (see #288)
+        #return DatabaseManager(self).get_connection()
+        raise NotImplementedError
+
+    @lazy
+    def db_exc(self):
+        """Return an object (typically a module) containing all the
+        backend-specific exception types as attributes, named
+        according to the Python Database API
+        (http://www.python.org/dev/peps/pep-0249/).
+
+        To catch a database exception, use the following pattern::
+
+            try:
+                with env.db_transaction as db:
+                    ...
+            except env.db_exc.IntegrityError, e:
+                ...
+        """
+        return DatabaseManager(self).get_exceptions()
+
+    def with_transaction(self, db=None):
+        """Decorator for transaction functions :deprecated:"""
+        # TODO: What shall we do ?
+        #return with_transaction(self, db)
+        raise NotImplementedError
+
+    def get_read_db(self):
+        """Return a database connection for read purposes :deprecated:
+
+        See `trac.db.api.get_read_db` for detailed documentation."""
+        # TODO: Install database schema proxy with limited scope (see #288)
+        #return DatabaseManager(self).get_connection(readonly=True)
+        raise NotImplementedError
+
+    @property
+    def db_query(self):
+        """Return a context manager which can be used to obtain a
+        read-only database connection.
+
+        Example::
+
+            with env.db_query as db:
+                cursor = db.cursor()
+                cursor.execute("SELECT ...")
+                for row in cursor.fetchall():
+                    ...
+
+        Note that a connection retrieved this way can be "called"
+        directly in order to execute a query::
+
+            with env.db_query as db:
+                for row in db("SELECT ..."):
+                    ...
+
+        If you don't need to manipulate the connection itself, this
+        can even be simplified to::
+
+            for row in env.db_query("SELECT ..."):
+                ...
+
+        :warning: after a `with env.db_query as db` block, though the
+          `db` variable is still available, you shouldn't use it as it
+          might have been closed when exiting the context, if this
+          context was the outermost context (`db_query` or
+          `db_transaction`).
+        """
+        # TODO: Install database schema proxy with limited scope (see #288)
+        #return QueryContextManager(self)
+        raise NotImplementedError
+
+    @property
+    def db_transaction(self):
+        """Return a context manager which can be used to obtain a
+        writable database connection.
+
+        Example::
+
+            with env.db_transaction as db:
+                cursor = db.cursor()
+                cursor.execute("UPDATE ...")
+
+        Upon successful exit of the context, the context manager will
+        commit the transaction. In case of nested contexts, only the
+        outermost context performs a commit. However, should an
+        exception happen, any context manager will perform a rollback.
+
+        Like for its read-only counterpart, you can directly execute a
+        DML query on the `db`::
+
+            with env.db_transaction as db:
+                db("UPDATE ...")
+
+        If you don't need to manipulate the connection itself, this
+        can also be simplified to::
+
+            env.db_transaction("UPDATE ...")
+
+        :warning: after a `with env.db_transaction` as db` block,
+          though the `db` variable is still available, you shouldn't
+          use it as it might have been closed when exiting the
+          context, if this context was the outermost context
+          (`db_query` or `db_transaction`).
+        """
+        # TODO: Install database schema proxy with limited scope (see #288)
+        #return TransactionContextManager(self)
+        raise NotImplementedError
+
+    def shutdown(self, tid=None):
+        """Close the environment."""
+        RepositoryManager(self).shutdown(tid)
+        # FIXME: Shared DB so IMO this should not happen ... at least not here
+        #DatabaseManager(self).shutdown(tid)
+        if tid is None:
+            self.log.removeHandler(self._log_handler)
+            self._log_handler.flush()
+            self._log_handler.close()
+            del self._log_handler
+
+    def create(self, options=[]):
+        """Placeholder for compatibility when trying to create the basic 
+        directory structure of the environment, etc ...
+
+        This method does nothing at all.
+        """
+        # TODO: Handle options args
+
+    def get_version(self, db=None, initial=False):
+        """Return the current version of the database.  If the
+        optional argument `initial` is set to `True`, the version of
+        the database used at the time of creation will be returned.
+
+        In practice, for database created before 0.11, this will
+        return `False` which is "older" than any db version number.
+
+        :since: 0.11
+
+        :since 1.0: deprecation warning: the `db` parameter is no
+                    longer used and will be removed in version 1.1.1
+        """
+        return self.env.get_version(db, initial)
+
+    def setup_config(self):
+        """Load the configuration object.
+        """
+        # FIXME: Install product-specific configuration object
+        self.config = self.env.config
+        self.setup_log()
+
+    def get_templates_dir(self):
+        """Return absolute path to the templates directory.
+        """
+        return self.env.get_templates_dir()
+
+    def get_htdocs_dir(self):
+        """Return absolute path to the htdocs directory."""
+        return self.env.get_htdocs_dir()
+
+    def get_log_dir(self):
+        """Return absolute path to the log directory."""
+        return self.env.get_log_dir()
+
+    def setup_log(self):
+        """Initialize the logging sub-system."""
+        from trac.log import logger_handler_factory
+        logtype = self.log_type
+        self.env.log.debug("Log type '%s' for product '%s'", 
+                logtype, self.product.prefix)
+        if logtype == 'inherit':
+            logtype = self.env.log_type
+            logfile = self.env.log_file
+            format = self.env.log_format
+        else:
+            logfile = self.log_file
+            format = self.log_format
+        if logtype == 'file' and not os.path.isabs(logfile):
+            logfile = os.path.join(self.get_log_dir(), logfile)
+        logid = 'Trac.%s.%s' % \
+                (sha1(self.env.path).hexdigest(), self.product.prefix)
+        if format:
+            format = format.replace('$(', '%(') \
+                     .replace('%(path)s', self.path) \
+                     .replace('%(basename)s', os.path.basename(self.path)) \
+                     .replace('%(project)s', self.project_name)
+        self.log, self._log_handler = logger_handler_factory(
+            logtype, logfile, self.log_level, logid, format=format)
+
+        from trac import core, __version__ as VERSION
+        self.log.info('-' * 32 + ' environment startup [Trac %s] ' + '-' * 32,
+                      get_pkginfo(core).get('version', VERSION))
+
+    def backup(self, dest=None):
+        """Create a backup of the database.
+
+        :param dest: Destination file; if not specified, the backup is
+                     stored in a file called db_name.trac_version.bak
+        """
+        return self.env.backup(dest)
+
+    def needs_upgrade(self):
+        """Return whether the environment needs to be upgraded."""
+        #for participant in self.setup_participants:
+        #    with self.db_query as db:
+        #        if participant.environment_needs_upgrade(db):
+        #            self.log.warn("Component %s requires environment upgrade",
+        #                          participant)
+        #            return True
+
+        # FIXME: For the time being no need to upgrade the environment
+        # TODO: Determine the role of product environments at upgrade time
+        return False
+
+    def upgrade(self, backup=False, backup_dest=None):
+        """Upgrade database.
+
+        :param backup: whether or not to backup before upgrading
+        :param backup_dest: name of the backup file
+        :return: whether the upgrade was performed
+        """
+        # (Database) upgrades handled by global environment
+        # FIXME: True or False ?
+        return True
+
+    @property
+    def href(self):
+        """The application root path"""
+        if not self._href:
+            self._href = Href(urlsplit(self.abs_href.base)[2])
+        return self._href
+
+    @property
+    def abs_href(self):
+        """The application URL"""
+        if not self._abs_href:
+            if not self.base_url:
+                self.log.warn("base_url option not set in configuration, "
+                              "generated links may be incorrect")
+                self._abs_href = Href('')
+            else:
+                self._abs_href = Href(self.base_url)
+        return self._abs_href
+

Modified: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/model.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/model.py?rev=1430768&r1=1430767&r2=1430768&view=diff
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/model.py (original)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/multiproduct/model.py Wed Jan  9 10:24:22 2013
@@ -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 trac.util.datefmt import utc
 
 from bhdashboard.model import ModelBase
 
+# -------------------------------------------
+# Product API
+# -------------------------------------------
+
 
 class Product(ModelBase):
     """The Product table"""
@@ -37,12 +42,12 @@ class Product(ModelBase):
             'no_change_fields':['prefix',],
             'unique_fields':['name'],
             }
-    
+
     @property
     def resource(self):
         """Allow Product to be treated as a Resource"""
         return Resource('product', self.prefix)
-    
+
     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 +64,7 @@ class Product(ModelBase):
         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 +76,7 @@ class Product(ModelBase):
             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."""
@@ -102,7 +107,7 @@ class ProductResourceMap(ModelBase):
             'unique_fields':[],
             'auto_inc_fields': ['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:
@@ -115,3 +120,39 @@ class ProductResourceMap(ModelBase):
         self._data['product_id'] = product
         self.update()
 
+# -------------------------------------------
+# Configuration
+# -------------------------------------------
+
+class ProductSetting(ModelBase):
+    """The Product configuration table
+    """
+    _meta = {'table_name':'bloodhound_productconfig',
+            'object_name':'ProductSetting',
+            'key_fields':['product', 'section', 'option'],
+            'non_key_fields':['value', ],
+            'no_change_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,)) ]
+

Added: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/config.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/config.py?rev=1430768&view=auto
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/config.py (added)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/config.py Wed Jan  9 10:24:22 2013
@@ -0,0 +1,197 @@
+# -*- coding: utf-8 -*-
+#
+#  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 configuration objects"""
+
+from ConfigParser import ConfigParser
+from itertools import groupby
+import os.path
+import shutil
+from StringIO import StringIO
+import unittest
+
+from trac.config import Option
+from trac.tests.config import ConfigurationTestCase
+from trac.util.text import to_unicode
+
+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()
+
+        # Dummy config file, a sibling of trac.ini
+        self.filename = os.path.join(self.env.path, 'conf', 'product.ini')
+        # Ensure conf sub-folder is created
+        os.mkdir(os.path.dirname(self.filename))
+
+        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
+        shutil.rmtree(self.env.path)
+        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
+        product = to_unicode(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_unicode(section), 
+                        product=to_unicode(product)
+                    )
+                for option, value in parser.items(section):
+                    option_key.update(dict(option=to_unicode(option)))
+                    setting = ProductSetting(self.env)
+                    setting._data.update(option_key)
+                    setting._data['value'] = to_unicode(value)
+                    setting.insert()
+
+    def _test_with_inherit(self, testcb):
+        """Almost exact copy of `trac.tests.config.ConfigurationTestCase`.
+        Differences explained in inline comments.
+        """
+        # Parent configuration file created in environment's conf sub-folder
+        # PS: This modification would not be necessary if the corresponding
+        #     statement in overriden method would be written the same way
+        #     but the fact that both files have the same parent folder
+        #     is not made obvious in there
+        sitename = os.path.join(os.path.dirname(self.filename), 'trac-site.ini')
+
+        try:
+            with open(sitename, 'w') as sitefile:
+                sitefile.write('[a]\noption = x\n')
+
+            self._write(['[inherit]', 'file = trac-site.ini'])
+            testcb()
+        finally:
+            os.remove(sitename)
+
+    def _dump_settings(self, config):
+        product = config.product
+        fields = ('section', 'option', 'value')
+        rows = [tuple(getattr(s, f, None) for f in fields) for s in 
+                ProductSetting.select(config.env, where={'product' : product})]
+
+        dump = []
+        for section, group in groupby(sorted(rows), lambda row: row[0]):
+            dump.append('[%s]\n' % (section,))
+            for row in group:
+                dump.append('%s = %s\n' % (row[1], row[2]))
+        return dump
+
+    # Test cases rewritten to avoid reading config file. 
+    # It does make sense for product config as it's stored in the database
+
+    def test_set_and_save(self):
+        config = self._read()
+        config.set('b', u'öption0', 'y')
+        config.set(u'aä', 'öption0', 'x')
+        config.set('aä', 'option2', "Voilà l'été")  # UTF-8
+        config.set(u'aä', 'option1', u"Voilà l'été") # unicode
+        # Note: the following would depend on the locale.getpreferredencoding()
+        # config.set('a', 'option3', "Voil\xe0 l'\xe9t\xe9") # latin-1
+        self.assertEquals('x', config.get(u'aä', u'öption0'))
+        self.assertEquals(u"Voilà l'été", config.get(u'aä', 'option1'))
+        self.assertEquals(u"Voilà l'été", config.get(u'aä', 'option2'))
+        config.save()
+
+        dump = self._dump_settings(config)
+        self.assertEquals([
+                           u'[aä]\n',
+                           u"option1 = Voilà l'été\n", 
+                           u"option2 = Voilà l'été\n", 
+                           u'öption0 = x\n', 
+                           # u"option3 = Voilà l'été\n", 
+                           u'[b]\n',
+                           u'öption0 = y\n', 
+                           ],
+                          dump)
+        config2 = self._read()
+        self.assertEquals('x', config2.get(u'aä', u'öption0'))
+        self.assertEquals(u"Voilà l'été", config2.get(u'aä', 'option1'))
+        self.assertEquals(u"Voilà l'été", config2.get(u'aä', 'option2'))
+        # self.assertEquals(u"Voilà l'été", config2.get('a', 'option3'))
+
+    def test_set_and_save_inherit(self):
+        def testcb():
+            config = self._read()
+            config.set('a', 'option2', "Voilà l'été")  # UTF-8
+            config.set('a', 'option1', u"Voilà l'été") # unicode
+            self.assertEquals('x', config.get('a', 'option'))
+            self.assertEquals(u"Voilà l'été", config.get('a', 'option1'))
+            self.assertEquals(u"Voilà l'été", config.get('a', 'option2'))
+            config.save()
+
+            dump = self._dump_settings(config)
+            self.assertEquals([
+                               u'[a]\n',
+                               u"option1 = Voilà l'été\n", 
+                               u"option2 = Voilà l'été\n", 
+                               u'[inherit]\n',
+                               u"file = trac-site.ini\n", 
+                               ],
+                              dump)
+            config2 = self._read()
+            self.assertEquals('x', config2.get('a', 'option'))
+            self.assertEquals(u"Voilà l'été", config2.get('a', 'option1'))
+            self.assertEquals(u"Voilà l'été", config2.get('a', 'option2'))
+        self._test_with_inherit(testcb)
+
+
+def suite():
+    return unittest.makeSuite(ProductConfigTestCase,'test')
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
+

Added: incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/env.py
URL: http://svn.apache.org/viewvc/incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/env.py?rev=1430768&view=auto
==============================================================================
--- incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/env.py (added)
+++ incubator/bloodhound/branches/bep_0003_multiproduct/bloodhound_multiproduct/tests/env.py Wed Jan  9 10:24:22 2013
@@ -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
+
+# 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.
+    """
+
+    # Product data
+
+    default_product = 'tp1'
+    MAX_TEST_PRODUCT = 3
+
+    PRODUCT_DATA = dict(
+            ['tp' + str(i), {'prefix':'tp' + str(i),
+                             'name' : 'test product ' + str(i),
+                             'description' : 'desc for tp' + str(i)}]
+            for i in xrange(1, MAX_TEST_PRODUCT)
+        )
+
+    # Test setup
+
+    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 = env.config
+        config.set('logging', 'log_file', logpath)
+        config.set('logging', 'log_type', 'file')
+        config.set('logging', 'log_level', 'DEBUG')
+        config.save()
+        env.setup_log()
+        env.log.info('%s test case: %s %s',
+                '-' * 10, self.id(), '-' * 10)
+
+    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(env)
+        product._data.update(product_data)
+        product.insert()
+
+    def _upgrade_mp(self, env):
+        r"""Apply multi product upgrades
+        """
+        self.mpsystem = MultiProductSystem(env)
+        try:
+            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')
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
+



Mime
View raw message