From 592a0b6971d867cf1ec9d82c5202085cedc9e6b5 Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Wed, 26 Nov 2014 15:38:21 +0000 Subject: [PATCH 01/25] added ngMessages support and example --- client/src/js/ng-django-forms.js | 4 +- client/src/js/ng-django-messages.js | 111 ++++++++++++++++++ djangular/forms/__init__.py | 1 + djangular/forms/angular_messages.py | 40 +++++++ djangular/static/djangular/css/styles.css | 9 +- examples/server/forms/ng_messages.py | 17 +++ examples/server/static/css/djangular-demo.css | 9 +- examples/server/static/js/lodash.min.js | 56 +++++++++ examples/server/static/js/messages-demo.js | 15 +++ examples/server/templates/base.html | 6 + examples/server/templates/model-scope.html | 2 +- examples/server/templates/ng-messages.html | 51 ++++++++ examples/server/urls.py | 3 + examples/server/views/ng_messages.py | 24 ++++ examples/test.sqlite | Bin 0 -> 172032 bytes 15 files changed, 340 insertions(+), 8 deletions(-) create mode 100644 client/src/js/ng-django-messages.js create mode 100644 djangular/forms/angular_messages.py create mode 100644 examples/server/forms/ng_messages.py create mode 100644 examples/server/static/js/lodash.min.js create mode 100644 examples/server/static/js/messages-demo.js create mode 100644 examples/server/templates/ng-messages.html create mode 100644 examples/server/views/ng_messages.py create mode 100644 examples/test.sqlite diff --git a/client/src/js/ng-django-forms.js b/client/src/js/ng-django-forms.js index d8d8bdc..02e4866 100644 --- a/client/src/js/ng-django-forms.js +++ b/client/src/js/ng-django-forms.js @@ -286,7 +286,9 @@ djng_forms_module.factory('djangoForm', function() { } else { // this field is a composite of input elements angular.forEach(field, function(subField, subKey) { - if (angular.isArray(subField.$viewChangeListeners)) { + //this can occur in 1.3 due to new 'pending' prop being undefined + if (angular.isDefined(subField) && + angular.isArray(subField.$viewChangeListeners)) { resetFieldValidity(subField); } }); diff --git a/client/src/js/ng-django-messages.js b/client/src/js/ng-django-messages.js new file mode 100644 index 0000000..51120f8 --- /dev/null +++ b/client/src/js/ng-django-messages.js @@ -0,0 +1,111 @@ +(function(angular, undefined) { +'use strict'; + +angular + .module('ng.django.messages',[ + 'ngMessages' + ]) + + .directive('validateRejected', validateRejected) + .factory('djangoMessagesForm', djangoMessagesForm); + + + + +function validateRejected() { + + return { + require: 'ngModel', + link: function($scope, $element, $attrs, ngModel) { + + var _hasMessage = false, + _value; + + ngModel.$validators.rejected = function(value) { + + if(_hasMessage && (_value && _value !== value)) { + + _hasMessage = false; + _value = undefined; + ngModel.$message = undefined; + + }else{ + + _hasMessage = ngModel.$message !== undefined; + if(_hasMessage) { + _value = value; + } + } + + return !_hasMessage; + } + } + } +} + + +function djangoMessagesForm() { + + return { + setErrors: setErrors + } + + /* ============================ */ + + function setErrors(form, errors) { + _clearFormMessage(form); + _displayErrors(form, errors); + return _isNotEmpty(errors); + }; + + function _clearFormMessage(form) { + form.$message = ''; + }; + + function _displayErrors(form, errors) { + angular.forEach(errors, + function(error, key) { + var field, + message = error[0]; + + form.$setDirty(); + + if(form.hasOwnProperty(key)) { + + field = form[key]; + field.$dirty = true; + field.$message = message; + + if (angular.isFunction(field.$validate)) { + field.$validate(); + } else { + // this field is a composite of input elements + angular.forEach(field, function(subField, subKey) { + if (angular.isDefined(subField) && + angular.isFunction(subField.$validate)) { + subField.$validate(); + } + }); + } + + }else{ + + form.$message = message; + form.$setPristine(); + } + } + ); + } + + function _isNotEmpty(obj) { + for (var p in obj) { + if (obj.hasOwnProperty(p)) + return true; + } + return false; + } +}; + + + +})(window.angular); diff --git a/djangular/forms/__init__.py b/djangular/forms/__init__.py index 7d07c0d..949d4da 100644 --- a/djangular/forms/__init__.py +++ b/djangular/forms/__init__.py @@ -11,6 +11,7 @@ from .models import PatchedModelFormMetaclass as ModelFormMetaclass else: from django.forms.models import ModelFormMetaclass +from .angular_messages import NgMessagesMixin class NgDeclarativeFieldsMetaclass(BaseFieldsModifierMetaclass, DeclarativeFieldsMetaclass): diff --git a/djangular/forms/angular_messages.py b/djangular/forms/angular_messages.py new file mode 100644 index 0000000..fef4c1d --- /dev/null +++ b/djangular/forms/angular_messages.py @@ -0,0 +1,40 @@ +from django.utils.html import format_html +from django.utils.encoding import force_text +from django.utils.safestring import mark_safe, SafeText + +from djangular.forms.angular_base import TupleErrorList +from djangular.forms.angular_base import NgFormBaseMixin + + +class NgMessagesTupleErrorList(TupleErrorList): + + msgs_format = '
{2}
' + msg_format = '
{3}
' + """ span's necessary due to this bug https://github.com/angular/angular.js/issues/8089""" + msg_format_bind = '
' + + def as_ul(self): + if not self: + return SafeText() + first = self[0] + if isinstance(first, tuple): + error_list = [] + for e in self: + if e[3] == '$valid': + continue + msg_format = e[5] == '$message' and self.msg_format_bind or self.msg_format + msg_type = e[3].split('.') + err_tuple = (e[0], msg_type[0] if len(msg_type) == 1 else msg_type.pop(), e[4], force_text(e[5])) + error_list.append(format_html(msg_format, *err_tuple)) + + return (error_list and \ + format_html(self.msgs_format, first[0], first[1], mark_safe(''.join(error_list))) + or '') + + +class NgMessagesMixin(NgFormBaseMixin): + def __init__(self, data=None, *args, **kwargs): + kwargs['error_class'] = NgMessagesTupleErrorList + super(NgMessagesMixin, self).__init__(data, *args, **kwargs) + for field in self.fields.itervalues(): + field.widget.attrs['validate-rejected'] = "" \ No newline at end of file diff --git a/djangular/static/djangular/css/styles.css b/djangular/static/djangular/css/styles.css index 5ece16e..2baea64 100644 --- a/djangular/static/djangular/css/styles.css +++ b/djangular/static/djangular/css/styles.css @@ -9,16 +9,19 @@ form .ng-invalid-bound.ng-pristine:focus, form .ng-invalid.ng-dirty:focus { box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; } -ul.djng-form-errors, ul.djng-field-errors { +ul.djng-form-errors, ul.djng-field-errors, +div.djng-form-errors, div.djng-field-errors { display: inline-block; list-style-type: none; margin: 5px 0 0 0; padding: 0; } -ul.djng-form-errors li, ul.djng-field-errors li { +ul.djng-form-errors li, ul.djng-field-errors li, +div.djng-form-errors div, div.djng-field-errors div { color: #e9322d; } -ul.djng-field-errors li.invalid:before { +ul.djng-field-errors li.invalid:before, +div.djng-field-errors div.invalid:before { content: "\2716"; margin-right: 10px; } diff --git a/examples/server/forms/ng_messages.py b/examples/server/forms/ng_messages.py new file mode 100644 index 0000000..3a7df40 --- /dev/null +++ b/examples/server/forms/ng_messages.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +# start tutorial +from django.core.exceptions import ValidationError +from djangular.forms import NgModelFormMixin, NgFormValidationMixin, NgMessagesMixin + +from . import subscribe_form + + +class SubscribeForm(NgModelFormMixin, NgFormValidationMixin, NgMessagesMixin, subscribe_form.SubscribeForm): + scope_prefix = 'subscribe_data' + form_name = 'my_form' + + def clean(self): + if self.cleaned_data.get('first_name') == 'John' and self.cleaned_data.get('last_name') == 'Doe': + raise ValidationError('The full name "John Doe" is rejected by the server.') + return super(SubscribeForm, self).clean() diff --git a/examples/server/static/css/djangular-demo.css b/examples/server/static/css/djangular-demo.css index b05d850..23e3f29 100644 --- a/examples/server/static/css/djangular-demo.css +++ b/examples/server/static/css/djangular-demo.css @@ -2,7 +2,8 @@ body{ position:relative; } table.subscribe ul.errorlist, -table.subscribe ul.djng-field-errors { +table.subscribe ul.djng-field-errors, +table.subscribe div.djng-field-errors { display: inline-block; list-style-type: none; position: absolute; @@ -12,11 +13,13 @@ table.subscribe ul.djng-field-errors { table.subscribe ul.errorlist li { color: #e9322d; } -table.subscribe ul.djng-field-errors li.invalid { +table.subscribe ul.djng-field-errors li.invalid, { +table.subscribe div.djng-field-errors div.invalid { color: #e9322d; } table.subscribe ul.errorlist li:before, -table.subscribe ul.djng-field-errors li.invalid:before { +table.subscribe ul.djng-field-errors li.invalid:before, +table.subscribe div.djng-field-errors div.invalid:before { content: "\2716\20"; } table.subscribe ul.djng-field-errors li.valid:before { diff --git a/examples/server/static/js/lodash.min.js b/examples/server/static/js/lodash.min.js new file mode 100644 index 0000000..85a9626 --- /dev/null +++ b/examples/server/static/js/lodash.min.js @@ -0,0 +1,56 @@ +/** + * @license + * Lo-Dash 2.4.1 (Custom Build) lodash.com/license | Underscore.js 1.5.2 underscorejs.org/LICENSE + * Build: `lodash modern -o ./dist/lodash.js` + */ +;(function(){function n(n,t,e){e=(e||0)-1;for(var r=n?n.length:0;++ea||typeof i=="undefined")return 1;if(ie?0:e);++r=b&&i===n,l=[];if(f){var p=o(r);p?(i=t,r=p):f=false}for(;++ui(r,p)&&l.push(p);return f&&c(r),l}function ut(n,t,e,r){r=(r||0)-1;for(var u=n?n.length:0,o=[];++r=b&&f===n,h=u||v?a():s; +for(v&&(h=o(h),f=t);++if(h,y))&&((u||v)&&h.push(y),s.push(g))}return v?(l(h.k),c(h)):u&&l(h),s}function lt(n){return function(t,e,r){var u={};e=J.createCallback(e,r,3),r=-1;var o=t?t.length:0;if(typeof o=="number")for(;++re?Ie(0,o+e):e)||0,Te(n)?i=-1o&&(o=a)}}else t=null==t&&kt(n)?r:J.createCallback(t,e,3),St(n,function(n,e,r){e=t(n,e,r),e>u&&(u=e,o=n)});return o}function Dt(n,t,e,r){if(!n)return e;var u=3>arguments.length;t=J.createCallback(t,r,4);var o=-1,i=n.length;if(typeof i=="number")for(u&&(e=n[++o]);++oarguments.length;return t=J.createCallback(t,r,4),Et(n,function(n,r,o){e=u?(u=false,n):t(e,n,r,o)}),e}function Tt(n){var t=-1,e=n?n.length:0,r=Xt(typeof e=="number"?e:0);return St(n,function(n){var e=at(0,++t);r[t]=r[e],r[e]=n}),r}function Ft(n,t,e){var r;t=J.createCallback(t,e,3),e=-1;var u=n?n.length:0;if(typeof u=="number")for(;++er?Ie(0,u+r):r||0}else if(r)return r=zt(t,e),t[r]===e?r:-1;return n(t,e,r)}function qt(n,t,e){if(typeof t!="number"&&null!=t){var r=0,u=-1,o=n?n.length:0;for(t=J.createCallback(t,e,3);++u>>1,e(n[r])e?0:e);++t=v; +m?(i&&(i=ve(i)),s=f,a=n.apply(l,o)):i||(i=_e(r,v))}return m&&c?c=ve(c):c||t===h||(c=_e(u,t)),e&&(m=true,a=n.apply(l,o)),!m||c||i||(o=l=null),a}}function Ut(n){return n}function Gt(n,t,e){var r=true,u=t&&bt(t);t&&(e||u.length)||(null==e&&(e=t),o=Q,t=n,n=J,u=bt(t)),false===e?r=false:wt(e)&&"chain"in e&&(r=e.chain);var o=n,i=dt(o);St(u,function(e){var u=n[e]=t[e];i&&(o.prototype[e]=function(){var t=this.__chain__,e=this.__wrapped__,i=[e];if(be.apply(i,arguments),i=u.apply(n,i),r||t){if(e===i&&wt(i))return this; +i=new o(i),i.__chain__=t}return i})})}function Ht(){}function Jt(n){return function(t){return t[n]}}function Qt(){return this.__wrapped__}e=e?Y.defaults(G.Object(),e,Y.pick(G,A)):G;var Xt=e.Array,Yt=e.Boolean,Zt=e.Date,ne=e.Function,te=e.Math,ee=e.Number,re=e.Object,ue=e.RegExp,oe=e.String,ie=e.TypeError,ae=[],fe=re.prototype,le=e._,ce=fe.toString,pe=ue("^"+oe(ce).replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/toString| for [^\]]+/g,".*?")+"$"),se=te.ceil,ve=e.clearTimeout,he=te.floor,ge=ne.prototype.toString,ye=vt(ye=re.getPrototypeOf)&&ye,me=fe.hasOwnProperty,be=ae.push,_e=e.setTimeout,de=ae.splice,we=ae.unshift,je=function(){try{var n={},t=vt(t=re.defineProperty)&&t,e=t(n,n,n)&&t +}catch(r){}return e}(),ke=vt(ke=re.create)&&ke,xe=vt(xe=Xt.isArray)&&xe,Ce=e.isFinite,Oe=e.isNaN,Ne=vt(Ne=re.keys)&&Ne,Ie=te.max,Se=te.min,Ee=e.parseInt,Re=te.random,Ae={};Ae[$]=Xt,Ae[T]=Yt,Ae[F]=Zt,Ae[B]=ne,Ae[q]=re,Ae[W]=ee,Ae[z]=ue,Ae[P]=oe,Q.prototype=J.prototype;var De=J.support={};De.funcDecomp=!vt(e.a)&&E.test(s),De.funcNames=typeof ne.name=="string",J.templateSettings={escape:/<%-([\s\S]+?)%>/g,evaluate:/<%([\s\S]+?)%>/g,interpolate:N,variable:"",imports:{_:J}},ke||(nt=function(){function n(){}return function(t){if(wt(t)){n.prototype=t; +var r=new n;n.prototype=null}return r||e.Object()}}());var $e=je?function(n,t){M.value=t,je(n,"__bindData__",M)}:Ht,Te=xe||function(n){return n&&typeof n=="object"&&typeof n.length=="number"&&ce.call(n)==$||false},Fe=Ne?function(n){return wt(n)?Ne(n):[]}:H,Be={"&":"&","<":"<",">":">",'"':""","'":"'"},We=_t(Be),qe=ue("("+Fe(We).join("|")+")","g"),ze=ue("["+Fe(Be).join("")+"]","g"),Pe=ye?function(n){if(!n||ce.call(n)!=q)return false;var t=n.valueOf,e=vt(t)&&(e=ye(t))&&ye(e);return e?n==e||ye(n)==e:ht(n) +}:ht,Ke=lt(function(n,t,e){me.call(n,e)?n[e]++:n[e]=1}),Le=lt(function(n,t,e){(me.call(n,e)?n[e]:n[e]=[]).push(t)}),Me=lt(function(n,t,e){n[e]=t}),Ve=Rt,Ue=vt(Ue=Zt.now)&&Ue||function(){return(new Zt).getTime()},Ge=8==Ee(d+"08")?Ee:function(n,t){return Ee(kt(n)?n.replace(I,""):n,t||0)};return J.after=function(n,t){if(!dt(t))throw new ie;return function(){return 1>--n?t.apply(this,arguments):void 0}},J.assign=U,J.at=function(n){for(var t=arguments,e=-1,r=ut(t,true,false,1),t=t[2]&&t[2][t[1]]===n?1:r.length,u=Xt(t);++e=b&&o(r?e[r]:s)))}var p=e[0],h=-1,g=p?p.length:0,y=[];n:for(;++h(m?t(m,v):f(s,v))){for(r=u,(m||s).push(v);--r;)if(m=i[r],0>(m?t(m,v):f(e[r],v)))continue n;y.push(v)}}for(;u--;)(m=i[u])&&c(m);return l(i),l(s),y},J.invert=_t,J.invoke=function(n,t){var e=p(arguments,2),r=-1,u=typeof t=="function",o=n?n.length:0,i=Xt(typeof o=="number"?o:0);return St(n,function(n){i[++r]=(u?t:n[t]).apply(n,e)}),i},J.keys=Fe,J.map=Rt,J.mapValues=function(n,t,e){var r={}; +return t=J.createCallback(t,e,3),h(n,function(n,e,u){r[e]=t(n,e,u)}),r},J.max=At,J.memoize=function(n,t){function e(){var r=e.cache,u=t?t.apply(this,arguments):m+arguments[0];return me.call(r,u)?r[u]:r[u]=n.apply(this,arguments)}if(!dt(n))throw new ie;return e.cache={},e},J.merge=function(n){var t=arguments,e=2;if(!wt(n))return n;if("number"!=typeof t[2]&&(e=t.length),3e?Ie(0,r+e):Se(e,r-1))+1);r--;)if(n[r]===t)return r;return-1},J.mixin=Gt,J.noConflict=function(){return e._=le,this},J.noop=Ht,J.now=Ue,J.parseInt=Ge,J.random=function(n,t,e){var r=null==n,u=null==t;return null==e&&(typeof n=="boolean"&&u?(e=n,n=1):u||typeof t!="boolean"||(e=t,u=true)),r&&u&&(t=1),n=+n||0,u?(t=n,n=0):t=+t||0,e||n%1||t%1?(e=Re(),Se(n+e*(t-n+parseFloat("1e-"+((e+"").length-1))),t)):at(n,t) +},J.reduce=Dt,J.reduceRight=$t,J.result=function(n,t){if(n){var e=n[t];return dt(e)?n[t]():e}},J.runInContext=s,J.size=function(n){var t=n?n.length:0;return typeof t=="number"?t:Fe(n).length},J.some=Ft,J.sortedIndex=zt,J.template=function(n,t,e){var r=J.templateSettings;n=oe(n||""),e=_({},e,r);var u,o=_({},e.imports,r.imports),r=Fe(o),o=xt(o),a=0,f=e.interpolate||S,l="__p+='",f=ue((e.escape||S).source+"|"+f.source+"|"+(f===N?x:S).source+"|"+(e.evaluate||S).source+"|$","g");n.replace(f,function(t,e,r,o,f,c){return r||(r=o),l+=n.slice(a,c).replace(R,i),e&&(l+="'+__e("+e+")+'"),f&&(u=true,l+="';"+f+";\n__p+='"),r&&(l+="'+((__t=("+r+"))==null?'':__t)+'"),a=c+t.length,t +}),l+="';",f=e=e.variable,f||(e="obj",l="with("+e+"){"+l+"}"),l=(u?l.replace(w,""):l).replace(j,"$1").replace(k,"$1;"),l="function("+e+"){"+(f?"":e+"||("+e+"={});")+"var __t,__p='',__e=_.escape"+(u?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+l+"return __p}";try{var c=ne(r,"return "+l).apply(v,o)}catch(p){throw p.source=l,p}return t?c(t):(c.source=l,c)},J.unescape=function(n){return null==n?"":oe(n).replace(qe,gt)},J.uniqueId=function(n){var t=++y;return oe(null==n?"":n)+t +},J.all=Ot,J.any=Ft,J.detect=It,J.findWhere=It,J.foldl=Dt,J.foldr=$t,J.include=Ct,J.inject=Dt,Gt(function(){var n={};return h(J,function(t,e){J.prototype[e]||(n[e]=t)}),n}(),false),J.first=Bt,J.last=function(n,t,e){var r=0,u=n?n.length:0;if(typeof t!="number"&&null!=t){var o=u;for(t=J.createCallback(t,e,3);o--&&t(n[o],o,n);)r++}else if(r=t,null==r||e)return n?n[u-1]:v;return p(n,Ie(0,u-r))},J.sample=function(n,t,e){return n&&typeof n.length!="number"&&(n=xt(n)),null==t||e?n?n[at(0,n.length-1)]:v:(n=Tt(n),n.length=Se(Ie(0,t),n.length),n) +},J.take=Bt,J.head=Bt,h(J,function(n,t){var e="sample"!==t;J.prototype[t]||(J.prototype[t]=function(t,r){var u=this.__chain__,o=n(this.__wrapped__,t,r);return u||null!=t&&(!r||e&&typeof t=="function")?new Q(o,u):o})}),J.VERSION="2.4.1",J.prototype.chain=function(){return this.__chain__=true,this},J.prototype.toString=function(){return oe(this.__wrapped__)},J.prototype.value=Qt,J.prototype.valueOf=Qt,St(["join","pop","shift"],function(n){var t=ae[n];J.prototype[n]=function(){var n=this.__chain__,e=t.apply(this.__wrapped__,arguments); +return n?new Q(e,n):e}}),St(["push","reverse","sort","unshift"],function(n){var t=ae[n];J.prototype[n]=function(){return t.apply(this.__wrapped__,arguments),this}}),St(["concat","slice","splice"],function(n){var t=ae[n];J.prototype[n]=function(){return new Q(t.apply(this.__wrapped__,arguments),this.__chain__)}}),J}var v,h=[],g=[],y=0,m=+new Date+"",b=75,_=40,d=" \t\x0B\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000",w=/\b__p\+='';/g,j=/\b(__p\+=)''\+/g,k=/(__e\(.*?\)|\b__t\))\+'';/g,x=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,C=/\w*$/,O=/^\s*function[ \n\r\t]+\w/,N=/<%=([\s\S]+?)%>/g,I=RegExp("^["+d+"]*0+(?=.$)"),S=/($^)/,E=/\bthis\b/,R=/['\n\r\t\u2028\u2029\\]/g,A="Array Boolean Date Function Math Number Object RegExp String _ attachEvent clearTimeout isFinite isNaN parseInt setTimeout".split(" "),D="[object Arguments]",$="[object Array]",T="[object Boolean]",F="[object Date]",B="[object Function]",W="[object Number]",q="[object Object]",z="[object RegExp]",P="[object String]",K={}; +K[B]=false,K[D]=K[$]=K[T]=K[F]=K[W]=K[q]=K[z]=K[P]=true;var L={leading:false,maxWait:0,trailing:false},M={configurable:false,enumerable:false,value:null,writable:false},V={"boolean":false,"function":true,object:true,number:false,string:false,undefined:false},U={"\\":"\\","'":"'","\n":"n","\r":"r","\t":"t","\u2028":"u2028","\u2029":"u2029"},G=V[typeof window]&&window||this,H=V[typeof exports]&&exports&&!exports.nodeType&&exports,J=V[typeof module]&&module&&!module.nodeType&&module,Q=J&&J.exports===H&&H,X=V[typeof global]&&global;!X||X.global!==X&&X.window!==X||(G=X); +var Y=s();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(G._=Y, define(function(){return Y})):H&&J?Q?(J.exports=Y)._=Y:H._=Y:G._=Y}).call(this); \ No newline at end of file diff --git a/examples/server/static/js/messages-demo.js b/examples/server/static/js/messages-demo.js new file mode 100644 index 0000000..adf81c4 --- /dev/null +++ b/examples/server/static/js/messages-demo.js @@ -0,0 +1,15 @@ +angular.module('djangular-demo').controller('MyFormCtrl', function($scope, $http, $window, djangoMessagesForm) { + $scope.submit = function() { + if ($scope.subscribe_data) { + $http.post(".", $scope.subscribe_data).success(function(out_data) { + if (!djangoMessagesForm.setErrors($scope.my_form, out_data.errors)) { + // on successful post, redirect onto success page + $window.location.href = out_data.success_url; + } + }).error(function() { + console.error('An error occured during submission'); + }); + } + return false; + }; +}); diff --git a/examples/server/templates/base.html b/examples/server/templates/base.html index c362a88..b906743 100644 --- a/examples/server/templates/base.html +++ b/examples/server/templates/base.html @@ -35,6 +35,9 @@
  • Combined Validation
  • +
  • + angular-messages +
  • {% if WITH_WS4REDIS %}
  • Three-Way Data-Binding @@ -48,7 +51,10 @@
    {% block container %}{% endblock %}
    + + {% block angular_script %} + {% endblock %} {% block scripts %}{% endblock %} diff --git a/examples/server/templates/model-scope.html b/examples/server/templates/model-scope.html index cf30198..10db8a1 100644 --- a/examples/server/templates/model-scope.html +++ b/examples/server/templates/model-scope.html @@ -22,7 +22,7 @@
    MyFormCtrl's scope:
    {% block scripts %} {{ block.super }} +{% endblock %} +{% block scripts %} + {{ block.super }} + + + +{% endblock scripts %} + +{% block app_dependencies %}['ng.django.forms', 'ng.django.messages']{% endblock app_dependencies %} diff --git a/examples/server/urls.py b/examples/server/urls.py index 50dc004..85a71ea 100644 --- a/examples/server/urls.py +++ b/examples/server/urls.py @@ -7,6 +7,7 @@ from server.views.model_scope import SubscribeView as ModelScopeView from server.views.combined_validation import SubscribeView as CombinedValidationView from server.views.threeway_databinding import SubscribeView as ThreeWayDataBindingView +from server.views.ng_messages import SubscribeView as NgMessagesView from server.views import NgFormDataValidView @@ -19,6 +20,8 @@ name='djng_model_scope'), url(r'^combined_validation/$', CombinedValidationView.as_view(), name='djng_combined_validation'), + url(r'^angular_messages/$', NgMessagesView.as_view(), + name='djng_angular_messages'), url(r'^threeway_databinding/$', ThreeWayDataBindingView.as_view(), name='djng_3way_databinding'), url(r'^form_data_valid', NgFormDataValidView.as_view(), name='form_data_valid'), diff --git a/examples/server/views/ng_messages.py b/examples/server/views/ng_messages.py new file mode 100644 index 0000000..af39c58 --- /dev/null +++ b/examples/server/views/ng_messages.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from server.forms.ng_messages import SubscribeForm +# start tutorial +import json +from django.http import HttpResponse +from django.core.urlresolvers import reverse_lazy +from django.views.generic.edit import FormView +from django.utils.encoding import force_text + + +class SubscribeView(FormView): + template_name = 'ng-messages.html' + form_class = SubscribeForm + success_url = reverse_lazy('form_data_valid') + + def post(self, request, **kwargs): + if request.is_ajax(): + return self.ajax(request) + return super(NgMessagesView, self).post(request, **kwargs) + + def ajax(self, request): + form = self.form_class(data=json.loads(request.body)) + response_data = {'errors': form.errors, 'success_url': force_text(self.success_url)} + return HttpResponse(json.dumps(response_data), content_type="application/json") diff --git a/examples/test.sqlite b/examples/test.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..a358684eabf3652c6e5e56caf28aa417c5df4e35 GIT binary patch literal 172032 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 b4m!FF0SG_<0uX=z1Rwwb2tWV=T_^DWv?yk; literal 0 HcmV?d00001 From c05680352ff2d153379aac49a53863a7e0ea0a15 Mon Sep 17 00:00:00 2001 From: jRobb Date: Wed, 26 Nov 2014 15:45:49 +0000 Subject: [PATCH 02/25] Delete test.sqlite --- examples/test.sqlite | Bin 172032 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 examples/test.sqlite diff --git a/examples/test.sqlite b/examples/test.sqlite deleted file mode 100644 index a358684eabf3652c6e5e56caf28aa417c5df4e35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 172032 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 b4m!FF0SG_<0uX=z1Rwwb2tWV=T_^DWv?yk; From 72787035dba5090efca14a782f802455a25dfaca Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Wed, 26 Nov 2014 19:50:04 +0000 Subject: [PATCH 03/25] added overriden get_widget_attrs to NgMessagesMixin to add rejected-validator --- .gitignore | 1 + djangular/forms/angular_messages.py | 8 ++++++-- examples/server/forms/ng_messages.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 1be7f55..671c689 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ docs/_build/ node_modules/ bower_components/ build/ +example/test.sqlite diff --git a/djangular/forms/angular_messages.py b/djangular/forms/angular_messages.py index fef4c1d..71e4101 100644 --- a/djangular/forms/angular_messages.py +++ b/djangular/forms/angular_messages.py @@ -33,8 +33,12 @@ def as_ul(self): class NgMessagesMixin(NgFormBaseMixin): + def __init__(self, data=None, *args, **kwargs): kwargs['error_class'] = NgMessagesTupleErrorList super(NgMessagesMixin, self).__init__(data, *args, **kwargs) - for field in self.fields.itervalues(): - field.widget.attrs['validate-rejected'] = "" \ No newline at end of file + + def get_widget_attrs(self, bound_field): + attrs = super(NgMessagesMixin, self).get_widget_attrs(bound_field) + attrs['validate-rejected'] = "" + return attrs \ No newline at end of file diff --git a/examples/server/forms/ng_messages.py b/examples/server/forms/ng_messages.py index 3a7df40..040074e 100644 --- a/examples/server/forms/ng_messages.py +++ b/examples/server/forms/ng_messages.py @@ -7,7 +7,7 @@ from . import subscribe_form -class SubscribeForm(NgModelFormMixin, NgFormValidationMixin, NgMessagesMixin, subscribe_form.SubscribeForm): +class SubscribeForm(NgMessagesMixin, NgModelFormMixin, NgFormValidationMixin, subscribe_form.SubscribeForm): scope_prefix = 'subscribe_data' form_name = 'my_form' From 7e1bc4dab38b63c8fae8656c0916194364eb5008 Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Wed, 26 Nov 2014 21:03:14 +0000 Subject: [PATCH 04/25] removed need for hardcoded rejected from msg_format_bind --- .gitignore | 2 +- djangular/forms/angular_messages.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 671c689..e0b7922 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,4 @@ docs/_build/ node_modules/ bower_components/ build/ -example/test.sqlite +examples/test.sqlite \ No newline at end of file diff --git a/djangular/forms/angular_messages.py b/djangular/forms/angular_messages.py index 71e4101..b62f171 100644 --- a/djangular/forms/angular_messages.py +++ b/djangular/forms/angular_messages.py @@ -2,8 +2,7 @@ from django.utils.encoding import force_text from django.utils.safestring import mark_safe, SafeText -from djangular.forms.angular_base import TupleErrorList -from djangular.forms.angular_base import NgFormBaseMixin +from djangular.forms.angular_base import TupleErrorList, SafeTuple, NgFormBaseMixin class NgMessagesTupleErrorList(TupleErrorList): @@ -11,7 +10,7 @@ class NgMessagesTupleErrorList(TupleErrorList): msgs_format = '
    {2}
    ' msg_format = '
    {3}
    ' """ span's necessary due to this bug https://github.com/angular/angular.js/issues/8089""" - msg_format_bind = '
    ' + msg_format_bind = '
    ' def as_ul(self): if not self: @@ -20,7 +19,7 @@ def as_ul(self): if isinstance(first, tuple): error_list = [] for e in self: - if e[3] == '$valid': + if e[3] == '$valid' or e[2] == '$pristine': continue msg_format = e[5] == '$message' and self.msg_format_bind or self.msg_format msg_type = e[3].split('.') @@ -38,6 +37,14 @@ def __init__(self, data=None, *args, **kwargs): kwargs['error_class'] = NgMessagesTupleErrorList super(NgMessagesMixin, self).__init__(data, *args, **kwargs) + def get_field_errors(self, field): + errors = super(NgMessagesMixin, self).get_field_errors(field) + if field.is_hidden: + return errors + identifier = format_html('{0}.{1}', self.form_name, field.name) + errors.append(SafeTuple((identifier, self.field_error_css_classes, '$dirty', 'rejected', 'invalid', '$message'))) + return errors + def get_widget_attrs(self, bound_field): attrs = super(NgMessagesMixin, self).get_widget_attrs(bound_field) attrs['validate-rejected'] = "" From 048ffe10ec9c8f6095a99d312cf9293f7a1e2025 Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Thu, 27 Nov 2014 10:16:34 +0000 Subject: [PATCH 05/25] bindable field error messages are now assigned to a related property on the fields $message object - this allows for multiple binded validators --- client/src/js/ng-django-messages.js | 8 +++++++- djangular/forms/angular_messages.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/client/src/js/ng-django-messages.js b/client/src/js/ng-django-messages.js index 51120f8..168b00e 100644 --- a/client/src/js/ng-django-messages.js +++ b/client/src/js/ng-django-messages.js @@ -32,6 +32,7 @@ function validateRejected() { }else{ _hasMessage = ngModel.$message !== undefined; + if(_hasMessage) { _value = value; } @@ -74,7 +75,12 @@ function djangoMessagesForm() { field = form[key]; field.$dirty = true; - field.$message = message; + + if(!angular.isObject(field.$message)) { + field.$message = {}; + } + + field.$message.rejected = message; if (angular.isFunction(field.$validate)) { field.$validate(); diff --git a/djangular/forms/angular_messages.py b/djangular/forms/angular_messages.py index b62f171..d51d4b7 100644 --- a/djangular/forms/angular_messages.py +++ b/djangular/forms/angular_messages.py @@ -10,7 +10,7 @@ class NgMessagesTupleErrorList(TupleErrorList): msgs_format = '
    {2}
    ' msg_format = '
    {3}
    ' """ span's necessary due to this bug https://github.com/angular/angular.js/issues/8089""" - msg_format_bind = '
    ' + msg_format_bind = '
    ' def as_ul(self): if not self: From 5369961d605d4e8b44fe302fa5193b1191fe6ae5 Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Thu, 27 Nov 2014 10:22:26 +0000 Subject: [PATCH 06/25] changed exclusion of $pristine error to exclusion of $message error, to prevent NgModelFoermMixin rejected error from being included --- djangular/forms/angular_messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangular/forms/angular_messages.py b/djangular/forms/angular_messages.py index d51d4b7..4186734 100644 --- a/djangular/forms/angular_messages.py +++ b/djangular/forms/angular_messages.py @@ -19,7 +19,7 @@ def as_ul(self): if isinstance(first, tuple): error_list = [] for e in self: - if e[3] == '$valid' or e[2] == '$pristine': + if e[3] in ('$valid', '$message'): continue msg_format = e[5] == '$message' and self.msg_format_bind or self.msg_format msg_type = e[3].split('.') From b0f013c66e4862ea69b22b1c7eba010a43400c47 Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Thu, 27 Nov 2014 14:54:44 +0000 Subject: [PATCH 07/25] refactoring --- djangular/forms/angular_messages.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/djangular/forms/angular_messages.py b/djangular/forms/angular_messages.py index 4186734..7714d5c 100644 --- a/djangular/forms/angular_messages.py +++ b/djangular/forms/angular_messages.py @@ -1,10 +1,14 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + from django.utils.html import format_html -from django.utils.encoding import force_text +from django.utils.encoding import python_2_unicode_compatible, force_text from django.utils.safestring import mark_safe, SafeText from djangular.forms.angular_base import TupleErrorList, SafeTuple, NgFormBaseMixin +@python_2_unicode_compatible class NgMessagesTupleErrorList(TupleErrorList): msgs_format = '
    {2}
    ' @@ -12,14 +16,17 @@ class NgMessagesTupleErrorList(TupleErrorList): """ span's necessary due to this bug https://github.com/angular/angular.js/issues/8089""" msg_format_bind = '
    ' - def as_ul(self): + def __str__(self): + return self.as_divs() + + def as_divs(self): if not self: return SafeText() first = self[0] if isinstance(first, tuple): error_list = [] for e in self: - if e[3] in ('$valid', '$message'): + if e[3] == '$valid': continue msg_format = e[5] == '$message' and self.msg_format_bind or self.msg_format msg_type = e[3].split('.') @@ -41,6 +48,10 @@ def get_field_errors(self, field): errors = super(NgMessagesMixin, self).get_field_errors(field) if field.is_hidden: return errors + #remove error added by NgModelFormMixin + for item in errors[:]: + if item[2] == '$pristine' and item[5] == '$message': + errors.remove(item) identifier = format_html('{0}.{1}', self.form_name, field.name) errors.append(SafeTuple((identifier, self.field_error_css_classes, '$dirty', 'rejected', 'invalid', '$message'))) return errors From 35a3a8db956de2239e211d30b684545709e8d471 Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Sat, 29 Nov 2014 14:25:35 +0000 Subject: [PATCH 08/25] fixed super c and p error --- examples/server/views/ng_messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/server/views/ng_messages.py b/examples/server/views/ng_messages.py index af39c58..eec3245 100644 --- a/examples/server/views/ng_messages.py +++ b/examples/server/views/ng_messages.py @@ -16,7 +16,7 @@ class SubscribeView(FormView): def post(self, request, **kwargs): if request.is_ajax(): return self.ajax(request) - return super(NgMessagesView, self).post(request, **kwargs) + return super(SubscribeView, self).post(request, **kwargs) def ajax(self, request): form = self.form_class(data=json.loads(request.body)) From d278bf1cd2631e864a59efb856753a0e7a8231fe Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Mon, 1 Dec 2014 22:06:08 +0000 Subject: [PATCH 09/25] added seperate form and field error classes for ngMessages mixin --- client/src/js/ng-django-messages.js | 67 ++++++++++++++++++++++++++++- djangular/forms/angular_base.py | 1 + djangular/forms/angular_messages.py | 35 ++++++++++----- 3 files changed, 92 insertions(+), 11 deletions(-) diff --git a/client/src/js/ng-django-messages.js b/client/src/js/ng-django-messages.js index 168b00e..437a2f8 100644 --- a/client/src/js/ng-django-messages.js +++ b/client/src/js/ng-django-messages.js @@ -6,12 +6,77 @@ angular 'ngMessages' ]) + .directive('form', formExtension) .directive('validateRejected', validateRejected) .factory('djangoMessagesForm', djangoMessagesForm); +/** + * An extension to form + * + * Adds the following methods and functionality: + * + * - setValidFieldsPristine() + */ +function formExtension() { + + return { + restrict: 'E', + require: [ + '^?form' + ], + link: { + pre: function($scope, $element, $attrs, ctrls) { + + var ctrl = ctrls[0], + controls, + modelName; + + var _superAdd = ctrl.$addControl; + + ctrl.$addControl = function(control) { + + _superAdd(control) + + controls = controls || []; + + if(controls.indexOf(control) === -1) { + controls.push(control); + } + } + + var _superRemove = ctrl.$removeControl; + + ctrl.$removeControl = function(control) { + + _superRemove(control) + + if(controls && controls.indexOf(control) !== -1) { + controls.splice(controls.indexOf(control), 1); + } + } + + ctrl.setValidFieldsPristine = function() { + + var i = 0, + len = controls.length, + control; + + for(; i < len; i++) { + control = controls[i]; + if(control.$valid) { + control.$setPristine(); + } + } + } + } + } + } +}; + + function validateRejected() { return { @@ -97,7 +162,7 @@ function djangoMessagesForm() { }else{ form.$message = message; - form.$setPristine(); + form.setValidFieldsPristine(); } } ); diff --git a/djangular/forms/angular_base.py b/djangular/forms/angular_base.py index 54dee88..4afffe1 100644 --- a/djangular/forms/angular_base.py +++ b/djangular/forms/angular_base.py @@ -71,6 +71,7 @@ def as_ul(self): li_format = e[5] == '$message' and self.li_format_bind or self.li_format err_tuple = (e[0], e[3], e[4], force_text(e[5])) error_lists[e[2]].append(format_html(li_format, *err_tuple)) + print err_tuple # renders and combine both of these lists return mark_safe(''.join([format_html(self.ul_format, first[0], first[1], prop, mark_safe(''.join(list_items))) for prop, list_items in error_lists.items()])) diff --git a/djangular/forms/angular_messages.py b/djangular/forms/angular_messages.py index 7714d5c..5eb1f44 100644 --- a/djangular/forms/angular_messages.py +++ b/djangular/forms/angular_messages.py @@ -8,8 +8,13 @@ from djangular.forms.angular_base import TupleErrorList, SafeTuple, NgFormBaseMixin + +class NgMessagesFormErrorList(TupleErrorList): + ul_format = '
      {3}
    ' + + @python_2_unicode_compatible -class NgMessagesTupleErrorList(TupleErrorList): +class NgMessagesFieldErrorList(TupleErrorList): msgs_format = '
    {2}
    ' msg_format = '
    {3}
    ' @@ -33,30 +38,40 @@ def as_divs(self): err_tuple = (e[0], msg_type[0] if len(msg_type) == 1 else msg_type.pop(), e[4], force_text(e[5])) error_list.append(format_html(msg_format, *err_tuple)) - return (error_list and \ - format_html(self.msgs_format, first[0], first[1], mark_safe(''.join(error_list))) - or '') + return mark_safe(format_html(self.msgs_format, first[0], first[1], mark_safe(''.join(error_list)))) + + return format_html('
    {0}
    ', + format_html_join('', '
    {0}
    ', ((force_text(e),) for e in self))) class NgMessagesMixin(NgFormBaseMixin): def __init__(self, data=None, *args, **kwargs): - kwargs['error_class'] = NgMessagesTupleErrorList + self.form_error_class = kwargs.pop('form_error_class', NgMessagesFormErrorList) + error_class = kwargs.pop('error_class', NgMessagesFieldErrorList) + kwargs.setdefault('error_class', error_class) super(NgMessagesMixin, self).__init__(data, *args, **kwargs) def get_field_errors(self, field): errors = super(NgMessagesMixin, self).get_field_errors(field) if field.is_hidden: return errors - #remove error added by NgModelFormMixin - for item in errors[:]: - if item[2] == '$pristine' and item[5] == '$message': - errors.remove(item) + self._remove_ng_model_form_mixin_rejected_error(errors) identifier = format_html('{0}.{1}', self.form_name, field.name) errors.append(SafeTuple((identifier, self.field_error_css_classes, '$dirty', 'rejected', 'invalid', '$message'))) return errors + def non_field_errors(self): + errors = super(NgMessagesMixin, self).non_field_errors() + return self.form_error_class(errors) + def get_widget_attrs(self, bound_field): attrs = super(NgMessagesMixin, self).get_widget_attrs(bound_field) attrs['validate-rejected'] = "" - return attrs \ No newline at end of file + return attrs + + def _remove_ng_model_form_mixin_rejected_error(self, errors): + for item in errors: + if item[2] == '$pristine' and item[5] == '$message': + errors.remove(item) + return From 5f3af874df87f8d826ed277f696b149485818b4f Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Tue, 2 Dec 2014 16:03:37 +0000 Subject: [PATCH 10/25] added valid message display and changed field error format to ul/li --- client/src/js/ng-django-messages.js | 17 +++++---- djangular/forms/angular_base.py | 1 - djangular/forms/angular_messages.py | 38 +++++++++++-------- djangular/static/djangular/css/styles.css | 9 ++--- examples/server/static/css/djangular-demo.css | 9 ++--- examples/server/templates/ng-messages.html | 2 +- 6 files changed, 40 insertions(+), 36 deletions(-) diff --git a/client/src/js/ng-django-messages.js b/client/src/js/ng-django-messages.js index 437a2f8..919c037 100644 --- a/client/src/js/ng-django-messages.js +++ b/client/src/js/ng-django-messages.js @@ -84,14 +84,14 @@ function validateRejected() { link: function($scope, $element, $attrs, ngModel) { var _hasMessage = false, - _value; + _value = null; ngModel.$validators.rejected = function(value) { - - if(_hasMessage && (_value && _value !== value)) { + + if(_hasMessage && (_value !== value)) { _hasMessage = false; - _value = undefined; + _value = null; ngModel.$message = undefined; }else{ @@ -146,14 +146,17 @@ function djangoMessagesForm() { } field.$message.rejected = message; - + if (angular.isFunction(field.$validate)) { + field.$validate(); + } else { // this field is a composite of input elements angular.forEach(field, function(subField, subKey) { - if (angular.isDefined(subField) && - angular.isFunction(subField.$validate)) { + if(angular.isDefined(subField) && + angular.isFunction(subField.$validate)) { + subField.$validate(); } }); diff --git a/djangular/forms/angular_base.py b/djangular/forms/angular_base.py index 4afffe1..54dee88 100644 --- a/djangular/forms/angular_base.py +++ b/djangular/forms/angular_base.py @@ -71,7 +71,6 @@ def as_ul(self): li_format = e[5] == '$message' and self.li_format_bind or self.li_format err_tuple = (e[0], e[3], e[4], force_text(e[5])) error_lists[e[2]].append(format_html(li_format, *err_tuple)) - print err_tuple # renders and combine both of these lists return mark_safe(''.join([format_html(self.ul_format, first[0], first[1], prop, mark_safe(''.join(list_items))) for prop, list_items in error_lists.items()])) diff --git a/djangular/forms/angular_messages.py b/djangular/forms/angular_messages.py index 5eb1f44..c150973 100644 --- a/djangular/forms/angular_messages.py +++ b/djangular/forms/angular_messages.py @@ -13,35 +13,43 @@ class NgMessagesFormErrorList(TupleErrorList): ul_format = '
      {3}
    ' -@python_2_unicode_compatible class NgMessagesFieldErrorList(TupleErrorList): - msgs_format = '
    {2}
    ' - msg_format = '
    {3}
    ' - """ span's necessary due to this bug https://github.com/angular/angular.js/issues/8089""" - msg_format_bind = '
    ' + ul_format_valid = '
      {2}
    ' + li_format_valid = '
  • {3}
  • ' - def __str__(self): - return self.as_divs() + ul_format = '
      {2}
    ' + li_format = '
  • {3}
  • ' + """ span's necessary due to this bug https://github.com/angular/angular.js/issues/8089""" + li_format_bind = '
  • ' - def as_divs(self): + def as_ul(self): if not self: return SafeText() first = self[0] if isinstance(first, tuple): - error_list = [] + valid_list = [] + invalid_list = [] for e in self: if e[3] == '$valid': - continue - msg_format = e[5] == '$message' and self.msg_format_bind or self.msg_format + li_format = self.li_format_valid + error_list = valid_list + elif e[5] == '$message': + li_format = self.li_format_bind + error_list = invalid_list + else: + li_format = self.li_format + error_list = invalid_list + msg_type = e[3].split('.') err_tuple = (e[0], msg_type[0] if len(msg_type) == 1 else msg_type.pop(), e[4], force_text(e[5])) - error_list.append(format_html(msg_format, *err_tuple)) + error_list.append(format_html(li_format, *err_tuple)) - return mark_safe(format_html(self.msgs_format, first[0], first[1], mark_safe(''.join(error_list)))) + return mark_safe(format_html(self.ul_format_valid, first[0], first[1], mark_safe(''.join(valid_list)))) \ + + mark_safe(format_html(self.ul_format, first[0], first[1], mark_safe(''.join(invalid_list)))) - return format_html('
    {0}
    ', - format_html_join('', '
    {0}
    ', ((force_text(e),) for e in self))) + return format_html('
      {0}
    ', + format_html_join('', '
  • {0}
  • ', ((force_text(e),) for e in self))) class NgMessagesMixin(NgFormBaseMixin): diff --git a/djangular/static/djangular/css/styles.css b/djangular/static/djangular/css/styles.css index 2baea64..5ece16e 100644 --- a/djangular/static/djangular/css/styles.css +++ b/djangular/static/djangular/css/styles.css @@ -9,19 +9,16 @@ form .ng-invalid-bound.ng-pristine:focus, form .ng-invalid.ng-dirty:focus { box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; } -ul.djng-form-errors, ul.djng-field-errors, -div.djng-form-errors, div.djng-field-errors { +ul.djng-form-errors, ul.djng-field-errors { display: inline-block; list-style-type: none; margin: 5px 0 0 0; padding: 0; } -ul.djng-form-errors li, ul.djng-field-errors li, -div.djng-form-errors div, div.djng-field-errors div { +ul.djng-form-errors li, ul.djng-field-errors li { color: #e9322d; } -ul.djng-field-errors li.invalid:before, -div.djng-field-errors div.invalid:before { +ul.djng-field-errors li.invalid:before { content: "\2716"; margin-right: 10px; } diff --git a/examples/server/static/css/djangular-demo.css b/examples/server/static/css/djangular-demo.css index 23e3f29..b05d850 100644 --- a/examples/server/static/css/djangular-demo.css +++ b/examples/server/static/css/djangular-demo.css @@ -2,8 +2,7 @@ body{ position:relative; } table.subscribe ul.errorlist, -table.subscribe ul.djng-field-errors, -table.subscribe div.djng-field-errors { +table.subscribe ul.djng-field-errors { display: inline-block; list-style-type: none; position: absolute; @@ -13,13 +12,11 @@ table.subscribe div.djng-field-errors { table.subscribe ul.errorlist li { color: #e9322d; } -table.subscribe ul.djng-field-errors li.invalid, { -table.subscribe div.djng-field-errors div.invalid { +table.subscribe ul.djng-field-errors li.invalid { color: #e9322d; } table.subscribe ul.errorlist li:before, -table.subscribe ul.djng-field-errors li.invalid:before, -table.subscribe div.djng-field-errors div.invalid:before { +table.subscribe ul.djng-field-errors li.invalid:before { content: "\2716\20"; } table.subscribe ul.djng-field-errors li.valid:before { diff --git a/examples/server/templates/ng-messages.html b/examples/server/templates/ng-messages.html index f5eb6ec..dbf7e9b 100644 --- a/examples/server/templates/ng-messages.html +++ b/examples/server/templates/ng-messages.html @@ -3,7 +3,7 @@ {% block form_title %}NgMessages Demo{% endblock %} -{% block form_header %}Combine client-side Form validation with two-way data-binding and angular-messages.
    Requires angular 1.3.{% endblock %} +{% block form_header %}Combine client-side Form validation with two-way data-binding and angular-messages.
    Requires angular 1.3+{% endblock %} {% block form_tag %}name="{{ form.form_name }}" method="post" action="." novalidate ng-controller="MyFormCtrl"{% endblock %} From 546ac9df4c1a99e3ab6f8b978d83b0bc34349dd4 Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Wed, 3 Dec 2014 11:13:21 +0000 Subject: [PATCH 11/25] tweaking field states to display errors correctly --- client/src/js/ng-django-messages.js | 16 ++++++++++++---- djangular/forms/angular_messages.py | 6 +++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/client/src/js/ng-django-messages.js b/client/src/js/ng-django-messages.js index 919c037..818491b 100644 --- a/client/src/js/ng-django-messages.js +++ b/client/src/js/ng-django-messages.js @@ -129,18 +129,24 @@ function djangoMessagesForm() { }; function _displayErrors(form, errors) { + console.log(errors) + console.log(form) angular.forEach(errors, function(error, key) { var field, message = error[0]; - form.$setDirty(); + form.$setSubmitted(); + //form.$setDirty(); if(form.hasOwnProperty(key)) { field = form[key]; - field.$dirty = true; - + // + // field.$pristine = false; + // field.$dirty = true; + if(field.$name === 'password' || field.$name === 'height') + console.log(field) if(!angular.isObject(field.$message)) { field.$message = {}; } @@ -149,6 +155,7 @@ function djangoMessagesForm() { if (angular.isFunction(field.$validate)) { + field.$setTouched(); field.$validate(); } else { @@ -156,7 +163,8 @@ function djangoMessagesForm() { angular.forEach(field, function(subField, subKey) { if(angular.isDefined(subField) && angular.isFunction(subField.$validate)) { - + + subField.$setTouched(); subField.$validate(); } }); diff --git a/djangular/forms/angular_messages.py b/djangular/forms/angular_messages.py index c150973..15b21f8 100644 --- a/djangular/forms/angular_messages.py +++ b/djangular/forms/angular_messages.py @@ -10,15 +10,15 @@ class NgMessagesFormErrorList(TupleErrorList): - ul_format = '
      {3}
    ' + ul_format = '
      {3}
    ' class NgMessagesFieldErrorList(TupleErrorList): - ul_format_valid = '
      {2}
    ' + ul_format_valid = '
      {2}
    ' li_format_valid = '
  • {3}
  • ' - ul_format = '
      {2}
    ' + ul_format = '
      {2}
    ' li_format = '
  • {3}
  • ' """ span's necessary due to this bug https://github.com/angular/angular.js/issues/8089""" li_format_bind = '
  • ' From 7e8d0e54a6aafec7a31266290d364c1e2637674b Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Wed, 3 Dec 2014 14:07:28 +0000 Subject: [PATCH 12/25] styling and error/valid display trigger properties updated --- client/src/js/ng-django-messages.js | 25 ++++++++--------------- djangular/forms/angular_messages.py | 12 ++++++++--- djangular/static/djangular/css/styles.css | 9 ++++++-- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/client/src/js/ng-django-messages.js b/client/src/js/ng-django-messages.js index 818491b..d02ad33 100644 --- a/client/src/js/ng-django-messages.js +++ b/client/src/js/ng-django-messages.js @@ -129,42 +129,35 @@ function djangoMessagesForm() { }; function _displayErrors(form, errors) { - console.log(errors) - console.log(form) + + form.$setSubmitted(); + angular.forEach(errors, function(error, key) { var field, message = error[0]; - form.$setSubmitted(); - //form.$setDirty(); - if(form.hasOwnProperty(key)) { field = form[key]; - // - // field.$pristine = false; - // field.$dirty = true; - if(field.$name === 'password' || field.$name === 'height') - console.log(field) + if(!angular.isObject(field.$message)) { field.$message = {}; } - + field.$message.rejected = message; if (angular.isFunction(field.$validate)) { - - field.$setTouched(); + field.$validate(); } else { // this field is a composite of input elements + field.$setSubmitted(); + angular.forEach(field, function(subField, subKey) { if(angular.isDefined(subField) && - angular.isFunction(subField.$validate)) { - - subField.$setTouched(); + angular.isFunction(subField.$validate)) { subField.$validate(); } }); diff --git a/djangular/forms/angular_messages.py b/djangular/forms/angular_messages.py index 15b21f8..6d4335b 100644 --- a/djangular/forms/angular_messages.py +++ b/djangular/forms/angular_messages.py @@ -15,10 +15,10 @@ class NgMessagesFormErrorList(TupleErrorList): class NgMessagesFieldErrorList(TupleErrorList): - ul_format_valid = '
      {2}
    ' + ul_format_valid = '
      {2}
    ' li_format_valid = '
  • {3}
  • ' - ul_format = '
      {2}
    ' + ul_format = '
      {3}
    ' li_format = '
  • {3}
  • ' """ span's necessary due to this bug https://github.com/angular/angular.js/issues/8089""" li_format_bind = '
  • ' @@ -46,11 +46,17 @@ def as_ul(self): error_list.append(format_html(li_format, *err_tuple)) return mark_safe(format_html(self.ul_format_valid, first[0], first[1], mark_safe(''.join(valid_list)))) \ - + mark_safe(format_html(self.ul_format, first[0], first[1], mark_safe(''.join(invalid_list)))) + + mark_safe(format_html(self.ul_format, first[0], first[1], self._get_form_name(first[0]), mark_safe(''.join(invalid_list)))) return format_html('
      {0}
    ', format_html_join('', '
  • {0}
  • ', ((force_text(e),) for e in self))) + def _get_form_name(self, value): + parts = value.split('.') + parts.pop() + return '.'.join(parts) + + class NgMessagesMixin(NgFormBaseMixin): diff --git a/djangular/static/djangular/css/styles.css b/djangular/static/djangular/css/styles.css index 5ece16e..3730322 100644 --- a/djangular/static/djangular/css/styles.css +++ b/djangular/static/djangular/css/styles.css @@ -2,10 +2,15 @@ display: none !important; } -form .ng-invalid-bound.ng-pristine, form .ng-invalid.ng-dirty { +form.ng-submitted .ng-invalid, +form .ng-invalid-bound.ng-pristine, +form .ng-invalid.ng-dirty { border-color: #e9322d; } -form .ng-invalid-bound.ng-pristine:focus, form .ng-invalid.ng-dirty:focus { + +form.ng-submitted .ng-invalid:focus, +form .ng-invalid-bound.ng-pristine:focus, +form .ng-invalid.ng-dirty:focus { box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; } From f5829c26647942d70495367f83dead1fe104ac5d Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Thu, 4 Dec 2014 14:23:56 +0000 Subject: [PATCH 13/25] updated rejected validation directive to check $message.rejected value --- client/src/js/ng-django-messages.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/js/ng-django-messages.js b/client/src/js/ng-django-messages.js index d02ad33..a237ad4 100644 --- a/client/src/js/ng-django-messages.js +++ b/client/src/js/ng-django-messages.js @@ -92,11 +92,11 @@ function validateRejected() { _hasMessage = false; _value = null; - ngModel.$message = undefined; + ngModel.$message.rejected = undefined; }else{ - _hasMessage = ngModel.$message !== undefined; + _hasMessage = ngModel.$message.rejected !== undefined; if(_hasMessage) { _value = value; From 6fed82e8cf456b3fb357b22dd9bb2eeb1270535e Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Thu, 4 Dec 2014 14:30:21 +0000 Subject: [PATCH 14/25] updated rejected validation directive to check $message.rejected value --- client/src/js/ng-django-messages.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/src/js/ng-django-messages.js b/client/src/js/ng-django-messages.js index a237ad4..62dcaf0 100644 --- a/client/src/js/ng-django-messages.js +++ b/client/src/js/ng-django-messages.js @@ -92,11 +92,14 @@ function validateRejected() { _hasMessage = false; _value = null; - ngModel.$message.rejected = undefined; + + if(ngModel.$message) { + ngModel.$message.rejected = undefined; + } }else{ - _hasMessage = ngModel.$message.rejected !== undefined; + _hasMessage = ngModel.$message && ngModel.$message.rejected !== undefined; if(_hasMessage) { _value = value; From 778be3e8172aa3dc39beb34467b8d4eb0f1896b8 Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Mon, 8 Dec 2014 11:22:29 +0000 Subject: [PATCH 15/25] added client tests --- client/package.json | 72 ++++++++++++++++------------- client/src/js/ng-django-messages.js | 5 +- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/client/package.json b/client/package.json index 225cbf8..f4e72f3 100644 --- a/client/package.json +++ b/client/package.json @@ -1,35 +1,41 @@ { - "name": "django-angular", - "description": "Let Django play nice together with AngularJS", - "version": "0.7.0", - "homepage": "https://github.com/jrief/django-angular", - "author": { - "name": "Jacob Rief", - "email": "jacob.rief@gmail.com" - }, - "repository": { - "type": "git", - "url": "git://github.com/jrief/django-angular.git" - }, - "bugs": { - "url": "https://github.com/jrief/django-angular/issues" - }, - "licenses": [{ - "type": "MIT", - "url": "https://github.com/jrief/django-angular/LICENSE.txt" - }], - "dependencies": {}, - "devDependencies": { - "karma": "latest", - "karma-firefox-launcher": "latest", - "karma-chrome-launcher": "latest", - "karma-jasmine": "latest", - "grunt": "latest", - "grunt-contrib-concat": "latest", - "grunt-contrib-jshint": "latest", - "grunt-contrib-uglify": "latest", - "grunt-ngmin": "latest", - "grunt-karma": "latest" - }, - "keywords": ["Django", "AngularJS", "djangular"] + "name": "django-angular", + "description": "Let Django play nice together with AngularJS", + "version": "0.7.0", + "homepage": "https://github.com/jrief/django-angular", + "author": { + "name": "Jacob Rief", + "email": "jacob.rief@gmail.com" + }, + "repository": { + "type": "git", + "url": "git://github.com/jrief/django-angular.git" + }, + "bugs": { + "url": "https://github.com/jrief/django-angular/issues" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://github.com/jrief/django-angular/LICENSE.txt" + } + ], + "dependencies": {}, + "devDependencies": { + "grunt": "latest", + "grunt-contrib-concat": "latest", + "grunt-contrib-jshint": "latest", + "grunt-contrib-uglify": "latest", + "grunt-karma": "latest", + "grunt-ngmin": "latest", + "karma": "^0.12.28", + "karma-chrome-launcher": "latest", + "karma-firefox-launcher": "latest", + "karma-jasmine": "latest" + }, + "keywords": [ + "Django", + "AngularJS", + "djangular" + ] } diff --git a/client/src/js/ng-django-messages.js b/client/src/js/ng-django-messages.js index 62dcaf0..63ca9bb 100644 --- a/client/src/js/ng-django-messages.js +++ b/client/src/js/ng-django-messages.js @@ -128,7 +128,7 @@ function djangoMessagesForm() { }; function _clearFormMessage(form) { - form.$message = ''; + form.$message = undefined; }; function _displayErrors(form, errors) { @@ -160,7 +160,8 @@ function djangoMessagesForm() { angular.forEach(field, function(subField, subKey) { if(angular.isDefined(subField) && - angular.isFunction(subField.$validate)) { + angular.isFunction(subField.$validate)) { + subField.$validate(); } }); From f7ce5c8b7e68f1c60cbc973413d3b3d510f7eb0c Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Fri, 13 Mar 2015 14:09:32 +0000 Subject: [PATCH 16/25] added ngMessages client tests --- client/karma.conf.1.3.js | 59 ++++++++++++ client/karma.conf.js | 2 +- client/src/js/ng-django-forms.js | 4 +- client/src/js/ng-django-messages.js | 34 +++++-- client/tests/djangoNgMessagesSpec.js | 136 +++++++++++++++++++++++++++ 5 files changed, 222 insertions(+), 13 deletions(-) create mode 100644 client/karma.conf.1.3.js create mode 100644 client/tests/djangoNgMessagesSpec.js diff --git a/client/karma.conf.1.3.js b/client/karma.conf.1.3.js new file mode 100644 index 0000000..b4b3951 --- /dev/null +++ b/client/karma.conf.1.3.js @@ -0,0 +1,59 @@ +'use strict'; + +// Karma configuration +module.exports = function(config) { + function getFiles() { + var fs = require('fs'); + var files = []; + ['angular.js', 'angular-mocks.js', 'angular-messages.js'].forEach(function(item) { + var cachename = 'cdncache/' + item; + files.push(fs.existsSync(cachename) ? cachename : 'http://code.angularjs.org/1.3.0/' + item); + }); + return files.concat(['src/js/*.js', 'tests/*.js', 'mocks/*.js']); + } + + config.set({ + // frameworks to use + frameworks: ['jasmine'], + + // list of files / patterns to load in the browser + files: getFiles(), + + // list of files to exclude + exclude: [], + + // test results reporter to use + // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' + reporters: ['progress'], + + // web server port + port: 9090, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera (has to be installed with `npm install karma-opera-launcher`) + // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) + // - PhantomJS + // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) + browsers: ['ChromeCanary'], + + // If browser does not capture in given timeout [ms], kill it + captureTimeout: 60000, + + // Continuous Integration mode + // if true, it capture browsers, run tests and exit + singleRun: false + }); +}; diff --git a/client/karma.conf.js b/client/karma.conf.js index f6041fd..71c2512 100644 --- a/client/karma.conf.js +++ b/client/karma.conf.js @@ -20,7 +20,7 @@ module.exports = function(config) { files: getFiles(), // list of files to exclude - exclude: [], + exclude: ['tests/djangoNgMessagesSpec.js'], // test results reporter to use // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' diff --git a/client/src/js/ng-django-forms.js b/client/src/js/ng-django-forms.js index 88b3c6f..92b20c6 100644 --- a/client/src/js/ng-django-forms.js +++ b/client/src/js/ng-django-forms.js @@ -273,12 +273,12 @@ djng_forms_module.factory('djangoForm', function() { } function isField(field) { - return angular.isArray(field.$viewChangeListeners) + return angular.isArray(field.$viewChangeListeners); } return { // setErrors takes care of updating prepared placeholder fields for displaying form errors - // deteced by an AJAX submission. Returns true if errors have been added to the form. + // detected by an AJAX submission. Returns true if errors have been added to the form. setErrors: function(form, errors) { // remove errors from this form, which may have been rejected by an earlier validation form.$message = ''; diff --git a/client/src/js/ng-django-messages.js b/client/src/js/ng-django-messages.js index 63ca9bb..90b00f6 100644 --- a/client/src/js/ng-django-messages.js +++ b/client/src/js/ng-django-messages.js @@ -6,7 +6,8 @@ angular 'ngMessages' ]) - .directive('form', formExtension) + .directive('form', formDirectiveFactory()) + .directive('ngForm', formDirectiveFactory(true)) .directive('validateRejected', validateRejected) .factory('djangoMessagesForm', djangoMessagesForm); @@ -20,15 +21,18 @@ angular * * - setValidFieldsPristine() */ -function formExtension() { + +function formDirectiveFactory(isNgForm) { + +return function() { return { - restrict: 'E', + restrict: isNgForm ? 'EAC' : 'E', require: [ '^?form' ], link: { - pre: function($scope, $element, $attrs, ctrls) { + pre: function(scope, element, attrs, ctrls) { var ctrl = ctrls[0], controls, @@ -74,14 +78,16 @@ function formExtension() { } } } -}; +} + +} function validateRejected() { return { require: 'ngModel', - link: function($scope, $element, $attrs, ngModel) { + link: function(scope, element, attrs, ngModel) { var _hasMessage = false, _value = null; @@ -99,7 +105,7 @@ function validateRejected() { }else{ - _hasMessage = ngModel.$message && ngModel.$message.rejected !== undefined; + _hasMessage = !!(ngModel.$message && ngModel.$message.rejected !== undefined); if(_hasMessage) { _value = value; @@ -133,13 +139,11 @@ function djangoMessagesForm() { function _displayErrors(form, errors) { - form.$setSubmitted(); - angular.forEach(errors, function(error, key) { var field, message = error[0]; - + if(form.hasOwnProperty(key)) { field = form[key]; @@ -170,6 +174,16 @@ function djangoMessagesForm() { }else{ form.$message = message; + /* + * Only set current valid fields to pristine + * + * Any field that's been submitted with an error should + * still display its error + * + * Any field that was valid when the form was submitted, + * may have caused the NON_FIELD_ERRORS, so should be set + * to pristine to prevent it's valid state being displayed + */ form.setValidFieldsPristine(); } } diff --git a/client/tests/djangoNgMessagesSpec.js b/client/tests/djangoNgMessagesSpec.js new file mode 100644 index 0000000..5433347 --- /dev/null +++ b/client/tests/djangoNgMessagesSpec.js @@ -0,0 +1,136 @@ +'use strict'; + +describe('unit tests for module ng.django.messages', function() { + + function compileForm($compile, scope, replace_value) { + var template = + '
    ' + + '' + + '
    '; + var form = angular.element(template.replace('{value}', replace_value)); + $compile(form)(scope); + scope.$digest(); + } + + beforeEach(function() { + module('ng.django.messages'); + }); + + + describe('form extension behaviour', function() { + + var scope, form, field; + + beforeEach(inject(function($rootScope, $compile) { + scope = $rootScope.$new(); + compileForm($compile, scope, ''); + form = scope.valid_form; + field = scope.valid_form.email_field; + })); + + it('should set valid fields back to pristine', function() { + field.$setViewValue('example@example.com'); + expect(field.$valid).toBe(true); + expect(field.$pristine).toBe(false); + form.setValidFieldsPristine(); + expect(field.$valid).toBe(true); + expect(field.$pristine).toBe(true); + }); + + it('should leave invalid fields dirty', function() { + field.$setViewValue('example'); + expect(field.$valid).toBe(false); + expect(field.$pristine).toBe(false); + form.setValidFieldsPristine(); + expect(field.$valid).toBe(false); + expect(field.$pristine).toBe(false); + }); + + }); + + describe('rejected validation directive', function() { + + var scope, form, field; + + beforeEach(inject(function($rootScope, $compile) { + scope = $rootScope.$new(); + compileForm($compile, scope, ''); + form = scope.valid_form; + field = scope.valid_form.email_field; + })); + + it('should invalidate field when rejected message exists', function() { + field.$setViewValue('example@example.com'); + field.$message = {rejected: 'email already in use'}; + field.$validate(); + expect(field.$valid).toBe(false); + expect(field.$error.rejected).toBe(true); + }); + + it('should clear message and return true when new value is set on field and rejected message exists from previous failed rejected validation', function() { + field.$setViewValue('example@example.com'); + field.$message = {rejected: 'email already in use'}; + field.$validate(); + expect(field.$valid).toBe(false); + expect(field.$error.rejected).toBe(true); + field.$setViewValue('example@example2.com'); + expect(field.$valid).toBe(true); + expect(field.$error.rejected).toBe(undefined); + }); + + it('should not remove message if field value is the same as previous failed rejected validation', function() { + field.$setViewValue('example@example.com'); + field.$message = {rejected: 'email already in use'}; + field.$validate(); + expect(field.$valid).toBe(false); + expect(field.$error.rejected).toBe(true); + field.$setViewValue('example@example.com'); + field.$validate(); + expect(field.$valid).toBe(false); + expect(field.$error.rejected).toBe(true); + }); + }); + + + describe('form rejected error handling', function() { + + var scope, form, field, djangoMessagesForm, + formError = {errors: {__all__: ["The email is rejected by the server."]}}, + fieldError = {errors: {email_field: ["This field is required."]}}; + + beforeEach(inject(function($rootScope, $compile, _djangoMessagesForm_){ + scope = $rootScope.$new(); + compileForm($compile, scope, ''); + form = scope.valid_form; + field = scope.valid_form.email_field; + djangoMessagesForm = _djangoMessagesForm_; + })); + + it('should return false if no errors listed', function() { + expect(djangoMessagesForm.setErrors(form, undefined)).toBe(false); + }); + + it('should return true if non field errors listed', function() { + expect(djangoMessagesForm.setErrors(form, formError.errors)).toBe(true); + }); + + it('should return true if field errors listed', function() { + expect(djangoMessagesForm.setErrors(form, fieldError.errors)).toBe(true); + }); + + it('should add rejected non field errors to form.$message and set valid fields pristine', function() { + field.$setViewValue('example@example.com'); + expect(field.$pristine).toBe(false); + djangoMessagesForm.setErrors(form, formError.errors); + expect(form.$message).toBe('The email is rejected by the server.'); + expect(field.$pristine).toBe(true); + }); + + it('should add rejected error message to field.$message.rejected and validate', function() { + djangoMessagesForm.setErrors(form, fieldError.errors); + expect(field.$message.rejected).toBe('This field is required.'); + expect(field.$valid).toBe(false); + }); + }); + +}); From 2eb373169a4be9886fd2791dbfa4e8289a36a5d7 Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Wed, 18 Mar 2015 17:36:16 +0000 Subject: [PATCH 17/25] added bound form error handling and tests --- client/src/js/ng-django-messages.js | 37 +- client/tests/djangoNgMessagesSpec.js | 39 ++- djangular/forms/angular_messages.py | 21 +- docs/angular-messages.rst | 322 ++++++++++++++++++ examples/server/forms/subscribe_form.py | 3 +- examples/server/templates/base.html | 19 +- .../server/templates/form-data-valid.html | 7 - examples/server/templates/model-scope.html | 12 +- examples/server/templates/ng-messages.html | 43 +-- examples/server/templates/subscribe-form.html | 6 +- .../templates/three-way-data-binding.html | 18 +- examples/server/tutorial/ng-messages.html | 13 + 12 files changed, 479 insertions(+), 61 deletions(-) create mode 100644 docs/angular-messages.rst create mode 100644 examples/server/tutorial/ng-messages.html diff --git a/client/src/js/ng-django-messages.js b/client/src/js/ng-django-messages.js index 90b00f6..e9cbdf0 100644 --- a/client/src/js/ng-django-messages.js +++ b/client/src/js/ng-django-messages.js @@ -8,7 +8,8 @@ angular .directive('form', formDirectiveFactory()) .directive('ngForm', formDirectiveFactory(true)) - .directive('validateRejected', validateRejected) + .directive('djngError', djngError) + .directive('djngValidateRejected', validateRejected) .factory('djangoMessagesForm', djangoMessagesForm); @@ -83,12 +84,40 @@ return function() { } +function djngError($timeout) { + + return { + restrict: 'A', + require: '?^form', + link: function(scope, element, attrs, formCtrl) { + var boundField; + if(!formCtrl || attrs.djngError === 'bound-field') + return; + element.removeAttr('djng-error'); + + $timeout(function(){ + formCtrl.$setSubmitted(); + boundField = formCtrl[attrs.name]; + + if(boundField) { + boundField.$message = {rejected: attrs.djngError}; + boundField.$validate(); + } + }); + } + } +} + + function validateRejected() { return { - require: 'ngModel', + restrict: 'A', + require: '?ngModel', link: function(scope, element, attrs, ngModel) { - + + if(!ngModel) return; + var _hasMessage = false, _value = null; @@ -139,6 +168,8 @@ function djangoMessagesForm() { function _displayErrors(form, errors) { + form.$setSubmitted(); + angular.forEach(errors, function(error, key) { var field, diff --git a/client/tests/djangoNgMessagesSpec.js b/client/tests/djangoNgMessagesSpec.js index 5433347..d0bd743 100644 --- a/client/tests/djangoNgMessagesSpec.js +++ b/client/tests/djangoNgMessagesSpec.js @@ -5,7 +5,17 @@ describe('unit tests for module ng.django.messages', function() { function compileForm($compile, scope, replace_value) { var template = '
    ' + - '' + + '' + + '
    '; + var form = angular.element(template.replace('{value}', replace_value)); + $compile(form)(scope); + scope.$digest(); + } + + function compileFormWithBoundError($compile, scope, replace_value) { + var template = + '
    ' + + '' + '
    '; var form = angular.element(template.replace('{value}', replace_value)); $compile(form)(scope); @@ -48,6 +58,33 @@ describe('unit tests for module ng.django.messages', function() { }); + + ddescribe('bound form error handling', function() { + + var scope, form, field, $timeout; + + beforeEach(inject(function($rootScope, $compile, _$timeout_) { + scope = $rootScope.$new(); + compileFormWithBoundError($compile, scope, 'value="barry"'); + form = scope.valid_form; + field = scope.valid_form.email_field; + $timeout = _$timeout_; + })); + + it('should invalidate field when djng-error exists', function() { + $timeout.flush(); + expect(field.$valid).toBe(false); + expect(field.$error.rejected).toBe(true); + expect(field.$message.rejected).toBe('valid email required'); + }); + + it('should set form to $submitted', function() { + $timeout.flush(); + expect(form.$submitted).toBe(true); + }); + }); + + describe('rejected validation directive', function() { var scope, form, field; diff --git a/djangular/forms/angular_messages.py b/djangular/forms/angular_messages.py index 6d4335b..bf55a94 100644 --- a/djangular/forms/angular_messages.py +++ b/djangular/forms/angular_messages.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.utils.html import format_html +from django.utils.html import format_html, format_html_join from django.utils.encoding import python_2_unicode_compatible, force_text from django.utils.safestring import mark_safe, SafeText @@ -20,7 +20,7 @@ class NgMessagesFieldErrorList(TupleErrorList): ul_format = '
      {3}
    ' li_format = '
  • {3}
  • ' - """ span's necessary due to this bug https://github.com/angular/angular.js/issues/8089""" + """ a span is necessary due to this bug https://github.com/angular/angular.js/issues/8089 """ li_format_bind = '
  • ' def as_ul(self): @@ -31,6 +31,9 @@ def as_ul(self): valid_list = [] invalid_list = [] for e in self: + if e[2] == '$pristine': + continue + if e[3] == '$valid': li_format = self.li_format_valid error_list = valid_list @@ -70,7 +73,6 @@ def get_field_errors(self, field): errors = super(NgMessagesMixin, self).get_field_errors(field) if field.is_hidden: return errors - self._remove_ng_model_form_mixin_rejected_error(errors) identifier = format_html('{0}.{1}', self.form_name, field.name) errors.append(SafeTuple((identifier, self.field_error_css_classes, '$dirty', 'rejected', 'invalid', '$message'))) return errors @@ -81,11 +83,12 @@ def non_field_errors(self): def get_widget_attrs(self, bound_field): attrs = super(NgMessagesMixin, self).get_widget_attrs(bound_field) - attrs['validate-rejected'] = "" + attrs.update({'djng-validate-rejected': ''}) + if self.is_bound: + self._apply_bound_error(bound_field, attrs) return attrs - def _remove_ng_model_form_mixin_rejected_error(self, errors): - for item in errors: - if item[2] == '$pristine' and item[5] == '$message': - errors.remove(item) - return + def _apply_bound_error(self, bound_field, attrs): + for error in bound_field.errors: + if error[3] == '$pristine': + attrs.update({'djng-error': error[5]}) diff --git a/docs/angular-messages.rst b/docs/angular-messages.rst new file mode 100644 index 0000000..69a59d0 --- /dev/null +++ b/docs/angular-messages.rst @@ -0,0 +1,322 @@ +.. _angular-messages: + +============================================ +Render form error lists in ngMessages format +============================================ + +https://docs.angularjs.org/api/ngMessages + +The following classes add support for rendering and using ngMessages to manage form error message +display + +NgMessagesMixin +=============== + +Works in combination with other django-angular form mixins (), to facilitate the rendering of +form errors in the correct format for ngMessages and + +.. code-block:: python + + + +Django's forms.Form_ class offers many possibilities to validate a given form. This, for obvious +reasons is done on the server. However, customers may not always accept to submit a form, just to +find out that they missed to input some correct data into a field. Therefore, adding client side +form validation is a good idea and very common. But since client side validation easily can be +bypassed, the same validation has to occur a second time, when the server accepts the forms data +for final processing. + +*This leads to code duplication and generally violates the DRY principle!* + + +A workaround to this problem is to use Django's form declaration to automatically generate client +side validation code, suitable for AngularJS. By adding a special mixin class to the form class, +this can be achieved automatically and on the fly + +.. code-block:: python + + from django import forms + from django.utils import six + from djangular.forms import NgDeclarativeFieldsMetaclass, NgFormValidationMixin + + class MyValidatedForm(six.with_metaclass(NgDeclarativeFieldsMetaclass, NgFormValidationMixin, forms.Form)): + form_name = 'my_valid_form' + surname = forms.CharField(label='Surname', min_length=3, max_length=20) + age = forms.DecimalField(min_value=18, max_value=99) + +In the majority of cases, the Form is derived from Django's ``forms.Form``, so the above example +can be rewritten in a simpler way, by using the convenience class ``NgForm`` as a replacement: + +.. code-block:: python + + from djangular.forms import NgFormValidationMixin, NgForm + + class MyValidatedForm(NgFormValidationMixin, NgForm): + # members as above + +If the Form shall inherit from Django's ``forms.ModelForm``, use the convenience class +``NgModelForm``: + +.. code-block:: python + + from djangular.forms import NgFormValidationMixin, NgModelForm + + class MyValidatedForm(NgFormValidationMixin, NgModelForm): + class Meta: + model = Article + + # fields as usual + +Each page under control of AngularJS requires a unique form name, otherwise the AngularJS's form +validation engine shows undefined behavior. Therefore you must name each form inheriting from +``NgFormValidationMixin``. If a form is used only once per page, the form's name can be added to +the class declaration, as shown above. If no form name is specified, it defaults to ``form``, +limiting the number of validated forms per page to one. + +If a form inheriting from ``NgFormValidationMixin`` shall be instantiated more than once per page, +each instance of that form must be instantiated with a different name. This then must be done in +the constructor of the form, by passing in the argument ``form_name='my_form'``. + +In the view class, add the created form to the rendering context: + +.. code-block:: python + + def get_context_data(self, **kwargs): + context = super(MyRenderingView, self).get_context_data(**kwargs) + context.update(form=MyValidatedForm()) + return context + +or if the same form declaration shall be used more than once: + +.. code-block:: python + + def get_context_data(self, **kwargs): + context = super(MyRenderingView, self).get_context_data(**kwargs) + context.update(form1=MyValidatedForm(form_name='my_valid_form1'), + form2=MyValidatedForm(form_name='my_valid_form2')) + return context + +.. note:: Do not use an empty ``label`` when declaring a form field, otherwise the class + ``NgFormValidationMixin`` won't be able to render AngularJS's validation error elements. + This also applies to ``auto_id``, which if False, will not include ``
      `` elements. The behavior for potential errors remains the same, but +detected errors are hidden the moment, the user sets the form into a dirty state. + +.. note:: AngularJS normally hides the content of bound forms, which means that ```` fields + seem empty, even if their ``value`` attribute is set. In order to restore the content of + those input fields to their default value, initialize your AngularJS application with + ``angular.module('MyApp', ['ng.django.forms']);``. + + +Combine NgFormValidationMixin with NgModelFormMixin +--------------------------------------------------- +While it is possible to use ``NgFormValidationMixin`` on itself, it is perfectly legal to mix +``NgModelFormMixin`` with ``NgFormValidationMixin``. However, a few precautions have to be taken. + +On class declaration inherit first from ``NgModelFormMixin`` and *afterward* from +``NgFormValidationMixin``. Valid example: + +.. code-block:: python + + from django import forms + from djangular.forms import NgFormValidationMixin, NgModelFormMixin + + class MyValidatedForm(NgModelFormMixin, NgFormValidationMixin, forms.Form): + # custom form fields + +but don't do this + +.. code-block:: python + + class MyValidatedForm(NgFormValidationMixin, NgModelFormMixin, forms.Form): + # custom form fields + +Another precaution to take, is to use different names for the forms name and the ``scope_prefix``. +So, this is legal + +.. code-block:: python + + form = MyValidatedForm(form_name='my_form', scope_prefix='my_model') + +but this is not + +.. code-block:: python + + form = MyValidatedForm(form_name='my_form', scope_prefix='my_form') + +An implementation note +...................... +AngularJS names each input field to validate, by concatenating its forms name with its fields name. +This object member then contains an error object, named ``my_form.field_name.$error`` filled by +the AngularJS validation mechanism. The placeholder for the error object would clash with +``ng-model``, if the form name is identical to the scope prefix. Therefore, just remember to use +different names. + + +Customize detected and potential validation errors +================================================== +If a form with AngularJS validation is rendered, each input field is prefixed with an unsorted list +``
        `` of potential validation errors. For each possible constraint violation, a list item +``
      • `` containing a descriptive message is added to that list. + +If a client enters invalid data into that form, AngularJS unhides one of these prepared error +messages, using ng-show_. The displayed message text is exactly the same as would be shown if +the server side code complains about invalid data during form validation. These prepared error +messages can be customized during `form field definition`_. + +The default error list is rendered as ``
          ...
        ``. To each ``
      • `` +of this error list, the attribute ``class="invalid"`` is added. The last list-item +``
      • `` is somehow special, as it is only visible if the corresponding input +field contains valid data. By using special style sheets, one can for instance add a green +tick after a validated input field, to signal that everything is OK. + +The styling of these validation elements must be done through CSS, for example with: + +.. code-block:: css + + ul.djng-form-errors { + margin-left: 0; + display: inline-block; + list-style-type: none; + } + ul.djng-form-errors li.invalid { + color: #e9322d; + } + ul.djng-form-errors li.invalid:before { + content: "\2716\20"; /* adds a red cross before the error message */ + } + ul.djng-form-errors li.valid:before { + color: #00c900; + content: "\2714"; /* adds a green tick */ + } + +If you desire an alternative CSS class or an alternative way of rendering the list of errors, then +initialize the form instance with + +.. code-block:: python + + class MyErrorList(list): + # rendering methods go here + + # during form instantiation + my_form = MyForm(error_class=MyErrorList) + +Refer to ``TupleErrorList`` on how to implement an alternative error list renderer. Currently this +error list renderer, renders two ``
          ``-elements for each input field, one to be shown for +*pristine* forms and one to be shown for *dirty* forms. + + +Adding form validation to customized fields +------------------------------------------- +Django's form validation is not 1:1 compatible with AngularJS's validation. Therefore **djangular** +is shipped with a mapping module, which translate Django's form validation to AngularJS. This module +is located in ``djangular.forms.patched_fields``. + +If you need to add or to replace any of these mappings, create a Python module which implements an +alternative mapping to the module shipped with **djangular**. Refer to an alternative module in your +``settings.py`` with the configuration directive ``DJANGULAR_VALIDATION_MAPPING_MODULE``. + +For further information about how to use form validation with AngularJS, please refer to the +:ref:`demo pages`. + + +Adding an AngularJS directive for validating form fields +-------------------------------------------------------- +Sometimes it can be useful to add a generic field validator on the client side, which can be +controlled by the form's definition on the server. One such example is Django's DateField: + +.. code-block:: python + + from django import forms + + class MyForm(forms.Form): + # other fields + date = forms.DateField(label='Date', + widget=forms.widgets.DateInput(attrs={'validate-date': '^(\d{4})-(\d{1,2})-(\d{1,2})$'})) + +Since AngularJS can not validate dates, such a field requires a customized directive, which with +the above definition, will be added as new attribute to the input element for date: + +.. code-block:: html + + + +If your AngularJS application has been initialized with + +.. code-block:: javascript + + angular.module('MyApp', ['ng.django.forms']); + +then this new attribute is detected by the AngularJS directive ``validateDate``, which in turn +checks the date for valid input and shows the content of the errors fields, if not. + +If you need to write a reusable component for customized form fields, refer to that directive as a +starting point. + +.. _forms.Form: https://docs.djangoproject.com/en/dev/topics/forms/#form-objects +.. _form field definition: https://docs.djangoproject.com/en/dev/ref/forms/fields/#error-messages +.. _ng-show: http://docs.angularjs.org/api/ng.directive:ngShow +.. _form is bound: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.BoundField.errors diff --git a/examples/server/forms/subscribe_form.py b/examples/server/forms/subscribe_form.py index 1cfafb7..3897845 100644 --- a/examples/server/forms/subscribe_form.py +++ b/examples/server/forms/subscribe_form.py @@ -21,6 +21,7 @@ class SubscribeForm(Bootstrap3Form): NOTIFY_BY = (('email', 'EMail'), ('phone', 'Phone'), ('sms', 'SMS'), ('postal', 'Postcard'),) first_name = forms.CharField(label='First name', min_length=3, max_length=20) + last_name = forms.RegexField(r'^[A-Z][a-z -]?', label='Last name', error_messages={'invalid': 'Last names shall start in upper case'}) sex = forms.ChoiceField(choices=(('m', 'Male'), ('f', 'Female')), @@ -53,4 +54,4 @@ class SubscribeForm(Bootstrap3Form): validators=[validate_password], help_text='The password is "secret"') confirmation_key = forms.CharField(max_length=40, required=True, widget=forms.HiddenInput(), - initial='hidden value') + initial='hidden value') \ No newline at end of file diff --git a/examples/server/templates/base.html b/examples/server/templates/base.html index b906743..506d4e6 100644 --- a/examples/server/templates/base.html +++ b/examples/server/templates/base.html @@ -36,7 +36,7 @@ Combined Validation
        • - angular-messages + Angular Messages
        • {% if WITH_WS4REDIS %}
        • @@ -51,12 +51,21 @@
          {% block container %}{% endblock %}
          - - {% block angular_script %} - - {% endblock %} + + + {% block scripts %}{% endblock %} + + + + {% block demo_scripts %} + {% endblock %} + Fork me on GitHub diff --git a/examples/server/templates/form-data-valid.html b/examples/server/templates/form-data-valid.html index b1d8cd2..d4163fa 100644 --- a/examples/server/templates/form-data-valid.html +++ b/examples/server/templates/form-data-valid.html @@ -4,10 +4,3 @@

          Thank's for submission

          The entered data was valid!

          {% endblock container %} - -{% block scripts %} - {{ block.super }} - -{% endblock scripts %} diff --git a/examples/server/templates/model-scope.html b/examples/server/templates/model-scope.html index 10db8a1..0935bd6 100644 --- a/examples/server/templates/model-scope.html +++ b/examples/server/templates/model-scope.html @@ -19,18 +19,18 @@
          MyFormCtrl's scope:
          {% endverbatim %} {% endblock form_foot %} -{% block scripts %} +{% block ng_module_definition %} {{ block.super }} - +{% endblock %} + +{% block demo_scripts %} + {{ block.super }} - -{% endblock scripts %} +{% endblock %} {% block tutorial_intro %}

          This example shows how to diff --git a/examples/server/templates/ng-messages.html b/examples/server/templates/ng-messages.html index dbf7e9b..3813e81 100644 --- a/examples/server/templates/ng-messages.html +++ b/examples/server/templates/ng-messages.html @@ -1,9 +1,10 @@ {% extends "model-scope.html" %} {% load tutorial_tags %} -{% block form_title %}NgMessages Demo{% endblock %} +{% block form_title %}Angular Messages Demo{% endblock %} + +{% block form_header %}Adds support for form field error lists to be rendered to use the ngMessages directive. Requires angular 1.3+.{% endblock %} -{% block form_header %}Combine client-side Form validation with two-way data-binding and angular-messages.
          Requires angular 1.3+{% endblock %} {% block form_tag %}name="{{ form.form_name }}" method="post" action="." novalidate ng-controller="MyFormCtrl"{% endblock %} @@ -20,32 +21,32 @@ {% block tutorial_intro %}

          In this example, -client-side Form validation is combined with two-way data-binding.

          -

          If a Form inherits from both mixin classes NgModelFormMixin and -NgFormValidationMixin, then django-angular combines -client-side validation with AngularJS's -model scope.

          -

          Refer to the previous examples for a detailed explanation.

          +Form field error display and management is handled by the ngMessages directive.

          +

          If a Form inherits from NgMessagesMixin then each field error list is rendered +to use the ng-messages module directive.

          + {% endblock tutorial_intro %} {% block tutorial_code %} -
          {% pygments "forms/combined_validation.py" %}
          -
          {% pygments "views/combined_validation.py" %}
          -
          {% pygments "tutorial/combined-validation.html" %}
          -
          {% pygments "static/js/model-scope.js" %}
          -

          This configuration is the most flexible one. Use it on productive web-sites.
          -Note: The submit buttons are disabled, until the client-side Form validation has -validated all the fields.

          +
          {% pygments "forms/ng_messages.py" %}
          +
          {% pygments "views/ng_messages.py" %}
          +
          {% pygments "tutorial/ng-messages.html" %}
          +
          {% pygments "static/js/messages-demo.js" %}
          +

          Use this mixin if you want your field error lists to be managed by the ng-messages directive.
          +Note: Another advantage over the standard django-angular field error list handling, is up to a 30% reduction in watchers.

          {% endblock tutorial_code %} -{% block angular_script %} - -{% endblock %} +{% block angular_version %}1.3.0{% endblock %} + {% block scripts %} + {{ block.super }} - - {% endblock scripts %} -{% block app_dependencies %}['ng.django.forms', 'ng.django.messages']{% endblock app_dependencies %} +{% block ng_module_dependencies %}['ng.django.forms', 'ng.django.messages']{% endblock %} + +{% block demo_scripts %} + {{ block.super }} + +{% endblock %} diff --git a/examples/server/templates/subscribe-form.html b/examples/server/templates/subscribe-form.html index 1ff91f1..eac1172 100644 --- a/examples/server/templates/subscribe-form.html +++ b/examples/server/templates/subscribe-form.html @@ -74,7 +74,7 @@

          How does it work?

          {% block scripts %} - {% endblock %} + +{% block ng_module_dependencies %}['ng.django.forms']{% endblock %} + diff --git a/examples/server/templates/three-way-data-binding.html b/examples/server/templates/three-way-data-binding.html index 89ade8c..2c86f9c 100644 --- a/examples/server/templates/three-way-data-binding.html +++ b/examples/server/templates/three-way-data-binding.html @@ -19,18 +19,26 @@
          MyWebsocketCtrl's scope:
          {% block scripts %} {{ block.super }} - +{% endblock %} + +{% block demo_scripts %} - -{% endblock scripts %} + {{ block.super }} +{% endblock %} + + + {% block tutorial_intro %}

          This example shows, diff --git a/examples/server/tutorial/ng-messages.html b/examples/server/tutorial/ng-messages.html new file mode 100644 index 0000000..465a902 --- /dev/null +++ b/examples/server/tutorial/ng-messages.html @@ -0,0 +1,13 @@ + + +

          + {% csrf_token %} + {{ form.as_div }} + + +
          From 589dad4646d73f99cd083fc4754b4e9007d35b06 Mon Sep 17 00:00:00 2001 From: jamesrobb Date: Thu, 19 Mar 2015 12:51:21 +0000 Subject: [PATCH 18/25] added docs for ngMessages support --- client/src/js/ng-django-messages.js | 4 +- client/tests/djangoNgMessagesSpec.js | 18 +- djangular/forms/angular_messages.py | 1 + docs/angular-form-validation.rst | 2 +- docs/angular-messages.rst | 349 +++++---------------- examples/server/static/js/lodash.min.js | 56 ---- examples/server/static/js/messages-demo.js | 4 +- examples/server/templates/ng-messages.html | 2 +- 8 files changed, 88 insertions(+), 348 deletions(-) delete mode 100644 examples/server/static/js/lodash.min.js diff --git a/client/src/js/ng-django-messages.js b/client/src/js/ng-django-messages.js index e9cbdf0..679a799 100644 --- a/client/src/js/ng-django-messages.js +++ b/client/src/js/ng-django-messages.js @@ -10,7 +10,7 @@ angular .directive('ngForm', formDirectiveFactory(true)) .directive('djngError', djngError) .directive('djngValidateRejected', validateRejected) - .factory('djangoMessagesForm', djangoMessagesForm); + .factory('djngMessagesForm', djngMessagesForm); @@ -148,7 +148,7 @@ function validateRejected() { } -function djangoMessagesForm() { +function djngMessagesForm() { return { setErrors: setErrors diff --git a/client/tests/djangoNgMessagesSpec.js b/client/tests/djangoNgMessagesSpec.js index d0bd743..d2cfb79 100644 --- a/client/tests/djangoNgMessagesSpec.js +++ b/client/tests/djangoNgMessagesSpec.js @@ -59,7 +59,7 @@ describe('unit tests for module ng.django.messages', function() { }); - ddescribe('bound form error handling', function() { + describe('bound form error handling', function() { var scope, form, field, $timeout; @@ -131,40 +131,40 @@ describe('unit tests for module ng.django.messages', function() { describe('form rejected error handling', function() { - var scope, form, field, djangoMessagesForm, + var scope, form, field, djngMessagesForm, formError = {errors: {__all__: ["The email is rejected by the server."]}}, fieldError = {errors: {email_field: ["This field is required."]}}; - beforeEach(inject(function($rootScope, $compile, _djangoMessagesForm_){ + beforeEach(inject(function($rootScope, $compile, _djngMessagesForm_){ scope = $rootScope.$new(); compileForm($compile, scope, ''); form = scope.valid_form; field = scope.valid_form.email_field; - djangoMessagesForm = _djangoMessagesForm_; + djngMessagesForm = _djngMessagesForm_; })); it('should return false if no errors listed', function() { - expect(djangoMessagesForm.setErrors(form, undefined)).toBe(false); + expect(djngMessagesForm.setErrors(form, undefined)).toBe(false); }); it('should return true if non field errors listed', function() { - expect(djangoMessagesForm.setErrors(form, formError.errors)).toBe(true); + expect(djngMessagesForm.setErrors(form, formError.errors)).toBe(true); }); it('should return true if field errors listed', function() { - expect(djangoMessagesForm.setErrors(form, fieldError.errors)).toBe(true); + expect(djngMessagesForm.setErrors(form, fieldError.errors)).toBe(true); }); it('should add rejected non field errors to form.$message and set valid fields pristine', function() { field.$setViewValue('example@example.com'); expect(field.$pristine).toBe(false); - djangoMessagesForm.setErrors(form, formError.errors); + djngMessagesForm.setErrors(form, formError.errors); expect(form.$message).toBe('The email is rejected by the server.'); expect(field.$pristine).toBe(true); }); it('should add rejected error message to field.$message.rejected and validate', function() { - djangoMessagesForm.setErrors(form, fieldError.errors); + djngMessagesForm.setErrors(form, fieldError.errors); expect(field.$message.rejected).toBe('This field is required.'); expect(field.$valid).toBe(false); }); diff --git a/djangular/forms/angular_messages.py b/djangular/forms/angular_messages.py index bf55a94..1199d87 100644 --- a/djangular/forms/angular_messages.py +++ b/djangular/forms/angular_messages.py @@ -31,6 +31,7 @@ def as_ul(self): valid_list = [] invalid_list = [] for e in self: + """ Ignore $pristine errors, as they relate to the original rejected error handling or djng-error bound-field""" if e[2] == '$pristine': continue diff --git a/docs/angular-form-validation.rst b/docs/angular-form-validation.rst index 4f57b7f..73a1e41 100644 --- a/docs/angular-form-validation.rst +++ b/docs/angular-form-validation.rst @@ -127,7 +127,7 @@ rendered in templates using a special field tag. Say, the form contains from djangular.forms import NgFormValidationMixin class MyValidatedForm(NgFormValidationMixin, forms.Form): - email = forms.EmailField(label='Email') + email = forms.EmailField(label='Email') then access the potential validation errors in templates using ``{{ form.email.errors }}``. This renders the form with an unsorted list of potential errors, which may occur during client side diff --git a/docs/angular-messages.rst b/docs/angular-messages.rst index 69a59d0..1012f32 100644 --- a/docs/angular-messages.rst +++ b/docs/angular-messages.rst @@ -1,322 +1,117 @@ .. _angular-messages: -============================================ -Render form error lists in ngMessages format -============================================ +================================================== +Render Form field error lists in ngMessages format +================================================== -https://docs.angularjs.org/api/ngMessages +.. note:: This requires Angular 1.3+ and the ``angular-messages`` module - https://docs.angularjs.org/api/ngMessages -The following classes add support for rendering and using ngMessages to manage form error message -display NgMessagesMixin =============== -Works in combination with other django-angular form mixins (), to facilitate the rendering of -form errors in the correct format for ngMessages and - -.. code-block:: python - - - -Django's forms.Form_ class offers many possibilities to validate a given form. This, for obvious -reasons is done on the server. However, customers may not always accept to submit a form, just to -find out that they missed to input some correct data into a field. Therefore, adding client side -form validation is a good idea and very common. But since client side validation easily can be -bypassed, the same validation has to occur a second time, when the server accepts the forms data -for final processing. - -*This leads to code duplication and generally violates the DRY principle!* - - -A workaround to this problem is to use Django's form declaration to automatically generate client -side validation code, suitable for AngularJS. By adding a special mixin class to the form class, -this can be achieved automatically and on the fly +The ``NgMessagesMixin`` mixin can be used in conjunction with other django-angular form mixins (``NgFormValidationMixin`` +and ``NgModelFormMixin``) or on its own, to facilitate the rendering of form field error lists, in the +correct format for the ngMessages directive. .. code-block:: python from django import forms - from django.utils import six - from djangular.forms import NgDeclarativeFieldsMetaclass, NgFormValidationMixin - - class MyValidatedForm(six.with_metaclass(NgDeclarativeFieldsMetaclass, NgFormValidationMixin, forms.Form)): - form_name = 'my_valid_form' - surname = forms.CharField(label='Surname', min_length=3, max_length=20) - age = forms.DecimalField(min_value=18, max_value=99) + from djangular.forms import NgForm, NgFormValidationMixin, NgMessagesMixin -In the majority of cases, the Form is derived from Django's ``forms.Form``, so the above example -can be rewritten in a simpler way, by using the convenience class ``NgForm`` as a replacement: + class MyNgMessagesForm(NgMessagesMixin, NgFormValidationMixin, NgForm): + form_name = 'my_form' + email = forms.EmailField(label='Email') + +Then using ``{{ form.email.errors }}`` would output the following markup: -.. code-block:: python +.. code-block:: html - from djangular.forms import NgFormValidationMixin, NgForm +
            +
          • This field is required.
          • +
          • Enter a valid email address.
          • +
          • + +
          • +
          - class MyValidatedForm(NgFormValidationMixin, NgForm): - # members as above - -If the Form shall inherit from Django's ``forms.ModelForm``, use the convenience class -``NgModelForm``: - -.. code-block:: python - - from djangular.forms import NgFormValidationMixin, NgModelForm - class MyValidatedForm(NgFormValidationMixin, NgModelForm): - class Meta: - model = Article +Handling Ajax form errors +......................... - # fields as usual - -Each page under control of AngularJS requires a unique form name, otherwise the AngularJS's form -validation engine shows undefined behavior. Therefore you must name each form inheriting from -``NgFormValidationMixin``. If a form is used only once per page, the form's name can be added to -the class declaration, as shown above. If no form name is specified, it defaults to ``form``, -limiting the number of validated forms per page to one. - -If a form inheriting from ``NgFormValidationMixin`` shall be instantiated more than once per page, -each instance of that form must be instantiated with a different name. This then must be done in -the constructor of the form, by passing in the argument ``form_name='my_form'``. - -In the view class, add the created form to the rendering context: - -.. code-block:: python - - def get_context_data(self, **kwargs): - context = super(MyRenderingView, self).get_context_data(**kwargs) - context.update(form=MyValidatedForm()) - return context - -or if the same form declaration shall be used more than once: - -.. code-block:: python - - def get_context_data(self, **kwargs): - context = super(MyRenderingView, self).get_context_data(**kwargs) - context.update(form1=MyValidatedForm(form_name='my_valid_form1'), - form2=MyValidatedForm(form_name='my_valid_form2')) - return context - -.. note:: Do not use an empty ``label`` when declaring a form field, otherwise the class - ``NgFormValidationMixin`` won't be able to render AngularJS's validation error elements. - This also applies to ``auto_id``, which if False, will not include ``