Commits

Branko Vukelic  committed bebf09e Merge
  • Participants
  • Parent commits e40105a, 305e88b

Comments (0)

Files changed (15)

 *.py[oc]
 *.sw[op]
+*.db
+dist
+MANIFEST
+Copyright (c) 2013 Monwara LLC.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    1. Redistributions of source code must retain the above copyright notice,
+    this list of conditions and the following disclaimer.
+
+    2. Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in the
+    documentation and/or other materials provided with the distribution.
+
+    3. All advertising materials mentioning features or use of this software
+    must display the following acknowledgement: This product includes software
+    developed by Monwara LLC.
+
+    4. Neither the name of Monwara LLC nor the names of its contributors may be
+    used to endorse or promote products derived from this software without
+    specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY MONWARA LLC ''AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+EVENT SHALL MONWARA LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+include README.rst
+include LICENSE
+include NEWS
+2013-03-05 Released 0.0.1
+=========================
+
+- Initial release with good-enough feature set
 Django URL tools
 ================
 
-Nothing to see here right now.
+------------------------------------------------------
+Django helper tools for dealing with URLs in templates
+------------------------------------------------------
+
+.. contents::
+
+Overview
+========
+
+Django URL tools are context processors, and template tags that help you deal
+with URL manipulations in templates. The heavy lifting is done by the
+``url_tools.helper.UrlHelper`` class which wraps around ``urllib``,
+``urlparse``, and Django's ``QueryDict`` to provide facilities for parsing and
+manipulating URLs.
+
+Installation
+============
+
+Simply install the ``django-url-tools`` package using ``easy_install`` or
+``pip``::
+
+    pip install django-url-tools
+
+Configuring your Django project
+===============================
+
+To use the context processor, add the following to the middlewares stack::
+
+    TEMPLATE_CONTEXT_PROCESSORS = (
+        ...
+        'url_tools.context_processors.current_url',
+    )
+
+If you want to use the template tags, add ``url_tools`` to installed apps::
+
+    INSTALLED_APPS = (
+        ...
+        'url_tools',
+    )
+
+UrlHelper class
+===============
+
+``UrlHelper`` class implements all methods for manipulating URLs that are used
+in other parts of this app. You can also use this class directly by importing
+it from the ``helper`` module::
+
+    from url_tools.helper import UrlHelper
+
+``UrlHelper`` constructor accepts only one argument, which is the full path of
+the URL you want to manipulate. Although we can technically make ``UrlHelper``
+deal with full absolute URLs, we have opted to implement only methods for
+dealing with paths instead. Therefore, if you pass UrlHelper an full URL with
+scheme, host, port, and user credentials, it would still only use the path,
+query parameters, and the fragment identifiers.
+
+The class has following properties:
+
++ ``path``: URL's path without query string and fragment identifier
++ ``fragment``: URL's fragment identifier (without the pound character ``#``)
++ ``query_dict``: ``QueryDict`` instance containing the URL's query parameters
++ ``query``: similar to ``query_dict`` but also does more when assigning
++ ``query_string``: URL's query string
+
+UrlHelper.path
+--------------
+
+This is a simple string property containing the URl's path. For example, in an
+URL ``'/foo/bar?baz=1#boo'``, the property contains ``'/foo/bar'``.
+
+UrlHelper.fragment
+------------------
+
+Contains the fragment identifier. In the URL ``'/foo/bar?baz=1#boo'``, this
+property contains ``'foo'``.
+
+UrlHelper.query_dict
+--------------------
+
+Contains the query parameters parsed from the URL in form of
+``django.http.request.QueryDict`` instance. You can read more about the
+``QueryDict`` API in `Django documentation on QueryDict`_.
+
+UrlHelper.query
+---------------
+
+This is a property returns the ``UrlHelper.query_dict`` when read, but
+overrides it when assigend a normal dictionary or a string. For example::
+
+    u = UrlHelper('/foo/bar')
+    u.query = 'foo=1&bar=2'
+    # or
+    u.query = dict(foo=1, bar=2)
+
+Both above assignment work.
+
+UrlHelper.query_string
+----------------------
+
+This property returns a query string when read, and behaves the same way as the
+query property when assigning a string. However, you cannot assign dictionaries
+to this property. ::
+
+    u = UrlHelper('/foo/bar')
+    u.query_string = 'foo=1&bar=2'       # this works
+    u.query_string = dict(foo=1, bar=2)  # but this doesn't
+
+UrlHelper.get_query_string(**kwargs)
+------------------------------------
+
+This method returns the query string using ``QueryDict``'s ``urlencode()``
+method. Any keyword parameters you pass to this method are forwarded to the
+``urlencode()`` method. Currently, the only keyword argument is ``safe`` which
+instructs the method to not escape specified characters.
+
+UrlHelper.get_query_data()
+--------------------------
+
+Returns the ``UrlHelper.query_dict`` property. This methods exist mostly to
+help customize the behavior of ``UrlHelper.query`` in subclasses, since the
+getter calls this method instead of returning the ``query_dict`` property
+directly.
+
+UrlHelper.update_query_data(**kwargs)
+-------------------------------------
+
+This method takes any number of keyword arguments and updates the
+``UrlHelper.query_dict`` instance. Since, unlike Python dictionary, each
+``QueryDict`` key can have multple values, you can pass multiple values as
+Python iterables such as lists or tuples. For example::
+
+    u = UrlHelper('/foo')
+    u.update_query_data(bar=[1, 2, 3])
+    u.query_string  # returns '/foo?bar=1&bar=2&bar=3'
+
+UrlHelper.get_path()
+--------------------
+
+Returns the ``UrlHelper.path`` property. This method exist to help
+customization of ``UrlHelper.get_full_path()`` method in subclasses. Other than
+that, it's the same as using the ``path`` property.
+
+UrlHelper.get_full_path(**kwargs)
+---------------------------------
+
+Returns the full path with query string and fragment identifier (if any). The
+keyword arguments passed to this function are passed onto 
+``UrlHelper.get_query_string()`` method, and therefore to
+``QueryDict.urlencode()`` method.
+
+UrlHelper.get_full_quoted_path(**kwargs)
+----------------------------------------
+
+Same as ``UrlHelper.get_full_path()`` method, but returns the full path quoted
+so that it can be used as an URL parameter value.
+
+ContextProcessors
+=================
+
+current_url
+-----------
+
+The ``current_url`` context processor will add a new variable to the template's
+context.  This variable is called ``current_url``, and it's an ``UrlHelper``
+instance.  Therefore, this variable has all the properties and methods of the
+``UrlHelper`` class. For instance, if we are currently on ``/foo/bar?baz=1``
+path, you can do the following in a template::
+
+    {{ current_url.query_string }} {# renders `baz=1` #}
+    {{ current_url.get_path }} {# renders `/foo/bar` #}
+
+and so on. The variable itself renders as full relative path with query string
+and fragment identifier (identical to output of ``UrlHelper.get_full_path()``
+method).
+
+Template tags
+=============
+
+To use the template tags, first load the ``urls`` library::
+
+    {% load urls %}
+
+URL tools currently has only one template tag, which is an assignment tag.
+
+{% url_params %}
+---------------------
+
+This tag is used as an assignment tag. Its first argument is an URL, followed
+by any number of keyword arguments that represent the URL parameters. For
+example, if we are requesting a page on ``'/foo'`` path, and do this::
+
+    {% url_params request.get_full_path foo='bar' as new_url %}
+
+We can use the ``new_url`` variable from that point on, that represents the
+``/foo?foo=bar`` URL. To use this with your configured URLs, you can use the
+built-in ``url`` tag::
+
+    {% url 'foo' as foo_url %}
+    {% url_arams foo_url foo='bar' as foo_url %}
+
+If the reverse match for ``'foo'`` is, say, ``'/foo'``, then the ``foo_url``
+variable will, predictably, contain ``'/foo?foo=bar'``.
+
+This tag will override existing parameters rather than adding new values for
+existing keywords. Therefore, you can safely use it to set URL parameters
+whether they exist or not. This is typically useful when you are building URLs
+for controls like pagers. Regardless of whether there is a ``page`` parameter
+or not, setting it with ``url_params`` tag will correctly set the parameter to
+desired value::
+
+    {% url_params current_url page=2 %}
+    {# this works for both ``/foo?page=1`` and just ``/foo`` #}
+
+Reporting bugs
+==============
+
+Please report any bugs to our BitBucket `issue tracker`_.
+
+.. _Django documentation on QueryDict: https://docs.djangoproject.com/en/dev/ref/request-response/?from=olddocs#querydict-objects
+.. _issue tracker: https://bitbucket.org/monwara/django-url-tools/issues

File dev_requirements.txt

+django==1.5
+mock==1.0.1
+nose==1.2.1
+#!/usr/bin/env python
+
+import sys
+from os.path import dirname, abspath
+
+from django.conf import settings, global_settings
+
+from nose.plugins.plugintest import run_buffered as run
+
+
+if not settings.configured:
+    settings.configure(
+        DATABASES={
+            'default': {
+                'ENGINE': 'django.db.backends.sqlite3',
+                'NAME': '%s/test.db' % dirname(abspath(__file__)),
+            }
+        },
+        INSTALLED_APPS=[
+            'url_tools',
+        ],
+        ROOT_URLCONF='',
+        DEBUG=False,
+        SITE_ID=1,
+    )
+
+
+def runtests(*test_args, **kwargs):
+    if 'south' in settings.INSTALLED_APPS:
+        from south.management.commands import patch_for_test_db_setup
+        patch_for_test_db_setup()
+
+    if not test_args:
+        test_args = ['tests']
+
+    parent = dirname(abspath(__file__))
+    sys.path.insert(0, parent)
+
+    run(argv=sys.argv)
+
+if __name__ == '__main__':
+    runtests()
+
+from distutils.core import setup
+
+setup(
+    name='django-url-tools',
+    description='Django helpers for dealing with URLs in templates',
+    long_description=open('README.rst').read(),
+    version='0.0.1',
+    packages=['url_tools', 'url_tools.templatetags'],
+    author='Monwara LLC',
+    author_email='branko@monwara.com',
+    url='https://bitbucket.org/monwara/django-url-tools',
+    download_url='https://bitbucket.org/monwara/django-url-tools/downloads',
+    license='BSD',
+    classifiers = [
+        'Development Status :: 2 - Pre-Alpha',
+        'Framework :: Django',
+        'Environment :: Web Environment',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: BSD License',
+    ],
+)
+
+

File tests/test_context_processors.py

+from unittest import TestCase
+
+from mock import Mock
+
+from url_tools.context_processors import current_url
+
+
+class CurrentUrlProcessorTestCase(TestCase):
+    def setUp(self):
+        self.request = Mock()
+        self.request.get_full_path.return_value = '/foo?foo=1'
+
+    def test_can_get_url_from_request(self):
+        current_url(self.request)
+        self.assertTrue(self.request.get_full_path.called)
+
+    def test_can_insert_url_helper_into_context(self):
+        d = current_url(self.request)
+        self.assertTrue('current_url' in d)
+        self.assertEqual(
+            d['current_url'].get_full_path(),
+            self.request.get_full_path.return_value
+        )

File tests/test_url_helper.py

+from __future__ import unicode_literals
+
+from unittest import TestCase
+
+from url_tools.helper import UrlHelper
+
+
+class UrlHelperTestCase(TestCase):
+    def test_url_helper_get_query_string(self):
+        u = UrlHelper('/foo?foo=1&bar=2')
+        self.assertEqual(u.get_query_string(), 'foo=1&bar=2')
+
+    def test_url_helper_get_query_data(self):
+        u = UrlHelper('/foo?foo=1&bar=2')
+        data = u.get_query_data()
+        self.assertEqual(data['foo'], '1')
+        self.assertEqual(data['bar'], '2')
+
+    def test_update_query_data(self):
+        u = UrlHelper('/foo?foo=1&bar=2')
+        u.update_query_data(foo=2)
+        self.assertEqual(u.get_query_data()['foo'], '2')
+
+    def test_update_query_data_with_multiple_values(self):
+        u = UrlHelper('/foo?foo=1&bar=2')
+        u.update_query_data(foo=[1,2,3])
+        self.assertEqual(u.get_query_data()['foo'], '3')
+        self.assertEqual(u.get_query_data().getlist('foo'), ['1', '2', '3'])
+
+    def test_get_query_string_after_modification(self):
+        u = UrlHelper('/foo?foo=1&bar=2')
+        u.update_query_data(foo=2)
+        self.assertEqual(u.get_query_string(), 'foo=2&bar=2')
+
+    def test_get_query_with_multiple_values(self):
+        u = UrlHelper('/foo')
+        u.update_query_data(foo=[1, 2, 3])
+        self.assertEqual(u.get_query_string(), 'foo=1&foo=2&foo=3')
+
+    def test_safe_slash_argument(self):
+        u = UrlHelper('/foo')
+        u.update_query_data(redir='/foo/bar/')
+        self.assertEqual(u.get_query_string(safe='/'), 'redir=/foo/bar/')
+
+    def test_with_query_params_in_url(self):
+        u = UrlHelper('/foo')
+        u.update_query_data(redir='/foo/bar/?q=Mickey+Mouse')
+        self.assertEqual(u.get_query_string(safe='/'),
+                         'redir=/foo/bar/%3Fq%3DMickey%2BMouse')
+
+    def test_get_path(self):
+        u = UrlHelper('/foo')
+        self.assertEqual(u.get_path(), '/foo')
+
+    def test_get_full_path_with_no_querystring(self):
+        u = UrlHelper('/foo')
+        self.assertEqual(u.get_full_path(), '/foo')
+
+    def test_get_full_path(self):
+        u = UrlHelper('/foo')
+        u.update_query_data(foo=1)
+        self.assertEqual(u.get_full_path(), '/foo?foo=1')
+
+    def test_retains_fragment(self):
+        u = UrlHelper('/foo#bar')
+        u.update_query_data(foo=1)
+        self.assertEqual(u.get_full_path(), '/foo?foo=1#bar')
+
+    def test_query_property(self):
+        u = UrlHelper('/foo?foo=1')
+        self.assertEqual(u.query['foo'], '1')
+
+    def test_query_setter(self):
+        u = UrlHelper('/foo')
+        u.query = dict(foo=1)
+        self.assertEqual(u.query['foo'], '1')
+
+    def test_query_setter_with_string(self):
+        u = UrlHelper('/foo')
+        u.query = 'foo=1&bar=2'
+        self.assertEqual(u.query['foo'], '1')
+        self.assertEqual(u.query['bar'], '2')
+
+    def test_query_string_property(self):
+        u = UrlHelper('/foo?foo=1&bar=2')
+        self.assertEqual(u.query_string, 'foo=1&bar=2')
+
+    def test_query_string_setter(self):
+        u = UrlHelper('/foo')
+        u.query_string = 'foo=1&bar=2'
+        self.assertEqual(u.query['foo'], '1')
+        self.assertEqual(u.query['bar'], '2')
+
+    def test_get_full_quoted_path(self):
+        u = UrlHelper('/foo/bar?foo=1&bar=2#foo')
+        self.assertEqual(u.get_full_quoted_path(),
+                         '/foo/bar%3Ffoo%3D1%26bar%3D2%23foo')
+
+    def test_use_as_string(self):
+        u = UrlHelper('/foo/bar')
+        u.query = dict(foo=1, bar=2)
+        u.fragment = 'baz'
+        self.assertEqual(str(u), '/foo/bar?foo=1&bar=2#baz')

File tests/test_url_params.py

+from __future__ import absolute_import, unicode_literals
+
+from unittest import TestCase
+
+from url_tools.templatetags.urls import url_params
+
+
+class UrlParamsTestCase(TestCase):
+    def test_url_params_basically_works(self):
+        self.assertEquals(
+            url_params('/foo?foo=1', bar='2'),
+            '/foo?foo=1&bar=2'
+        )
+
+    def test_can_override_existing_params(self):
+        self.assertEqual(
+            url_params('/foo?foo=1', foo='2'),
+            '/foo?foo=2'
+        )

File url_tools/context_processors.py

+from __future__ import absolute_import, unicode_literals
+
+from .helper import UrlHelper
+
+
+def current_url(request):
+    full_path = request.get_full_path()
+    return dict(current_url=UrlHelper(full_path))

File url_tools/helper.py

+from __future__ import absolute_import, unicode_literals
+
+import urllib
+import urlparse
+
+from django.http.request import QueryDict
+from django.utils.encoding import iri_to_uri
+
+
+class UrlHelper(object):
+    def __init__(self, full_path):
+        # parse the path
+        r = urlparse.urlparse(full_path)
+        self.path = r.path
+        self.fragment = r.fragment
+        self.query_dict = QueryDict(r.query, mutable=True)
+
+    def get_query_string(self, **kwargs):
+        return self.query_dict.urlencode(**kwargs)
+
+    def get_query_data(self):
+        return self.query_dict
+
+    def update_query_data(self, **kwargs):
+        for key in kwargs:
+            val = kwargs[key]
+            if hasattr(val, '__iter__'):
+                self.query_dict.setlist(key, [iri_to_uri(v) for v in val])
+            else:
+                self.query_dict[key] = iri_to_uri(val)
+
+    def get_path(self):
+        return self.path
+
+    def get_full_path(self, **kwargs):
+        query_string = self.get_query_string(**kwargs)
+        if query_string:
+            query_string = '?%s' % query_string
+        fragment = self.fragment and '#%s' % iri_to_uri(self.fragment) or ''
+
+        return '%s%s%s' % (
+            iri_to_uri(self.get_path()),
+            query_string,
+            fragment
+        )
+
+    def get_full_quoted_path(self, **kwargs):
+        return urllib.quote_plus(self.get_full_path(**kwargs), safe='/')
+
+    @property
+    def query(self):
+        return self.get_query_data()
+
+    @query.setter
+    def query(self, value):
+        if type(value) is dict:
+            self.query_dict = QueryDict('', mutable=True)
+            self.update_query_data(**value)
+        else:
+            self.query_dict = QueryDict(value, mutable=True)
+
+    @property
+    def query_string(self):
+        return self.get_query_string()
+
+    @query_string.setter
+    def query_string(self, value):
+        self.query_dict = QueryDict(value, mutable=True)
+
+    def __str__(self):
+        return self.get_full_path()

File url_tools/templatetags/__init__.py

Empty file added.

File url_tools/templatetags/urls.py

+from __future__ import absolute_import, unicode_literals
+
+from django import template
+
+from ..helper import UrlHelper
+
+register = template.Library()
+
+
+@register.assignment_tag
+def url_params(url, **kwargs):
+    u = UrlHelper(url)
+    u.update_query_data(**kwargs)
+    return u.get_full_path()
+