You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

600 lines
22 KiB

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import sys
import warnings
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse
from django.utils import six
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.functional import Promise
from django.utils.translation import activate, get_language, ugettext_lazy as _
from cms.exceptions import PluginAlreadyRegistered, PluginNotRegistered
from cms.models import CMSPlugin
from cms.plugin_pool import plugin_pool
from cms.toolbar.items import SubMenu, Break, AjaxItem
from ..cms_plugins import SegmentPluginBase
from ..models import SegmentBasePluginModel
#
# A simple enum so we can use the same code in Python's < 3.4.
#
class SegmentOverride:
NoOverride, ForcedActive, ForcedInactive = range(3)
overrides_list = [
(NoOverride, _('No override')),
(ForcedActive, _('Forced active')),
(ForcedInactive, _('Forced inactive')),
]
@python_2_unicode_compatible
class SegmentPool(object):
'''
This maintains a set of nested sorted dicts containing, among other
attributes, a list of segment plugin instances in the form:
segments = {
/class/ : {
NAME: _(/name/),
CFGS: {
/configuration_string/ : {
LABEL: _(/configuration_string/),
OVERRIDES: {
/user.id/: /SegmentOverride enum value/,
...
},
INSTANCES: [ ... ]
}
}
}
}
The outer-most dict organizes everything by the class name of the plugin.
The actual human-readable version of the plugin's type name is stored
under the key 'NAME'.
Each plugin's unique configuration is stored in the plugin type's CFGS
dict keyed with the instance's configuration_string realised as 'en'
unicode. The unresolved version of the string--usually a lazy translation
proxy object--is stored under the configuration's 'LABEL' key. This allows
translation to any other language (that is in the gettext catalog) at
will. This is also used for correctly sorting the configurations in the
current language.
This is implmented as a non-persistent, system-wide singleton, it is
shared by all operators of the system. In order to keep overrides for each
user distinct, they are stored in the OVERRIDES dict, keyed with the
user's username.
Plugin instances are recorded in the INSTANCES list. As well as allowing
us to find instances that have changed their configuration (likely to
happen when an operator changes the plugin's configuration), this allows
us to prune no-longer relevant parts of this structure as instances are
de-registered.
'''
#
# Magic Strings for managing the structures below.
#
CFGS = 'CFGS'
NAME = 'NAME'
LABEL = 'LABEL'
OVERRIDES = 'OVERRIDES'
INSTANCES = 'INSTANCES'
def __init__(self):
self.segments = dict()
self._sorted_segments = dict()
def discover(self):
'''
Find and register any SegmentPlugins already configured in the CMS and
register them.
'''
#
# To reduce the number of queries we'll be making against CMSPlugin,
# let's build a set of eligible plugin_types. This part should not hit
# the database at all (provided that the plugin_pool is already
# populated.)
#
# In this case, 'eligible' means that the plugin class subclasses
# SegmentPluginBase and that it has allow_overrides = True. Segment
# plugins that have allow_overrides = False are not registered.
#
plugin_types = []
for plugin_class in plugin_pool.get_all_plugins():
if (issubclass(plugin_class, SegmentPluginBase) and
plugin_class.allow_overrides):
plugin_types.append(plugin_class.__name__)
#
# Process the plugins that are one of these types.
#
for plugin_instance in CMSPlugin.objects.filter(plugin_type__in=plugin_types):
#
# Get the instance as an instance of its proper class rather than
# this CMSPlugin object.
#
plugin_instance = plugin_instance.get_plugin_instance()[0]
self.register_segment_plugin(plugin_instance, suppress_discovery=True)
def register_segment_plugin(self, plugin_instance, suppress_discovery=False):
'''
Registers the provided plugin_instance into the SegmentPool.
Raises:
PluginAlreadyRegistered: if the plugin is already registered and
ImproperlyConfigured: if not an appropriate type of plugin.
Note: plugin_instance.configuration_string can return either of:
1. A normal string of text,
2. A gettext_lazy object,
3. A extra-lazy object (Promise to return a gettext_lazy object)
the `suppress_discovery` flag, when set to true, prevents recursion
and should be only used by the self.discovery() method.
'''
if not suppress_discovery and not self.segments:
self.discover()
if isinstance(plugin_instance, SegmentBasePluginModel):
plugin_class_instance = plugin_instance.get_plugin_class_instance()
if plugin_class_instance.allow_overrides:
#
# There is no need to register a plugin that doesn't
# allow overrides.
#
plugin_class_name = plugin_class_instance.__class__.__name__
plugin_name = plugin_class_instance.name
if plugin_class_name not in self.segments:
self.segments[plugin_class_name] = {
self.NAME: plugin_name,
self.CFGS: dict(),
}
self._sorted_segments = dict()
segment_class = self.segments[plugin_class_name]
plugin_config = plugin_instance.configuration_string
#
# NOTE: We always use the 'en' version of the configuration string
# as the key.
#
lang = get_language()
activate('en')
if isinstance(plugin_config, Promise):
plugin_config_key = force_text(plugin_config)
elif isinstance(plugin_config, six.text_type):
plugin_config_key = plugin_config
else:
warnings.warn('register_segment: Not really sure what '
'‘plugin_instance.configuration_string’ returned!')
activate(lang)
segment_configs = segment_class[self.CFGS]
if plugin_config_key not in segment_configs:
# We store the un-translated version as the LABEL
segment_configs[plugin_config_key] = {
self.LABEL : plugin_config,
self.OVERRIDES : dict(),
self.INSTANCES : list(),
}
self._sorted_segments = dict()
segment = segment_configs[plugin_config_key]
if plugin_instance not in segment[self.INSTANCES]:
segment[self.INSTANCES].append( plugin_instance )
self._sorted_segments = dict()
else:
cls = plugin_instance.get_plugin_class_instance().__class__.__name__
raise PluginAlreadyRegistered('The segment plugin {0} cannot '
'be registered because it already is.'.format(cls))
else:
cls = plugin_instance.__class__.__name__
raise ImproperlyConfigured('Segment Plugin models must '
'subclass SegmentBasePluginModel. {0!r} does not.'.format(cls))
def unregister_segment_plugin(self, plugin_instance):
'''
Removes the given plugin from the SegmentPool.
'''
#
# NOTE: In many cases, the configuration of a given plugin may have
# changed before we receive the call to unregister it. So, we'll look
# for the plugin in all CFGS for this plugin's class.
#
if not self.segments:
self.discover()
if not isinstance(plugin_instance, SegmentBasePluginModel):
raise ImproperlyConfigured('Segment Plugins must subclasses of '
'SegmentBasePluginModel. {0} is not.'.format(
plugin_instance.get_plugin_class_instance().__class__.__name__
))
else:
plugin_class_instance = plugin_instance.get_plugin_class_instance()
if plugin_class_instance.allow_overrides:
#
# A segment plugin that doesn't allow overrides wouldn't be
# registered in the first place.
#
plugin_class_name = plugin_class_instance.__class__.__name__
if plugin_class_name in self.segments:
segment_class = self.segments[plugin_class_name]
segment_configs = segment_class[self.CFGS]
for configuration, data in segment_configs.items():
if plugin_instance in data[self.INSTANCES]:
# Found it! Now remove it...
data[self.INSTANCES].remove(plugin_instance)
self._sorted_segments = dict()
# Clean-up any empty elements caused by this removal...
if len(data[self.INSTANCES]) == 0:
# OK, this was the last one, so...
del segment_configs[configuration]
if len(segment_configs) == 0:
# This too was the last one
del self.segments[plugin_class_name]
return
try:
cls = plugin_instance.get_plugin_class_instance().__class__.__name__
raise PluginNotRegistered('The segment plugin {0} cannot be '
'unregistered because it is not currently registered in the '
'SegmentPool.'.format(cls))
except:
raise PluginNotRegistered()
def set_override(self, user, segment_class, segment_config, override):
'''
(Re-)Set an override on a segment (segment_class x segment_config).
'''
if not self.segments:
self.discover()
overrides = self.segments[segment_class][self.CFGS][segment_config][self.OVERRIDES]
if override == SegmentOverride.NoOverride:
del overrides[user.username]
else:
overrides[user.username] = override
self._sorted_segments = dict()
def reset_all_segment_overrides(self, user):
'''
Resets (disables) the overrides for all segments.
'''
if not self.segments:
self.discover()
for segment_class in self.segments.values():
for configuration in segment_class[self.CFGS].values():
for username, override in configuration[self.OVERRIDES].items():
if username == user.username:
configuration[self.OVERRIDES][username] = SegmentOverride.NoOverride
self._sorted_segments = dict()
def get_num_overrides_for_user(self, user):
'''
Returns a count of the number of overrides for all segments for the
given user. This is used for the toolbar menu where we show the number
of active overrides.
'''
if not self.segments:
self.discover()
num = 0
for segment_class_name, segment_class in self.segments.items():
for config_str, config in segment_class[self.CFGS].items():
for username, override in config[self.OVERRIDES].items():
if username == user.username and int(override):
num += 1
return num
def get_override_for_classname(self, user, plugin_class_name, segment_config):
'''
Given the user, plugin_class_name and segment_config, return the
current override, if any.
'''
#
# Note: segment_config can be either of:
#
# 1. A number string of text
# 2. A lazy translation object (Promise)
#
if not self.segments:
self.discover()
lang = get_language()
activate('en')
if isinstance(segment_config, Promise):
segment_key = force_text(segment_config)
elif isinstance(segment_config, six.text_type):
segment_key = segment_config
else:
segment_key = segment_config
activate(lang)
try:
overrides = self.segments[plugin_class_name][self.CFGS][segment_key][self.OVERRIDES]
if user.username in overrides:
# TODO: I don't like this int-casting used here or anywhere.
return int(overrides[user.username])
except KeyError:
if not isinstance(segment_config, Promise):
import inspect
warnings.warn('get_override_for_segment() received '
'segment_config: “{0}” as type {1} from: {2!r}. '
'This has resulted in a failure to retrieve a '
'segment override.'.format(
segment_config,
type(segment_config),
inspect.stack()[1][3]
)
)
return SegmentOverride.NoOverride
def get_override_for_segment(self, user, plugin_class_instance, plugin_instance):
'''
Given a specific user, plugin class and instance, return the current
override. This is a wrapper around get_override_for_classname() and
provides the appropriate duck-checking and is therefore more useful as
an external entry-point into the segment_pool.
'''
if not self.segments:
self.discover()
if (hasattr(plugin_class_instance, 'allow_overrides') and
plugin_class_instance.allow_overrides and
hasattr(plugin_instance, 'configuration_string')):
segment_class = plugin_class_instance.__class__.__name__
segment_config = plugin_instance.configuration_string
return self.get_override_for_classname(user, segment_class, segment_config)
return SegmentOverride.NoOverride
def _get_sorted_copy(self):
'''
Returns the SegmentPool as a list of tuples sorted appropriately for
human consumption in *the current language*. This means that the
_(NAME) value should determine the sort order of the outer dict and
the _('segment_config') key should determine the order of the inner
dicts. In both cases, the keys need to be compared in the provided
language.
Further note that the current language is given by get_language() and
that this will reflect the CMS operator's user settings, NOT the current
PAGE language.
NOTE: that the structure of the sorted pool is different. Two of the
nested dicts are now lists of tuples so that the sort can be retained.
_sorted_segments = [
(/class/, {
NAME: _(/name/),
CFGS: [
(/configuration_string/, {
LABEL: _(/configuration_string/),
OVERRIDES: {
/user.id/: /SegmentOverride enum value/,
...
},
INSTANCES: [ ... ]
})
]
})
]
NOTE: On Python 3.0+ systems, we depend on pyuca for collation, which
produces excellent results. On earlier systems, this is not available,
so, we use a cruder mapping of accented characters into their
unaccented ASCII equivalents.
'''
sort_key = None
if sys.version_info >= (3, 0):
uca = None
#
# Unfortunately, the pyuca class–which can provide collation of
# strings in a thread-safe manner–is for Python 3.0+ only
#
try:
from pyuca import Collator
uca = Collator()
sort_key = uca.sort_key
except:
pass
if not sort_key:
#
# Our fallback position is to use a more simple approach of
# mapping 'accented' chars to latin equivalents before sorting,
# this is crude, but better than nothing.
#
from .unaccent import unaccented_map
def sort_key(s):
return s.translate(unaccented_map())
pool = self.segments
clone = []
for cls_key in sorted(pool.keys()):
cls_dict = {
self.NAME: pool[cls_key][self.NAME],
self.CFGS: list(),
}
clone.append(( cls_key, cls_dict ))
# We'll build the CFG as a list in arbitrary order for now...
for cfg_key in pool[cls_key][self.CFGS]:
cfg_dict = {
self.LABEL: pool[cls_key][self.CFGS][cfg_key][self.LABEL],
self.OVERRIDES: dict(),
self.INSTANCES: list(),
}
for username, override in pool[cls_key][self.CFGS][cfg_key][self.OVERRIDES].items():
cfg_dict[self.OVERRIDES][username] = override
for instance in pool[cls_key][self.CFGS][cfg_key][self.INSTANCES]:
cfg_dict[self.INSTANCES].append(instance)
cls_dict[self.CFGS].append( (cfg_key, cfg_dict) )
#
# Now, sort the CFGS by their LABEL, using which every means we
# have available to us at this moment.
#
cls_dict[self.CFGS] = sorted(cls_dict[self.CFGS], key=lambda x: sort_key(force_text(x[1][self.LABEL])))
return clone
def get_registered_segments(self):
'''
This is the interfact for obtaining a copy of the pool. It is returned
sorted for the current language.
'''
if not self.segments:
self.discover()
lang = get_language()
if not lang in self._sorted_segments:
self._sorted_segments[lang] = self._get_sorted_copy()
return self._sorted_segments[lang]
def get_segments_toolbar_menu(self, user, toolbar, csrf_token):
'''
Returns a CMSToolbar "Segments" menu from the pool.
'''
#
# NOTE: This is usually when the discovery process starts.
#
if not self.segments:
self.discover()
pool = self.get_registered_segments()
num_overrides = self.get_num_overrides_for_user(user)
if num_overrides:
segment_menu_name = _('Segments ({num:d})'.format(num=num_overrides))
else:
segment_menu_name = _('Segments')
segment_menu = toolbar.get_or_create_menu(
'segmentation-menu',
segment_menu_name
)
for segment_class_name, segment_class in pool:
segment_name = segment_class[self.NAME]
segment_class_menu = segment_menu.get_or_create_menu(
segment_class_name,
segment_name
)
for config_str, config in segment_class[self.CFGS]:
user_override = segment_pool.get_override_for_classname(
user,
segment_class_name,
config_str
)
config_menu = SubMenu(config[self.LABEL], csrf_token)
segment_class_menu.add_item(config_menu)
for override, override_label in SegmentOverride.overrides_list:
if override == SegmentOverride.NoOverride:
# We don't really want to show the 'No override' as an
# actionable item.
continue
active = bool(override == user_override)
if active:
# Mark parent menus active too
config_menu.active = True
segment_class_menu.active = True
if (override != user_override):
override_value = override
else:
override_value = SegmentOverride.NoOverride
config_menu.add_ajax_item(
override_label,
action=reverse('admin:set_segment_override'),
data={
'segment_class': segment_class_name,
'segment_config': config_str,
'override': override_value,
},
active=active,
on_success=toolbar.REFRESH_PAGE
)
segment_menu.add_item(Break())
reset_ajax_item = AjaxItem(
_('Reset all segments'),
# TODO: This should not use a named pattern
action=reverse('admin:reset_all_segment_overrides'),
csrf_token=csrf_token,
data={},
disabled=bool(num_overrides == 0),
on_success=toolbar.REFRESH_PAGE
)
segment_menu.add_item(reset_ajax_item)
def __str__(self):
'''
Returns the whole segment_pool structure. Useful for debugging. Not
much else.
'''
return self.segments
segment_pool = SegmentPool()