1# -*- coding: utf-8 -*-
2import operator
3import warnings
4from collections import OrderedDict
5
6from django.conf import settings
7from django.core.exceptions import ImproperlyConfigured
8from django.db.models.query_utils import Q
9from django.template import TemplateSyntaxError, NodeList, Variable, Context, Template, engines
10from django.template.base import VariableNode
11from django.template.loader import get_template
12from django.template.loader_tags import BlockNode, ExtendsNode, IncludeNode
13from django.utils import six
14
15from sekizai.helpers import get_varname
16
17from cms.exceptions import DuplicatePlaceholderWarning
18from cms.utils.conf import get_cms_setting
19
20
21def _get_nodelist(tpl):
22    if hasattr(tpl, 'template'):
23        return tpl.template.nodelist
24    else:
25        return tpl.nodelist
26
27
28def get_context():
29    if engines is not None:
30        context = Context()
31        context.template = Template('')
32        return context
33    else:
34        return {}
35
36
37def get_placeholder_conf(setting, placeholder, template=None, default=None):
38    """
39    Returns the placeholder configuration for a given setting. The key would for
40    example be 'plugins' or 'name'.
41
42    Resulting value will be the last from:
43
44    CMS_PLACEHOLDER_CONF[None] (global)
45    CMS_PLACEHOLDER_CONF['template'] (if template is given)
46    CMS_PLACEHOLDER_CONF['placeholder']
47    CMS_PLACEHOLDER_CONF['template placeholder'] (if template is given)
48    """
49
50    if placeholder:
51        keys = []
52        placeholder_conf = get_cms_setting('PLACEHOLDER_CONF')
53        # 1st level
54        if template:
55            keys.append(u'%s %s' % (template, placeholder))
56        # 2nd level
57        keys.append(placeholder)
58        # 3rd level
59        if template:
60            keys.append(template)
61        # 4th level
62        keys.append(None)
63        for key in keys:
64            try:
65                conf = placeholder_conf[key]
66                value = conf.get(setting, None)
67                if value is not None:
68                    return value
69                inherit = conf.get('inherit')
70                if inherit:
71                    if ' ' in inherit:
72                        inherit = inherit.split(' ')
73                    else:
74                        inherit = (None, inherit)
75                    value = get_placeholder_conf(setting, inherit[1], inherit[0], default)
76                    if value is not None:
77                        return value
78            except KeyError:
79                continue
80    return default
81
82
83def get_toolbar_plugin_struct(plugins, slot=None, page=None):
84    """
85    Return the list of plugins to render in the toolbar.
86    The dictionary contains the label, the classname and the module for the
87    plugin.
88    Names and modules can be defined on a per-placeholder basis using
89    'plugin_modules' and 'plugin_labels' attributes in CMS_PLACEHOLDER_CONF
90
91    :param plugins: list of plugins
92    :param slot: placeholder slot name
93    :param page: the page
94    :return: list of dictionaries
95    """
96    template = None
97
98    if page:
99        template = page.template
100
101    modules = get_placeholder_conf("plugin_modules", slot, template, default={})
102    names = get_placeholder_conf("plugin_labels", slot, template, default={})
103
104    main_list = []
105
106    # plugin.value points to the class name of the plugin
107    # It's added on registration. TIL.
108    for plugin in plugins:
109        main_list.append({'value': plugin.value,
110                          'name': names.get(plugin.value, plugin.name),
111                          'module': modules.get(plugin.value, plugin.module)})
112    return sorted(main_list, key=operator.itemgetter("module"))
113
114
115def validate_placeholder_name(name):
116    if not isinstance(name, six.string_types):
117        raise ImproperlyConfigured("Placeholder identifier names need to be of type string. ")
118
119    if not all(ord(char) < 128 for char in name):
120        raise ImproperlyConfigured("Placeholder identifiers names may not "
121                                   "contain non-ascii characters. If you wish your placeholder "
122                                   "identifiers to contain non-ascii characters when displayed to "
123                                   "users, please use the CMS_PLACEHOLDER_CONF setting with the 'name' "
124                                   "key to specify a verbose name.")
125
126
127class PlaceholderNoAction(object):
128    can_copy = False
129
130    def copy(self, **kwargs):
131        return False
132
133    def get_copy_languages(self, **kwargs):
134        return []
135
136
137class MLNGPlaceholderActions(PlaceholderNoAction):
138    can_copy = True
139
140    def copy(self, target_placeholder, source_language, fieldname, model, target_language, **kwargs):
141        from cms.utils.copy_plugins import copy_plugins_to
142        trgt = model.objects.get(**{fieldname: target_placeholder})
143        src = model.objects.get(master=trgt.master, language_code=source_language)
144
145        source_placeholder = getattr(src, fieldname, None)
146        if not source_placeholder:
147            return False
148        return copy_plugins_to(source_placeholder.get_plugins_list(),
149                               target_placeholder, target_language)
150
151    def get_copy_languages(self, placeholder, model, fieldname, **kwargs):
152        manager = model.objects
153        src = manager.get(**{fieldname: placeholder})
154        query = Q(master=src.master)
155        query &= Q(**{'%s__cmsplugin__isnull' % fieldname: False})
156        query &= ~Q(pk=src.pk)
157
158        language_codes = manager.filter(query).values_list('language_code', flat=True).distinct()
159        return [(lc, dict(settings.LANGUAGES)[lc]) for lc in language_codes]
160
161
162def restore_sekizai_context(context, changes):
163    varname = get_varname()
164    sekizai_container = context.get(varname)
165    for key, values in changes.items():
166        sekizai_namespace = sekizai_container[key]
167        for value in values:
168            sekizai_namespace.append(value)
169
170
171def _scan_placeholders(nodelist, node_class=None, current_block=None, ignore_blocks=None):
172    from cms.templatetags.cms_tags import Placeholder
173
174    if not node_class:
175        node_class = Placeholder
176
177    nodes = []
178
179    if ignore_blocks is None:
180        # List of BlockNode instances to ignore.
181        # This is important to avoid processing overriden block nodes.
182        ignore_blocks = []
183
184    for node in nodelist:
185        # check if this is a placeholder first
186        if isinstance(node, node_class):
187            nodes.append(node)
188        elif isinstance(node, IncludeNode):
189            # if there's an error in the to-be-included template, node.template becomes None
190            if node.template:
191                # Check if it quacks like a template object, if not
192                # presume is a template path and get the object out of it
193                if not callable(getattr(node.template, 'render', None)):
194                    # If it's a variable there is no way to expand it at this stage so we
195                    # need to skip it
196                    if isinstance(node.template.var, Variable):
197                        continue
198                    else:
199                        template = get_template(node.template.var)
200                else:
201                    template = node.template
202                nodes += _scan_placeholders(_get_nodelist(template), node_class, current_block)
203        # handle {% extends ... %} tags
204        elif isinstance(node, ExtendsNode):
205            nodes += _get_placeholder_nodes_from_extend(node, node_class)
206        # in block nodes we have to scan for super blocks
207        elif isinstance(node, VariableNode) and current_block:
208            if node.filter_expression.token == 'block.super':
209                if not hasattr(current_block.super, 'nodelist'):
210                    raise TemplateSyntaxError("Cannot render block.super for blocks without a parent.")
211                nodes += _scan_placeholders(_get_nodelist(current_block.super), node_class, current_block.super)
212        # ignore nested blocks which are already handled
213        elif isinstance(node, BlockNode) and node.name in ignore_blocks:
214            continue
215        # if the node has the newly introduced 'child_nodelists' attribute, scan
216        # those attributes for nodelists and recurse them
217        elif hasattr(node, 'child_nodelists'):
218            for nodelist_name in node.child_nodelists:
219                if hasattr(node, nodelist_name):
220                    subnodelist = getattr(node, nodelist_name)
221                    if isinstance(subnodelist, NodeList):
222                        if isinstance(node, BlockNode):
223                            current_block = node
224                        nodes += _scan_placeholders(subnodelist, node_class, current_block, ignore_blocks)
225        # else just scan the node for nodelist instance attributes
226        else:
227            for attr in dir(node):
228                obj = getattr(node, attr)
229                if isinstance(obj, NodeList):
230                    if isinstance(node, BlockNode):
231                        current_block = node
232                    nodes += _scan_placeholders(obj, node_class, current_block, ignore_blocks)
233    return nodes
234
235
236def _scan_static_placeholders(nodelist):
237    from cms.templatetags.cms_tags import StaticPlaceholderNode
238
239    return _scan_placeholders(nodelist, node_class=StaticPlaceholderNode)
240
241
242def get_placeholders(template):
243    compiled_template = get_template(template)
244
245    placeholders = []
246    nodes = _scan_placeholders(_get_nodelist(compiled_template))
247    clean_placeholders = []
248
249    for node in nodes:
250        placeholder = node.get_declaration()
251        slot = placeholder.slot
252
253        if slot in clean_placeholders:
254            warnings.warn("Duplicate {{% placeholder \"{0}\" %}} "
255                          "in template {1}."
256                          .format(slot, template, slot),
257                          DuplicatePlaceholderWarning)
258        else:
259            validate_placeholder_name(slot)
260            placeholders.append(placeholder)
261            clean_placeholders.append(slot)
262    return placeholders
263
264
265def get_static_placeholders(template, context):
266    compiled_template = get_template(template)
267    nodes = _scan_static_placeholders(_get_nodelist(compiled_template))
268    placeholders = [node.get_declaration(context) for node in nodes]
269    placeholders_with_code = []
270
271    for placeholder in placeholders:
272        if placeholder.slot:
273            placeholders_with_code.append(placeholder)
274        else:
275            warnings.warn('Unable to resolve static placeholder '
276                          'name in template "{}"'.format(template),
277                          Warning)
278    return placeholders_with_code
279
280
281def _get_block_nodes(extend_node):
282    parent = extend_node.get_parent(get_context())
283    parent_nodelist = _get_nodelist(parent)
284    parent_nodes = parent_nodelist.get_nodes_by_type(BlockNode)
285    parent_extend_nodes = parent_nodelist.get_nodes_by_type(ExtendsNode)
286
287    if parent_extend_nodes:
288        # Start at the top
289        # Scan the extends node from the parent (if any)
290        nodes = _get_block_nodes(parent_extend_nodes[0])
291    else:
292        nodes = OrderedDict()
293
294    # Continue with the parent template nodes
295    for node in parent_nodes:
296        nodes[node.name] = node
297
298    # Move on to the current template nodes
299    current_nodes = _get_nodelist(extend_node).get_nodes_by_type(BlockNode)
300
301    for node in current_nodes:
302        if node.name in nodes:
303            # set this node as the super node (for {{ block.super }})
304            node.super = nodes[node.name]
305        nodes[node.name] = node
306    return nodes
307
308
309def _get_placeholder_nodes_from_extend(extend_node, node_class):
310    """
311    Returns a list of placeholders found in the parent template(s) of this
312    ExtendsNode
313    """
314    # This is a dictionary mapping all BlockNode instances found
315    # in the template that contains the {% extends %} tag
316    block_nodes = _get_block_nodes(extend_node)
317    block_names = list(block_nodes.keys())
318
319    placeholders = []
320
321    for block in block_nodes.values():
322        placeholders.extend(_scan_placeholders(_get_nodelist(block), node_class, block, block_names))
323
324    # Scan topmost template for placeholder outside of blocks
325    parent_template = _find_topmost_template(extend_node)
326    placeholders += _scan_placeholders(_get_nodelist(parent_template), node_class, None, block_names)
327    return placeholders
328
329
330def _find_topmost_template(extend_node):
331    parent_template = extend_node.get_parent(get_context())
332    for node in _get_nodelist(parent_template).get_nodes_by_type(ExtendsNode):
333        # Their can only be one extend block in a template, otherwise django raises an exception
334        return _find_topmost_template(node)
335        # No ExtendsNode
336    return extend_node.get_parent(get_context())
337