Commits

Olemis Lang  committed 3512cd0

BH Multiproduct #115 : Product settings model . Rebase against r1427886 .

  • Participants
  • Parent commits 12b3a2a
  • Branches t115_product_env

Comments (0)

Files changed (4)

-t115/t115_r1423431_product_envs.diff
+t115/t115_r1427886_product_envs.diff
+t115_r1427886_product_config.diff
 t115/t115_r1423431_product_envs_testing.diff

File t115/t115_r1423431_product_envs.diff

-# HG changeset patch
-# Parent 818091ba4950f98af041fb73703e40acd792b59b
-BH Multiproduct #115 : Product environments
-
-diff --git a/bloodhound_multiproduct/multiproduct/api.py b/bloodhound_multiproduct/multiproduct/env.py
-copy from bloodhound_multiproduct/multiproduct/api.py
-copy to bloodhound_multiproduct/multiproduct/env.py
---- a/bloodhound_multiproduct/multiproduct/api.py
-+++ b/bloodhound_multiproduct/multiproduct/env.py
-@@ -16,167 +16,461 @@
- #  specific language governing permissions and limitations
- #  under the License.
- 
--"""Core components to support multi-product"""
--from datetime import datetime
-+"""Bloodhound product environment and related APIs"""
- 
--from genshi.builder import tag
-+import os.path
- 
--from pkg_resources import resource_filename
--from trac.core import Component, TracError, implements
--from trac.db import Table, Column, DatabaseManager
--from trac.env import IEnvironmentSetupParticipant
--from trac.perm import IPermissionRequestor
--from trac.resource import IResourceManager
--from trac.ticket.api import ITicketFieldProvider
--from trac.util.translation import _, N_
--from trac.web.chrome import ITemplateProvider
-+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
- 
--DB_VERSION = 2
--DB_SYSTEM_KEY = 'bloodhound_multi_product_version'
--PLUGIN_NAME = 'Bloodhound multi product'
-+class ProductEnvironment(Component, ComponentManager):
-+    """Bloodhound product-aware environment manager.
- 
--class MultiProductSystem(Component):
--    """Creates the database tables and template directories"""
--    
--    implements(IEnvironmentSetupParticipant, ITemplateProvider,
--            IPermissionRequestor, ITicketFieldProvider, IResourceManager)
--    
--    SCHEMA = [
--        Table('bloodhound_product', key = ['prefix', 'name']) [
--            Column('prefix'),
--            Column('name'),
--            Column('description'),
--            Column('owner'),
--            ],
--        Table('bloodhound_productresourcemap', key = 'id') [
--            Column('id', auto_increment=True),
--            Column('product_id'),
--            Column('resource_type'),
--            Column('resource_id'),
--            ]
--        ]
--    
--    def get_version(self):
--        """Finds the current version of the bloodhound database schema"""
--        rows = self.env.db_query("""
--            SELECT value FROM system WHERE name = %s
--            """, (DB_SYSTEM_KEY,))
--        return int(rows[0][0]) if rows else -1
--    
--    # IEnvironmentSetupParticipant methods
--    def environment_created(self):
--        """Insertion of any default data into the database."""
--        self.log.debug("creating environment for %s plugin." % PLUGIN_NAME)
--    
--    def environment_needs_upgrade(self, db_dummy=None):
--        """Detects if the installed db version matches the running system"""
--        db_installed_version = self.get_version()
--        
--        if db_installed_version > DB_VERSION:
--            raise TracError('''Current db version (%d) newer than supported by
--            this version of the %s (%d).''' % (db_installed_version,
--                                               PLUGIN_NAME,
--                                               DB_VERSION))
--        return db_installed_version < DB_VERSION
--    
--    def upgrade_environment(self, db_dummy=None):
--        """Installs or updates tables to current version"""
--        self.log.debug("upgrading existing environment for %s plugin." % 
--                       PLUGIN_NAME)
--        db_installed_version = self.get_version()
--        #cursor = db.cursor()
--        with self.env.db_transaction as db:
--            if db_installed_version < 0:
--                # Initial installation
--                db("""
--                    INSERT INTO system (name, value) VALUES ('%s','%s')
--                    """  % (DB_SYSTEM_KEY, DB_VERSION))
--                db("ALTER TABLE ticket ADD COLUMN product TEXT")
--                self.log.debug("creating initial db tables for %s plugin." % 
--                               PLUGIN_NAME)
--                
--                db_connector, dummy = DatabaseManager(self.env)._get_connector()
--                for table in self.SCHEMA:
--                    for statement in db_connector.to_sql(table):
--                        db(statement)
--                db_installed_version = self.get_version()
--            
--            if db_installed_version == 1:
--                from multiproduct.model import Product
--                products = Product.select(self.env)
--                for prod in products:
--                    db("""UPDATE ticket SET product=%s
--                          WHERE product=%s""", (prod.prefix, prod.name))
--                
--                db("""UPDATE system SET value=%s
--                      WHERE name=%s""", (DB_VERSION, DB_SYSTEM_KEY))
--                self.log.info("Upgraded multiproduct db schema from version %d"
--                              " to %d" % (db_installed_version, DB_VERSION))
--    
--    # ITemplateProvider methods
--    def get_templates_dirs(self):
--        """provide the plugin templates"""
--        return [resource_filename(__name__, 'templates')]
--    
--    def get_htdocs_dirs(self):
--        """proved the plugin htdocs"""
--        return []
-+    Bloodhound encapsulates access to product resources stored inside a
-+    Trac environment via product environments. They are compatible lightweight
-+    irepresentations of top level environment. 
- 
--    # IPermissionRequestor methods
--    def get_permission_actions(self):
--        acts = ['PRODUCT_CREATE', 'PRODUCT_DELETE', 'PRODUCT_MODIFY',
--                'PRODUCT_VIEW']
--        return acts + [('PRODUCT_ADMIN', acts)] + [('ROADMAP_ADMIN', acts)]
-+    Product environments contain among other things:
- 
--    # ITicketFieldProvider methods
--    def get_select_fields(self):
--        """Product select fields"""
--        return [(35, {'name': 'product', 'label': N_('Product'),
--                      'cls': Product, 'pk': 'prefix', 'optional': True})]
--    
--    def get_radio_fields(self):
--        """Product radio fields"""
--        return []
-+    * a configuration file, 
-+    * product-aware clones of the wiki and ticket attachments files,
- 
--    # IResourceManager methods
-+    Product environments do not have:
- 
--    def get_resource_realms(self):
--        """Manage 'product' realm.
-+    * 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.
-         """
--        yield 'product'
-+        return self.env.system_info_providers
- 
--    def get_resource_description(self, resource, format='default', context=None,
--                                 **kwargs):
--        """Describe product resource.
-+    @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.
-         """
--        desc = resource.id
--        if format != 'compact':
--            desc = _('Product %(name)s', name=resource.id)
--        if context:
--            return self._render_link(context, resource.id, desc)
-+        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:
--            return desc
-+            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)
- 
--    def _render_link(self, context, name, label, extra=''):
--        """Render link to product page.
-+        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
-         """
--        product = Product.select(self.env, where={'name' : name})
--        if product:
--            product = product[0]
--            href = context.href.products(product.prefix)
--            if 'PRODUCT_VIEW' in context.perm(product.resource):
--                return tag.a(label, class_='product', href=href + extra)
--        elif 'PRODUCT_CREATE' in context.perm('product', name):
--            return tag.a(label, class_='missing product', 
--                    href=context.href('products', action='new'),
--                    rel='nofollow')
--        return tag.a(label, class_='missing product')
-+        return self.env.backup(dest)
- 
--    def resource_exists(self, resource):
--        """Check whether product exists physically.
-+    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
-         """
--        products = Product.select(self.env, where={'name' : resource.id})
--        return bool(products)
-+        # (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
-+

File t115/t115_r1427886_product_config.diff

+# HG changeset patch
+# Parent cbbe8130b4d14f7221cc35722e9a78057efdc414
+BH Multiproduct #115 : Product-specific settings
+
+diff -r cbbe8130b4d1 bloodhound_multiproduct/multiproduct/model.py
+--- a/bloodhound_multiproduct/multiproduct/model.py	Wed Jan 02 22:32:51 2013 -0500
++++ b/bloodhound_multiproduct/multiproduct/model.py	Wed Jan 02 23:59:19 2013 -0500
+@@ -27,6 +27,10 @@
+ 
+ from bhdashboard.model import ModelBase
+ 
++# -------------------------------------------
++# Product API
++# -------------------------------------------
++
+ 
+ class Product(ModelBase):
+     """The Product table"""
+@@ -37,12 +41,12 @@
+             '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 +63,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 @@
+             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."""
+@@ -87,7 +91,7 @@
+             'no_change_fields':['id',],
+             'unique_fields':[],
+             }
+-    
++
+     def reparent_resource(self, product=None):
+         """a specific function to update a record when it is to move product"""
+         if product is not None:
+@@ -100,3 +104,17 @@
+         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':[],
++            }
++

