Fix constraint sync during migration
- restore constraints lost during merge in test schema.
- use constraint_by_eid in BeforeDeleteCWConstraintHook as done in
3.17.14 for BeforeDeleteConstrainedByHook. Fixes handling of multiple
constraints of the same type.
- make sync_schema_props_perms() delete the CWConstraint entity instead
of the constrained_by relation. In 3.19, the latter doesn't
automatically result in the former just because the relation is
composite. Simplify the constraint migration to delete all removed
constraints and recreate new ones even if they share the same type; that
optimization made the code more complicated for (AFAICT) no significant
reason.
/** * Functions dedicated to widgets. * * :organization: Logilab * :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. * :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr * * */// widget namespaceWidgets={};/** * .. function:: buildWidget(wdgnode) * * this function takes a DOM node defining a widget and * instantiates / builds the appropriate widget class */functionbuildWidget(wdgnode){varwdgclass=Widgets[wdgnode.getAttribute('cubicweb:wdgtype')];if(wdgclass){returnnewwdgclass(wdgnode);}returnnull;}/** * .. function:: buildWidgets(root) * * This function is called on load and is in charge to build * JS widgets according to DOM nodes found in the page */functionbuildWidgets(root){root=root||document;jQuery(root).find('.widget').each(function(){if(this.getAttribute('cubicweb:loadtype')=='auto'){buildWidget(this);}});}jQuery(document).ready(function(){buildWidgets();});functionpostJSON(url,data,callback){returnjQuery.post(url,data,callback,AJAX_BASE_URL);}functiongetJSON(url,data,callback){returnjQuery.get(url,data,callback,AJAX_BASE_URL);}(function($){vardefaultSettings={initialvalue:'',multiple:false,mustMatch:false,delay:50,limit:50};functionsplit(val){returnval.split(/\s*,\s*/);}functionextractLast(term){returnsplit(term).pop();}functionallButLast(val){varterms=split(val);terms.pop();returnterms;}varmethods={__init__:function(suggestions,options){returnthis.each(function(){// here, `this` refers to the DOM element (e.g. input) being wrapped// by cwautomplete pluginvarinstanceData=$(this).data('cwautocomplete');if(instanceData){// already initializedreturn;}varsettings=$.extend({},defaultSettings,options);instanceData={initialvalue:settings.initialvalue,userInput:this,hiddenInput:null};varhiHandlers=methods.hiddenInputHandlers;$(this).data('cwautocomplete',instanceData);// in case of an existing value, the hidden input must be initialized even if// the value is not changedif(($(instanceData.userInput).attr('cubicweb:initialvalue')!==undefined)&&!instanceData.hiddenInput){hiHandlers.initializeHiddenInput(instanceData);}$.ui.autocomplete.prototype._value=methods._value;$.data(this,'settings',settings);if(settings.multiple){$.ui.autocomplete.filter=methods.multiple.makeFilter(this);$(this).bind({autocompleteselect:methods.multiple.select,autocompletefocus:methods.multiple.focus,keydown:methods.multiple.keydown});}// XXX katia we dont need it if minLength == 0, but by setting minLength = 0// we probably break the backward compatibility$(this).bind('blur',methods.blur);if($.isArray(suggestions)){// precomputed list of suggestionssettings.source=hiHandlers.checkSuggestionsDataFormat(instanceData,suggestions);}else{// url to call each time something is typedsettings.source=function(request,response){vard=loadRemote(suggestions,{q:request.term,limit:settings.limit},'POST');d.addCallback(function(suggestions){suggestions=hiHandlers.checkSuggestionsDataFormat(instanceData,suggestions);response(suggestions);if((suggestions.length)==0){methods.resetValues(instanceData);}});};}$(this).autocomplete(settings);if(settings.mustMatch){$(this).keypress(methods.ensureExactMatch);}});},_value:function(){/* We extend the widget with the ability to lookup and handle several terms at once ('multiple' option). E.g.: toto, titi, tu.... The autocompletion must be performed only on the last of such a list of terms. */varsettings=$(this.element).data('settings');varvalue=this.valueMethod.apply(this.element,arguments);if(settings.multiple&arguments.length===0){returnextractLast(value);}returnvalue},multiple:{focus:function(){// prevent value inserted on focusreturnfalse;},select:function(event,ui){varterms=allButLast(this.value);// add the selected itemterms.push(ui.item.value);// add placeholder to get the comma-and-space at the endterms.push("");this.value=terms.join(", ");returnfalse;},keydown:function(evt){if(evt.keyCode==$.ui.keyCode.TAB){evt.preventDefault();}},makeFilter:function(userInput){returnfunction(array,term){// remove already entered terms from suggestion listarray=cw.utils.difference(array,allButLast(userInput.value));varmatcher=newRegExp($.ui.autocomplete.escapeRegex(term),"i");return$.grep(array,function(value){returnmatcher.test(value.label||value.value||value);});};}},blur:function(evt){varinstanceData=$(this).data('cwautocomplete');if($(instanceData.userInput).val().strip().length==0){methods.resetValues(instanceData);}},ensureExactMatch:function(evt){varinstanceData=$(this).data('cwautocomplete');if(evt.keyCode==$.ui.keyCode.ENTER||evt.keyCode==$.ui.keyCode.TAB){varvalidChoices=$.map($('ul.ui-autocomplete li'),function(li){return$(li).text();});if($.inArray($(instanceData.userInput).val(),validChoices)==-1){$(instanceData.userInput).val('');$(instanceData.hiddenInput).val(instanceData.initialvalue||'');}}},resetValues:function(instanceData){$(instanceData.userInput).val('');$(instanceData.hiddenInput).val('');},hiddenInputHandlers:{/** * `hiddenInputHandlers` defines all methods specific to handle the * hidden input created along the standard text input. * An hiddenInput is necessary when displayed suggestions are * different from actual values to submit. * Imagine an autocompletion widget to choose among a list of CWusers. * Suggestions would be the list of logins, but actual values would * be the corresponding eids. * To handle such cases, suggestions list should be a list of JS objects * with two `label` and `value` properties. **/suggestionSelected:function(evt,ui){varinstanceData=$(this).data('cwautocomplete');instanceData.hiddenInput.value=ui.item.value;instanceData.value=ui.item.label;returnfalse;// stop propagation},suggestionFocusChanged:function(evt,ui){varinstanceData=$(this).data('cwautocomplete');instanceData.userInput.value=ui.item.label;returnfalse;// stop propagation},needsHiddenInput:function(suggestions){returnsuggestions[0].label!==undefined;},initializeHiddenInput:function(instanceData){varuserInput=instanceData.userInput;varhiddenInput=INPUT({type:"hidden",name:userInput.name,// XXX katia : this must be handeled in .SuggestField widget, but// it seems not to be used anymorevalue:$(userInput).attr('cubicweb:initialvalue')||userInput.value});$(userInput).removeAttr('name').after(hiddenInput);instanceData.hiddenInput=hiddenInput;$(userInput).bind({autocompleteselect:methods.hiddenInputHandlers.suggestionSelected,autocompletefocus:methods.hiddenInputHandlers.suggestionFocusChanged});},/* * internal convenience function: old jquery plugin accepted to be fed * with a list of couples (value, label). The new (jquery-ui) autocomplete * plugin expects a list of objects with "value" and "label" properties. * * This function converts the old format to the new one. */checkSuggestionsDataFormat:function(instanceData,suggestions){// check for old (value, label) formatif($.isArray(suggestions)&&suggestions.length&&$.isArray(suggestions[0])){if(suggestions[0].length==2){cw.log('[3.10] autocomplete init func should return {label,value} dicts instead of lists');suggestions=$.map(suggestions,function(sugg){return{value:sugg[0],label:sugg[1]};});}else{if(suggestions[0].length==1){suggestions=$.map(suggestions,function(sugg){return{value:sugg[0],label:sugg[0]};});}}}varhiHandlers=methods.hiddenInputHandlers;if(suggestions.length&&hiHandlers.needsHiddenInput(suggestions)&&!instanceData.hiddenInput){hiHandlers.initializeHiddenInput(instanceData);hiHandlers.fixUserInputInitialValue(instanceData,suggestions);}// otherwise, assume data shape is correctreturnsuggestions;},fixUserInputInitialValue:function(instanceData,suggestions){// called when the data is loaded to reset the correct displayed// value in the visible input field (typically replacing an eid// by a displayable value)varcurvalue=instanceData.userInput.value;if(!curvalue){return;}for(vari=0,length=suggestions.length;i<length;i++){varsugg=suggestions[i];if(sugg.value==curvalue){instanceData.userInput.value=sugg.label;return;}}}}};$.fn.cwautocomplete=function(data,options){returnmethods.__init__.apply(this,[data,options]);};})(jQuery);Widgets.SuggestField=defclass('SuggestField',null,{__init__:function(node,options){options=options||{};varmulti=node.getAttribute('cubicweb:multi');options.multiple=(multi=="yes")?true:false;vard=loadRemote(node.getAttribute('cubicweb:dataurl'));d.addCallback(function(data){$(node).cwautocomplete(data,options);});}});Widgets.StaticFileSuggestField=defclass('StaticSuggestField',[Widgets.SuggestField],{__init__:function(node){Widgets.SuggestField.__init__(this,node,{method:'get'// XXX});}});Widgets.RestrictedSuggestField=defclass('RestrictedSuggestField',[Widgets.SuggestField],{__init__:function(node){Widgets.SuggestField.__init__(this,node,{mustMatch:true});}});//remote version of RestrictedSuggestFieldWidgets.LazySuggestField=defclass('LazySuggestField',[Widgets.SuggestField],{__init__:function(node,options){varself=this;options=options||{};options.delay=50;// multiple selection not supported yet (still need to formalize correctly// initial values / display values)varinitialvalue=cw.evalJSON(node.getAttribute('cubicweb:initialvalue')||'null');if(!initialvalue){initialvalue=node.value;}options.initialvalue=initialvalue;Widgets.SuggestField.__init__(this,node,options);}});/** * .. class:: Widgets.SuggestForm * * suggestform displays a suggest field and associated validate / cancel buttons * constructor's argumemts are the same that BaseSuggestField widget */Widgets.SuggestForm=defclass("SuggestForm",null,{__init__:function(inputid,initfunc,varargs,validatefunc,options){this.validatefunc=validatefunc||noop;this.sgfield=newWidgets.BaseSuggestField(inputid,initfunc,varargs,options);this.oklabel=options.oklabel||'ok';this.cancellabel=options.cancellabel||'cancel';bindMethods(this);connect(this.sgfield,'validate',this,this.entryValidated);},show:function(parentnode){varsgnode=this.sgfield.builddom();varbuttons=DIV({'class':"sgformbuttons"},[A({'href':"javascript: noop();",'onclick':this.onValidateClicked},this.oklabel),' / ',A({'href':"javascript: noop();",'onclick':this.destroy},escapeHTML(this.cancellabel))]);varformnode=DIV({'class':"sgform"},[sgnode,buttons]);appendChildNodes(parentnode,formnode);this.sgfield.textinput.focus();this.formnode=formnode;returnformnode;},destroy:function(){signal(this,'destroy');this.sgfield.destroy();removeElement(this.formnode);},onValidateClicked:function(){this.validatefunc(this,this.sgfield.taglist());},/* just an indirection to pass the form instead of the sgfield as first parameter */entryValidated:function(sgfield,taglist){this.validatefunc(this,taglist);}});/** * .. function:: toggleTree(event) * * called when the use clicks on a tree node * - if the node has a `cubicweb:loadurl` attribute, replace the content of the node * by the url's content. * - else, there's nothing to do, let the jquery plugin handle it. */functiontoggleTree(event){varlinode=jQuery(this);varurl=linode.attr('cubicweb:loadurl');if(url){linode.find('ul.placeholder').remove();vard=linode.loadxhtml(url,null,'post','append');d.addCallback(function(domnode){linode.removeAttr('cubicweb:loadurl');linode.find('> ul.treeview').treeview({toggle:toggleTree,prerendered:true});returnnull;});}}/** * .. class:: Widgets.TimelineWidget * * widget based on SIMILE's timeline widget * http://code.google.com/p/simile-widgets/ * * Beware not to mess with SIMILE's Timeline JS namepsace ! */Widgets.TimelineWidget=defclass("TimelineWidget",null,{__init__:function(wdgnode){vartldiv=DIV({id:"tl",style:'height: 200px; border: 1px solid #ccc;'});wdgnode.appendChild(tldiv);vartlunit=wdgnode.getAttribute('cubicweb:tlunit')||'YEAR';vareventSource=newTimeline.DefaultEventSource();varbandData={eventPainter:Timeline.CubicWebEventPainter,eventSource:eventSource,width:"100%",intervalUnit:Timeline.DateTime[tlunit.toUpperCase()],intervalPixels:100};varbandInfos=[Timeline.createBandInfo(bandData)];this.tl=Timeline.create(tldiv,bandInfos);varloadurl=wdgnode.getAttribute('cubicweb:loadurl');Timeline.loadJSON(loadurl,function(json,url){eventSource.loadJSON(json,url);});}});Widgets.TemplateTextField=defclass("TemplateTextField",null,{__init__:function(wdgnode){this.variables=jQuery(wdgnode).attr('cubicweb:variables').split(',');this.options={name:wdgnode.getAttribute('cubicweb:inputid'),rows:wdgnode.getAttribute('cubicweb:rows')||40,cols:wdgnode.getAttribute('cubicweb:cols')||80};// this.variableRegexp = /%\((\w+)\)s/;this.errorField=DIV({'class':"errorMessage"});this.textField=TEXTAREA(this.options);jQuery(this.textField).bind('keyup',{'self':this},this.highlightInvalidVariables);jQuery('#substitutions').prepend(this.errorField);jQuery('#substitutions .errorMessage').hide();wdgnode.appendChild(this.textField);},/* signal callbacks */highlightInvalidVariables:function(event){varself=event.data.self;vartext=self.textField.value;varunknownVariables=[];varit=0;vargroup=null;varvariableRegexp=/%\((\w+)\)s/g;// emulates rgx.findAll()while((group=variableRegexp.exec(text))){if($.inArray(group[1],self.variables)==-1){unknownVariables.push(group[1]);}it++;if(it>5){break;}}varerrText='';if(unknownVariables.length){errText="Detected invalid variables : "+unknownVariables.join(', ');jQuery('#substitutions .errorMessage').show();}else{jQuery('#substitutions .errorMessage').hide();}self.errorField.innerHTML=errText;}});cw.widgets={/** * .. function:: insertText(text, areaId) * * inspects textarea with id `areaId` and replaces the current selected text * with `text`. Cursor is then set at the end of the inserted text. */insertText:function(text,areaId){vartextarea=jQuery('#'+areaId);if(document.selection){// IEvarselLength;textarea.focus();varsel=document.selection.createRange();selLength=sel.text.length;sel.text=text;sel.moveStart('character',selLength-text.length);sel.select();}elseif(textarea.selectionStart||textarea.selectionStart=='0'){// mozillavarstartPos=textarea.selectionStart;varendPos=textarea.selectionEnd;// insert text so that it replaces the [startPos, endPos] parttextarea.value=textarea.value.substring(0,startPos)+text+textarea.value.substring(endPos,textarea.value.length);// set cursor pos at the end of the inserted texttextarea.selectionStart=textarea.selectionEnd=startPos+text.length;textarea.focus();}else{// safety belt for other browserstextarea.value+=text;}}};// InOutWidget This contains specific InOutnWidget javascript// IE things can not handle hide/show options on select, this cloned list solition (should propably have 2 widgets)(function($){varmethods={__init__:function(fromSelect,toSelect){// closed over statevarstate={'$fromNode':$(cw.escape('#'+fromSelect)),'$toNode':$(cw.escape('#'+toSelect)),'name':this.attr('id')};functionsortoptions($optionlist){var$sorted=$optionlist.find('option').sort(function(opt1,opt2){return$(opt1).text()>$(opt2).text()?1:-1;});// this somehow translates to an inplace sort$optionlist.append($sorted);};sortoptions(state.$fromNode);sortoptions(state.$toNode);// will move selected options from one list to the other// and call an option handler on each optionfunctionmoveoptions($fromlist,$tolist,opthandler){$fromlist.find('option:selected').each(function(index,option){var$option=$(option);// add a new option to the target list$tolist.append(OPTION({'value':$option.val()},$option.text()));// process callback on the optionopthandler.call(null,$option);// remove option from the source list$option.remove();});// re-sort both listssortoptions($fromlist);sortoptions($tolist);};functionaddvalues(){moveoptions(state.$fromNode,state.$toNode,function($option){// add an hidden input for the edit controllervarhiddenInput=INPUT({type:'hidden',name:state.name,value:$option.val()});state.$toNode.parent().append(hiddenInput);});};functionremovevalues(){moveoptions(state.$toNode,state.$fromNode,function($option){// remove hidden inputs for the edit controllervarselector='input[name='+cw.escape(state.name)+']'state.$toNode.parent().find(selector).each(function(index,input){if($(input).val()==$option.val()){$(input).remove();}});});};var$this=$(this);$this.find('.cwinoutadd').bind(// 'add >>>' symbol'click',{'state':state},addvalues);$this.find('.cwinoutremove').bind(// 'remove <<<' symbol'click',{'state':state},removevalues);state.$fromNode.bind('dblclick',{'state':state},addvalues);state.$toNode.bind('dblclick',{'state':state},removevalues);}};$.fn.cwinoutwidget=function(fromSelect,toSelect){returnmethods.__init__.apply(this,[fromSelect,toSelect]);};})(jQuery);