bzr revid: nicolas.vanhoren@openerp.com-20110506133818-hnvn0nnfo9sqqpbu
This commit is contained in:
niv-openerp
2011-05-06 15:38:18 +02:00
38 changed files with 2961 additions and 334 deletions
+45 -14
View File
@@ -346,12 +346,17 @@ class DataSet(openerpweb.Controller):
:rtype: list
"""
Model = request.session.model(model)
ids = Model.search(domain or [], offset or 0, limit or False,
sort or False, request.context)
if fields and fields == ['id']:
# shortcut read if we only want the ids
return map(lambda id: {'id': id}, ids)
return Model.read(ids, fields or False, request.context)
reads = Model.read(ids, fields or False, request.context)
reads.sort(key=lambda obj: ids.index(obj['id']))
return reads
@openerpweb.jsonrequest
def get(self, request, model, ids, fields=False):
@@ -409,6 +414,11 @@ class DataSet(openerpweb.Controller):
r = getattr(m, method)(ids, *args)
return {'result': r}
@openerpweb.jsonrequest
def exec_workflow(self, req, model, id, signal):
r = req.session.exec_workflow(model, id, signal)
return {'result': r}
@openerpweb.jsonrequest
def default_get(self, req, model, fields, context={}):
m = req.session.model(model)
@@ -551,11 +561,11 @@ class ListView(View):
@openerpweb.jsonrequest
def fill(self, request, model, id, domain,
offset=0, limit=False):
return self.do_fill(request, model, id, domain, offset, limit)
offset=0, limit=False, sort=None):
return self.do_fill(request, model, id, domain, offset, limit, sort)
def do_fill(self, request, model, id, domain,
offset=0, limit=False):
offset=0, limit=False, sort=None):
""" Returns all information needed to fill a table:
* view with processed ``editable`` flag
@@ -573,19 +583,33 @@ class ListView(View):
:param int limit: search limit, for pagination
:returns: hell if I have any idea yet
"""
view = self.fields_view_get(request, model, id)
view = self.fields_view_get(request, model, id, toolbar=True)
print sort
rows = DataSet().do_search_read(request, model,
offset=offset, limit=limit,
domain=domain)
domain=domain, sort=sort)
eval_context = request.session.evaluation_context(
request.context)
return [
{'data': dict((key, {'value': value})
for key, value in row.iteritems()),
'color': self.process_colors(view, row, eval_context)}
for row in rows
]
if sort:
sort_criteria = sort.split(',')[0].split(' ')
print sort, sort_criteria
view['sorted'] = {
'field': sort_criteria[0],
'reversed': sort_criteria[1] == 'DESC'
}
else:
view['sorted'] = {}
return {
'view': view,
'records': [
{'data': dict((key, {'value': value})
for key, value in row.iteritems()),
'color': self.process_colors(view, row, eval_context)}
for row in rows
]
}
def process_colors(self, view, row, context):
colors = view['arch']['attrs'].get('colors')
@@ -618,5 +642,12 @@ class Action(openerpweb.Controller):
_cp_path = "/base/action"
@openerpweb.jsonrequest
def load(self, req, action_id):
return {}
def load(self, req, action_id, context={}):
Actions = req.session.model('ir.actions.actions')
value = False
action_type = Actions.read([action_id], ['type'], context)
if action_type:
action = req.session.model(action_type[0]['type']).read([action_id], False, context)
if action:
value = action[0]
return {'result': value}
+145
View File
@@ -0,0 +1,145 @@
/**
* @version: 1.0 Alpha-1
* @author: Coolite Inc. http://www.coolite.com/
* @date: 2008-05-13
* @copyright: Copyright (c) 2006-2008, Coolite Inc. (http://www.coolite.com/). All rights reserved.
* @license: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/.
* @website: http://www.datejs.com/
*/
Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|aft(er)?|from|hence)/i,subtract:/^(\-|bef(ore)?|ago)/i,yesterday:/^yes(terday)?/i,today:/^t(od(ay)?)?/i,tomorrow:/^tom(orrow)?/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^mn|min(ute)?s?/i,hour:/^h(our)?s?/i,week:/^w(eek)?s?/i,month:/^m(onth)?s?/i,day:/^d(ay)?s?/i,year:/^y(ear)?s?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt|utc)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a(?!u|p)|p)/i},timezones:[{name:"UTC",offset:"-000"},{name:"GMT",offset:"-000"},{name:"EST",offset:"-0500"},{name:"EDT",offset:"-0400"},{name:"CST",offset:"-0600"},{name:"CDT",offset:"-0500"},{name:"MST",offset:"-0700"},{name:"MDT",offset:"-0600"},{name:"PST",offset:"-0800"},{name:"PDT",offset:"-0700"}]};
(function(){var $D=Date,$P=$D.prototype,$C=$D.CultureInfo,p=function(s,l){if(!l){l=2;}
return("000"+s).slice(l*-1);};$P.clearTime=function(){this.setHours(0);this.setMinutes(0);this.setSeconds(0);this.setMilliseconds(0);return this;};$P.setTimeToNow=function(){var n=new Date();this.setHours(n.getHours());this.setMinutes(n.getMinutes());this.setSeconds(n.getSeconds());this.setMilliseconds(n.getMilliseconds());return this;};$D.today=function(){return new Date().clearTime();};$D.compare=function(date1,date2){if(isNaN(date1)||isNaN(date2)){throw new Error(date1+" - "+date2);}else if(date1 instanceof Date&&date2 instanceof Date){return(date1<date2)?-1:(date1>date2)?1:0;}else{throw new TypeError(date1+" - "+date2);}};$D.equals=function(date1,date2){return(date1.compareTo(date2)===0);};$D.getDayNumberFromName=function(name){var n=$C.dayNames,m=$C.abbreviatedDayNames,o=$C.shortestDayNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s||o[i].toLowerCase()==s){return i;}}
return-1;};$D.getMonthNumberFromName=function(name){var n=$C.monthNames,m=$C.abbreviatedMonthNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s){return i;}}
return-1;};$D.isLeapYear=function(year){return((year%4===0&&year%100!==0)||year%400===0);};$D.getDaysInMonth=function(year,month){return[31,($D.isLeapYear(year)?29:28),31,30,31,30,31,31,30,31,30,31][month];};$D.getTimezoneAbbreviation=function(offset){var z=$C.timezones,p;for(var i=0;i<z.length;i++){if(z[i].offset===offset){return z[i].name;}}
return null;};$D.getTimezoneOffset=function(name){var z=$C.timezones,p;for(var i=0;i<z.length;i++){if(z[i].name===name.toUpperCase()){return z[i].offset;}}
return null;};$P.clone=function(){return new Date(this.getTime());};$P.compareTo=function(date){return Date.compare(this,date);};$P.equals=function(date){return Date.equals(this,date||new Date());};$P.between=function(start,end){return this.getTime()>=start.getTime()&&this.getTime()<=end.getTime();};$P.isAfter=function(date){return this.compareTo(date||new Date())===1;};$P.isBefore=function(date){return(this.compareTo(date||new Date())===-1);};$P.isToday=function(){return this.isSameDay(new Date());};$P.isSameDay=function(date){return this.clone().clearTime().equals(date.clone().clearTime());};$P.addMilliseconds=function(value){this.setMilliseconds(this.getMilliseconds()+value);return this;};$P.addSeconds=function(value){return this.addMilliseconds(value*1000);};$P.addMinutes=function(value){return this.addMilliseconds(value*60000);};$P.addHours=function(value){return this.addMilliseconds(value*3600000);};$P.addDays=function(value){this.setDate(this.getDate()+value);return this;};$P.addWeeks=function(value){return this.addDays(value*7);};$P.addMonths=function(value){var n=this.getDate();this.setDate(1);this.setMonth(this.getMonth()+value);this.setDate(Math.min(n,$D.getDaysInMonth(this.getFullYear(),this.getMonth())));return this;};$P.addYears=function(value){return this.addMonths(value*12);};$P.add=function(config){if(typeof config=="number"){this._orient=config;return this;}
var x=config;if(x.milliseconds){this.addMilliseconds(x.milliseconds);}
if(x.seconds){this.addSeconds(x.seconds);}
if(x.minutes){this.addMinutes(x.minutes);}
if(x.hours){this.addHours(x.hours);}
if(x.weeks){this.addWeeks(x.weeks);}
if(x.months){this.addMonths(x.months);}
if(x.years){this.addYears(x.years);}
if(x.days){this.addDays(x.days);}
return this;};var $y,$m,$d;$P.getWeek=function(){var a,b,c,d,e,f,g,n,s,w;$y=(!$y)?this.getFullYear():$y;$m=(!$m)?this.getMonth()+1:$m;$d=(!$d)?this.getDate():$d;if($m<=2){a=$y-1;b=(a/4|0)-(a/100|0)+(a/400|0);c=((a-1)/4|0)-((a-1)/100|0)+((a-1)/400|0);s=b-c;e=0;f=$d-1+(31*($m-1));}else{a=$y;b=(a/4|0)-(a/100|0)+(a/400|0);c=((a-1)/4|0)-((a-1)/100|0)+((a-1)/400|0);s=b-c;e=s+1;f=$d+((153*($m-3)+2)/5)+58+s;}
g=(a+b)%7;d=(f+g-e)%7;n=(f+3-d)|0;if(n<0){w=53-((g-s)/5|0);}else if(n>364+s){w=1;}else{w=(n/7|0)+1;}
$y=$m=$d=null;return w;};$P.getISOWeek=function(){$y=this.getUTCFullYear();$m=this.getUTCMonth()+1;$d=this.getUTCDate();return p(this.getWeek());};$P.setWeek=function(n){return this.moveToDayOfWeek(1).addWeeks(n-this.getWeek());};$D._validate=function(n,min,max,name){if(typeof n=="undefined"){return false;}else if(typeof n!="number"){throw new TypeError(n+" is not a Number.");}else if(n<min||n>max){throw new RangeError(n+" is not a valid value for "+name+".");}
return true;};$D.validateMillisecond=function(value){return $D._validate(value,0,999,"millisecond");};$D.validateSecond=function(value){return $D._validate(value,0,59,"second");};$D.validateMinute=function(value){return $D._validate(value,0,59,"minute");};$D.validateHour=function(value){return $D._validate(value,0,23,"hour");};$D.validateDay=function(value,year,month){return $D._validate(value,1,$D.getDaysInMonth(year,month),"day");};$D.validateMonth=function(value){return $D._validate(value,0,11,"month");};$D.validateYear=function(value){return $D._validate(value,0,9999,"year");};$P.set=function(config){if($D.validateMillisecond(config.millisecond)){this.addMilliseconds(config.millisecond-this.getMilliseconds());}
if($D.validateSecond(config.second)){this.addSeconds(config.second-this.getSeconds());}
if($D.validateMinute(config.minute)){this.addMinutes(config.minute-this.getMinutes());}
if($D.validateHour(config.hour)){this.addHours(config.hour-this.getHours());}
if($D.validateMonth(config.month)){this.addMonths(config.month-this.getMonth());}
if($D.validateYear(config.year)){this.addYears(config.year-this.getFullYear());}
if($D.validateDay(config.day,this.getFullYear(),this.getMonth())){this.addDays(config.day-this.getDate());}
if(config.timezone){this.setTimezone(config.timezone);}
if(config.timezoneOffset){this.setTimezoneOffset(config.timezoneOffset);}
if(config.week&&$D._validate(config.week,0,53,"week")){this.setWeek(config.week);}
return this;};$P.moveToFirstDayOfMonth=function(){return this.set({day:1});};$P.moveToLastDayOfMonth=function(){return this.set({day:$D.getDaysInMonth(this.getFullYear(),this.getMonth())});};$P.moveToNthOccurrence=function(dayOfWeek,occurrence){var shift=0;if(occurrence>0){shift=occurrence-1;}
else if(occurrence===-1){this.moveToLastDayOfMonth();if(this.getDay()!==dayOfWeek){this.moveToDayOfWeek(dayOfWeek,-1);}
return this;}
return this.moveToFirstDayOfMonth().addDays(-1).moveToDayOfWeek(dayOfWeek,+1).addWeeks(shift);};$P.moveToDayOfWeek=function(dayOfWeek,orient){var diff=(dayOfWeek-this.getDay()+7*(orient||+1))%7;return this.addDays((diff===0)?diff+=7*(orient||+1):diff);};$P.moveToMonth=function(month,orient){var diff=(month-this.getMonth()+12*(orient||+1))%12;return this.addMonths((diff===0)?diff+=12*(orient||+1):diff);};$P.getOrdinalNumber=function(){return Math.ceil((this.clone().clearTime()-new Date(this.getFullYear(),0,1))/86400000)+1;};$P.getTimezone=function(){return $D.getTimezoneAbbreviation(this.getUTCOffset());};$P.setTimezoneOffset=function(offset){var here=this.getTimezoneOffset(),there=Number(offset)*-6/10;return this.addMinutes(there-here);};$P.setTimezone=function(offset){return this.setTimezoneOffset($D.getTimezoneOffset(offset));};$P.hasDaylightSavingTime=function(){return(Date.today().set({month:0,day:1}).getTimezoneOffset()!==Date.today().set({month:6,day:1}).getTimezoneOffset());};$P.isDaylightSavingTime=function(){return(this.hasDaylightSavingTime()&&new Date().getTimezoneOffset()===Date.today().set({month:6,day:1}).getTimezoneOffset());};$P.getUTCOffset=function(){var n=this.getTimezoneOffset()*-10/6,r;if(n<0){r=(n-10000).toString();return r.charAt(0)+r.substr(2);}else{r=(n+10000).toString();return"+"+r.substr(1);}};$P.getElapsed=function(date){return(date||new Date())-this;};if(!$P.toISOString){$P.toISOString=function(){function f(n){return n<10?'0'+n:n;}
return'"'+this.getUTCFullYear()+'-'+
f(this.getUTCMonth()+1)+'-'+
f(this.getUTCDate())+'T'+
f(this.getUTCHours())+':'+
f(this.getUTCMinutes())+':'+
f(this.getUTCSeconds())+'Z"';};}
$P._toString=$P.toString;$P.toString=function(format){var x=this;if(format&&format.length==1){var c=$C.formatPatterns;x.t=x.toString;switch(format){case"d":return x.t(c.shortDate);case"D":return x.t(c.longDate);case"F":return x.t(c.fullDateTime);case"m":return x.t(c.monthDay);case"r":return x.t(c.rfc1123);case"s":return x.t(c.sortableDateTime);case"t":return x.t(c.shortTime);case"T":return x.t(c.longTime);case"u":return x.t(c.universalSortableDateTime);case"y":return x.t(c.yearMonth);}}
var ord=function(n){switch(n*1){case 1:case 21:case 31:return"st";case 2:case 22:return"nd";case 3:case 23:return"rd";default:return"th";}};return format?format.replace(/(\\)?(dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|S)/g,function(m){if(m.charAt(0)==="\\"){return m.replace("\\","");}
x.h=x.getHours;switch(m){case"hh":return p(x.h()<13?(x.h()===0?12:x.h()):(x.h()-12));case"h":return x.h()<13?(x.h()===0?12:x.h()):(x.h()-12);case"HH":return p(x.h());case"H":return x.h();case"mm":return p(x.getMinutes());case"m":return x.getMinutes();case"ss":return p(x.getSeconds());case"s":return x.getSeconds();case"yyyy":return p(x.getFullYear(),4);case"yy":return p(x.getFullYear());case"dddd":return $C.dayNames[x.getDay()];case"ddd":return $C.abbreviatedDayNames[x.getDay()];case"dd":return p(x.getDate());case"d":return x.getDate();case"MMMM":return $C.monthNames[x.getMonth()];case"MMM":return $C.abbreviatedMonthNames[x.getMonth()];case"MM":return p((x.getMonth()+1));case"M":return x.getMonth()+1;case"t":return x.h()<12?$C.amDesignator.substring(0,1):$C.pmDesignator.substring(0,1);case"tt":return x.h()<12?$C.amDesignator:$C.pmDesignator;case"S":return ord(x.getDate());default:return m;}}):this._toString();};}());
(function(){var $D=Date,$P=$D.prototype,$C=$D.CultureInfo,$N=Number.prototype;$P._orient=+1;$P._nth=null;$P._is=false;$P._same=false;$P._isSecond=false;$N._dateElement="day";$P.next=function(){this._orient=+1;return this;};$D.next=function(){return $D.today().next();};$P.last=$P.prev=$P.previous=function(){this._orient=-1;return this;};$D.last=$D.prev=$D.previous=function(){return $D.today().last();};$P.is=function(){this._is=true;return this;};$P.same=function(){this._same=true;this._isSecond=false;return this;};$P.today=function(){return this.same().day();};$P.weekday=function(){if(this._is){this._is=false;return(!this.is().sat()&&!this.is().sun());}
return false;};$P.at=function(time){return(typeof time==="string")?$D.parse(this.toString("d")+" "+time):this.set(time);};$N.fromNow=$N.after=function(date){var c={};c[this._dateElement]=this;return((!date)?new Date():date.clone()).add(c);};$N.ago=$N.before=function(date){var c={};c[this._dateElement]=this*-1;return((!date)?new Date():date.clone()).add(c);};var dx=("sunday monday tuesday wednesday thursday friday saturday").split(/\s/),mx=("january february march april may june july august september october november december").split(/\s/),px=("Millisecond Second Minute Hour Day Week Month Year").split(/\s/),pxf=("Milliseconds Seconds Minutes Hours Date Week Month FullYear").split(/\s/),nth=("final first second third fourth fifth").split(/\s/),de;$P.toObject=function(){var o={};for(var i=0;i<px.length;i++){o[px[i].toLowerCase()]=this["get"+pxf[i]]();}
return o;};$D.fromObject=function(config){config.week=null;return Date.today().set(config);};var df=function(n){return function(){if(this._is){this._is=false;return this.getDay()==n;}
if(this._nth!==null){if(this._isSecond){this.addSeconds(this._orient*-1);}
this._isSecond=false;var ntemp=this._nth;this._nth=null;var temp=this.clone().moveToLastDayOfMonth();this.moveToNthOccurrence(n,ntemp);if(this>temp){throw new RangeError($D.getDayName(n)+" does not occur "+ntemp+" times in the month of "+$D.getMonthName(temp.getMonth())+" "+temp.getFullYear()+".");}
return this;}
return this.moveToDayOfWeek(n,this._orient);};};var sdf=function(n){return function(){var t=$D.today(),shift=n-t.getDay();if(n===0&&$C.firstDayOfWeek===1&&t.getDay()!==0){shift=shift+7;}
return t.addDays(shift);};};for(var i=0;i<dx.length;i++){$D[dx[i].toUpperCase()]=$D[dx[i].toUpperCase().substring(0,3)]=i;$D[dx[i]]=$D[dx[i].substring(0,3)]=sdf(i);$P[dx[i]]=$P[dx[i].substring(0,3)]=df(i);}
var mf=function(n){return function(){if(this._is){this._is=false;return this.getMonth()===n;}
return this.moveToMonth(n,this._orient);};};var smf=function(n){return function(){return $D.today().set({month:n,day:1});};};for(var j=0;j<mx.length;j++){$D[mx[j].toUpperCase()]=$D[mx[j].toUpperCase().substring(0,3)]=j;$D[mx[j]]=$D[mx[j].substring(0,3)]=smf(j);$P[mx[j]]=$P[mx[j].substring(0,3)]=mf(j);}
var ef=function(j){return function(){if(this._isSecond){this._isSecond=false;return this;}
if(this._same){this._same=this._is=false;var o1=this.toObject(),o2=(arguments[0]||new Date()).toObject(),v="",k=j.toLowerCase();for(var m=(px.length-1);m>-1;m--){v=px[m].toLowerCase();if(o1[v]!=o2[v]){return false;}
if(k==v){break;}}
return true;}
if(j.substring(j.length-1)!="s"){j+="s";}
return this["add"+j](this._orient);};};var nf=function(n){return function(){this._dateElement=n;return this;};};for(var k=0;k<px.length;k++){de=px[k].toLowerCase();$P[de]=$P[de+"s"]=ef(px[k]);$N[de]=$N[de+"s"]=nf(de);}
$P._ss=ef("Second");var nthfn=function(n){return function(dayOfWeek){if(this._same){return this._ss(arguments[0]);}
if(dayOfWeek||dayOfWeek===0){return this.moveToNthOccurrence(dayOfWeek,n);}
this._nth=n;if(n===2&&(dayOfWeek===undefined||dayOfWeek===null)){this._isSecond=true;return this.addSeconds(this._orient);}
return this;};};for(var l=0;l<nth.length;l++){$P[nth[l]]=(l===0)?nthfn(-1):nthfn(l);}}());
(function(){Date.Parsing={Exception:function(s){this.message="Parse error at '"+s.substring(0,10)+" ...'";}};var $P=Date.Parsing;var _=$P.Operators={rtoken:function(r){return function(s){var mx=s.match(r);if(mx){return([mx[0],s.substring(mx[0].length)]);}else{throw new $P.Exception(s);}};},token:function(s){return function(s){return _.rtoken(new RegExp("^\s*"+s+"\s*"))(s);};},stoken:function(s){return _.rtoken(new RegExp("^"+s));},until:function(p){return function(s){var qx=[],rx=null;while(s.length){try{rx=p.call(this,s);}catch(e){qx.push(rx[0]);s=rx[1];continue;}
break;}
return[qx,s];};},many:function(p){return function(s){var rx=[],r=null;while(s.length){try{r=p.call(this,s);}catch(e){return[rx,s];}
rx.push(r[0]);s=r[1];}
return[rx,s];};},optional:function(p){return function(s){var r=null;try{r=p.call(this,s);}catch(e){return[null,s];}
return[r[0],r[1]];};},not:function(p){return function(s){try{p.call(this,s);}catch(e){return[null,s];}
throw new $P.Exception(s);};},ignore:function(p){return p?function(s){var r=null;r=p.call(this,s);return[null,r[1]];}:null;},product:function(){var px=arguments[0],qx=Array.prototype.slice.call(arguments,1),rx=[];for(var i=0;i<px.length;i++){rx.push(_.each(px[i],qx));}
return rx;},cache:function(rule){var cache={},r=null;return function(s){try{r=cache[s]=(cache[s]||rule.call(this,s));}catch(e){r=cache[s]=e;}
if(r instanceof $P.Exception){throw r;}else{return r;}};},any:function(){var px=arguments;return function(s){var r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
try{r=(px[i].call(this,s));}catch(e){r=null;}
if(r){return r;}}
throw new $P.Exception(s);};},each:function(){var px=arguments;return function(s){var rx=[],r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
try{r=(px[i].call(this,s));}catch(e){throw new $P.Exception(s);}
rx.push(r[0]);s=r[1];}
return[rx,s];};},all:function(){var px=arguments,_=_;return _.each(_.optional(px));},sequence:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;if(px.length==1){return px[0];}
return function(s){var r=null,q=null;var rx=[];for(var i=0;i<px.length;i++){try{r=px[i].call(this,s);}catch(e){break;}
rx.push(r[0]);try{q=d.call(this,r[1]);}catch(ex){q=null;break;}
s=q[1];}
if(!r){throw new $P.Exception(s);}
if(q){throw new $P.Exception(q[1]);}
if(c){try{r=c.call(this,r[1]);}catch(ey){throw new $P.Exception(r[1]);}}
return[rx,(r?r[1]:s)];};},between:function(d1,p,d2){d2=d2||d1;var _fn=_.each(_.ignore(d1),p,_.ignore(d2));return function(s){var rx=_fn.call(this,s);return[[rx[0][0],r[0][2]],rx[1]];};},list:function(p,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return(p instanceof Array?_.each(_.product(p.slice(0,-1),_.ignore(d)),p.slice(-1),_.ignore(c)):_.each(_.many(_.each(p,_.ignore(d))),px,_.ignore(c)));},set:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return function(s){var r=null,p=null,q=null,rx=null,best=[[],s],last=false;for(var i=0;i<px.length;i++){q=null;p=null;r=null;last=(px.length==1);try{r=px[i].call(this,s);}catch(e){continue;}
rx=[[r[0]],r[1]];if(r[1].length>0&&!last){try{q=d.call(this,r[1]);}catch(ex){last=true;}}else{last=true;}
if(!last&&q[1].length===0){last=true;}
if(!last){var qx=[];for(var j=0;j<px.length;j++){if(i!=j){qx.push(px[j]);}}
p=_.set(qx,d).call(this,q[1]);if(p[0].length>0){rx[0]=rx[0].concat(p[0]);rx[1]=p[1];}}
if(rx[1].length<best[1].length){best=rx;}
if(best[1].length===0){break;}}
if(best[0].length===0){return best;}
if(c){try{q=c.call(this,best[1]);}catch(ey){throw new $P.Exception(best[1]);}
best[1]=q[1];}
return best;};},forward:function(gr,fname){return function(s){return gr[fname].call(this,s);};},replace:function(rule,repl){return function(s){var r=rule.call(this,s);return[repl,r[1]];};},process:function(rule,fn){return function(s){var r=rule.call(this,s);return[fn.call(this,r[0]),r[1]];};},min:function(min,rule){return function(s){var rx=rule.call(this,s);if(rx[0].length<min){throw new $P.Exception(s);}
return rx;};}};var _generator=function(op){return function(){var args=null,rx=[];if(arguments.length>1){args=Array.prototype.slice.call(arguments);}else if(arguments[0]instanceof Array){args=arguments[0];}
if(args){for(var i=0,px=args.shift();i<px.length;i++){args.unshift(px[i]);rx.push(op.apply(null,args));args.shift();return rx;}}else{return op.apply(null,arguments);}};};var gx="optional not ignore cache".split(/\s/);for(var i=0;i<gx.length;i++){_[gx[i]]=_generator(_[gx[i]]);}
var _vector=function(op){return function(){if(arguments[0]instanceof Array){return op.apply(null,arguments[0]);}else{return op.apply(null,arguments);}};};var vx="each any all".split(/\s/);for(var j=0;j<vx.length;j++){_[vx[j]]=_vector(_[vx[j]]);}}());(function(){var $D=Date,$P=$D.prototype,$C=$D.CultureInfo;var flattenAndCompact=function(ax){var rx=[];for(var i=0;i<ax.length;i++){if(ax[i]instanceof Array){rx=rx.concat(flattenAndCompact(ax[i]));}else{if(ax[i]){rx.push(ax[i]);}}}
return rx;};$D.Grammar={};$D.Translator={hour:function(s){return function(){this.hour=Number(s);};},minute:function(s){return function(){this.minute=Number(s);};},second:function(s){return function(){this.second=Number(s);};},meridian:function(s){return function(){this.meridian=s.slice(0,1).toLowerCase();};},timezone:function(s){return function(){var n=s.replace(/[^\d\+\-]/g,"");if(n.length){this.timezoneOffset=Number(n);}else{this.timezone=s.toLowerCase();}};},day:function(x){var s=x[0];return function(){this.day=Number(s.match(/\d+/)[0]);};},month:function(s){return function(){this.month=(s.length==3)?"jan feb mar apr may jun jul aug sep oct nov dec".indexOf(s)/4:Number(s)-1;};},year:function(s){return function(){var n=Number(s);this.year=((s.length>2)?n:(n+(((n+2000)<$C.twoDigitYearMax)?2000:1900)));};},rday:function(s){return function(){switch(s){case"yesterday":this.days=-1;break;case"tomorrow":this.days=1;break;case"today":this.days=0;break;case"now":this.days=0;this.now=true;break;}};},finishExact:function(x){x=(x instanceof Array)?x:[x];for(var i=0;i<x.length;i++){if(x[i]){x[i].call(this);}}
var now=new Date();if((this.hour||this.minute)&&(!this.month&&!this.year&&!this.day)){this.day=now.getDate();}
if(!this.year){this.year=now.getFullYear();}
if(!this.month&&this.month!==0){this.month=now.getMonth();}
if(!this.day){this.day=1;}
if(!this.hour){this.hour=0;}
if(!this.minute){this.minute=0;}
if(!this.second){this.second=0;}
if(this.meridian&&this.hour){if(this.meridian=="p"&&this.hour<12){this.hour=this.hour+12;}else if(this.meridian=="a"&&this.hour==12){this.hour=0;}}
if(this.day>$D.getDaysInMonth(this.year,this.month)){throw new RangeError(this.day+" is not a valid value for days.");}
var r=new Date(this.year,this.month,this.day,this.hour,this.minute,this.second);if(this.timezone){r.set({timezone:this.timezone});}else if(this.timezoneOffset){r.set({timezoneOffset:this.timezoneOffset});}
return r;},finish:function(x){x=(x instanceof Array)?flattenAndCompact(x):[x];if(x.length===0){return null;}
for(var i=0;i<x.length;i++){if(typeof x[i]=="function"){x[i].call(this);}}
var today=$D.today();if(this.now&&!this.unit&&!this.operator){return new Date();}else if(this.now){today=new Date();}
var expression=!!(this.days&&this.days!==null||this.orient||this.operator);var gap,mod,orient;orient=((this.orient=="past"||this.operator=="subtract")?-1:1);if(!this.now&&"hour minute second".indexOf(this.unit)!=-1){today.setTimeToNow();}
if(this.month||this.month===0){if("year day hour minute second".indexOf(this.unit)!=-1){this.value=this.month+1;this.month=null;expression=true;}}
if(!expression&&this.weekday&&!this.day&&!this.days){var temp=Date[this.weekday]();this.day=temp.getDate();if(!this.month){this.month=temp.getMonth();}
this.year=temp.getFullYear();}
if(expression&&this.weekday&&this.unit!="month"){this.unit="day";gap=($D.getDayNumberFromName(this.weekday)-today.getDay());mod=7;this.days=gap?((gap+(orient*mod))%mod):(orient*mod);}
if(this.month&&this.unit=="day"&&this.operator){this.value=(this.month+1);this.month=null;}
if(this.value!=null&&this.month!=null&&this.year!=null){this.day=this.value*1;}
if(this.month&&!this.day&&this.value){today.set({day:this.value*1});if(!expression){this.day=this.value*1;}}
if(!this.month&&this.value&&this.unit=="month"&&!this.now){this.month=this.value;expression=true;}
if(expression&&(this.month||this.month===0)&&this.unit!="year"){this.unit="month";gap=(this.month-today.getMonth());mod=12;this.months=gap?((gap+(orient*mod))%mod):(orient*mod);this.month=null;}
if(!this.unit){this.unit="day";}
if(!this.value&&this.operator&&this.operator!==null&&this[this.unit+"s"]&&this[this.unit+"s"]!==null){this[this.unit+"s"]=this[this.unit+"s"]+((this.operator=="add")?1:-1)+(this.value||0)*orient;}else if(this[this.unit+"s"]==null||this.operator!=null){if(!this.value){this.value=1;}
this[this.unit+"s"]=this.value*orient;}
if(this.meridian&&this.hour){if(this.meridian=="p"&&this.hour<12){this.hour=this.hour+12;}else if(this.meridian=="a"&&this.hour==12){this.hour=0;}}
if(this.weekday&&!this.day&&!this.days){var temp=Date[this.weekday]();this.day=temp.getDate();if(temp.getMonth()!==today.getMonth()){this.month=temp.getMonth();}}
if((this.month||this.month===0)&&!this.day){this.day=1;}
if(!this.orient&&!this.operator&&this.unit=="week"&&this.value&&!this.day&&!this.month){return Date.today().setWeek(this.value);}
if(expression&&this.timezone&&this.day&&this.days){this.day=this.days;}
return(expression)?today.add(this):today.set(this);}};var _=$D.Parsing.Operators,g=$D.Grammar,t=$D.Translator,_fn;g.datePartDelimiter=_.rtoken(/^([\s\-\.\,\/\x27]+)/);g.timePartDelimiter=_.stoken(":");g.whiteSpace=_.rtoken(/^\s*/);g.generalDelimiter=_.rtoken(/^(([\s\,]|at|@|on)+)/);var _C={};g.ctoken=function(keys){var fn=_C[keys];if(!fn){var c=$C.regexPatterns;var kx=keys.split(/\s+/),px=[];for(var i=0;i<kx.length;i++){px.push(_.replace(_.rtoken(c[kx[i]]),kx[i]));}
fn=_C[keys]=_.any.apply(null,px);}
return fn;};g.ctoken2=function(key){return _.rtoken($C.regexPatterns[key]);};g.h=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2]|[1-9])/),t.hour));g.hh=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2])/),t.hour));g.H=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3]|[0-9])/),t.hour));g.HH=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3])/),t.hour));g.m=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.minute));g.mm=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.minute));g.s=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.second));g.ss=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.second));g.hms=_.cache(_.sequence([g.H,g.m,g.s],g.timePartDelimiter));g.t=_.cache(_.process(g.ctoken2("shortMeridian"),t.meridian));g.tt=_.cache(_.process(g.ctoken2("longMeridian"),t.meridian));g.z=_.cache(_.process(_.rtoken(/^((\+|\-)\s*\d\d\d\d)|((\+|\-)\d\d\:?\d\d)/),t.timezone));g.zz=_.cache(_.process(_.rtoken(/^((\+|\-)\s*\d\d\d\d)|((\+|\-)\d\d\:?\d\d)/),t.timezone));g.zzz=_.cache(_.process(g.ctoken2("timezone"),t.timezone));g.timeSuffix=_.each(_.ignore(g.whiteSpace),_.set([g.tt,g.zzz]));g.time=_.each(_.optional(_.ignore(_.stoken("T"))),g.hms,g.timeSuffix);g.d=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1]|\d)/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.dd=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1])/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.ddd=g.dddd=_.cache(_.process(g.ctoken("sun mon tue wed thu fri sat"),function(s){return function(){this.weekday=s;};}));g.M=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d|\d)/),t.month));g.MM=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d)/),t.month));g.MMM=g.MMMM=_.cache(_.process(g.ctoken("jan feb mar apr may jun jul aug sep oct nov dec"),t.month));g.y=_.cache(_.process(_.rtoken(/^(\d\d?)/),t.year));g.yy=_.cache(_.process(_.rtoken(/^(\d\d)/),t.year));g.yyy=_.cache(_.process(_.rtoken(/^(\d\d?\d?\d?)/),t.year));g.yyyy=_.cache(_.process(_.rtoken(/^(\d\d\d\d)/),t.year));_fn=function(){return _.each(_.any.apply(null,arguments),_.not(g.ctoken2("timeContext")));};g.day=_fn(g.d,g.dd);g.month=_fn(g.M,g.MMM);g.year=_fn(g.yyyy,g.yy);g.orientation=_.process(g.ctoken("past future"),function(s){return function(){this.orient=s;};});g.operator=_.process(g.ctoken("add subtract"),function(s){return function(){this.operator=s;};});g.rday=_.process(g.ctoken("yesterday tomorrow today now"),t.rday);g.unit=_.process(g.ctoken("second minute hour day week month year"),function(s){return function(){this.unit=s;};});g.value=_.process(_.rtoken(/^\d\d?(st|nd|rd|th)?/),function(s){return function(){this.value=s.replace(/\D/g,"");};});g.expression=_.set([g.rday,g.operator,g.value,g.unit,g.orientation,g.ddd,g.MMM]);_fn=function(){return _.set(arguments,g.datePartDelimiter);};g.mdy=_fn(g.ddd,g.month,g.day,g.year);g.ymd=_fn(g.ddd,g.year,g.month,g.day);g.dmy=_fn(g.ddd,g.day,g.month,g.year);g.date=function(s){return((g[$C.dateElementOrder]||g.mdy).call(this,s));};g.format=_.process(_.many(_.any(_.process(_.rtoken(/^(dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?)/),function(fmt){if(g[fmt]){return g[fmt];}else{throw $D.Parsing.Exception(fmt);}}),_.process(_.rtoken(/^[^dMyhHmstz]+/),function(s){return _.ignore(_.stoken(s));}))),function(rules){return _.process(_.each.apply(null,rules),t.finishExact);});var _F={};var _get=function(f){return _F[f]=(_F[f]||g.format(f)[0]);};g.formats=function(fx){if(fx instanceof Array){var rx=[];for(var i=0;i<fx.length;i++){rx.push(_get(fx[i]));}
return _.any.apply(null,rx);}else{return _get(fx);}};g._formats=g.formats(["\"yyyy-MM-ddTHH:mm:ssZ\"","yyyy-MM-ddTHH:mm:ssZ","yyyy-MM-ddTHH:mm:ssz","yyyy-MM-ddTHH:mm:ss","yyyy-MM-ddTHH:mmZ","yyyy-MM-ddTHH:mmz","yyyy-MM-ddTHH:mm","ddd, MMM dd, yyyy H:mm:ss tt","ddd MMM d yyyy HH:mm:ss zzz","MMddyyyy","ddMMyyyy","Mddyyyy","ddMyyyy","Mdyyyy","dMyyyy","yyyy","Mdyy","dMyy","d"]);g._start=_.process(_.set([g.date,g.time,g.expression],g.generalDelimiter,g.whiteSpace),t.finish);g.start=function(s){try{var r=g._formats.call({},s);if(r[1].length===0){return r;}}catch(e){}
return g._start.call({},s);};$D._parse=$D.parse;$D.parse=function(s){var r=null;if(!s){return null;}
if(s instanceof Date){return s;}
try{r=$D.Grammar.start.call({},s.replace(/^\s*(\S*(\s+\S+)*)\s*$/,"$1"));}catch(e){return null;}
return((r[1].length===0)?r[0]:null);};$D.getParseFunction=function(fx){var fn=$D.Grammar.formats(fx);return function(s){var r=null;try{r=fn.call({},s);}catch(e){return null;}
return((r[1].length===0)?r[0]:null);};};$D.parseExact=function(s,fx){return $D.getParseFunction(fx)(s);};}());
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html style="height: 100%">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<script type="text/javascript" src="qweb.js"></script>
<script type="text/javascript" src="qweb2.js"></script>
<script type="text/javascript">
var dict = {
session : true,
testing : 'yes',
name : 'AGR'
}
console.time("Load template with QWeb");
QWeb.add_template("qweb-benchmark.xml");
console.timeEnd("Load template with QWeb");
console.time("Load template with QWeb2");
var engine = new QWeb2.Engine("qweb-benchmark.xml")
engine.debug = true;
console.timeEnd("Load template with QWeb2")
var iter = 1000;
console.log("Rendering...");
console.time("Render " + iter + " templates with QWeb");
for (var i = 0; i < iter; i++) {
var qweb = QWeb.render('benchmark', dict);
}
console.timeEnd("Render " + iter + " templates with QWeb");
console.time("Render " + iter + " templates with QWeb2");
for (var i = 0; i < iter; i++) {
var qweb2 = engine.render('benchmark', dict);
}
console.timeEnd("Render " + iter + " templates with QWeb2");
</script>
</head>
<body>
Please, check your console for results
</body>
</html>
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- vim:fdl=1:
-->
<templates id="template">
<t t-name="benchmark"><div id="oe_notification" class="oe_notification">
<div id="oe_notification_default">
<a class="ui-notify-cross ui-notify-close" href="#">x</a>
<h1>title</h1>
<p>text</p>
</div>
<div id="oe_notification_alert" class="ui-state-error">
<a class="ui-notify-cross ui-notify-close" href="#">x</a>
<span style="float:left; margin:2px 5px 0 0;" class="ui-icon ui-icon-alert"></span>
<h1>title</h1>
<p>text</p>
</div>
</div>
<t t-js="d">
d.iter = 'one,two,three,four,five'.split(',')
</t>
<t t-foreach="iter" t-as="i">
<t t-call="benchmark_call">
+ <t t-esc="i"/>
</t>
</t>
<t t-set="enplus">1</t>
<t t-set="novar">true</t>
<div t-attf-class="id_#{enplus}"/>
<div t-if="testing || true" t-att-class="novar || 'yes'" style="display: none">
<t t-set="novar"></t>
<t t-set="style">height: 200px; border: 1px solid red;</t>
<div t-att="{ 'style' : style, 'disabled' : 'false', 'readonly' : novar or undefined }" t-opentag="true"/>
<t t-foreach="{'my': 'first', 'my2': 'second' }" t-as="v">
* <t t-esc="v"/> : <t t-esc="v_value"/>
</t>
Ok this is good <t t-esc="name"/>!
<t t-set="myvar">Hi there !</t>
[<t t-raw="myvar"/>]
<t t-set="myvar2" t-value="'a,b,c,d,e'.split(',')"/>
<t t-foreach="myvar2" t-as="i">
(<t t-esc="i"/>)
</t>
</div>
<div id="oe_notification" class="oe_notification">
<div id="oe_notification_default">
<a class="ui-notify-cross ui-notify-close" href="#">x</a>
<h1>title</h1>
<p>text</p>
</div>
</div>
</t>
<t t-name="benchmark_call">
<div id="oe_notification_alert" class="ui-state-error">
<a class="ui-notify-cross ui-notify-close" href="#">x</a>
<span style="float:left; margin:2px 5px 0 0;" class="ui-icon ui-icon-alert"></span>
<h1>Here's your value : (<t t-esc="0"/>) !!</h1>
</div>
</t>
</templates>
@@ -1,40 +1,17 @@
<templates>
<t t-name="repetition-text-content" t-trim="both">
<t t-foreach="seq">*</t>
</t>
<t t-name="repetition-dom-content" t-trim="both">
<t t-foreach="seq"><b/></t>
</t>
<t t-name="repetition-self" t-trim="both">
<b t-foreach="seq"/>
</t>
<t t-name="repetition-text-content" t-foreach="seq">*</t>
<t t-name="repetition-dom-content" t-foreach="seq"><b/></t>
<b t-name="repetition-self" t-foreach="seq"/>
<t t-name="scope-self" t-trim="both">
<t t-foreach="seq"><t t-esc="seq"/></t>
</t>
<t t-name="scope-value" t-trim="both">
<t t-foreach="seq"><t t-esc="seq_value"/></t>
</t>
<t t-name="scope-index" t-trim="both">
<t t-foreach="seq"><t t-esc="seq_index"/></t>
</t>
<t t-name="scope-first" t-trim="both">
<t t-foreach="seq"><t t-esc="seq_first"/> </t>
</t>
<t t-name="scope-last" t-trim="both">
<t t-foreach="seq"><t t-esc="seq_last"/> </t>
</t>
<t t-name="scope-parity" t-trim="both">
<t t-foreach="seq"><t t-esc="seq_parity"/> </t>
</t>
<t t-name="scope-size" t-trim="both">
<t t-foreach="seq"><t t-esc="seq_size"/> </t>
</t>
<t t-name="scope-self" t-foreach="seq" t-esc="seq"/>
<t t-name="scope-value" t-foreach="seq" t-esc="seq_value"/>
<t t-name="scope-index" t-foreach="seq" t-esc="seq_index"/>
<t t-name="scope-first" t-foreach="seq"><t t-esc="seq_first"/> </t>
<t t-name="scope-last" t-foreach="seq"><t t-esc="seq_last"/> </t>
<t t-name="scope-parity" t-foreach="seq"><t t-esc="seq_parity"/> </t>
<t t-name="scope-size" t-foreach="seq"><t t-esc="seq_size"/> </t>
<t t-name="aliasing" t-trim="both">
<t t-foreach="seq" t-as="item"><t t-esc="item"/></t>
</t>
<t t-name="loopvars-aliasing" t-trim="both">
<t t-foreach="seq" t-as="item"><t t-esc="item_parity"/> </t>
</t>
<t t-name="aliasing" t-foreach="seq" t-as="item" t-esc="item"/>
<t t-name="loopvars-aliasing"
t-foreach="seq" t-as="item"><t t-esc="item_parity"/> </t>
</templates>
@@ -1,19 +0,0 @@
<templates>
<t t-name="trim-content-both" t-trim="both">
ok
</t>
<t t-name="trim-content-before" t-trim="left"> ok </t>
<t t-name="trim-content-after" t-trim="right"> ok </t>
<t t-name="trim-content-unknown" t-trim="stuff"> ok </t>
<t t-name="trim-children-text" t-trim="inner">o k</t>
<t t-name="trim-children-elements" t-trim="inner">
<foo/>
<bar/>
</t>
<t t-name="trim-mixed-children" t-trim="inner">
[ <foo/> ]
</t>
<t t-name='trim-children-left' t-trim="left inner"> <foo/> <bar/> </t>
<t t-name='trim-children-right' t-trim="right inner"> <foo/> <bar/> </t>
</templates>
+7 -39
View File
@@ -5,9 +5,10 @@
<link rel="stylesheet" href="http://code.jquery.com/qunit/git/qunit.css" type="text/css" media="screen"/>
<script type="text/javascript" src="http://code.jquery.com/qunit/git/qunit.js"></script>
<script type="text/javascript" src="qweb.js"></script>
<script type="text/javascript" src="qweb2.js"></script>
<script>
QWeb = new QWeb2.Engine();
function trim(s) {
return s.replace(/(^\s+|\s+$)/g, '');
}
@@ -193,39 +194,6 @@
"Call with t-import (calls in current context)");
});
module("Trim", {
setup: function () {
QWeb.add_template('qweb-test-trim.xml');
},
teardown: function () {
QWeb.templates = [];
QWeb.tag = {};
QWeb.att = {};
}
});
test('Trim content', function () {
equals(QWeb.render('trim-content-both', {}), 'ok',
"Trims before and after content");
equals(QWeb.render('trim-content-before', {}), 'ok ',
"Trims before content");
equals(QWeb.render('trim-content-after', {}), ' ok',
"Trims after content");
raises(function () {QWeb.render('trim-content-unknown', {});},
"Trims with an unknown mode");
});
test('Trim content *and* children', function () {
equals(QWeb.render('trim-children-text', {}), 'o k',
"Trims text children");
equals(QWeb.render('trim-children-elements', {}), '<foo/><bar/>',
"Full trim");
equals(QWeb.render('trim-mixed-children', {}), '[<foo/>]',
"Trims mixed content of text and elements");
equals(QWeb.render('trim-children-left', {}), '<foo/><bar/> ',
"Trims children and left of content");
equals(QWeb.render('trim-children-right', {}), ' <foo/><bar/>',
"Trims children and right of content");
});
module("Foreach", {
setup: function () {
QWeb.add_template('qweb-test-foreach.xml');
@@ -252,19 +220,19 @@
"each value of the sequence is also via the _value");
equals(QWeb.render('scope-index', {seq:seq}), '01234',
"the current 0-based index is available via _index");
equals(QWeb.render('scope-first', {seq:seq}), 'true false false false false',
equals(QWeb.render('scope-first', {seq:seq}), 'true false false false false ',
"_first says whether the current item is the first of the sequence");
equals(QWeb.render('scope-last', {seq:seq}), 'false false false false true',
equals(QWeb.render('scope-last', {seq:seq}), 'false false false false true ',
"_last says whether the current item is the last of the sequence");
equals(QWeb.render('scope-parity', {seq:seq}), 'even odd even odd even',
equals(QWeb.render('scope-parity', {seq:seq}), 'even odd even odd even ',
"the parity (odd/even) of the current row is available via _parity");
equals(QWeb.render('scope-size', {seq:seq}), '5 5 5 5 5',
equals(QWeb.render('scope-size', {seq:seq}), '5 5 5 5 5 ',
"the total length of the sequence is available through _size");
});
test('Name aliasing via t-as', function () {
equals(QWeb.render('aliasing', {seq:seq}), '43210',
"the inner value can be re-bound via t-as");
equals(QWeb.render('loopvars-aliasing', {seq:seq}), 'even odd even odd even',
equals(QWeb.render('loopvars-aliasing', {seq:seq}), 'even odd even odd even ',
"inner loop variables should be rebound as well");
});
});
+4 -1
View File
@@ -234,8 +234,11 @@ var QWeb = {
return this.render(t_att["call"], d);
},
render_tag_js:function(e, t_att, g_att, v) {
var dict_name = t_att["js"] || "dict";
v[dict_name] = v;
var r = this.eval_str(this.render_element(e, t_att, g_att, v), v);
return t_att["js"] != "quiet" ? r : "";
delete(v[dict_name]);
return r || '';
},
/**
* Renders a foreach loop (@t-foreach).
+579
View File
@@ -0,0 +1,579 @@
// TODO: trim support
var QWeb2 = {
expressions_cache: {},
reserved_words: 'true,false,NaN,null,undefined,debugger,in,instanceof,new,function,return,this,typeof,eval,Math,RegExp,Array,Object'.split(','),
actions_precedence: 'foreach,if,call,set,esc,escf,raw,rawf,js,debug,log'.split(','),
word_replacement: {
'and': '&&',
'or': '||',
'gt': '>',
'gte': '>=',
'lt': '<',
'lte': '<='
},
tools: {
exception: function(message, context) {
context = context || {};
var prefix = 'QWeb2';
if (context.template) {
prefix += " - template['" + context.template + "']";
}
throw prefix + ": " + message;
},
trim: function(s, mode) {
switch (mode) {
case "left":
return s.replace(/^\s*/, "");
case "right":
return s.replace(/\s*$/, "");
default:
return s.replace(/^\s*|\s*$/g, "");
}
},
js_escape: function(s, noquotes) {
return (noquotes ? '' : "'") + s.replace(/\r?\n/g, "\\n").replace(/'/g, "\\'") + (noquotes ? '' : "'");
},
html_escape: function(s, attribute) {
if (s === null || s === undefined) {
return '';
}
s = (new String(s)).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
if (attribute) {
s = s.replace(/"/g, '&quot;');
}
return s;
},
gen_attribute: function(o) {
if (o !== null && o !== undefined) {
if (o.constructor === Array) {
if (o[1] !== null && o[1] !== undefined) {
return ' ' + o[0] + '="' + this.html_escape(o[1]) + '"';
}
} else if (typeof o === 'object') {
var r = '';
for (var k in o) {
if (o.hasOwnProperty(k)) {
r += this.gen_attribute([k, o[k]]);
}
}
return r;
}
}
return '';
},
extend: function(dst, src, exclude) {
for (var p in src) {
if (src.hasOwnProperty(p) && !(exclude && this.arrayIndexOf(exclude, p) !== -1)) {
dst[p] = src[p];
}
}
return dst;
},
arrayIndexOf : function(array, item) {
for (var i = 0, ilen = array.length; i < ilen; i++) {
if (array[i] === item) {
return i;
}
}
return -1;
},
call: function(context, template, old_dict, _import, callback) {
var new_dict = this.extend({}, old_dict);
new_dict['__caller__'] = old_dict['__template__'];
if (callback) {
new_dict[0] = callback(context, new_dict);
}
var r = context.engine._render(template, new_dict);
if (_import) {
if (_import === '*') {
this.extend(old_dict, new_dict, ['__caller__', '__template__']);
} else {
_import = _import.split(',');
for (var i = 0, ilen = _import.length; i < ilen; i++) {
var v = _import[i];
old_dict[v] = new_dict[v];
}
}
}
return r;
},
foreach: function(context, enu, as, old_dict, callback) {
if (enu != null) {
var size, new_dict = this.extend({}, old_dict);
new_dict[as + "_all"] = enu;
var as_value = as + "_value",
as_index = as + "_index",
as_first = as + "_first",
as_last = as + "_last",
as_parity = as + "_parity";
if (size = enu.length) {
new_dict[as + "_size"] = size;
for (var j = 0, jlen = enu.length; j < jlen; j++) {
var cur = enu[j];
new_dict[as_value] = cur;
new_dict[as_index] = j;
new_dict[as_first] = j === 0;
new_dict[as_last] = j + 1 === size;
new_dict[as_parity] = (j % 2 == 1 ? 'odd' : 'even');
if (cur.constructor === Object) {
this.extend(new_dict, cur);
}
new_dict[as] = cur;
callback(context, new_dict);
}
} else if (enu.constructor == Number) {
var _enu = [];
for (var i = 0; i < enu; i++) {
_enu.push(i);
}
this.foreach(context, enu, as, old_dict, callback);
} else {
var index = 0;
for (var k in enu) {
if (enu.hasOwnProperty(k)) {
var v = enu[k];
new_dict[as_value] = v;
new_dict[as_index] = index;
new_dict[as_first] = index === 0;
new_dict[as_parity] = (j % 2 == 1 ? 'odd' : 'even');
new_dict[as] = k;
callback(context, new_dict);
index += 1;
}
}
}
} else {
this.exception("No enumerator given to foreach", context);
}
}
}
};
QWeb2.Engine = (function() {
function Engine() {
// TODO: handle prefix at template level : t-prefix="x"
this.prefix = 't';
this.debug = false;
this.templates_resources = []; // TODO: implement this.reload()
this.templates = {};
this.compiled_templates = {};
this.dict = {};
this.tools = QWeb2.tools;
for (var i = 0; i < arguments.length; i++) {
this.add_template(arguments[i]);
}
}
QWeb2.tools.extend(Engine.prototype, {
add_template : function(template) {
this.templates_resources.push(template);
if (template.constructor === String) {
template = this.load_xml(template);
}
var ec = (template.documentElement && template.documentElement.childNodes) || template.childNodes || [];
for (var i = 0; i < ec.length; i++) {
var node = ec[i];
if (node.nodeType === 1) {
if (node.nodeName == 'parsererror') {
return this.tools.exception(node.innerText);
}
var name = node.getAttribute(this.prefix + '-name');
if (name) {
this.templates[name] = node;
}
}
}
return true;
},
load_xml : function(s) {
s = s.replace(/^\s*|\s*$/g, '');
if (this.tools.trim(s)[0] === '<') {
return this.load_xml_string(s);
} else {
var req = this.get_xhr();
if (req) {
// TODO: third parameter is async : https://developer.mozilla.org/en/XMLHttpRequest#open()
// do an on_ready in QWeb2{} that could be passed to add_template
req.open('GET', s, false);
req.send(null);
if (req.responseXML) {
if (req.responseXML.documentElement.nodeName == "parsererror") {
return this.tools.exception(req.responseXML.documentElement.childNodes[0].nodeValue);
}
return req.responseXML;
} else {
return this.load_xml_string(req.responseText);
}
}
}
},
load_xml_string : function(s) {
if (window.DOMParser) {
var dp = new DOMParser();
var r = dp.parseFromString(s, "text/xml");
if (r.body && r.body.firstChild && r.body.firstChild.nodeName == 'parsererror') {
return this.tools.exception(r.body.innerText);
}
return r;
}
var xDoc;
try {
// new ActiveXObject("Msxml2.DOMDocument.4.0");
xDoc = new ActiveXObject("MSXML2.DOMDocument");
} catch (e) {
return this.tools.exception("Could not find a DOM Parser");
}
xDoc.async = false;
xDoc.preserveWhiteSpace = true;
xDoc.loadXML(s);
return xDoc;
},
get_xhr : function() {
if (window.XMLHttpRequest) {
return new window.XMLHttpRequest();
}
try {
return new ActiveXObject('MSXML2.XMLHTTP.3.0');
} catch (e) {
return null;
}
},
compile : function(node) {
var e = new QWeb2.Element(this, node);
var template = node.getAttribute(this.prefix + '-name');
return "(function(dict) {\n" +
" /* 'this' refers to Qweb2.Engine instance */\n" +
" var context = { engine : this, template : " + (this.tools.js_escape(template)) + " };\n" +
" dict = dict || {};\n" +
" dict['__template__'] = '" + template + "';\n" +
" var r = [];\n" +
" /* START TEMPLATE */ try {\n" +
(e.compile()) + "\n" +
" /* END OF TEMPLATE */ } catch(error) {\n" +
" context.engine.tools.exception('Runtime Error: ' + error, context);\n" +
" }\n" +
" return r.join('');\n" +
"});";
},
render : function(template, dict) {
//console.time("QWeb render template " + template);
var r = this._render(template, dict);
//console.timeEnd("QWeb render template " + template);
return r;
},
_render : function(template, dict) {
if (this.compiled_templates[template]) {
return this.compiled_templates[template].apply(this, [dict || {}]);
} else if (this.templates[template]) {
var code = this.compile(this.templates[template]), tcompiled;
try {
// tcompiled = new Function(code); TODO: Check if could use this in order to avoid eval
tcompiled = eval(code);
} catch (error) {
console.log(code);
this.tools.exception("Error evaluating template: " + error, { template: name });
}
this.compiled_templates[template] = tcompiled;
return this.render(template, dict);
} else {
return this.tools.exception("Template '" + template + "' not found");
}
}
});
return Engine;
})();
QWeb2.Element = (function() {
function Element(engine, node) {
this.engine = engine;
this.node = node;
this.tag = node.tagName;
this.actions = {};
this.actions_done = [];
this.attributes = {};
this.children = [];
this._top = [];
this._bottom = [];
this._indent = 1;
this.process_children = true;
var childs = this.node.childNodes;
if (childs) {
for (var i = 0, ilen = childs.length; i < ilen; i++) {
this.children.push(new QWeb2.Element(this.engine, childs[i]));
}
}
var attrs = this.node.attributes;
if (attrs) {
for (var j = 0, jlen = attrs.length; j < jlen; j++) {
var attr = attrs[j];
var name = attr.name;
var m = name.match(new RegExp("^" + this.engine.prefix + "-(.+)"));
if (m) {
name = m[1];
if (name === 'name') {
continue;
}
this.actions[name] = attr.value;
} else {
this.attributes[name] = attr.value;
}
}
}
}
QWeb2.tools.extend(Element.prototype, {
compile : function() {
var r = [],
instring = false,
lines = this._compile().split('\n');
for (var i = 0, ilen = lines.length; i < ilen; i++) {
var m, line = lines[i];
if (m = line.match(/^(\s*)\/\/@string=(.*)/)) {
if (instring) {
if (this.engine.debug) {
// Split string lines in indented r.push arguments
r.push((m[2].indexOf("\\n") != -1 ? "',\n\t" + m[1] + "'" : '') + m[2]);
} else {
r.push(m[2]);
}
} else {
r.push(m[1] + "r.push('" + m[2]);
instring = true;
}
} else {
if (instring) {
r.push("');\n");
}
instring = false;
r.push(line + '\n');
}
}
return r.join('');
},
_compile : function() {
switch (this.node.nodeType) {
case 3:
case 4:
this.top_string(this.node.data);
break;
case 1:
this.compile_element();
}
var r = this._top.join('');
if (this.process_children) {
for (var i = 0, ilen = this.children.length; i < ilen; i++) {
var child = this.children[i];
child._indent = this._indent;
r += child._compile();
}
}
r += this._bottom.join('');
return r;
},
format_expression : function(e) {
/* Naive format expression builder. Replace reserved words and variables to dict[variable]
* Does not handle spaces before dot yet, and causes problems for anonymous functions. Use t-js="" for that */
if (e === '0') {
return "dict['0']";
}
if (QWeb2.expressions_cache[e]) {
return QWeb2.expressions_cache[e];
}
var chars = e.split(''),
instring = '',
invar = '',
invar_pos = 0,
r = '';
chars.push(' ');
for (var i = 0, ilen = chars.length; i < ilen; i++) {
var c = chars[i];
if (instring.length) {
if (c === instring && chars[i - 1] !== "\\") {
instring = '';
}
} else if (c === '"' || c === "'") {
instring = c;
} else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
invar = c;
invar_pos = i;
continue;
} else if (c.match(/\W/) && invar.length) {
// TODO: Should check for possible spaces before dot
if (chars[invar_pos - 1] !== '.' && QWeb2.tools.arrayIndexOf(QWeb2.reserved_words, invar) < 0) {
invar = QWeb2.word_replacement[invar] || ("dict['" + invar + "']");
}
r += invar;
invar = '';
} else if (invar.length) {
invar += c;
continue;
}
r += c;
}
r = r.slice(0, -1);
QWeb2.expressions_cache[e] = r;
return r;
},
string_interpolation : function(s) {
if (!s) {
return "''";
}
var regex = /^{(.*)}(.*)/,
src = s.split(/#/),
r = [];
for (var i = 0, ilen = src.length; i < ilen; i++) {
var val = src[i],
m = val.match(regex);
if (m) {
r.push("(" + this.format_expression(m[1]) + ")");
if (m[2]) {
r.push(this.engine.tools.js_escape(m[2]));
}
} else if (!(i === 0 && val === '')) {
r.push(this.engine.tools.js_escape((i === 0 ? '' : '#') + val));
}
}
return r.join(' + ');
},
indent : function() {
return this._indent++;
},
dedent : function() {
if (this._indent !== 0) {
return this._indent--;
}
},
get_indent : function() {
return new Array(this._indent + 1).join("\t");
},
top : function(s) {
return this._top.push(this.get_indent() + s + '\n');
},
top_string : function(s) {
return this._top.push(this.get_indent() + "//@string=" + this.engine.tools.js_escape(s, true) + '\n');
},
bottom : function(s) {
return this._bottom.unshift(this.get_indent() + s + '\n');
},
bottom_string : function(s) {
return this._bottom.unshift(this.get_indent() + "//@string=" + this.engine.tools.js_escape(s, true) + '\n');
},
compile_element : function() {
for (var i = 0, ilen = QWeb2.actions_precedence.length; i < ilen; i++) {
var a = QWeb2.actions_precedence[i];
if (a in this.actions) {
var value = this.actions[a];
var key = 'compile_action_' + a;
if (!this[key]) {
this.engine.tools.exception("No handler method for action '" + a + "'");
}
this[key](value);
}
}
if (this.tag !== this.engine.prefix) {
var tag = "<" + this.tag;
for (var a in this.attributes) {
tag += this.engine.tools.gen_attribute([a, this.attributes[a]]);
}
this.top_string(tag);
if (this.actions.att) {
this.top("r.push(context.engine.tools.gen_attribute(" + (this.format_expression(this.actions.att)) + "));");
}
for (var a in this.actions) {
var v = this.actions[a];
var m = a.match(/att-(.+)/);
if (m) {
this.top("r.push(context.engine.tools.gen_attribute(['" + m[1] + "', (" + (this.format_expression(v)) + ")]));");
}
var m = a.match(/attf-(.+)/);
if (m) {
this.top("r.push(context.engine.tools.gen_attribute(['" + m[1] + "', (" + (this.string_interpolation(v)) + ")]));");
}
}
if (this.children.length || this.actions.opentag === 'true') {
this.top_string(">");
this.bottom_string("</" + this.tag + ">");
} else {
this.top_string("/>");
}
}
},
compile_action_if : function(value) {
this.top("if (" + (this.format_expression(value)) + ") {");
this.bottom("}");
this.indent();
},
compile_action_foreach : function(value) {
var as = this.actions['as'] || value.replace(/[^a-zA-Z0-9]/g, '_');
//TODO: exception if t-as not valid
this.top("context.engine.tools.foreach(context, " + (this.format_expression(value)) + ", " + (this.engine.tools.js_escape(as)) + ", dict, function(context, dict) {");
this.bottom("});");
this.indent();
},
compile_action_call : function(value) {
var _import = this.actions['import'] || '';
if (this.children.length === 0) {
return this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict, " + (this.engine.tools.js_escape(_import)) + "));");
} else {
this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict, " + (this.engine.tools.js_escape(_import)) + ", function(context, dict) {");
this.bottom("}));");
this.indent();
this.top("var r = [];");
return this.bottom("return r.join('');");
}
},
compile_action_set : function(value) {
if (this.actions['value']) {
this.top("dict['" + value + "'] = (" + (this.format_expression(this.actions['value'])) + ");");
this.process_children = false;
} else {
if (this.children.length === 0) {
this.top("dict['" + value + "'] = '';");
} else if (this.children.length === 1 && this.children[0].node.nodeType === 3) {
this.top("dict['" + value + "'] = " + (this.engine.tools.js_escape(this.children[0].node.data)) + ";");
this.process_children = false;
} else {
this.top("dict['" + value + "'] = (function(dict) {");
this.bottom("})(dict);");
this.indent();
this.top("var r = [];");
this.bottom("return r.join('');");
}
}
},
compile_action_esc : function(value) {
this.top("r.push(context.engine.tools.html_escape(" + (this.format_expression(value)) + "));");
},
compile_action_escf : function(value) {
this.top("r.push(context.engine.tools.html_escape(" + (this.string_interpolation(value)) + "));");
},
compile_action_raw : function(value) {
this.top("r.push(" + (this.format_expression(value)) + ");");
},
compile_action_rawf : function(value) {
this.top("r.push(" + (this.string_interpolation(value)) + ");");
},
compile_action_js : function(value) {
this.top("(function(" + value + ") {");
this.bottom("})(dict);");
this.indent();
if (this.children.length === 1) {
var lines = this.children[0].node.data.split(/\r?\n/);
for (var i = 0, ilen = lines.length; i < ilen; i++) {
this.top(lines[i]);
}
} else {
this.engine.tools.exception("'js' code block contains " + this.children.length + " nodes instead of 1");
}
// Maybe I could handle the children ?
this.process_children = false;
},
compile_action_debug : function(value) {
this.top("debugger;");
},
compile_action_log : function(value) {
this.top("console.log(" + this.format_expression(value) + "});");
}
});
return Element;
})();
+6
View File
@@ -8,7 +8,10 @@
<script type="text/javascript" src="/base/static/lib/LABjs/LAB.js"></script>
<script type="text/javascript" src="/base/static/lib/underscore/underscore.js"></script>
<script type="text/javascript" src="/base/static/lib/underscore/underscore.strings.js"></script>
<script type="text/javascript" src="/base/static/lib/qweb/qweb2.js"></script>
<!-- Uncomment in order to use legacy QWeb
<script type="text/javascript" src="/base/static/lib/qweb/qweb.js"></script>
-->
<script type="text/javascript" src="/base/static/lib/jquery/jquery-1.5.2.js"></script>
<script type="text/javascript" src="/base/static/lib/jquery.ui/js/jquery-ui-1.8.9.custom.min.js"></script>
<script type="text/javascript" src="/base/static/lib/jquery.ui/js/jquery-ui-timepicker-addon.js"></script>
@@ -16,7 +19,9 @@
<script type="text/javascript" src="/base/static/lib/jquery.superfish/js/hoverIntent.js"></script>
<script type="text/javascript" src="/base/static/lib/jquery.superfish/js/superfish.js"></script>
<script type="text/javascript" src="/base/static/lib/jquery.ba-bbq/jquery.ba-bbq.js"></script>
<script type="text/javascript" src="/base/static/lib/datejs/date-en-US.js"></script>
<script type="text/javascript" src="/base/static/src/js/base.js"></script>
<script type="text/javascript" src="/base/static/src/js/dates.js"></script>
<script type="text/javascript" src="/base/static/src/js/chrome.js"></script>
<script type="text/javascript" src="/base/static/src/js/data.js"></script>
<script type="text/javascript" src="/base/static/src/js/form.js"></script>
@@ -32,6 +37,7 @@
<script type="text/javascript">
$(function() {
QWeb = window.QWeb || new QWeb2.Engine();
var oe = openerp.init();
oe.base.webclient("oe");
});
+42 -41
View File
@@ -1,8 +1,8 @@
/* TODO: seperate openerp webclient page css from openerp views css */
/* TODO: separate openerp web client page css from openerp views css */
body {
padding: 0;
margin: 0;
font-family: helvetica, sans, arial;
font-family: helvetica, arial, sans-serif;
font-size: 80%;
}
@@ -85,7 +85,7 @@ body.openerp {
/* Menu */
.openerp .sf-menu {
margin-bottom: 0px;
margin-bottom: 0;
}
/*
.sf-menu a {
@@ -169,10 +169,9 @@ body.openerp {
border: none;
}
.openerp .secondary_menu h4 {
padding: 0 0 2px;
padding: 0 0 2px 10px;
border: none;
background: none;
padding-left: 10px;
}
.openerp .secondary_menu h3 span, .openerp .secondary_menu h4 span {
left: 0 !important;
@@ -224,10 +223,9 @@ body.openerp {
margin-right: 10px;
line-height: 63px;
text-align: center;
border-bottom: 1px solid black;
border-right: 1px solid black;
border-top: 1px solid white;
border-left: 1px solid white;
border: 1px solid white;
border-right-color: black;
border-bottom-color: black;
background: #FFFFFF;
background: -moz-linear-gradient(top, #FFFFFF 0%, #CECECE 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFFFFF), color-stop(100%,#CECECE));
@@ -301,7 +299,6 @@ body.openerp {
right: 0;
top: 5px;
padding: 1px 4px 2px;
background: #333;
background: rgba(0, 0, 0, 0.75);
border-radius: 7px;
-moz-border-radius: 7px;
@@ -316,9 +313,9 @@ body.openerp {
overflow: hidden;
padding: 5px 0;
position: relative;
-moz-box-shadow: inset 0px 3px 5px rgba(0, 0, 0, 0.4);
-webkit-box-shadow: inset 0px 3px 5px rgba(0, 0, 0, 0.4);
box-shadow: inset 0px 3px 5px rgba(0, 0, 0, 0.4);
-moz-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.4);
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.4);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.4);
}
.openerp div.oe_footer p.oe_footer_powered {
left: 50%;
@@ -336,8 +333,9 @@ body.openerp {
}
/* Main Application */
.openerp .application {
padding: 0px;
.openerp .oe-application {
padding: 0;
height: 100%;
}
.openerp h2.oe_view_title {
@@ -379,7 +377,7 @@ body.openerp {
background: #AAAAAA;
}
.openerp .filter_icon {
padding: 1px 2px 0px 2px;
padding: 1px 2px 0 2px;
margin-left: 0;
margin-right: 0;
}
@@ -484,8 +482,16 @@ body.openerp {
.openerp .oe-listview td,
.openerp .oe-listview th {
vertical-align: middle;
text-align: left;
}
.openerp .oe-listview th.oe-sortable,
.openerp .oe-listview th.oe-sortable .ui-icon {
cursor: pointer;
}
.openerp .oe-listview .oe-field-cell {
cursor: pointer;
}
.openerp .oe-listview .oe-field-cell button {
padding: 0;
border: none;
@@ -535,17 +541,17 @@ body.openerp {
/* Notebook */
.openerp .oe_form_notebook {
padding: 0px;
padding: 0;
background: none;
border-width: 0px;
border-width: 0;
}
.openerp .oe_form_notebook ul.ui-tabs-nav {
padding-left: 0px;
padding-left: 0;
background: transparent;
border-width: 0px 0px 1px 0px;
border-radius: 0px;
-moz-border-radius: 0px;
-webkit-border-radius: 0px;
border-width: 0 0 1px 0;
border-radius: 0;
-moz-border-radius: 0;
-webkit-border-radius: 0;
line-height: 0.5em;
}
.openerp .oe_form_notebook ul.ui-tabs-nav li {
@@ -553,7 +559,7 @@ body.openerp {
}
.openerp .oe_form_notebook .ui-tabs-panel {
background: #f9f9f9;
border-width: 0px 1px 1px 1px;
border-width: 0 1px 1px 1px;
}
.openerp .oe_form_notebook .ui-tabs-selected {
background: #f9f9f9;
@@ -562,7 +568,7 @@ body.openerp {
/* Form */
.openerp table.oe_frame td {
color: #4c4c4c;
font-weight: 80%;
font-weight: bold;
}
.openerp .required.error {
border: 1px solid #900;
@@ -636,7 +642,7 @@ body.openerp {
.openerp td.required input, .openerp td.required select {
background-color: #D2D2FF;
}
.openerp td.invalid input, .openerp td.invalid select {
.openerp td.invalid input, .openerp td.invalid select, .openerp td.invalid textarea {
background-color: #F66;
border: 1px solid #D00;
}
@@ -673,13 +679,14 @@ body.openerp {
}
.openerp .view-manager-main-content {
width: 100%;
padding: 6px;
}
.openerp .view-manager-main-sidebar {
width:0;
padding:0;
margin:0;
width: 180px;
padding: 0;
margin: 0;
}
.openerp .sidebar-main-div {
@@ -690,9 +697,7 @@ body.openerp {
padding:0;
margin:0;
width:180px;
border-left-color: #D2CFCF;
border-left-style: solid;
border-left-width: 1px;
border-left: 1px solid #D2CFCF;
height:100%;
font-family: Ubuntu, Helvetica, sans-serif;
font-size: 0.9em;
@@ -720,17 +725,14 @@ body.openerp {
font-family: Ubuntu, Helvetica, sans-serif;
font-size: 1.15em;
color: #8E8E8E;
text-shadow: white 0px 1px 0px;
text-shadow: white 0 1px 0;
padding-left: 10px;
padding-right: 21px;
height: 21px;
background: url(../img/sideheader-a-bg.png) repeat-x;
border-color: #D2CFCF;
border-style: solid;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-width: 0;
border: 1px solid #D2CFCF;
border-right-width: 0;
border-left-width: 0;
}
.openerp .view-manager-main-sidebar ul {
@@ -746,9 +748,8 @@ body.openerp {
}
.openerp .toggle-sidebar {
border-color: #D2CFCF;
border-style: solid;
border-width: 1px;
cursor: pointer;
border: 1px solid #D2CFCF;
display: block;
background: url(../img/toggle-a-bg.png);
width: 21px;
+1
View File
@@ -120,6 +120,7 @@
/** @namespace */
openerp.base = function(instance) {
openerp.base.dates(instance);
openerp.base.chrome(instance);
openerp.base.data(instance);
openerp.base.views(instance);
+47 -2
View File
@@ -118,6 +118,13 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base.
ids: ids,
args: args
}, callback);
},
exec_workflow: function (id, signal, callback) {
return this.rpc('/base/dataset/exec_workflow', {
model: this.model,
id: id,
signal: signal
}, callback);
}
});
@@ -137,7 +144,7 @@ openerp.base.DataSetSearch = openerp.base.DataSet.extend({
init: function(session, model) {
this._super(session, model);
this.domain = [];
this.sort = [];
this._sort = [];
this.offset = 0;
// subset records[offset:offset+limit]
// is it necessary ?
@@ -159,7 +166,7 @@ openerp.base.DataSetSearch = openerp.base.DataSet.extend({
fields: fields,
domain: this.domain,
context: this.context,
sort: this.sort,
sort: this.sort(),
offset: offset,
limit: limit
}, function (records) {
@@ -170,12 +177,50 @@ openerp.base.DataSetSearch = openerp.base.DataSet.extend({
}
callback(records);
});
},
/**
* Reads or changes sort criteria on the dataset.
*
* If not provided with any argument, serializes the sort criteria to
* an SQL-like form usable by OpenERP's ORM.
*
* If given a field, will set that field as first sorting criteria or,
* if the field is already the first sorting criteria, will reverse it.
*
* @param {String} [field] field to sort on, reverses it (toggle from ASC to DESC) if already the main sort criteria
* @param {Boolean} [force_reverse=false] forces inserting the field as DESC
* @returns {String|undefined}
*/
sort: function (field, force_reverse) {
if (!field) {
return _.map(this._sort, function (criteria) {
if (criteria[0] === '-') {
return criteria.slice(1) + ' DESC';
}
return criteria + ' ASC';
}).join(', ');
}
var reverse = force_reverse || (this._sort[0] === field);
this._sort = _.without(this._sort, field, '-' + field);
this._sort.unshift((reverse ? '-' : '') + field);
return undefined;
}
});
openerp.base.DataSetRelational = openerp.base.DataSet.extend( /** @lends openerp.base.DataSet# */{
});
openerp.base.DataSetMany2Many = openerp.base.DataSetStatic.extend({
/* should extend DataSetStatic instead, but list view still does not support it
*/
unlink: function(ids) {
// just do nothing
},
});
};
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
+89
View File
@@ -0,0 +1,89 @@
openerp.base.dates = function(openerp) {
openerp.base.parse_datetime = function(str) {
if(!str) {
return str;
}
var regex = /\d\d\d\d-\d\d-\d\) \d\d:\d\d:\d\d/;
var res = regex.exec(str);
if ( res[0] != str ) {
throw "'" + str + "' is not a valid datetime";
}
var obj = Date.parse(str + " GMT");
if (! obj) {
throw "'" + str + "' is not a valid datetime";
}
return obj;
};
openerp.base.parse_date = function(str) {
if(!str) {
return str;
}
var regex = /\d\d\d\d-\d\d-\d\d/;
var res = regex.exec(str);
if ( res[0] != str ) {
throw "'" + str + "' is not a valid date";
}
var obj = Date.parse(str);
if (! obj) {
throw "'" + str + "' is not a valid date";
}
return obj;
};
openerp.base.parse_time = function(str) {
if(!str) {
return str;
}
var regex = /\d\d:\d\d:\d\d/;
var res = regex.exec(str);
if ( res[0] != str ) {
throw "'" + str + "' is not a valid time";
}
var obj = Date.parse(str);
if (! obj) {
throw "'" + str + "' is not a valid time";
}
return obj;
};
/**
* Just a simple function to add some '0' if an integer it too small.
*/
var fts = function(str, size) {
str = "" + str;
var to_add = "";
_.each(_.range(size - str.length), function() {
to_add = to_add + "0";
});
return to_add + str;
};
openerp.base.format_datetime = function(obj) {
if(! str) {
return false;
}
return fts(obj.getUTCFullYear(),4) + "-" + fts(obj.getUTCMonth() + 1,2) + "-"
+ fts(obj.getUTCDate(),2) + " " + fts(obj.getUTCHours(),2) + ":"
+ fts(obj.getUTCMinutes(),2) + ":" + fts(obj.getUTCSeconds(),2);
};
openerp.base.format_date = function(obj) {
if(! str) {
return false;
}
return fts(obj.getFullYear(),4) + "-" + fts(obj.getMonth() + 1,2) + "-"
+ fts(obj.getDate(),2);
};
openerp.base.format_time = function(obj) {
if(! str) {
return false;
}
return fts(obj.getHours(),2) + ":" + fts(obj.getMinutes(),2) + ":"
+ fts(obj.getSeconds(),2);
};
};
+84 -33
View File
@@ -457,16 +457,14 @@ openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({
}
this.add_widget(widget);
},
add_widget: function(w) {
if (!w.invisible) {
var current_row = this.table[this.table.length - 1];
if (current_row.length && (this.x + w.colspan) > this.columns) {
current_row = this.add_row();
}
current_row.push(w);
this.x += w.colspan;
add_widget: function(widget) {
var current_row = this.table[this.table.length - 1];
if (current_row.length && (this.x + widget.colspan) > this.columns) {
current_row = this.add_row();
}
return w;
current_row.push(widget);
this.x += widget.colspan;
return widget;
}
});
@@ -532,33 +530,19 @@ openerp.base.form.WidgetButton = openerp.base.form.Widget.extend({
}
},
on_confirmed: function() {
var attrs = this.node.attrs;
if (attrs.special) {
this.on_button_object({
result : { type: 'ir.actions.act_window_close' }
var self = this;
this.execute_action(
this.node.attrs, this.view.dataset, this.session.action_manager,
this.view.datarecord.id, function (result) {
self.log("Button returned", result);
self.view.reload();
});
} else {
var type = attrs.type || 'workflow';
var context = _.extend({}, this.view.dataset.context, attrs.context || {});
switch(type) {
case 'object':
return this.view.dataset.call(attrs.name, [this.view.datarecord.id], [context], this.on_button_object);
break;
default:
this.log(_.sprintf("Unsupported button type : %s", type));
}
}
},
on_button_object: function(r) {
if (r.result === false) {
this.log("Button object returns false");
} else if (r.result.constructor == Object) {
this.session.action_manager.do_action(r.result);
} else {
this.view.reload();
}
}
});
// let WidgetButton execute actions
_.extend(openerp.base.form.WidgetButton.prototype,
openerp.base.ActionExecutor);
openerp.base.form.WidgetLabel = openerp.base.form.Widget.extend({
init: function(view, node) {
@@ -897,6 +881,73 @@ openerp.base.form.FieldMany2Many = openerp.base.form.Field.extend({
init: function(view, node) {
this._super(view, node);
this.template = "FieldMany2Many";
this.list_id = _.uniqueId("many2many");
},
start: function() {
this._super.apply(this, arguments);
this.dataset = new openerp.base.DataSetMany2Many(this.session, this.field.relation);
this.list_view = new openerp.base.form.Many2ManyListView(undefined, this.view.session,
this.list_id, this.dataset, false, {'selected': false, 'addable': 'Add'});
var self = this;
this.list_view.m2m_field = this;
this.list_view.start();
},
set_value: function(value) {
if (value != false) {
this.dataset.ids = value;
this.dataset.count = value.length;
this.list_view.do_reload();
}
},
get_value: function() {
return [[6,false,this.dataset.ids]];
}
});
openerp.base.form.Many2ManyListView = openerp.base.ListView.extend({
do_delete: function (e) {
e.stopImmediatePropagation();
var ids = [this.rows[$(e.currentTarget).closest('tr').prevAll().length].data.id.value];
this.dataset.ids = _.without.apply(null, [this.dataset.ids].concat(ids));
this.dataset.count = this.dataset.ids.length;
// there may be a faster way
this.do_reload();
this.m2m_field.on_ui_change();
},
do_reload: function () {
/* Dear xmo, according to your comments, this method's implementation in list view seems
* to be a little bit bullshit.
* I assume the list view will be changed later, so I hope it will support static datasets.
*/
return this.rpc('/base/listview/fill', {
'model': this.dataset.model,
'id': this.view_id,
'domain': [["id", "in", this.dataset.ids]],
'context': this.dataset.context
}, this.do_fill_table);
},
do_add_record: function (e) {
e.stopImmediatePropagation();
// TODO: need to open a popup with search view
},
on_select_row: function(event) {
var $target = $(event.currentTarget);
var row = this.rows[$target.prevAll().length];
var id = row.data.id.value;
if(! id) {
return;
}
var action_manager = this.m2m_field.view.session.action_manager;
action_manager.do_action({
"res_model": this.dataset.model,
"views":[[false,"form"]],
"res_id": id,
"type":"ir.actions.act_window",
"view_type":"form",
"view_mode":"form",
"target":"new"
});
}
});
+119 -21
View File
@@ -10,9 +10,23 @@ openerp.base.ListView = openerp.base.Controller.extend(
// whether the column headers should be displayed
'header': true,
// display addition button, with that label
'addable': "New"
'addable': "New",
// whether the list view can be sorted, note that once a view has been
// sorted it can not be reordered anymore
'sortable': true,
// whether the view rows can be reordered (via vertical drag & drop)
'reorderable': true
},
/**
* Core class for list-type displays.
*
* As a view, needs a number of view-related parameters to be correctly
* instantiated, provides options and overridable methods for behavioral
* customization.
*
* See constructor parameters and method documentations for information on
* the default behaviors and possible options for the list view.
*
* @constructs
* @param view_manager
* @param session An OpenERP session object
@@ -24,6 +38,10 @@ openerp.base.ListView = openerp.base.Controller.extend(
* @param {Boolean} [options.header=true] should the list's header be displayed
* @param {Boolean} [options.deletable=true] are the list rows deletable
* @param {null|String} [options.addable="New"] should the new-record button be displayed, and what should its label be. Use ``null`` to hide the button.
* @param {Boolean} [options.sortable=true] is it possible to sort the table by clicking on column headers
* @param {Boolean} [options.reorderable=true] is it possible to reorder list rows
*
* @borrows openerp.base.ActionExecutor#execute_action as #execute_action
*/
init: function(view_manager, session, element_id, dataset, view_id, options) {
this._super(session, element_id);
@@ -37,11 +55,42 @@ openerp.base.ListView = openerp.base.Controller.extend(
this.options = _.extend({}, this.defaults, options || {});
},
/**
* View startup method, the default behavior is to set the ``oe-listview``
* class on its root element and to perform an RPC load call.
*
* @returns {$.Deferred} loading promise
*/
start: function() {
this.$element.addClass('oe-listview');
return this.rpc("/base/listview/load", {"model": this.model, "view_id":this.view_id,
toolbar:!!this.view_manager.sidebar}, this.on_loaded);
return this.rpc("/base/listview/load", {
model: this.model,
view_id: this.view_id,
toolbar: this.view_manager ? !!this.view_manager.sidebar : false
}, this.on_loaded);
},
/**
* Called after loading the list view's description, sets up such things
* as the view table's columns, renders the table itself and hooks up the
* various table-level and row-level DOM events (action buttons, deletion
* buttons, selection of records, [New] button, selection of a given
* record, ...)
*
* Sets up the following:
*
* * Processes arch and fields to generate a complete field descriptor for each field
* * Create the table itself and allocate visible columns
* * Hook in the top-level (header) [New|Add] and [Delete] button
* * Sets up showing/hiding the top-level [Delete] button based on records being selected or not
* * Sets up event handlers for action buttons and per-row deletion button
* * Hooks global callback for clicking on a row
* * Sets up its sidebar, if any
*
* @param {Object} data wrapped fields_view_get result
* @param {Object} data.fields_view fields_view_get result (processed)
* @param {Object} data.fields_view.fields mapping of fields for the current model
* @param {Object} data.fields_view.arch current list view descriptor
*/
on_loaded: function(data) {
var self = this;
this.fields_view = data.fields_view;
@@ -74,6 +123,8 @@ openerp.base.ListView = openerp.base.Controller.extend(
this.visible_columns = _.filter(this.columns, function (column) {
return column.invisible !== '1';
});
if (!this.fields_view.sorted) { this.fields_view.sorted = {}; }
this.$element.html(QWeb.render("ListView", this));
// Head hook
@@ -81,6 +132,14 @@ openerp.base.ListView = openerp.base.Controller.extend(
this.$element.find('#oe-list-delete')
.hide()
.click(this.do_delete_selected);
this.$element.find('thead').delegate('th[data-id]', 'click', function (e) {
e.stopPropagation();
self.dataset.sort($(this).data('id'));
// TODO: should only reload content (and set the right column to a sorted display state)
self.do_reload();
});
var $table = this.$element.find('table');
// Cell events
@@ -98,19 +157,20 @@ openerp.base.ListView = openerp.base.Controller.extend(
});
$table.delegate(
'td.oe-field-cell button', 'click', function (e) {
e.stopImmediatePropagation();
var $cell = $(e.currentTarget).closest('td');
var col_index = $cell.prevAll('td').length;
var field = self.visible_columns[col_index];
var action = field.name;
var $row = $cell.parent('tr');
var row = self.rows[$row.prevAll().length];
var context = _.extend(
{}, self.dataset.context, field.context || {});
self.dataset.call(action, [row.data.id.value], [context],
self.do_reload);
e.stopImmediatePropagation();
// TODO: we should probably only reload content, also maybe diff records or something, instead of replacing every single row
self.execute_action(
field, self.dataset, self.session.action_manager,
row.data.id.value, self.do_reload);
});
$table.delegate(
'td.oe-record-delete button', 'click', this.do_delete);
@@ -120,17 +180,25 @@ openerp.base.ListView = openerp.base.Controller.extend(
'tr', 'click', this.on_select_row);
// sidebar stuff
if (this.view_manager.sidebar) {
if (this.view_manager && this.view_manager.sidebar) {
this.view_manager.sidebar.set_toolbar(data.fields_view.toolbar);
}
},
/**
* Fills the table with the provided records after emptying it
*
* @param {Array} records the records to fill the list view with
* TODO: should also re-load the table itself, as e.g. columns may have changed
*
* @param {Object} result filling result
* @param {Array} [result.view] the new view (wrapped fields_view_get result)
* @param {Array} result.records records the records to fill the list view with
* @returns {Promise} promise to the end of view rendering (list views are asynchronously filled for improved responsiveness)
*/
do_fill_table: function(records) {
do_fill_table: function(result) {
if (result.view) {
this.on_loaded({fields_view: result.view});
}
var records = result.records;
var $table = this.$element.find('table');
this.rows = records;
@@ -188,22 +256,39 @@ openerp.base.ListView = openerp.base.Controller.extend(
});
},
/**
* Asks the view manager to switch to a different view, using the provided
* record index (within the current dataset).
* Used to handle a click on a table row, if no other handler caught the
* event.
*
* The default implementation asks the list view's view manager to switch
* to a different view (by calling
* :js:func:`~openerp.base.ViewManager.on_mode_switch`), using the
* provided record index (within the current list view's dataset).
*
* If the index is null, ``switch_to_record`` asks for the creation of a
* new record.
*
* @param {Number|null} index the record index (in the current dataset) to switch to
* @param {String} [view="form"] the view to switch to
* @param {String} [view="form"] the view type to switch to
*/
switch_to_record:function (index, view) {
view = view || 'form';
this.dataset.index = index;
_.delay(_.bind(function () {
this.view_manager.on_mode_switch(view);
if(this.view_manager) {
this.view_manager.on_mode_switch(view);
}
}, this));
},
/**
* Base handler for clicking on a row, discovers the index of the record
* corresponding to the clicked row in the list view's dataset.
*
* Should not be overridden, use
* :js:func:`~openerp.base.ListView.switch_to_record` to customize the
* behavior of the list view when clicking on a row instead.
*
* @param {Object} event jQuery DOM event object
*/
on_select_row: function (event) {
var $target = $(event.currentTarget);
if (!$target.parent().is('tbody')) {
@@ -216,8 +301,8 @@ openerp.base.ListView = openerp.base.Controller.extend(
if (index == undefined || index === -1) {
return;
}
this.switch_to_record(index);
},
this.switch_to_record(index);
},
do_show: function () {
this.$element.show();
if (this.hidden) {
@@ -243,9 +328,19 @@ openerp.base.ListView = openerp.base.Controller.extend(
'model': this.dataset.model,
'id': this.view_id,
'context': this.dataset.context,
'domain': this.dataset.domain
'domain': this.dataset.domain,
'sort': this.dataset.sort && this.dataset.sort()
}, this.do_fill_table);
},
/**
* Event handler for a search, asks for the computation/folding of domains
* and contexts (and group-by), then reloads the view's content.
*
* @param {Array} domains a sequence of literal and non-literal domains
* @param {Array} contexts a sequence of literal and non-literal contexts
* @param {Array} groupbys a sequence of literal and non-literal group-by contexts
* @returns {$.Deferred} fold request evaluation promise
*/
do_search: function (domains, contexts, groupbys) {
var self = this;
return this.rpc('/base/session/eval_domain_and_context', {
@@ -266,7 +361,7 @@ openerp.base.ListView = openerp.base.Controller.extend(
/**
* Handles the signal to delete a line from the DOM
*
* @param e
* @param e jQuery event object
*/
do_delete: function (e) {
// don't link to forms
@@ -295,7 +390,7 @@ openerp.base.ListView = openerp.base.Controller.extend(
},
/**
* Gets the ids of all currently selected records, if any
* @returns a list of ids, empty if no record is selected (or the list view is not selectable
* @returns {Array} empty if no record is selected (or the list view is not selectable)
*/
get_selection: function () {
if (!this.options.selectable) {
@@ -307,7 +402,10 @@ openerp.base.ListView = openerp.base.Controller.extend(
return rows[$(this).prevAll().length].data.id.value;
}).get();
}
// TODO: implement sort (click on column headers), if sorted, the list can not be reordered anymore
// TODO: implement reorder (drag and drop rows)
});
_.extend(openerp.base.ListView.prototype, openerp.base.ActionExecutor);
openerp.base.TreeView = openerp.base.Controller.extend({
});
-1
View File
@@ -146,7 +146,6 @@ openerp.base.SearchView = openerp.base.Controller.extend({
do_search: function (e) {
if (e && e.preventDefault) { e.preventDefault(); }
//debugger;
var domains = [], contexts = [];
var errors = [];
+78 -11
View File
@@ -17,9 +17,10 @@ openerp.base.ActionManager = openerp.base.Controller.extend({
/**
* Process an action
* Supported actions: act_window
*
* If the action contains a 'no_sidebar' key, the resulting view will never contain a sidebar.
*/
do_action: function(action) {
this.log(action);
var self = this;
// instantiate the right controllers by understanding the action
switch (action.type) {
@@ -36,14 +37,18 @@ openerp.base.ActionManager = openerp.base.Controller.extend({
// When dialog is closed with ESC key or close manually, branch to act_window_close logic
self.do_action({ type: 'ir.actions.act_window_close' });
});
var viewmanager = new openerp.base.ViewManagerAction(this.session ,element_id, action, false);
var viewmanager = new openerp.base.ViewManagerAction(this.session, element_id, action, false);
viewmanager.start();
this.dialog_stack.push(viewmanager);
} else if (action.target == "current") {
} else {
if (this.viewmanager) {
this.viewmanager.stop();
}
this.viewmanager = new openerp.base.ViewManagerAction(this.session,this.element_id, action, true);
var sidebar = true;
if(action.no_sidebar) {
sidebar = false;
}
this.viewmanager = new openerp.base.ViewManagerAction(this.session, this.element_id, action, sidebar);
this.viewmanager.start();
}
break;
@@ -58,6 +63,58 @@ openerp.base.ActionManager = openerp.base.Controller.extend({
}
});
/**
* Mixin for action-executing objects, provides handling of OpenERP actions to
* all clients.
*
* Mix into existing classes via ``_.extend`` of the class's prototype.
*
* @class
*/
openerp.base.ActionExecutor =
/**
* @lends openerp.base.ActionExecutor#
*/ {
/**
* Fetches and executes the action identified by ``action_data``.
*
* @param {Object} action_data the action descriptor data
* @param {String} action_data.name the action name, used to uniquely identify the action to find and execute it
* @param {String} [action_data.special=null] special action handlers (currently: only ``'cancel'``)
* @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'``
* @param {Object} [action_data.context=null] additional action context, to add to the current context
* @param {openerp.base.DataSet} dataset a dataset object used to communicate with the server
* @param {openerp.base.ActionManager} action_manager object able to actually execute the action, if any is fetched
* @param {Number} [record_id] the identifier of the object on which the action is to be applied
* @param {Function} on_no_action callback to execute if the action does not generate any result (no new action)
*/
execute_action: function (action_data, dataset, action_manager, record_id, on_no_action) {
var handler = function (r) {
if (r.result && r.result.constructor == Object) {
action_manager.do_action(r.result);
} else {
on_no_action(r.result);
}
};
if (action_data.special) {
handler({
result : { type: 'ir.actions.act_window_close' }
});
} else {
var context = _.extend({}, dataset.context, action_data.context || {});
switch(action_data.type) {
case 'object':
return dataset.call(action_data.name, [record_id], [context], handler);
case 'action':
return this.rpc('/base/action/load', { action_id: parseInt(action_data.name, 10) }, handler);
default:
return dataset.exec_workflow(record_id, action_data.name, handler);
}
}
}
};
/**
* Registry for all the main views
*/
@@ -184,7 +241,14 @@ openerp.base.ViewManager = openerp.base.Controller.extend({
openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({
init: function(session, element_id, action, sidebar) {
var dataset = new openerp.base.DataSetSearch(session, action.res_model);
var dataset;
if(!action.res_id) {
dataset = new openerp.base.DataSetSearch(session, action.res_model);
} else {
dataset = new openerp.base.DataSetStatic(session, action.res_model);
dataset.ids = [action.res_id];
dataset.count = 1;
}
this._super(session, element_id, dataset, action.views);
this.action = action;
this.sidebar = sidebar;
@@ -211,14 +275,17 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({
// init search view
var searchview_id = this.action.search_view_id && this.action.search_view_id[0];
var searchview_loaded = this.setup_search_view(
searchview_id || false, search_defaults);
if (searchview_id) {
var searchview_loaded = this.setup_search_view(
searchview_id, search_defaults);
// schedule auto_search
if (searchview_loaded != null && this.action['auto_search']) {
$.when(searchview_loaded, inital_view_loaded)
.then(this.searchview.do_search);
// schedule auto_search
if (this.action['auto_search']) {
$.when(searchview_loaded, inital_view_loaded)
.then(this.searchview.do_search);
}
}
},
stop: function() {
// should be replaced by automatic destruction implemented in BaseWidget
+30 -32
View File
@@ -18,9 +18,6 @@
</div>
</div>
<div id="oe_login" class="login"></div>
<div style="position: absolute; right: 2px; top: 38px;">
<button onclick="QWeb.add_template('base.xml'); $('body').css('background-color', '#FFFF9C'); setTimeout(function() { $('body').css('background-color', '#FFF'); }, 500);">Reload QWEB</button>
</div>
<table border="0" cellpadding="0" cellspacing="0" width="100%" height="100%"
t-att-class="'main_table' + (typeof kitten == 'undefined' ? '' : ' kitten-mode-activated')">
<tr>
@@ -33,10 +30,7 @@
<td valign="top" id="oe_secondary_menu" class="secondary_menu">
</td>
<td valign="top" width="100%" height="100%">
<div id="oe_main" class="main">
<div id="oe_app" class="application"></div>
<div id="oe_sidebar"></div>
</div>
<div id="oe_app" class="oe-application"></div>
</td>
</tr>
<tr>
@@ -135,23 +129,23 @@
<t t-name="ViewManager">
<table class="view-manager-main-table">
<tr>
<td class="view-manager-main-content">
<!-- TODO prefix id with the element_id of the controller t-attf-id="#{prefix}_localid" -->
<div class="oe_vm_switch">
<t t-foreach="views" t-as="view">
<button type="button" t-att-data-view-type="view[1]">
<t t-esc="view[1]"/>
</button>
</t>
</div>
<div t-attf-id="#{prefix}_search"/>
<t t-foreach="views" t-as="view">
<div t-attf-id="#{prefix}_view_#{view[1]}"/>
</t>
</td>
<td class="view-manager-main-sidebar">
</td>
</tr>
<td class="view-manager-main-content">
<!-- TODO prefix id with the element_id of the controller t-attf-id="#{prefix}_localid" -->
<div class="oe_vm_switch">
<t t-foreach="views" t-as="view">
<button type="button" t-att-data-view-type="view[1]">
<t t-esc="view[1]"/>
</button>
</t>
</div>
<div t-attf-id="#{prefix}_search" t-opentag="true"/>
<t t-foreach="views" t-as="view">
<div t-attf-id="#{prefix}_view_#{view[1]}"/>
</t>
</td>
<td class="view-manager-main-sidebar">
</td>
</tr>
</table>
</t>
<table t-name="ListView">
@@ -187,9 +181,11 @@
<tr t-if="options.header">
<th t-if="options.selectable"/>
<t t-foreach="columns" t-as="column">
<th t-if="column.invisible !== '1'">
<th t-if="column.invisible !== '1'" t-att-data-id="column.id"
t-att-class="((options.sortable and column.tag !== 'button') ? 'oe-sortable' : null)">
<t t-if="column.tag !== 'button'">
<t t-esc="column.string"/>
<span t-att-class="(fields_view.sorted.field === column.id) ? ('ui-icon' + (fields_view.sorted.reversed ? ' ui-icon-triangle-1-n' : ' ui-icon-triangle-1-s')) : ''"/>
</t>
</th>
</t>
@@ -217,7 +213,8 @@
<t t-set="is_button" t-value="column.tag === 'button'"/>
<!-- TODO: get correct widget from form -->
<t t-if="!is_button and row['data'][column.id].value">
<t t-esc="row['data'][column.id].value"/>
<t t-set="value" t-value="row['data'][column.id].value"/>
<t t-esc="value instanceof Array ? value[1] : value"/>
</t>
<button type="button" t-att-title="column.help"
t-if="is_button">
@@ -299,7 +296,7 @@
t-att-ondblclick="'console.log(\'' + widget.element_id + '\', openerp.screen.' + widget.element_id + ')'">
<t t-esc="widget.string"/>
<span t-if="widget.help">?</span>
<t t-if="widget.string">:</t>
<t t-if="widget.string and widget.node.tag != 'label'">:</t>
</label>
</t>
<t t-name="FieldChar">
@@ -356,9 +353,7 @@
</div>
</t>
<t t-name="FieldMany2Many">
<div style="background: #ccc; padding: 20px">
Many2Many widget
</div>
<div t-att-id="widget.list_id"></div>
</t>
<t t-name="FieldReference">
<input type="text" t-att-name="widget.name" t-att-id="widget.element_id" t-att-class="'field_' + widget.type" style="width: 100%" placeholder="Widget Reference"/>
@@ -538,8 +533,11 @@
</div>
</t>
<t t-name="ViewManager.sidebar.internal">
<t t-set="the_condition" t-value="sections.length &gt; 0 &amp;&amp; _.detect(sections, function(x)
{return x.elements.length &gt; 0;}) != undefined"/>
<t t-js="d">
d.the_condition = d.sections.length &gt; 0 &amp;&amp; d._.detect(d.sections, function(x) {
return x.elements.length &gt; 0;
}) != undefined
</t>
<t t-if="the_condition">
<a class="toggle-sidebar"></a>
<div t-att-id="element_id" class="sidebar-sub-div">
+1
View File
@@ -7,6 +7,7 @@ $(document).ready(function () {
// views loader stuff
window.openerp.base.data(openerp);
window.openerp.base.views(openerp);
window.openerp.base.list(openerp);
window.openerp.base.form(openerp);
}
});
+19 -19
View File
@@ -26,23 +26,23 @@ $(document).ready(function () {
// views loader stuff
window.openerp.base.data(openerp);
window.openerp.base.views(openerp);
window.openerp.base.form(openerp);
window.openerp.base.list(openerp);
window.openerp.base.form(openerp);
}
});
asyncTest('render selection checkboxes', 2, function () {
var listview = new openerp.base.ListView(
{}, null,
'qunit-fixture', {model: null});
'qunit-fixture', {model: null, ids: [null, null, null], index: 0});
listview.on_loaded(fvg);
listview.do_fill_table([
listview.do_fill_table({records: [
{data: {id: {value: null}}},
{data: {id: {value: null}}},
{data: {id: {value: null}}}
]).then(function () {
]}).then(function () {
ok(are(listview.$element.find('tbody th'),
'.oe-record-selector'));
ok(are(listview.$element.find('tbody th input'),
@@ -53,30 +53,30 @@ $(document).ready(function () {
asyncTest('render no checkbox if selectable=false', 1, function () {
var listview = new openerp.base.ListView(
{}, null,
'qunit-fixture', {model: null}, false,
'qunit-fixture', {model: null, ids: [null, null, null], index: 0}, false,
{selectable: false});
listview.on_loaded(fvg);
listview.do_fill_table([
listview.do_fill_table({records: [
{data: {id: {value: null}}},
{data: {id: {value: null}}},
{data: {id: {value: null}}}
]).then(function () {
]}).then(function () {
equal(listview.$element.find('tbody th').length, 0);
start();
});
});
asyncTest('select a bunch of records', 2, function () {
var listview = new openerp.base.ListView(
{}, null, 'qunit-fixture', {model: null});
{}, null, 'qunit-fixture', {model: null, ids: [1, 2, 3], index: 0});
listview.on_loaded(fvg);
listview.do_fill_table([
listview.do_fill_table({records: [
{data: {id: {value: 1}}},
{data: {id: {value: 2}}},
{data: {id: {value: 3}}}
]).then(function () {
]}).then(function () {
listview.$element.find('tbody th input:eq(2)')
.attr('checked', true);
deepEqual(listview.get_selection(), [3]);
@@ -88,15 +88,15 @@ $(document).ready(function () {
});
asyncTest('render deletion button if list is deletable', 1, function () {
var listview = new openerp.base.ListView(
{}, null, 'qunit-fixture', {model: null});
{}, null, 'qunit-fixture', {model: null, ids: [null, null, null], index: 0});
listview.on_loaded(fvg);
listview.do_fill_table([
listview.do_fill_table({records: [
{data: {id: {value: null}}},
{data: {id: {value: null}}},
{data: {id: {value: null}}}
]).then(function () {
]}).then(function () {
equal(
listview.$element.find('tbody tr td.oe-record-delete button').length,
3);
@@ -109,15 +109,15 @@ $(document).ready(function () {
var listview = new openerp.base.ListView(
{}, null, 'qunit-fixture', {model: null, unlink: function (ids) {
deleted = ids;
}});
}, ids: [1, 2, 3], index: 0});
listview.on_loaded(fvg);
listview.do_fill_table([
listview.do_fill_table({records: [
{data: {id: {value: 1}}},
{data: {id: {value: 2}}},
{data: {id: {value: 3}}}
]).then(function () {
]}).then(function () {
listview.$element.find('tbody td.oe-record-delete:eq(2) button').click();
deepEqual(deleted, [3]);
listview.$element.find('tbody td.oe-record-delete:eq(0) button').click();
@@ -130,15 +130,15 @@ $(document).ready(function () {
var listview = new openerp.base.ListView(
{}, null, 'qunit-fixture', {model: null, unlink: function (ids) {
deleted = ids;
}});
}, ids: [1, 2, 3], index: 0});
listview.on_loaded(fvg);
listview.do_fill_table([
listview.do_fill_table({records: [
{data: {id: {value: 1}}},
{data: {id: {value: 2}}},
{data: {id: {value: 3}}}
]).then(function () {
]}).then(function () {
listview.$element.find('tbody th input:eq(2)')
.attr('checked', true);
listview.$element.find('tbody th input:eq(1)')
+1
View File
@@ -17,6 +17,7 @@
<script src="/base/static/lib/qweb/qweb.js"></script>
<script src="/base/static/src/js/base.js"></script>
<script src="/base/static/src/js/dates.js"></script>
<script src="/base/static/src/js/chrome.js"></script>
<script src="/base/static/src/js/data.js"></script>
<script src="/base/static/src/js/views.js"></script>
+1
View File
@@ -0,0 +1 @@
import controllers
+11
View File
@@ -0,0 +1,11 @@
{
"name": "Base Dashboard",
"version": "2.0",
"depends": ['base'],
"js": [
'static/lib/jquery.dashboard/js/jquery.dashboard.js',
'static/src/js/dashboard.js'
],
"css": ['static/lib/jquery.dashboard/css/dashboardui.css'],
'active': True
}
@@ -0,0 +1 @@
import main
+17
View File
@@ -0,0 +1,17 @@
from base.controllers.main import View, clean_action
import openerpweb
class Dashboard(View):
_cp_path = "/base_dashboard/dashboard"
@openerpweb.jsonrequest
def load(self, req, node_attrs):
action_id = int(node_attrs['name'])
actions = req.session.model('ir.actions.actions')
result = actions.read([action_id],['type'], req.session.context)
if not result:
raise _('Action not found!')
action = req.session.model(result[0]['type']).read([action_id], False, req.session.context)[0]
clean_action(action, req.session)
return {'action': action}
+1
View File
@@ -0,0 +1 @@
content 4 content 4 content 4 content 4 content 4 content 4 content 4 content 4 content 4 content 4 content 4
@@ -0,0 +1,261 @@
.ui-icon {
cursor:default;
}
.widgetheader {
cursor:move;
}
#layout-dialog .selected {
background: no-repeat scroll 0px -51px transparent;
}
#layout-dialog .layoutchoice {
width: 82px;
height: 51px;
float:left;
list-style-type:none;
margin:5px;
padding:0;
}
.selectedcolumn {
border: 3px dashed #aaaaaa;
}
.emptycolumn {
font-size:20px;
font-weight:bold;
color: #aaaaaa;
padding: 5px 5px 5px 5px;
}
.dashboard {
margin-top: 5px;
}
.right {
float:right;
}
.widgetheader {
padding:2px 2px 5px 5px;
}
.ui-icon {
float:left;
}
.widgetcontent {
padding:2px 2px 5px 5px;
}
.widget {
margin-bottom:10px;
}
.column {
float:left;
margin:0 1% -1.5em;
padding:0;
width:47.5%;
}
.layout-a .column {
width:98%;
}
.layout-a .column.second, .layout-a .column.third {
display:none;
}
.layout-aa .column {
width:47.5%;
}
.layout-aa .third {
display:none;
}
.layout-ba .column {
width:68%;
}
.layout-ba .first {
width:27%;
}
.layout-ba .third {
display:none;
}
.layout-ab .column {
width:27%;
}
.layout-ab .first {
width:68%;
}
.layout-ab .third {
display:none;
}
.layout-aaa .column {
width:30.9%;
}
/*body {
margin:0;
color:#333333;
font:12px/1.4 arial,FreeSans,Helvetica,sans-serif;
}*/
.headerlink {
color:#ffffff;
}
.headerlinks {
text-align:right;
margin-right:20px;
line-height:24px;
text-decoration:underline;
font-weight:bold;
}
.headerbox {
height:148px;
}
.ui-widget-overlay {
opacity:0.5;
}
/*.loading {
padding: 50px;
text-align: center;
background-image:loading.gif;
}*/
#layout-dialog ul {
margin:0;
padding:0;
}
#layout-dialog ul li a, #layout-dialog ul li a:link, #layout-dialog ul li a:visited {
border:1px solid #BBBBBB;
display:block;
float:left;
margin:0 1em 1em 0;
outline:medium none;
padding:0.35em;
width:auto;
}
.hidden {
display:none;
}
.controls {
border:1px solid #BBBBBB;
float:none;
margin:0;
padding:4px 0;
}
.controls {
width:100px;
background:none repeat scroll 0 0 #dddddd;
border:1px solid #6A8EB3;
color:#000000;
margin-top:-1px;
padding:4px 0;
position:absolute;
right:0;
z-index:2003;
}
.controls li {
float:none;
margin:0;
padding:0;
list-style-type:none;
margin:0 0 0 0.2em;
width:auto;
position:static;
}
.controls li a {
color:#000000;
font-weight:normal;
float:none;
margin:0;
text-decoration:none;
width:auto;
padding-left:5px;
}
.hiddenmenu {
position:relative;
}
.dialog .categories {
list-style:none outside none;
height: 414px;
}
.dialog .categories li.selected button {
color:#FFFFFF;
font-weight:800;
}
.dialog .categories li button {
background:none repeat scroll 0 0 transparent;
border:medium none;
color:#666666;
font-family:"segoe ui",helvetica,arial,sans-serif;
font-size:0.8em;
padding:0.4em 1.2em;
text-align:left;
width:100%;
}
ul.categories button {
cursor:pointer;
}
.widgetitem {
border:2px none white;
float:left;
font-size:0.77em;
height:142px;
margin:0;
overflow:hidden;
padding:0 20px 0 142px;
width:152px;
}
ol.widgets {
float:left;
list-style:none outside none;
margin:-10px;
padding:0;
width:auto;
}
.dialog .panel-body {
overflow:auto;
padding:10px;
}
.dialog .categories {
background:none repeat scroll 0 0 #FFFFFF;
border-right:1px solid #F0F0F0;
float:left;
height:100%;
list-style:none outside none;
margin:0 1.17em 0 0;
padding:10px 0 0;
width:25%;
}
.dialog .categories li.selected {
background:none repeat scroll 0 0 #6699CC;
color:#FFFFFF;
}
.widgetitem .add-button {
float:left;
margin:81px 0 0 -131px;
width:auto;
}
.widgetitem h3 {
margin:11px 0 0;
padding:0;
}
.widgetitem img {
border:1px solid #999999;
float:left;
margin:10px 0 0 -132px;
}
@@ -0,0 +1,936 @@
/*
* dashboard 1.0
* http://www.gxdeveloperweb.com/dashboard/
*
* Copyright (c) 2010 Mark Machielsen
*
* Dual licensed under the MIT and GPL licenses (same as jQuery):
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
*/
(function($) { // Create closure.
// Constructor for dashboard object.
$.fn.dashboard = function(options) {
// Public properties of dashboard.
var dashboard = {};
var loading;
var widgetDirectoryUrl;
dashboard.layout;
dashboard.element = this;
dashboard.id = this.attr("id");
dashboard.widgets = {};
dashboard.widgetsToAdd = {};
dashboard.widgetCategories = {};
dashboard.initialized = false;
// Public methods
dashboard.serialize = function() {
dashboard.log('entering serialize function',1);
var r = '{"layout": "' + dashboard.layout.id + '", "data" : [';
// add al widgets in the right order
var i=0;
if ($('.' + opts.columnClass).length == 0) dashboard.log(opts.columnClass + ' class not found',5);
$('.' + opts.columnClass).each(function() {
$(this).children().each(function() {
if ($(this).hasClass(opts.widgetClass)) {
if (i > 0) { r+= ','; }
r+= (dashboard.getWidget($(this).attr("id"))).serialize();
i++;
}
});
});
r+= ']}';
return r;
}
dashboard.log = function(msg, level) {
if (level >= opts.debuglevel && typeof console != 'undefined') {
var l = '';
if (level == 1) l = 'INFO';
if (level == 2) l = 'EVENT';
if (level == 3) l = 'WARNING';
if (level == 5) l = 'ERROR';
console.log(l + ' - ' + msg);
}
}
dashboard.setLayout = function(layout) {
if (layout != null) {
dashboard.log('entering setLayout function with layout ' + layout.id,1);
} else {
dashboard.log('entering setLayout function with layout null',1);
}
dashboard.layout = layout;
loading.remove();
if (dashboard.layout != null) {
if (typeof opts.layoutClass != 'undefined') {
this.element.find('.' + opts.layoutClass).addClass(dashboard.layout.classname);
} else {
this.element.html(dashboard.layout.html);
}
}
// make the columns sortable, see http://jqueryui.com/demos/sortable/ for explaination
$('.' + opts.columnClass).sortable({
connectWith: $('.' + opts.columnClass),
opacity: opts.opacity,
handle: '.' + opts.widgetHeaderClass,
over: function(event, ui) {
$(this).addClass("selectedcolumn");
},
out: function(event, ui) {
$(this).removeClass("selectedcolumn");
},
receive: function(event, ui) {
// update the column attribute for the widget
var w = dashboard.getWidget(ui.item.attr("id"));
w.column = getColumnIdentifier($(this).attr("class"));
dashboard.log('dashboardStateChange event thrown for widget ' + w.id,2);
dashboard.element.trigger("dashboardStateChange",{"stateChange":"widgetMoved","widget":w});
dashboard.log('widgetDropped event thrown for widget ' + w.id,2);
w.element.trigger("widgetDropped",{"widget":w});
},
deactivate: function(event, ui) {
// This event is called for each column
dashboard.log('Widget is dropped: check if the column is now empty.',1);
var childLength = $(this).children().length;
if (childLength == 0) {
dashboard.log('adding the empty text to the column',1);
$(this).html('<div class="emptycolumn">' + opts.emptyColumnHtml + '</div>');
} else {
if (childLength == 2) {
// remove the empty column HTML
$(this).find('.emptycolumn').remove();
}
}
},
start: function(event, ui) {
ui.item.find('.' + opts.widgetTitleClass).addClass('noclick');
},
stop: function(event, ui) {
//sorting changed (within one list)
setTimeout(function(){
ui.item.find('.' + opts.widgetTitleClass).removeClass('noclick');
}, 300);
}
});
fixSortableColumns();
// trigger the dashboardLayoutLoaded event
dashboard.log('dashboardLayoutLoaded event thrown',2);
dashboard.element.trigger("dashboardLayoutLoaded");
}
// This is a workaround for the following problem: when I drag a widget from column2 to column1, sometimes the widget is
// moved to column3, which is not visible
function fixSortableColumns() {
dashboard.log('entering fixSortableColumns function',1);
$('.nonsortablecolumn').removeClass('nonsortablecolumn').addClass(opts.columnClass);
$('.' + opts.columnClass).filter(function() {return $(this).css("display") == 'none'}).addClass('nonsortablecolumn').removeClass(opts.columnClass);
}
function getColumnIdentifier(classes) {
dashboard.log('entering getColumnIdentifier function',1);
var r;
var s = classes.split(" ");
for (var i = 0;i < s.length;i++) {
if (s[i].indexOf(opts.columnPrefix) === 0) { r = s[i] };
};
return r.replace(opts.columnPrefix,'');
}
dashboard.loadLayout = function() {
dashboard.log('entering loadLayout function',1);
if (typeof opts.json_data.url != 'undefined' && opts.json_data.url.length > 0) {
// ajax option
dashboard.log('Getting JSON feed : ' + opts.json_data.url,1);
$.getJSON(opts.json_data.url, function(json) {
if (json == null) {
alert('Unable to get json. If you are using chrome: there is an issue with loading json with local files. It works on a server :-)',5);
return;
}
// set the layout
var currentLayout = (typeof dashboard.layout != 'undefined') ? dashboard.layout : getLayout(json.layout);
dashboard.setLayout(currentLayout);
dashboard.loadWidgets(json.data);
});
} else {
// set the layout
var currentLayout = (typeof dashboard.layout != 'undefined') ? dashboard.layout : getLayout(opts.json_data.layout);
dashboard.setLayout(currentLayout);
dashboard.loadWidgets(opts.json_data.data);
}
};
dashboard.addWidget = function(obj, column) {
dashboard.log('entering addWidget function',1);
// add the widget to the column
var wid = obj.id;
// check if the widget is already registered and available in the dom
if (typeof dashboard.widgets[wid] != 'undefined' && $('#' + wid).length > 0) {
var wi = $('#' + wid);
column = dashboard.widgets[wid].column;
// add it to the column
wi.appendTo(column);
} else {
// build the widget
dashboard.log('Applying template : ' + opts.widgetTemplate,1);
if ($('#' + opts.widgetTemplate).length == 0) dashboard.log('Template "' + opts.widgetTemplate + ' not found',5);
var widgetStr = tmpl($('#' + opts.widgetTemplate).html(), obj);
var wi = $(widgetStr);
// add it to the column
wi.appendTo(column);
if(column.find('.emptycolumn').length)
column.find('.emptycolumn').remove();
dashboard.widgets[wid] = widget({
id: wid,
element: wi,
column: obj.column,
url: (typeof obj.url != 'undefined' ? obj.url : null),
editurl: obj.editurl,
title: obj.title,
open: obj.open,
metadata: obj.metadata
});
}
dashboard.log('widgetAdded event thrown for widget ' + wid,2);
dashboard.widgets[wid].element.trigger("widgetAdded", {"widget":dashboard.widgets[wid]});
if (dashboard.initialized) {
dashboard.log('dashboardStateChange event thrown for widget ' + wid,2);
dashboard.element.trigger("dashboardStateChange",{"stateChange":"widgetAdded","widget":wi});
}
}
dashboard.loadWidgets = function(data) {
dashboard.log('entering loadWidgets function',1);
dashboard.element.find('.' + opts.columnClass).empty();
// This is for the manual feed
$(data).each(function() {
var column = this.column;
dashboard.addWidget(this, dashboard.element.find('.' + opts.columnPrefix + column));
}); // end loop for widgets
// check if there are widgets in the temp dashboard which needs to be moved
// this is not the correct place, but otherwise we are too late
// check if there are still widgets in the temp
$('#tempdashboard').find('.' + opts.widgetClass).each(function() {
// append it to the first column
var firstCol = dashboard.element.find('.' + opts.columnClass + ':first');
$(this).appendTo(firstCol);
// set the new column
dashboard.getWidget($(this).attr("id")).column = firstCol.attr("id");
});
$('#tempdashboard').remove();
// add the text to the empty columns
$('.' + opts.columnClass).each(function() {
if ($(this).children().length == 0) {
$(this).html('<div class="emptycolumn">' + opts.emptyColumnHtml + '</div>');
}
});
dashboard.initialized = true;
};
dashboard.init = function() {
dashboard.log('entering init function',1);
// load the widgets as fast as we can. After that add the binding
dashboard.loadLayout();
}
dashboard.getWidget = function(id) {
dashboard.log('entering getWidget function',1);
var wi = dashboard.widgets[id];
if (typeof wi != 'undefined') {
return wi;
} else {
return null;
}
}
// Merge in the caller's options with the defaults.
var opts = $.extend({}, $.fn.dashboard.defaults, options);
var addOpts = $.extend({}, $.fn.dashboard.defaults.addWidgetSettings, options.addWidgetSettings);
var layoutOpts = $.extend({}, $.fn.dashboard.defaults.editLayoutSettings, options.editLayoutSettings);
// Execution 'forks' here and restarts in init(). Tell the user we're busy with a loading.
var loading = $(opts.loadingHtml).appendTo(dashboard.element);
/**
* widget object
* Private sub-class of dashboard
* Constructor starts
*/
function widget(widget) {
dashboard.log('entering widget constructor',1);
// Merge default options with the options defined for this widget.
widget = $.extend({}, $.fn.dashboard.widget.defaults, widget);
// public functions
widget.openContent = function() {
// hide the open link, show the close link
widget.element.find('.widgetOpen').hide();
widget.element.find('.widgetClose').show();
dashboard.log('entering openContent function',1);
widget.open = true;
if (!widget.loaded) {
// load the content in the widget if the state == open
if (this.url != '' && this.url != null && typeof this.url != 'undefined') {
// add the loading
$(opts.loadingHtml).appendTo(widget.element.find('.' + opts.widgetContentClass));
dashboard.log('widgetShow event thrown for widget ' + widget.id,2);
widget.element.trigger("widgetShow", {"widget":widget});
widget.element.find('.' + opts.widgetContentClass).load(this.url, function(response, status, xhr) {
if (status == "error") {
widget.element.find('.' + opts.widgetContentClass).html(opts.widgetNotFoundHtml);
}
widget.loaded = true;
dashboard.log('widgetLoaded event thrown for widget ' + widget.id,2);
widget.element.trigger("widgetLoaded", {"widget":widget});
});
} else {
dashboard.log('widgetShow event thrown for widget ' + widget.id,2);
widget.element.trigger("widgetShow", {"widget":widget});
dashboard.log('widgetLoaded event thrown',2);
widget.element.trigger("widgetLoaded", {"widget":widget});
}
} else {
dashboard.log('widgetShow event thrown for widget ' + widget.id,2);
widget.element.trigger("widgetShow", {"widget":widget});
}
if (dashboard.initialized) {
dashboard.log('dashboardStateChange event thrown for widget ' + widget.id,2);
dashboard.element.trigger("dashboardStateChange",{"stateChange":"widgetOpened","widget":widget});
}
};
widget.refreshContent = function() {
dashboard.log('entering refreshContent function',1);
widget.loaded = false;
if (widget.open) {
widget.openContent();
}
}
widget.setTitle = function(newTitle){
dashboard.log('entering setTitle function',1);
widget.title=newTitle;
widget.element.find('.' + opts.widgetTitleClass).html(newTitle);
if (dashboard.initialized) {
dashboard.log('dashboardStateChange event thrown for widget ' + widget.id,2);
dashboard.element.trigger("dashboardStateChange",{"stateChange":"titleChanged","widget":widget});
}
}
widget.closeContent = function() {
dashboard.log('entering closeContent function',1);
widget.open = false;
dashboard.log('widgetHide event thrown for widget ' + widget.id,2);
widget.element.trigger("widgetHide", {"widget":widget});
// show the open link, hide the close link
widget.element.find('.widgetOpen').show();
widget.element.find('.widgetClose').hide();
dashboard.log('dashboardStateChange event thrown for widget ' + widget.id,2);
dashboard.element.trigger("dashboardStateChange",{"stateChange":"widgetClosed","widget":widget});
};
widget.addMetadataValue = function(name, value) {
dashboard.log('entering addMetadataValue function',1);
if (typeof widget.metadata == 'undefined') {
widget.metadata = {};
}
widget.metadata[name] = value;
dashboard.log('dashboardStateChange event thrown for widget ' + widget.id,2);
dashboard.element.trigger("dashboardStateChange",{"stateChange":"metadataChanged","widget":widget});
};
widget.openMenu = function() {
dashboard.log('entering openMenu function',1);
widget.element.find('.' + opts.menuClass).show();
};
widget.closeMenu = function() {
dashboard.log('entering closeMenu function',1);
widget.element.find('.' + opts.menuClass).hide();
};
widget.remove = function() {
dashboard.log('entering remove function',1);
widget.element.remove();
dashboard.log('widgetDeleted event thrown for widget ' + widget.id,2);
widget.element.trigger('widgetDeleted', {"widget":widget});
dashboard.log('dashboardStateChange event thrown for widget ' + widget.id,2);
dashboard.element.trigger("dashboardStateChange",{"stateChange":"widgetRemoved","widget":widget});
};
widget.serialize = function() {
dashboard.log('entering serialize function',1);
var r = '{"title" : "' + widget.title + '", "id" : "' + widget.id + '", "column" : "' + widget.column + '","editurl" : "' + widget.editurl + '","open" : ' + widget.open + ',"url" : "' + widget.url + '"';
if (typeof widget.metadata != 'undefined') {
r+= ',"metadata":{'
var obj = widget.metadata;
var i=0;
for(var item in obj) {
if (i > 0) { r+= ',' };
// FIXME: support for more than string, eg numbers subobjects
r+= '"' + item + '":"' + obj[item] + '"';
i++;
}
r+= '}'
}
r += '}';
return r;
};
widget.openFullscreen = function() {
dashboard.log('entering openFullscreen function',1);
widget.fullscreen = true;
// create a clone
var clone = widget.element.clone();
// move the dashboard
var temp = $('<div style="display:none" id="tempdashboard_' + dashboard.id + '"></div>');
temp.appendTo($("body"));
dashboard.element.children().appendTo(temp);
// add the clone to the dashboard
var fs = $('<ul id="fullscreen_' + dashboard.id + '"></ul>');
fs.appendTo(dashboard.element);
clone.appendTo(fs);
};
widget.closeFullscreen = function() {
dashboard.log('entering closeFullscreen function',1);
widget.fullscreen = false;
// remove the fullscreen
$('#fullscreen_' + dashboard.id).remove();
// move the content from the tempdashboard back
$('#tempdashboard_' + dashboard.id).children().appendTo(dashboard.element);
$('#tempdashboard_' + dashboard.id).remove();
};
widget.openSettings = function() {
dashboard.log('entering openSettings function',1);
widget.element.find('.' + opts.widgetContentClass).load(widget.editurl);
};
// called when widget is initialized
if (widget.open) {
widget.openContent();
}
widget.initialized = true;
dashboard.log('widgetInitialized event thrown',2);
widget.element.trigger("widgetInitialized", {"widget":widget});
return widget;
};
// FIXME: can this be done easier??
function getLayout(id) {
dashboard.log('entering getLayout function',1);
var r = null;
var first = null;
if (typeof opts.layouts != 'undefined') {
$.each(opts.layouts,function(i, item) {
if (i == 0) { first = item; }
if (item.id == id) {
r = item;
}
});
}
if (r == null) { r = first }
return r;
}
$('#' + dashboard.id + ' .menutrigger').live('click', function() {
dashboard.log('widgetOpenMenu event thrown for widget ' + widget.id,2);
var wi = dashboard.getWidget($(this).closest('.' + opts.widgetClass).attr("id"));
wi.element.trigger('widgetOpenMenu', {"widget":wi});
return false;
});
// add event handlers to the menu
$('#' + dashboard.id + ' .' + opts.widgetFullScreenClass).live('click',function(e) {
// close the menu
dashboard.log('widgetCloseMenu event thrown for widget ' + widget.id,2);
var wi = dashboard.getWidget($(this).closest('.' + opts.widgetClass).attr("id"));
wi.element.trigger('widgetCloseMenu', {"widget":wi});
if (wi.fullscreen) {
dashboard.log('widgetCloseFullScreen event thrown for widget ' + wi.id,2);
wi.element.trigger('widgetCloseFullScreen', {"widget":wi});
} else {
dashboard.log('widgetOpenFullScreen event thrown for widget ' + wi.id,2);
wi.element.trigger('widgetOpenFullScreen', {"widget":wi});
}
return false;
});
$('#' + dashboard.id + ' .controls li').live('click',function(e) {
// close the menu
dashboard.log('widgetCloseMenu event thrown for widget ' + widget.id,2);
var wi = dashboard.getWidget($(this).closest('.' + opts.widgetClass).attr("id"));
wi.element.trigger('widgetCloseMenu', {"widget":wi});
// use the class on the li to determine what action to trigger
dashboard.log($(this).attr('class') + ' event thrown for widget ' + widget.id,2);
var wi = dashboard.getWidget($(this).closest('.' + opts.widgetClass).attr("id"));
wi.element.trigger($(this).attr('class'), {"widget":wi});
return false;
});
// add the menu events (by default triggers are connected in dashboard_jsonfeed)
$('#' + dashboard.id + ' .' + opts.widgetClass).live('widgetCloseMenu',function(e,o) {
dashboard.log("Closing menu " + $(this).attr("id"),1);
o.widget.closeMenu();
});
$('#' + dashboard.id + ' .' + opts.widgetClass).live('widgetOpenMenu',function(e,o) {
dashboard.log("Opening menu " + $(this).attr("id"),1);
o.widget.openMenu();
});
$('#' + dashboard.id + ' .' + opts.widgetClass).live('widgetDelete',function(e,o) {
if (confirm(opts.deleteConfirmMessage)) {
dashboard.log("Removing widget " + $(this).attr("id"),1);
o.widget.remove();
}
});
$('#' + dashboard.id + ' .' + opts.widgetClass).live('widgetRefresh',function(e,o) {
o.widget.refreshContent();
});
$('#' + dashboard.id + ' .' + opts.widgetClass).live('widgetSetTitle',function(event, o) {
o.widget.setTitle(o.title);
});
$('#' + dashboard.id + ' .' + opts.widgetClass).live('widgetClose',function(e,o) {
dashboard.log("Closing widget " + $(this).attr("id"),1);
o.widget.closeContent();
});
$('#' + dashboard.id + ' .' + opts.widgetClass).live('widgetOpen',function(e,o) {
dashboard.log("Opening widget " + $(this).attr("id"),1);
o.widget.openContent();
});
$('#' + dashboard.id + ' .' + opts.widgetClass).live('widgetShow',function() {
$(this).find('.' + opts.widgetContentClass).show();
});
$('#' + dashboard.id + ' .' + opts.widgetClass).live('widgetHide',function() {
$(this).find('.' + opts.widgetContentClass).hide();
});
$('#' + dashboard.id + ' .' + opts.widgetClass).live('widgetAddMetadataValue',function(e,o) {
dashboard.log("Changing metadata for widget " + $(this).attr("id") + ", metadata name: " + o.name + ", value: " + o.value, 1);
o.widget.addMetadataValue(o.name, o.value);
});
// Define a toggle event when clicking at the header
$('#' + dashboard.id + ' .' + opts.widgetTitleClass).live('click',function(e) {
dashboard.log("Click on the header detected for widget " + $(this).attr("id"),1);
if (!$(this).hasClass('noclick')) {
var wi = dashboard.getWidget($(this).closest('.' + opts.widgetClass).attr("id"));
if (wi.open) {
dashboard.log('widgetClose event thrown for widget ' + wi,2);
wi.element.trigger('widgetClose', {"widget":wi});
} else {
dashboard.log('widgetOpen event thrown for widget ' + wi,2);
wi.element.trigger('widgetOpen', {"widget":wi});
}
}
return false;
});
$('#' + dashboard.id + ' .' + opts.widgetHeaderClass).live('mouseover',function () {
$(this).find('.' + opts.iconsClass).removeClass("hidden");
});
$('#' + dashboard.id + ' .' + opts.widgetHeaderClass).live('mouseout', function () {
$(this).find('.' + opts.iconsClass).addClass("hidden");
});
$('body').click(function() {
$('.' + opts.menuClass).hide();
});
$('#' + dashboard.id + ' .' + opts.widgetClass).live('widgetOpenFullScreen',function(e,o) {
o.widget.openFullscreen();
});
$('.' + opts.widgetClass).live('widgetCloseFullScreen',function(e,o) {
o.widget.closeFullscreen();
});
$('#' + dashboard.id + ' .' + opts.widgetClass).live('widgetEdit',function(e,o) {
o.widget.openSettings();
});
if ($('#' + addOpts.dialogId).length == 0) dashboard.log('Unable to find ' + addOpts.dialogId,5);
$('#' + addOpts.dialogId).dialog({
autoOpen: false,
height: 414,
width: 550,
modal: true,
buttons: {
Cancel: function() {
$(this).dialog('close');
}
},
close: function() {
//close
}
});
if ($('#' + layoutOpts.dialogId).length == 0) dashboard.log('Unable to find ' + layoutOpts.dialogId,5);
$('#' + layoutOpts.dialogId).dialog({
autoOpen: false,
height: 300,
width: 600,
modal: true
});
$('.' + layoutOpts.openDialogClass).live('click', function(){
dashboard.log('dashboardOpenLayoutDialog event thrown',2);
dashboard.element.trigger("dashboardOpenLayoutDialog");
return false;
});
dashboard.element.live('dashboardOpenLayoutDialog', function(){
dashboard.log('Opening dialog ' + layoutOpts.dialogId,1);
$('#' + layoutOpts.dialogId).dialog('open');
// add the layout images
var h = $('#' + layoutOpts.dialogId).find('.' + layoutOpts.layoutClass);
h.empty();
if (h.children().length == 0) {
dashboard.log('Number of layouts : ' + opts.layouts.length,1);
$.each(opts.layouts,function(i, item) {
dashboard.log('Applying template : ' + layoutOpts.layoutTemplate,1);
if ($('#' + layoutOpts.layoutTemplate).length == 0) dashboard.log('Template "' + layoutOpts.layoutTemplate + ' not found',5);
h.append(tmpl($('#' + layoutOpts.layoutTemplate).html(), item));
});
}
// set the selected class for the selected layout
$('.' + layoutOpts.selectLayoutClass).removeClass(layoutOpts.selectedLayoutClass);
$('#' + dashboard.layout.id).addClass(layoutOpts.selectedLayoutClass);
bindSelectLayout();
});
dashboard.element.live('dashboardStateChange', function(){
if (typeof opts.stateChangeUrl != 'undefined' && opts.stateChangeUrl != null && opts.stateChangeUrl != '') {
$.ajax({type: 'POST',
url: opts.stateChangeUrl,
data: { dashboard: dashboard.element.attr("id"), settings: dashboard.serialize() },
success: function(data){
if (data == "NOK" || data.indexOf('<response>NOK</response>') != -1){
dashboard.log('dashboardSaveFailed event thrown',2);
dashboard.element.trigger("dashboardSaveFailed");
} else {
dashboard.log('dashboardSuccessfulSaved event thrown',2);
dashboard.element.trigger("dashboardSuccessfulSaved");
}
},
error: function(XMLHttpRequest, textStatus, errorThrown){
dashboard.log('dashboardSaveFailed event thrown',2);
dashboard.element.trigger("dashboardSaveFailed");
},
dataType: "text"
});
}
});
dashboard.element.live('dashboardCloseLayoutDialog', function(){
// close the dialog
$('#' + layoutOpts.dialogId).dialog('close');
});
// FIXME: why doesn't the live construct work in this case
function bindSelectLayout() {
if ($('.' + layoutOpts.selectLayoutClass).length == 0) dashboard.log('Unable to find ' + layoutOpts.selectLayoutClass,5);
$('.' + layoutOpts.selectLayoutClass).bind('click', function(e){
var currentLayout = dashboard.layout;
dashboard.log('dashboardCloseLayoutDialog event thrown',2);
dashboard.element.trigger('dashboardCloseLayoutDialog');
// Now set the new layout
var newLayout = getLayout($(this).attr("id"));
dashboard.layout = newLayout;
// remove the class of the old layout
if (typeof opts.layoutClass != 'undefined') {
dashboard.element.find('.' + opts.layoutClass).removeClass(currentLayout.classname).addClass(newLayout.classname);
fixSortableColumns();
// check if there are widgets in hidden columns, move them to the first column
if ($('.' + opts.columnClass).length == 0) dashboard.log('Unable to find ' + opts.columnClass,5);
dashboard.element.find('.' + opts.columnClass).each(function() {
if ($(this).css("display") == "none") {
// move the widgets to the first column
$(this).children().appendTo(dashboard.element.find('.' + opts.columnClass + ':first'));
}
$('.emptycolumn').remove();
// add the text to the empty columns
$('.' + opts.columnClass).each(function() {
if ($(this).children().length == 0) {
$(this).html('<div class="emptycolumn">' + opts.emptyColumnHtml + '</div>');
}
});
});
} else {
// set the new layout, but first move the dashboard to a temp
var temp = $('<div style="display:none" id="tempdashboard"></div>');
temp.appendTo($("body"));
dashboard.element.children().appendTo(temp);
// reload the dashboard
dashboard.init();
}
// throw an event upon changing the layout.
dashboard.log('dashboardChangeLayout event thrown',2);
dashboard.element.trigger('dashboardLayoutChanged');
});
return false;
}
$('.' + addOpts.selectCategoryClass).live('click', function(){
dashboard.log('addWidgetDialogSelectCategory event thrown',2);
dashboard.element.trigger('addWidgetDialogSelectCategory', {"category":$(this)});
return false;
});
dashboard.element.live('addWidgetDialogSelectCategory', function(e, obj){
// remove the category selection
$('.' + addOpts.selectCategoryClass).removeClass(addOpts.selectedCategoryClass);
// empty the widgets div
$('#' + addOpts.dialogId).find('.' + addOpts.widgetClass).empty();
// select the category
$(obj.category).addClass(addOpts.selectedCategoryClass);
// get the widgets
url = dashboard.widgetCategories[$(obj.category).attr("id")];
dashboard.log('Getting JSON feed : ' + url,1);
$.getJSON(url, {"cache":true}, function(json) {
// load the widgets from the category
if (json.data == 0) dashboard.log('Empty data returned',3);
$.each(json.data, function(i,item){
dashboard.widgetsToAdd[item.id] = item;
dashboard.log('Applying template : ' + addOpts.widgetTemplate,1);
if ($('#' + addOpts.widgetTemplate).length == 0) dashboard.log('Template "' + addOpts.widgetTemplate + ' not found',5);
var html = tmpl($('#' + addOpts.widgetTemplate).html(), item);
$('#' + addOpts.dialogId).find('.' + addOpts.widgetClass).append(html);
});
});
dashboard.log('addWidgetDialogWidgetsLoaded event thrown',2);
dashboard.element.trigger('addWidgetDialogWidgetsLoaded');
});
$('.' + addOpts.addWidgetClass).live('click', function(){
var widget = dashboard.widgetsToAdd[$(this).attr("id").replace('addwidget','')];
dashboard.log('dashboardAddWidget event thrown',2);
dashboard.element.trigger('dashboardAddWidget', {"widget":widget});
dashboard.log('dashboardCloseWidgetDialog event thrown',2);
dashboard.element.trigger('dashboardCloseWidgetDialog');
return false;
});
$('.' + addOpts.openDialogClass).live('click', function(){
dashboard.log('dashboardOpenWidgetDialog event thrown',2);
dashboard.element.trigger('dashboardOpenWidgetDialog');
return false;
});
dashboard.element.live('dashboardCloseWidgetDialog', function(){
// close the dialog
$('#' + addOpts.dialogId).dialog('close');
});
dashboard.element.live('dashboardOpenWidgetDialog', function(){
//remove existing categories/widgets from the DOM, to prevent duplications
$('#' + addOpts.dialogId).find('.' + addOpts.categoryClass).empty();
$('#' + addOpts.dialogId).find('.' + addOpts.widgetClass).empty();
dashboard.log('Opening dialog ' + addOpts.dialogId,1);
$('#' + addOpts.dialogId).dialog('open');
dashboard.log('Getting JSON feed : ' + addOpts.widgetDirectoryUrl,1);
$.getJSON(addOpts.widgetDirectoryUrl, function(json) {
if (json.category == 0) dashboard.log('Empty data returned',3);
$.each(json.category, function(i,item){
// Add the categories to the dashboard
dashboard.widgetCategories[item.id] = item.url;
dashboard.log('Applying template : ' + addOpts.categoryTemplate,1);
if ($('#' + addOpts.categoryTemplate).length == 0) dashboard.log('Template "' + addOpts.categoryTemplate + ' not found',5);
var html = tmpl($('#' + addOpts.categoryTemplate).html(),item);
$('#' + addOpts.dialogId).find('.' + addOpts.categoryClass).append(html);
});
dashboard.log('addWidgetDialogCategoriesLoaded event thrown',2);
dashboard.element.trigger('addWidgetDialogCategoriesLoaded');
dashboard.log('addWidgetDialogSelectCategory event thrown',2);
dashboard.element.trigger('addWidgetDialogSelectCategory', {"category":$('#' + addOpts.dialogId).find('.' + addOpts.categoryClass + '>li:first')});
});
});
return dashboard;
};
// Public static properties of dashboard. Default settings.
$.fn.dashboard.defaults = {
debuglevel:3,
json_data: {},
loadingHtml: '<div class="loading"><img alt="Loading, please wait" src="../themes/default/loading.gif" /><p>Loading...</p></div>',
emptyColumnHtml: 'Drag your widgets here',
widgetTemplate: 'widgettemplate',
columnPrefix: 'column-',
opacity:"0.2",
deleteConfirmMessage: "Are you sure you want to delete this widget?",
widgetNotFoundHtml: "The content of this widget is not available anymore. You may remove this widget.",
columnClass: 'column',
widgetClass: 'widget',
menuClass: 'controls',
widgetContentClass: 'widgetcontent',
widgetTitleClass: 'widgettitle',
widgetHeaderClass: 'widgetheader',
widgetFullScreenClass: 'widgetopenfullscreen',
iconsClass: 'icons',
stateChangeUrl: '',
addWidgetSettings: {
openDialogClass: 'openaddwidgetdialog',
addWidgetClass: 'addwidget',
selectCategoryClass: 'selectcategory',
selectedCategoryClass: 'selected',
categoryClass: 'categories',
widgetClass: 'widgets',
dialogId: 'addwidgetdialog',
categoryTemplate: 'categorytemplate',
widgetTemplate: 'addwidgettemplate'
},
editLayoutSettings: {
dialogId: 'editLayout',
layoutClass: 'layoutselection',
selectLayoutClass: 'layoutchoice',
selectedLayoutClass: 'selected',
openDialogClass: 'editlayout',
layoutTemplate: 'selectlayouttemplate'
}
};
// Default widget settings.
$.fn.dashboard.widget = {
defaults: {
open: true,
fullscreen: false,
loaded: false,
url: '',
metadata: {}
}
};
})(jQuery); // end of closure
// Simple JavaScript Templating
// John Resig - http://ejohn.org/ - MIT Licensed
(function(){
var cache = {};
this.tmpl = function tmpl(str, data){
// Figure out if we're getting a template, or if we need to
// load the template - and be sure to cache the result.
var fn = !/\W/.test(str) ?
cache[str] = cache[str] ||
tmpl(document.getElementById(str).innerHTML) :
// Generate a reusable function that will serve as a template
// generator (and which will be cached).
new Function("obj",
"var p=[],print=function(){p.push.apply(p,arguments);};" +
// Introduce the data as local variables using with(){}
"with(obj){p.push('" +
// Convert the template into pure JavaScript
str
.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',$1,'")
.split("\t").join("');")
.split("%>").join("p.push('")
.split("\r").join("\\'")
+ "');}return p.join('');");
// Provide some basic currying to the user
return data ? fn( data ) : fn;
};
})();
@@ -0,0 +1,85 @@
<script type="text/html" id="categorytemplate">
<li id="<%= id %>" class="selectcategory"><button><%= title %> (<%= amount %>)</button></li>
</script>
<script type="text/html" id="widgettemplate">
<div class="ui-widget ui-corner-all ui-widget-content widget" id="<%= id %>" title="<%= title %>">
<div class="ui-widget-header ui-corner-all widgetheader">
<span class="widgettitle"><%= title %></span>
<span class="right icons hidden">
<span class="ui-icon ui-icon-newwin widgetopenfullscreen"></span>
<span class="ui-icon ui-icon-arrowthickstop-1-s menutrigger"></span>
<span class="hiddenmenu">
<ul style="top: 13px;" class="hidden controls ui-widget-header">
<li class="widgetClose">
<span class="ui-icon ui-icon-minus"></span>
<a class="minimization" href="#">Minimize</a>
</li>
<li class="widgetOpen">
<span class="ui-icon ui-icon-extlink"></span>
<a class="minimization" href="#">Maximize</a>
</li>
<li class="widgetDelete">
<span class="ui-icon ui-icon-close"></span>
<a class="delete" href="#">Delete</a>
</li>
<!-- This could be implemented -->
<!--
<li class="widgetEdit">
<span class="ui-icon ui-icon-tag"></span>
<a class="no_target" href="#">Edit</a>
</li>
-->
<li class="widgetRefresh">
<span class="ui-icon ui-icon-arrowrefresh-1-w"></span>
<a class="no_target" href="#">Refresh</a>
</li>
</ul>
</span>
</span>
</div>
<div class="widgetcontent">
</div>
</div>
</script>
<script type="text/html" id="selectlayouttemplate">
<li class="layoutchoice" id="<%= id %>" style="background-image: url('<%= image %>')"></li>
</script>
<script type="text/html" id="addwidgettemplate">
<li class="widgetitem">
<img src="<%= image %>" alt="" height="60" width="120">
<div class="add-button">
<input class="macro-button-add addwidget" id="addwidget<%= id %>" value="Add it Now" type="button"><br>
<input class="macro-hidden-uri" value="<%= url %>" type="hidden">
</div>
<!-- // .add-button -->
<h3><a href=""><%= title %></a></h3>
<p>By <%= creator %></p>
<p><%= description %></p>
</li>
</script>
<div class="dialog" id="addwidgetdialog" title="Widget Directory">
<ul class="categories">
</ul>
<div class="panel-body">
<ol id="category-all" class="widgets">
</ol>
</div>
</div>
<div class="dialog" id="editLayout" title="Edit layout">
<div class="panel-body" id="layout-dialog">
<p><strong>Choose dashboard layout</strong></p>
<ul class="layoutselection">
</ul>
</div>
</div>
@@ -0,0 +1,104 @@
openerp.base_dashboard = function(openerp){
QWeb.add_template('/base_dashboard/static/src/xml/base_dashboard.xml');
openerp.base.form.Board = openerp.base.form.Widget.extend({
init: function(view, node) {
this._super(view, node);
this.template = "Board";
},
start: function() {
this._super.apply(this, arguments);
this.$element.html(QWeb.render(this.template));
},
render: function() {
var self = this;
jQuery('body').append(
jQuery('<div>', {'id': 'dashboard_template'}).load('/base_dashboard/static/src/dashboard_template.html',self.on_loaded).hide()
)
},
on_loaded: function() {
var children = this.node.children;
var board = jQuery('#dashboard').dashboard({
layoutClass:'layout'
});
board.init();
for(var ch = 0; ch < children.length; ch++) {
var ch_widgets = children[ch].children;
for(var chld = 0; chld < ch_widgets.length; chld++) {
var widget_type = ch_widgets[chld].tag;
var child_index = widget_type == 'action' ? chld : ch;
var widget = new (openerp.base.form.widgets.get_object(widget_type)) (this.view, ch_widgets[chld], board, child_index);
widget.start();
}
}
}
});
openerp.base.form.Action = openerp.base.form.Widget.extend({
init: function(view, node, board, child_index) {
this._super(view, node, board, child_index);
this.board = board;
this.child_index = child_index;
},
start: function() {
this._super.apply(this, arguments);
this.rpc('/base_dashboard/dashboard/load',{
node_attrs: this.node.attrs
},
this.on_load_action);
},
on_load_action: function(result) {
var action = result.action;
action.search_view = false;
action.no_sidebar = true;
action.search_view_id = false;
var node_attrs = this.node.attrs;
var get_column = ['first', 'second', 'third'];
var board_element = this.board.element.find('[id=column-'+get_column[this.child_index]+']');
this.board.addWidget({
'id': node_attrs.name,
'title': node_attrs.string,
}, board_element);
var content_id = node_attrs.name+'-widgetcontent';
this.board.getWidget(node_attrs.name).element.find('.widgetcontent').attr('id',content_id)
action_manager = new openerp.base.ActionManager(this.session, content_id);
action_manager.start();
this.board.getWidget(node_attrs.name).url = action_manager.do_action(action);
}
})
openerp.base.form.Vpaned = openerp.base.form.Widget.extend({
init: function(view, node, board, child_index) {
this._super(view, node, board, child_index);
this.board = board;
this.child_index = child_index;
},
start: function() {
this._super.apply(this, arguments);
var children = this.node.children;
for(var chld=0; chld<children.length; chld++) {
var ch_widget = children[chld].children;
for(var ch=0; ch<ch_widget.length; ch++) {
var widget_type = ch_widget[ch].tag;
var widget = new (openerp.base.form.widgets.get_object(widget_type)) (this.view, ch_widget[ch], this.board, this.child_index);
widget.start();
}
}
},
})
openerp.base.form.widgets.add('hpaned', 'openerp.base.form.Board');
openerp.base.form.widgets.add('vpaned', 'openerp.base.form.Vpaned');
openerp.base.form.widgets.add('action', 'openerp.base.form.Action');
}
@@ -0,0 +1,12 @@
<template>
<t t-name="Board">
<div id="dashboard" class="dashboard">
<!-- this HTML covers all layouts. The 5 different layouts are handled by setting another layout classname -->
<div class="layout">
<div id="column-first" class="column first column-first"></div>
<div id="column-second" class="column second column-second"></div>
<div id="column-third" class="column third column-third"></div>
</div>
</div>
</t>
</template>
+14 -13
View File
@@ -55,7 +55,7 @@ Events
Event triggered after a user asked for a search. The search view
fires this event after collecting all input data (contexts, domains
and group_by contexts). Note that the search view does *not* merge
those (or therwise evaluate them), they are returned as provided by
those (or otherwise evaluate them), they are returned as provided by
the various inputs within the view.
``on_clear``
@@ -71,7 +71,7 @@ An important concept in the search view is that of input. It is both
an informal protocol and an abstract type that can be inherited from.
Inputs are widgets which can contain user data (a char widget for
instance, or a selection box). They are able of action and of
instance, or a selection box). They are capable of action and of
reaction:
.. _views-search-registration:
@@ -93,9 +93,9 @@ reaction:
* Return a context (an object), this is the "normal" response if the
input holds a value.
* Return a falsy value (generally ``null``). This value indicates
the input does not contain any value and will not take part in the
research.
* Return a value that evaluates as false (generally ``null``). This
value indicates the input does not contain any value and will not
affect the results of the search.
* Raise :js:class:`openerp.base.search.Invalid` to indicate that it
holds a value but this value can not be used in the search
@@ -104,7 +104,7 @@ reaction:
the search process.
:js:class:`~openerp.base.search.Invalid` takes three mandatory
arguments: an indentifier (a name for instance), the invalid value
arguments: an identifier (a name for instance), the invalid value,
and a validation message indicating the issue.
``get_domain``
@@ -114,11 +114,11 @@ reaction:
The :js:class:`openerp.base.search.Input` type implements registration
on its own, but its implementations of ``get_context`` and
``get_domain`` simply raise errors and *have* to be overridden.
``get_domain`` simply raise errors and *must* be overridden.
One last action is for filters, as an activation order has to be kept
on them for some controls (establish the correct grouping sequence for
instance).
on them for some controls (to establish the correct grouping sequence,
for instance).
To that end, filters can call
:js:func:`openerp.base.Search.do_toggle_filter`, providing themselves
@@ -137,7 +137,7 @@ Life cycle
The search view has a pretty simple and linear life cycle, in three main steps:
:js:class:`init <openerp.base.SearchView>`
:js:class:`~openerp.base.SearchView.init`
Nothing interesting happens here
@@ -211,7 +211,7 @@ called in this order:
Gives the widget the opportunity to unbind its events, remove itself
from the DOM and perform any other cleanup task it may have.
Event if the widget does not do anything itself, it is responsible
Even if the widget does not do anything itself, it is responsible
for shutting down its children.
An abstract type is available and can be inherited from, to simplify
@@ -232,7 +232,8 @@ abstract types, used to implement input widgets:
<views-search-registration>`.
If inherited from, descendant classes should not call its
implementations of ``get_context`` and ``get_domain``.
implementations of :js:func:`~openerp.base.search.Input.get_context`
and :js:func:`~openerp.base.search.Input.get_domain`.
* :js:class:`openerp.base.search.Field` is used to implement more
"field" widgets (which allow the user to input potentially complex
@@ -251,7 +252,7 @@ abstract types, used to implement input widgets:
:js:func:`~openerp.base.search.Widget.make_id`.
* It sets up a basic (overridable)
:js:attr:`~opererp.base.search.Field.template` attribute, combined
:js:attr:`~openerp.base.search.Field.template` attribute, combined
with the previous tasks, this makes subclasses of
:js:class:`~openerp.base.search.Field` render themselves "for
free".
+3 -3
View File
@@ -31,7 +31,7 @@ Guides and main documentation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The meat and most important part of all documentation. Should be
written in plain english, using reStructuredText_ and taking advantage
written in plain English, using reStructuredText_ and taking advantage
of `Sphinx's extensions`_, especially `cross-references`_.
Python API Documentation
@@ -65,7 +65,7 @@ RST, using Sphinx's `Python domain`_ [#]_:
and ``__new__``), using the `info fields`_ as well.
* Attributes (class and instance) should be documented in their
class's docstrin via the ``.. attribute::`` directiveg, following
class's docstring via the ``.. attribute::`` directive, following
the class's own documentation.
* The relation between modules and module-level attributes is similar:
@@ -220,7 +220,7 @@ Roadmap
Release notes
+++++++++++++
.. [#] because Python is the default domain, the ``py:`` markup prefix
.. [#] Because Python is the default domain, the ``py:`` markup prefix
is optional and should be left out.
.. [#] Resig's Class still uses prototypes under the hood, it doesn't
+62
View File
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
# Copyright (C) 2010 OpenERP s.a. (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
""" We assume that, just like the OpenERP server, the python part of the client
doesn't need to handle timezone-aware datetimes, this could be changed in the future. """
import datetime
DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S"
DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % (
DEFAULT_SERVER_DATE_FORMAT,
DEFAULT_SERVER_TIME_FORMAT)
def parse_datetime(str):
if not str:
return str
return datetime.datetime.strptime(str, DEFAULT_SERVER_DATETIME_FORMAT)
def parse_date(str):
if not str:
return str
return datetime.datetime.strptime(str, DEFAULT_SERVER_DATE_FORMAT).date()
def parse_time(str):
if not str:
return str
return datetime.datetime.strptime(str, DEFAULT_SERVER_TIME_FORMAT).time()
def format_datetime(obj):
if not obj:
return False
return obj.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
def format_date(obj):
if not obj:
return False
return obj.strftime(DEFAULT_SERVER_DATE_FORMAT)
def format_time(obj):
if not obj:
return False
return obj.strftime(DEFAULT_SERVER_TIME_FORMAT)
-8
View File
@@ -80,10 +80,6 @@ class Domain(object):
def get_domain_string(self):
""" Retrieves the domain string linked to this non-literal domain in
the provided session.
:param session: the OpenERP Session used to store the domain string in
the first place.
:type session: openerpweb.openerpweb.OpenERPSession
"""
return self.session.domains_store[self.key]
@@ -129,10 +125,6 @@ class Context(object):
def get_context_string(self):
""" Retrieves the context string linked to this non-literal context in
the provided session.
:param session: the OpenERP Session used to store the context string in
the first place.
:type session: openerpweb.openerpweb.OpenERPSession
"""
return self.session.contexts_store[self.key]
+40 -11
View File
@@ -10,14 +10,12 @@ import time
import traceback
import uuid
import xmlrpclib
import pytz
import cherrypy
import cherrypy.lib.static
import simplejson
import nonliterals
import xmlrpctimeout
import logging
#-----------------------------------------------------------
@@ -94,7 +92,7 @@ class OpenERPSession(object):
self.client_timezone = False
def proxy(self, service):
s = xmlrpctimeout.TimeoutServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service), timeout=5)
s = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service))
return s
def bind(self, db, uid, password):
@@ -116,6 +114,12 @@ class OpenERPSession(object):
r = self.proxy('object').execute(self._db, self._uid, self._password, model, func, *l, **d)
return r
def exec_workflow(self, model, id, signal):
if not (self._db and self._uid and self._password):
raise OpenERPUnboundException()
r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id)
return r
def model(self, model):
""" Get an RPC proxy for the object ``model``, bound to this session.
@@ -136,8 +140,9 @@ class OpenERPSession(object):
self.context = self.model('res.users').context_get(self.context)
self.client_timezone = self.context.get("tz", False)
if self.client_timezone:
self.remote_timezone = self.execute('common', 'timezone_get')
# invalid code, anyway we decided the server will be in UTC
#if self.client_timezone:
# self.remote_timezone = self.execute('common', 'timezone_get')
self._locale = self.context.get('lang','en_US')
lang_ids = self.execute('res.lang','search', [('code', '=', self._locale)])
@@ -289,13 +294,34 @@ class JsonRequest(object):
* the json string is passed as a form parameter named "request"
* method is currently ignored
Sucessful request:
--> {"jsonrpc": "2.0", "method": "call", "params": {"session_id": "SID", "context": {}, "arg1": "val1" }, "id": null}
<-- {"jsonrpc": "2.0", "result": { "res1": "val1" }, "id": null}
Sucessful request::
Request producing a error:
--> {"jsonrpc": "2.0", "method": "call", "params": {"session_id": "SID", "context": {}, "arg1": "val1" }, "id": null}
<-- {"jsonrpc": "2.0", "error": {"code": 1, "message": "End user error message.", "data": {"code": "codestring", "debug": "traceback" } }, "id": null}
--> {"jsonrpc": "2.0",
"method": "call",
"params": {"session_id": "SID",
"context": {},
"arg1": "val1" },
"id": null}
<-- {"jsonrpc": "2.0",
"result": { "res1": "val1" },
"id": null}
Request producing a error::
--> {"jsonrpc": "2.0",
"method": "call",
"params": {"session_id": "SID",
"context": {},
"arg1": "val1" },
"id": null}
<-- {"jsonrpc": "2.0",
"error": {"code": 1,
"message": "End user error message.",
"data": {"code": "codestring",
"debug": "traceback" } },
"id": null}
"""
@@ -471,6 +497,9 @@ class Root(object):
default.exposed = True
def main(argv):
# change the timezone of the program to the OpenERP server's assumed timezone
os.environ["TZ"] = "UTC"
# Parse config
op = optparse.OptionParser()
op.add_option("-p", "--port", dest="socket_port", help="listening port", metavar="NUMBER", default=8002)
-30
View File
@@ -1,30 +0,0 @@
import xmlrpclib
import httplib
class TimeoutHTTPConnection(httplib.HTTPConnection):
def connect(self):
httplib.HTTPConnection.connect(self)
self.sock.settimeout(self.timeout)
class TimeoutHTTP(httplib.HTTP):
_connection_class = TimeoutHTTPConnection
def set_timeout(self, timeout):
self._conn.timeout = timeout
class TimeoutTransport(xmlrpclib.Transport):
def __init__(self, timeout=10, *l, **kw):
xmlrpclib.Transport.__init__(self,*l,**kw)
self.timeout=timeout
def make_connection(self, host):
conn = TimeoutHTTP(host)
conn.set_timeout(self.timeout)
return conn
class TimeoutServerProxy(xmlrpclib.ServerProxy):
def __init__(self,uri,timeout=10,*l,**kw):
kw['transport']=TimeoutTransport(timeout=timeout, use_datetime=kw.get('use_datetime',0))
xmlrpclib.ServerProxy.__init__(self,uri,*l,**kw)
if __name__ == "__main__":
s=TimeoutServerProxy('http://127.0.0.1:9090',timeout=2)
s.dummy()