├── .gitignore ├── LICENSE ├── README.rst ├── example_project ├── __init__.py ├── manage.py ├── media ├── settings.py ├── templates │ └── test.html └── urls.py ├── livevalidation ├── __init__.py ├── media │ ├── css │ │ └── livevalidation.css │ └── js │ │ └── livevalidation_standalone.compressed.js ├── models.py ├── settings.py ├── templates │ ├── admin │ │ ├── auth │ │ │ └── user │ │ │ │ └── add_form.html │ │ ├── base_site.html │ │ └── change_form.html │ └── livevalidation │ │ └── header.html ├── templatetags │ ├── __init__.py │ └── live_validation.py ├── tests.py └── validator.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LiveValidation is licensed under the terms of the MIT License: 2 | 3 | Copyright (c) 2007 Alec Hill ( www.livevalidation.com ) 4 | 5 | Django-LiveValidation is licensed under the terms of the MIT License: 6 | 7 | Copyright (c) 2010 Justin Quick 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a 10 | copy of this software and associated documentation files (the "Software"), 11 | to deal in the Software without restriction, including without limitation 12 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 13 | and/or sell copies of the Software, and to permit persons to whom the 14 | Software is furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Live Validation 2 | ====================== 3 | 4 | Django Live Validation provides quick and easy client-side form validation which validates as you type. 5 | It uses the `Live Validation `_ JS library in conjunction with Django Forms. 6 | This is by no means a replacement to Django's built in form validation, but it is a suppliment which is purely client-side baed which cuts down on server-side requests for validation. 7 | This version of django-livevalidation requires Django >= 1.2, for previous versions please use this project: http://opensource.washingtontimes.com/projects/django-livevalidation/ 8 | 9 | 10 | Install 11 | -------- 12 | 13 | Place ``'livevalidaiton'`` into your ``INSTALLED_APPS`` and make sure it is above the Django admin since it overrides some of the admin templates:: 14 | 15 | INSTALLED_APPS = ( 16 | 'livevalidation', 17 | ... 18 | 'django.contrib.admin', 19 | ) 20 | 21 | 22 | Usage 23 | ------ 24 | 25 | To use livevalidation in your templates, make sure you load the headers first before doing anything:: 26 | 27 | {% include 'livevalidation/header.html' %} 28 | 29 | This loads the JS library at ``js/livevalidation_standalone.compressed.js`` and the CSS at ``css/livevalidation.css``. Feel free to tweak the CSS to your liking 30 | 31 | Now you can use the templatetag to validate a form instance:: 32 | 33 | {% live_validate form [option=value ...] %} 34 | 35 | Where the ``form`` is any ``django.forms.Form`` (or subclass) instance. 36 | The optional option=value kwargs are in pairs as follows: 37 | 38 | - **validMessage** - message to be used upon successful validation (DEFAULT: "Thankyou!") 39 | - **onValid** - javascript function name to execute when field passes validation 40 | - **onInvalid** - javascript function name to execute when field fails validation 41 | - **insertAfterWhatNode** - id of node to have the message inserted after (DEFAULT: the field that is being validated) 42 | - **onlyOnBlur** - whether you want it to validate as you type or only on blur (DEFAULT: False) 43 | - **wait** - the time you want it to pause from the last keystroke before it validates (milliseconds) (DEFAULT: 0) 44 | - **onlyOnSubmit** - if it is part of a form, whether you want it to validate it only when the form is submitted (DEFAULT: False) -------------------------------------------------------------------------------- /example_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callowayproject/django-livevalidation/0f9dffea23a27618c61d7f88fb7490e81af9a860/example_project/__init__.py -------------------------------------------------------------------------------- /example_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /example_project/media: -------------------------------------------------------------------------------- 1 | ../livevalidation/media -------------------------------------------------------------------------------- /example_project/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for example_project project. 2 | import sys 3 | import os 4 | 5 | ROOT = os.path.dirname(__file__) 6 | 7 | sys.path.insert(0, os.path.join(ROOT, '..')) 8 | 9 | DEBUG = True 10 | TEMPLATE_DEBUG = DEBUG 11 | 12 | ADMINS = ( 13 | # ('Your Name', 'your_email@domain.com'), 14 | ) 15 | 16 | MANAGERS = ADMINS 17 | 18 | DATABASES = { 19 | 'default': { 20 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 21 | 'NAME': 'dev.db', # Or path to database file if using 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 | # On Unix systems, a value of None will cause Django to use the same 29 | # timezone as the operating system. 30 | # If running in a Windows environment this must be set to the same as your 31 | # system time zone. 32 | TIME_ZONE = 'America/Eastern' 33 | 34 | # Language code for this installation. All choices can be found here: 35 | # http://www.i18nguy.com/unicode/language-identifiers.html 36 | LANGUAGE_CODE = 'en-us' 37 | 38 | SITE_ID = 1 39 | 40 | # If you set this to False, Django will make some optimizations so as not 41 | # to load the internationalization machinery. 42 | USE_I18N = False 43 | 44 | # If you set this to False, Django will not format dates, numbers and 45 | # calendars according to the current locale 46 | USE_L10N = True 47 | 48 | # Absolute path to the directory that holds media. 49 | # Example: "/home/media/media.lawrence.com/" 50 | MEDIA_ROOT = os.path.join(ROOT, 'media') 51 | 52 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 53 | # trailing slash if there is a path component (optional in other cases). 54 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 55 | MEDIA_URL = '/media/' 56 | 57 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 58 | # trailing slash. 59 | # Examples: "http://foo.com/media/", "/media/". 60 | ADMIN_MEDIA_PREFIX = '/media/admin/' 61 | 62 | # Make this unique, and don't share it with anybody. 63 | SECRET_KEY = 'dth)zzxz*3=5u=_7$xzb(f+h7ye%d%+__gxlw^6uoh7h=&&!ux' 64 | 65 | # List of callables that know how to import templates from various sources. 66 | TEMPLATE_LOADERS = ( 67 | 'django.template.loaders.filesystem.Loader', 68 | 'django.template.loaders.app_directories.Loader', 69 | # 'django.template.loaders.eggs.Loader', 70 | ) 71 | 72 | MIDDLEWARE_CLASSES = ( 73 | 'django.middleware.common.CommonMiddleware', 74 | 'django.contrib.sessions.middleware.SessionMiddleware', 75 | 'django.middleware.csrf.CsrfViewMiddleware', 76 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 77 | 'django.contrib.messages.middleware.MessageMiddleware', 78 | ) 79 | 80 | ROOT_URLCONF = 'example_project.urls' 81 | 82 | TEMPLATE_DIRS = ( 83 | os.path.join(ROOT, 'templates'), 84 | ) 85 | 86 | INSTALLED_APPS = ( 87 | 'livevalidation', 88 | 'django.contrib.auth', 89 | 'django.contrib.contenttypes', 90 | 'django.contrib.sessions', 91 | 'django.contrib.sites', 92 | 'django.contrib.messages', 93 | 'django.contrib.admin', 94 | ) 95 | 96 | try: 97 | import django_coverage 98 | TEST_RUNNER = 'django_coverage.coverage_runner.run_tests' 99 | except ImportError: 100 | pass -------------------------------------------------------------------------------- /example_project/templates/test.html: -------------------------------------------------------------------------------- 1 | {% load lv_tags %} 2 | 3 | 4 | {% include 'lv/header.html' %} 5 | 6 | 7 | 8 |
9 | {{ form.as_p }} 10 | 11 | {% live_validate form failureMessage=OhNo! %} 12 |
13 | 14 | -------------------------------------------------------------------------------- /example_project/urls.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.conf.urls.defaults import * 3 | from django.contrib import admin 4 | 5 | admin.autodiscover() 6 | 7 | urlpatterns = patterns('', 8 | (r'^admin/', include(admin.site.urls)), 9 | (r'^media/(?P.*)$', 'django.views.static.serve', 10 | {'document_root': os.path.join(os.path.dirname(__file__), 'media')}), 11 | ) 12 | -------------------------------------------------------------------------------- /livevalidation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callowayproject/django-livevalidation/0f9dffea23a27618c61d7f88fb7490e81af9a860/livevalidation/__init__.py -------------------------------------------------------------------------------- /livevalidation/media/css/livevalidation.css: -------------------------------------------------------------------------------- 1 | .LV_validation_message{ 2 | font-weight:bold; 3 | margin:0 0 0 5px; 4 | } 5 | 6 | .LV_valid { 7 | color:#00CC00; 8 | } 9 | 10 | .LV_invalid { 11 | color:#CC0000; 12 | } 13 | 14 | .LV_valid_field, 15 | input.LV_valid_field:hover, 16 | input.LV_valid_field:active, 17 | textarea.LV_valid_field:hover, 18 | textarea.LV_valid_field:active { 19 | border: 1px solid #00CC00; 20 | } 21 | 22 | .LV_invalid_field, 23 | input.LV_invalid_field:hover, 24 | input.LV_invalid_field:active, 25 | textarea.LV_invalid_field:hover, 26 | textarea.LV_invalid_field:active { 27 | border: 1px solid #CC0000; 28 | } -------------------------------------------------------------------------------- /livevalidation/media/js/livevalidation_standalone.compressed.js: -------------------------------------------------------------------------------- 1 | // LiveValidation 1.3 (standalone version) 2 | // Copyright (c) 2007-2008 Alec Hill (www.livevalidation.com) 3 | // LiveValidation is licensed under the terms of the MIT License 4 | var LiveValidation=function(B,A){this.initialize(B,A);};LiveValidation.VERSION="1.3 standalone";LiveValidation.TEXTAREA=1;LiveValidation.TEXT=2;LiveValidation.PASSWORD=3;LiveValidation.CHECKBOX=4;LiveValidation.SELECT=5;LiveValidation.FILE=6;LiveValidation.massValidate=function(C){var D=true;for(var B=0,A=C.length;B=300){this.removeMessageAndFieldClass();}var A=this;if(this.timeout){clearTimeout(A.timeout);}this.timeout=setTimeout(function(){A.validate();},A.wait);},doOnBlur:function(A){this.focused=false;this.validate(A);},doOnFocus:function(A){this.focused=true;this.removeMessageAndFieldClass();},getElementType:function(){switch(true){case (this.element.nodeName.toUpperCase()=="TEXTAREA"):return LiveValidation.TEXTAREA;case (this.element.nodeName.toUpperCase()=="INPUT"&&this.element.type.toUpperCase()=="TEXT"):return LiveValidation.TEXT;case (this.element.nodeName.toUpperCase()=="INPUT"&&this.element.type.toUpperCase()=="PASSWORD"):return LiveValidation.PASSWORD;case (this.element.nodeName.toUpperCase()=="INPUT"&&this.element.type.toUpperCase()=="CHECKBOX"):return LiveValidation.CHECKBOX;case (this.element.nodeName.toUpperCase()=="INPUT"&&this.element.type.toUpperCase()=="FILE"):return LiveValidation.FILE;case (this.element.nodeName.toUpperCase()=="SELECT"):return LiveValidation.SELECT;case (this.element.nodeName.toUpperCase()=="INPUT"):throw new Error("LiveValidation::getElementType - Cannot use LiveValidation on an "+this.element.type+" input!");default:throw new Error("LiveValidation::getElementType - Element must be an input, select, or textarea!");}},doValidations:function(){this.validationFailed=false;for(var C=0,A=this.validations.length;CNumber(C)){Validate.fail(K);}break;}return true;},Format:function(C,E){var C=String(C);var E=E||{};var A=E.failureMessage||"Not valid!";var B=E.pattern||/./;var D=E.negate||false;if(!D&&!B.test(C)){Validate.fail(A);}if(D&&B.test(C)){Validate.fail(A);}return true;},Email:function(B,C){var C=C||{};var A=C.failureMessage||"Must be a valid email address!";Validate.Format(B,{failureMessage:A,pattern:/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i});return true;},Length:function(F,G){var F=String(F);var G=G||{};var E=((G.minimum)||(G.minimum==0))?G.minimum:null;var H=((G.maximum)||(G.maximum==0))?G.maximum:null;var C=((G.is)||(G.is==0))?G.is:null;var A=G.wrongLengthMessage||"Must be "+C+" characters long!";var B=G.tooShortMessage||"Must not be less than "+E+" characters long!";var D=G.tooLongMessage||"Must not be more than "+H+" characters long!";switch(true){case (C!==null):if(F.length!=Number(C)){Validate.fail(A);}break;case (E!==null&&H!==null):Validate.Length(F,{tooShortMessage:B,minimum:E});Validate.Length(F,{tooLongMessage:D,maximum:H});break;case (E!==null):if(F.lengthNumber(H)){Validate.fail(D);}break;default:throw new Error("Validate::Length - Length(s) to validate against must be provided!");}return true;},Inclusion:function(H,F){var F=F||{};var K=F.failureMessage||"Must be included in the list!";var G=(F.caseSensitive===false)?false:true;if(F.allowNull&&H==null){return true;}if(!F.allowNull&&H==null){Validate.fail(K);}var D=F.within||[];if(!G){var A=[];for(var C=0,B=D.length;C{% trans "First, enter a username and password. Then, you'll be able to edit more user options." %}

7 | {% else %} 8 |

{% trans "Enter a username and password." %}

9 | {% endif %} 10 | {% endblock %} 11 | 12 | {% block after_field_sets %} 13 | 14 | {% endblock %} 15 | 16 | {% block livevalidation %} 17 | {{ block.super }} 18 | 21 | {% endblock %} -------------------------------------------------------------------------------- /livevalidation/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{{ title }} | {% trans 'Django site admin' %}{% endblock %} 5 | 6 | {% block branding %} 7 |

{% trans 'Django administration' %}

8 | {% endblock %} 9 | 10 | {% block nav-global %}{% endblock %} 11 | 12 | {% block extrahead %}{{ block.super }} 13 | {% include 'livevalidation/header.html' %} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /livevalidation/templates/admin/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n admin_modify adminmedia live_validation %} 3 | 4 | {% block extrahead %}{{ block.super }} 5 | {% url admin:jsi18n as jsi18nurl %} 6 | 7 | {{ media }} 8 | {% endblock %} 9 | 10 | {% block extrastyle %}{{ block.super }}{% endblock %} 11 | 12 | {% block coltype %}{% if ordered_objects %}colMS{% else %}colM{% endif %}{% endblock %} 13 | 14 | {% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %} 15 | 16 | {% block breadcrumbs %}{% if not is_popup %} 17 | 23 | {% endif %}{% endblock %} 24 | 25 | {% block content %}
26 | {% block object-tools %} 27 | {% if change %}{% if not is_popup %} 28 | 31 | {% endif %}{% endif %} 32 | {% endblock %} 33 |
{% csrf_token %}{% block form_top %}{% endblock %} 34 |
35 | {% if is_popup %}{% endif %} 36 | {% if save_on_top %}{% submit_row %}{% endif %} 37 | {% if errors %} 38 |

