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