File t115/t115_r1427886_product_envs.diff

+# HG changeset patch
+# Parent 818091ba4950f98af041fb73703e40acd792b59b
+BH Multiproduct #115 : Product environments
+
+diff --git a/bloodhound_multiproduct/multiproduct/api.py b/bloodhound_multiproduct/multiproduct/env.py
+copy from bloodhound_multiproduct/multiproduct/api.py
+copy to bloodhound_multiproduct/multiproduct/env.py
+--- a/bloodhound_multiproduct/multiproduct/api.py
++++ b/bloodhound_multiproduct/multiproduct/env.py
+@@ -16,167 +16,461 @@
+ #  specific language governing permissions and limitations
+ #  under the License.
+ 
+-"""Core components to support multi-product"""
+-from datetime import datetime
++"""Bloodhound product environment and related APIs"""
+ 
+-from genshi.builder import tag
++import os.path
+ 
+-from pkg_resources import resource_filename
+-from trac.core import Component, TracError, implements
+-from trac.db import Table, Column, DatabaseManager
+-from trac.env import IEnvironmentSetupParticipant
+-from trac.perm import IPermissionRequestor
+-from trac.resource import IResourceManager
+-from trac.ticket.api import ITicketFieldProvider
+-from trac.util.translation import _, N_
+-from trac.web.chrome import ITemplateProvider
++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
+ 
+-DB_VERSION = 2
+-DB_SYSTEM_KEY = 'bloodhound_multi_product_version'
+-PLUGIN_NAME = 'Bloodhound multi product'
++class ProductEnvironment(Component, ComponentManager):
++    """Bloodhound product-aware environment manager.
+ 
+-class MultiProductSystem(Component):
+-    """Creates the database tables and template directories"""
+-    
+-    implements(IEnvironmentSetupParticipant, ITemplateProvider,
+-            IPermissionRequestor, ITicketFieldProvider, IResourceManager)
+-    
+-    SCHEMA = [
+-        Table('bloodhound_product', key = ['prefix', 'name']) [
+-            Column('prefix'),
+-            Column('name'),
+-            Column('description'),
+-            Column('owner'),
+-            ],
+-        Table('bloodhound_productresourcemap', key = 'id') [
+-            Column('id', auto_increment=True),
+-            Column('product_id'),
+-            Column('resource_type'),
+-            Column('resource_id'),
+-            ]
+-        ]
+-    
+-    def get_version(self):
+-        """Finds the current version of the bloodhound database schema"""
+-        rows = self.env.db_query("""
+-            SELECT value FROM system WHERE name = %s
+-            """, (DB_SYSTEM_KEY,))
+-        return int(rows[0][0]) if rows else -1
+-    
+-    # IEnvironmentSetupParticipant methods
+-    def environment_created(self):
+-        """Insertion of any default data into the database."""
+-        self.log.debug("creating environment for %s plugin." % PLUGIN_NAME)
+-    
+-    def environment_needs_upgrade(self, db_dummy=None):
+-        """Detects if the installed db version matches the running system"""
+-        db_installed_version = self.get_version()
+-        
+-        if db_installed_version > DB_VERSION:
+-            raise TracError('''Current db version (%d) newer than supported by
+-            this version of the %s (%d).''' % (db_installed_version,
+-                                               PLUGIN_NAME,
+-                                               DB_VERSION))
+-        return db_installed_version < DB_VERSION
+-    
+-    def upgrade_environment(self, db_dummy=None):
+-        """Installs or updates tables to current version"""
+-        self.log.debug("upgrading existing environment for %s plugin." % 
+-                       PLUGIN_NAME)
+-        db_installed_version = self.get_version()
+-        #cursor = db.cursor()
+-        with self.env.db_transaction as db:
+-            if db_installed_version < 0:
+-                # Initial installation
+-                db("""
+-                    INSERT INTO system (name, value) VALUES ('%s','%s')
+-                    """  % (DB_SYSTEM_KEY, DB_VERSION))
+-                db("ALTER TABLE ticket ADD COLUMN product TEXT")
+-                self.log.debug("creating initial db tables for %s plugin." % 
+-                               PLUGIN_NAME)
+-                
+-                db_connector, dummy = DatabaseManager(self.env)._get_connector()
+-                for table in self.SCHEMA:
+-                    for statement in db_connector.to_sql(table):
+-                        db(statement)
+-                db_installed_version = self.get_version()
+-            
+-            if db_installed_version == 1:
+-                from multiproduct.model import Product
+-                products = Product.select(self.env)
+-                for prod in products:
+-                    db("""UPDATE ticket SET product=%s
+-                          WHERE product=%s""", (prod.prefix, prod.name))
+-                
+-                db("""UPDATE system SET value=%s
+-                      WHERE name=%s""", (DB_VERSION, DB_SYSTEM_KEY))
+-                self.log.info("Upgraded multiproduct db schema from version %d"
+-                              " to %d" % (db_installed_version, DB_VERSION))
+-    
+-    # ITemplateProvider methods
+-    def get_templates_dirs(self):
+-        """provide the plugin templates"""
+-        return [resource_filename(__name__, 'templates')]
+-    
+-    def get_htdocs_dirs(self):
+-        """proved the plugin htdocs"""
+-        return []
++    Bloodhound encapsulates access to product resources stored inside a
++    Trac environment via product environments. They are compatible lightweight
++    irepresentations of top level environment. 
+ 
+-    # IPermissionRequestor methods
+-    def get_permission_actions(self):
+-        acts = ['PRODUCT_CREATE', 'PRODUCT_DELETE', 'PRODUCT_MODIFY',
+-                'PRODUCT_VIEW']
+-        return acts + [('PRODUCT_ADMIN', acts)] + [('ROADMAP_ADMIN', acts)]
++    Product environments contain among other things:
+ 
+-    # ITicketFieldProvider methods
+-    def get_select_fields(self):
+-        """Product select fields"""
+-        return [(35, {'name': 'product', 'label': N_('Product'),
+-                      'cls': Product, 'pk': 'prefix', 'optional': True})]
+-    
+-    def get_radio_fields(self):
+-        """Product radio fields"""
+-        return []
++    * a configuration file, 
++    * product-aware clones of the wiki and ticket attachments files,
+ 
+-    # IResourceManager methods
++    Product environments do not have:
+ 
+-    def get_resource_realms(self):
+-        """Manage 'product' realm.
++    * 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.
+         """
+-        yield 'product'
++        return self.env.system_info_providers
+ 
+-    def get_resource_description(self, resource, format='default', context=None,
+-                                 **kwargs):
+-        """Describe product resource.
++    @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.
+         """
+-        desc = resource.id
+-        if format != 'compact':
+-            desc = _('Product %(name)s', name=resource.id)
+-        if context:
+-            return self._render_link(context, resource.id, desc)
++        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:
+-            return desc
++            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)
+ 
+-    def _render_link(self, context, name, label, extra=''):
+-        """Render link to product page.
++        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
+         """
+-        product = Product.select(self.env, where={'name' : name})
+-        if product:
+-            product = product[0]
+-            href = context.href.products(product.prefix)
+-            if 'PRODUCT_VIEW' in context.perm(product.resource):
+-                return tag.a(label, class_='product', href=href + extra)
+-        elif 'PRODUCT_CREATE' in context.perm('product', name):
+-            return tag.a(label, class_='missing product', 
+-                    href=context.href('products', action='new'),
+-                    rel='nofollow')
+-        return tag.a(label, class_='missing product')
++        return self.env.backup(dest)
+ 
+-    def resource_exists(self, resource):
+-        """Check whether product exists physically.
++    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
+         """
+-        products = Product.select(self.env, where={'name' : resource.id})
+-        return bool(products)
++        # (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
++