├── .gitignore ├── MANIFEST.in ├── README.md ├── UNLICENSE ├── activelink ├── __init__.py ├── templatetags │ ├── __init__.py │ └── activelink.py └── tests │ ├── __init__.py │ ├── tests.py │ └── urls.py ├── setup.py └── test-requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.egg-info/ 3 | .DS_Store 4 | build/ 5 | dist/ 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include UNLICENSE 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-activelink 2 | 3 | **django-activelink** is a Django template library for checking whether the current page matches a given URL. It is useful for highlighting active links in menus. 4 | 5 | ## Installation 6 | 7 | You can install django-activelink from PyPI: 8 | 9 | pip install django-activelink 10 | 11 | Add `activelink` to your `INSTALLED_APPS`: 12 | 13 | INSTALLED_APPS = ( 14 | ... 15 | 'activelink', 16 | ... 17 | ) 18 | 19 | Whenever you want to use django-activelink in a template, you need to load its template library: 20 | 21 | {% load activelink %} 22 | 23 | **IMPORTANT**: django-activelink requires that the current request object is available in your template's context. This means you must be using a `RequestContext` when rendering your template, and `django.core.context_processors.request` must be in your `TEMPLATE_CONTEXT_PROCESSORS` setting. See [the documentation](https://docs.djangoproject.com/en/dev/ref/templates/api/#subclassing-context-requestcontext) for more information. 24 | 25 | ## Usage 26 | 27 | Three template tags are provided: `ifactive`, `ifstartswith` and `ifcontains`. These take exactly the same arguments as the built-in `url` template tag. They check whether the URL provided matches the current request URL. This is easiest to explain with an example: 28 | 29 | Menu item 30 | 31 | You can also pass a literal URL rather than a URL name: 32 | 33 | Menu item 34 | 35 | The `ifstartswith` tag checks whether the *beginning* of the current URL matches. This is useful for top-level menu items with submenus attached. 36 | 37 | The `ifcontains` tag checks that the current URL contains the searched part. 38 | 39 | **Note:** Django 1.3 started the process of gradually deprecating the existing `url` template tag and replacing it with a new one, which requires literal string arguments to be quoted. See [the release notes](https://docs.djangoproject.com/en/dev/releases/1.3/#changes-to-url-and-ssi) for more information. To be forwards-compatible, django-activelink *only* supports the new version of the syntax. You can still use it in templates using the old version, but you have to remember to quote your strings properly. 40 | 41 | ## Development 42 | 43 | To contribute, fork the repository, make your changes, add some tests, commit, push, and open a pull request. 44 | 45 | ### How to run the tests 46 | 47 | This project is tested with [nose](http://nose.readthedocs.org). Clone the repository, then run `pip install -r test-requirements.txt` to install nose and Django into your virtualenv. Then, simply type `nosetests` to find and run all the tests. 48 | 49 | ## (Un)license 50 | 51 | This is free and unencumbered software released into the public domain. 52 | 53 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this 54 | software, either in source code form or as a compiled binary, for any purpose, 55 | commercial or non-commercial, and by any means. 56 | 57 | In jurisdictions that recognize copyright laws, the author or authors of this 58 | software dedicate any and all copyright interest in the software to the public 59 | domain. We make this dedication for the benefit of the public at large and to 60 | the detriment of our heirs and successors. We intend this dedication to be an 61 | overt act of relinquishment in perpetuity of all present and future rights to 62 | this software under copyright law. 63 | 64 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 65 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 66 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE 67 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 68 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 69 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 70 | 71 | For more information, please refer to 72 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /activelink/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.0' 2 | __author__ = 'Jamie Matthews (http://j4mie.org) ' 3 | -------------------------------------------------------------------------------- /activelink/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j4mie/django-activelink/dfd8e6f56639688a50c69620d33f4e6b5514e03e/activelink/templatetags/__init__.py -------------------------------------------------------------------------------- /activelink/templatetags/activelink.py: -------------------------------------------------------------------------------- 1 | from django import VERSION as DJANGO_VERSION 2 | from django.template import Library, Node, NodeList, VariableDoesNotExist 3 | from django.core.urlresolvers import NoReverseMatch 4 | from django.template.defaulttags import TemplateIfParser 5 | 6 | if DJANGO_VERSION < (1, 5): 7 | from django.templatetags.future import url 8 | else: 9 | from django.template.defaulttags import url # NOQA 10 | 11 | 12 | register = Library() 13 | 14 | 15 | class ActiveLinkNodeBase(Node): 16 | 17 | def __init__(self, urlnode, var, nodelist_true, nodelist_false): 18 | self.urlnode = urlnode 19 | self.var = var 20 | self.nodelist_true = nodelist_true 21 | self.nodelist_false = nodelist_false 22 | 23 | def render(self, context): 24 | try: 25 | var = self.urlnode.render(context) 26 | except NoReverseMatch: 27 | try: 28 | var = self.var.eval(context) 29 | except VariableDoesNotExist: 30 | var = None 31 | 32 | request = context.get('request') 33 | 34 | # Gracefully fail if request is not in the context 35 | if not request: 36 | import warnings 37 | warnings.warn( 38 | "The activelink templatetags require that a " 39 | "'request' variable is available in the template's " 40 | "context. Check you are using a RequestContext to " 41 | "render your template, and that " 42 | "'django.core.context_processors.request' is in " 43 | "your TEMPLATE_CONTEXT_PROCESSORS setting" 44 | ) 45 | return self.nodelist_false.render(context) 46 | 47 | equal = self.is_active(request, var) 48 | 49 | if equal: 50 | return self.nodelist_true.render(context) 51 | else: 52 | return self.nodelist_false.render(context) 53 | 54 | 55 | class ActiveLinkEqualNode(ActiveLinkNodeBase): 56 | 57 | def is_active(self, request, path_to_check): 58 | return path_to_check == request.path 59 | 60 | 61 | class ActiveLinkStartsWithNode(ActiveLinkNodeBase): 62 | 63 | def is_active(self, request, path_to_check): 64 | return request.path.startswith(path_to_check) 65 | 66 | 67 | class ActiveLinkContainsNode(ActiveLinkNodeBase): 68 | 69 | def is_active(self, request, path_to_check): 70 | return path_to_check in request.path 71 | 72 | 73 | def parse(parser, token, end_tag): 74 | bits = token.split_contents()[1:2] 75 | var = TemplateIfParser(parser, bits).parse() 76 | nodelist_true = parser.parse(('else', end_tag)) 77 | token = parser.next_token() 78 | if token.contents == 'else': 79 | nodelist_false = parser.parse((end_tag,)) 80 | parser.delete_first_token() 81 | else: 82 | nodelist_false = NodeList() 83 | 84 | return var, nodelist_true, nodelist_false 85 | 86 | 87 | @register.tag 88 | def ifactive(parser, token): 89 | urlnode = url(parser, token) 90 | var, nodelist_true, nodelist_false = parse(parser, token, 'endifactive') 91 | return ActiveLinkEqualNode(urlnode, var, nodelist_true, nodelist_false) 92 | 93 | 94 | @register.tag 95 | def ifstartswith(parser, token): 96 | urlnode = url(parser, token) 97 | var, nodelist_true, nodelist_false = parse(parser, token, 'endifstartswith') 98 | return ActiveLinkStartsWithNode(urlnode, var, nodelist_true, nodelist_false) 99 | 100 | 101 | @register.tag 102 | def ifcontains(parser, token): 103 | urlnode = url(parser, token) 104 | var, nodelist_true, nodelist_false = parse(parser, token, 'endifcontains') 105 | return ActiveLinkContainsNode(urlnode, var, nodelist_true, nodelist_false) 106 | -------------------------------------------------------------------------------- /activelink/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django import VERSION as DJANGO_VERSION 3 | from django.conf import settings 4 | 5 | 6 | # bootstrap django 7 | TEMPLATES = [ 8 | { 9 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 10 | 'DIRS': [], 11 | 'APP_DIRS': True, 12 | }, 13 | ] 14 | 15 | if DJANGO_VERSION[:2] >= (1, 9): 16 | TEMPLATES[0].update( 17 | {'OPTIONS': {'builtins': ['activelink.templatetags.activelink']}} 18 | ) 19 | 20 | 21 | settings.configure( 22 | DATABASES={ 23 | 'default': { 24 | 'ENGINE': 'django.db.backends.sqlite3', 25 | 'NAME': ':memory:', 26 | } 27 | }, 28 | ROOT_URLCONF='activelink.tests.urls', 29 | INSTALLED_APPS=[ 30 | 'activelink', 31 | 'activelink.tests', 32 | ], 33 | TEMPLATES=TEMPLATES 34 | ) 35 | 36 | if DJANGO_VERSION >= (1, 7): 37 | django.setup() 38 | -------------------------------------------------------------------------------- /activelink/tests/tests.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from django import VERSION as DJANGO_VERSION 3 | from django.template import Template, Context 4 | from django.test.client import RequestFactory 5 | 6 | if DJANGO_VERSION < (1, 9): 7 | from django.template.base import add_to_builtins 8 | add_to_builtins('activelink.templatetags.activelink') 9 | 10 | 11 | rf = RequestFactory() 12 | 13 | 14 | def render(template_string, dictionary=None): 15 | """Render a template from the supplied string, with optional context data.""" 16 | context = Context(dictionary) 17 | return Template(template_string).render(context) 18 | 19 | 20 | def test_ifactive(): 21 | template = """{% ifactive "test" %}on{% else %}off{% endifactive %}""" 22 | 23 | data = {'request': rf.get('/test-url/')} 24 | rendered = render(template, data) 25 | assert rendered == 'on' 26 | 27 | data = {'request': rf.get('/not-test-url/')} 28 | rendered = render(template, data) 29 | assert rendered == 'off' 30 | 31 | 32 | def test_ifactive_without_else(): 33 | template = """{% ifactive "test" %}on{% endifactive %}""" 34 | 35 | data = {'request': rf.get('/test-url/')} 36 | rendered = render(template, data) 37 | assert rendered == 'on' 38 | 39 | data = {'request': rf.get('/not-test-url/')} 40 | rendered = render(template, data) 41 | assert rendered == '' 42 | 43 | 44 | def test_ifactive_with_literal_url(): 45 | template = """{% ifactive "/my-url/" %}on{% else %}off{% endifactive %}""" 46 | 47 | data = {'request': rf.get('/my-url/')} 48 | rendered = render(template, data) 49 | assert rendered == 'on' 50 | 51 | data = {'request': rf.get('/not-my-url/')} 52 | rendered = render(template, data) 53 | assert rendered == 'off' 54 | 55 | 56 | def test_ifactive_with_url_in_variable(): 57 | template = """{% ifactive myurl %}on{% else %}off{% endifactive %}""" 58 | 59 | data = {'request': rf.get('/test-url/'), 'myurl': '/test-url/'} 60 | rendered = render(template, data) 61 | assert rendered == 'on' 62 | 63 | data = {'request': rf.get('/test-url/'), 'myurl': '/not-test-url/'} 64 | rendered = render(template, data) 65 | assert rendered == 'off' 66 | 67 | 68 | def test_ifactive_with_url_arguments(): 69 | template = """{% ifactive "test_with_arg" "somearg" %}on{% else %}off{% endifactive %}""" 70 | 71 | data = {'request': rf.get('/test-url-with-arg/somearg/')} 72 | rendered = render(template, data) 73 | assert rendered == 'on' 74 | 75 | data = {'request': rf.get('/test-url-with-arg/other/')} 76 | rendered = render(template, data) 77 | assert rendered == 'off' 78 | 79 | template = """{% ifactive "test_with_kwarg" arg="somearg" %}on{% else %}off{% endifactive %}""" 80 | 81 | data = {'request': rf.get('/test-url-with-kwarg/somearg/')} 82 | rendered = render(template, data) 83 | assert rendered == 'on' 84 | 85 | data = {'request': rf.get('/test-url-with-kwarg/other/')} 86 | rendered = render(template, data) 87 | assert rendered == 'off' 88 | 89 | 90 | def test_ifstartswith(): 91 | template = """{% ifstartswith "test" %}on{% else %}off{% endifstartswith %}""" 92 | 93 | data = {'request': rf.get('/test-url/')} 94 | rendered = render(template, data) 95 | assert rendered == 'on' 96 | 97 | data = {'request': rf.get('/test-url/sub/')} 98 | rendered = render(template, data) 99 | assert rendered == 'on' 100 | 101 | data = {'request': rf.get('/not-test-url/')} 102 | rendered = render(template, data) 103 | assert rendered == 'off' 104 | 105 | 106 | def test_ifcontains(): 107 | template = """{% ifcontains "url" %}on{% else %}off{% endifcontains %}""" 108 | 109 | data = {'request': rf.get('/test-url/')} 110 | rendered = render(template, data) 111 | assert rendered == 'on' 112 | 113 | data = {'request': rf.get('/test-url/sub/')} 114 | rendered = render(template, data) 115 | assert rendered == 'on' 116 | 117 | data = {'request': rf.get('/test-should-fail/')} 118 | rendered = render(template, data) 119 | assert rendered == 'off' 120 | 121 | 122 | def test_fails_gracefully_without_request(): 123 | template = """{% ifactive "test" %}on{% else %}off{% endifactive %}""" 124 | 125 | with warnings.catch_warnings(record=True) as w: 126 | rendered = render(template) 127 | assert len(w) == 1 128 | assert rendered == 'off' 129 | 130 | 131 | def test_with_querystring(): 132 | template = """{% ifactive "test" %}on{% else %}off{% endifactive %}""" 133 | 134 | data = {'request': rf.get('/test-url/?foo=bar')} 135 | rendered = render(template, data) 136 | assert rendered == 'on' 137 | -------------------------------------------------------------------------------- /activelink/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django import VERSION as DJANGO_VERSION 2 | from django.http import HttpResponse 3 | 4 | 5 | if DJANGO_VERSION >= (1, 10): 6 | from django.conf.urls import url 7 | elif DJANGO_VERSION >= (1, 6): 8 | from django.conf.urls import patterns, url 9 | else: 10 | from django.conf.urls.defaults import patterns, url 11 | 12 | 13 | urlpatterns = [ 14 | url(r'^test-url/$', lambda r: HttpResponse('ok'), name='test'), 15 | url(r'^test-url-with-arg/([-\w]+)/$', lambda r, arg: HttpResponse('ok'), name='test_with_arg'), 16 | url(r'^test-url-with-kwarg/(?P[-\w]+)/$', lambda r, arg: HttpResponse('ok'), name='test_with_kwarg'), 17 | ] 18 | 19 | if DJANGO_VERSION < (1, 10): 20 | urlpatterns = patterns('', *urlpatterns) 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from setuptools import setup, find_packages 4 | 5 | description = ('A Django template library for checking whether the current ' 6 | 'page matches a given URL. Useful for highlighting active ' 7 | 'links in menus.') 8 | 9 | rel_file = lambda *args: os.path.join(os.path.dirname(os.path.abspath(__file__)), *args) 10 | 11 | def read_from(filename): 12 | fp = open(filename) 13 | try: 14 | return fp.read() 15 | finally: 16 | fp.close() 17 | 18 | def get_version(): 19 | data = read_from(rel_file('activelink', '__init__.py')) 20 | return re.search(r"__version__ = '([^']+)'", data).group(1) 21 | 22 | setup( 23 | name='django-activelink', 24 | version=get_version(), 25 | description=description, 26 | author='Jamie Matthews', 27 | author_email='jamie.matthews@gmail.com', 28 | url='http://github.com/j4mie/django-activelink/', 29 | packages=find_packages(exclude=['tests', 'tests.*']), 30 | classifiers = [ 31 | 'Programming Language :: Python', 32 | 'Development Status :: 3 - Alpha', 33 | 'Intended Audience :: Developers', 34 | 'License :: Public Domain', 35 | 'Framework :: Django', 36 | 'Operating System :: OS Independent', 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | nose==1.1.2 2 | django==1.3 3 | --------------------------------------------------------------------------------