├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── .travis.yml ├── CHANGES.rst ├── MANIFEST.in ├── README.rst ├── UNLICENSE ├── requirements.txt ├── setup.cfg ├── setup.py ├── spurl ├── __init__.py ├── models.py ├── templatetags │ ├── __init__.py │ └── spurl.py └── tests.py └── tox.ini /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-latest 14 | env: 15 | TWINE_USERNAME: "__token__" 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: '3.x' 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install setuptools wheel twine tox 26 | - name: Build package 27 | run: python setup.py sdist bdist_wheel 28 | - name: Publish package 29 | env: 30 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 31 | run: twine upload dist/* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.eggs/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | 44 | # Translations 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | dist: xenial 3 | language: python 4 | cache: pip 5 | sudo: false 6 | 7 | python: 8 | - "3.6" 9 | - "3.7" 10 | - "3.8" 11 | - "3.9" 12 | 13 | install: 14 | - pip install tox-travis 15 | 16 | script: 17 | - tox 18 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changes 2 | ------- 3 | 4 | 0.6.8 (2021-11-15) 5 | ~~~~~~~~~~~~~~~~~~ 6 | 7 | * Fix ``toggle_query`` support when one word is a fragment of the other. 8 | 9 | 0.6.7 (2020-05-22) 10 | ~~~~~~~~~~~~~~~~~~ 11 | 12 | * Fixed MANIFEST.in 13 | 14 | 0.6.6 (2019-03-29) 15 | ~~~~~~~~~~~~~~~~~~ 16 | 17 | * Added support for an except clause to remove all but specifed query vars. 18 | 19 | 0.6.5 (2018-05-09) 20 | ~~~~~~~~~~~~~~~~~~ 21 | 22 | * Added support for Django 2.x and dropped support for older and 23 | non-LTS version of Django. 24 | 25 | 0.6.4 (2015-12-26) 26 | ~~~~~~~~~~~~~~~~~~ 27 | 28 | * Getting ready for Django 1.10 release. 29 | * Dropped support for Django 1.3 and older. 30 | 31 | 0.6.3 (2015-12-17) 32 | ~~~~~~~~~~~~~~~~~~ 33 | 34 | * Django 1.9 compatible (Albert Koch) 35 | 36 | 0.6.2 (2015-09-17) 37 | ~~~~~~~~~~~~~~~~~~ 38 | 39 | * Add support for template variables to ``remove_query_param``. 40 | * Handle auth parameters to be able to add username:password to URLs. 41 | 42 | 0.6.1 (2015-07-14) 43 | ~~~~~~~~~~~~~~~~~~ 44 | 45 | * Python 3 compatible! 46 | 47 | 0.6.0 (2012-02-23) 48 | ~~~~~~~~~~~~~~~~~~ 49 | 50 | * Upgrade URLObject dependency to 2.0 51 | 52 | 0.5.0 (2011-12-14) 53 | ~~~~~~~~~~~~~~~~~~ 54 | 55 | * Fix typos in changelog. 56 | * Add family of arguments (\ ``_from``\ ) for combining URLs. 57 | * Add ``toggle_query`` argument. 58 | 59 | 0.4.0 (2011-12-07) 60 | ~~~~~~~~~~~~~~~~~~ 61 | 62 | * Upgrade URLObject dependency to 0.6.0 63 | * Add ``remove_query_param`` argument. 64 | * Add support for template tags embedded within argument values. 65 | * Extensive refactoring. 66 | 67 | 0.3.0 (2011-08-18) 68 | ~~~~~~~~~~~~~~~~~~ 69 | 70 | * Add ``set_query`` argument. 71 | 72 | 0.2.0 (2011-08-08) 73 | ~~~~~~~~~~~~~~~~~~ 74 | 75 | * Add ``as`` argument to insert generated URL into template context. 76 | 77 | 0.1.0 (2011-07-29) 78 | ~~~~~~~~~~~~~~~~~~ 79 | 80 | * Initial release. 81 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES.rst 2 | include README.rst 3 | include UNLICENSE 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Super URLs for Django 2 | ===================== 3 | 4 | .. code:: raw 5 | 6 | .=., 7 | ;c =\ 8 | __| _/ 9 | .'-'-._/-'-._ 10 | /.. ____ \ 11 | / {% spurl %} \ 12 | ( / \--\_>/-/'._ ) 13 | \-;_/\__;__/ _/ _/ 14 | '._}|==o==\{_\/ 15 | / /-._.--\ \_ 16 | // / /| \ \ \ 17 | / | | | \; | \ \ 18 | / / | :/ \: \ \_\ 19 | / | /.'| /: | \ \ 20 | | | |--| . |--| \_\ 21 | / _/ \ | : | /___--._) \ 22 | |_(---'-| >-'-| | '-' 23 | /_/ \_\ 24 | 25 | .. image:: https://img.shields.io/pypi/v/django-spurl.svg 26 | :target: https://pypi.python.org/pypi/django-spurl/ 27 | 28 | .. image:: https://img.shields.io/pypi/dm/django-spurl.svg 29 | :target: https://pypi.python.org/pypi/django-spurl/ 30 | 31 | .. image:: https://img.shields.io/github/license/j4mie/django-spurl.svg 32 | :target: https://pypi.python.org/pypi/django-spurl/ 33 | 34 | .. image:: https://img.shields.io/travis/j4mie/django-spurl.svg 35 | :target: https://travis-ci.com/github/j4mie/django-spurl/ 36 | 37 | .. image:: https://coveralls.io/repos/github/j4mie/django-spurl/badge.svg?branch=develop 38 | :target: https://coveralls.io/github/j4mie/django-spurl?branch=develop 39 | 40 | **Spurl** is a Django template library for manipulating URLs. It's built 41 | on top of Zachary Voase's excellent 42 | `urlobject `__. 43 | 44 | Authored by `Jamie Matthews `__, and some great 45 | `contributors `__. 46 | 47 | Installation 48 | ------------ 49 | 50 | Either checkout ``spurl`` from GitHub, or install using pip: 51 | 52 | .. code:: shell 53 | 54 | pip install django-spurl 55 | 56 | Add ``spurl`` to your ``INSTALLED_APPS``: 57 | 58 | .. code:: python 59 | 60 | INSTALLED_APPS = ( 61 | ... 62 | 'spurl', 63 | ) 64 | 65 | Finally, whenever you want to use Spurl in a template, you need to load 66 | its template library: 67 | 68 | .. code:: html+django 69 | 70 | {% load spurl %} 71 | 72 | Usage 73 | ----- 74 | 75 | Spurl is **not** a replacement for Django's built-in ``{% url %}`` 76 | template tag. It is a general-purpose toolkit for manipulating URL 77 | components in templates. You can use it alongside ``{% url %}`` if you 78 | like (see below). 79 | 80 | Spurl provides a single template tag, called (surprisingly enough), 81 | ``spurl``. You call it with a set of ``key=value`` keyword arguments, 82 | which are described fully below. 83 | 84 | To show some of the features of Spurl, we'll go over a couple of simple 85 | example use cases. 86 | 87 | Adding query parameters to URLs 88 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 89 | 90 | Say you have a list of external URLs in your database. When you create 91 | links to these URLs in a template, you need to add a 92 | ``referrer=mysite.com`` query parameter to each. The simple way to do 93 | this might be: 94 | 95 | .. code:: html+django 96 | 97 | {% for url, title in list_of_links %} 98 | {{ title }} 99 | {% endfor %} 100 | 101 | The problem here is that you don't know in advance if the URLs stored in 102 | your database *already* have query parameters. If they do, you'll 103 | generate malformed links like 104 | ``http://www.example.com?foo=bar?referrer=mysite.com``. 105 | 106 | Spurl can fix this. Because it knows about the components of a URL, it 107 | can add parameters onto an existing query, if there is one. 108 | 109 | .. code:: html+django 110 | 111 | {% for url, title in list_of_links %} 112 | {{ title }} 113 | {% endfor %} 114 | 115 | Note that **when you pass a literal string to Spurl, you have to wrap it 116 | in double quotes**. If you don't, Spurl will assume it's a variable name 117 | and try to look it up in the template's context. 118 | 119 | SSL-sensitive external URLs. 120 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 121 | 122 | Suppose your site needs to display a gallery of images, the URLs of 123 | which have come from some third-party web API. Additionally, imagine 124 | your site needs to run both in secure and non-secure mode - the same 125 | content is available at both ``https`` or ``http`` URLs (depending on 126 | whether a visitor is logged in, say). Some browsers will complain loudly 127 | (displaying "Mixed content warnings" to the user) if the page being 128 | displayed is ``https`` but some of the assets are ``http``. Spurl can 129 | fix this. 130 | 131 | .. code:: html+django 132 | 133 | {% for image_url in list_of_image_urls %} 134 | 135 | {% endfor %} 136 | 137 | This will take the image URL you supply and replace the scheme component 138 | (the ``http`` or ``https`` bit) with the correct version, depending on 139 | the return value of ``request.is_secure()``. Note that the above assumes 140 | you're using a ``RequestContext`` so that ``request`` is available in 141 | your template. 142 | 143 | Using alongside ``{% url %}`` 144 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 145 | 146 | Notice that Spurl's functionality doesn't overlap with Django's built-in 147 | ``{% url %}`` tag. Spurl doesn't know about your urlconf, and doesn't do 148 | any URL reversing. In fact, Spurl is mostly useful for manipulating 149 | **external** URLs, rather than URLs on your own site. However, you can 150 | easily use Spurl with ``{% url %}`` if you need to. You just have to use 151 | the ``as`` keyword to put your reversed URL in a template variable, and 152 | then pass this to Spurl. As it's a relative path (rather than a full 153 | URL) you should pass it using the ``path`` argument. For example, say 154 | you want to append some query parameters to a URL on your site: 155 | 156 | .. code:: html+django 157 | 158 | {% url your_url_name as my_url %} 159 | Click here! 160 | 161 | There is another way to use Spurl with ``{% url %}``, see *Embedding 162 | template tags* below. 163 | 164 | Available arguments 165 | ~~~~~~~~~~~~~~~~~~~ 166 | 167 | Below is a full list of arguments that Spurl understands. 168 | 169 | base 170 | ^^^^ 171 | 172 | If you pass a ``base`` argument to Spurl, it will parse its contents and 173 | use this as the base URL upon which all other arguments will operate. If 174 | you *don't* pass a ``base`` argument, Spurl will generate a URL from 175 | scratch based on the components that you pass in separately. 176 | 177 | scheme 178 | ^^^^^^ 179 | 180 | Set the scheme component of the URL. Example: 181 | 182 | .. code:: html+django 183 | 184 | {% spurl base="http://example.com" scheme="ftp" %} 185 | 186 | This will return ``ftp://example.com`` 187 | 188 | See also: ``scheme_from``, below. 189 | 190 | host 191 | ^^^^ 192 | 193 | Set the host component of the URL. Example: 194 | 195 | .. code:: html+django 196 | 197 | {% spurl base="http://example.com/some/path/" host="google.com" %} 198 | 199 | This will return ``http://google.com/some/path/`` 200 | 201 | See also: ``host_from``, below. 202 | 203 | auth 204 | ^^^^ 205 | 206 | Handle HTTP Basic authentication, username and password can be passed in 207 | URL. Example: 208 | 209 | .. code:: html+django 210 | 211 | {% spurl base="https://example.com" auth="user:pass" %} 212 | 213 | This will return ``https://user:pass@example.com`` 214 | 215 | path 216 | ^^^^ 217 | 218 | Set the path component of the URL. Example: 219 | 220 | .. code:: html+django 221 | 222 | {% spurl base="http://example.com/some/path/" path="/different/" %} 223 | 224 | This will return ``http://example.com/different/`` 225 | 226 | See also: ``path_from``, below. 227 | 228 | add\_path 229 | ^^^^^^^^^ 230 | 231 | Append a path component to the existing path. You can add multiple 232 | ``add_path`` calls, and the results of each will be combined. Example: 233 | 234 | .. code:: html+django 235 | 236 | {% spurl base=STATIC_URL add_path="javascript" add_path="lib" add_path="jquery.js" %} 237 | 238 | This will return ``http://cdn.example.com/javascript/lib/jquery.js`` 239 | (assuming ``STATIC_URL`` is set to ``http://cdn.example.com``) 240 | 241 | See also: ``add_path_from``, below. 242 | 243 | fragment 244 | ^^^^^^^^ 245 | 246 | Set the fragment component of the URL. Example: 247 | 248 | .. code:: html+django 249 | 250 | {% spurl base="http://example.com" fragment="myfragment" %} 251 | 252 | This will return ``http://example.com/#myfragment`` 253 | 254 | See also: ``fragment_from``, below. 255 | 256 | port 257 | ^^^^ 258 | 259 | Set the port component of the URL. Example: 260 | 261 | .. code:: html+django 262 | 263 | {% spurl base="http://example.com/some/path/" port="8080" %} 264 | 265 | This will return ``http://example.com:8080/some/path/`` 266 | 267 | See also: ``port_from``, below. 268 | 269 | query 270 | ^^^^^ 271 | 272 | Set the query component of the URL. Example: 273 | 274 | .. code:: html+django 275 | 276 | {% spurl base="http://example.com/" query="foo=bar&bar=baz" %} 277 | 278 | This will return ``http://example.com/?foo=bar&bar=baz`` 279 | 280 | The ``query`` argument can also be passed a dictionary from your 281 | template's context. 282 | 283 | .. code:: python 284 | 285 | # views.py 286 | def my_view(request): 287 | my_query_params = {'foo': 'bar', 'bar': 'baz'} 288 | return render(request, 'path/to/template.html', {'my_query_params': my_query_params}) 289 | 290 | .. code:: html+django 291 | 292 | 293 | {% spurl base="http://example.com/" query=my_query_params %} 294 | 295 | This will return ``http://example.com/?foo=bar&bar=baz`` 296 | 297 | Finally, you can pass individual template variables to the query. To do 298 | this, Spurl uses Django's template system. For example: 299 | 300 | .. code:: html+django 301 | 302 | {% spurl base="http://example.com/" query="foo={{ variable_name }}" %} 303 | 304 | See also: ``query_from``, below. 305 | 306 | add\_query 307 | ^^^^^^^^^^ 308 | 309 | Append a set of parameters to an existing query. If your base URL might 310 | already have a query component, this will merge the existing parameters 311 | with your new ones. Example: 312 | 313 | .. code:: html+django 314 | 315 | {% spurl base="http://example.com/?foo=bar" add_query="bar=baz" %} 316 | 317 | This will return ``http://example.com?foo=bar&bar=baz`` 318 | 319 | You can add multiple ``add_query`` calls, and the results of each will 320 | be combined: 321 | 322 | .. code:: html+django 323 | 324 | {% spurl base="http://example.com/" add_query="foo=bar" add_query="bar=baz" %} 325 | 326 | This will return ``http://example.com?foo=bar&bar=baz`` 327 | 328 | Like the ``query`` argument above, the values passed to ``add_query`` 329 | can also be dictionaries, and they can contain Django template 330 | variables. 331 | 332 | See also: ``add_query_from``, below. 333 | 334 | set\_query 335 | ^^^^^^^^^^ 336 | 337 | Appends a set of parameters to an existing query, overwriting existing 338 | parameters with the same name. Otherwise uses the exact same syntax as 339 | ``add_query``. 340 | 341 | See also: ``set_query_from``, below. 342 | 343 | toggle\_query 344 | ^^^^^^^^^^^^^ 345 | 346 | Toggle the value of one or more query parameters between two possible 347 | values. Useful when reordering list views. Example: 348 | 349 | .. code:: html+django 350 | 351 | {% spurl base=request.get_full_path toggle_query="sort=ascending,descending" %} 352 | 353 | If the value of ``request.get_full_path()`` doesn't have a ``sort`` 354 | parameter, one will be added with a value of ``ascending`` (the first 355 | item in the list is the default). If it already has a ``sort`` 356 | parameter, and it is currently set to ``ascending``, it will be set to 357 | ``descending``. If it's already set to ``descending``, it will be set to 358 | ``ascending``. 359 | 360 | You can also specify the options as a dictionary, mapping the parameter 361 | name to a two-tuple containing the values to toggle. Example: 362 | 363 | .. code:: python 364 | 365 | # views.py 366 | 367 | SORT_PARAM = 'sort' 368 | ASCENDING = 'ascending' 369 | DESCENDING = 'descending' 370 | 371 | def my_view(request): 372 | 373 | if request.GET.get(SORT_PARAM, ASCENDING) == DESCENDING: 374 | object_list = MyModel.objects.order_by('-somefield') 375 | else: 376 | object_list = MyModel.objects.order_by('somefield') 377 | 378 | return render(request, 'path/to/template.html', { 379 | 'object_list': object_list, 380 | 'sort_params': {SORT_PARAM: (ASCENDING, DESCENDING)}, 381 | }) 382 | 383 | .. code:: html+django 384 | 385 | 386 | Reverse order 387 | 388 | remove\_query\_param 389 | ^^^^^^^^^^^^^^^^^^^^ 390 | 391 | Remove a query parameter from an existing query: 392 | 393 | .. code:: html+django 394 | 395 | {% spurl base="http://example.com/?foo=bar&bar=baz" remove_query_param="foo" %} 396 | 397 | This will return ``http://example.com?bar=baz`` 398 | 399 | Again, you can add multiple ``remove_query_param`` calls, and the 400 | results will be combined: 401 | 402 | .. code:: html+django 403 | 404 | {% spurl base="http://example.com/?foo=bar&bar=baz" remove_query_param="foo" remove_query_param="bar" %} 405 | 406 | This will return ``http://example.com/`` 407 | 408 | You can also remove parameters with specific values: 409 | 410 | .. code:: html+django 411 | 412 | {% spurl base="http://example.com/?foo=bar&bar=baz&foo=baz" remove_query_param="foo" remove_query_param="foo=baz" %} 413 | 414 | This will return ``http://example.com/?bar=baz`` 415 | 416 | Finally, you can pass individual template variables to the 417 | ``remove_query_param`` calls. To do this, Spurl uses Django's template 418 | system. For example: 419 | 420 | .. code:: html+django 421 | 422 | {% spurl base="http://example.com/?foo=bar&bar=baz" remove_query_param="{{ variable_name }}" %} 423 | 424 | secure 425 | ^^^^^^ 426 | 427 | Control whether the generated URL starts with ``http`` or ``https``. The 428 | value of this argument can be a boolean (``True`` or ``False``), if 429 | you're using a context variable. If you're using a literal argument 430 | here, it must be a quoted string. The strings ``"True"`` or ``"on"`` 431 | (case-insensitive) will be converted to ``True``, any other string will 432 | be converted to ``False``. Example: 433 | 434 | .. code:: html+django 435 | 436 | {% spurl base="http://example.com/" secure="True" %} 437 | 438 | This will return ``https://example.com/`` 439 | 440 | autoescape 441 | ^^^^^^^^^^ 442 | 443 | By default, Spurl will escape its output in the same way as Django's 444 | template system. For example, an ``&`` character in a URL will be 445 | rendered as ``&``. You can override this behaviour by passing an 446 | ``autoescape`` argument, which must be either a boolean (if passed from 447 | a template variable) or a string. The strings ``"True"`` or ``"on"`` 448 | (case-insensitive) will be converted to ``True``, any other string will 449 | be converted to ``False``. 450 | 451 | Added bonus: ``_from`` parameters 452 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 453 | 454 | As well as those listed above, Spurl provides a family of parameters for 455 | *combining* URLs. Given a base URL to start with, you can copy a 456 | component from another URL. These arguments expect to be passed a full 457 | URL (or anything that can be understood by ``URLObject.parse``). This 458 | URL will be parsed, and then the component in question will be extracted 459 | and combined with the base URL. 460 | 461 | Below is a full list of the available ``_from`` methods. They have 462 | identical semantics to their counterparts above (except they expect a 463 | full URL, not just a URL component). 464 | 465 | - ``query_from`` 466 | - ``add_query_from`` 467 | - ``set_query_from`` 468 | - ``scheme_from`` 469 | - ``host_from`` 470 | - ``path_from`` 471 | - ``add_path_from`` 472 | - ``fragment_from`` 473 | - ``port_from`` 474 | 475 | Example: 476 | 477 | .. code:: html+django 478 | 479 | {% spurl base="http://example.com/foo/bar/?foo=bar path_from="http://another.com/something/?bar=foo" %} 480 | 481 | This will return ``http://example.com/something/?foo=bar`` 482 | 483 | Building a URL without displaying it 484 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 485 | 486 | Like Django's ``{% url %}`` tag, Spurl allows you to insert the 487 | generated URL into the template's context for later use. Example: 488 | 489 | .. code:: html+django 490 | 491 | {% spurl base="http://example.com" secure="True" as secure_url %} 492 |

The secure version of the url is {{ secure_url }}

493 | 494 | Embedding template tags 495 | ~~~~~~~~~~~~~~~~~~~~~~~ 496 | 497 | As mentioned above, Spurl uses Django's template system to individually 498 | parse any arguments which can be passed strings. This allows the use of 499 | syntax such as: 500 | 501 | .. code:: html+django 502 | 503 | {% spurl base="http://example.com" add_query="foo={{ bar }}" %} 504 | 505 | This works fine for variable and filters, but what if we want to use 506 | other template tags *inside* our Spurl tag? We can't nest ``{%`` and 507 | ``%}`` tokens inside each other, because Django's template parser would 508 | get very confused. Instead, we have to escape the inner set of tag 509 | markers with backslashes: 510 | 511 | .. code:: html+django 512 | 513 | {% spurl base="http://example.com" add_query="next={\% url home %\}" %} 514 | 515 | Note that any tags or filters loaded in your template are automatically 516 | available in the nested templates used to render each variable. This 517 | means we can do: 518 | 519 | .. code:: html+django 520 | 521 | {% load url from future %} 522 | {% spurl base="{\% url 'home' %\}" %} 523 | 524 | Be careful with your quotation marks! If you use double-quotes to 525 | surround the nested template, you have to use single quotes inside it. 526 | 527 | **Warning!** This functionality only exists to serve the most complex of 528 | use cases, and is extremely magical (and probably a bad idea). You may 529 | prefer to use: 530 | 531 | .. code:: html+django 532 | 533 | {% url "home" as my_url %} 534 | {% spurl base=my_url %} 535 | 536 | Development 537 | ----------- 538 | 539 | To contribute, fork the repository, make your changes, add some tests, 540 | commit, push, and open a pull request. 541 | 542 | How to run the tests 543 | ~~~~~~~~~~~~~~~~~~~~ 544 | 545 | Spurl is tested with `nose `__. Clone the 546 | repository, then run ``pip install -r requirements.txt`` to install nose 547 | and Django into your virtualenv. Then, simply type ``nosetests`` to find 548 | and run all the tests. 549 | 550 | (Un)license 551 | ----------- 552 | 553 | This is free and unencumbered software released into the public domain. 554 | 555 | Anyone is free to copy, modify, publish, use, compile, sell, or 556 | distribute this software, either in source code form or as a compiled 557 | binary, for any purpose, commercial or non-commercial, and by any means. 558 | 559 | In jurisdictions that recognize copyright laws, the author or authors of 560 | this software dedicate any and all copyright interest in the software to 561 | the public domain. We make this dedication for the benefit of the public 562 | at large and to the detriment of our heirs and successors. We intend 563 | this dedication to be an overt act of relinquishment in perpetuity of 564 | all present and future rights to this software under copyright law. 565 | 566 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 567 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 568 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 569 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 570 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 571 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 572 | DEALINGS IN THE SOFTWARE. 573 | 574 | For more information, please refer to http://unlicense.org/ 575 | 576 | Artwork credit 577 | -------------- 578 | 579 | Superman ASCII art comes from http://ascii.co.uk/art/superman 580 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django >= 1.4 2 | coverage 3 | nose 4 | six 5 | urlobject >= 2.0.0 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = django-spurl 3 | version = 0.6.8 4 | description = A Django template library for manipulating URLs. 5 | long_description = file: README.rst, CHANGES.rst 6 | long_description_content_type = text/x-rst 7 | author = Jamie Matthews 8 | author_email = jamie.matthews@jamesturk.net 9 | maintainer = Basil Shubin 10 | maintainer_email = basil.shubin@gmail.com 11 | url = https://github.com/j4mie/django-spurl/ 12 | download_url = https://github.com/j4mie/django-spurl/zipball/master 13 | license = Public Domain 14 | classifiers = 15 | Development Status :: 5 - Production/Stable 16 | Environment :: Web Environment 17 | Intended Audience :: Developers 18 | License :: Public Domain 19 | Operating System :: OS Independent 20 | Programming Language :: Python 21 | Programming Language :: Python :: 3 :: Only 22 | Programming Language :: Python :: 3.6 23 | Programming Language :: Python :: 3.7 24 | Programming Language :: Python :: 3.8 25 | Programming Language :: Python :: 3.9 26 | Framework :: Django 27 | Framework :: Django :: 2.2 28 | Framework :: Django :: 3.0 29 | Framework :: Django :: 3.1 30 | Framework :: Django :: 3.2 31 | 32 | [options] 33 | zip_safe = False 34 | include_package_data = True 35 | packages = find: 36 | install_requires = 37 | urlobject>=2.4.0 38 | six 39 | 40 | [options.extras_require] 41 | develop = 42 | tox 43 | django 44 | nose 45 | test = 46 | coverage 47 | nose 48 | 49 | [bdist_wheel] 50 | # No longer universal (Python 3 only) but leaving this section in here will 51 | # trigger zest to build a wheel. 52 | universal = 0 53 | 54 | [flake8] 55 | # Some sane defaults for the code style checker flake8 56 | # black compatibility 57 | max-line-length = 88 58 | # E203 and W503 have edge cases handled by black 59 | extend-ignore = E203, W503 60 | exclude = 61 | .tox 62 | build 63 | dist 64 | .eggs 65 | 66 | [nosetests] 67 | verbosity = 1 68 | detailed-errors = 1 69 | with-coverage = 1 70 | cover-package = spurl 71 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | setup() 5 | -------------------------------------------------------------------------------- /spurl/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.6.7" 2 | -------------------------------------------------------------------------------- /spurl/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j4mie/django-spurl/3a3f5a2dcad52c8fd48d4200bf3828ced86329b8/spurl/models.py -------------------------------------------------------------------------------- /spurl/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j4mie/django-spurl/3a3f5a2dcad52c8fd48d4200bf3828ced86329b8/spurl/templatetags/__init__.py -------------------------------------------------------------------------------- /spurl/templatetags/spurl.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import django 4 | from django.conf import settings 5 | from django.template import Library, Node, Origin, Template, TemplateSyntaxError 6 | from django.template.base import Lexer, Parser 7 | from django.template.defaulttags import kwarg_re 8 | from django.utils.encoding import smart_str 9 | from django.utils.html import escape 10 | from urlobject import URLObject 11 | from urlobject.query_string import QueryString 12 | 13 | register = Library() 14 | 15 | TRUE_RE = re.compile(r"^(true|on)$", flags=re.IGNORECASE) 16 | 17 | 18 | def convert_to_boolean(string_or_boolean): 19 | if isinstance(string_or_boolean, bool): 20 | return string_or_boolean 21 | if isinstance(string_or_boolean, str): 22 | return bool(TRUE_RE.match(string_or_boolean)) 23 | 24 | 25 | class SpurlURLBuilder: 26 | def __init__(self, args, context, tags, filters): 27 | self.args = args 28 | self.context = context 29 | self.tags = tags 30 | self.filters = filters 31 | self.autoescape = self.context.autoescape 32 | self.url = URLObject() 33 | 34 | def build(self): 35 | for argument, value in self.args: 36 | self.handle_argument(argument, value) 37 | 38 | self.set_sensible_defaults() 39 | 40 | url = str(self.url) 41 | 42 | if self.autoescape: 43 | url = escape(url) 44 | 45 | return url 46 | 47 | def handle_argument(self, argument, value): 48 | argument = smart_str(argument, "ascii") 49 | handler_name = "handle_%s" % argument 50 | handler = getattr(self, handler_name, None) 51 | 52 | if handler is not None: 53 | value = value.resolve(self.context) 54 | handler(value) 55 | 56 | def handle_base(self, value): 57 | base = self.prepare_value(value) 58 | self.url = URLObject(base) 59 | 60 | def handle_auth(self, value): 61 | auth = self.prepare_value(value) 62 | self.url = self.url.with_auth(*auth.split(":", 1)) 63 | 64 | def handle_secure(self, value): 65 | is_secure = convert_to_boolean(value) 66 | scheme = "https" if is_secure else "http" 67 | self.url = self.url.with_scheme(scheme) 68 | 69 | def handle_query(self, value): 70 | query = self.prepare_value(value) 71 | if isinstance(query, dict): 72 | query = QueryString().set_params(**query) 73 | self.url = self.url.with_query(QueryString(query)) 74 | 75 | def handle_query_from(self, value): 76 | url = URLObject(value) 77 | self.url = self.url.with_query(url.query) 78 | 79 | def handle_add_query(self, value): 80 | query_to_add = self.prepare_value(value) 81 | if isinstance(query_to_add, str): 82 | query_to_add = QueryString(query_to_add).dict 83 | self.url = self.url.add_query_params(**query_to_add) 84 | 85 | def handle_add_query_from(self, value): 86 | url = URLObject(value) 87 | self.url = self.url.add_query_params(**url.query.dict) 88 | 89 | def handle_set_query(self, value): 90 | query_to_set = self.prepare_value(value) 91 | if isinstance(query_to_set, str): 92 | query_to_set = QueryString(query_to_set).dict 93 | self.url = self.url.set_query_params(**query_to_set) 94 | 95 | def handle_set_query_from(self, value): 96 | url = URLObject(value) 97 | self.url = self.url.set_query_params(**url.query.dict) 98 | 99 | def handle_remove_query_param(self, value): 100 | query_to_remove = self.prepare_value(value) 101 | if "=" in query_to_remove: 102 | k, v = query_to_remove.split("=", 1) 103 | self.url = self.url.del_query_param_value(k, v) 104 | else: 105 | self.url = self.url.del_query_param(query_to_remove) 106 | 107 | def handle_remove_query_params_except(self, value): 108 | params_to_keep = self.prepare_value(value).split(" ") 109 | params_to_remove = [pair[0] for pair in self.url.query_list if pair[0] not in params_to_keep] 110 | self.url = self.url.with_query(self.url.query.del_params(params_to_remove)) 111 | 112 | def handle_toggle_query(self, value): 113 | query_to_toggle = self.prepare_value(value) 114 | if isinstance(query_to_toggle, str): 115 | query_to_toggle = QueryString(query_to_toggle).dict 116 | current_query = self.url.query.dict 117 | for key, value in list(query_to_toggle.items()): 118 | if isinstance(value, str): 119 | value = value.split(",") 120 | first, second = value 121 | if key in current_query and first == current_query[key]: 122 | self.url = self.url.set_query_param(key, second) 123 | else: 124 | self.url = self.url.set_query_param(key, first) 125 | 126 | def handle_scheme(self, value): 127 | self.url = self.url.with_scheme(value) 128 | 129 | def handle_scheme_from(self, value): 130 | url = URLObject(value) 131 | self.url = self.url.with_scheme(url.scheme) 132 | 133 | def handle_host(self, value): 134 | host = self.prepare_value(value) 135 | self.url = self.url.with_hostname(host) 136 | 137 | def handle_host_from(self, value): 138 | url = URLObject(value) 139 | self.url = self.url.with_hostname(url.hostname) 140 | 141 | def handle_path(self, value): 142 | path = self.prepare_value(value) 143 | self.url = self.url.with_path(path) 144 | 145 | def handle_path_from(self, value): 146 | url = URLObject(value) 147 | self.url = self.url.with_path(url.path) 148 | 149 | def handle_add_path(self, value): 150 | path_to_add = self.prepare_value(value) 151 | self.url = self.url.add_path(path_to_add) 152 | 153 | def handle_add_path_from(self, value): 154 | url = URLObject(value) 155 | path_to_add = url.path 156 | if path_to_add.startswith("/"): 157 | path_to_add = path_to_add[1:] 158 | self.url = self.url.add_path(path_to_add) 159 | 160 | def handle_fragment(self, value): 161 | fragment = self.prepare_value(value) 162 | self.url = self.url.with_fragment(fragment) 163 | 164 | def handle_fragment_from(self, value): 165 | url = URLObject(value) 166 | self.url = self.url.with_fragment(url.fragment) 167 | 168 | def handle_port(self, value): 169 | self.url = self.url.with_port(int(value)) 170 | 171 | def handle_port_from(self, value): 172 | url = URLObject(value) 173 | self.url = self.url.with_port(url.port) 174 | 175 | def handle_autoescape(self, value): 176 | self.autoescape = convert_to_boolean(value) 177 | 178 | def set_sensible_defaults(self): 179 | if self.url.hostname and not self.url.scheme: 180 | self.url = self.url.with_scheme("http") 181 | 182 | def prepare_value(self, value): 183 | """Prepare a value by unescaping embedded template tags 184 | and rendering through Django's template system""" 185 | if isinstance(value, str): 186 | value = self.render_template(self.unescape_tags(value)) 187 | return value 188 | 189 | def unescape_tags(self, template_string): 190 | r"""Spurl allows the use of templatetags inside templatetags, if 191 | the inner templatetags are escaped - {\% and %\}""" 192 | return template_string.replace(r"{\%", "{%").replace(r"%\}", "%}") 193 | 194 | def compile_string(self, template_string, origin, template_debug=False): 195 | """Re-implementation of django.template.base.compile_string 196 | that takes into account the tags and filter of the parser 197 | that rendered the parent template""" 198 | if template_debug is True: 199 | if django.VERSION < (1, 9): 200 | from django.template.debug import DebugLexer, DebugParser 201 | 202 | lexer_class, parser_class = DebugLexer, DebugParser 203 | else: 204 | from django.template.base import DebugLexer 205 | 206 | lexer_class, parser_class = DebugLexer, Parser 207 | else: 208 | lexer_class, parser_class = Lexer, Parser 209 | if django.VERSION < (1, 9): 210 | lexer = lexer_class(template_string, origin) 211 | else: 212 | lexer = lexer_class(template_string) 213 | parser = parser_class(lexer.tokenize()) 214 | 215 | # Attach the tags and filters from the parent parser 216 | parser.tags = self.tags 217 | parser.filters = self.filters 218 | 219 | return parser.parse() 220 | 221 | def render_template(self, template_string): 222 | """Used to render an "inner" template, ie one which 223 | is passed as an argument to spurl""" 224 | original_autoescape = self.context.autoescape 225 | self.context.autoescape = False 226 | 227 | template = Template("") 228 | template_debug = getattr( 229 | settings, "TEMPLATE_DEBUG", template.engine.debug if hasattr(template, "engine") else False 230 | ) 231 | if template_debug is True: 232 | origin = Origin(template_string) 233 | else: 234 | origin = None 235 | 236 | template.nodelist = self.compile_string(template_string, origin, template_debug) 237 | 238 | rendered = template.render(self.context) 239 | self.context.autoescape = original_autoescape 240 | return rendered 241 | 242 | 243 | class SpurlNode(Node): 244 | def __init__(self, args, tags, filters, asvar=None): 245 | self.args = args 246 | self.asvar = asvar 247 | self.tags = tags 248 | self.filters = filters 249 | 250 | def render(self, context): 251 | builder = SpurlURLBuilder(self.args, context, self.tags, self.filters) 252 | url = builder.build() 253 | 254 | if self.asvar: 255 | context[self.asvar] = url 256 | return "" 257 | 258 | return url 259 | 260 | 261 | @register.tag 262 | def spurl(parser, token): 263 | bits = token.split_contents() 264 | if len(bits) < 2: 265 | raise TemplateSyntaxError("'spurl' takes at least one argument") 266 | 267 | args = [] 268 | asvar = None 269 | bits = bits[1:] 270 | 271 | if len(bits) >= 2 and bits[-2] == "as": 272 | asvar = bits[-1] 273 | bits = bits[:-2] 274 | 275 | for bit in bits: 276 | name, value = kwarg_re.match(bit).groups() 277 | if not (name and value): 278 | raise TemplateSyntaxError("Malformed arguments to spurl tag") 279 | args.append((name, parser.compile_filter(value))) 280 | return SpurlNode(args, parser.tags, parser.filters, asvar) 281 | -------------------------------------------------------------------------------- /spurl/tests.py: -------------------------------------------------------------------------------- 1 | import django 2 | import nose 3 | from django.conf import settings 4 | from django.http import HttpResponse 5 | from django.template import Context, Template, TemplateSyntaxError 6 | from django.urls import path 7 | 8 | from spurl.templatetags.spurl import convert_to_boolean 9 | 10 | # This file acts as a urlconf 11 | urlpatterns = [path("test/", lambda r: HttpResponse("ok"), name="test")] 12 | 13 | # bootstrap django 14 | configure_kwargs = { 15 | "ROOT_URLCONF": "spurl.tests", 16 | "INSTALLED_APPS": ["spurl.tests"], 17 | } 18 | configure_kwargs["TEMPLATES"] = [ 19 | { 20 | "BACKEND": "django.template.backends.django.DjangoTemplates", 21 | "OPTIONS": { 22 | "builtins": ["spurl.templatetags.spurl"], 23 | }, 24 | } 25 | ] 26 | settings.configure(**configure_kwargs) 27 | 28 | if django.VERSION >= (1, 7): 29 | django.setup() 30 | 31 | 32 | def render(template_string, dictionary=None, autoescape=False): 33 | """ 34 | Render a template from the supplied string, with optional context data. 35 | 36 | This differs from Django's normal template system in that autoescaping 37 | is disabled by default. This is simply to make the tests below easier 38 | to read and write. You can re-enable the default behavior by passing True 39 | as the value of the autoescape parameter 40 | """ 41 | context = Context(dictionary, autoescape=autoescape) 42 | return Template(template_string).render(context) 43 | 44 | 45 | def test_convert_argument_value_to_boolean(): 46 | assert convert_to_boolean(True) is True 47 | assert convert_to_boolean(False) is False 48 | assert convert_to_boolean("True") is True 49 | assert convert_to_boolean("true") is True 50 | assert convert_to_boolean("On") is True 51 | assert convert_to_boolean("on") is True 52 | assert convert_to_boolean("False") is False 53 | assert convert_to_boolean("false") is False 54 | assert convert_to_boolean("Off") is False 55 | assert convert_to_boolean("off") is False 56 | assert convert_to_boolean("randomstring") is False 57 | 58 | 59 | @nose.tools.raises(TemplateSyntaxError) 60 | def test_noargs_raises_exception(): 61 | render("""{% spurl %}""") 62 | 63 | 64 | @nose.tools.raises(TemplateSyntaxError) 65 | def test_malformed_args_raises_exception(): 66 | render("""{% spurl something %}""") 67 | 68 | 69 | def test_passthrough(): 70 | template = """{% spurl base="http://www.google.com" %}""" 71 | rendered = render(template) 72 | assert rendered == "http://www.google.com" 73 | 74 | 75 | def test_url_in_variable(): 76 | template = """{% spurl base=myurl %}""" 77 | data = {"myurl": "http://www.google.com"} 78 | rendered = render(template, data) 79 | assert rendered == "http://www.google.com" 80 | 81 | 82 | def test_make_secure(): 83 | template = """{% spurl base="http://www.google.com" secure="True" %}""" 84 | rendered = render(template) 85 | assert rendered == "https://www.google.com" 86 | 87 | 88 | def test_make_secure_with_variable(): 89 | template = """{% spurl base=myurl secure=is_secure %}""" 90 | data = {"myurl": "http://www.google.com", "is_secure": True} 91 | rendered = render(template, data) 92 | assert rendered == "https://www.google.com" 93 | 94 | 95 | def test_make_insecure(): 96 | template = """{% spurl base="https://www.google.com" secure="False" %}""" 97 | rendered = render(template) 98 | assert rendered == "http://www.google.com" 99 | 100 | 101 | def test_make_insecure_with_variable(): 102 | template = """{% spurl base=myurl secure=is_secure %}""" 103 | data = {"myurl": "https://www.google.com", "is_secure": False} 104 | rendered = render(template, data) 105 | assert rendered == "http://www.google.com" 106 | 107 | 108 | def test_set_query_from_string(): 109 | template = """{% spurl base="http://www.google.com" query="foo=bar&bar=foo" %}""" 110 | rendered = render(template) 111 | assert rendered == "http://www.google.com?foo=bar&bar=foo" 112 | 113 | 114 | def test_set_query_from_string_with_variable(): 115 | template = """{% spurl base=myurl query=myquery %}""" 116 | data = {"myurl": "http://www.google.com", "myquery": "foo=bar&bar=foo"} 117 | rendered = render(template, data) 118 | assert rendered == "http://www.google.com?foo=bar&bar=foo" 119 | 120 | 121 | def test_set_query_from_dict_with_variable(): 122 | template = """{% spurl base=myurl query=myquery %}""" 123 | data = { 124 | "myurl": "http://www.google.com", 125 | "myquery": {"foo": "bar", "bar": "foo"}, 126 | } 127 | rendered = render(template, data) 128 | if "http://www.google.com?" not in rendered: 129 | raise AssertionError 130 | assert "foo=bar" in rendered and "bar=foo" in rendered 131 | 132 | 133 | def test_set_query_from_template_variables(): 134 | template = """{% spurl base=myurl query="foo={{ first_var }}&bar={{ second_var }}" %}""" 135 | data = { 136 | "myurl": "http://www.google.com", 137 | "first_var": "bar", 138 | "second_var": "foo", 139 | } 140 | rendered = render(template, data) 141 | if "http://www.google.com?" not in rendered: 142 | raise AssertionError 143 | assert "foo=bar" in rendered and "bar=foo" in rendered 144 | 145 | 146 | def test_set_query_from_template_variables_not_double_escaped(): 147 | template = """{% spurl base="http://www.google.com" query="{{ query }}" %}""" 148 | data = {"query": "foo=bar&bar=foo"} 149 | rendered = render(template, data) 150 | assert rendered == "http://www.google.com?foo=bar&bar=foo" 151 | 152 | 153 | def test_set_query_removes_existing_query(): 154 | template = """{% spurl base="http://www.google.com?something=somethingelse" query="foo=bar&bar=foo" %}""" 155 | rendered = render(template) 156 | assert rendered == "http://www.google.com?foo=bar&bar=foo" 157 | 158 | 159 | def test_query_from(): 160 | template = """{% spurl base="http://www.google.com/" query_from=url %}""" 161 | data = {"url": "http://example.com/some/path/?foo=bar&bar=foo"} 162 | rendered = render(template, data) 163 | assert rendered == "http://www.google.com/?foo=bar&bar=foo" 164 | 165 | 166 | def test_add_to_query_from_string(): 167 | template = """{% spurl base="http://www.google.com?something=somethingelse" add_query="foo=bar&bar=foo" %}""" 168 | rendered = render(template) 169 | if "http://www.google.com?something=somethingelse" not in rendered: 170 | raise AssertionError 171 | assert "&foo=bar" in rendered and "&bar=foo" in rendered 172 | 173 | 174 | def test_add_to_query_from_dict_with_variable(): 175 | template = """{% spurl base=myurl add_query=myquery %}""" 176 | data = { 177 | "myurl": "http://www.google.com?something=somethingelse", 178 | "myquery": {"foo": "bar", "bar": "foo"}, 179 | } 180 | rendered = render(template, data) 181 | if "http://www.google.com?something=somethingelse" not in rendered: 182 | raise AssertionError 183 | assert "&foo=bar" in rendered and "&bar=foo" in rendered 184 | 185 | 186 | def test_multiple_add_query(): 187 | template = """{% spurl base="http://www.google.com/" add_query="foo=bar" add_query="bar=baz" %}""" 188 | rendered = render(template) 189 | assert rendered == "http://www.google.com/?foo=bar&bar=baz" 190 | 191 | 192 | def test_add_to_query_from_template_variables(): 193 | template = """{% spurl base="http://www.google.com/?foo=bar" add_query="bar={{ var }}" %}""" 194 | data = {"var": "baz"} 195 | rendered = render(template, data) 196 | assert rendered == "http://www.google.com/?foo=bar&bar=baz" 197 | 198 | 199 | def test_add_query_from(): 200 | template = """{% spurl base="http://www.google.com/?bla=bla&flub=flub" add_query_from=url %}""" 201 | data = {"url": "http://example.com/some/path/?foo=bar&bar=foo"} 202 | rendered = render(template, data) 203 | if "http://www.google.com/?bla=bla&flub=flub" not in rendered: 204 | raise AssertionError 205 | assert "&foo=bar" in rendered and "&bar=foo" in rendered 206 | 207 | 208 | def test_set_query_param_from_string(): 209 | template = """{% spurl base="http://www.google.com?something=somethingelse" set_query="something=foo&somethingelse=bar" %}""" 210 | rendered = render(template) 211 | if "http://www.google.com?" not in rendered: 212 | raise AssertionError 213 | assert "somethingelse=bar" in rendered and "something=foo" in rendered 214 | 215 | 216 | def test_set_query_param_from_dict_with_variable(): 217 | template = """{% spurl base=myurl set_query=myquery %}""" 218 | data = { 219 | "myurl": "http://www.google.com?something=somethingelse", 220 | "myquery": {"something": "foo", "somethingelse": "bar"}, 221 | } 222 | rendered = render(template, data) 223 | if "http://www.google.com?" not in rendered: 224 | raise AssertionError 225 | assert "somethingelse=bar" in rendered and "something=foo" in rendered 226 | 227 | 228 | def test_toggle_query(): 229 | template = """{% spurl base="http://www.google.com/?foo=bar" toggle_query="bar=first,second" %}""" 230 | rendered = render(template) 231 | assert rendered == "http://www.google.com/?foo=bar&bar=first" 232 | 233 | template = """{% spurl base="http://www.google.com/?foo=bar&bar=first" toggle_query="bar=first,second" %}""" 234 | rendered = render(template) 235 | assert rendered == "http://www.google.com/?foo=bar&bar=second" 236 | 237 | template = """{% spurl base="http://www.google.com/?foo=bar&bar=second" toggle_query="bar=first,second" %}""" 238 | rendered = render(template) 239 | assert rendered == "http://www.google.com/?foo=bar&bar=first" 240 | 241 | template = """{% spurl base="http://www.google.com/?foo=bar&bar=first" toggle_query=to_toggle %}""" 242 | data = {"to_toggle": {"bar": ("first", "second")}} 243 | rendered = render(template, data) 244 | assert rendered == "http://www.google.com/?foo=bar&bar=second" 245 | 246 | template = """{% spurl base="http://www.google.com/?foo=bar&bar=second" toggle_query=to_toggle %}""" 247 | data = {"to_toggle": {"bar": ("first", "second")}} 248 | rendered = render(template, data) 249 | assert rendered == "http://www.google.com/?foo=bar&bar=first" 250 | 251 | template = """{% spurl base="http://www.google.com/?foo=bar&bar=javascript" toggle_query=to_toggle %}""" 252 | data = {"to_toggle": {"bar": ("java", "javascript")}} 253 | rendered = render(template, data) 254 | assert rendered == "http://www.google.com/?foo=bar&bar=java" 255 | 256 | 257 | def test_multiple_set_query(): 258 | template = """{% spurl base="http://www.google.com/?foo=test" set_query="foo=bar" set_query="bar=baz" %}""" 259 | rendered = render(template) 260 | assert rendered == "http://www.google.com/?foo=bar&bar=baz" 261 | 262 | 263 | def test_set_query_param_from_template_variables(): 264 | template = """{% spurl base="http://www.google.com/?foo=bar" set_query="foo={{ var }}" %}""" 265 | data = {"var": "baz"} 266 | rendered = render(template, data) 267 | assert rendered == "http://www.google.com/?foo=baz" 268 | 269 | 270 | def test_empty_parameters_preserved(): 271 | template = """{% spurl base="http://www.google.com/?foo=bar" set_query="bar={{ emptyvar }}" %}""" 272 | data = {} # does not contain and "emptyvar" key 273 | rendered = render(template, data) 274 | assert rendered == "http://www.google.com/?foo=bar&bar=" 275 | 276 | 277 | def test_none_values_are_removed_when_setting_query(): 278 | template = """{% spurl base="http://www.google.com/?foo=bar" set_query="bar={{ nonevar|default_if_none:'' }}" %}""" 279 | data = {"nonevar": None} 280 | rendered = render(template, data) 281 | assert rendered == "http://www.google.com/?foo=bar&bar=" 282 | 283 | 284 | def test_set_query_from(): 285 | template = """{% spurl base="http://www.google.com?bla=bla&foo=something" set_query_from=url %}""" 286 | data = {"url": "http://example.com/some/path?foo=bar&bar=foo"} 287 | rendered = render(template, data) 288 | if "http://www.google.com?" not in rendered: 289 | raise AssertionError 290 | assert "bla=bla" in rendered and "foo=bar" in rendered and "bar=foo" in rendered 291 | 292 | 293 | def test_none_values_are_removed_when_adding_query(): 294 | template = """{% spurl base="http://www.google.com/?foo=bar" add_query="bar={{ nonevar|default_if_none:'' }}" %}""" 295 | data = {"nonevar": None} 296 | rendered = render(template, data) 297 | assert rendered == "http://www.google.com/?foo=bar&bar=" 298 | 299 | 300 | def test_remove_from_query(): 301 | template = """{% spurl base="http://www.google.com/?foo=bar&bar=baz" remove_query_param="foo" %}""" 302 | rendered = render(template) 303 | assert rendered == "http://www.google.com/?bar=baz" 304 | 305 | 306 | def test_remove_except_from_query(): 307 | template = """{% spurl base="http://www.google.com/?foo=bar&bar=baz&baz=foo" remove_query_params_except="baz" %}""" 308 | rendered = render(template) 309 | assert rendered == "http://www.google.com/?baz=foo" 310 | 311 | 312 | def test_remove_except_from_query_with_template_variable(): 313 | template = ( 314 | """{% spurl base="http://www.google.com/?foo=bar&bar=baz&baz=foo" remove_query_params_except="{{ baz }}" %}""" 315 | ) 316 | data = {"baz": "baz"} 317 | rendered = render(template, data) 318 | assert rendered == "http://www.google.com/?baz=foo" 319 | 320 | 321 | def test_remove_from_query_with_value(): 322 | template = """{% spurl base="http://www.google.com/?foo=bar&bar=baz&bar=foo" remove_query_param="bar=foo" %}""" 323 | rendered = render(template) 324 | assert rendered == "http://www.google.com/?foo=bar&bar=baz" 325 | 326 | 327 | def test_remove_multiple_from_query_with_value(): 328 | template = """{% spurl base="http://www.google.com/?foo=bar&bar=baz&bar=foo&foo=baz" remove_query_param="bar=foo" remove_query_param="foo=baz" %}""" 329 | rendered = render(template) 330 | assert rendered == "http://www.google.com/?foo=bar&bar=baz" 331 | 332 | 333 | def test_remove_multiple_params(): 334 | template = """{% spurl base="http://www.google.com/?foo=bar&bar=baz" remove_query_param="foo" remove_query_param="bar" %}""" 335 | rendered = render(template) 336 | assert rendered == "http://www.google.com/" 337 | 338 | 339 | def test_remove_from_query_with_value_from_template_variable(): 340 | template = """{% spurl base="http://www.google.com/?foo=bar&bar=baz&bar=foo&foo=baz" remove_query_param="{{ foo }}={{ baz }}" remove_query_param="{{ bar }}={{ foo }}" %}""" 341 | data = {"foo": "foo", "bar": "bar", "baz": "baz"} 342 | rendered = render(template, data) 343 | assert rendered == "http://www.google.com/?foo=bar&bar=baz" 344 | 345 | 346 | def test_remove_param_from_template_variable(): 347 | template = """{% spurl base="http://www.google.com/?foo=bar&bar=baz" remove_query_param="{{ foo }}" remove_query_param="{{ bar }}" %}""" 348 | data = {"foo": "foo", "bar": "bar"} 349 | rendered = render(template, data) 350 | assert rendered == "http://www.google.com/" 351 | 352 | 353 | def test_override_scheme(): 354 | template = """{% spurl base="http://google.com" scheme="ftp" %}""" 355 | rendered = render(template) 356 | assert rendered == "ftp://google.com" 357 | 358 | 359 | def test_scheme_from(): 360 | template = """{% spurl base="http://www.google.com/?bla=bla&foo=bar" scheme_from=url %}""" 361 | data = {"url": "https://example.com/some/path/?foo=bar&bar=foo"} 362 | rendered = render(template, data) 363 | assert rendered == "https://www.google.com/?bla=bla&foo=bar" 364 | 365 | 366 | def test_override_host(): 367 | template = """{% spurl base="http://www.google.com/some/path/" host="www.example.com" %}""" 368 | rendered = render(template) 369 | assert rendered == "http://www.example.com/some/path/" 370 | 371 | 372 | def test_host_from(): 373 | template = """{% spurl base="http://www.google.com/?bla=bla&foo=bar" host_from=url %}""" 374 | data = {"url": "https://example.com/some/path/?foo=bar&bar=foo"} 375 | rendered = render(template, data) 376 | assert rendered == "http://example.com/?bla=bla&foo=bar" 377 | 378 | 379 | def test_override_path(): 380 | template = """{% spurl base="http://www.google.com/some/path/" path="/another/different/one/" %}""" 381 | rendered = render(template) 382 | assert rendered == "http://www.google.com/another/different/one/" 383 | 384 | 385 | def test_path_from(): 386 | template = """{% spurl base="http://www.google.com/original/?bla=bla&foo=bar" path_from=url %}""" 387 | data = {"url": "https://example.com/some/path/?foo=bar&bar=foo"} 388 | rendered = render(template, data) 389 | assert rendered == "http://www.google.com/some/path/?bla=bla&foo=bar" 390 | 391 | 392 | def test_add_path(): 393 | template = """{% spurl base="http://www.google.com/some/path/" add_path="another/" %}""" 394 | rendered = render(template) 395 | assert rendered == "http://www.google.com/some/path/another/" 396 | 397 | 398 | def test_multiple_add_path(): 399 | template = """{% spurl base="http://www.google.com/" add_path="some" add_path="another/" %}""" 400 | rendered = render(template) 401 | assert rendered == "http://www.google.com/some/another/" 402 | 403 | 404 | def test_multiple_add_path_from_template_variables(): 405 | """Usage example for building media urls""" 406 | template = """{% spurl base=STATIC_URL add_path="javascript" add_path="lib" add_path="jquery.js" %}""" 407 | data = {"STATIC_URL": "http://cdn.example.com"} 408 | rendered = render(template, data) 409 | assert rendered == "http://cdn.example.com/javascript/lib/jquery.js" 410 | 411 | 412 | def test_add_path_from(): 413 | template = """{% spurl base="http://www.google.com/original/?bla=bla&foo=bar" add_path_from=url %}""" 414 | data = {"url": "https://example.com/some/path/?foo=bar&bar=foo"} 415 | rendered = render(template, data) 416 | assert rendered == "http://www.google.com/original/some/path/?bla=bla&foo=bar" 417 | 418 | 419 | def test_override_fragment(): 420 | template = """{% spurl base="http://www.google.com/#somefragment" fragment="someotherfragment" %}""" 421 | rendered = render(template) 422 | assert rendered == "http://www.google.com/#someotherfragment" 423 | 424 | 425 | def test_fragment_from(): 426 | template = """{% spurl base="http://www.google.com/?bla=bla&foo=bar#fragment" fragment_from=url %}""" 427 | data = {"url": "https://example.com/some/path/?foo=bar&bar=foo#newfragment"} 428 | rendered = render(template, data) 429 | assert rendered == "http://www.google.com/?bla=bla&foo=bar#newfragment" 430 | 431 | 432 | def test_override_port(): 433 | template = """{% spurl base="http://www.google.com:80" port="8080" %}""" 434 | rendered = render(template) 435 | assert rendered == "http://www.google.com:8080" 436 | 437 | 438 | def test_port_from(): 439 | template = """{% spurl base="http://www.google.com:8000/?bla=bla&foo=bar" port_from=url %}""" 440 | data = {"url": "https://example.com:8888/some/path/?foo=bar&bar=foo"} 441 | rendered = render(template, data) 442 | assert rendered == "http://www.google.com:8888/?bla=bla&foo=bar" 443 | 444 | 445 | def test_build_complete_url(): 446 | template = ( 447 | """{% spurl scheme="http" host="www.google.com" path="/some/path/" port="8080" fragment="somefragment" %}""" 448 | ) 449 | rendered = render(template) 450 | assert rendered == "http://www.google.com:8080/some/path/#somefragment" 451 | 452 | 453 | def test_sensible_defaults(): 454 | template = """{% spurl path="/some/path/" %}""" 455 | rendered = render(template) 456 | assert rendered == "/some/path/" 457 | 458 | template = """{% spurl path="/some/path/" host="www.google.com" %}""" 459 | rendered = render(template) 460 | assert rendered == "http://www.google.com/some/path/" 461 | 462 | 463 | def test_autoescaping(): 464 | template = ( 465 | """{% spurl base="http://www.google.com" query="a=b" add_query="c=d" add_query="e=f" fragment="frag" %}""" 466 | ) 467 | rendered = render(template, autoescape=True) # Ordinarily, templates will be autoescaped by default 468 | assert rendered == "http://www.google.com?a=b&c=d&e=f#frag" 469 | 470 | 471 | def test_disable_autoescaping_with_parameter(): 472 | template = """{% spurl base="http://www.google.com" query="a=b" add_query="c=d" autoescape="False" %}""" 473 | rendered = render(template, autoescape=True) 474 | assert rendered == "http://www.google.com?a=b&c=d" 475 | 476 | 477 | def test_url_as_template_variable(): 478 | template = """{% spurl base="http://www.google.com" as foo %}The url is {{ foo }}""" 479 | rendered = render(template) 480 | assert rendered == "The url is http://www.google.com" 481 | 482 | 483 | def test_reversing_inside_spurl_tag(): 484 | template = r"""{% spurl base="http://www.google.com/" path="{\% url 'test' %\}" %}""" 485 | if django.VERSION < (1, 9): 486 | template = r"""{% load url from future %}{% spurl base="http://www.google.com/" path="{\% url 'test' %\}" %}""" 487 | else: 488 | template = r"""{% spurl base="http://www.google.com/" path="{\% url 'test' %\}" %}""" 489 | rendered = render(template) 490 | assert rendered == "http://www.google.com/test/" 491 | 492 | if django.VERSION < (1, 9): 493 | template = ( 494 | r"""{% load url from future %}{% spurl base="http://www.google.com/" query="next={\% url 'test' %\}" %}""" 495 | ) 496 | else: 497 | template = r"""{% spurl base="http://www.google.com/" query="next={\% url 'test' %\}" %}""" 498 | rendered = render(template) 499 | assert rendered == "http://www.google.com/?next=/test/" 500 | 501 | 502 | def test_xzibit(): 503 | template = r"""Yo dawg, the URL is: {% spurl base="http://www.google.com/" query="foo={\% spurl base='http://another.com' secure='true' %\}" %}""" 504 | rendered = render(template) 505 | assert rendered == "Yo dawg, the URL is: http://www.google.com/?foo=https://another.com" 506 | 507 | 508 | def test_auth_with_username_and_password(): 509 | template = """{% spurl base=myurl auth=auth %}""" 510 | data = {"myurl": "https://www.google.com", "auth": "user:pass"} 511 | rendered = render(template, data) 512 | assert rendered == "https://user:pass@www.google.com" 513 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | distribute = False 3 | envlist = 4 | py{36,37,38,39}-dj{22,30,31,32} 5 | skip_missing_interpreters = True 6 | 7 | [travis] 8 | python = 9 | 3.6: py36 10 | 3.7: py37 11 | 3.8: py38 12 | 3.9: py39 13 | 14 | [testenv] 15 | usedevelop = True 16 | extras = test 17 | deps = 18 | dj22: Django>=2.2,<3.0 19 | dj30: Django>=3.0,<3.1 20 | dj31: Django>=3.1,<3.2 21 | dj32: Django>=3.2,<3.3 22 | commands = python setup.py nosetests 23 | --------------------------------------------------------------------------------