├── .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 |
--------------------------------------------------------------------------------