├── .coveragerc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.rst ├── example ├── core │ ├── __init__.py │ ├── admin.py │ ├── forms.py │ ├── models.py │ ├── static │ │ └── core │ │ │ ├── css │ │ │ ├── bootstrap-responsive_2.3.2.css │ │ │ └── bootstrap_2.3.2.css │ │ │ └── js │ │ │ └── bootstrap-tab.min.js │ ├── templates │ │ └── home.html │ ├── tests.py │ └── views.py ├── example │ ├── __init__.py │ ├── settings │ │ ├── __init__.py │ │ ├── common.py │ │ └── local.py-dist │ ├── urls.py │ └── wsgi.py ├── manage.py ├── parsley └── requirements.txt ├── parsley ├── __init__.py ├── decorators.py ├── mixins.py ├── models.py ├── static │ └── parsley │ │ └── js │ │ ├── parsley.django-admin.js │ │ ├── parsley.js │ │ └── parsley.min.js ├── templatetags │ ├── __init__.py │ └── parsley.py ├── tests │ ├── __init__.py │ ├── admin.py │ ├── forms.py │ ├── models.py │ ├── tests.py │ └── views.py └── widgets.py ├── runtests.py ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = parsley 3 | branch = 1 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | *egg-info/ 3 | *.egg/ 4 | .coverage 5 | .tox 6 | htmlcov/ 7 | build/ 8 | _build/ 9 | *.pyc 10 | test.db 11 | example/example/settings/local.py 12 | /.idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 2.7 5 | - 3.4 6 | - 3.5 7 | - 3.6 8 | 9 | env: 10 | - DJANGO='Django>=1.8,<1.9' 11 | - DJANGO='Django>=1.9,<1.10' 12 | - DJANGO='Django>=1.10,<1.11' 13 | - DJANGO='Django>=1.11,<2.0' 14 | 15 | install: 16 | - pip install $DJANGO 17 | - pip install coverage coveralls 18 | 19 | script: coverage run setup.py test 20 | 21 | matrix: 22 | exclude: 23 | - python: 3.6 24 | env: DJANGO='Django>=1.8,<1.9' 25 | - python: 3.6 26 | env: DJANGO='Django>=1.9,<1.10' 27 | - python: 3.6 28 | env: DJANGO='Django>=1.10,<1.11' 29 | 30 | after_success: coveralls 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Django-parsley changelog 2 | 3 | ## 0.7 4 | 5 | - Added support for django 1.10 and 1.11 6 | - Dropped support for django 1.3 - 1.7 7 | - Fixed issue where django version would be forced to 1.8 when installing django-parsley. More details here. 8 | - Added parsleyfy template tag 9 | 10 | ## 0.6 11 | 12 | - Added checkbox and radio button validation support. 13 | - Updated parsley.js, included with this library, to 2.0.7 14 | - Changed error message attributes to confirm with parsley.js 2.x. More details here. 15 | - Changed RegexField attributes to confirm with parsley.js 2.x. More details here. 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Agiliq(http://www.agiliq.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 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. 8 | Neither the name of the Agiliq nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT HOLDER OR CONTRIBUTORS 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. 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include parsley/static * 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: check-ver 2 | 3 | all: init test 4 | 5 | init: 6 | pip install tox coverage 7 | 8 | clean: 9 | pyclean . 10 | rm -rf __pycache__ .tox dist django_parsley.egg-info 11 | 12 | test: 13 | coverage erase 14 | tox 15 | coverage html 16 | 17 | fast_test: 18 | coverage run setup.py test 19 | coverage report 20 | coverage html 21 | 22 | release: check-ver 23 | @echo ${VER} 24 | sed -i "s/^VERSION = .*/VERSION = '${VER}'/g" setup.py 25 | git add setup.py 26 | git commit -m "version bump" 27 | git tag v${VER} 28 | git push --tags 29 | python setup.py sdist upload 30 | 31 | check-ver: 32 | ifndef VER 33 | $(error VER is undefined) 34 | endif 35 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-parsley 2 | ============== 3 | 4 | .. image:: https://img.shields.io/pypi/dm/django-parsley.svg 5 | :target: https://pypi.python.org/pypi/django-parsley 6 | :alt: Downloads 7 | 8 | .. image:: https://img.shields.io/pypi/v/django-parsley.svg 9 | :target: https://pypi.python.org/pypi/django-parsley 10 | :alt: Latest Release 11 | 12 | .. image:: https://travis-ci.org/agiliq/Django-parsley.png?branch=master 13 | :target: https://travis-ci.org/agiliq/Django-parsley 14 | :alt: Build Status 15 | 16 | .. image:: https://coveralls.io/repos/agiliq/Django-parsley/badge.png?branch=master 17 | :target: https://coveralls.io/r/agiliq/Django-parsley 18 | :alt: Coverage Status 19 | 20 | What is it? 21 | ----------- 22 | 23 | `Parsleyjs`_ is a JavaScript library to do client side data validations. 24 | It does this in a non-intrusive way via adding a ``data-parsley-*`` attributes to form fields. 25 | 26 | When you define a Django form, you get server side validations for free using 27 | the form field attributes. Django-parsley adds these validations to client side, by tagging your form with ``data-parsley-*`` attributes. 28 | 29 | Parsley plays well with ``crispy-forms`` et all. 30 | 31 | Demo 32 | ---- 33 | `Demo`_ at https://agiliq.com/demo/parsley/ 34 | 35 | 36 | Installation 37 | ------------ 38 | 39 | 1. pip install ``django-parsley`` (or add to your requirements.txt) 40 | 2. add ``parsley`` to your ``INSTALLED_APPS`` (required for static files) 41 | 42 | Upgrading 43 | --------- 44 | 45 | Upgrading from 0.2 to 0.3: 46 | .......................... 47 | 48 | If you're using parsley.js 1.x, make sure to set the ``parsley_namespace`` Meta attribute 49 | to ``parsley`` for backward compatibility. 50 | 51 | .. code-block:: python 52 | 53 | class Meta: 54 | parsley_namespace = 'parsley' 55 | 56 | Usage 57 | ----- 58 | 59 | ``parsley`` provides a single class decorator called ``parsleyfy``. Decorate your ``Form`` or ``ModelForm`` with ``parsleyfy`` to get the validations. 60 | 61 | Eg. 62 | 63 | .. code-block:: python 64 | 65 | from parsley.decorators import parsleyfy 66 | 67 | 68 | @parsleyfy 69 | class FieldTypeForm(forms.Form): 70 | name = forms.CharField(min_length=3, max_length=30) 71 | url = forms.URLField() 72 | url2 = forms.URLField(required=False) 73 | email = forms.EmailField() 74 | email2 = forms.EmailField(required=False) 75 | age = forms.IntegerField() 76 | income = forms.DecimalField() 77 | 78 | Your rendered form's HTML will look like this 79 | 80 | .. code-block:: html 81 | 82 |

83 |

84 |

85 |

86 |

87 |

88 |

