blob: 23a0b6ded8f0f1010ff0304d683461caa886d968 [file] [log] [blame]
# -*- 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.
import os.path
from urlparse import urlsplit
from trac.config import BoolOption, ConfigSection, Option
from trac.core import Component, ComponentManager, ExtensionPoint, implements, \
ComponentMeta
from trac.db.api import TransactionContextManager, QueryContextManager, \
DatabaseManager
from trac.perm import IPermissionRequestor, PermissionSystem
from trac.util import get_pkginfo, lazy
from trac.util.compat import sha1
from trac.util.text import to_unicode, unicode_quote
from trac.versioncontrol import RepositoryManager
from trac.web.href import Href
from multiproduct.api import MultiProductSystem, ISupportMultiProductEnvironment
from multiproduct.cache import lru_cache, default_keymap
from multiproduct.config import Configuration
from multiproduct.dbcursor import BloodhoundConnectionWrapper, BloodhoundIterableCursor, \
ProductEnvContextManager
from multiproduct.model import Product
import trac.env
class ComponentEnvironmentContext(object):
def __init__(self, env, component):
self._env = env
self._component = component
def __enter__(self):
self._old_env = self._component.env
self._env.component_activated(self._component)
return self
def __exit__(self, type, value, traceback):
self._old_env.component_activated(self._component)
class Environment(trac.env.Environment):
"""Bloodhound environment manager
This class is intended as monkey-patch replacement for
trac.env.Environment. Required database access methods/properties
are replaced to provide global view of the database in contrast
to ProductEnvironment that features per-product view of the database
(in the context of selected product).
:param path: the absolute path to the Trac environment
:param create: if `True`, the environment is created and
populated with default data; otherwise, the
environment is expected to already exist.
:param options: A list of `(section, name, value)` tuples that
define configuration options
"""
multi_product_support_components = ExtensionPoint(ISupportMultiProductEnvironment)
@property
def global_setup_participants(self):
"""If multi product schema is enabled, return only setup participants
that implement ISupportMultiProduct. Otherwise, all setup participants
are considered global.
"""
if self._multiproduct_schema_enabled:
all_participants = self.setup_participants
multiproduct_aware = set(self.multi_product_support_components)
priority = lambda x: 0 if isinstance(x, MultiProductSystem) else 10
return sorted(
(c for c in all_participants if c in multiproduct_aware),
key=priority
)
else:
return self.setup_participants
@property
def product_setup_participants(self):
"""If multi product schema is enabled, return setup participants that
need to be instantiated for each product env. Otherwise, return an
empty list.
"""
if self._multiproduct_schema_enabled:
all_participants = self.setup_participants
multiproduct_aware = set(self.multi_product_support_components)
return [
c for c in all_participants if c not in multiproduct_aware
]
else:
return []
def __init__(self, path, create=False, options=[]):
# global environment w/o parent, set these two before super.__init__
# as database access can take place within trac.env.Environment
self.parent = None
self.product = None
# `trac.env.Environment.__init__` is not invoked as creation is handled differently
# from base implementation - different setup participants are invoked when creating
# global environment.
ComponentManager.__init__(self)
self.path = path
self.systeminfo = []
self._href = self._abs_href = None
self._multiproduct_schema_enabled = False
if create:
self.create(options)
else:
self.verify()
self.setup_config()
# invoke `IEnvironmentSetupParticipant.environment_created` for all
# global setup participants
if create:
for participant in self.global_setup_participants:
with ComponentEnvironmentContext(self, participant):
participant.environment_created()
@property
def db_query(self):
return ProductEnvContextManager(super(Environment, self).db_query, self) \
if self._multiproduct_schema_enabled else self.db_direct_query
@property
def db_transaction(self):
return ProductEnvContextManager(super(Environment, self).db_transaction, self) \
if self._multiproduct_schema_enabled else self.db_direct_transaction
@property
def db_direct_query(self):
return ProductEnvContextManager(super(Environment, self).db_query)
@property
def db_direct_transaction(self):
return ProductEnvContextManager(super(Environment, self).db_transaction)
def all_product_envs(self):
return [ProductEnvironment(self, product) for product in Product.select(self)]
def needs_upgrade(self):
"""Return whether the environment needs to be upgraded."""
def needs_upgrade_in_env(participant, env):
with ComponentEnvironmentContext(env, participant):
with env.db_query as db:
if participant.environment_needs_upgrade(db):
self.log.warn("component %s.%s requires environment upgrade in environment %s...",
participant.__module__, participant.__class__.__name__,
env)
return True
if any(needs_upgrade_in_env(participant, self)
for participant in self.global_setup_participants):
return True
# until schema is multi product aware, product environments can't (and shouldn't) be
# instantiated
if self._multiproduct_schema_enabled:
if any(needs_upgrade_in_env(participant, env)
for env in [self] + self.all_product_envs()
for participant in self.product_setup_participants):
return True
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
"""
def upgrader_for_env(participant, env):
with ComponentEnvironmentContext(env, participant):
with env.db_query as db:
if participant.environment_needs_upgrade(db):
self.log.info(
"%s.%s needs upgrade in environment %s...",
participant.__module__,
participant.__class__.__name__,
env)
return env, participant
def upgraders_for_product_envs():
upgraders = (upgrader_for_env(participant, env)
for participant in self.product_setup_participants
for env in [self] + self.all_product_envs())
return [u for u in upgraders if u]
# first enumerate components that are multi product aware and
# require upgrade in global environment
global_upgraders = [upgrader_for_env(participant, self)
for participant in self.global_setup_participants]
global_upgraders = [u for u in global_upgraders if u]
product_upgraders = None
if not global_upgraders and self._multiproduct_schema_enabled:
# if no upgrades required in global environment, enumerate
# required upgrades for product environments
product_upgraders = upgraders_for_product_envs()
if not global_upgraders + (product_upgraders or []):
return False
if backup:
try:
self.backup(backup_dest)
except Exception, e:
raise trac.env.BackupError(e)
def execute_upgrades(upgraders_list):
for env, participant in upgraders_list:
self.log.info("%s.%s upgrading in environment %s...",
participant.__module__,
participant.__class__.__name__,
env)
with ComponentEnvironmentContext(env, participant):
with env.db_transaction as db:
participant.upgrade_environment(db)
# Database schema may have changed, so close all connections
DatabaseManager(env).shutdown()
# execute global upgrades first, product environment upgrades next
execute_upgrades(global_upgraders)
if product_upgraders is None and self._multiproduct_schema_enabled:
product_upgraders = upgraders_for_product_envs()
if product_upgraders:
execute_upgrades(product_upgraders)
return True
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.
"""
rows = self.db_direct_query("""
SELECT value FROM system WHERE name='%sdatabase_version'
""" % ('initial_' if initial else ''))
return (rows and int(rows[0][0])) or 0
def enable_multiproduct_schema(self, enable=True):
self._multiproduct_schema_enabled = enable
BloodhoundIterableCursor.cache_reset()
# replace trac.env.Environment with Environment
trac.env.Environment = Environment
# this must follow the monkey patch (trac.env.Environment) above, otherwise
# trac.test.EnvironmentStub will not be correct as the class will derive from
# not replaced trac.env.Environment
import trac.test
class EnvironmentStub(trac.test.EnvironmentStub):
"""Bloodhound test environment stub
This class replaces trac.test.EnvironmentStub and extends it with parent
and product properties (same case as with the Environment).
"""
def __init__(self, default_data=False, enable=None, disable=None,
path=None, destroying=False):
self.parent = None
self.product = None
self._multiproduct_schema_enabled = False
super(EnvironmentStub, self).__init__(default_data=False,
enable=enable, disable=disable,
path=path, destroying=destroying)
if default_data:
self.reset_db(default_data)
@staticmethod
def enable_component_in_config(env, cls):
"""Keep track of enabled state in configuration as well
during test runs. This is closer to reality than
inherited `enable_component` method.
"""
env.config['components'].set(env._component_name(cls), 'enabled')
env.enabled.clear()
env.components.pop(cls, None)
try:
del env._rules
except AttributeError:
pass
# FIXME: Shall we ?
#env.config.save()
@staticmethod
def disable_component_in_config(env, component):
"""Keep track of disabled state in configuration as well
during test runs. This is closer to reality than
inherited `disable_component` method.
"""
if isinstance(component, type):
cls = component
else:
cls = component.__class__
env.config['components'].set(env._component_name(cls), 'disabled')
env.enabled.clear()
env.components.pop(cls, None)
try:
del env._rules
except AttributeError:
pass
env.config.save()
def reset_db(self, default_data=None):
multiproduct_schema = self._multiproduct_schema_enabled
self._multiproduct_schema_enabled = False
try:
super(EnvironmentStub, self).reset_db(default_data=default_data)
finally:
self._multiproduct_schema_enabled = multiproduct_schema
# replace trac.test.EnvironmentStub
trac.test.EnvironmentStub = EnvironmentStub
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:
* configuration key-value pairs stored in the database,
* 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
"""
class __metaclass__(ComponentMeta):
def select_global_env(f):
"""Replaces env with env.parent where appropriate"""
# Keep the signature of __call__ method
def __call__(self, env, *args, **kwargs):
g_env = env.parent if isinstance(env,
ProductEnvironment) else env
return f(self, g_env, *args, **kwargs)
__call__.clear = f.clear
return __call__
def product_env_keymap(args, kwds, kwd_mark):
# Remove meta-reference to self (i.e. product env class)
args = args[1:]
try:
product = kwds['product']
except KeyError:
# Product provided as positional argument
if isinstance(args[1], Product):
args = (args[0], args[1].prefix) + args[2:]
else:
# Product supplied as keyword argument
if isinstance(product, Product):
kwds['product'] = product.prefix
return default_keymap(args, kwds, kwd_mark)
@select_global_env
@lru_cache(maxsize=100, keymap=product_env_keymap)
def __call__(self, *args, **kwargs):
"""Return an existing instance if there is a hit
in the global LRU cache, otherwise create a new instance.
"""
return ComponentMeta.__call__(self, *args, **kwargs)
del product_env_keymap, select_global_env
implements(trac.env.ISystemInfoProvider, IPermissionRequestor)
setup_participants = ExtensionPoint(trac.env.IEnvironmentSetupParticipant)
multi_product_support_components = ExtensionPoint(ISupportMultiProductEnvironment)
@classmethod
def clear_env_cache(cls):
cls.__metaclass__.__call__.clear()
@property
def product_setup_participants(self):
return [
component for component in self.setup_participants
if component not in self.multi_product_support_components
]
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 ''
_base_url = Option('trac', 'base_url', '',
"""Reference URL for the Trac deployment.
This is the base URL that will be used when producing
documents that will be used outside of the web browsing
context, like for example when inserting URLs pointing to Trac
resources in notification e-mails.""", doc_domain='multiproduct')
@property
def base_url(self):
base_url = self._base_url
if base_url == self.parent.base_url:
return ''
return base_url
_base_url_for_redirect = BoolOption('trac', 'use_base_url_for_redirect',
False,
"""Optionally use `[trac] base_url` for redirects.
In some configurations, usually involving running Trac behind
a HTTP proxy, Trac can't automatically reconstruct the URL
that is used to access it. You may need to use this option to
force Trac to use the `base_url` setting also for
redirects. This introduces the obvious limitation that this
environment will only be usable when accessible from that URL,
as redirects are frequently used. ''(since 0.10.5)''""",
doc_domain='multiproduct')
@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.
"""
# FIXME: Should products have different values i.e. config option ?
return self.parent.project_url
project_admin = Option('project', 'admin', '',
"""E-Mail address of the product's leader / administrator.""",
doc_domain='multiproduct')
@property
def project_footer(self):
"""Page footer text (right-aligned).
"""
# FIXME: Should products have different values i.e. config option ?
return self.parent.project_footer
project_icon = Option('project', 'icon', 'common/trac.ico',
"""URL of the icon of the product.""", doc_domain='multiproduct')
log_type = Option('logging', 'log_type', 'inherit',
"""Logging facility to use.
Should be one of (`inherit`, `none`, `file`, `stderr`,
`syslog`, `winlog`).""", doc_domain='multiproduct')
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.""", doc_domain='multiproduct')
log_level = Option('logging', 'log_level', 'DEBUG',
"""Level of verbosity in log.
Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""",
doc_domain='multiproduct')
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)''""", doc_domain='multiproduct')
def __init__(self, env, product, create=False):
"""Initialize the product environment.
:param env: the global Trac environment
:param product: product prefix or an instance of
multiproduct.model.Product
"""
if not isinstance(env, trac.env.Environment):
cls = self.__class__
raise TypeError("Initializer must be called with " \
"trac.env.Environment instance as first argument " \
"(got %s instance instead)" %
(cls.__module__ + '.' + cls.__name__, ))
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.parent = env
self.product = product
self.systeminfo = []
self.setup_config()
# when creating product environment, invoke `IEnvironmentSetupParticipant.environment_created`
# for all setup participants that don't support multi product environments
if create:
for participant in self.product_setup_participants:
with ComponentEnvironmentContext(self, participant):
participant.environment_created()
def __getitem__(self, cls):
if issubclass(cls, trac.env.Environment):
return self.parent
elif cls is self.__class__:
return self
else:
return ComponentManager.__getitem__(self, cls)
def __getattr__(self, attrnm):
"""Forward attribute access request to parent environment.
Initially this will affect the following members of
`trac.env.Environment` class:
system_info_providers, secure_cookies, project_admin_trac_url,
get_system_info, get_version, get_templates_dir, get_templates_dir,
get_log_dir, backup
"""
try:
if attrnm in ('parent', '_rules'):
raise AttributeError
return getattr(self.parent, attrnm)
except AttributeError:
raise AttributeError("'%s' object has no attribute '%s'" %
(self.__class__.__name__, attrnm))
def __repr__(self):
return "<%s %s at %s>" % (self.__class__.__name__,
repr(self.product.prefix),
hex(id(self)))
@lazy
def path(self):
"""The subfolder `./products/<product prefix>` relative to the
top-level directory of the global environment will be the root of
product file system area.
"""
folder = os.path.join(self.parent.path, 'products', self.product.prefix)
if not os.path.exists(folder):
os.makedirs(folder)
return folder
# IPermissionRequestor methods
def get_permission_actions(self):
"""Implement the product-specific `PRODUCT_ADMIN` meta permission.
"""
actions = set()
permsys = PermissionSystem(self)
for requestor in permsys.requestors:
if requestor is not self and requestor is not permsys:
for action in requestor.get_permission_actions() or []:
if isinstance(action, tuple):
actions.add(action[0])
else:
actions.add(action)
# PermissionSystem's method was not invoked
actions.add('EMAIL_VIEW')
# FIXME: should not be needed, JIC better double check
actions.discard('TRAC_ADMIN')
return [('PRODUCT_ADMIN', list(actions))]
# ISystemInfoProvider methods
# Same as parent environment's . Avoid duplicated code
component_activated = trac.env.Environment.component_activated.im_func
_component_name = trac.env.Environment._component_name.im_func
_component_rules = trac.env.Environment._component_rules
enable_component = trac.env.Environment.enable_component.im_func
get_known_users = trac.env.Environment.get_known_users.im_func
get_repository = trac.env.Environment.get_repository.im_func
is_component_enabled_local = trac.env.Environment.is_component_enabled.im_func
def is_enabled(self, cls):
"""Return whether the given component class is enabled."""
modtime = max(self.config.get_lock_file_mtime(),
self.config._lastmtime)
if modtime > self._config_mtime:
self.enabled.clear()
try:
del self._rules
except AttributeError:
pass
# FIXME : Improve cache hits by tracking global env last mtime
self.parent.enabled.clear()
try:
del self.parent._rules
except AttributeError:
pass
self._config_mtime = modtime
return super(ProductEnvironment, self).is_enabled(cls)
def is_component_enabled(self, cls):
"""Implemented to only allow activation of components already
activated in the global environment that are in turn not disabled in
the configuration.
This is called by the `ComponentManager` base class when a
component is about to be activated. If this method returns
`False`, the component does not get activated. If it returns
`None`, the component only gets activated if it is located in
the `plugins` directory of the environment.
"""
if cls is self.__class__:
# Prevent lookups in parent env ... will always fail
return True
# FIXME : Maybe checking for ComponentManager is too drastic
elif issubclass(cls, ComponentManager):
# Avoid clashes with overridden Environment's options
return False
elif self.parent[cls] is None:
return False
return self.is_component_enabled_local(cls)
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:
...
"""
return BloodhoundConnectionWrapper(self.parent.get_db_cnx(), self)
@property
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:
...
"""
# exception types same as in global environment
return self.parent.db_exc
def with_transaction(self, db=None):
"""Decorator for transaction functions :deprecated:
"""
raise NotImplementedError('Deprecated method')
def get_read_db(self):
"""Return a database connection for read purposes :deprecated:
See `trac.db.api.get_read_db` for detailed documentation.
"""
return BloodhoundConnectionWrapper(self.parent.get_read_db(), self)
@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`).
"""
return ProductEnvContextManager(QueryContextManager(self.parent), self)
@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`).
"""
return ProductEnvContextManager(TransactionContextManager(self.parent), self)
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 setup_config(self):
"""Load the configuration object.
"""
import trac.config
parent_path = MultiProductSystem(self.parent).product_config_parent
if parent_path and os.path.isfile(parent_path):
parents = [trac.config.Configuration(parent_path)]
else:
parents = [self.parent.config]
self.config = Configuration(self.parent, self.product.prefix, parents)
self._config_mtime = 0
self.setup_log()
def setup_log(self):
"""Initialize the logging sub-system."""
from trac.log import logger_handler_factory
logtype = self.log_type
logfile = self.log_file
format = self.log_format
self.parent.log.debug("Log type '%s' for product '%s'",
logtype, self.product.prefix)
# Force logger inheritance on identical configuration
if (logtype, logfile, format) == (self.parent.log_type,
self.parent.log_file, self.parent.log_format):
logtype = 'inherit'
if logtype == 'inherit':
self.log = self.parent.log
self._log_handler = self.parent._log_handler
self.parent.log.info("Inheriting parent logger for product '%s'",
self.product.prefix)
else:
if logtype == 'file' and not os.path.isabs(logfile):
logfile = os.path.join(self.get_log_dir(), logfile)
logid = 'Trac.%s.%s' % \
(sha1(self.parent.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 +
' product %s environment startup [Trac %s] ' +
'-' * 32,
self.product.prefix,
get_pkginfo(core).get('version', VERSION))
def needs_upgrade(self):
"""Return whether the environment needs to be upgraded."""
# Upgrades are handled by global environment
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
"""
# Upgrades handled by global environment
return True
@lazy
def href(self):
"""The application root path"""
return Href(urlsplit(self.abs_href.base).path)
@lazy
def abs_href(self):
"""The application URL"""
if not self.base_url:
urlpattern = MultiProductSystem(self.parent).product_base_url
if not urlpattern:
self.log.warn("product_base_url option not set in "
"configuration, generated links may be "
"incorrect")
urlpattern = 'products/$(prefix)s'
envname = os.path.basename(self.parent.path)
prefix = unicode_quote(self.product.prefix, safe="")
name = unicode_quote(self.product.name, safe="")
url = urlpattern.replace('$(', '%(') \
.replace('%(envname)s', envname) \
.replace('%(prefix)s', prefix) \
.replace('%(name)s', name)
if urlsplit(url).netloc:
# Absolute URLs
_abs_href = Href(url)
else:
# Relative URLs
parent_href = Href(self.parent.abs_href(),
path_safe="/!~*'()%",
query_safe="!~*'()%")
_abs_href = Href(parent_href(url))
else:
_abs_href = Href(self.base_url)
return _abs_href
# Multi-product API extensions
@classmethod
def lookup_global_env(cls, env):
return env.parent if isinstance(env, ProductEnvironment) else env
@classmethod
def lookup_env(cls, env, prefix=None, name=None):
"""Instantiate environment according to product prefix or name
@throws LookupError if no product matches neither prefix nor name
"""
if isinstance(env, ProductEnvironment):
global_env = env.parent
else:
global_env = env
# FIXME: Update if multiproduct.dbcursor.GLOBAL_PRODUCT != ''
if not prefix and not name:
return global_env
elif isinstance(env, ProductEnvironment) and \
env.product.prefix == prefix:
return env
if prefix:
try:
return ProductEnvironment(global_env, prefix)
except LookupError:
if not name:
raise
if name:
# Lookup product by name
products = Product.select(global_env, where={'name' : name})
if products:
return ProductEnvironment(global_env, products[0])
else:
raise LookupError("Missing product '%s'" % (name,))
else:
raise LookupError("Mising product '%s'" % (prefix or name,))
@classmethod
def resolve_href(cls, to_env, at_env):
"""Choose absolute or relative href when generating links to
a product (or global) environment.
@param at_env: href expansion is taking place in the
scope of this environment
@param to_env: generated URLs point to resources in
this environment
"""
at_href = at_env.abs_href()
target_href = to_env.abs_href()
if urlsplit(at_href)[1] == urlsplit(target_href)[1]:
return to_env.href
else:
return to_env.abs_href
lookup_product_env = ProductEnvironment.lookup_env
resolve_product_href = ProductEnvironment.resolve_href
# Override product-specific options
from multiproduct.config import ProductPermissionPolicyOption
PermissionSystem.policies.__class__ = ProductPermissionPolicyOption