1import json
2from abc import ABCMeta
3from collections import defaultdict
4
5from django.template.loader import render_to_string
6from django.utils import six
7from django.utils.encoding import force_text
8from django.utils.functional import Promise
9
10from cms.constants import RIGHT, LEFT, REFRESH_PAGE, URL_CHANGE
11
12
13class ItemSearchResult(object):
14    def __init__(self, item, index):
15        self.item = item
16        self.index = index
17
18    def __add__(self, other):
19        return ItemSearchResult(self.item, self.index + other)
20
21    def __sub__(self, other):
22        return ItemSearchResult(self.item, self.index - other)
23
24    def __int__(self):
25        return self.index
26
27
28def may_be_lazy(thing):
29    if isinstance(thing, Promise):
30        return thing._proxy____args[0]
31    else:
32        return thing
33
34
35class ToolbarAPIMixin(six.with_metaclass(ABCMeta)):
36    REFRESH_PAGE = REFRESH_PAGE
37    URL_CHANGE = URL_CHANGE
38    LEFT = LEFT
39    RIGHT = RIGHT
40
41    def __init__(self):
42        self.items = []
43        self.menus = {}
44        self._memo = defaultdict(list)
45
46    def _memoize(self, item):
47        self._memo[item.__class__].append(item)
48
49    def _unmemoize(self, item):
50        self._memo[item.__class__].remove(item)
51
52    def _item_position(self, item):
53        return self.items.index(item)
54
55    def _add_item(self, item, position):
56        if position is not None:
57            self.items.insert(position, item)
58        else:
59            self.items.append(item)
60
61    def _remove_item(self, item):
62        if item in self.items:
63            self.items.remove(item)
64        else:
65            raise KeyError("Item %r not found" % item)
66
67    def get_item_count(self):
68        return len(self.items)
69
70    def add_item(self, item, position=None):
71        if not isinstance(item, BaseItem):
72            raise ValueError("Items must be subclasses of cms.toolbar.items.BaseItem, %r isn't" % item)
73        if isinstance(position, ItemSearchResult):
74            position = position.index
75        elif isinstance(position, BaseItem):
76            position = self._item_position(position)
77        elif not (position is None or isinstance(position, (int,))):
78            raise ValueError("Position must be None, an integer, an item or an ItemSearchResult, got %r instead" % position)
79        self._add_item(item, position)
80        self._memoize(item)
81        return item
82
83    def find_items(self, item_type, **attributes):
84        results = []
85        attr_items = attributes.items()
86        notfound = object()
87        for candidate in self._memo[item_type]:
88            if all(may_be_lazy(getattr(candidate, key, notfound)) == value for key, value in attr_items):
89                results.append(ItemSearchResult(candidate, self._item_position(candidate)))
90        return results
91
92    def find_first(self, item_type, **attributes):
93        try:
94            return self.find_items(item_type, **attributes)[0]
95        except IndexError:
96            return None
97
98    #
99    # This will only work if it is used to determine the insert position for
100    # all items in the same menu.
101    #
102    def get_alphabetical_insert_position(self, new_menu_name, item_type,
103                                         default=0):
104        results = self.find_items(item_type)
105
106        # No items yet? Use the default value provided
107        if not len(results):
108            return default
109
110        last_position = 0
111
112        for result in sorted(results, key=lambda x: x.item.name):
113            if result.item.name > new_menu_name:
114                return result.index
115
116            if result.index > last_position:
117                last_position = result.index
118        else:
119            return last_position + 1
120
121    def remove_item(self, item):
122        self._remove_item(item)
123        self._unmemoize(item)
124
125    def add_sideframe_item(self, name, url, active=False, disabled=False,
126                           extra_classes=None, on_close=None, side=LEFT, position=None):
127        item = SideframeItem(name, url,
128                             active=active,
129                             disabled=disabled,
130                             extra_classes=extra_classes,
131                             on_close=on_close,
132                             side=side,
133        )
134        self.add_item(item, position=position)
135        return item
136
137    def add_modal_item(self, name, url, active=False, disabled=False,
138                       extra_classes=None, on_close=REFRESH_PAGE, side=LEFT, position=None):
139        item = ModalItem(name, url,
140                         active=active,
141                         disabled=disabled,
142                         extra_classes=extra_classes,
143                         on_close=on_close,
144                         side=side,
145        )
146        self.add_item(item, position=position)
147        return item
148
149    def add_link_item(self, name, url, active=False, disabled=False,
150                      extra_classes=None, side=LEFT, position=None):
151        item = LinkItem(name, url,
152                        active=active,
153                        disabled=disabled,
154                        extra_classes=extra_classes,
155                        side=side
156        )
157        self.add_item(item, position=position)
158        return item
159
160    def add_ajax_item(self, name, action, active=False, disabled=False,
161                      extra_classes=None, data=None, question=None,
162                      side=LEFT, position=None, on_success=None, method='POST'):
163        item = AjaxItem(name, action, self.csrf_token,
164                        active=active,
165                        disabled=disabled,
166                        extra_classes=extra_classes,
167                        data=data,
168                        question=question,
169                        side=side,
170                        on_success=on_success,
171                        method=method,
172        )
173        self.add_item(item, position=position)
174        return item
175
176
177class BaseItem(six.with_metaclass(ABCMeta)):
178    toolbar = None
179    template = None
180
181    def __init__(self, side=LEFT):
182        self.side = side
183
184    @property
185    def right(self):
186        return self.side is RIGHT
187
188    def render(self):
189        if self.toolbar:
190            template = self.toolbar.templates.get_cached_template(self.template)
191            return template.render(self.get_context())
192        # Backwards compatibility
193        return render_to_string(self.template, self.get_context())
194
195    def get_context(self):
196        return {}
197
198
199class TemplateItem(BaseItem):
200
201    def __init__(self, template, extra_context=None, side=LEFT):
202        super(TemplateItem, self).__init__(side)
203        self.template = template
204        self.extra_context = extra_context
205
206    def get_context(self):
207        if self.extra_context:
208            return self.extra_context
209        return {}
210
211
212class SubMenu(ToolbarAPIMixin, BaseItem):
213    template = "cms/toolbar/items/menu.html"
214    sub_level = True
215    active = False
216
217    def __init__(self, name, csrf_token, disabled=False, side=LEFT):
218        ToolbarAPIMixin.__init__(self)
219        BaseItem.__init__(self, side)
220        self.name = name
221        self.disabled = disabled
222        self.csrf_token = csrf_token
223
224    def __repr__(self):
225        return '<Menu:%s>' % force_text(self.name)
226
227    def add_break(self, identifier=None, position=None):
228        item = Break(identifier)
229        self.add_item(item, position=position)
230        return item
231
232    def get_items(self):
233        items = self.items
234        for item in items:
235            item.toolbar = self.toolbar
236            if hasattr(item, 'disabled'):
237                item.disabled = self.disabled or item.disabled
238        return items
239
240    def get_context(self):
241        return {
242            'active': self.active,
243            'disabled': self.disabled,
244            'items': self.get_items(),
245            'title': self.name,
246            'sub_level': self.sub_level
247        }
248
249
250class Menu(SubMenu):
251    sub_level = False
252
253    def get_or_create_menu(self, key, verbose_name, disabled=False, side=LEFT, position=None):
254        if key in self.menus:
255            return self.menus[key]
256        menu = SubMenu(verbose_name, self.csrf_token, disabled=disabled, side=side)
257        self.menus[key] = menu
258        self.add_item(menu, position=position)
259        return menu
260
261
262class LinkItem(BaseItem):
263    template = "cms/toolbar/items/item_link.html"
264
265    def __init__(self, name, url, active=False, disabled=False, extra_classes=None, side=LEFT):
266        super(LinkItem, self).__init__(side)
267        self.name = name
268        self.url = url
269        self.active = active
270        self.disabled = disabled
271        self.extra_classes = extra_classes or []
272
273    def __repr__(self):
274        return '<LinkItem:%s>' % force_text(self.name)
275
276    def get_context(self):
277        return {
278            'url': self.url,
279            'name': self.name,
280            'active': self.active,
281            'disabled': self.disabled,
282            'extra_classes': self.extra_classes,
283        }
284
285
286class FrameItem(BaseItem):
287    # Be sure to define the correct template
288
289    def __init__(self, name, url, active=False, disabled=False,
290                 extra_classes=None, on_close=None, side=LEFT):
291        super(FrameItem, self).__init__(side)
292        self.name = "%s..." % force_text(name)
293        self.url = url
294        self.active = active
295        self.disabled = disabled
296        self.extra_classes = extra_classes or []
297        self.on_close = on_close
298
299    def __repr__(self):
300        # Should be overridden
301        return '<FrameItem:%s>' % force_text(self.name)
302
303    def get_context(self):
304        return {
305            'url': self.url,
306            'name': self.name,
307            'active': self.active,
308            'disabled': self.disabled,
309            'extra_classes': self.extra_classes,
310            'on_close': self.on_close,
311        }
312
313
314class SideframeItem(FrameItem):
315    template = "cms/toolbar/items/item_sideframe.html"
316
317    def __repr__(self):
318        return '<SideframeItem:%s>' % force_text(self.name)
319
320
321class ModalItem(FrameItem):
322    template = "cms/toolbar/items/item_modal.html"
323
324    def __repr__(self):
325        return '<ModalItem:%s>' % force_text(self.name)
326
327
328class AjaxItem(BaseItem):
329    template = "cms/toolbar/items/item_ajax.html"
330
331    def __init__(self, name, action, csrf_token, data=None, active=False,
332                 disabled=False, extra_classes=None,
333                 question=None, side=LEFT, on_success=None, method='POST'):
334        super(AjaxItem, self).__init__(side)
335        self.name = name
336        self.action = action
337        self.active = active
338        self.disabled = disabled
339        self.csrf_token = csrf_token
340        self.data = data or {}
341        self.extra_classes = extra_classes or []
342        self.question = question
343        self.on_success = on_success
344        self.method = method
345
346    def __repr__(self):
347        return '<AjaxItem:%s>' % force_text(self.name)
348
349    def get_context(self):
350        data = self.data.copy()
351
352        if self.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
353            data['csrfmiddlewaretoken'] = self.csrf_token
354
355        return {
356            'action': self.action,
357            'name': self.name,
358            'active': self.active,
359            'disabled': self.disabled,
360            'extra_classes': self.extra_classes,
361            'data': json.dumps(data),
362            'question': self.question,
363            'on_success': self.on_success,
364            'method': self.method,
365        }
366
367
368class Break(BaseItem):
369    template = "cms/toolbar/items/break.html"
370
371    def __init__(self, identifier=None):
372        self.identifier = identifier
373
374
375class BaseButton(six.with_metaclass(ABCMeta)):
376    toolbar = None
377    template = None
378
379    def render(self):
380        if self.toolbar:
381            template = self.toolbar.templates.get_cached_template(self.template)
382            return template.render(self.get_context())
383        # Backwards compatibility
384        return render_to_string(self.template, self.get_context())
385
386    def get_context(self):
387        return {}
388
389
390class Button(BaseButton):
391    template = "cms/toolbar/items/button.html"
392
393    def __init__(self, name, url, active=False, disabled=False,
394                 extra_classes=None):
395        self.name = name
396        self.url = url
397        self.active = active
398        self.disabled = disabled
399        self.extra_classes = extra_classes or []
400
401    def __repr__(self):
402        return '<Button:%s>' % force_text(self.name)
403
404    def get_context(self):
405        return {
406            'name': self.name,
407            'url': self.url,
408            'active': self.active,
409            'disabled': self.disabled,
410            'extra_classes': self.extra_classes,
411        }
412
413
414class ModalButton(Button):
415    template = "cms/toolbar/items/button_modal.html"
416
417    def __init__(self, name, url, active=False, disabled=False,  extra_classes=None, on_close=None):
418        self.name = name
419        self.url = url
420        self.active = active
421        self.disabled = disabled
422        self.extra_classes = extra_classes or []
423        self.on_close = on_close
424
425    def __repr__(self):
426        return '<ModalButton:%s>' % force_text(self.name)
427
428    def get_context(self):
429        return {
430            'name': self.name,
431            'url': self.url,
432            'active': self.active,
433            'disabled': self.disabled,
434            'extra_classes': self.extra_classes,
435            'on_close': self.on_close,
436        }
437
438
439class SideframeButton(ModalButton):
440    template = "cms/toolbar/items/button_sideframe.html"
441
442    def __repr__(self):
443        return '<SideframeButton:%s>' % force_text(self.name)
444
445
446class ButtonList(BaseItem):
447    template = "cms/toolbar/items/button_list.html"
448
449    def __init__(self, identifier=None, extra_classes=None, side=LEFT):
450        super(ButtonList, self).__init__(side)
451        self.extra_classes = extra_classes or []
452        self.buttons = []
453        self.identifier = identifier
454
455    def __repr__(self):
456        return '<ButtonList:%s>' % self.identifier
457
458    def add_item(self, item):
459        if not isinstance(item, Button):
460            raise ValueError("Expected instance of cms.toolbar.items.Button, got %r instead" % item)
461        self.buttons.append(item)
462
463    def add_button(self, name, url, active=False, disabled=False,
464                   extra_classes=None):
465        item = Button(name, url,
466                      active=active,
467                      disabled=disabled,
468                      extra_classes=extra_classes
469        )
470        self.buttons.append(item)
471        return item
472
473    def add_modal_button(self, name, url, active=False, disabled=False, extra_classes=None, on_close=REFRESH_PAGE):
474        item = ModalButton(name, url,
475                      active=active,
476                      disabled=disabled,
477                      extra_classes=extra_classes,
478                      on_close=on_close,
479        )
480        self.buttons.append(item)
481        return item
482
483    def add_sideframe_button(self, name, url, active=False, disabled=False, extra_classes=None, on_close=None):
484        item = SideframeButton(name, url,
485                      active=active,
486                      disabled=disabled,
487                      extra_classes=extra_classes,
488                      on_close=on_close,
489        )
490        self.buttons.append(item)
491        return item
492
493    def get_buttons(self):
494        for button in self.buttons:
495            button.toolbar = self.toolbar
496            yield button
497
498    def get_context(self):
499        context = {
500            'buttons': list(self.get_buttons()),
501            'extra_classes': self.extra_classes
502        }
503
504        if self.toolbar:
505            context['cms_structure_on'] = self.toolbar.structure_mode_url_on
506        return context
507
508
509class Dropdown(ButtonList):
510
511    template = "cms/toolbar/items/dropdown.html"
512
513    def __init__(self, *args, **kwargs):
514        super(Dropdown, self).__init__(*args, **kwargs)
515        self.primary_button = None
516
517    def __repr__(self):
518        return '<Dropdown:%s>' % force_text(self.name)
519
520    def add_primary_button(self, button):
521        self.primary_button = button
522
523    def get_buttons(self):
524        for button in self.buttons:
525            button.toolbar = self.toolbar
526            button.is_in_dropdown = True
527            yield button
528
529    def get_context(self):
530        return {
531            'primary_button': self.primary_button,
532            'buttons': list(self.get_buttons()),
533            'extra_classes': self.extra_classes,
534        }
535
536
537class DropdownToggleButton(BaseButton):
538    template = "cms/toolbar/items/dropdown_button.html"
539    has_no_action = True
540
541    def __init__(self, name, active=False, disabled=False,
542                 extra_classes=None):
543        self.name = name
544        self.active = active
545        self.disabled = disabled
546        self.extra_classes = extra_classes or []
547
548    def __repr__(self):
549        return '<DropdownToggleButton:%s>' % force_text(self.name)
550
551    def get_context(self):
552        return {
553            'name': self.name,
554            'active': self.active,
555            'disabled': self.disabled,
556            'extra_classes': self.extra_classes,
557        }
558