89 | 90 | Note the ``data-parsley-*`` attributes. 91 | 92 | You could also do 93 | 94 | .. code-block:: python 95 | 96 | FieldTypeForm = parsleyfy(FieldTypeForm) 97 | 98 | Which is the same thing. 99 | 100 | Put this form inside a 101 | 102 | .. code-block:: html 103 | 104 |
105 | {{ form.as_p }} 106 |
107 | 108 | .. note:: Make sure `jquery.js` and `parsley.js` are included in the template. 109 | 110 | Admin 111 | ----- 112 | 113 | To add parsley validations to admin, use the ``ParsleyAdminMixin`` with your ``ModelAdmin`` like so: 114 | 115 | .. code-block:: python 116 | 117 | class StudentAdmin(ParsleyAdminMixin, admin.ModelAdmin): 118 | pass 119 | 120 | .. note:: Use the `parsley.django-admin.js` helper from parsley static to auto-validate admin forms. 121 | 122 | Advanced Usage 123 | -------------- 124 | 125 | In addition to the default validators if you want to add extra client side validations 126 | or if you want to add custom validators, add a ``parsley_extras`` Meta attribute. For e.g 127 | if you wanted to add ``minlength`` and ``equalto`` validations on a ``PasswordChangeForm``: 128 | 129 | .. code-block:: python 130 | 131 | @parsleyfy 132 | class PasswordChangeForm(BasePasswordChangeForm): 133 | class Meta: 134 | parsley_extras = { 135 | 'new_password1': { 136 | 'minlength': "5", 137 | }, 138 | 'new_password2': { 139 | 'equalto': "new_password1", 140 | 'error-message': "Your passwords do not match.", 141 | }, 142 | } 143 | 144 | To use a custom namespace for parsley (e.g when using parsley with the ``data-parsley-namespace`` 145 | option) you can provide a namespace by using the ``parsley_namespace`` Meta attribute. 146 | 147 | .. code-block:: python 148 | 149 | class Meta: 150 | parsley_namespace = 'custom' 151 | 152 | License 153 | ------- 154 | 155 | 3 Clause BSD. 156 | 157 | Bug report and Help 158 | ------------------- 159 | 160 | For bug reports open a github ticket. Patches gratefully accepted. Need help? `Contact us here`_ 161 | 162 | .. _parsleyjs: http://parsleyjs.org/ 163 | .. _contact us here: http://agiliq.com/contactus 164 | .. _demo: http://agiliq.com/demo/parsley/ 165 | -------------------------------------------------------------------------------- /example/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/Django-parsley/f1901625034029a09a926e39ed7cff24b4d512b2/example/core/__init__.py -------------------------------------------------------------------------------- /example/core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from django.contrib.auth.models import User 4 | from django.contrib.auth.admin import UserAdmin 5 | 6 | from parsley.mixins import ParsleyAdminMixin 7 | 8 | 9 | class MyUserAdmin(ParsleyAdminMixin, UserAdmin): 10 | 11 | class Media: 12 | js = ( 13 | "//code.jquery.com/jquery-latest.min.js", 14 | "parsley/js/parsley.min.js", 15 | "parsley/js/parsley.django-admin.js" 16 | ) 17 | 18 | 19 | admin.site.unregister(User) 20 | admin.site.register(User, MyUserAdmin) 21 | -------------------------------------------------------------------------------- /example/core/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from parsley.decorators import parsleyfy 3 | 4 | 5 | @parsleyfy 6 | class FieldTypeForm(forms.Form): 7 | name = forms.CharField(min_length=3, max_length=30) 8 | url = forms.URLField() 9 | url2 = forms.URLField(required=False) 10 | email = forms.EmailField() 11 | email2 = forms.EmailField(required=False) 12 | age = forms.IntegerField() 13 | income = forms.DecimalField() 14 | -------------------------------------------------------------------------------- /example/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /example/core/static/core/css/bootstrap-responsive_2.3.2.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.3.2 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | 11 | .clearfix { 12 | *zoom: 1; 13 | } 14 | 15 | .clearfix:before, 16 | .clearfix:after { 17 | display: table; 18 | line-height: 0; 19 | content: ""; 20 | } 21 | 22 | .clearfix:after { 23 | clear: both; 24 | } 25 | 26 | .hide-text { 27 | font: 0/0 a; 28 | color: transparent; 29 | text-shadow: none; 30 | background-color: transparent; 31 | border: 0; 32 | } 33 | 34 | .input-block-level { 35 | display: block; 36 | width: 100%; 37 | min-height: 30px; 38 | -webkit-box-sizing: border-box; 39 | -moz-box-sizing: border-box; 40 | box-sizing: border-box; 41 | } 42 | 43 | @-ms-viewport { 44 | width: device-width; 45 | } 46 | 47 | .hidden { 48 | display: none; 49 | visibility: hidden; 50 | } 51 | 52 | .visible-phone { 53 | display: none !important; 54 | } 55 | 56 | .visible-tablet { 57 | display: none !important; 58 | } 59 | 60 | .hidden-desktop { 61 | display: none !important; 62 | } 63 | 64 | .visible-desktop { 65 | display: inherit !important; 66 | } 67 | 68 | @media (min-width: 768px) and (max-width: 979px) { 69 | .hidden-desktop { 70 | display: inherit !important; 71 | } 72 | .visible-desktop { 73 | display: none !important ; 74 | } 75 | .visible-tablet { 76 | display: inherit !important; 77 | } 78 | .hidden-tablet { 79 | display: none !important; 80 | } 81 | } 82 | 83 | @media (max-width: 767px) { 84 | .hidden-desktop { 85 | display: inherit !important; 86 | } 87 | .visible-desktop { 88 | display: none !important; 89 | } 90 | .visible-phone { 91 | display: inherit !important; 92 | } 93 | .hidden-phone { 94 | display: none !important; 95 | } 96 | } 97 | 98 | .visible-print { 99 | display: none !important; 100 | } 101 | 102 | @media print { 103 | .visible-print { 104 | display: inherit !important; 105 | } 106 | .hidden-print { 107 | display: none !important; 108 | } 109 | } 110 | 111 | @media (min-width: 1200px) { 112 | .row { 113 | margin-left: -30px; 114 | *zoom: 1; 115 | } 116 | .row:before, 117 | .row:after { 118 | display: table; 119 | line-height: 0; 120 | content: ""; 121 | } 122 | .row:after { 123 | clear: both; 124 | } 125 | [class*="span"] { 126 | float: left; 127 | min-height: 1px; 128 | margin-left: 30px; 129 | } 130 | .container, 131 | .navbar-static-top .container, 132 | .navbar-fixed-top .container, 133 | .navbar-fixed-bottom .container { 134 | width: 1170px; 135 | } 136 | .span12 { 137 | width: 1170px; 138 | } 139 | .span11 { 140 | width: 1070px; 141 | } 142 | .span10 { 143 | width: 970px; 144 | } 145 | .span9 { 146 | width: 870px; 147 | } 148 | .span8 { 149 | width: 770px; 150 | } 151 | .span7 { 152 | width: 670px; 153 | } 154 | .span6 { 155 | width: 570px; 156 | } 157 | .span5 { 158 | width: 470px; 159 | } 160 | .span4 { 161 | width: 370px; 162 | } 163 | .span3 { 164 | width: 270px; 165 | } 166 | .span2 { 167 | width: 170px; 168 | } 169 | .span1 { 170 | width: 70px; 171 | } 172 | .offset12 { 173 | margin-left: 1230px; 174 | } 175 | .offset11 { 176 | margin-left: 1130px; 177 | } 178 | .offset10 { 179 | margin-left: 1030px; 180 | } 181 | .offset9 { 182 | margin-left: 930px; 183 | } 184 | .offset8 { 185 | margin-left: 830px; 186 | } 187 | .offset7 { 188 | margin-left: 730px; 189 | } 190 | .offset6 { 191 | margin-left: 630px; 192 | } 193 | .offset5 { 194 | margin-left: 530px; 195 | } 196 | .offset4 { 197 | margin-left: 430px; 198 | } 199 | .offset3 { 200 | margin-left: 330px; 201 | } 202 | .offset2 { 203 | margin-left: 230px; 204 | } 205 | .offset1 { 206 | margin-left: 130px; 207 | } 208 | .row-fluid { 209 | width: 100%; 210 | *zoom: 1; 211 | } 212 | .row-fluid:before, 213 | .row-fluid:after { 214 | display: table; 215 | line-height: 0; 216 | content: ""; 217 | } 218 | .row-fluid:after { 219 | clear: both; 220 | } 221 | .row-fluid [class*="span"] { 222 | display: block; 223 | float: left; 224 | width: 100%; 225 | min-height: 30px; 226 | margin-left: 2.564102564102564%; 227 | *margin-left: 2.5109110747408616%; 228 | -webkit-box-sizing: border-box; 229 | -moz-box-sizing: border-box; 230 | box-sizing: border-box; 231 | } 232 | .row-fluid [class*="span"]:first-child { 233 | margin-left: 0; 234 | } 235 | .row-fluid .controls-row [class*="span"] + [class*="span"] { 236 | margin-left: 2.564102564102564%; 237 | } 238 | .row-fluid .span12 { 239 | width: 100%; 240 | *width: 99.94680851063829%; 241 | } 242 | .row-fluid .span11 { 243 | width: 91.45299145299145%; 244 | *width: 91.39979996362975%; 245 | } 246 | .row-fluid .span10 { 247 | width: 82.90598290598291%; 248 | *width: 82.8527914166212%; 249 | } 250 | .row-fluid .span9 { 251 | width: 74.35897435897436%; 252 | *width: 74.30578286961266%; 253 | } 254 | .row-fluid .span8 { 255 | width: 65.81196581196582%; 256 | *width: 65.75877432260411%; 257 | } 258 | .row-fluid .span7 { 259 | width: 57.26495726495726%; 260 | *width: 57.21176577559556%; 261 | } 262 | .row-fluid .span6 { 263 | width: 48.717948717948715%; 264 | *width: 48.664757228587014%; 265 | } 266 | .row-fluid .span5 { 267 | width: 40.17094017094017%; 268 | *width: 40.11774868157847%; 269 | } 270 | .row-fluid .span4 { 271 | width: 31.623931623931625%; 272 | *width: 31.570740134569924%; 273 | } 274 | .row-fluid .span3 { 275 | width: 23.076923076923077%; 276 | *width: 23.023731587561375%; 277 | } 278 | .row-fluid .span2 { 279 | width: 14.52991452991453%; 280 | *width: 14.476723040552828%; 281 | } 282 | .row-fluid .span1 { 283 | width: 5.982905982905983%; 284 | *width: 5.929714493544281%; 285 | } 286 | .row-fluid .offset12 { 287 | margin-left: 105.12820512820512%; 288 | *margin-left: 105.02182214948171%; 289 | } 290 | .row-fluid .offset12:first-child { 291 | margin-left: 102.56410256410257%; 292 | *margin-left: 102.45771958537915%; 293 | } 294 | .row-fluid .offset11 { 295 | margin-left: 96.58119658119658%; 296 | *margin-left: 96.47481360247316%; 297 | } 298 | .row-fluid .offset11:first-child { 299 | margin-left: 94.01709401709402%; 300 | *margin-left: 93.91071103837061%; 301 | } 302 | .row-fluid .offset10 { 303 | margin-left: 88.03418803418803%; 304 | *margin-left: 87.92780505546462%; 305 | } 306 | .row-fluid .offset10:first-child { 307 | margin-left: 85.47008547008548%; 308 | *margin-left: 85.36370249136206%; 309 | } 310 | .row-fluid .offset9 { 311 | margin-left: 79.48717948717949%; 312 | *margin-left: 79.38079650845607%; 313 | } 314 | .row-fluid .offset9:first-child { 315 | margin-left: 76.92307692307693%; 316 | *margin-left: 76.81669394435352%; 317 | } 318 | .row-fluid .offset8 { 319 | margin-left: 70.94017094017094%; 320 | *margin-left: 70.83378796144753%; 321 | } 322 | .row-fluid .offset8:first-child { 323 | margin-left: 68.37606837606839%; 324 | *margin-left: 68.26968539734497%; 325 | } 326 | .row-fluid .offset7 { 327 | margin-left: 62.393162393162385%; 328 | *margin-left: 62.28677941443899%; 329 | } 330 | .row-fluid .offset7:first-child { 331 | margin-left: 59.82905982905982%; 332 | *margin-left: 59.72267685033642%; 333 | } 334 | .row-fluid .offset6 { 335 | margin-left: 53.84615384615384%; 336 | *margin-left: 53.739770867430444%; 337 | } 338 | .row-fluid .offset6:first-child { 339 | margin-left: 51.28205128205128%; 340 | *margin-left: 51.175668303327875%; 341 | } 342 | .row-fluid .offset5 { 343 | margin-left: 45.299145299145295%; 344 | *margin-left: 45.1927623204219%; 345 | } 346 | .row-fluid .offset5:first-child { 347 | margin-left: 42.73504273504273%; 348 | *margin-left: 42.62865975631933%; 349 | } 350 | .row-fluid .offset4 { 351 | margin-left: 36.75213675213675%; 352 | *margin-left: 36.645753773413354%; 353 | } 354 | .row-fluid .offset4:first-child { 355 | margin-left: 34.18803418803419%; 356 | *margin-left: 34.081651209310785%; 357 | } 358 | .row-fluid .offset3 { 359 | margin-left: 28.205128205128204%; 360 | *margin-left: 28.0987452264048%; 361 | } 362 | .row-fluid .offset3:first-child { 363 | margin-left: 25.641025641025642%; 364 | *margin-left: 25.53464266230224%; 365 | } 366 | .row-fluid .offset2 { 367 | margin-left: 19.65811965811966%; 368 | *margin-left: 19.551736679396257%; 369 | } 370 | .row-fluid .offset2:first-child { 371 | margin-left: 17.094017094017094%; 372 | *margin-left: 16.98763411529369%; 373 | } 374 | .row-fluid .offset1 { 375 | margin-left: 11.11111111111111%; 376 | *margin-left: 11.004728132387708%; 377 | } 378 | .row-fluid .offset1:first-child { 379 | margin-left: 8.547008547008547%; 380 | *margin-left: 8.440625568285142%; 381 | } 382 | input, 383 | textarea, 384 | .uneditable-input { 385 | margin-left: 0; 386 | } 387 | .controls-row [class*="span"] + [class*="span"] { 388 | margin-left: 30px; 389 | } 390 | input.span12, 391 | textarea.span12, 392 | .uneditable-input.span12 { 393 | width: 1156px; 394 | } 395 | input.span11, 396 | textarea.span11, 397 | .uneditable-input.span11 { 398 | width: 1056px; 399 | } 400 | input.span10, 401 | textarea.span10, 402 | .uneditable-input.span10 { 403 | width: 956px; 404 | } 405 | input.span9, 406 | textarea.span9, 407 | .uneditable-input.span9 { 408 | width: 856px; 409 | } 410 | input.span8, 411 | textarea.span8, 412 | .uneditable-input.span8 { 413 | width: 756px; 414 | } 415 | input.span7, 416 | textarea.span7, 417 | .uneditable-input.span7 { 418 | width: 656px; 419 | } 420 | input.span6, 421 | textarea.span6, 422 | .uneditable-input.span6 { 423 | width: 556px; 424 | } 425 | input.span5, 426 | textarea.span5, 427 | .uneditable-input.span5 { 428 | width: 456px; 429 | } 430 | input.span4, 431 | textarea.span4, 432 | .uneditable-input.span4 { 433 | width: 356px; 434 | } 435 | input.span3, 436 | textarea.span3, 437 | .uneditable-input.span3 { 438 | width: 256px; 439 | } 440 | input.span2, 441 | textarea.span2, 442 | .uneditable-input.span2 { 443 | width: 156px; 444 | } 445 | input.span1, 446 | textarea.span1, 447 | .uneditable-input.span1 { 448 | width: 56px; 449 | } 450 | .thumbnails { 451 | margin-left: -30px; 452 | } 453 | .thumbnails > li { 454 | margin-left: 30px; 455 | } 456 | .row-fluid .thumbnails { 457 | margin-left: 0; 458 | } 459 | } 460 | 461 | @media (min-width: 768px) and (max-width: 979px) { 462 | .row { 463 | margin-left: -20px; 464 | *zoom: 1; 465 | } 466 | .row:before, 467 | .row:after { 468 | display: table; 469 | line-height: 0; 470 | content: ""; 471 | } 472 | .row:after { 473 | clear: both; 474 | } 475 | [class*="span"] { 476 | float: left; 477 | min-height: 1px; 478 | margin-left: 20px; 479 | } 480 | .container, 481 | .navbar-static-top .container, 482 | .navbar-fixed-top .container, 483 | .navbar-fixed-bottom .container { 484 | width: 724px; 485 | } 486 | .span12 { 487 | width: 724px; 488 | } 489 | .span11 { 490 | width: 662px; 491 | } 492 | .span10 { 493 | width: 600px; 494 | } 495 | .span9 { 496 | width: 538px; 497 | } 498 | .span8 { 499 | width: 476px; 500 | } 501 | .span7 { 502 | width: 414px; 503 | } 504 | .span6 { 505 | width: 352px; 506 | } 507 | .span5 { 508 | width: 290px; 509 | } 510 | .span4 { 511 | width: 228px; 512 | } 513 | .span3 { 514 | width: 166px; 515 | } 516 | .span2 { 517 | width: 104px; 518 | } 519 | .span1 { 520 | width: 42px; 521 | } 522 | .offset12 { 523 | margin-left: 764px; 524 | } 525 | .offset11 { 526 | margin-left: 702px; 527 | } 528 | .offset10 { 529 | margin-left: 640px; 530 | } 531 | .offset9 { 532 | margin-left: 578px; 533 | } 534 | .offset8 { 535 | margin-left: 516px; 536 | } 537 | .offset7 { 538 | margin-left: 454px; 539 | } 540 | .offset6 { 541 | margin-left: 392px; 542 | } 543 | .offset5 { 544 | margin-left: 330px; 545 | } 546 | .offset4 { 547 | margin-left: 268px; 548 | } 549 | .offset3 { 550 | margin-left: 206px; 551 | } 552 | .offset2 { 553 | margin-left: 144px; 554 | } 555 | .offset1 { 556 | margin-left: 82px; 557 | } 558 | .row-fluid { 559 | width: 100%; 560 | *zoom: 1; 561 | } 562 | .row-fluid:before, 563 | .row-fluid:after { 564 | display: table; 565 | line-height: 0; 566 | content: ""; 567 | } 568 | .row-fluid:after { 569 | clear: both; 570 | } 571 | .row-fluid [class*="span"] { 572 | display: block; 573 | float: left; 574 | width: 100%; 575 | min-height: 30px; 576 | margin-left: 2.7624309392265194%; 577 | *margin-left: 2.709239449864817%; 578 | -webkit-box-sizing: border-box; 579 | -moz-box-sizing: border-box; 580 | box-sizing: border-box; 581 | } 582 | .row-fluid [class*="span"]:first-child { 583 | margin-left: 0; 584 | } 585 | .row-fluid .controls-row [class*="span"] + [class*="span"] { 586 | margin-left: 2.7624309392265194%; 587 | } 588 | .row-fluid .span12 { 589 | width: 100%; 590 | *width: 99.94680851063829%; 591 | } 592 | .row-fluid .span11 { 593 | width: 91.43646408839778%; 594 | *width: 91.38327259903608%; 595 | } 596 | .row-fluid .span10 { 597 | width: 82.87292817679558%; 598 | *width: 82.81973668743387%; 599 | } 600 | .row-fluid .span9 { 601 | width: 74.30939226519337%; 602 | *width: 74.25620077583166%; 603 | } 604 | .row-fluid .span8 { 605 | width: 65.74585635359117%; 606 | *width: 65.69266486422946%; 607 | } 608 | .row-fluid .span7 { 609 | width: 57.18232044198895%; 610 | *width: 57.12912895262725%; 611 | } 612 | .row-fluid .span6 { 613 | width: 48.61878453038674%; 614 | *width: 48.56559304102504%; 615 | } 616 | .row-fluid .span5 { 617 | width: 40.05524861878453%; 618 | *width: 40.00205712942283%; 619 | } 620 | .row-fluid .span4 { 621 | width: 31.491712707182323%; 622 | *width: 31.43852121782062%; 623 | } 624 | .row-fluid .span3 { 625 | width: 22.92817679558011%; 626 | *width: 22.87498530621841%; 627 | } 628 | .row-fluid .span2 { 629 | width: 14.3646408839779%; 630 | *width: 14.311449394616199%; 631 | } 632 | .row-fluid .span1 { 633 | width: 5.801104972375691%; 634 | *width: 5.747913483013988%; 635 | } 636 | .row-fluid .offset12 { 637 | margin-left: 105.52486187845304%; 638 | *margin-left: 105.41847889972962%; 639 | } 640 | .row-fluid .offset12:first-child { 641 | margin-left: 102.76243093922652%; 642 | *margin-left: 102.6560479605031%; 643 | } 644 | .row-fluid .offset11 { 645 | margin-left: 96.96132596685082%; 646 | *margin-left: 96.8549429881274%; 647 | } 648 | .row-fluid .offset11:first-child { 649 | margin-left: 94.1988950276243%; 650 | *margin-left: 94.09251204890089%; 651 | } 652 | .row-fluid .offset10 { 653 | margin-left: 88.39779005524862%; 654 | *margin-left: 88.2914070765252%; 655 | } 656 | .row-fluid .offset10:first-child { 657 | margin-left: 85.6353591160221%; 658 | *margin-left: 85.52897613729868%; 659 | } 660 | .row-fluid .offset9 { 661 | margin-left: 79.8342541436464%; 662 | *margin-left: 79.72787116492299%; 663 | } 664 | .row-fluid .offset9:first-child { 665 | margin-left: 77.07182320441989%; 666 | *margin-left: 76.96544022569647%; 667 | } 668 | .row-fluid .offset8 { 669 | margin-left: 71.2707182320442%; 670 | *margin-left: 71.16433525332079%; 671 | } 672 | .row-fluid .offset8:first-child { 673 | margin-left: 68.50828729281768%; 674 | *margin-left: 68.40190431409427%; 675 | } 676 | .row-fluid .offset7 { 677 | margin-left: 62.70718232044199%; 678 | *margin-left: 62.600799341718584%; 679 | } 680 | .row-fluid .offset7:first-child { 681 | margin-left: 59.94475138121547%; 682 | *margin-left: 59.838368402492065%; 683 | } 684 | .row-fluid .offset6 { 685 | margin-left: 54.14364640883978%; 686 | *margin-left: 54.037263430116376%; 687 | } 688 | .row-fluid .offset6:first-child { 689 | margin-left: 51.38121546961326%; 690 | *margin-left: 51.27483249088986%; 691 | } 692 | .row-fluid .offset5 { 693 | margin-left: 45.58011049723757%; 694 | *margin-left: 45.47372751851417%; 695 | } 696 | .row-fluid .offset5:first-child { 697 | margin-left: 42.81767955801105%; 698 | *margin-left: 42.71129657928765%; 699 | } 700 | .row-fluid .offset4 { 701 | margin-left: 37.01657458563536%; 702 | *margin-left: 36.91019160691196%; 703 | } 704 | .row-fluid .offset4:first-child { 705 | margin-left: 34.25414364640884%; 706 | *margin-left: 34.14776066768544%; 707 | } 708 | .row-fluid .offset3 { 709 | margin-left: 28.45303867403315%; 710 | *margin-left: 28.346655695309746%; 711 | } 712 | .row-fluid .offset3:first-child { 713 | margin-left: 25.69060773480663%; 714 | *margin-left: 25.584224756083227%; 715 | } 716 | .row-fluid .offset2 { 717 | margin-left: 19.88950276243094%; 718 | *margin-left: 19.783119783707537%; 719 | } 720 | .row-fluid .offset2:first-child { 721 | margin-left: 17.12707182320442%; 722 | *margin-left: 17.02068884448102%; 723 | } 724 | .row-fluid .offset1 { 725 | margin-left: 11.32596685082873%; 726 | *margin-left: 11.219583872105325%; 727 | } 728 | .row-fluid .offset1:first-child { 729 | margin-left: 8.56353591160221%; 730 | *margin-left: 8.457152932878806%; 731 | } 732 | input, 733 | textarea, 734 | .uneditable-input { 735 | margin-left: 0; 736 | } 737 | .controls-row [class*="span"] + [class*="span"] { 738 | margin-left: 20px; 739 | } 740 | input.span12, 741 | textarea.span12, 742 | .uneditable-input.span12 { 743 | width: 710px; 744 | } 745 | input.span11, 746 | textarea.span11, 747 | .uneditable-input.span11 { 748 | width: 648px; 749 | } 750 | input.span10, 751 | textarea.span10, 752 | .uneditable-input.span10 { 753 | width: 586px; 754 | } 755 | input.span9, 756 | textarea.span9, 757 | .uneditable-input.span9 { 758 | width: 524px; 759 | } 760 | input.span8, 761 | textarea.span8, 762 | .uneditable-input.span8 { 763 | width: 462px; 764 | } 765 | input.span7, 766 | textarea.span7, 767 | .uneditable-input.span7 { 768 | width: 400px; 769 | } 770 | input.span6, 771 | textarea.span6, 772 | .uneditable-input.span6 { 773 | width: 338px; 774 | } 775 | input.span5, 776 | textarea.span5, 777 | .uneditable-input.span5 { 778 | width: 276px; 779 | } 780 | input.span4, 781 | textarea.span4, 782 | .uneditable-input.span4 { 783 | width: 214px; 784 | } 785 | input.span3, 786 | textarea.span3, 787 | .uneditable-input.span3 { 788 | width: 152px; 789 | } 790 | input.span2, 791 | textarea.span2, 792 | .uneditable-input.span2 { 793 | width: 90px; 794 | } 795 | input.span1, 796 | textarea.span1, 797 | .uneditable-input.span1 { 798 | width: 28px; 799 | } 800 | } 801 | 802 | @media (max-width: 767px) { 803 | body { 804 | padding-right: 20px; 805 | padding-left: 20px; 806 | } 807 | .navbar-fixed-top, 808 | .navbar-fixed-bottom, 809 | .navbar-static-top { 810 | margin-right: -20px; 811 | margin-left: -20px; 812 | } 813 | .container-fluid { 814 | padding: 0; 815 | } 816 | .dl-horizontal dt { 817 | float: none; 818 | width: auto; 819 | clear: none; 820 | text-align: left; 821 | } 822 | .dl-horizontal dd { 823 | margin-left: 0; 824 | } 825 | .container { 826 | width: auto; 827 | } 828 | .row-fluid { 829 | width: 100%; 830 | } 831 | .row, 832 | .thumbnails { 833 | margin-left: 0; 834 | } 835 | .thumbnails > li { 836 | float: none; 837 | margin-left: 0; 838 | } 839 | [class*="span"], 840 | .uneditable-input[class*="span"], 841 | .row-fluid [class*="span"] { 842 | display: block; 843 | float: none; 844 | width: 100%; 845 | margin-left: 0; 846 | -webkit-box-sizing: border-box; 847 | -moz-box-sizing: border-box; 848 | box-sizing: border-box; 849 | } 850 | .span12, 851 | .row-fluid .span12 { 852 | width: 100%; 853 | -webkit-box-sizing: border-box; 854 | -moz-box-sizing: border-box; 855 | box-sizing: border-box; 856 | } 857 | .row-fluid [class*="offset"]:first-child { 858 | margin-left: 0; 859 | } 860 | .input-large, 861 | .input-xlarge, 862 | .input-xxlarge, 863 | input[class*="span"], 864 | select[class*="span"], 865 | textarea[class*="span"], 866 | .uneditable-input { 867 | display: block; 868 | width: 100%; 869 | min-height: 30px; 870 | -webkit-box-sizing: border-box; 871 | -moz-box-sizing: border-box; 872 | box-sizing: border-box; 873 | } 874 | .input-prepend input, 875 | .input-append input, 876 | .input-prepend input[class*="span"], 877 | .input-append input[class*="span"] { 878 | display: inline-block; 879 | width: auto; 880 | } 881 | .controls-row [class*="span"] + [class*="span"] { 882 | margin-left: 0; 883 | } 884 | .modal { 885 | position: fixed; 886 | top: 20px; 887 | right: 20px; 888 | left: 20px; 889 | width: auto; 890 | margin: 0; 891 | } 892 | .modal.fade { 893 | top: -100px; 894 | } 895 | .modal.fade.in { 896 | top: 20px; 897 | } 898 | } 899 | 900 | @media (max-width: 480px) { 901 | .nav-collapse { 902 | -webkit-transform: translate3d(0, 0, 0); 903 | } 904 | .page-header h1 small { 905 | display: block; 906 | line-height: 20px; 907 | } 908 | input[type="checkbox"], 909 | input[type="radio"] { 910 | border: 1px solid #ccc; 911 | } 912 | .form-horizontal .control-label { 913 | float: none; 914 | width: auto; 915 | padding-top: 0; 916 | text-align: left; 917 | } 918 | .form-horizontal .controls { 919 | margin-left: 0; 920 | } 921 | .form-horizontal .control-list { 922 | padding-top: 0; 923 | } 924 | .form-horizontal .form-actions { 925 | padding-right: 10px; 926 | padding-left: 10px; 927 | } 928 | .media .pull-left, 929 | .media .pull-right { 930 | display: block; 931 | float: none; 932 | margin-bottom: 10px; 933 | } 934 | .media-object { 935 | margin-right: 0; 936 | margin-left: 0; 937 | } 938 | .modal { 939 | top: 10px; 940 | right: 10px; 941 | left: 10px; 942 | } 943 | .modal-header .close { 944 | padding: 10px; 945 | margin: -10px; 946 | } 947 | .carousel-caption { 948 | position: static; 949 | } 950 | } 951 | 952 | @media (max-width: 979px) { 953 | body { 954 | padding-top: 0; 955 | } 956 | .navbar-fixed-top, 957 | .navbar-fixed-bottom { 958 | position: static; 959 | } 960 | .navbar-fixed-top { 961 | margin-bottom: 20px; 962 | } 963 | .navbar-fixed-bottom { 964 | margin-top: 20px; 965 | } 966 | .navbar-fixed-top .navbar-inner, 967 | .navbar-fixed-bottom .navbar-inner { 968 | padding: 5px; 969 | } 970 | .navbar .container { 971 | width: auto; 972 | padding: 0; 973 | } 974 | .navbar .brand { 975 | padding-right: 10px; 976 | padding-left: 10px; 977 | margin: 0 0 0 -5px; 978 | } 979 | .nav-collapse { 980 | clear: both; 981 | } 982 | .nav-collapse .nav { 983 | float: none; 984 | margin: 0 0 10px; 985 | } 986 | .nav-collapse .nav > li { 987 | float: none; 988 | } 989 | .nav-collapse .nav > li > a { 990 | margin-bottom: 2px; 991 | } 992 | .nav-collapse .nav > .divider-vertical { 993 | display: none; 994 | } 995 | .nav-collapse .nav .nav-header { 996 | color: #777777; 997 | text-shadow: none; 998 | } 999 | .nav-collapse .nav > li > a, 1000 | .nav-collapse .dropdown-menu a { 1001 | padding: 9px 15px; 1002 | font-weight: bold; 1003 | color: #777777; 1004 | -webkit-border-radius: 3px; 1005 | -moz-border-radius: 3px; 1006 | border-radius: 3px; 1007 | } 1008 | .nav-collapse .btn { 1009 | padding: 4px 10px 4px; 1010 | font-weight: normal; 1011 | -webkit-border-radius: 4px; 1012 | -moz-border-radius: 4px; 1013 | border-radius: 4px; 1014 | } 1015 | .nav-collapse .dropdown-menu li + li a { 1016 | margin-bottom: 2px; 1017 | } 1018 | .nav-collapse .nav > li > a:hover, 1019 | .nav-collapse .nav > li > a:focus, 1020 | .nav-collapse .dropdown-menu a:hover, 1021 | .nav-collapse .dropdown-menu a:focus { 1022 | background-color: #f2f2f2; 1023 | } 1024 | .navbar-inverse .nav-collapse .nav > li > a, 1025 | .navbar-inverse .nav-collapse .dropdown-menu a { 1026 | color: #999999; 1027 | } 1028 | .navbar-inverse .nav-collapse .nav > li > a:hover, 1029 | .navbar-inverse .nav-collapse .nav > li > a:focus, 1030 | .navbar-inverse .nav-collapse .dropdown-menu a:hover, 1031 | .navbar-inverse .nav-collapse .dropdown-menu a:focus { 1032 | background-color: #111111; 1033 | } 1034 | .nav-collapse.in .btn-group { 1035 | padding: 0; 1036 | margin-top: 5px; 1037 | } 1038 | .nav-collapse .dropdown-menu { 1039 | position: static; 1040 | top: auto; 1041 | left: auto; 1042 | display: none; 1043 | float: none; 1044 | max-width: none; 1045 | padding: 0; 1046 | margin: 0 15px; 1047 | background-color: transparent; 1048 | border: none; 1049 | -webkit-border-radius: 0; 1050 | -moz-border-radius: 0; 1051 | border-radius: 0; 1052 | -webkit-box-shadow: none; 1053 | -moz-box-shadow: none; 1054 | box-shadow: none; 1055 | } 1056 | .nav-collapse .open > .dropdown-menu { 1057 | display: block; 1058 | } 1059 | .nav-collapse .dropdown-menu:before, 1060 | .nav-collapse .dropdown-menu:after { 1061 | display: none; 1062 | } 1063 | .nav-collapse .dropdown-menu .divider { 1064 | display: none; 1065 | } 1066 | .nav-collapse .nav > li > .dropdown-menu:before, 1067 | .nav-collapse .nav > li > .dropdown-menu:after { 1068 | display: none; 1069 | } 1070 | .nav-collapse .navbar-form, 1071 | .nav-collapse .navbar-search { 1072 | float: none; 1073 | padding: 10px 15px; 1074 | margin: 10px 0; 1075 | border-top: 1px solid #f2f2f2; 1076 | border-bottom: 1px solid #f2f2f2; 1077 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1078 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1079 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 1080 | } 1081 | .navbar-inverse .nav-collapse .navbar-form, 1082 | .navbar-inverse .nav-collapse .navbar-search { 1083 | border-top-color: #111111; 1084 | border-bottom-color: #111111; 1085 | } 1086 | .navbar .nav-collapse .nav.pull-right { 1087 | float: none; 1088 | margin-left: 0; 1089 | } 1090 | .nav-collapse, 1091 | .nav-collapse.collapse { 1092 | height: 0; 1093 | overflow: hidden; 1094 | } 1095 | .navbar .btn-navbar { 1096 | display: block; 1097 | } 1098 | .navbar-static .navbar-inner { 1099 | padding-right: 10px; 1100 | padding-left: 10px; 1101 | } 1102 | } 1103 | 1104 | @media (min-width: 980px) { 1105 | .nav-collapse.collapse { 1106 | height: auto !important; 1107 | overflow: visible !important; 1108 | } 1109 | } 1110 | -------------------------------------------------------------------------------- /example/core/static/core/js/bootstrap-tab.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap.js by @fat & @mdo 3 | * plugins: bootstrap-tab.js 4 | * Copyright 2013 Twitter, Inc. 5 | * http://www.apache.org/licenses/LICENSE-2.0.txt 6 | */ 7 | !function(a){var b=function(b){this.element=a(b)};b.prototype={constructor:b,show:function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.attr("data-target"),e,f,g;d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,""));if(b.parent("li").hasClass("active"))return;e=c.find(".active:last a")[0],g=a.Event("show",{relatedTarget:e}),b.trigger(g);if(g.isDefaultPrevented())return;f=a(d),this.activate(b.parent("li"),c),this.activate(f,f.parent(),function(){b.trigger({type:"shown",relatedTarget:e})})},activate:function(b,c,d){function g(){e.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),f?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var e=c.find("> .active"),f=d&&a.support.transition&&e.hasClass("fade");f?e.one(a.support.transition.end,g):g(),e.removeClass("in")}};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("tab");e||d.data("tab",e=new b(this)),typeof c=="string"&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(window.jQuery) -------------------------------------------------------------------------------- /example/core/templates/home.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 31 | 32 | 33 | 34 | {{ form.media }} 35 | 36 | 37 | 38 | 39 |
40 |
41 |
42 | 43 |
44 | 57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |

