├── menu ├── migrations │ ├── __init__.py │ ├── 0002_booleandefaults.py │ └── 0001_initial.py ├── south_migrations │ ├── __init__.py │ ├── 0002_auto__add_field_menuitem_anonymous_only.py │ └── 0001_initial.py ├── templatetags │ ├── __init__.py │ └── menubuilder.py ├── views.py ├── __init__.py ├── locale │ ├── ru │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── zh_Hans │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── apps.py ├── admin.py └── models.py ├── .gitignore ├── MANIFEST.in ├── README.rst └── setup.py /menu/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /menu/south_migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /menu/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /menu/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | dist 3 | django_helpdesk.egg-info 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include setup.py 3 | 4 | recursive-include menu *.py 5 | -------------------------------------------------------------------------------- /menu/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | default_app_config = 'menu.apps.AppConfig' 4 | -------------------------------------------------------------------------------- /menu/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossp/django-menu/HEAD/menu/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /menu/locale/zh_Hans/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossp/django-menu/HEAD/menu/locale/zh_Hans/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /menu/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | try: 4 | from django.utils.translation import ugettext_lazy as _ 5 | except ImportError: 6 | from django.utils.translation import gettext_lazy as _ 7 | 8 | from django.apps import AppConfig as BaseConfig 9 | 10 | 11 | class AppConfig(BaseConfig): 12 | name = 'menu' 13 | verbose_name = _('Menu') 14 | 15 | -------------------------------------------------------------------------------- /menu/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from menu.models import Menu, MenuItem 3 | 4 | 5 | class MenuItemInline(admin.TabularInline): 6 | model = MenuItem 7 | ordering = ('order',) 8 | 9 | 10 | class MenuAdmin(admin.ModelAdmin): 11 | list_display = ['name', 'slug', 'description'] 12 | inlines = [MenuItemInline,] 13 | 14 | 15 | admin.site.register(Menu, MenuAdmin) 16 | -------------------------------------------------------------------------------- /menu/migrations/0002_booleandefaults.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.2 on 2016-10-12 03:55 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('menu', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='menuitem', 17 | name='anonymous_only', 18 | field=models.BooleanField(default=False, help_text='Should this item only be shown to non-logged-in users?', verbose_name='Anonymous only'), 19 | ), 20 | migrations.AlterField( 21 | model_name='menuitem', 22 | name='login_required', 23 | field=models.BooleanField(default=False, help_text='Should this item only be shown to authenticated users?', verbose_name='Login required'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /menu/south_migrations/0002_auto__add_field_menuitem_anonymous_only.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'MenuItem.anonymous_only' 12 | db.add_column('menu_menuitem', 'anonymous_only', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Deleting field 'MenuItem.anonymous_only' 18 | db.delete_column('menu_menuitem', 'anonymous_only') 19 | 20 | 21 | models = { 22 | 'menu.menu': { 23 | 'Meta': {'object_name': 'Menu'}, 24 | 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 25 | 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 26 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 27 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 28 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}) 29 | }, 30 | 'menu.menuitem': { 31 | 'Meta': {'object_name': 'MenuItem'}, 32 | 'anonymous_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 33 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 34 | 'link_url': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 35 | 'login_required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 36 | 'menu': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['menu.Menu']"}), 37 | 'order': ('django.db.models.fields.IntegerField', [], {}), 38 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 39 | } 40 | } 41 | 42 | complete_apps = ['menu'] 43 | -------------------------------------------------------------------------------- /menu/locale/zh_Hans/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2016-05-22 22:05+0800\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Wang WenPei \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 20 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" 21 | 22 | #: menu/apps.py:8 menu/apps.py:9 23 | msgid "Menu" 24 | msgstr "菜单项" 25 | 26 | #: menu/models.py:7 menu/models.py:57 27 | msgid "Name" 28 | msgstr "名称" 29 | 30 | #: menu/models.py:12 31 | msgid "Slug" 32 | msgstr "菜单组标签" 33 | 34 | #: menu/models.py:16 35 | msgid "Base URL" 36 | msgstr "基础URL" 37 | 38 | #: menu/models.py:23 39 | msgid "Description" 40 | msgstr "描述" 41 | 42 | #: menu/models.py:29 43 | msgid "menu" 44 | msgstr "菜单组" 45 | 46 | #: menu/models.py:30 47 | msgid "menus" 48 | msgstr "菜单组" 49 | 50 | #: menu/models.py:61 51 | msgid "Order" 52 | msgstr "排序" 53 | 54 | #: menu/models.py:66 55 | msgid "Link URL" 56 | msgstr "链接地址" 57 | 58 | #: menu/models.py:68 59 | msgid "URL or URI to the content, eg /about/ or http://foo.com/" 60 | msgstr "支持站内链接或站外链接, 比如: /about/ 或 http://foo.com/" 61 | 62 | #: menu/models.py:72 63 | msgid "Title" 64 | msgstr "菜单标题" 65 | 66 | #: menu/models.py:77 67 | msgid "Login required" 68 | msgstr "需要登录" 69 | 70 | #: menu/models.py:79 71 | msgid "Should this item only be shown to authenticated users?" 72 | msgstr "设定当前菜单只在登录时才显示" 73 | 74 | #: menu/models.py:83 75 | msgid "Anonymous only" 76 | msgstr "只在匿名时显示" 77 | 78 | #: menu/models.py:85 79 | msgid "Should this item only be shown to non-logged-in users?" 80 | msgstr "设定当前菜单只在未登录时才显示" 81 | 82 | #: menu/models.py:89 83 | msgid "menu item" 84 | msgstr "菜单" 85 | 86 | #: menu/models.py:90 87 | msgid "menu items" 88 | msgstr "菜单" 89 | -------------------------------------------------------------------------------- /menu/locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2013-01-13 02:21+0400\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 20 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" 21 | 22 | #: models.py:7 models.py:54 23 | msgid "Name" 24 | msgstr "Название" 25 | 26 | #: models.py:12 27 | msgid "Slug" 28 | msgstr "Код" 29 | 30 | #: models.py:16 31 | msgid "Base URL" 32 | msgstr "Базовый URL" 33 | 34 | #: models.py:23 35 | msgid "Description" 36 | msgstr "Описание" 37 | 38 | #: models.py:29 39 | msgid "menu" 40 | msgstr "меню" 41 | 42 | #: models.py:30 43 | msgid "menus" 44 | msgstr "меню" 45 | 46 | #: models.py:58 47 | msgid "Order" 48 | msgstr "Сортировка" 49 | 50 | #: models.py:63 51 | msgid "Link URL" 52 | msgstr "URL" 53 | 54 | #: models.py:65 55 | msgid "URL or URI to the content, eg /about/ or http://foo.com/" 56 | msgstr "URL или URI страницы, например /about/ or http://foo.com/" 57 | 58 | #: models.py:69 59 | msgid "Title" 60 | msgstr "Заголовок" 61 | 62 | #: models.py:74 63 | msgid "Login required" 64 | msgstr "Для авторизированных" 65 | 66 | #: models.py:76 67 | msgid "Should this item only be shown to authenticated users?" 68 | msgstr "Если установить, то пункт меню будет показан только для авторизированных пользователей" 69 | 70 | #: models.py:80 71 | msgid "Anonymous only" 72 | msgstr "Для анонимных" 73 | 74 | #: models.py:82 75 | msgid "Should this item only be shown to non-logged-in users?" 76 | msgstr "Если установить, то пункт меню будет показан только для не авторизированных пользователей" 77 | 78 | #: models.py:86 79 | msgid "menu item" 80 | msgstr "пункт меню" 81 | 82 | #: models.py:87 83 | msgid "menu items" 84 | msgstr "пункты меню" 85 | -------------------------------------------------------------------------------- /menu/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-02-01 01:32 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Menu', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(max_length=100, verbose_name='Name')), 22 | ('slug', models.SlugField(verbose_name='Slug')), 23 | ('base_url', models.CharField(blank=True, max_length=100, null=True, verbose_name='Base URL')), 24 | ('description', models.TextField(blank=True, null=True, verbose_name='Description')), 25 | ], 26 | options={ 27 | 'verbose_name_plural': 'menus', 28 | 'verbose_name': 'menu', 29 | }, 30 | ), 31 | migrations.CreateModel( 32 | name='MenuItem', 33 | fields=[ 34 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 35 | ('order', models.IntegerField(default=500, verbose_name='Order')), 36 | ('link_url', models.CharField(help_text='URL or URI to the content, eg /about/ or http://foo.com/', max_length=100, verbose_name='Link URL')), 37 | ('title', models.CharField(max_length=100, verbose_name='Title')), 38 | ('login_required', models.BooleanField(help_text='Should this item only be shown to authenticated users?', verbose_name='Login required')), 39 | ('anonymous_only', models.BooleanField(help_text='Should this item only be shown to non-logged-in users?', verbose_name='Anonymous only')), 40 | ('menu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='menu.Menu', verbose_name='Name')), 41 | ], 42 | options={ 43 | 'verbose_name_plural': 'menu items', 44 | 'verbose_name': 'menu item', 45 | }, 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /menu/models.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.utils.translation import ugettext_lazy as _ 3 | except ImportError: 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from django.db import models 7 | 8 | 9 | class Menu(models.Model): 10 | name = models.CharField( 11 | _(u'Name'), 12 | max_length=100 13 | ) 14 | 15 | slug = models.SlugField( 16 | _(u'Slug') 17 | ) 18 | 19 | base_url = models.CharField( 20 | _(u'Base URL'), 21 | max_length=100, 22 | blank=True, 23 | null=True 24 | ) 25 | 26 | description = models.TextField( 27 | _(u'Description'), 28 | blank=True, 29 | null=True 30 | ) 31 | 32 | class Meta: 33 | verbose_name = _(u'menu') 34 | verbose_name_plural = _(u'menus') 35 | 36 | def __unicode__(self): 37 | return u"%s" % self.name 38 | 39 | def __str__(self): 40 | return self.__unicode__() 41 | 42 | def save(self, *args, **kwargs): 43 | """ 44 | Re-order all items from 10 upwards, at intervals of 10. 45 | This makes it easy to insert new items in the middle of 46 | existing items without having to manually shuffle 47 | them all around. 48 | """ 49 | super(Menu, self).save(*args, **kwargs) 50 | 51 | current = 10 52 | for item in MenuItem.objects.filter(menu=self).order_by('order'): 53 | item.order = current 54 | item.save() 55 | current += 10 56 | 57 | 58 | class MenuItem(models.Model): 59 | menu = models.ForeignKey( 60 | Menu, 61 | verbose_name=_(u'Name'), 62 | on_delete=models.CASCADE, 63 | ) 64 | 65 | order = models.IntegerField( 66 | _(u'Order'), 67 | default=500 68 | ) 69 | 70 | link_url = models.CharField( 71 | _(u'Link URL'), 72 | max_length=100, 73 | help_text=_(u'URL or URI to the content, eg /about/ or http://foo.com/') 74 | ) 75 | 76 | title = models.CharField( 77 | _(u'Title'), 78 | max_length=100 79 | ) 80 | 81 | login_required = models.BooleanField( 82 | _(u'Login required'), 83 | blank=True, 84 | default=False, 85 | help_text=_(u'Should this item only be shown to authenticated users?') 86 | ) 87 | 88 | anonymous_only = models.BooleanField( 89 | _(u'Anonymous only'), 90 | blank=True, 91 | default=False, 92 | help_text=_(u'Should this item only be shown to non-logged-in users?') 93 | ) 94 | 95 | class Meta: 96 | verbose_name = _(u'menu item') 97 | verbose_name_plural = _(u'menu items') 98 | 99 | def __unicode__(self): 100 | return u"%s %s. %s" % (self.menu.slug, self.order, self.title) 101 | -------------------------------------------------------------------------------- /menu/south_migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'Menu' 12 | db.create_table('menu_menu', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('name', self.gf('django.db.models.fields.CharField')(max_length=100)), 15 | ('slug', self.gf('django.db.models.fields.SlugField')(max_length=50, db_index=True)), 16 | ('base_url', self.gf('django.db.models.fields.CharField')(max_length=100, null=True, blank=True)), 17 | ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), 18 | )) 19 | db.send_create_signal('menu', ['Menu']) 20 | 21 | # Adding model 'MenuItem' 22 | db.create_table('menu_menuitem', ( 23 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 24 | ('menu', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['menu.Menu'])), 25 | ('order', self.gf('django.db.models.fields.IntegerField')()), 26 | ('link_url', self.gf('django.db.models.fields.CharField')(max_length=100)), 27 | ('title', self.gf('django.db.models.fields.CharField')(max_length=100)), 28 | ('login_required', self.gf('django.db.models.fields.BooleanField')(default=False)), 29 | )) 30 | db.send_create_signal('menu', ['MenuItem']) 31 | 32 | 33 | def backwards(self, orm): 34 | 35 | # Deleting model 'Menu' 36 | db.delete_table('menu_menu') 37 | 38 | # Deleting model 'MenuItem' 39 | db.delete_table('menu_menuitem') 40 | 41 | 42 | models = { 43 | 'menu.menu': { 44 | 'Meta': {'object_name': 'Menu'}, 45 | 'base_url': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 46 | 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 47 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 48 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 49 | 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}) 50 | }, 51 | 'menu.menuitem': { 52 | 'Meta': {'object_name': 'MenuItem'}, 53 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 54 | 'link_url': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 55 | 'login_required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 56 | 'menu': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['menu.Menu']"}), 57 | 'order': ('django.db.models.fields.IntegerField', [], {}), 58 | 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 59 | } 60 | } 61 | 62 | complete_apps = ['menu'] 63 | 64 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-menu 2 | ----------- 3 | 4 | BSD-licensed menu tools for Django, built by Ross Poulton 5 | 6 | django-menu provides a basic structure for you to build multiple navigation 7 | menus for your website, such as the header menubar. These menus can be easily 8 | maintained by staff using the Django administration without any knowledge 9 | of HTML or Django internals. 10 | 11 | Sub-menus can also be easily built and displayed only for particular URIs. 12 | 13 | Installation & Configuration: 14 | ----------------------------- 15 | 16 | 1. ``pip install django-menu`` 17 | 18 | 2. Add ``menu`` to your ``INSTALLED_APPS`` 19 | 20 | 3. ``./manage.py migrate menu`` (or ``./manage.py syncdb`` if you don't use South. You should use South.) 21 | 22 | 4. Add ``django.template.context_processors.request`` to your ``TEMPLATE`` settings. Below is a reasonably safe ``TEMPLATES`` setting for most small projects, however yours may vary.: 23 | 24 | .. code-block:: python 25 | 26 | TEMPLATES = [ 27 | { 28 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 29 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 30 | 'APP_DIRS': True, 31 | 'OPTIONS': { 32 | 'context_processors': [ 33 | 'django.template.context_processors.debug', 34 | 'django.template.context_processors.request', 35 | 'django.contrib.auth.context_processors.auth', 36 | 'django.contrib.messages.context_processors.messages', 37 | 'django.template.context_processors.request', 38 | ], 39 | }, 40 | }, 41 | ] 42 | 43 | 5. Add a Menu (eg called ``headernavigation``) and add some items to that menu 44 | 45 | 6. In your template, load the menu tags and embed your primary menu. 46 | 47 | .. code-block:: html+django 48 | 49 |
    {% load menubuilder %}{% menu headernavigation %} 50 | {% for item in menuitems %}
  • {{ item.title }}
  • 51 | {% endfor %} 52 |
53 | 54 | 55 | Submenus: 56 | --------- 57 | If your template has a spot for navigation for the current sub-level of your 58 | website tree (i.e. a dozen pages underneath ``/about/``, plus a few under 59 | ``/products/``) you can create a new menu with a ``URI`` of ``/about/``. 60 | 61 | In your template, instead of the ``{% menu %}`` tag use ``{% submenu %}``. If a 62 | submenu for the current URI exists, it will be shown. The ``{{ submenu_items }}`` 63 | list contains your navigation items, ready to output like in the examples above. 64 | 65 | Caching: 66 | -------- 67 | To avoid hitting the database every time a user requests a page, the menu items are 68 | cached if you have a cache configured. Caching is not used when ``settings.DEBUG`` is ``True``. 69 | 70 | To disable caching, set the setting ``MENU_CACHE_TIME`` to ``-1`` or remove your 71 | Django Cache configuration. 72 | 73 | To enable caching to continue to let you make items available to anonymous or 74 | authenticated users, and to enable the "Current Page" functionality, the cache 75 | will contain one dataset for each menu, authentication & path combination. 76 | -------------------------------------------------------------------------------- /menu/templatetags/menubuilder.py: -------------------------------------------------------------------------------- 1 | from menu.models import Menu, MenuItem 2 | from django import template 3 | from django.core.cache import cache 4 | 5 | 6 | register = template.Library() 7 | 8 | 9 | def build_menu(parser, token): 10 | """ 11 | {% menu menu_name %} 12 | """ 13 | try: 14 | tag_name, menu_name = token.split_contents() 15 | except: 16 | raise template.TemplateSyntaxError("%r tag requires exactly one argument" % token.contents.split()[0]) 17 | return MenuObject(menu_name) 18 | 19 | 20 | class MenuObject(template.Node): 21 | def __init__(self, menu_name): 22 | self.menu_name = menu_name 23 | 24 | def render(self, context): 25 | try: 26 | current_path = context['request'].path 27 | user = context['request'].user 28 | except KeyError: 29 | current_path = None 30 | user = None 31 | 32 | context['menuitems'] = get_items(self.menu_name, current_path, user) 33 | return '' 34 | 35 | 36 | def build_sub_menu(parser, token): 37 | """ 38 | {% submenu %} 39 | """ 40 | return SubMenuObject() 41 | 42 | 43 | class SubMenuObject(template.Node): 44 | def __init__(self): 45 | pass 46 | 47 | def render(self, context): 48 | current_path = context['request'].path 49 | user = context['request'].user 50 | menu = False 51 | for m in Menu.objects.filter(base_url__isnull=False): 52 | if m.base_url and current_path.startswith(m.base_url): 53 | menu = m 54 | 55 | if menu: 56 | context['submenu_items'] = get_items(menu.slug, current_path, user) 57 | context['submenu'] = menu 58 | else: 59 | context['submenu_items'] = context['submenu'] = None 60 | return '' 61 | 62 | 63 | def get_items(menu_name, current_path, user): 64 | """ 65 | If possible, use a cached list of items to avoid continually re-querying 66 | the database. 67 | The key contains the menu name, whether the user is authenticated, and the current path. 68 | Disable caching by setting MENU_CACHE_TIME to -1. 69 | """ 70 | from django.conf import settings 71 | cache_time = getattr(settings, 'MENU_CACHE_TIME', 1800) 72 | debug = getattr(settings, 'DEBUG', False) 73 | 74 | if user: 75 | is_authenticated = user.is_authenticated 76 | is_anonymous = user.is_anonymous 77 | else: 78 | is_authenticated = False 79 | is_anonymous = True 80 | 81 | if cache_time >= 0 and not debug: 82 | cache_key = 'django-menu-items/%s/%s/%s' % (menu_name, current_path, is_authenticated) 83 | menuitems = cache.get(cache_key, []) 84 | if menuitems: 85 | return menuitems 86 | else: 87 | menuitems = [] 88 | 89 | 90 | menu = Menu.objects.filter(slug=menu_name).first() 91 | 92 | if not menu: 93 | return [] 94 | 95 | for i in MenuItem.objects.filter(menu=menu).order_by('order'): 96 | if current_path: 97 | current = ( i.link_url != '/' and current_path.startswith(i.link_url)) or ( i.link_url == '/' and current_path == '/' ) 98 | if menu.base_url and i.link_url == menu.base_url and current_path != i.link_url: 99 | current = False 100 | else: 101 | current =False 102 | 103 | show_anonymous = i.anonymous_only and is_anonymous 104 | show_auth = i.login_required and is_authenticated 105 | if (not (i.login_required or i.anonymous_only)) or (i.login_required and show_auth) or (i.anonymous_only and show_anonymous): 106 | menuitems.append({'url': i.link_url, 'title': i.title, 'current': current,}) 107 | 108 | if cache_time >= 0 and not debug: 109 | cache.set(cache_key, menuitems, cache_time) 110 | return menuitems 111 | 112 | 113 | register.tag('menu', build_menu) 114 | register.tag('submenu', build_sub_menu) 115 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from distutils.util import convert_path 4 | from fnmatch import fnmatchcase 5 | from setuptools import setup, find_packages 6 | 7 | version = '0.1.13' 8 | 9 | # Provided as an attribute, so you can append to these instead 10 | # of replicating them: 11 | standard_exclude = ('*.py', '*.pyc', '*$py.class', '*~', '.*', '*.bak') 12 | standard_exclude_directories = ('.*', 'CVS', '_darcs', './build', 13 | './dist', 'EGG-INFO', '*.egg-info') 14 | 15 | # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) 16 | # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 17 | # Note: you may want to copy this into your setup.py file verbatim, as 18 | # you can't import this from another package, when you don't know if 19 | # that package is installed yet. 20 | def find_package_data( 21 | where='.', package='', 22 | exclude=standard_exclude, 23 | exclude_directories=standard_exclude_directories, 24 | only_in_packages=True, 25 | show_ignored=False): 26 | """ 27 | Return a dictionary suitable for use in ``package_data`` 28 | in a distutils ``setup.py`` file. 29 | 30 | The dictionary looks like:: 31 | 32 | {'package': [files]} 33 | 34 | Where ``files`` is a list of all the files in that package that 35 | don't match anything in ``exclude``. 36 | 37 | If ``only_in_packages`` is true, then top-level directories that 38 | are not packages won't be included (but directories under packages 39 | will). 40 | 41 | Directories matching any pattern in ``exclude_directories`` will 42 | be ignored; by default directories with leading ``.``, ``CVS``, 43 | and ``_darcs`` will be ignored. 44 | 45 | If ``show_ignored`` is true, then all the files that aren't 46 | included in package data are shown on stderr (for debugging 47 | purposes). 48 | 49 | Note patterns use wildcards, or can be exact paths (including 50 | leading ``./``), and all searching is case-insensitive. 51 | """ 52 | 53 | out = {} 54 | stack = [(convert_path(where), '', package, only_in_packages)] 55 | while stack: 56 | where, prefix, package, only_in_packages = stack.pop(0) 57 | for name in os.listdir(where): 58 | fn = os.path.join(where, name) 59 | if os.path.isdir(fn): 60 | bad_name = False 61 | for pattern in exclude_directories: 62 | if (fnmatchcase(name, pattern) 63 | or fn.lower() == pattern.lower()): 64 | bad_name = True 65 | if show_ignored: 66 | print >> sys.stderr, ( 67 | "Directory %s ignored by pattern %s" 68 | % (fn, pattern)) 69 | break 70 | if bad_name: 71 | continue 72 | if (os.path.isfile(os.path.join(fn, '__init__.py')) 73 | and not prefix): 74 | if not package: 75 | new_package = name 76 | else: 77 | new_package = package + '.' + name 78 | stack.append((fn, '', new_package, False)) 79 | else: 80 | stack.append((fn, prefix + name + '/', package, only_in_packages)) 81 | elif package or not only_in_packages: 82 | # is a file 83 | bad_name = False 84 | for pattern in exclude: 85 | if (fnmatchcase(name, pattern) 86 | or fn.lower() == pattern.lower()): 87 | bad_name = True 88 | if show_ignored: 89 | print >> sys.stderr, ( 90 | "File %s ignored by pattern %s" 91 | % (fn, pattern)) 92 | break 93 | if bad_name: 94 | continue 95 | out.setdefault(package, []).append(prefix+name) 96 | return out 97 | 98 | 99 | 100 | LONG_DESCRIPTION = """ 101 | =========== 102 | django-menu 103 | =========== 104 | 105 | This is a Django-powered navigation management system for 106 | basic websites. 107 | """ 108 | 109 | setup( 110 | name='django-menu', 111 | version=version, 112 | description="Django-powered website navigation maintenance tool", 113 | long_description=LONG_DESCRIPTION, 114 | classifiers=[ 115 | "Programming Language :: Python", 116 | "Topic :: Software Development :: Libraries :: Python Modules", 117 | "Framework :: Django", 118 | "Environment :: Web Environment", 119 | "Operating System :: OS Independent", 120 | "License :: OSI Approved :: BSD License", 121 | "Natural Language :: English", 122 | ], 123 | keywords=['django', 'menus', 'navigatino'], 124 | author='Ross Poulton', 125 | author_email='ross@rossp.org', 126 | url='https://github.com/rossp/django-menu', 127 | license='BSD', 128 | packages=find_packages(), 129 | package_data=find_package_data("menu", only_in_packages=False), 130 | include_package_data=True, 131 | zip_safe=False, 132 | install_requires=['setuptools'], 133 | ) 134 | 135 | --------------------------------------------------------------------------------