├── .gitignore ├── CHANGELOG.md ├── LICENSE-MIT ├── MANIFEST.in ├── README.md ├── assets ├── language-chooser.png ├── navbar.png └── paginator.png ├── cms_bootstrap ├── __init__.py ├── models.py ├── static │ └── cms_bootstrap │ │ └── js │ │ └── ng-nav-navbar.js ├── templates │ ├── bootstrap3 │ │ ├── components │ │ │ └── paginator.html │ │ ├── includes │ │ │ ├── breadcrumb-row.html │ │ │ ├── language-chooser.html │ │ │ ├── nav-navbar.html │ │ │ ├── nav-secondary.html │ │ │ └── ng-nav-navbar.html │ │ └── menu │ │ │ ├── breadcrumb.html │ │ │ ├── navbar.html │ │ │ └── ng-navbar.html │ └── bootstrap4 │ │ ├── components │ │ ├── menu-icon.html │ │ └── paginator.html │ │ ├── includes │ │ ├── language-chooser.html │ │ ├── nav-breadcrumb.html │ │ ├── nav-navbar.html │ │ ├── nav-secondary.html │ │ ├── ng-language-chooser.html │ │ └── ng-nav-navbar.html │ │ └── menu │ │ ├── breadcrumb.html │ │ ├── navbar.html │ │ └── ng-navbar.html └── templatetags │ ├── __init__.py │ └── bootstrap_tags.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Operating System files 26 | *.DS_Store 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | - 1.1.2 4 | * Fix external dependencies to latest available versions. 5 | 6 | - 1.1.1 7 | * Replace `mark_safe` modules path by `from django.utils.safestring` for compatibility in Django 2.0. 8 | 9 | - 1.1 10 | * Adopt to django-cms version 3.6. 11 | * Do no render menu item pointing onto current page in bold. 12 | * Add templatetag `menu_icon`, which in combination with **djangocms-cascade** version 0.19+, renders an icon 13 | in front of the menu title. 14 | * Add Bootstrap-4 template for templatetag `language_chooser`, which renders a drop-down menu to choose one of 15 | the languages configured for **django-CMS** 16 | * Add Bootstrap-4 template for templatetag `paginator`, which renders a paginator according to the best given 17 | practices. 18 | 19 | - 1.0.2 20 | * Add AngularJS directive for toggling navbar in responsive mode. 21 | 22 | - 1.0.1 23 | * Fix navbar and menu templates if used in combination with **Bootstrap-4** and jQuery. 24 | 25 | - 1.0 26 | * In addition to **Bootstrap-3** add support for **Bootstrap-4**. This requires to be more generic, therefore also 27 | * rename project from `djangocms-bootstrap3` to `djangocms-bootstrap` in your requirements. 28 | * rename Django app from `cms_bootstrap3` to `cms_bootstrap` in your `INSTALLED_APPS`. 29 | * rename in existing templates `bootstrap/…` to `bootstrap3/…`. 30 | 31 | - 0.4.2 32 | * adopted navbar to work with `angular-ui-bootstrap` version 2.5 and later. 33 | 34 | - 0.4.1 35 | * Fix versions in `install_requires` of `setup.py`. 36 | 37 | - 0.4 38 | * Add support for django-CMS version 3.5 39 | * Replace `dropdown`, `dropdown-toggle` by `uib-dropdown`, `uib-dropdown-toggle` to be compatible 40 | with **angular-ui-bootstrap** version 0.14 and later. 41 | * In hamburger menu, replace ` 13 | {% endblock %} 14 | 15 | 20 | 21 | {% endblock navbar %} 22 | -------------------------------------------------------------------------------- /cms_bootstrap/templates/bootstrap3/includes/nav-secondary.html: -------------------------------------------------------------------------------- 1 | {% load menu_tags i18n %} 2 | {% get_available_languages as languages %} 3 | -------------------------------------------------------------------------------- /cms_bootstrap/templates/bootstrap3/includes/ng-nav-navbar.html: -------------------------------------------------------------------------------- 1 | {% load bootstrap_tags i18n %} 2 | -------------------------------------------------------------------------------- /cms_bootstrap/templates/bootstrap3/menu/breadcrumb.html: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 | {% for ance in ancestors %} 3 | {% if not forloop.last or extra_ance %} 4 |
  • {{ ance.get_menu_title|safe }}
  • 5 | {% else %} 6 |
  • {{ ance.get_menu_title|safe }}
  • 7 | {% endif %} 8 | {% endfor %} 9 | {% if extra_ance %} 10 |
  • {{ extra_ance }}
  • 11 | {% endif %} 12 | {% endspaceless %} -------------------------------------------------------------------------------- /cms_bootstrap/templates/bootstrap3/menu/navbar.html: -------------------------------------------------------------------------------- 1 | {% load bootstrap_tags %} 2 | {% for child in children %} 3 | 27 | {% endfor %} 28 | -------------------------------------------------------------------------------- /cms_bootstrap/templates/bootstrap3/menu/ng-navbar.html: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 | {% for child in children %} 3 | {% if child.is_leaf_node %} 4 | 5 | {{ child.get_menu_title|safe }} 6 | 7 | {% else %} 8 | 28 | {% endif %} 29 | {% endfor %} 30 | {% endspaceless %} -------------------------------------------------------------------------------- /cms_bootstrap/templates/bootstrap4/components/menu-icon.html: -------------------------------------------------------------------------------- 1 | {% load sekizai_tags %} 2 | {% if stylesheet_url %} 3 | {% addtoblock "css" %}{% endaddtoblock %} 4 | 5 | {% endif %} -------------------------------------------------------------------------------- /cms_bootstrap/templates/bootstrap4/components/paginator.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% spaceless %}{% if show_paginator %} 3 | 25 | {% endif %}{% endspaceless %} -------------------------------------------------------------------------------- /cms_bootstrap/templates/bootstrap4/includes/language-chooser.html: -------------------------------------------------------------------------------- 1 | {% load i18n menu_tags %}{% spaceless %} 2 | {% if languages|length > 1 %} 3 | 13 | {% endif %}{% endspaceless %} 14 | -------------------------------------------------------------------------------- /cms_bootstrap/templates/bootstrap4/includes/nav-breadcrumb.html: -------------------------------------------------------------------------------- 1 | {% load menu_tags %}{% spaceless %} 2 | 7 | {% endspaceless %} -------------------------------------------------------------------------------- /cms_bootstrap/templates/bootstrap4/includes/nav-navbar.html: -------------------------------------------------------------------------------- 1 | {% load bootstrap_tags %} 2 | 3 | {% block extra-styles %}{% endblock %} 4 | {% block extra-scripts %}{% endblock %} 5 | 6 | 23 | -------------------------------------------------------------------------------- /cms_bootstrap/templates/bootstrap4/includes/nav-secondary.html: -------------------------------------------------------------------------------- 1 | {% load menu_tags i18n %} 2 | {% get_available_languages as languages %} 3 | 11 | -------------------------------------------------------------------------------- /cms_bootstrap/templates/bootstrap4/includes/ng-language-chooser.html: -------------------------------------------------------------------------------- 1 | {% load i18n menu_tags %}{% spaceless %} 2 | {% if languages|length > 1 %} 3 | 13 | {% endif %}{% endspaceless %} 14 | -------------------------------------------------------------------------------- /cms_bootstrap/templates/bootstrap4/includes/ng-nav-navbar.html: -------------------------------------------------------------------------------- 1 | {% load bootstrap_tags i18n %} 2 | 3 | -------------------------------------------------------------------------------- /cms_bootstrap/templates/bootstrap4/menu/breadcrumb.html: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 | {% with item_class=item_class|default:"breadcrumb-item" %} 3 | {% for ance in ancestors %} 4 | {% if not forloop.last or extra_ance %} 5 |
  • {{ ance.get_menu_title|safe }}
  • 6 | {% else %} 7 |
  • {{ ance.get_menu_title|safe }}
  • 8 | {% endif %} 9 | {% endfor %} 10 | {% if extra_ance %} 11 |
  • {{ extra_ance }}
  • 12 | {% endif %} 13 | 14 | {% endwith %} 15 | {% endspaceless %} -------------------------------------------------------------------------------- /cms_bootstrap/templates/bootstrap4/menu/navbar.html: -------------------------------------------------------------------------------- 1 | {% load bootstrap_tags %}{% spaceless %} 2 | {% for child in children %} 3 | 35 | {% endfor %} 36 | {% endspaceless %} 37 | -------------------------------------------------------------------------------- /cms_bootstrap/templates/bootstrap4/menu/ng-navbar.html: -------------------------------------------------------------------------------- 1 | {% load i18n bootstrap_tags %} 2 | {% spaceless %} 3 | {% with item_class=item_class|default:"nav-item" %} 4 | {% for child in children %} 5 | {% if child.is_leaf_node %} 6 |
  • 7 | 8 | {% menu_icon child.id %} 9 | {{ child.get_menu_title|safe }} 10 | 11 |
  • 12 | {% else %} 13 | 40 | {% endif %} 41 | {% endfor %} 42 | {% endwith %} 43 | {% endspaceless %} -------------------------------------------------------------------------------- /cms_bootstrap/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/djangocms-bootstrap/27a53fe50b97b56260df8f037b48e6974b351bbd/cms_bootstrap/templatetags/__init__.py -------------------------------------------------------------------------------- /cms_bootstrap/templatetags/bootstrap_tags.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.conf import settings 5 | from django import template 6 | from django.utils.safestring import mark_safe 7 | from cms.models.pagemodel import Page 8 | from menus.menu_pool import menu_pool 9 | from classytags.arguments import IntegerArgument, StringArgument, Argument, Flag 10 | from classytags.helpers import InclusionTag 11 | from classytags.core import Options 12 | from menus.templatetags.menu_tags import flatten, remove 13 | 14 | register = template.Library() 15 | 16 | if 'cmsplugin_cascade.bootstrap3' in settings.CMSPLUGIN_CASCADE_PLUGINS: 17 | BOOTSTRAP="3" 18 | else: 19 | BOOTSTRAP="4" 20 | 21 | def cut_levels(nodes, start_level): 22 | """ 23 | cutting nodes away from menus 24 | """ 25 | final = [] 26 | removed = [] 27 | for node in nodes: 28 | if not hasattr(node, 'level'): 29 | # remove and ignore nodes that don't have level information 30 | remove(node, removed) 31 | continue 32 | if node.attr.get('soft_root', False): 33 | # remove and ignore nodes that are behind a node marked as 'soft_root' 34 | remove(node, removed) 35 | continue 36 | if node.level == start_level: 37 | # turn nodes that are on from_level into root nodes 38 | final.append(node) 39 | node.parent = None 40 | if not node.visible and not node.children: 41 | remove(node, removed) 42 | elif node.level == start_level + 1: 43 | # remove nodes that are deeper than one level 44 | node.children = [] 45 | else: 46 | remove(node, removed) 47 | if not node.visible: 48 | keep_node = False 49 | for child in node.children: 50 | keep_node = keep_node or child.visible 51 | if not keep_node: 52 | remove(node, removed) 53 | for node in removed: 54 | if node in final: 55 | final.remove(node) 56 | return final 57 | 58 | 59 | class MainMenu(InclusionTag): 60 | name = 'main_menu' 61 | template = 'menu/dummy.html' 62 | 63 | options = Options( 64 | StringArgument('template', default='bootstrap{}/menu/navbar.html'.format(BOOTSTRAP), required=False), 65 | StringArgument('namespace', default=None, required=False), 66 | StringArgument('root_id', default=None, required=False), 67 | IntegerArgument('offset', default=0, required=False), 68 | IntegerArgument('limit', default=100, required=False), 69 | Flag('embody_root', default=False, true_values=['embody_root']), 70 | Argument('next_page', default=None, required=False), 71 | ) 72 | 73 | def get_context(self, context, template, namespace, root_id, offset, limit, embody_root, next_page): 74 | try: 75 | # If there's an exception (500), default context_processors may not be called. 76 | request = context['request'] 77 | except KeyError: 78 | return {'template': 'menu/empty.html'} 79 | 80 | start_level = 0 81 | 82 | if next_page: 83 | children = next_page.children 84 | else: 85 | menu_renderer = context.get('cms_menu_renderer') 86 | 87 | if not menu_renderer: 88 | menu_renderer = menu_pool.get_renderer(request) 89 | 90 | nodes = menu_renderer.get_nodes(namespace, root_id) 91 | if root_id: 92 | # find the root id and cut the nodes 93 | id_nodes = menu_pool.get_nodes_by_attribute(nodes, "reverse_id", root_id) 94 | if id_nodes: 95 | node = id_nodes[0] 96 | nodes = node.children 97 | for remove_parent in nodes: 98 | remove_parent.parent = None 99 | start_level = node.level + 1 100 | nodes = flatten(nodes) 101 | if embody_root: 102 | node.level = start_level 103 | nodes.insert(0, node) 104 | else: 105 | nodes = [] 106 | children = cut_levels(nodes, start_level) 107 | children = menu_renderer.apply_modifiers(children, namespace, root_id, post_cut=True) 108 | children = children[offset:offset + limit] 109 | context.update({'children': children, 'template': template}) 110 | return context 111 | 112 | register.tag(MainMenu) 113 | 114 | 115 | class MainMenuBelowId(MainMenu): 116 | name = 'main_menu_below_id' 117 | options = Options( 118 | Argument('root_id', default=None, required=False), 119 | StringArgument('template', default='bootstrap{}/menu/navbar.html'.format(BOOTSTRAP), required=False), 120 | IntegerArgument('offset', default=0, required=False), 121 | IntegerArgument('limit', default=100, required=False), 122 | StringArgument('namespace', default=None, required=False), 123 | Flag('embody_root', default=False, true_values=['embody_root']), 124 | Argument('next_page', default=None, required=False), 125 | ) 126 | 127 | register.tag(MainMenuBelowId) 128 | 129 | 130 | class MainMenuEmbodyId(MainMenu): 131 | name = 'main_menu_embody_id' 132 | options = Options( 133 | Argument('root_id', default=None, required=False), 134 | StringArgument('template', default='bootstrap{}/menu/navbar.html'.format(BOOTSTRAP), required=False), 135 | IntegerArgument('offset', default=0, required=False), 136 | IntegerArgument('limit', default=100, required=False), 137 | StringArgument('namespace', default=None, required=False), 138 | Flag('embody_root', default=True, false_values=['skip_root']), 139 | Argument('next_page', default=None, required=False), 140 | ) 141 | 142 | register.tag(MainMenuEmbodyId) 143 | 144 | 145 | class MenuIcon(InclusionTag): 146 | name = 'menu_icon' 147 | template = 'bootstrap{}/components/menu-icon.html'.format(BOOTSTRAP) 148 | 149 | options = Options( 150 | Argument('child_id'), 151 | StringArgument('template', default=None, required=False), 152 | ) 153 | 154 | def get_template(self, context, template=None, **kwargs): 155 | return template if template else self.template 156 | 157 | def get_context(self, context, child_id, template): 158 | try: 159 | cascadepage = Page.objects.get(id=child_id).cascadepage 160 | except (Page.DoesNotExist, AttributeError): 161 | pass 162 | else: 163 | icon_font, symbol = cascadepage.icon_font, cascadepage.menu_symbol 164 | if icon_font and symbol: 165 | prefix = icon_font.config_data.get('css_prefix_text', 'icon-') 166 | font_attr = 'class="{}{}"'.format(prefix, symbol) 167 | context.update({ 168 | 'stylesheet_url': icon_font.get_stylesheet_url(), 169 | 'icon_font_attrs': mark_safe(font_attr), 170 | }) 171 | return context 172 | 173 | def render_tag(self, context, **kwargs): 174 | context.push() 175 | output = super(MenuIcon, self).render_tag(context, **kwargs) 176 | context.pop() 177 | return output 178 | 179 | register.tag(MenuIcon) 180 | 181 | 182 | class Paginator(InclusionTag): 183 | name = 'paginator' 184 | template = 'bootstrap{}/components/paginator.html'.format(BOOTSTRAP) 185 | 186 | options = Options( 187 | IntegerArgument('page_range', default=10, required=False), 188 | StringArgument('template', default=None, required=False), 189 | ) 190 | 191 | def get_context(self, context, page_range, template): 192 | try: 193 | current_page = int(context['request'].GET['page']) 194 | except (KeyError, ValueError): 195 | current_page = 1 196 | page_range -= 1 197 | template = template or self.template 198 | context.update({'template': template}) 199 | context.setdefault('paginator_classes', 'pagination') 200 | paginator = context.get('paginator') or getattr(context.get('request'), 'paginator', None) 201 | if paginator: 202 | first_page = max(1, min(int(current_page - page_range / 2), paginator.num_pages - page_range)) 203 | last_page = min(first_page + page_range, paginator.num_pages) 204 | context.update({ 205 | 'show_paginator': paginator.num_pages > 1, 206 | 'show_aquos': paginator.num_pages > page_range + 1, 207 | 'pages': [{'num': p, 'active': p == current_page} for p in range(first_page, last_page + 1)], 208 | 'laquo': {'num': first_page - 1, 'paginate': first_page > 1}, 209 | 'raquo': {'num': last_page + 1, 'paginate': last_page < paginator.num_pages}, 210 | }) 211 | return context 212 | 213 | register.tag(Paginator) 214 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import unicode_literals 4 | 5 | from setuptools import setup, find_packages 6 | from cms_bootstrap import __version__ 7 | 8 | 9 | with open('README.md') as fh: 10 | long_description = fh.read() 11 | 12 | 13 | CLASSIFIERS = [ 14 | 'Development Status :: 4 - Beta', 15 | 'Environment :: Web Environment', 16 | 'Framework :: Django', 17 | 'Intended Audience :: Developers', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Operating System :: OS Independent', 20 | 'Programming Language :: Python', 21 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 22 | 'Programming Language :: Python :: 2.7', 23 | 'Programming Language :: Python :: 3.4', 24 | 'Programming Language :: Python :: 3.5', 25 | 'Programming Language :: Python :: 3.6', 26 | 'Programming Language :: Python :: 3.7', 27 | 'Framework :: Django :: 1.10', 28 | 'Framework :: Django :: 1.11', 29 | 'Framework :: Django :: 2.0', 30 | 'Framework :: Django :: 2.1', 31 | 'Framework :: Django :: 2.2', 32 | ] 33 | 34 | setup( 35 | name='djangocms-bootstrap', 36 | version=__version__, 37 | description='Templates and templatetags to be used with django-CMS with Bootstrap3 or Bootstrap4.', 38 | author='Jacob Rief', 39 | author_email='jacob.rief@gmail.com', 40 | url='https://github.com/jrief/djangocms-bootstrap', 41 | packages=find_packages(), 42 | install_requires=[ 43 | 'django-cms>3.4', 44 | ], 45 | license='MIT', 46 | platforms=['OS Independent'], 47 | keywords=['django-cms', 'bootstrap-3', 'bootstrap-4'], 48 | classifiers=CLASSIFIERS, 49 | long_description=long_description, 50 | long_description_content_type='text/markdown', 51 | include_package_data=True, 52 | zip_safe=False 53 | ) 54 | --------------------------------------------------------------------------------