Its time to Play !

65 |

Try submitting this form without entering anything.

66 |
67 |

Form

68 |
69 | {% csrf_token %} 70 | {{ form.as_p }} 71 | 72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /example/core/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /example/core/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic.edit import FormView 2 | 3 | from core.forms import FieldTypeForm 4 | 5 | 6 | class HomeView(FormView): 7 | template_name = "home.html" 8 | form_class = FieldTypeForm 9 | success_url = '/' 10 | -------------------------------------------------------------------------------- /example/example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/Django-parsley/f1901625034029a09a926e39ed7cff24b4d512b2/example/example/__init__.py -------------------------------------------------------------------------------- /example/example/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/Django-parsley/f1901625034029a09a926e39ed7cff24b4d512b2/example/example/settings/__init__.py -------------------------------------------------------------------------------- /example/example/settings/common.py: -------------------------------------------------------------------------------- 1 | # Django settings for example project. 2 | import os 3 | PROJECT_PATH = os.path.dirname(os.path.abspath(__file__)) 4 | 5 | DEBUG = False 6 | TEMPLATE_DEBUG = DEBUG 7 | 8 | ADMINS = ( 9 | # ('Your Name', 'your_email@example.com'), 10 | ) 11 | 12 | MANAGERS = ADMINS 13 | 14 | DATABASES = { 15 | 'default': { 16 | 'ENGINE': '', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 17 | 'NAME': '', # Or path to database file if using sqlite3. 18 | 'USER': '', # Not used with sqlite3. 19 | 'PASSWORD': '', # Not used with sqlite3. 20 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 21 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 22 | } 23 | } 24 | 25 | # Local time zone for this installation. Choices can be found here: 26 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 27 | # although not all choices may be available on all operating systems. 28 | # In a Windows environment this must be set to your system time zone. 29 | TIME_ZONE = 'America/Chicago' 30 | 31 | # Language code for this installation. All choices can be found here: 32 | # http://www.i18nguy.com/unicode/language-identifiers.html 33 | LANGUAGE_CODE = 'en-us' 34 | 35 | SITE_ID = 1 36 | 37 | # If you set this to False, Django will make some optimizations so as not 38 | # to load the internationalization machinery. 39 | USE_I18N = True 40 | 41 | # If you set this to False, Django will not format dates, numbers and 42 | # calendars according to the current locale. 43 | USE_L10N = True 44 | 45 | # If you set this to False, Django will not use timezone-aware datetimes. 46 | USE_TZ = True 47 | 48 | # Absolute filesystem path to the directory that will hold user-uploaded files. 49 | # Example: "/home/media/media.lawrence.com/media/" 50 | MEDIA_ROOT = '' 51 | 52 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 53 | # trailing slash. 54 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 55 | MEDIA_URL = '' 56 | 57 | # Absolute path to the directory static files should be collected to. 58 | # Don't put anything in this directory yourself; store your static files 59 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 60 | # Example: "/home/media/media.lawrence.com/static/" 61 | STATIC_ROOT = 'static' 62 | 63 | # URL prefix for static files. 64 | # Example: "http://media.lawrence.com/static/" 65 | STATIC_URL = '/static/' 66 | 67 | # Additional locations of static files 68 | STATICFILES_DIRS = ( 69 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 70 | # Always use forward slashes, even on Windows. 71 | # Don't forget to use absolute paths, not relative paths. 72 | ) 73 | 74 | # List of finder classes that know how to find static files in 75 | # various locations. 76 | STATICFILES_FINDERS = ( 77 | 'django.contrib.staticfiles.finders.FileSystemFinder', 78 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 79 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 80 | ) 81 | 82 | # Make this unique, and don't share it with anybody. 83 | SECRET_KEY = '1&(uu+$bep+e%8%cg^)i2#--@o83208&#yr&1bi49&5u(gsl1(' 84 | 85 | # List of callables that know how to import templates from various sources. 86 | TEMPLATE_LOADERS = ( 87 | 'django.template.loaders.filesystem.Loader', 88 | 'django.template.loaders.app_directories.Loader', 89 | # 'django.template.loaders.eggs.Loader', 90 | ) 91 | 92 | MIDDLEWARE_CLASSES = ( 93 | 'django.middleware.common.CommonMiddleware', 94 | 'django.contrib.sessions.middleware.SessionMiddleware', 95 | 'django.middleware.csrf.CsrfViewMiddleware', 96 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 97 | 'django.contrib.messages.middleware.MessageMiddleware', 98 | # Uncomment the next line for simple clickjacking protection: 99 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 100 | ) 101 | 102 | ROOT_URLCONF = 'example.urls' 103 | 104 | # Python dotted path to the WSGI application used by Django's runserver. 105 | WSGI_APPLICATION = 'example.wsgi.application' 106 | 107 | TEMPLATES = [ 108 | { 109 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 110 | 'DIRS': [ 111 | os.path.join(PROJECT_PATH, 'templates'), 112 | ], 113 | 'APP_DIRS': True, 114 | 'OPTIONS': { 115 | 'context_processors': [ 116 | 'django.template.context_processors.debug', 117 | 'django.template.context_processors.request', 118 | 'django.contrib.auth.context_processors.auth', 119 | 'django.contrib.messages.context_processors.messages', 120 | ], 121 | }, 122 | }, 123 | ] 124 | 125 | 126 | 127 | INSTALLED_APPS = ( 128 | 'django.contrib.auth', 129 | 'django.contrib.contenttypes', 130 | 'django.contrib.sessions', 131 | 'django.contrib.sites', 132 | 'django.contrib.messages', 133 | 'django.contrib.staticfiles', 134 | 'django.contrib.admin', 135 | 'core', 136 | 'parsley', 137 | # Uncomment the next line to enable the admin: 138 | # Uncomment the next line to enable admin documentation: 139 | # 'django.contrib.admindocs', 140 | ) 141 | 142 | # A sample logging configuration. The only tangible logging 143 | # performed by this configuration is to send an email to 144 | # the site admins on every HTTP 500 error when DEBUG=False. 145 | # See http://docs.djangoproject.com/en/dev/topics/logging for 146 | # more details on how to customize your logging configuration. 147 | LOGGING = { 148 | 'version': 1, 149 | 'disable_existing_loggers': False, 150 | 'filters': { 151 | 'require_debug_false': { 152 | '()': 'django.utils.log.RequireDebugFalse' 153 | } 154 | }, 155 | 'handlers': { 156 | 'mail_admins': { 157 | 'level': 'ERROR', 158 | 'filters': ['require_debug_false'], 159 | 'class': 'django.utils.log.AdminEmailHandler' 160 | } 161 | }, 162 | 'loggers': { 163 | 'django.request': { 164 | 'handlers': ['mail_admins'], 165 | 'level': 'ERROR', 166 | 'propagate': True, 167 | }, 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /example/example/settings/local.py-dist: -------------------------------------------------------------------------------- 1 | from .common import * 2 | 3 | DEBUG = True 4 | 5 | 6 | DATABASES = { 7 | 'default': { 8 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 9 | 'NAME': 'test.db', # Or path to database file if using sqlite3. 10 | 'USER': '', # Not used with sqlite3. 11 | 'PASSWORD': '', # Not used with sqlite3. 12 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 13 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /example/example/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.conf.urls import include, url 3 | 4 | from core.views import HomeView 5 | 6 | admin.autodiscover() 7 | 8 | 9 | urlpatterns =[ 10 | url(r'^admin/', include(admin.site.urls)), 11 | url(r'^$', HomeView.as_view(), name='home'), 12 | ] 13 | -------------------------------------------------------------------------------- /example/example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings.local") 19 | 20 | # This application object is used by any WSGI server configured to use this 21 | # file. This includes Django's development server, if the WSGI_APPLICATION 22 | # setting points here. 23 | from django.core.wsgi import get_wsgi_application 24 | application = get_wsgi_application() 25 | 26 | # Apply WSGI middleware here. 27 | # from helloworld.wsgi import HelloWorldApplication 28 | # application = HelloWorldApplication(application) 29 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings.local") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /example/parsley: -------------------------------------------------------------------------------- 1 | ../parsley/ -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.10 2 | gunicorn 3 | -------------------------------------------------------------------------------- /parsley/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/Django-parsley/f1901625034029a09a926e39ed7cff24b4d512b2/parsley/__init__.py -------------------------------------------------------------------------------- /parsley/decorators.py: -------------------------------------------------------------------------------- 1 | import re 2 | import types 3 | 4 | from django import forms 5 | from parsley.widgets import ParsleyChoiceFieldRendererMixin 6 | 7 | FIELD_TYPES = [ 8 | (forms.URLField, "url"), 9 | (forms.EmailField, "email"), 10 | (forms.IntegerField, "digits"), 11 | (forms.DecimalField, "number"), 12 | (forms.FloatField, "number"), 13 | ] 14 | 15 | 16 | FIELD_ATTRS = [ 17 | ("min_length", "minlength"), 18 | ("max_length", "maxlength"), 19 | ("min_value", "min"), 20 | ("max_value", "max"), 21 | ] 22 | 23 | 24 | def update_widget_attrs(field, prefix='data'): 25 | attrs = field.widget.attrs 26 | if field.required: 27 | if isinstance(field.widget, forms.widgets.RadioSelect): 28 | try: 29 | # django >= 1.11 30 | original_create_option = field.widget.create_option 31 | 32 | def new_create_option(self, name, value, label, selected, index, subindex=None, attrs=None): 33 | if index == len(self.choices) - 1: 34 | attrs = attrs or {} 35 | attrs["{prefix}-required".format(prefix=prefix)] = "true" 36 | 37 | return original_create_option(name, value, label, selected, index, subindex, attrs) 38 | 39 | field.widget.create_option = types.MethodType(new_create_option, field.widget) 40 | except AttributeError: 41 | # django < 1.11 42 | 43 | # Use a mixin, to try and support non-standard renderers if possible 44 | class ParsleyChoiceFieldRenderer(ParsleyChoiceFieldRendererMixin, field.widget.renderer): 45 | parsley_namespace = prefix 46 | field.widget.renderer = ParsleyChoiceFieldRenderer 47 | else: 48 | attrs["{prefix}-required".format(prefix=prefix)] = "true" 49 | error_message = field.error_messages.get('required', None) 50 | if error_message: 51 | attrs["{prefix}-required-message".format(prefix=prefix)] = error_message 52 | 53 | if isinstance(field, forms.RegexField): 54 | attrs.update({"{prefix}-pattern".format(prefix=prefix): field.regex.pattern}) 55 | 56 | error_message = field.error_messages.get('invalid', None) 57 | if error_message: 58 | attrs["{prefix}-pattern-message".format(prefix=prefix)] = error_message 59 | 60 | if field.regex.flags & re.IGNORECASE: 61 | attrs.update({"{prefix}-regexp-flag".format(prefix=prefix): "i"}) 62 | 63 | if isinstance(field, forms.MultiValueField): 64 | for subfield in field.fields: 65 | update_widget_attrs(subfield) 66 | 67 | # Set {prefix}-* attributes for parsley based on Django field attributes 68 | for attr, data_attr, in FIELD_ATTRS: 69 | if getattr(field, attr, None): 70 | attrs["{prefix}-{0}".format(data_attr, prefix=prefix)] = getattr(field, attr) 71 | 72 | error_message = field.error_messages.get(attr, None) 73 | if error_message: 74 | attrs["{prefix}-{0}-message".format(data_attr, prefix=prefix)] = error_message 75 | 76 | # Set {prefix}-type attribute based on Django field instance type 77 | for klass, field_type in FIELD_TYPES: 78 | if isinstance(field, klass): 79 | attrs["{prefix}-type".format(prefix=prefix)] = field_type 80 | 81 | error_message = field.error_messages.get('invalid', None) 82 | if error_message: 83 | attrs["{prefix}-type-message".format(field_type, prefix=prefix)] = error_message 84 | 85 | 86 | def parsley_form(form): 87 | prefix = getattr(getattr(form, 'Meta', None), 'parsley_namespace', 'data-parsley') 88 | for _, field in form.fields.items(): 89 | update_widget_attrs(field, prefix) 90 | extras = getattr(getattr(form, 'Meta', None), 'parsley_extras', {}) 91 | for field_name, data in extras.items(): 92 | for key, value in data.items(): 93 | if field_name not in form.fields: 94 | continue 95 | attrs = form.fields[field_name].widget.attrs 96 | if key == 'equalto': 97 | # Use HTML id for {prefix}-equalto 98 | value = '#' + form[value].id_for_label 99 | if isinstance(value, bool): 100 | value = "true" if value else "false" 101 | attrs["{prefix}-%s".format(prefix=prefix) % key] = value 102 | return form 103 | 104 | 105 | def parsleyfy(klass): 106 | """ 107 | A decorator to add {prefix}-* attributes to your form.fields 108 | """ 109 | 110 | old_init = klass.__init__ 111 | 112 | def new_init(self, *args, **kwargs): 113 | old_init(self, *args, **kwargs) 114 | parsley_form(self) 115 | 116 | klass.__init__ = new_init 117 | 118 | return klass 119 | -------------------------------------------------------------------------------- /parsley/mixins.py: -------------------------------------------------------------------------------- 1 | from parsley.decorators import parsleyfy 2 | 3 | 4 | class ParsleyAdminMixin(object): 5 | 6 | def get_form(self, *args, **kwargs): 7 | form = super(ParsleyAdminMixin, self).get_form(*args, **kwargs) 8 | return parsleyfy(form) 9 | -------------------------------------------------------------------------------- /parsley/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/Django-parsley/f1901625034029a09a926e39ed7cff24b4d512b2/parsley/models.py -------------------------------------------------------------------------------- /parsley/static/parsley/js/parsley.django-admin.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Parsley.js - django admin helper 3 | */ 4 | ! function($) { 5 | 'use strict'; 6 | 7 | $(window).on('load', function() { 8 | // Don't parsleyfy Django search boxes. It breaks layout 9 | $('form:not(#changelist-search, #changelist-form)').each(function() { 10 | $(this).parsley({}); 11 | }); 12 | }); 13 | }(window.jQuery || window.Zepto); 14 | -------------------------------------------------------------------------------- /parsley/static/parsley/js/parsley.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Parsleyjs 3 | * Guillaume Potier - 4 | * Version 2.0.7 - built Sat Jan 24 2015 14:50:11 5 | * MIT Licensed 6 | * 7 | */ 8 | !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a(jQuery)}(function(a){"undefined"==typeof a&&"undefined"!=typeof window.jQuery&&(a=window.jQuery);var b={attr:function(a,b,c){var d,e={},f=this.msieversion(),g=new RegExp("^"+b,"i");if("undefined"==typeof a||"undefined"==typeof a[0])return{};for(var h in a[0].attributes)if(d=a[0].attributes[h],"undefined"!=typeof d&&null!==d&&(!f||f>=8||d.specified)&&g.test(d.name)){if("undefined"!=typeof c&&new RegExp(c+"$","i").test(d.name))return!0;e[this.camelize(d.name.replace(b,""))]=this.deserializeValue(d.value)}return"undefined"==typeof c?e:!1},setAttr:function(a,b,c,d){a[0].setAttribute(this.dasherize(b+c),String(d))},get:function(a,b){for(var c=0,d=(b||"").split(".");this.isObject(a)||this.isArray(a);)if(a=a[d[c++]],c===d.length)return a;return void 0},hash:function(a){return String(Math.random()).substring(2,a?a+2:9)},isArray:function(a){return"[object Array]"===Object.prototype.toString.call(a)},isObject:function(a){return a===Object(a)},deserializeValue:function(b){var c;try{return b?"true"==b||("false"==b?!1:"null"==b?null:isNaN(c=Number(b))?/^[\[\{]/.test(b)?a.parseJSON(b):b:c):b}catch(d){return b}},camelize:function(a){return a.replace(/-+(.)?/g,function(a,b){return b?b.toUpperCase():""})},dasherize:function(a){return a.replace(/::/g,"/").replace(/([A-Z]+)([A-Z][a-z])/g,"$1_$2").replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/_/g,"-").toLowerCase()},msieversion:function(){var a=window.navigator.userAgent,b=a.indexOf("MSIE ");return b>0||navigator.userAgent.match(/Trident.*rv\:11\./)?parseInt(a.substring(b+5,a.indexOf(".",b)),10):0}},c={namespace:"data-parsley-",inputs:"input, textarea, select",excluded:"input[type=button], input[type=submit], input[type=reset], input[type=hidden]",priorityEnabled:!0,uiEnabled:!0,validationThreshold:3,focus:"first",trigger:!1,errorClass:"parsley-error",successClass:"parsley-success",classHandler:function(){},errorsContainer:function(){},errorsWrapper:'',errorTemplate:"
  • "},d=function(){};d.prototype={asyncSupport:!1,actualizeOptions:function(){return this.options=this.OptionsFactory.get(this),this},validateThroughValidator:function(a,b,c){return window.ParsleyValidator.validate(a,b,c)},subscribe:function(b,c){return a.listenTo(this,b.toLowerCase(),c),this},unsubscribe:function(b){return a.unsubscribeTo(this,b.toLowerCase()),this},reset:function(){if("ParsleyForm"!==this.__class__)return a.emit("parsley:field:reset",this);for(var b=0;b=0;l--)"Required"!==k[l].__class__||(i=k[l].requiresValidation(b));if(this.has(h,a)||this.options.strict||i)try{this.has(h,this.options.strict||i?a:void 0)||(new e).HaveProperty(h).validate(a),c=this._check(h,a[h],b),(g(c)&&c.length>0||!g(c)&&!f(c))&&(d[h]=c)}catch(m){d[h]=m}}return f(d)?!0:d},add:function(a,b){if(b instanceof e||g(b)&&b[0]instanceof e)return this.nodes[a]=b,this;if("object"==typeof b&&!g(b))return this.nodes[a]=b instanceof c?b:new c(b),this;throw new Error("Should give an Assert, an Asserts array, a Constraint",b)},has:function(a,b){return b="undefined"!=typeof b?b:this.nodes,"undefined"!=typeof b[a]},get:function(a,b){return this.has(a)?this.nodes[a]:b||null},remove:function(a){var b=[];for(var c in this.nodes)c!==a&&(b[c]=this.nodes[c]);return this.nodes=b,this},_bootstrap:function(a){if(a instanceof c)return this.nodes=a.nodes;for(var b in a)this.add(b,a[b])},_check:function(a,b,d){if(this.nodes[a]instanceof e)return this._checkAsserts(b,[this.nodes[a]],d);if(g(this.nodes[a]))return this._checkAsserts(b,this.nodes[a],d);if(this.nodes[a]instanceof c)return this.nodes[a].check(b,d);throw new Error("Invalid node",this.nodes[a])},_checkAsserts:function(a,b,c){for(var d,e=[],f=0;f0},addGroup:function(a){return g(a)?this.addGroups(a):(this.hasGroup(a)||this.groups.push(a),this)},removeGroup:function(a){for(var b=[],c=0;c=a)throw new d(this,a,{threshold:this.threshold});return!0},this},GreaterThanOrEqual:function(a){if(this.__class__="GreaterThanOrEqual","undefined"==typeof a)throw new Error("Should give a threshold value");return this.threshold=a,this.validate=function(a){if(""===a||isNaN(Number(a)))throw new d(this,a,{value:b.errorCode.must_be_a_number});if(this.threshold>a)throw new d(this,a,{threshold:this.threshold});return!0},this},InstanceOf:function(a){if(this.__class__="InstanceOf","undefined"==typeof a)throw new Error("InstanceOf must be instanciated with a value");return this.classRef=a,this.validate=function(a){if(!0!=a instanceof this.classRef)throw new d(this,a,{classRef:this.classRef});return!0},this},Length:function(a){if(this.__class__="Length",!a.min&&!a.max)throw new Error("Lenth assert must be instanciated with a { min: x, max: y } object");return this.min=a.min,this.max=a.max,this.validate=function(a){if("string"!=typeof a&&!g(a))throw new d(this,a,{value:b.errorCode.must_be_a_string_or_array});if("undefined"!=typeof this.min&&this.min===this.max&&a.length!==this.min)throw new d(this,a,{min:this.min,max:this.max});if("undefined"!=typeof this.max&&a.length>this.max)throw new d(this,a,{max:this.max});if("undefined"!=typeof this.min&&a.length>>0;if(0===c)return-1;var d=0;if(arguments.length>1&&(d=Number(arguments[1]),d!=d?d=0:0!==d&&1/0!=d&&d!=-1/0&&(d=(d>0||-1)*Math.floor(Math.abs(d)))),d>=c)return-1;for(var e=d>=0?d:Math.max(c-Math.abs(d),0);c>e;e++)if(e in b&&b[e]===a)return e;return-1});var f=function(a){for(var b in a)return!1;return!0},g=function(a){return"[object Array]"===Object.prototype.toString.call(a)};return"function"==typeof define&&define.amd?define("vendors/validator.js/dist/validator",[],function(){return a}):"undefined"!=typeof module&&module.exports?module.exports=a:window["undefined"!=typeof validatorjs_ns?validatorjs_ns:"Validator"]=a,a}();e="undefined"!=typeof e?e:"undefined"!=typeof module?module.exports:null;var f=function(a,b){this.__class__="ParsleyValidator",this.Validator=e,this.locale="en",this.init(a||{},b||{})};f.prototype={init:function(b,c){this.catalog=c;for(var d in b)this.addValidator(d,b[d].fn,b[d].priority,b[d].requirementsTransformer);a.emit("parsley:validator:init")},setLocale:function(a){if("undefined"==typeof this.catalog[a])throw new Error(a+" is not available in the catalog");return this.locale=a,this},addCatalog:function(a,b,c){return"object"==typeof b&&(this.catalog[a]=b),!0===c?this.setLocale(a):this},addMessage:function(a,b,c){return"undefined"==typeof this.catalog[a]&&(this.catalog[a]={}),this.catalog[a][b.toLowerCase()]=c,this},validate:function(){return(new this.Validator.Validator).validate.apply(new e.Validator,arguments)},addValidator:function(b,c,d,f){return this.validators[b.toLowerCase()]=function(b){return a.extend((new e.Assert).Callback(c,b),{priority:d,requirementsTransformer:f})},this},updateValidator:function(a,b,c,d){return this.addValidator(a,b,c,d)},removeValidator:function(a){return delete this.validators[a],this},getErrorMessage:function(a){var b;return b="type"===a.name?this.catalog[this.locale][a.name][a.requirements]:this.formatMessage(this.catalog[this.locale][a.name],a.requirements),""!==b?b:this.catalog[this.locale].defaultMessage},formatMessage:function(a,b){if("object"==typeof b){for(var c in b)a=this.formatMessage(a,b[c]);return a}return"string"==typeof a?a.replace(new RegExp("%s","i"),b):""},validators:{notblank:function(){return a.extend((new e.Assert).NotBlank(),{priority:2})},required:function(){return a.extend((new e.Assert).Required(),{priority:512})},type:function(b){var c;switch(b){case"email":c=(new e.Assert).Email();break;case"range":case"number":c=(new e.Assert).Regexp("^-?(?:\\d+|\\d{1,3}(?:,\\d{3})+)?(?:\\.\\d+)?$");break;case"integer":c=(new e.Assert).Regexp("^-?\\d+$");break;case"digits":c=(new e.Assert).Regexp("^\\d+$");break;case"alphanum":c=(new e.Assert).Regexp("^\\w+$","i");break;case"url":c=(new e.Assert).Regexp("(https?:\\/\\/)?(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,24}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)","i");break;default:throw new Error("validator type `"+b+"` is not supported")}return a.extend(c,{priority:256})},pattern:function(b){var c="";return/^\/.*\/(?:[gimy]*)$/.test(b)&&(c=b.replace(/.*\/([gimy]*)$/,"$1"),b=b.replace(new RegExp("^/(.*?)/"+c+"$"),"$1")),a.extend((new e.Assert).Regexp(b,c),{priority:64})},minlength:function(b){return a.extend((new e.Assert).Length({min:b}),{priority:30,requirementsTransformer:function(){return"string"!=typeof b||isNaN(b)?b:parseInt(b,10)}})},maxlength:function(b){return a.extend((new e.Assert).Length({max:b}),{priority:30,requirementsTransformer:function(){return"string"!=typeof b||isNaN(b)?b:parseInt(b,10)}})},length:function(b){return a.extend((new e.Assert).Length({min:b[0],max:b[1]}),{priority:32})},mincheck:function(a){return this.minlength(a)},maxcheck:function(a){return this.maxlength(a)},check:function(a){return this.length(a)},min:function(b){return a.extend((new e.Assert).GreaterThanOrEqual(b),{priority:30,requirementsTransformer:function(){return"string"!=typeof b||isNaN(b)?b:parseInt(b,10)}})},max:function(b){return a.extend((new e.Assert).LessThanOrEqual(b),{priority:30,requirementsTransformer:function(){return"string"!=typeof b||isNaN(b)?b:parseInt(b,10)}})},range:function(b){return a.extend((new e.Assert).Range(b[0],b[1]),{priority:32,requirementsTransformer:function(){for(var a=0;a0?this._errorClass(a):this._resetClass(a)},manageErrorsMessages:function(b,c){if("undefined"==typeof b.options.errorsMessagesDisabled){if("undefined"!=typeof b.options.errorMessage)return c.added.length||c.kept.length?(0===b._ui.$errorsWrapper.find(".parsley-custom-error-message").length&&b._ui.$errorsWrapper.append(a(b.options.errorTemplate).addClass("parsley-custom-error-message")),b._ui.$errorsWrapper.addClass("filled").find(".parsley-custom-error-message").html(b.options.errorMessage)):b._ui.$errorsWrapper.removeClass("filled").find(".parsley-custom-error-message").remove();for(var d=0;d0&&"undefined"==typeof a.fields[b].options.noFocus){if("first"===a.options.focus)return a._focusedField=a.fields[b].$element,a._focusedField.focus();a._focusedField=a.fields[b].$element}return null===a._focusedField?null:a._focusedField.focus()},_getErrorMessage:function(a,b){var c=b.name+"Message";return"undefined"!=typeof a.options[c]?window.ParsleyValidator.formatMessage(a.options[c],b.requirements):window.ParsleyValidator.getErrorMessage(b)},_diff:function(a,b,c){for(var d=[],e=[],f=0;f0&&this.validationResult&&(this.validationResult=!1));return a.emit("parsley:form:"+(this.validationResult?"success":"error"),this),a.emit("parsley:form:validated",this),this.validationResult},isValid:function(a,b){this._refreshFields();for(var c=0;c1){var c=[];return this.each(function(){c.push(a(this).parsley(b))}),c}return a(this).length?new o(this,b):void(window.console&&window.console.warn&&window.console.warn("You must bind Parsley on an existing element."))},window.ParsleyUI="function"==typeof b.get(window,"ParsleyConfig.ParsleyUI")?(new window.ParsleyConfig.ParsleyUI).listen():(new g).listen(),"undefined"==typeof window.ParsleyExtend&&(window.ParsleyExtend={}),"undefined"==typeof window.ParsleyConfig&&(window.ParsleyConfig={}),window.Parsley=window.psly=o,window.ParsleyUtils=b,window.ParsleyValidator=new f(window.ParsleyConfig.validators,window.ParsleyConfig.i18n),!1!==b.get(window,"ParsleyConfig.autoBind")&&a(function(){a("[data-parsley-validate]").length&&a("[data-parsley-validate]").parsley()})}); -------------------------------------------------------------------------------- /parsley/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agiliq/Django-parsley/f1901625034029a09a926e39ed7cff24b4d512b2/parsley/templatetags/__init__.py -------------------------------------------------------------------------------- /parsley/templatetags/parsley.py: -------------------------------------------------------------------------------- 1 | from django import template, VERSION 2 | 3 | from ..decorators import parsley_form 4 | 5 | register = template.Library() 6 | 7 | if VERSION[:2] >= (1, 9): 8 | # in django 1.9 and above, the simple_tag can do assignment 9 | tag_decorator = register.simple_tag 10 | else: 11 | # django 1.8 and below needs the assignment_tag 12 | tag_decorator = register.assignment_tag 13 | 14 | 15 | @tag_decorator() 16 | def parsleyfy(form): 17 | return parsley_form(form) 18 | -------------------------------------------------------------------------------- /parsley/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .tests import * 2 | -------------------------------------------------------------------------------- /parsley/tests/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from parsley.mixins import ParsleyAdminMixin 4 | 5 | from .models import Student 6 | 7 | 8 | class StudentAdmin(ParsleyAdminMixin, admin.ModelAdmin): 9 | pass 10 | -------------------------------------------------------------------------------- /parsley/tests/forms.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django import forms 4 | from parsley.decorators import parsleyfy 5 | from .models import Student 6 | 7 | 8 | class TextForm(forms.Form): 9 | name = forms.CharField(required=True,) 10 | university = forms.CharField(required=False) 11 | 12 | 13 | @parsleyfy 14 | class TextForm2(forms.Form): 15 | name = forms.CharField(required=True) 16 | university = forms.CharField(required=False) 17 | 18 | 19 | @parsleyfy 20 | class FieldTypeForm(forms.Form): 21 | name = forms.CharField(min_length=3, max_length=30) 22 | url = forms.URLField() 23 | url2 = forms.URLField(required=False) 24 | email = forms.EmailField() 25 | email2 = forms.EmailField(required=False) 26 | age = forms.IntegerField() 27 | income = forms.DecimalField() 28 | income2 = forms.FloatField() 29 | topnav = forms.RegexField(regex="#[A-Fa-f0-9]{6}") 30 | topnav2 = forms.RegexField(regex=re.compile("#[a-z]+", re.IGNORECASE)) 31 | some_num = forms.IntegerField(min_value=10, max_value=100) 32 | amount = forms.DecimalField(max_digits=12, decimal_places=2, required=True, max_value=999999999999.99, min_value=-999999999999.99) 33 | 34 | 35 | @parsleyfy 36 | class ExtraDataForm(forms.Form): 37 | name = forms.CharField() 38 | email = forms.EmailField() 39 | email2 = forms.EmailField() 40 | hide_errors = forms.CharField() 41 | 42 | class Meta: 43 | parsley_extras = { 44 | "name": { 45 | "error-message": "Name invalid", 46 | }, 47 | "email2": { 48 | "equalto": "email", 49 | "equalto-message": "Must match", 50 | }, 51 | "hide_errors": { 52 | "show-errors": False, 53 | }, 54 | } 55 | 56 | 57 | class ExtraDataMissingFieldForm(ExtraDataForm): 58 | def __init__(self, *args, **kwargs): 59 | del self.base_fields['email2'] 60 | super(ExtraDataMissingFieldForm, self).__init__(*args, **kwargs) 61 | 62 | 63 | @parsleyfy 64 | class FormWithWidgets(forms.Form): 65 | description = forms.CharField(widget=forms.TextInput) 66 | blurb = forms.CharField(widget=forms.TextInput(attrs={ 67 | "class": "highlight"})) 68 | 69 | @parsleyfy 70 | class StudentModelForm(forms.ModelForm): 71 | class Meta: 72 | model = Student 73 | fields = '__all__' 74 | 75 | def __init__(self, *args, **kwargs): 76 | super(StudentModelForm, self).__init__(*args, **kwargs) 77 | 78 | def save(self, *args, **kwargs): 79 | if not self.instance.name: 80 | self.name = "Luke Skywalker" 81 | return super(StudentModelForm, self).save(*args, **kwargs) 82 | 83 | @parsleyfy 84 | class FormWithCustomInit(forms.Form): 85 | description = forms.CharField() 86 | 87 | def __init__(self, *args, **kwargs): 88 | super(FormWithCustomInit, self).__init__(*args, **kwargs) 89 | self.fields["description"].initial = "Hello" 90 | 91 | 92 | 93 | class FormWithCleanField(forms.Form): 94 | description = forms.CharField(widget=forms.TextInput) 95 | 96 | def clean_description(self): 97 | raise forms.ValidationError("Error") 98 | 99 | 100 | def get_state_choices(): 101 | return [("NY", "NY"), ("OH", "OH")] 102 | 103 | @parsleyfy 104 | class FormWithCustomChoices(forms.Form): 105 | state = forms.ChoiceField(widget=forms.Select(choices=[])) 106 | def __init__(self, *args, **kwargs): 107 | super(FormWithCustomChoices, self).__init__(*args, **kwargs) 108 | self.fields['state'] = forms.ChoiceField( 109 | choices=get_state_choices()) 110 | 111 | @parsleyfy 112 | class FormWithRadioSelect(forms.Form): 113 | state = forms.ChoiceField(choices=get_state_choices(), widget=forms.RadioSelect) 114 | 115 | 116 | @parsleyfy 117 | class FormWithRadioSelectNotRequired(forms.Form): 118 | state = forms.ChoiceField(choices=get_state_choices(), required=False, widget=forms.RadioSelect) 119 | 120 | 121 | @parsleyfy 122 | class FormWithMedia(forms.Form): 123 | name = forms.CharField(required=True) 124 | 125 | class Media: 126 | js = ("jquery.min.js",) 127 | css = {"all": ("jquery.css",)} 128 | 129 | 130 | @parsleyfy 131 | class FormWithoutMedia(forms.Form): 132 | name = forms.CharField(required=True) 133 | 134 | 135 | class SSNWidget(forms.MultiWidget): 136 | def __init__(self, *args, **kwargs): 137 | kwargs['widgets'] = [ 138 | forms.TextInput(), 139 | forms.TextInput(), 140 | forms.TextInput(), 141 | ] 142 | super(SSNWidget, self).__init__(*args, **kwargs) 143 | 144 | def decompress(self, value): 145 | return value.split('-') if value else [None, None, None] 146 | 147 | 148 | class SSN(forms.MultiValueField): 149 | widget = SSNWidget 150 | def __init__(self, *args, **kwargs): 151 | fields = ( 152 | forms.RegexField(r'^(\d)+$', min_length=3, max_length=3), 153 | forms.RegexField(r'^(\d)+$', min_length=3, max_length=3), 154 | forms.RegexField(r'^(\d)+$', min_length=4, max_length=4), 155 | ) 156 | super(SSN, self).__init__(fields=fields, *args, **kwargs) 157 | 158 | 159 | @parsleyfy 160 | class MultiWidgetForm(forms.Form): 161 | ssn = SSN() 162 | 163 | 164 | @parsleyfy 165 | class CustomErrorMessageForm(forms.Form): 166 | name = forms.CharField(error_messages={"max_length": "Please only 30 characters"}, max_length=30, required=False) 167 | email = forms.EmailField(error_messages={"invalid": "Invalid email"}, required=False) 168 | favorite_color = forms.CharField(error_messages={"required": "Favorite color is required"}) 169 | 170 | 171 | @parsleyfy 172 | class CustomPrefixForm(forms.Form): 173 | name = forms.CharField(required=True) 174 | 175 | class Meta: 176 | parsley_namespace = 'custom' 177 | -------------------------------------------------------------------------------- /parsley/tests/models.py: -------------------------------------------------------------------------------- 1 | #Django requires every app to have models. 2 | from django.db import models 3 | 4 | class Student(models.Model): 5 | name = models.CharField(max_length=100) 6 | -------------------------------------------------------------------------------- /parsley/tests/tests.py: -------------------------------------------------------------------------------- 1 | import re 2 | import six 3 | 4 | from django import forms 5 | from django.template import Context, Template 6 | from django.test import TestCase 7 | from django.utils.translation import ugettext_lazy as _ 8 | 9 | from parsley.decorators import parsleyfy, parsley_form 10 | 11 | from .forms import (TextForm, TextForm2, FieldTypeForm, ExtraDataForm, 12 | ExtraDataMissingFieldForm, FormWithWidgets, StudentModelForm, 13 | FormWithCleanField, FormWithCustomInit, FormWithCustomChoices, 14 | MultiWidgetForm, CustomErrorMessageForm, 15 | CustomPrefixForm, FormWithRadioSelect, FormWithRadioSelectNotRequired) 16 | 17 | 18 | class ParsleyTestCase(TestCase): 19 | 20 | def assertAttrsEqual(self, a, b): 21 | for k in a.keys(): # ignore unspecified keys 22 | if k in b: 23 | if six.PY3: 24 | x, y = str(a[k]), str(b[k]) 25 | else: 26 | x, y = unicode(a[k]), unicode(b[k]) 27 | self.assertEqual(x, y) 28 | 29 | 30 | class CharFieldTest(ParsleyTestCase): 31 | def test_basic(self): 32 | """ 33 | Tests that parsleyfy will add data-parsley-required for required fields, 34 | but not for required=False fields for CharFields 35 | """ 36 | form = TextForm() 37 | self.assertEqual(form.fields["name"].widget.attrs, {}) 38 | self.assertEqual(form.fields["university"].widget.attrs, {}) 39 | form = parsley_form(TextForm()) 40 | self.assertAttrsEqual(form.fields["name"].widget.attrs, { 41 | "data-parsley-required": "true", 42 | "data-parsley-required-message": _("This field is required.") 43 | }) 44 | self.assertEqual(form.fields["university"].widget.attrs, {}) 45 | 46 | 47 | class CharFieldDecoratedTest(ParsleyTestCase): 48 | def test_decorated(self): 49 | "Tests that parsleyfy works as a class Decorator" 50 | form = TextForm2() 51 | self.assertAttrsEqual(form.fields["name"].widget.attrs, { 52 | "data-parsley-required": "true", 53 | "data-parsley-required-message": _("This field is required.") 54 | }) 55 | self.assertEqual(form.fields["university"].widget.attrs, {}) 56 | 57 | 58 | class FieldTypeFormTest(ParsleyTestCase): 59 | def test_fields(self): 60 | "Tests that parsleyfy adds data-parsley-required for things other than CharField" 61 | form = FieldTypeForm() 62 | fields = form.fields 63 | self.assertEqual(fields["url"].widget.attrs["data-parsley-required"], "true") 64 | self.assertFalse("data-parsley-required" in fields["url2"].widget.attrs) 65 | self.assertEqual(fields["email"].widget.attrs["data-parsley-required"], "true") 66 | self.assertFalse("data-parsley-required" in fields["email2"].widget.attrs) 67 | 68 | 69 | class DataTypeTest(ParsleyTestCase): 70 | def test_data_types(self): 71 | "Test that different field types get correct data-parsley-type" 72 | form = FieldTypeForm() 73 | fields = form.fields 74 | self.assertTrue("data-parsley-type" in fields["url"].widget.attrs) 75 | self.assertEqual(fields["url"].widget.attrs["data-parsley-type"], "url") 76 | self.assertTrue("data-parsley-type" in fields["email"].widget.attrs) 77 | self.assertEqual(fields["email"].widget.attrs["data-parsley-type"], "email") 78 | self.assertEqual(fields["age"].widget.attrs["data-parsley-type"], "digits") 79 | self.assertEqual(fields["income"].widget.attrs["data-parsley-type"], "number") 80 | self.assertEqual(fields["income2"].widget.attrs["data-parsley-type"], "number") 81 | self.assertEqual(fields["topnav"].widget.attrs["data-parsley-pattern"], "#[A-Fa-f0-9]{6}") 82 | self.assertNotIn("data-parsley-regexp-flag", fields["topnav"].widget.attrs) 83 | self.assertEqual(fields["topnav2"].widget.attrs["data-parsley-pattern"], "#[a-z]+") 84 | self.assertEqual(fields["topnav2"].widget.attrs["data-parsley-regexp-flag"], "i") 85 | 86 | 87 | class LengthTest(ParsleyTestCase): 88 | def test_length(self): 89 | form = FieldTypeForm() 90 | fields = form.fields 91 | name_attrs = fields["name"].widget.attrs 92 | self.assertTrue("data-parsley-minlength" in name_attrs) 93 | self.assertEqual(name_attrs["data-parsley-minlength"], 3) 94 | self.assertEqual(name_attrs["data-parsley-maxlength"], 30) 95 | self.assertEqual(fields["amount"].widget.attrs["data-parsley-max"], 999999999999.99) 96 | self.assertEqual(fields["amount"].widget.attrs["data-parsley-min"], -999999999999.99) 97 | 98 | class ValueTest(ParsleyTestCase): 99 | def test_value(self): 100 | form = FieldTypeForm() 101 | fields = form.fields 102 | num_attrs = fields['some_num'].widget.attrs 103 | self.assertTrue("data-parsley-min" in num_attrs, True) 104 | self.assertTrue("data-parsley-max" in num_attrs, True) 105 | self.assertEqual(num_attrs["data-parsley-min"], 10) 106 | self.assertEqual(num_attrs["data-parsley-max"], 100) 107 | 108 | 109 | class FormWithWidgetsTest(ParsleyTestCase): 110 | def test_widgets(self): 111 | "Assert that @parsleyfy doesn't cloober existing attrs" 112 | form = FormWithWidgets() 113 | self.assertTrue(form.fields["description"].widget, forms.TextInput) 114 | self.assertEqual("highlight", form.fields["blurb"].widget.attrs["class"]) 115 | 116 | 117 | class TestMetadata(ParsleyTestCase): 118 | def test_docstring(self): 119 | form1 = TextForm() 120 | form2 = parsley_form(TextForm()) 121 | self.assertEqual(form1.__doc__, form2.__doc__) 122 | 123 | def test_module(self): 124 | form1 = TextForm() 125 | form2 = parsley_form(TextForm()) 126 | self.assertEqual(form1.__module__, form2.__module__) 127 | 128 | def test_name(self): 129 | form1 = TextForm() 130 | form2 = parsley_form(TextForm()) 131 | self.assertEqual(form1.__class__.__name__, form2.__class__.__name__) 132 | 133 | 134 | class TestModelForm(ParsleyTestCase): 135 | def test_model_form(self): 136 | form = StudentModelForm() 137 | fields = form.fields 138 | foo_attrs = fields["name"].widget.attrs 139 | self.assertEqual(foo_attrs["data-parsley-required"], "true") 140 | 141 | def test_model_form_save(self): 142 | form = StudentModelForm({"name": "Luke Skywalker"}) 143 | form.save(commit=False) 144 | 145 | 146 | class TestCustomInit(ParsleyTestCase): 147 | def test_custom_init(self): 148 | form = FormWithCustomInit() 149 | self.assertEqual(form.fields["description"].initial, "Hello") 150 | 151 | def test_custom_choices(self): 152 | form = FormWithCustomChoices() 153 | self.assertNotEqual(len(form.fields['state'].choices), 0) 154 | self.assertEqual(form.fields['state'].choices, 155 | [("NY", "NY"), ("OH", "OH")]) 156 | 157 | 158 | class TestRadioSelect(ParsleyTestCase): 159 | def test_radio_select(self): 160 | form = FormWithRadioSelect() 161 | self.assertEqual(form.fields['state'].choices, 162 | [("NY", "NY"), ("OH", "OH")]) 163 | radio_select_html = form.fields['state'].widget.render("state", "NY") 164 | self.assertEqual(1, len(re.findall('data-parsley-required', radio_select_html))) 165 | 166 | def test_radio_select_not_required(self): 167 | form = FormWithRadioSelectNotRequired() 168 | self.assertEqual(form.fields['state'].choices, 169 | [("NY", "NY"), ("OH", "OH")]) 170 | radio_select_html = form.fields['state'].widget.render("state", "NY") 171 | self.assertEqual(0, len(re.findall('data-parsley-required', radio_select_html))) 172 | 173 | 174 | class TestCleanFields(ParsleyTestCase): 175 | def test_clean(self): 176 | form = FormWithCleanField(data={"description": "foo"}) 177 | self.assertEqual(form.is_bound, True) 178 | self.assertEqual(form.is_valid(), False) 179 | self.assertTrue(hasattr(form, "clean_description")) 180 | 181 | def test_clean_parslyfied(self): 182 | form = parsleyfy(FormWithCleanField)(data={"description": "foo"}) 183 | self.assertEqual(form.is_bound, True) 184 | self.assertEqual(form.is_valid(), False) 185 | self.assertTrue(hasattr(form, "clean_description")) 186 | 187 | 188 | class TestExtraAttributes(ParsleyTestCase): 189 | def test_equalto(self): 190 | form = ExtraDataForm() 191 | attrs = form.fields["email2"].widget.attrs 192 | self.assertAttrsEqual(attrs, { 193 | "data-parsley-type": "email", 194 | "data-parsley-required": "true", 195 | "data-parsley-equalto-message": "Must match", 196 | "data-parsley-equalto": "#id_email", 197 | "data-parsley-required-message": _("This field is required."), 198 | }) 199 | 200 | def test_default_data(self): 201 | form = ExtraDataForm() 202 | attrs = form.fields["name"].widget.attrs 203 | self.assertAttrsEqual(attrs, { 204 | "data-parsley-required": "true", 205 | "data-parsley-error-message": "Name invalid", 206 | "data-parsley-required-message": _("This field is required.") 207 | }) 208 | 209 | def test_boolean_values(self): 210 | form = ExtraDataForm() 211 | attrs = form.fields["hide_errors"].widget.attrs 212 | self.assertAttrsEqual(attrs, { 213 | "data-parsley-required": "true", 214 | "data-parsley-show-errors": "false", 215 | "data-parsley-required-message": _("This field is required.") 216 | }) 217 | 218 | def test_missing_field(self): 219 | ExtraDataMissingFieldForm() # No error should be raised 220 | 221 | 222 | class TestMultiValueField(ParsleyTestCase): 223 | def test_parsley_attributes(self): 224 | form = MultiWidgetForm() 225 | fields = form.fields["ssn"].fields 226 | self.assertAttrsEqual(fields[0].widget.attrs, { 227 | "data-parsley-minlength": 3, 228 | "data-parsley-maxlength": 3, 229 | "maxlength": "3", 230 | "data-parsley-pattern": r'^(\d)+$', 231 | }) 232 | self.assertAttrsEqual(fields[1].widget.attrs, { 233 | "data-parsley-minlength": 3, 234 | "data-parsley-maxlength": 3, 235 | "maxlength": "3", 236 | "data-parsley-pattern": r'^(\d)+$', 237 | }) 238 | self.assertAttrsEqual(fields[2].widget.attrs, { 239 | "data-parsley-minlength": 4, 240 | "data-parsley-maxlength": 4, 241 | "maxlength": "4", 242 | "data-parsley-pattern": r'^(\d)+$', 243 | }) 244 | 245 | 246 | class TestCustomErrorMessages(TestCase): 247 | 248 | def test_new_message(self): 249 | form = CustomErrorMessageForm() 250 | attrs = form.fields['name'].widget.attrs 251 | self.assertEqual(attrs, { 252 | "maxlength": '30', 253 | "data-parsley-maxlength": 30, 254 | "data-parsley-maxlength-message": "Please only 30 characters" 255 | }) 256 | 257 | def test_field_type_message(self): 258 | form = CustomErrorMessageForm() 259 | attrs = form.fields['email'].widget.attrs 260 | self.assertEqual(attrs, { 261 | "data-parsley-type": "email", 262 | "data-parsley-type-message": "Invalid email" 263 | }) 264 | 265 | def test_override_default_message(self): 266 | form = CustomErrorMessageForm() 267 | attrs = form.fields['favorite_color'].widget.attrs 268 | self.assertEqual(attrs, { 269 | "data-parsley-required": "true", 270 | "data-parsley-required-message": "Favorite color is required" 271 | }) 272 | 273 | 274 | class TestCustomPrefix(TestCase): 275 | 276 | def test_default_prefix(self): 277 | form = TextForm2() 278 | attrs = form.fields['name'].widget.attrs 279 | self.assertTrue('data-parsley-required' in attrs) 280 | 281 | def test_custom_prefix(self): 282 | form = CustomPrefixForm() 283 | attrs = form.fields['name'].widget.attrs 284 | self.assertTrue('custom-required' in attrs) 285 | 286 | 287 | class TemplateTagTest(ParsleyTestCase): 288 | def test_basic(self): 289 | """ 290 | Tests that parsleyfy will work using the template tag 291 | """ 292 | 293 | template = Template("{% load parsley %}") 294 | form = TextForm() 295 | context = Context({'form': form}) 296 | template.render(context) 297 | 298 | self.assertEqual(context['form'].fields["name"].widget.attrs, {}) 299 | self.assertEqual(context['form'].fields["university"].widget.attrs, {}) 300 | 301 | template = Template("{% load parsley %}{% parsleyfy form as form %}") 302 | form = TextForm() 303 | context = Context({'form': form}) 304 | template.render(context) 305 | 306 | self.assertAttrsEqual(context['form'].fields["name"].widget.attrs, { 307 | "data-parsley-required": "true", 308 | "data-parsley-required-message": _("This field is required.") 309 | }) 310 | self.assertEqual(context['form'].fields["university"].widget.attrs, {}) -------------------------------------------------------------------------------- /parsley/tests/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /parsley/widgets.py: -------------------------------------------------------------------------------- 1 | class ParsleyChoiceFieldRendererMixin(object): 2 | def __init__(self, name, value, attrs, choices): 3 | self.name = name 4 | self.value = value 5 | self.attrs = attrs 6 | self._choices = choices 7 | 8 | @property 9 | def choices(self): 10 | def choices_iter(): 11 | choices_list = self._choices 12 | for idx, choice in enumerate(choices_list): 13 | if idx == len(choices_list) - 1: 14 | self.attrs["{prefix}-required".format(prefix=self.parsley_namespace)] = "true" 15 | yield choice 16 | return choices_iter() 17 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import os 5 | from os.path import dirname, abspath 6 | from optparse import OptionParser 7 | 8 | from django.conf import settings 9 | from django.test.runner import DiscoverRunner 10 | 11 | # For convenience configure settings if they are not pre-configured or if we 12 | # haven't been provided settings to use by environment variable. 13 | if not settings.configured and not os.environ.get('DJANGO_SETTINGS_MODULE'): 14 | settings.configure( 15 | DATABASES={ 16 | 'default': { 17 | 'ENGINE': 'django.db.backends.sqlite3', 18 | } 19 | }, 20 | INSTALLED_APPS=[ 21 | 'parsley', 22 | ], 23 | STATIC_URL="/static/", 24 | TEMPLATES=[ 25 | { 26 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 27 | 'APP_DIRS': True, 28 | }, 29 | ] 30 | ) 31 | 32 | # Setup Django 1.7+ (AppRegistryNotReady). 33 | import django 34 | if hasattr(django, 'setup'): 35 | django.setup() 36 | 37 | 38 | def runtests(*test_args, **kwargs): 39 | if not test_args: 40 | test_args = ['parsley'] 41 | parent = dirname(abspath(__file__)) 42 | sys.path.insert(0, parent) 43 | test_runner = DiscoverRunner( 44 | verbosity=kwargs.get('verbosity', 1), 45 | interactive=kwargs.get('interactive', False), 46 | failfast=kwargs.get('failfast')) 47 | failures = test_runner.run_tests(test_args) 48 | sys.exit(failures) 49 | 50 | if __name__ == '__main__': 51 | parser = OptionParser() 52 | parser.add_option('--failfast', action='store_true', default=False, dest='failfast') 53 | (options, args) = parser.parse_args() 54 | runtests(failfast=options.failfast, *args) 55 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | VERSION = '0.7' 2 | 3 | import os 4 | import sys 5 | from fnmatch import fnmatchcase 6 | from distutils.util import convert_path 7 | from setuptools import setup, find_packages 8 | 9 | 10 | def read(fname): 11 | try: 12 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 13 | except IOError: 14 | return "" 15 | 16 | # Provided as an attribute, so you can append to these instead 17 | # of replicating them: 18 | standard_exclude = ('*.py', '*.pyc', '*$py.class', '*~', '.*', '*.bak') 19 | standard_exclude_directories = ('.*', 'CVS', '_darcs', './build', 20 | './dist', 'EGG-INFO', '*.egg-info') 21 | 22 | # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) 23 | # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 24 | # Note: you may want to copy this into your setup.py file verbatim, as 25 | # you can't import this from another package, when you don't know if 26 | # that package is installed yet. 27 | def find_package_data( 28 | where='.', package='', 29 | exclude=standard_exclude, 30 | exclude_directories=standard_exclude_directories, 31 | only_in_packages=True, 32 | show_ignored=False): 33 | """ 34 | Return a dictionary suitable for use in ``package_data`` 35 | in a distutils ``setup.py`` file. 36 | 37 | The dictionary looks like:: 38 | 39 | {'package': [files]} 40 | 41 | Where ``files`` is a list of all the files in that package that 42 | don't match anything in ``exclude``. 43 | 44 | If ``only_in_packages`` is true, then top-level directories that 45 | are not packages won't be included (but directories under packages 46 | will). 47 | 48 | Directories matching any pattern in ``exclude_directories`` will 49 | be ignored; by default directories with leading ``.``, ``CVS``, 50 | and ``_darcs`` will be ignored. 51 | 52 | If ``show_ignored`` is true, then all the files that aren't 53 | included in package data are shown on stderr (for debugging 54 | purposes). 55 | 56 | Note patterns use wildcards, or can be exact paths (including 57 | leading ``./``), and all searching is case-insensitive. 58 | """ 59 | 60 | out = {} 61 | stack = [(convert_path(where), '', package, only_in_packages)] 62 | while stack: 63 | where, prefix, package, only_in_packages = stack.pop(0) 64 | for name in os.listdir(where): 65 | fn = os.path.join(where, name) 66 | if os.path.isdir(fn): 67 | bad_name = False 68 | for pattern in exclude_directories: 69 | if (fnmatchcase(name, pattern) 70 | or fn.lower() == pattern.lower()): 71 | bad_name = True 72 | if show_ignored: 73 | print >> sys.stderr, ( 74 | "Directory %s ignored by pattern %s" 75 | % (fn, pattern)) 76 | break 77 | if bad_name: 78 | continue 79 | if (os.path.isfile(os.path.join(fn, '__init__.py')) 80 | and not prefix): 81 | if not package: 82 | new_package = name 83 | else: 84 | new_package = package + '.' + name 85 | stack.append((fn, '', new_package, False)) 86 | else: 87 | stack.append((fn, prefix + name + '/', package, only_in_packages)) 88 | elif package or not only_in_packages: 89 | # is a file 90 | bad_name = False 91 | for pattern in exclude: 92 | if (fnmatchcase(name, pattern) 93 | or fn.lower() == pattern.lower()): 94 | bad_name = True 95 | if show_ignored: 96 | print >> sys.stderr, ( 97 | "File %s ignored by pattern %s" 98 | % (fn, pattern)) 99 | break 100 | if bad_name: 101 | continue 102 | out.setdefault(package, []).append(prefix+name) 103 | return out 104 | 105 | setup( 106 | name="django-parsley", 107 | version=VERSION, 108 | description="Django app for client-side form validation. Wrapper for parsley.js", 109 | long_description=read("README.md"), 110 | author="Agiliq", 111 | author_email="hello@agiliq.com", 112 | license="BSD", 113 | url="http://github.com/agiliq/django-parsley", 114 | packages=find_packages(), 115 | package_data=find_package_data("parsley", only_in_packages=False), 116 | classifiers=[ 117 | "Development Status :: 3 - Alpha", 118 | "Environment :: Web Environment", 119 | "Intended Audience :: Developers", 120 | "License :: OSI Approved :: BSD License", 121 | "Operating System :: OS Independent", 122 | "Programming Language :: Python", 123 | "Programming Language :: Python :: 3", 124 | "Framework :: Django", 125 | ], 126 | zip_safe=False, 127 | install_requires=["Django>=1.8"], 128 | tests_require=["Django>=1.8", "six"], 129 | test_suite='runtests.runtests', 130 | ) 131 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{27,34,35}-dj{18,19,110} 4 | py{27,34,35,36}-dj{111} 5 | skipsdist = True 6 | 7 | 8 | [testenv] 9 | commands = coverage run -a setup.py test 10 | basepython = 11 | py27: python2.7 12 | py34: python3.4 13 | py35: python3.5 14 | py36: python3.6 15 | deps = 16 | coverage>=4 17 | dj18: django==1.8.18 18 | dj19: django==1.9.13 19 | dj110: django==1.10.7 20 | dj111: django==1.11.1 21 | --------------------------------------------------------------------------------