39 | {% blocktrans count errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} 40 |

41 | {{ adminform.form.non_field_errors }} 42 | {% endif %} 43 | 44 | {% for fieldset in adminform %} 45 | {% include "admin/includes/fieldset.html" %} 46 | {% endfor %} 47 | 48 | {% block after_field_sets %}{% endblock %} 49 | 50 | {% for inline_admin_formset in inline_admin_formsets %} 51 | {% include inline_admin_formset.opts.template %} 52 | {% endfor %} 53 | 54 | {% block after_related_objects %}{% endblock %} 55 | 56 | {% submit_row %} 57 | 58 | {% if adminform and add %} 59 | 60 | {% endif %} 61 | 62 | {% if adminform %} 63 | {% block livevalidation %}{% live_validate adminform %}{% endblock %} 64 | {% endif %} 65 | 66 | {# JavaScript for prepopulated fields #} 67 | {% prepopulated_fields_js %} 68 | 69 |
70 |
71 | {% endblock %} 72 | -------------------------------------------------------------------------------- /livevalidation/templates/livevalidation/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /livevalidation/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callowayproject/django-livevalidation/0f9dffea23a27618c61d7f88fb7490e81af9a860/livevalidation/templatetags/__init__.py -------------------------------------------------------------------------------- /livevalidation/templatetags/live_validation.py: -------------------------------------------------------------------------------- 1 | from livevalidation.validator import * 2 | from livevalidation.settings import * 3 | from django import template 4 | from django.forms import fields 5 | 6 | register = template.Library() 7 | 8 | class ValidationNode(template.Node): 9 | def __init__(self, form, *opts): 10 | self.form = form 11 | self.opts = {'validMessage':' '} 12 | for opt in opts: 13 | a,b = map(str,opt.split('=')[:2]) 14 | if a in ('onValid','onInvalid'): 15 | b = '%s()'%b 16 | self.opts[a] = b 17 | 18 | def render(self, context): 19 | result = ['') 40 | return '\n\n'.join(filter(None,result)) 41 | 42 | def do_field(self, name, field, count=0): 43 | fname = 'id_%s'%name 44 | # TODO: make a special case for the split dt field (id_0,id_1) 45 | #if isinstance(field, fields.SplitDateTimeField): 46 | # fname += '_%d'%count 47 | if field.__class__ in LV_FIELDS and not LV_FIELDS[field.__class__]: 48 | self.opts.update(onlyOnSubmit=True) 49 | lv = LiveValidation(fname, **self.opts) 50 | fail = field.default_error_messages.get('invalid',None) 51 | extrakw = {'validMessage':' '} 52 | if fail: 53 | extrakw['failureMessage'] = str(fail[:]) 54 | if self.formcls in LV_VALIDATORS: 55 | if name in LV_VALIDATORS[self.formcls]: 56 | for v,kw in LV_VALIDATORS[self.formcls][name].items(): 57 | extrakw.update(kw) 58 | lv.add(v,**extrakw) 59 | return str(lv) 60 | # We have to check for FileFields and ImageFields since if you are changing 61 | # a form, they will already be set, and you don't need to re-upload them. 62 | # TODO: Find a way around skipping file and image fields 63 | if hasattr(field,'required') and field.required and not isinstance(field, (fields.FileField, fields.ImageField)): 64 | lv.add(Presence, **extrakw) 65 | #else: 66 | # return str(lv) 67 | if hasattr(field, 'max_length'): 68 | v = getattr(field,'max_length') 69 | if v: lv.add(Length, maximum=v, **extrakw) 70 | if hasattr(field, 'min_length'): 71 | v = getattr(field,'min_length') 72 | if v: lv.add(Length, minimum=v, **extrakw) 73 | if not (isinstance(field, fields.EmailField) or isinstance(field, fields.URLField)) and hasattr(field, 'regex'): 74 | lv.add(Format, pattern=field.regex.pattern, **extrakw) 75 | if field.__class__ in LV_FIELDS and LV_FIELDS[field.__class__]: 76 | for v,kw in LV_FIELDS[field.__class__].items(): 77 | extrakw.update(kw) 78 | lv.add(v, **extrakw) 79 | 80 | if str(lv): 81 | return """try{ 82 | %s 83 | }catch(e){}"""%str(lv) 84 | return '' 85 | 86 | def live_validate(parser, token): 87 | """Live Validation JavaScript Generator for Django Forms 88 | 89 | {% live_validate
[option=value ...] %} 90 | 91 | Where the is any django.forms.Form (or subclass) instance 92 | The optional option=value kwargs are in pairs as follows: 93 | 94 | - validMessage = message to be used upon successful validation (DEFAULT: "Thankyou!") 95 | - onValid = javascript function name to execute when field passes validation 96 | - onInvalid = javascript function name to execute when field fails validation 97 | - insertAfterWhatNode = id of node to have the message inserted after (DEFAULT: the field that is being validated) 98 | - onlyOnBlur = whether you want it to validate as you type or only on blur (DEFAULT: False) 99 | - wait = the time you want it to pause from the last keystroke before it validates (milliseconds) (DEFAULT: 0) 100 | - onlyOnSubmit = if it is part of a form, whether you want it to validate it only when the form is submitted (DEFAULT: False) 101 | """ 102 | return ValidationNode(*token.split_contents()[1:]) 103 | register.tag(live_validate) 104 | -------------------------------------------------------------------------------- /livevalidation/tests.py: -------------------------------------------------------------------------------- 1 | from doctest import testmod 2 | 3 | from django.test import TestCase 4 | from django import template 5 | from django.contrib.auth.forms import UserChangeForm 6 | 7 | from livevalidation import validator 8 | 9 | 10 | class TestValidation(TestCase): 11 | def test_form(self): 12 | t = template.Template('{% load live_validation %}{% live_validate form %}') 13 | content = t.render(template.Context({'form':UserChangeForm()})) 14 | 15 | look_for = [ 16 | "LVid_username.add(Validate.Format, { failureMessage: 'Alphanumeric characters only!', pattern: new RegExp(/^\w+$/), validMessage: ' ' });", 17 | "LVid_last_name.add(Validate.Length, { failureMessage: 'Enter a valid value.', maximum: 30, validMessage: ' ' });", 18 | #"LVid_email.add(Validate.Email, { failureMessage: 'Enter a valid e-mail address.', validMessage: ' ' });", 19 | "LVid_last_login.add(Validate.Format, { failureMessage: 'Must be in valid \"YYYY-MM-DD HH:MM:SS\" format!', pattern: new RegExp(/^(19|20)\d\d\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01]) ([0-1]\d|2[0-3]):([0-5]\d):([0-5]\d)$/), validMessage: ' ' });", 20 | ] 21 | 22 | for text in look_for: 23 | self.assert_(content.find(text) >- 1) 24 | 25 | def test_validator(self): 26 | testmod(validator) 27 | -------------------------------------------------------------------------------- /livevalidation/validator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Wrapper for Live Validation JavaScript 3 | 4 | Generates javascript using template tags to comply with the LiveValidation library. 5 | When used in combination, any form instance can be turned into javascript code that 6 | validates the form fields before posting the form. This reduces server load by cutting 7 | down on requests containing invalid fields (eg email=IAmSoNotAnEmail) and 8 | improves user experience with live feedback and reduces human error. 9 | """ 10 | 11 | def inner(items): 12 | """ 13 | Sorted items to display as compatable js objects (eg bool,regex) 14 | """ 15 | for k,v in sorted(items): 16 | if k == 'is_': 17 | k = 'is' 18 | if isinstance(v,bool): 19 | yield '%s: %s'%(k,repr(v).lower()) 20 | elif k == 'pattern': 21 | yield '%s: new RegExp(/%s/)'%(k,v) 22 | else: 23 | yield '%s: %r'%(k,v) 24 | 25 | class Meta: 26 | """ 27 | Abstract meta class for formatting the javascript commands 28 | """ 29 | def __init__(self, *a, **kw): 30 | self.a = a 31 | self.kw = kw 32 | 33 | def format_kw(self): 34 | return '{ %s }'%', '.join(inner(self.kw.items())) 35 | 36 | 37 | def format_a(self): 38 | if not len(self.a): return 39 | if isinstance(self.a[0],basestring): 40 | return repr(self.a[0]) 41 | elif isinstance(self.a[0],bool): 42 | return repr(self.a[0]).lower() 43 | return self.a[0] 44 | 45 | def __str__(self): 46 | if len(self.a): 47 | return 'Validate.%s( %s, %s)'% ( 48 | self.__class__.__name__, 49 | self.format_a(), 50 | self.format_kw() 51 | ) 52 | return 'Validate.%s, %s'% ( 53 | self.__class__.__name__, 54 | self.format_kw() 55 | ) 56 | 57 | class Presence(Meta): 58 | """Validates that a value is present (ie. not null, undefined, or an empty string) 59 | 60 | args: 61 | - value - {mixed} - value to be checked 62 | 63 | kwargs: 64 | - failureMessage (optional) - {String} - message to be used upon validation failure (DEFAULT: "Can't be empty!") 65 | 66 | >>> print Presence('hello world', failureMessage='Supply a value!') 67 | Validate.Presence( 'hello world', { failureMessage: 'Supply a value!' }) 68 | """ 69 | 70 | class Format(Meta): 71 | """Validates a value against a regular expression 72 | 73 | args: 74 | - value - {mixed} - value to be checked 75 | 76 | kwargs: 77 | - failureMessage (optional) - {String} - message to be used upon validation failure (DEFAULT: "Not valid!") 78 | - pattern - {RegExp} - regular expression to validate against (DEFAULT: /./i) 79 | - negate - {Boolean} - if true will be valid if the value DOES NOT match the regular expression (DEFAULT: false) 80 | 81 | >>> # check that 'validation' exists in the string, case insensitive... 82 | >>> print Format('live validation', pattern = r'^validation$', failureMessage = 'Failed!' ) 83 | Validate.Format( 'live validation', { failureMessage: 'Failed!', pattern: new RegExp(/^validation$/) }) 84 | """ 85 | 86 | class Numericality(Meta): 87 | """Validates that the value is numeric and: is an integer, is a specific number, 88 | is more than a minimum number, less than a maximum number, is within a range of numbers, or a combination of these 89 | 90 | args: 91 | - value - {mixed} - value to be checked 92 | 93 | kwargs: 94 | - notANumberMessage (optional) - {String} - message to be used when validation fails because value is not a number (DEFAULT: "Must be a number!") 95 | - notAnIntegerMessage (optional) - {String} - message to be used when validation fails because value is not an integer (DEFAULT: "Must be an integer!") 96 | - wrongNumberMessage (optional) - {String} - message to be used when validation fails when 'is_' param is used (DEFAULT: "Must be {is}!") 97 | - tooLowMessage (optional) - {String} - message to be used when validation fails when 'minimum' param is used (DEFAULT: "Must not be less than {minimum}!") 98 | - tooHighMessage (optional) - {String} - message to be used when validation fails when 'maximum' param is used (DEFAULT: "Must not be more than {maximum}!") 99 | - is_ (optional) - {mixed} - the value must be equal to this numeric value 100 | - minimum (optional) - {mixed} - the minimum numeric allowed 101 | - maximum (optional) - {mixed} - the maximum numeric allowed 102 | - onlyInteger (optional) - {Boolean} - if true will only allow integers to be valid (DEFAULT: false) 103 | 104 | >>> # check that value is an integer between -5 and 2000 exists in the string, case insensitive... 105 | >>> print Numericality( 2000.0, minimum= -5, maximum= 2000) 106 | Validate.Numericality( 2000.0, { maximum: 2000, minimum: -5 }) 107 | """ 108 | 109 | class Length(Meta): 110 | """Validates the length of a value is a particular length, 111 | is more than a minimum, less than a maximum, or between a range of lengths 112 | 113 | args: 114 | - value - {mixed} - value to be checked 115 | 116 | kwargs: 117 | - wrongLengthMessage (optional) - {String} - message to be used when validation fails when 'is_' param is used (DEFAULT: "Must be {is_} characters long!") 118 | - tooShortMessage (optional) - {String} - message to be used when validation fails when 'minimum' param is used (DEFAULT: "Must not be less than {minimum} characters long!") 119 | - tooLongMessage (optional) - {String} - message to be used when validation fails when 'maximum' param is used (DEFAULT: "Must not be more than {maximum} characters long!") 120 | - is_ (optional) - {mixed} - the value must be this length 121 | - minimum (optional) - {mixed} - the minimum length allowed 122 | - maximum (optional) - {mixed} - the maximum length allowed 123 | 124 | >>> # check that value is between 3 and 255 characters long... 125 | >>> print Length( 'cow', minimum=3, maximum=255 ) 126 | Validate.Length( 'cow', { maximum: 255, minimum: 3 }) 127 | """ 128 | 129 | class Inclusion(Meta): 130 | """Validates that a value falls within a given set of values 131 | 132 | args: 133 | - value - {mixed} - value to be checked 134 | 135 | kwargs: 136 | - failureMessage (optional) - {String} - message to be used upon validation failure (DEFAULT: "Must be included in the list!") 137 | - within - {Array} - an array of values that the value should fall in (DEFAULT: Empty array) 138 | - allowNull (optional) - {Boolean} - if true, and a null value is passed in, validates as true (DEFAULT: false) 139 | - partialMatch (optional) - {Boolean}- if true, will not only validate against the whole value to check, but also if it is a substring of the value (DEFAULT: false) 140 | - caseSensitive (optional) - {Boolean} - if false will compare strings case insensitively(DEFAULT: true) 141 | 142 | >>> print Inclusion( 'cat', within = [ 'cow', 277, 'catdog' ], allowNull = True, partialMatch = True, caseSensitive= False) 143 | Validate.Inclusion( 'cat', { allowNull: true, caseSensitive: false, partialMatch: true, within: ['cow', 277, 'catdog'] }) 144 | """ 145 | 146 | class Exclusion(Meta): 147 | """Validates that a value does not fall within a given set of values 148 | 149 | args: 150 | - value - {mixed} - value to be checked 151 | 152 | kwargs: 153 | - failureMessage (optional) - {String} - message to be used upon validation failure (DEFAULT: "Must not be included in the list!") 154 | - within - {Array} - an array of values that the given value should not fall in (DEFAULT: Empty array) 155 | - allowNull (optional) - {Boolean} - if true, and a null value is passed in, validates as true (DEFAULT: false) 156 | - partialMatch (optional) - {Boolean} - if true, will not only validate against the whole value to check, but also if it is a substring of the value (DEFAULT: false) 157 | - caseSensitive (optional) - {Boolean} - if false will compare strings case insensitively(DEFAULT: true) 158 | 159 | >>> print Exclusion( 'pig', within = [ 'cow', 277, 'catdog' ], allowNull = True, partialMatch = True, caseSensitive= False) 160 | Validate.Exclusion( 'pig', { allowNull: true, caseSensitive: false, partialMatch: true, within: ['cow', 277, 'catdog'] }) 161 | """ 162 | 163 | class Acceptance(Meta): 164 | """Validates that a value equates to true (for use primarily in detemining if a checkbox has been checked) 165 | 166 | args: 167 | - value - {mixed} - value to be checked 168 | 169 | kwargs: 170 | - failureMessage (optional) - {String} - message to be used upon validation failure (DEFAULT: "Must be accepted!") 171 | 172 | >>> print Acceptance( True, failureMessage="You must be true!" ) 173 | Validate.Acceptance( true, { failureMessage: 'You must be true!' }) 174 | """ 175 | 176 | class Confirmation(Meta): 177 | """Validates that a value matches that of a given form field 178 | 179 | args: 180 | - value - {mixed} - value to be checked 181 | 182 | kwargs: 183 | - failureMessage (optional) - {String} - message to be used upon validation failure (DEFAULT: "Does not match!") 184 | - match -{mixed} - a reference to, or string id of the field that this should match 185 | 186 | >>> print Confirmation( 'open sesame', match = 'myPasswordField', failureMessage= "Your passwords don't match!" ) 187 | Validate.Confirmation( 'open sesame', { failureMessage: "Your passwords don't match!", match: 'myPasswordField' }) 188 | """ 189 | 190 | class Email(Meta): 191 | """Validates a value is a valid email address 192 | 193 | args: 194 | - value - {mixed} - value to be checked 195 | 196 | kwargs: 197 | - failureMessage (optional) - {String} - message to be used upon validation failure (DEFAULT: "Must be a valid email address!") 198 | 199 | >>> print Email( 'live@validation.com', failureMessage= "I am an overridden message!" ) 200 | Validate.Email( 'live@validation.com', { failureMessage: 'I am an overridden message!' }) 201 | """ 202 | 203 | class Custom(Meta): 204 | """Validates a value against a custom function that returns true when valid or false when not valid. 205 | You can use this to easily wrap any special validations that are not covered by the core ones, 206 | in a way that the LiveValidation class can use to give the feedback, without having to worry about the details. 207 | 208 | args: 209 | - value - {mixed} - value to be checked 210 | 211 | kwargs: 212 | - against - {Function} - a function that will take the value and an object of arguments and return true or false(DEFAULT: function( value, args ){ return true; } ) 213 | - args - {Object} - an object of named arguments that will be passed to the custom function so are accessible through this object (DEFAULT: Empty object) 214 | - failureMessage (optional) - {String} - message to be used upon validation failure (DEFAULT: "Not valid!") 215 | 216 | #>>> # Pass a function that checks if a number is divisible by one that you pass it in args object 217 | #>>> # In this case, 5 is passed, so should return true and validation will pass 218 | #>>> Custom( 55, against="function(value,args){ return !(value % args.divisibleBy) }", args= "{divisibleBy: 5}" ) 219 | #... "Validate.Custom( 55, { against: function(value,args){ return !(value % args.divisibleBy) }, args: {divisibleBy: 5} } );" 220 | """ 221 | 222 | class now(Meta): 223 | """Validates a passed in value using the passed in validation function, 224 | and handles the validation error for you so it gives a nice true or false reply 225 | 226 | args: 227 | - validationFunction - {Function} - reference to the validation function to be used (ie Validate.Presence ) 228 | - value - {mixed} - value to be checked 229 | 230 | kwargs: 231 | - Parameters to be used for the validation (optional depends upon the validation function) 232 | 233 | >>> print now( Numericality, '2007', is_= 2007 ) 234 | Validate.now( Numericality, '2007', { is: 2007 }) 235 | """ 236 | def format_a(self): 237 | return '%s, %r'%(self.a[0].__name__,self.a[1]) 238 | 239 | class LiveValidation: 240 | """The LiveValidation class sets up a text, checkbox, file, or password input, 241 | or a textarea to allow its value to be validated in real-time based upon the validations you assign to it. 242 | 243 | args: 244 | - element - {String} - the string id of the element to validate 245 | 246 | kwargs: 247 | - validMessage (optional) - {String} - message to be used upon successful validation (DEFAULT: "Thankyou!") 248 | - onValid (optional) - {Function} - function to execute when field passes validation (DEFAULT: function(){ this.insertMessage( this.createMessageSpan() ); this.addFieldClass(); } ) 249 | - onInvalid (optional) - {Function} - function to execute when field fails validation (DEFAULT: function(){ this.insertMessage( this.createMessageSpan() ); this.addFieldClass(); }) 250 | - insertAfterWhatNode (optional) - {mixed} - reference or id of node to have the message inserted after (DEFAULT: the field that is being validated) 251 | - onlyOnBlur (optional) - {Boolean} - whether you want it to validate as you type or only on blur (DEFAULT: false) 252 | - wait (optional) - {Integer} - the time you want it to pause from the last keystroke before it validates (milliseconds) (DEFAULT: 0) 253 | - onlyOnSubmit (optional) - {Boolean} - if it is part of a form, whether you want it to validate it only when the form is submitted (DEFAULT: false) 254 | 255 | >>> usern = LiveValidation('id_username', wait=10) 256 | >>> usern.add(Format, pattern= r'/^hello$/i') #doctest: +ELLIPSIS 257 | >> usern.disable() #doctest: +ELLIPSIS 259 | >> usern.enable() #doctest: +ELLIPSIS 261 | >> usern.remove(Format, minimum= r'/^woohoo+$/') #doctest: +ELLIPSIS 263 | 1: 326 | return '\n'.join(self.commands) 327 | return '' 328 | 329 | 330 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | try: 4 | long_description = open('README.rst').read() 5 | except IOError: 6 | long_description = '' 7 | 8 | setup( 9 | name = "django-livevalidation", 10 | version = '0.1.1', 11 | url = 'http://github.com/washingtontimes/django-livevalidation', 12 | author = 'Justin Quick', 13 | author_email = 'justquick@gmail.com', 14 | description = 'Live validation for Django forms. It validates as you type. Uses scripts from livevalidation.com', 15 | packages = find_packages(), 16 | include_package_data = True, 17 | long_description = long_description, 18 | ) 19 | --------------------------------------------------------------------------------