[MERGE] latest trunk-proto61.

bzr revid: kch@tinyerp.com-20110518055511-zg1wuyfv10xalnxm
This commit is contained in:
Kunal Chavda (OpenERP) 2011-05-18 11:25:11 +05:30
commit cc41897bd2
56 changed files with 4987 additions and 1030 deletions

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import base64
import glob, os
import pprint
from xml.etree import ElementTree
@ -346,12 +347,21 @@ 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 read(self, request, model, ids, fields=False):
return self.do_search_read(request, model, ids, fields)
@openerpweb.jsonrequest
def get(self, request, model, ids, fields=False):
@ -376,7 +386,7 @@ class DataSet(openerpweb.Controller):
:rtype: list
"""
Model = request.session.model(model)
records = Model.read(ids, fields)
records = Model.read(ids, fields, request.context)
record_map = dict((record['id'], record) for record in records)
@ -409,11 +419,29 @@ 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)
r = m.default_get(fields, context)
return {'result': r}
@openerpweb.jsonrequest
def name_search(self, req, model, search_str, domain=[], context={}):
m = req.session.model(model)
r = m.name_search(search_str+'%', domain, '=ilike', context)
return {'result': r}
class DataGroup(openerpweb.Controller):
_cp_path = "/base/group"
@openerpweb.jsonrequest
def read(self, request, model, group_by_fields, domain=None, context=None):
Model = request.session.model(model)
return Model.read_group(domain or False, False, group_by_fields, 0, False, context or False)
class View(openerpweb.Controller):
def fields_view_get(self, request, model, view_id, view_type,
@ -527,6 +555,21 @@ class FormView(View):
fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
return {'fields_view': fields_view}
@openerpweb.httprequest
def image(self, request, session_id, model, id, field):
cherrypy.response.headers['Content-Type'] = 'image/png'
Model = request.session.model(model)
try:
if not id:
res = Model.default_get([field], request.context).get(field, '')
else:
res = Model.read([id], [field], request.context)[0].get(field, '')
return base64.decodestring(res)
except:
return self.placeholder()
def placeholder(self):
return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
class ListView(View):
_cp_path = "/base/listview"
@ -551,11 +594,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 +616,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 +675,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}

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);};}());

View File

@ -0,0 +1,480 @@
/*
http://www.JSON.org/json2.js
2011-02-23
Public Domain.
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
See http://www.JSON.org/js.html
This code should be minified before deployment.
See http://javascript.crockford.com/jsmin.html
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
NOT CONTROL.
This file creates a global JSON object containing two methods: stringify
and parse.
JSON.stringify(value, replacer, space)
value any JavaScript value, usually an object or array.
replacer an optional parameter that determines how object
values are stringified for objects. It can be a
function or an array of strings.
space an optional parameter that specifies the indentation
of nested structures. If it is omitted, the text will
be packed without extra whitespace. If it is a number,
it will specify the number of spaces to indent at each
level. If it is a string (such as '\t' or '&nbsp;'),
it contains the characters used to indent at each level.
This method produces a JSON text from a JavaScript value.
When an object value is found, if the object contains a toJSON
method, its toJSON method will be called and the result will be
stringified. A toJSON method does not serialize: it returns the
value represented by the name/value pair that should be serialized,
or undefined if nothing should be serialized. The toJSON method
will be passed the key associated with the value, and this will be
bound to the value
For example, this would serialize Dates as ISO strings.
Date.prototype.toJSON = function (key) {
function f(n) {
// Format integers to have at least two digits.
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';
};
You can provide an optional replacer method. It will be passed the
key and value of each member, with this bound to the containing
object. The value that is returned from your method will be
serialized. If your method returns undefined, then the member will
be excluded from the serialization.
If the replacer parameter is an array of strings, then it will be
used to select the members to be serialized. It filters the results
such that only members with keys listed in the replacer array are
stringified.
Values that do not have JSON representations, such as undefined or
functions, will not be serialized. Such values in objects will be
dropped; in arrays they will be replaced with null. You can use
a replacer function to replace those with JSON values.
JSON.stringify(undefined) returns undefined.
The optional space parameter produces a stringification of the
value that is filled with line breaks and indentation to make it
easier to read.
If the space parameter is a non-empty string, then that string will
be used for indentation. If the space parameter is a number, then
the indentation will be that many spaces.
Example:
text = JSON.stringify(['e', {pluribus: 'unum'}]);
// text is '["e",{"pluribus":"unum"}]'
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
text = JSON.stringify([new Date()], function (key, value) {
return this[key] instanceof Date ?
'Date(' + this[key] + ')' : value;
});
// text is '["Date(---current time---)"]'
JSON.parse(text, reviver)
This method parses a JSON text to produce an object or array.
It can throw a SyntaxError exception.
The optional reviver parameter is a function that can filter and
transform the results. It receives each of the keys and values,
and its return value is used instead of the original value.
If it returns what it received, then the structure is not modified.
If it returns undefined then the member is deleted.
Example:
// Parse the text. Values that look like ISO date strings will
// be converted to Date objects.
myData = JSON.parse(text, function (key, value) {
var a;
if (typeof value === 'string') {
a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
if (a) {
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+a[5], +a[6]));
}
}
return value;
});
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
var d;
if (typeof value === 'string' &&
value.slice(0, 5) === 'Date(' &&
value.slice(-1) === ')') {
d = new Date(value.slice(5, -1));
if (d) {
return d;
}
}
return value;
});
This is a reference implementation. You are free to copy, modify, or
redistribute.
*/
/*jslint evil: true, strict: false, regexp: false */
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
lastIndex, length, parse, prototype, push, replace, slice, stringify,
test, toJSON, toString, valueOf
*/
// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.
var JSON;
if (!JSON) {
JSON = {};
}
(function () {
"use strict";
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
if (typeof Date.prototype.toJSON !== 'function') {
Date.prototype.toJSON = function (key) {
return isFinite(this.valueOf()) ?
this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z' : null;
};
String.prototype.toJSON =
Number.prototype.toJSON =
Boolean.prototype.toJSON = function (key) {
return this.valueOf();
};
}
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
gap,
indent,
meta = { // table of character substitutions
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
},
rep;
function quote(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
escapable.lastIndex = 0;
return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
var c = meta[a];
return typeof c === 'string' ? c :
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"' : '"' + string + '"';
}
function str(key, holder) {
// Produce a string from holder[key].
var i, // The loop counter.
k, // The member key.
v, // The member value.
length,
mind = gap,
partial,
value = holder[key];
// If the value has a toJSON method, call it to obtain a replacement value.
if (value && typeof value === 'object' &&
typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.
if (typeof rep === 'function') {
value = rep.call(holder, key, value);
}
// What happens next depends on the value's type.
switch (typeof value) {
case 'string':
return quote(value);
case 'number':
// JSON numbers must be finite. Encode non-finite numbers as null.
return isFinite(value) ? String(value) : 'null';
case 'boolean':
case 'null':
// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.
return String(value);
// If the type is 'object', we might be dealing with an object or an array or
// null.
case 'object':
// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.
if (!value) {
return 'null';
}
// Make an array to hold the partial results of stringifying this object value.
gap += indent;
partial = [];
// Is the value an array?
if (Object.prototype.toString.apply(value) === '[object Array]') {
// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.
length = value.length;
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value) || 'null';
}
// Join all of the elements together, separated with commas, and wrap them in
// brackets.
v = partial.length === 0 ? '[]' : gap ?
'[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' :
'[' + partial.join(',') + ']';
gap = mind;
return v;
}
// If the replacer is an array, use it to select the members to be stringified.
if (rep && typeof rep === 'object') {
length = rep.length;
for (i = 0; i < length; i += 1) {
if (typeof rep[i] === 'string') {
k = rep[i];
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
} else {
// Otherwise, iterate through all of the keys in the object.
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
}
// Join all of the member texts together, separated with commas,
// and wrap them in braces.
v = partial.length === 0 ? '{}' : gap ?
'{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' :
'{' + partial.join(',') + '}';
gap = mind;
return v;
}
}
// If the JSON object does not yet have a stringify method, give it one.
if (typeof JSON.stringify !== 'function') {
JSON.stringify = function (value, replacer, space) {
// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.
var i;
gap = '';
indent = '';
// If the space parameter is a number, make an indent string containing that
// many spaces.
if (typeof space === 'number') {
for (i = 0; i < space; i += 1) {
indent += ' ';
}
// If the space parameter is a string, it will be used as the indent string.
} else if (typeof space === 'string') {
indent = space;
}
// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.
rep = replacer;
if (replacer && typeof replacer !== 'function' &&
(typeof replacer !== 'object' ||
typeof replacer.length !== 'number')) {
throw new Error('JSON.stringify');
}
// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.
return str('', {'': value});
};
}
// If the JSON object does not yet have a parse method, give it one.
if (typeof JSON.parse !== 'function') {
JSON.parse = function (text, reviver) {
// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.
var j;
function walk(holder, key) {
// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.
var k, v, value = holder[key];
if (value && typeof value === 'object') {
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}
// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.
text = String(text);
cx.lastIndex = 0;
if (cx.test(text)) {
text = text.replace(cx, function (a) {
return '\\u' +
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
});
}
// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.
// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
if (/^[\],:{}\s]*$/
.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
.replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.
j = eval('(' + text + ')');
// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.
return typeof reviver === 'function' ?
walk({'': j}, '') : j;
}
// If the text is not JSON parseable, then a SyntaxError is thrown.
throw new SyntaxError('JSON.parse');
};
}
}());

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,32 @@
<templates>
<t t-name="jquery-extend">
<ul><li>one</li></ul>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul" t-operation="append"><li>3</li></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul li:first-child" t-operation="replace"><li>2</li></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul" t-operation="prepend"><li>1</li></t>
<t t-jquery="ul" t-operation="before"><hr/></t>
<t t-jquery="ul" t-operation="after"><hr/></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul">
this.attr('class', 'main');
</t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="hr:eq(1)" t-operation="replace"><footer></footer></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="footer" t-operation="inner"><b>END</b></t>
</t>
<t t-name="jquery-extend-clone" t-extend="jquery-extend">
<t t-jquery="ul" t-operation="append"><li>CLONED TEMPLATE</li></t>
</t>
</templates>

View File

@ -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>

View File

@ -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>

View File

@ -5,9 +5,11 @@
<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="/base/static/lib/jquery/jquery-1.5.2.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 +195,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,21 +221,39 @@
"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");
});
module("Template inheritance tests", {
setup: function () {
QWeb.add_template('qweb-test-extend.xml');
},
teardown: function () {
QWeb.templates = [];
QWeb.tag = {};
QWeb.att = {};
}
});
test("jQuery extend", function () {
equals(render('jquery-extend', {}), '<hr/><ul class="main"><li>1</li><li>2</li><li>3</li></ul><footer><b>END</b></footer>',
"Extend template with jQuery");
equals(render('jquery-extend-clone', {}), '<ul><li>one</li><li>CLONED TEMPLATE</li></ul>',
"Clone template");
});
});
</script>

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).

View File

@ -0,0 +1,691 @@
// TODO: trim support
// TODO: line number -> https://bugzilla.mozilla.org/show_bug.cgi?id=618650
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;
},
xml_node_to_string : function(node, childs_only) {
if (childs_only) {
var childs = node.childNodes, r = [];
for (var i = 0, ilen = childs.length; i < ilen; i++) {
r.push(this.xml_node_to_string(childs[i]));
}
return r.join('');
} else {
if (node.xml !== undefined) {
return node.xml;
} else {
return (new XMLSerializer()).serializeToString(node);
}
}
},
xpath_selectNodes : function(node, expr) {
if (node.selectNodes !== undefined) {
return node.selectNodes(expr);
} else {
var xpath = new XPathEvaluator();
try {
var result = xpath.evaluate(expr, node, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
} catch(error) {
this.exception("Error with XPath expression '" + expr + "' : " + error);
}
var r = [];
if (result != null) {
var element;
while (element = result.iterateNext()) {
r.push(element);
}
}
return r
}
},
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['__content__'] = 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.extend_templates = {};
this.dict = {};
this.tools = QWeb2.tools;
this.jQuery = window.jQuery;
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');
var extend = node.getAttribute(this.prefix + '-extend');
if (name && extend) {
// Clone template and extend it
if (!this.templates[extend]) {
return this.tools.exception("Can't clone undefined template " + extend);
}
this.templates[name] = this.templates[extend].cloneNode(true);
extend = name;
name = undefined;
}
if (name) {
this.templates[name] = node;
} else if (extend) {
delete(this.compiled_templates[extend]);
if (this.extend_templates[extend]) {
this.extend_templates[extend].push(node);
} else {
this.extend_templates[extend] = [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 " /* '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('');";
},
render : function(template, dict) {
if (this.debug && window['console'] !== undefined) {
console.time("QWeb render template " + template);
}
var r = this._render(template, dict);
if (this.debug && window['console'] !== undefined) {
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 ext;
if (ext = this.extend_templates[template]) {
var extend_node;
while (extend_node = ext.shift()) {
this.extend(template, extend_node);
}
}
var code = this.compile(this.templates[template]), tcompiled;
try {
tcompiled = new Function(['dict'], code);
} catch (error) {
if (this.debug && window.console) {
console.log(code);
}
this.tools.exception("Error evaluating template: " + error, { template: name });
}
if (!tcompiled) {
this.tools.exception("Error evaluating template: (IE?)" + error, { template: name });
}
this.compiled_templates[template] = tcompiled;
return this.render(template, dict);
} else {
return this.tools.exception("Template '" + template + "' not found");
}
},
extend : function(template, extend_node) {
if (!this.jQuery) {
return this.tools.exception("Can't extend template " + template + " without jQuery");
}
for (var i = 0, ilen = extend_node.childNodes.length; i < ilen; i++) {
var child = extend_node.childNodes[i];
if (child.nodeType === 1) {
var xpath = child.getAttribute(this.prefix + '-xpath'),
jquery = child.getAttribute(this.prefix + '-jquery'),
operation = child.getAttribute(this.prefix + '-operation'),
target,
error_msg = "Error while extending template '" + template;
if (jquery) {
target = this.jQuery(jquery, this.templates[template]);
} else if (xpath) {
// NOTE: due to the XPath implementation, extending a template will only work once
// when using XPath because XPathResult won't match objects with other constructor than 'Element'
// As jQuery will create HTMLElements, xpath will ignore them then causing problems when a
// template has already been extended. XPath selectors will probably be removed in QWeb.
target = this.jQuery(this.tools.xpath_selectNodes(this.templates[template], xpath));
} else {
this.tools.exception(error_msg + "No expression given");
}
error_msg += "' (expression='" + (jquery || xpath) + "') : ";
var inner = this.tools.xml_node_to_string(child, true);
if (operation) {
var allowed_operations = "append,prepend,before,after,replace,inner".split(',');
if (this.tools.arrayIndexOf(allowed_operations, operation) == -1) {
this.tools.exception(error_msg + "Invalid operation : '" + operation + "'");
}
operation = {'replace' : 'replaceWith', 'inner' : 'html'}[operation] || operation;
target[operation]($(inner));
} else {
try {
var f = new Function(['$'], inner);
} catch(error) {
return this.tools.exception("Parse " + error_msg + error);
}
try {
f.apply(target, [this.jQuery]);
} catch(error) {
return this.tools.exception("Runtime " + error_msg + error);
}
}
}
}
}
});
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 (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;
})();

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,22 +19,30 @@
<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/lib/json/json2.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/views.js"></script>
<script type="text/javascript" src="/base/static/src/js/form.js"></script>
<script type="text/javascript" src="/base/static/src/js/list.js"></script>
<script type="text/javascript" src="/base/static/src/js/search.js"></script>
<script type="text/javascript" src="/base/static/src/js/views.js"></script>
<script type="text/javascript" src="/base/static/src/js/m2o.js"></script>
<link rel="stylesheet" type="text/css" media="screen" href="/base/static/lib/jquery.ui/css/smoothness/jquery-ui-1.8.9.custom.css" />
<link rel="stylesheet" type="text/css" media="screen" href="/base/static/lib/jquery.ui.notify/css/ui.notify.css" />
<link rel="stylesheet" type="text/css" href="/base/static/lib/jquery.superfish/css/superfish.css" media="screen">
<link rel="stylesheet" href="/base/static/src/css/base.css" type="text/css"/>
<!--[if lte IE 7]>
<link rel="stylesheet" href="/base/static/src/css/base-ie7.css" type="text/css"/>
<![endif]-->
<script type="text/javascript">
$(function() {
QWeb = window.QWeb || new QWeb2.Engine();
var oe = openerp.init();
oe.base.webclient("oe");
});

View File

@ -0,0 +1,4 @@
.openerp .menu span {
min-width: 0px;
}

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 {
@ -99,53 +99,38 @@ body.openerp {
white-space: nowrap;
background: url(../img/menu-bg.png) repeat-x;
}
.openerp .menu ul {
margin: 0;
padding: 0;
list-style: none;
overflow: hidden;
position: absolute;
left: 0;
right: 0;
text-align: center;
}
.openerp .menu li {
display: -moz-inline-stack;
display: inline; /*IE7 */
display: inline-block;
margin: 6px 1px;
text-align: center;
text-transform: uppercase;
}
.openerp .menu a {
display: block;
float: left;
padding: 0 0 0 3px;
background: url(../img/menu-item.png) no-repeat;
color: #eee;
text-shadow: #222 0 1px 0;
text-decoration: none;
text-align: center;
text-transform: uppercase;
margin: 2px 1px;
height: 23px;
}
.openerp .menu span {
display: block;
float: left;
min-width: 60px;
height: 23px;
padding: 0 9px 0 6px;
background: url(../img/menu-item.png) 100% 0 no-repeat;
line-height: 22px;
line-height: 23px;
font-weight: bold;
font-size: 85%;
}
.openerp .menu ul li a:hover,
.openerp .menu li a:focus,
.openerp .menu ul li a.active {
.openerp .menu a:hover,
.openerp .menu a:focus,
.openerp .menu a.active {
background-position: 0 -23px;
color: #fff;
}
.openerp .menu ul li a:hover span,
.openerp .menu ul li a:focus span,
.openerp .menu ul li a.active span {
.openerp .menu a:hover span,
.openerp .menu a:focus span,
.openerp .menu a.active span {
background-position: 100% -23px;
}
/* Secondary Menu */
@ -169,10 +154,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;
@ -222,19 +206,21 @@ body.openerp {
height: 63px;
width: 200px;
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));
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#FFFFFF', endColorstr='#CECECE',GradientType=0 );
}
.openerp .company_logo {
vertical-align: middle
margin-top: 7px;
margin-left: 10px;
display: block;
background: url(/base/static/src/img/logo.png);
width:180px;
height:46px;
}
.openerp .header_title {
float: left;
@ -301,7 +287,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 +301,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 +321,9 @@ body.openerp {
}
/* Main Application */
.openerp .application {
padding: 0px;
.openerp .oe-application {
padding: 0;
height: 100%;
}
.openerp h2.oe_view_title {
@ -379,7 +365,7 @@ body.openerp {
background: #AAAAAA;
}
.openerp .filter_icon {
padding: 1px 2px 0px 2px;
padding: 1px 2px 0 2px;
margin-left: 0;
margin-right: 0;
}
@ -442,6 +428,9 @@ body.openerp {
.openerp .searchview_group_content {
padding-left: 10px;
}
.openerp .searchview_group_content .oe-searchview-render-line {
width:0;
}
.openerp .oe-searchview-render-line {
@ -454,12 +443,12 @@ body.openerp {
margin: 2px;
}
.openerp .searchview_extended_add_proposition, .openerp .searchview_extended_add_group {
.openerp .searchview_extended_add_proposition span, .openerp .searchview_extended_add_group span {
background: url(../img/icons/gtk-add.png) repeat-y;
padding-left: 18px;
}
.openerp .searchview_extended_delete_group, .openerp .searchview_extended_delete_prop {
.openerp .searchview_extended_delete_group span, .openerp .searchview_extended_delete_prop span {
background: url(../img/icons/gtk-remove.png) repeat-y;
padding-left: 18px;
}
@ -484,8 +473,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 +532,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 +550,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 +559,6 @@ body.openerp {
/* Form */
.openerp table.oe_frame td {
color: #4c4c4c;
font-weight: 80%;
}
.openerp .required.error {
border: 1px solid #900;
@ -605,6 +601,9 @@ body.openerp {
min-width: 90px;
color: #1f1f1f;
}
.openerp textarea {
resize:vertical;
}
.openerp input[type="text"], .openerp input[type="password"], .openerp select, .openerp .button {
height: 22px;
}
@ -636,10 +635,16 @@ 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;
}
.openerp div.oe-progressbar span {
position: absolute;
margin-left: 10px;
margin-top: 5px;
font-weight: bold;
}
/* jQuery UI override */
.openerp .ui-widget {
@ -673,13 +678,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 +696,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 +724,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 +747,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;
@ -761,15 +761,62 @@ body.openerp {
background-position: 21px 0;
}
.openerp .kitten-mode-activated {
.openerp.kitten-mode-activated .main_table {
background: url(http://placekitten.com/g/1500/800) repeat;
}
.openerp .kitten-mode-activated .header {
.openerp.kitten-mode-activated .header {
background: url(http://placekitten.com/g/211/65) repeat;
}
.openerp .kitten-mode-activated .secondary_menu {
.openerp.kitten-mode-activated .secondary_menu {
background: url(http://placekitten.com/g/212/100) repeat;
}
.openerp.kitten-mode-activated .menu {
background: #828282;
background: -moz-linear-gradient(top, #828282 0%, #4D4D4D 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#828282), color-stop(100%,#4D4D4D));
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#828282', endColorstr='#4D4D4D',GradientType=0 );
}
.openerp.kitten-mode-activated .menu a {
background: none;
}
.openerp.kitten-mode-activated .menu span {
background: none;
}
.openerp.kitten-mode-activated .sidebar-displaying-div li a,
.openerp.kitten-mode-activated .oe-application .view-manager-main-content h2.oe_view_title,
.openerp.kitten-mode-activated .oe-application .view-manager-main-content a.searchview_group_string,
.openerp.kitten-mode-activated .oe-application .view-manager-main-content label {
color: white;
}
.openerp.kitten-mode-activated .menu,
.openerp.kitten-mode-activated .header_corner,
.openerp.kitten-mode-activated .header_title,
.openerp.kitten-mode-activated .secondary_menu div,
.openerp.kitten-mode-activated .oe-application,
.openerp.kitten-mode-activated .oe_footer,
.openerp.kitten-mode-activated .loading,
.openerp.kitten-mode-activated .ui-dialog {
opacity:0.8;
filter:alpha(opacity=80);
}
.openerp.kitten-mode-activated .header .company_logo {
background: url(http://placekitten.com/g/180/46);
}
.openerp.kitten-mode-activated .loading {
background: #828282;
border-color: #828282;
}
/* Many2one Autosearch */
.openerp .ui_combo {
height: 19px;
top: 6px;
width: 20px;
position: relative;
margin-left: -26px;
}
/*------------- End autocomplete ------- */

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -44,8 +44,11 @@
// The dummy class constructor
function Class() {
// All construction is actually done in the init method
if ( !initializing && this.init )
this.init.apply(this, arguments);
if ( !initializing && this.init ) {
var ret = this.init.apply(this, arguments);
if (ret) { return ret; }
}
return this;
}
// Populate our constructed prototype object
@ -120,11 +123,13 @@
/** @namespace */
openerp.base = function(instance) {
openerp.base.dates(instance);
openerp.base.chrome(instance);
openerp.base.data(instance);
openerp.base.views(instance);
openerp.base.search(instance);
openerp.base.list(instance);
openerp.base.m2o(instance);
openerp.base.form(instance);
};

View File

@ -296,6 +296,9 @@ openerp.base.Session = openerp.base.BasicController.extend( /** @lends openerp.b
// Construct a JSON-RPC2 request, method is currently unused
params.session_id = this.session_id;
params.context = typeof(params.context) != "undefined" ? params.context : this.context;
if (!params.context.bin_size) {
params.context.bin_size = true;
}
// Use a default error handler unless defined
error_callback = typeof(error_callback) != "undefined" ? error_callback : this.on_rpc_error;
@ -790,10 +793,6 @@ openerp.base.Header = openerp.base.Controller.extend({
this.do_update();
},
do_update: function() {
if(jQuery.param != undefined &&
jQuery.deparam(jQuery.param.querystring()).kitten != undefined) {
this.kitten = 1;
}
this.$element.html(QWeb.render("Header", this));
}
});
@ -897,7 +896,7 @@ openerp.base.WebClient = openerp.base.Controller.extend({
var params = {};
if(jQuery.param != undefined &&
jQuery.deparam(jQuery.param.querystring()).kitten != undefined) {
params = {kitten:1};
this.$element.addClass("kitten-mode-activated");
}
this.$element.html(QWeb.render("Interface", params));

View File

@ -3,15 +3,139 @@ openerp.base.data = function(openerp) {
openerp.base.DataGroup = openerp.base.Controller.extend( /** @lends openerp.base.DataGroup# */{
/**
* Management interface between views and the collection of selected OpenERP
* records (represents the view's state?)
* Management interface between views and grouped collections of OpenERP
* records.
*
* The root DataGroup is instantiated with the relevant information
* (a session, a model, a domain, a context and a group_by sequence), the
* domain and context may be empty. It is then interacted with via
* :js:func:`~openerp.base.DataGroup.list`, which is used to read the
* content of the current grouping level, and
* :js:func:`~openerp.base.DataGroup.get`, which is used to fetch an item
* of the current grouping level.
*
* @constructs
* @extends openerp.base.Controller
*
* @param {openerp.base.Session} session Current OpenERP session
* @param {String} model name of the model managed by this DataGroup
* @param {Array} domain search domain for this DataGroup
* @param {Object} context context of the DataGroup's searches
* @param {Array} group_by sequence of fields by which to group
*/
init: function(session) {
init: function(session, model, domain, context, group_by) {
this._super(session, null);
this.model = model;
this.context = context;
this.domain = domain;
this.group_by = group_by;
this.groups = null;
},
fetch: function () {
// internal method
var d = new $.Deferred();
var self = this;
if (this.groups) {
d.resolveWith(this, [this.groups]);
} else {
this.rpc('/base/group/read', {
model: this.model,
context: this.context,
domain: this.domain,
group_by_fields: this.group_by
}, function () { }).then(function (response) {
self.groups = response.result;
// read_group results are annoying: they use the name of the
// field grouped on to hold the value and the count, so no
// generic access to those values is possible.
// Alias them to `value` and `length`.
d.resolveWith(self, [_(response.result).map(function (group) {
var field_name = self.group_by[0];
return _.extend({}, group, {
// provide field used for grouping
grouped_on: field_name,
length: group[field_name + '_count'],
value: group[field_name]
});
})]);
}, function () {
d.rejectWith.apply(d, self, [arguments]);
});
}
return d.promise();
},
/**
* Retrieves the content of an item in the DataGroup, which results in
* either a DataSet or new DataGroup instance.
*
* Calling :js:func:`~openerp.base.DataGroup.get` without having called
* :js:func:`~openerp.base.DataGroup.list` beforehand will likely result
* in an error.
*
* The resulting :js:class:`~openerp.base.DataGroup` or
* :js:class:`~openerp.base.DataSet` will be provided through the relevant
* callback function. In both functions, the current DataGroup will be
* provided as context (``this``)
*
* @param {Number} index the index of the group to open in the datagroup's collection
* @param {Function} ifDataSet executed if the item results in a DataSet, provided with the new dataset as parameter
* @param {Function} ifDataGroup executed if the item results in a DataSet, provided with the new datagroup as parameter
*/
get: function (index, ifDataSet, ifDataGroup) {
var group = this.groups[index];
if (!group) {
throw new Error("No group at index " + index);
}
var child_context = _.extend({}, this.context, group.__context);
if (group.__context.group_by.length) {
var datagroup = new openerp.base.DataGroup(
this.session, this.model, group.__domain, child_context,
group.__context.group_by);
ifDataGroup.call(this, datagroup);
} else {
var dataset = new openerp.base.DataSetSearch(this.session, this.model);
dataset.domain = group.__domain;
dataset.context = child_context;
ifDataSet.call(this, dataset);
}
},
/**
* Gathers the content of the current data group (the current grouping
* level), and provides it via a promise object to which callbacks should
* be added::
*
* datagroup.list().then(function (list) {
* // manipulate list here
* });
*
* The argument to the callback is the list of elements fetched, the
* context (``this``) is the datagroup itself.
*
* The items of a list are the standard objects returned by ``read_group``
* with three properties added:
*
* ``length``
* the number of records contained in the group (and all of its
* sub-groups). This does *not* provide the size of the "next level"
* of the group, unless the group is terminal (no more groups within
* it).
* ``grouped_on``
* the name of the field this level was grouped on, this is mostly
* used for display purposes, in order to know the name of the current
* level of grouping. The ``grouped_on`` should be the same for all
* objects of the list.
* ``value``
* the value which led to this group (this is the value all contained
* records have for the current ``grouped_on`` field name).
*
* @returns {$.Deferred}
*/
list: function () {
return this.fetch();
}
});
@ -89,12 +213,12 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base.
context: this.context
}, callback);
},
create: function(data, callback) {
create: function(data, callback, error_callback) {
return this.rpc('/base/dataset/create', {
model: this.model,
data: data,
context: this.context
}, callback);
}, callback, error_callback);
},
write: function (id, data, callback) {
return this.rpc('/base/dataset/save', {
@ -118,6 +242,22 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base.
ids: ids,
args: args
}, callback);
},
name_search: function (search_str, callback) {
search_str = search_str || '';
return this.rpc('/base/dataset/name_search', {
model: this.model,
search_str: search_str,
domain: this.domain || [],
context: this.context
}, callback);
},
exec_workflow: function (id, signal, callback) {
return this.rpc('/base/dataset/exec_workflow', {
model: this.model,
id: id,
signal: signal
}, callback);
}
});
@ -137,7 +277,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 +299,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 +310,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:

View File

@ -0,0 +1,137 @@
openerp.base.dates = function(openerp) {
/**
* Converts a string to a Date javascript object using OpenERP's
* datetime string format (exemple: '2011-12-01 15:12:35').
*
* The timezone is assumed to be UTC (standard for OpenERP 6.1)
* and will be converted to the browser's timezone.
*
* @param {String} str A string representing a datetime.
* @returns {Date}
*/
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\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;
};
/**
* Converts a string to a Date javascript object using OpenERP's
* date string format (exemple: '2011-12-01').
*
* @param {String} str A string representing a date.
* @returns {Date}
*/
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;
};
/**
* Converts a string to a Date javascript object using OpenERP's
* time string format (exemple: '15:12:35').
*
* @param {String} str A string representing a time.
* @returns {Date}
*/
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;
};
/**
* Converts a Date javascript object to a string using OpenERP's
* datetime string format (exemple: '2011-12-01 15:12:35').
*
* The timezone of the Date object is assumed to be the one of the
* browser and it will be converted to UTC (standard for OpenERP 6.1).
*
* @param {Date} obj
* @returns {String} A string representing a datetime.
*/
openerp.base.format_datetime = function(obj) {
if (!obj) {
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);
};
/**
* Converts a Date javascript object to a string using OpenERP's
* date string format (exemple: '2011-12-01').
*
* @param {Date} obj
* @returns {String} A string representing a date.
*/
openerp.base.format_date = function(obj) {
if (!obj) {
return false;
}
return fts(obj.getFullYear(),4) + "-" + fts(obj.getMonth() + 1,2) + "-"
+ fts(obj.getDate(),2);
};
/**
* Converts a Date javascript object to a string using OpenERP's
* time string format (exemple: '15:12:35').
*
* @param {Date} obj
* @returns {String} A string representing a time.
*/
openerp.base.format_time = function(obj) {
if (!obj) {
return false;
}
return fts(obj.getHours(),2) + ":" + fts(obj.getMinutes(),2) + ":"
+ fts(obj.getSeconds(),2);
};
};

View File

@ -2,7 +2,7 @@
openerp.base.form = function (openerp) {
openerp.base.views.add('form', 'openerp.base.FormView');
openerp.base.FormView = openerp.base.Controller.extend( /** @lends openerp.base.FormView# */{
openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormView# */{
/**
* Indicates that this view is not searchable, and thus that no search
* view should be displayed (if there is one active).
@ -143,14 +143,21 @@ openerp.base.FormView = openerp.base.Controller.extend( /** @lends openerp.base
var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
if (call) {
var method = call[1], args = [];
var argument_replacement = {
'False' : false,
'True' : true,
'None' : null
}
_.each(call[2].split(','), function(a) {
var field = _.trim(a);
if (self.fields[field]) {
if (field in argument_replacement) {
args.push(argument_replacement[field]);
} else if (self.fields[field]) {
var value = self.fields[field].value;
args.push(value == null ? false : value);
} else {
args.push(false);
this.log("warning : on_change can't find field " + field, onchange);
self.log("warning : on_change can't find field " + field, onchange);
}
});
var ajax = {
@ -314,7 +321,7 @@ openerp.base.form.compute_domain = function(expr, fields) {
var ex = expr[i];
if (ex.length == 1) {
var top = stack.pop();
switch (ex[0]) {
switch (ex) {
case '|':
stack.push(stack.pop() || top);
continue;
@ -325,7 +332,7 @@ openerp.base.form.compute_domain = function(expr, fields) {
stack.push(!top);
continue;
default:
throw new Error('Unknown domain operator ' + ex[0]);
throw new Error('Unknown domain operator ' + ex);
}
}
@ -355,17 +362,18 @@ openerp.base.form.compute_domain = function(expr, fields) {
stack.push(field >= val);
break;
case 'in':
stack.push(_.indexOf(val, field) > -1);
stack.push(_(val).contains(field));
break;
case 'not in':
stack.push(_.indexOf(val, field) == -1);
stack.push(!_(val).contains(field));
break;
default:
this.log("Unsupported operator in attrs :", op);
}
}
return _.indexOf(stack, false) == -1;
},
return _.all(stack);
};
openerp.base.form.Widget = openerp.base.Controller.extend({
init: function(view, node) {
this.view = view;
@ -457,16 +465,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,31 +538,14 @@ 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.view.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();
}
}
});
@ -665,7 +654,24 @@ openerp.base.form.FieldChar = openerp.base.form.Field.extend({
openerp.base.form.FieldEmail = openerp.base.form.FieldChar.extend({
init: function(view, node) {
this._super(view, node);
this.template = "FieldEmail";
this.validation_regex = /@/;
},
start: function() {
this._super.apply(this, arguments);
this.$element.find('button').click(this.on_button_clicked);
},
on_button_clicked: function() {
if (!this.value || this.invalid) {
this.notification.warn("E-mail error", "Can't send email to invalid e-mail address");
} else {
location.href = 'mailto:' + this.value;
}
},
set_value: function(value) {
this._super.apply(this, arguments);
var show_value = (value != null && value !== false) ? value : '';
this.$element.find('a').attr('href', 'mailto:' + show_value);
}
});
@ -675,44 +681,91 @@ openerp.base.form.FieldUrl = openerp.base.form.FieldChar.extend({
openerp.base.form.FieldFloat = openerp.base.form.FieldChar.extend({
init: function(view, node) {
this._super(view, node);
this.validation_regex = /^\d+(\.\d+)?$/;
this.validation_regex = /^-?\d+(\.\d+)?$/;
},
set_value: function(value) {
this._super.apply(this, arguments);
var show_value = (value != null && value !== false) ? value.toFixed(2) : '';
if (!value) {
// As in GTK client, floats default to 0
value = 0;
}
this._super.apply(this, [value]);
var show_value = value.toFixed(2);
this.$element.find('input').val(show_value);
},
set_value_from_ui: function() {
this.value = this.$element.find('input').val().replace(/,/g, '.');
},
validate: function() {
this._super.apply(this, arguments);
if (!this.invalid) {
this.value = Number(this.value);
}
}
});
openerp.base.form.FieldDate = openerp.base.form.FieldChar.extend({
openerp.base.form.FieldDatetime = openerp.base.form.Field.extend({
init: function(view, node) {
this._super(view, node);
this.template = "FieldDate";
this.validation_regex = /^\d+-\d+-\d+$/;
this.jqueryui_object = 'datetimepicker';
},
start: function() {
this._super.apply(this, arguments);
this.$element.find('input').change(this.on_ui_change).datepicker({
dateFormat: 'yy-mm-dd'
});
}
});
openerp.base.form.FieldDatetime = openerp.base.form.FieldChar.extend({
init: function(view, node) {
this._super(view, node);
this.template = "FieldDatetime";
this.validation_regex = /^\d+-\d+-\d+( \d+:\d+(:\d+)?)?$/;
},
start: function() {
this._super.apply(this, arguments);
this.$element.find('input').change(this.on_ui_change).datetimepicker({
this.$element.find('input').change(this.on_ui_change)[this.jqueryui_object]({
dateFormat: 'yy-mm-dd',
timeFormat: 'hh:mm:ss'
});
},
set_value: function(value) {
this._super.apply(this, arguments);
if (value == null || value == false) {
this.$element.find('input').val('');
} else {
this.value = this.parse(value);
this.$element.find('input')[this.jqueryui_object]('setDate', this.value);
}
},
set_value_from_ui: function() {
this.value = this.$element.find('input')[this.jqueryui_object]('getDate') || false;
if (this.value) {
this.value = this.format(this.value);
}
},
validate: function() {
this.invalid = this.required && this.value === false;
},
parse: openerp.base.parse_datetime,
format: openerp.base.format_datetime
});
openerp.base.form.FieldDate = openerp.base.form.FieldDatetime.extend({
init: function(view, node) {
this._super(view, node);
this.jqueryui_object = 'datepicker';
},
parse: openerp.base.parse_date,
format: openerp.base.format_date
});
openerp.base.form.FieldFloatTime = openerp.base.form.FieldChar.extend({
init: function(view, node) {
this._super(view, node);
this.validation_regex = /^\d+:\d+$/;
},
set_value: function(value) {
value = value || 0;
this._super.apply(this, [value]);
var show_value = _.sprintf("%02d:%02d", Math.floor(value), Math.round((value % 1) * 60));
this.$element.find('input').val(show_value);
},
validate: function() {
if (typeof(this.value) == "string") {
this._super.apply(this, arguments);
if (!this.invalid) {
var time = this.value.split(':');
this.value = parseInt(time[0], 10) + parseInt(time[1], 10) / 60;
}
}
}
});
@ -789,6 +842,14 @@ openerp.base.form.FieldProgressBar = openerp.base.form.Field.extend({
value: this.value,
disabled: this.readonly
});
},
set_value: function(value) {
this._super.apply(this, arguments);
var show_value = Number(value);
if (show_value === NaN) {
show_value = 0;
}
this.$element.find('div').progressbar('option', 'value', show_value).find('span').html(show_value + '%');
}
});
@ -825,10 +886,33 @@ openerp.base.form.FieldSelection = openerp.base.form.Field.extend({
}
});
openerp.base.form.FieldMany2OneDatasSet = openerp.base.DataSetStatic.extend({
start: function() {
},
write: function (id, data, callback) {
this._super(id, data, callback);
}
});
openerp.base.form.FieldMany2OneViewManager = openerp.base.ViewManager.extend({
init: function(session, element_id, dataset, views) {
this._super(session, element_id, dataset, views);
}
});
openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({
init: function(view, node) {
this._super(view, node);
this.template = "FieldMany2One";
this.is_field_m2o = true;
},
start: function() {
this.$element = $('#' + this.element_id);
this.dataset = new openerp.base.form.FieldMany2OneDatasSet(this.session, this.field.relation);
var views = [ [false,"list"], [false,"form"] ];
this.viewmanager = new openerp.base.form.FieldMany2OneViewManager(this.view.session, this.element_id, this.dataset, views);
new openerp.base.m2o(this.viewmanager, this.$element, this.field.relation, this.dataset, this.session)
this.$element.find('input').change(this.on_ui_change);
},
set_value: function(value) {
this._super.apply(this, arguments);
@ -838,6 +922,16 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({
this.value = value[0];
}
this.$element.find('input').val(show_value);
this.$element.find('input').attr('m2o_id', this.value);
},
get_value: function() {
var val = this.$element.find('input').attr('m2o_id') || this.value
return val;
},
on_ui_change: function() {
this.touched = this.view.touched = true;
}
});
@ -897,6 +991,189 @@ 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");
this.is_started = false;
this.is_setted = false;
},
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();
var hack = {loaded: false};
this.list_view.on_loaded.add_last(function() {
if (! hack.loaded) {
self.is_started = true;
self.check_load();
hack.loaded = true;
}
});
},
set_value: function(value) {
if (value != false) {
this.dataset.ids = value;
this.dataset.count = value.length;
this.is_setted = true;
this.check_load();
}
},
get_value: function() {
return [[6,false,this.dataset.ids]];
},
check_load: function() {
if(this.is_started && this.is_setted) {
this.list_view.do_reload();
}
}
});
openerp.base.form.Many2ManyListView = openerp.base.ListView.extend({
do_delete: function (ids) {
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();
var pop = new openerp.base.form.Many2XSelectPopup(null, this.m2m_field.view.session);
pop.select_element(this.model);
var self = this;
pop.on_select_element.add(function(element_id) {
if(! _.detect(self.dataset.ids, function(x) {return x == element_id;})) {
self.dataset.ids.push(element_id);
self.dataset.count = self.dataset.ids.length;
self.do_reload();
}
pop.stop();
});
},
select_record: function(index) {
var id = this.rows[index].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"
});
}
});
openerp.base.form.Many2XSelectPopup = openerp.base.BaseWidget.extend({
identifier_prefix: "many2xselectpopup",
template: "Many2XSelectPopup",
select_element: function(model, dataset) {
this.model = model;
this.dataset = dataset
var html = this.render();
jQuery(html).dialog({title: '',
modal: true,
minWidth: 800});
this.start();
},
start: function() {
this._super();
if (!this.dataset) {
this.dataset = new openerp.base.DataSetSearch(this.session, this.model);
}
this.setup_search_view();
},
setup_search_view: function() {
var self = this;
if (this.searchview) {
this.searchview.stop();
}
this.searchview = new openerp.base.SearchView(this, this.session, this.element_id + "_search",
this.dataset, false, {});
this.searchview.on_search.add(function(domains, contexts, groupbys) {
self.view_list.do_search.call(
self, domains, contexts, groupbys);
});
this.searchview.on_loaded.add_last(function () {
var $buttons = self.searchview.$element.find(".oe_search-view-buttons");
$buttons.append(QWeb.render("Many2XSelectPopup.search.buttons"));
var $nbutton = $buttons.find(".oe_many2xselectpopup-search-new");
$nbutton.click(function() {
self.new_object();
});
var $cbutton = $buttons.find(".oe_many2xselectpopup-search-close");
$cbutton.click(function() {
self.stop();
});
self.view_list = new openerp.base.form.Many2XPopupListView( null, self.session,
self.element_id + "_view_list", self.dataset, false);
self.view_list.popup = self;
self.view_list.do_show();
self.view_list.start();
var tmphack = {"loaded": false};
self.view_list.on_loaded.add_last(function() {
if ( !tmphack.loaded ) {
self.view_list.do_reload();
tmphack.loaded = true;
};
});
});
this.searchview.start();
},
on_select_element: function(element_id) {
},
new_object: function() {
var self = this;
this.searchview.hide();
this.view_list.$element.hide();
this.dataset.index = null;
this.view_form = new openerp.base.FormView({}, this.session,
this.element_id + "_view_form", this.dataset, false);
this.view_form.start();
this.view_form.on_loaded.add_last(function() {
var $buttons = self.view_form.$element.find(".oe_form_buttons");
$buttons.html(QWeb.render("Many2XSelectPopup.form.buttons"));
var $nbutton = $buttons.find(".oe_many2xselectpopup-form-save");
$nbutton.click(function() {
self.view_form.do_save();
});
var $cbutton = $buttons.find(".oe_many2xselectpopup-form-close");
$cbutton.click(function() {
self.stop();
});
});
this.view_form.on_created.add_last(function(r, success) {
if (r.result) {
var id = arguments[0].result;
self.on_select_element(id);
}
});
this.view_form.do_show();
}
});
openerp.base.form.Many2XPopupListView = openerp.base.ListView.extend({
switch_to_record: function(index) {
this.popup.on_select_element(this.dataset.ids[index]);
}
});
@ -907,6 +1184,19 @@ openerp.base.form.FieldReference = openerp.base.form.Field.extend({
}
});
openerp.base.form.FieldImage = openerp.base.form.Field.extend({
init: function(view, node) {
this._super(view, node);
this.template = "FieldImage";
},
set_value: function(value) {
this._super.apply(this, arguments);
var url = '/base/formview/image?session_id=' + this.session.session_id + '&model=' +
this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name
this.$element.find('img').show().attr('src', url);
}
});
/**
* Registry of form widgets, called by :js:`openerp.base.FormView`
*/
@ -933,7 +1223,8 @@ openerp.base.form.widgets = new openerp.base.Registry({
'float' : 'openerp.base.form.FieldFloat',
'integer': 'openerp.base.form.FieldFloat',
'progressbar': 'openerp.base.form.FieldProgressBar',
'float_time': 'openerp.base.form.FieldFloat'
'float_time': 'openerp.base.form.FieldFloatTime',
'image': 'openerp.base.form.FieldImage'
});
};

View File

@ -1,7 +1,6 @@
openerp.base.list = function (openerp) {
openerp.base.views.add('list', 'openerp.base.ListView');
openerp.base.ListView = openerp.base.Controller.extend(
/** @lends openerp.base.ListView# */ {
openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListView# */ {
defaults: {
// records can be selected one by one
'selectable': true,
@ -10,9 +9,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,8 +37,13 @@ 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) {
var self = this;
this._super(session, element_id);
this.view_manager = view_manager;
this.dataset = dataset;
@ -36,12 +54,76 @@ openerp.base.ListView = openerp.base.Controller.extend(
this.rows = [];
this.options = _.extend({}, this.defaults, options || {});
this.list = new openerp.base.ListView.List({
options: this.options,
columns: this.columns,
rows: this.rows
});
this.groups = new openerp.base.ListView.Groups({
options: this.options,
columns: this.columns
});
$([this.list, this.groups]).bind({
'selected': function (e, selection) {
self.$element.find('#oe-list-delete')
.toggle(!!selection.length);
},
'deleted': function (e, ids) {
self.do_delete(ids);
},
'action': function (e, action_name, id) {
var action = _.detect(self.columns, function (field) {
return field.name === action_name;
});
if (!action) { return; }
// TODO: not supposed to reload everything, I think
self.execute_action(
action, self.dataset, self.session.action_manager,
id, self.do_reload);
},
'row_link': function (e, index) {
self.select_record(index);
}
});
},
/**
* 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;
@ -50,7 +132,9 @@ openerp.base.ListView = openerp.base.Controller.extend(
var fields = this.fields_view.fields;
var domain_computer = openerp.base.form.compute_domain;
this.columns = _(this.fields_view.arch.children).chain()
this.columns.splice(0, this.columns.length);
this.columns.push.apply(this.columns, _(this.fields_view.arch.children).chain()
.map(function (field) {
var name = field.attrs.name;
var column = _.extend({id: name, tag: field.tag},
@ -69,11 +153,14 @@ openerp.base.ListView = openerp.base.Controller.extend(
column.attrs_for = function () { return {}; };
}
return column;
}).value();
}).value());
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,58 +168,40 @@ 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
$table.delegate(
'th.oe-record-selector', 'click', function (e) {
// TODO: ~linear performances, would a simple counter work?
if ($table.find('th.oe-record-selector input:checked').length) {
$table.find('#oe-list-delete').show();
} else {
$table.find('#oe-list-delete').hide();
}
// A click in the selection cell should not activate the
// linking feature
e.stopImmediatePropagation();
});
$table.delegate(
'td.oe-field-cell button', 'click', function (e) {
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();
});
$table.delegate(
'td.oe-record-delete button', 'click', this.do_delete);
// Global rows handlers
$table.delegate(
'tr', 'click', this.on_select_row);
this.list.move_to($table);
// 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
* @returns {Promise} promise to the end of view rendering (list views are asynchronously filled for improved responsiveness)
* 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
*/
do_fill_table: function(records) {
var $table = this.$element.find('table');
this.rows = records;
do_fill_table: function(result) {
if (result.view) {
this.on_loaded({fields_view: result.view});
}
var records = result.records;
this.rows.splice(0, this.rows.length);
this.rows.push.apply(this.rows, records);
// Keep current selected record, if it's still in our new search
var current_record_id = this.dataset.ids[this.dataset.index];
@ -146,78 +215,36 @@ openerp.base.ListView = openerp.base.Controller.extend(
this.dataset.count = this.dataset.ids.length;
var results = this.rows.length;
$table.find('.oe-pager-last').text(results);
$table.find('.oe-pager-total').text(results);
this.$element.find('table')
.find('.oe-pager-last').text(results).end()
.find('.oe-pager-total').text(results);
// remove all data lines
var $old_body = $table.find('tbody');
// add new content
var columns = this.columns,
rows = this.rows,
options = this.options;
// Paginate by groups of 50 for rendering
var PAGE_SIZE = 50,
bodies_count = Math.ceil(this.rows.length / PAGE_SIZE),
body = 0,
$body = $('<tbody class="ui-widget-content">').appendTo($table);
var rendered = $.Deferred();
var render_body = function () {
setTimeout(function () {
$body.append(
QWeb.render("ListView.rows", {
columns: columns,
rows: rows.slice(body*PAGE_SIZE, (body+1)*PAGE_SIZE),
options: options
}));
++body;
if (body < bodies_count) {
render_body();
} else {
rendered.resolve();
}
}, 0);
};
render_body();
return rendered.promise().then(function () {
$old_body.remove();
});
this.list.refresh();
},
/**
* 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) {
select_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));
},
on_select_row: function (event) {
var $target = $(event.currentTarget);
if (!$target.parent().is('tbody')) {
return;
}
// count number of preceding siblings to line clicked
var row = this.rows[$target.prevAll().length];
var index = _.indexOf(this.dataset.ids, row.data.id.value);
if (index == undefined || index === -1) {
return;
}
this.switch_to_record(index);
},
do_show: function () {
this.$element.show();
if (this.hidden) {
@ -243,9 +270,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', {
@ -256,6 +293,14 @@ openerp.base.ListView = openerp.base.Controller.extend(
// TODO: handle non-empty results.group_by with read_group
self.dataset.context = results.context;
self.dataset.domain = results.domain;
if (results.group_by.length) {
self.groups.datagroup = new openerp.base.DataGroup(
self.session, self.dataset.model,
results.domain, results.context,
results.group_by);
self.$element.html(self.groups.render());
return;
}
return self.do_reload();
});
},
@ -266,13 +311,35 @@ openerp.base.ListView = openerp.base.Controller.extend(
/**
* Handles the signal to delete a line from the DOM
*
* @param e
* @param {Array} ids the id of the object to delete
*/
do_delete: function (e) {
// don't link to forms
e.stopImmediatePropagation();
this.dataset.unlink(
[this.rows[$(e.currentTarget).closest('tr').prevAll().length].data.id.value]);
do_delete: function (ids) {
if (!ids.length) {
return;
}
var self = this;
return $.when(this.dataset.unlink(ids)).then(function () {
_(self.rows).chain()
.map(function (row, index) {
return {
index: index,
id: row.data.id.value
};})
.filter(function (record) {
return _.contains(ids, record.id);
})
.sort(function (a, b) {
// sort in reverse index order, so we delete from the end
// and don't blow up the following indexes (leading to
// removing the wrong records from the visible list)
return b.index - a.index;
})
.each(function (record) {
self.rows.splice(record.index, 1);
});
// TODO only refresh modified rows
self.list.refresh();
});
},
/**
* Handles signal for the addition of a new record (can be a creation,
@ -282,36 +349,177 @@ openerp.base.ListView = openerp.base.Controller.extend(
*/
do_add_record: function () {
this.notification.notify('Add', "New record");
this.switch_to_record(null);
this.select_record(null);
},
/**
* Handles deletion of all selected lines
*/
do_delete_selected: function () {
var selection = this.get_selection();
if (selection.length) {
this.dataset.unlink(selection);
}
this.do_delete(
this.list.get_selection());
}
// TODO: implement reorder (drag and drop rows)
});
openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List# */{
/**
* List display for the ListView, handles basic DOM events and transforms
* them in the relevant higher-level events, to which the list view (or
* other consumers) can subscribe.
*
* Events on this object are registered via jQuery.
*
* Available events:
*
* `selected`
* Triggered when a row is selected (using check boxes), provides an
* array of ids of all the selected records.
* `deleted`
* Triggered when deletion buttons are hit, provide an array of ids of
* all the records being marked for suppression.
* `action`
* Triggered when an action button is clicked, provides two parameters:
*
* * The name of the action to execute (as a string)
* * The id of the record to execute the action on
* `row_link`
* Triggered when a row of the table is clicked, provides the index (in
* the rows array) and id of the selected record to the handle function.
*
* @constructs
* @param {Object} opts display options, identical to those of :js:class:`openerp.base.ListView`
*/
init: function (opts) {
var self = this;
// columns, rows, options
this.options = opts.options;
this.columns = opts.columns;
this.rows = opts.rows;
this.$_element = $('<tbody class="ui-widget-content">')
.appendTo(document.body)
.delegate('th.oe-record-selector', 'click', function (e) {
e.stopPropagation();
$(self).trigger('selected', [self.get_selection()]);
})
.delegate('td.oe-record-delete button', 'click', function (e) {
e.stopPropagation();
var $row = $(e.target).closest('tr');
$(self).trigger('deleted', [[self.row_id($row)]]);
})
.delegate('td.oe-field-cell button', 'click', function (e) {
e.stopPropagation();
var $target = $(e.currentTarget),
field = $target.closest('td').data('field'),
record_id = self.row_id($target.closest('tr'));
$(self).trigger('action', [field, record_id]);
})
.delegate('tr', 'click', function (e) {
e.stopPropagation();
$(self).trigger(
'row_link',
[self.row_position(e.currentTarget),
self.row_id(e.currentTarget)]);
});
},
move_to: function (element) {
this.$current = this.$_element.clone(true).appendTo(element);
this.render();
return this;
},
render: function () {
this.$current.empty().append($(QWeb.render('ListView.rows', this)));
return this;
},
refresh: function () {
this.render();
return this;
},
/**
* 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) {
return [];
}
var rows = this.rows;
return this.$element.find('th.oe-record-selector input:checked')
return this.$current.find('th.oe-record-selector input:checked')
.closest('tr').map(function () {
return rows[$(this).prevAll().length].data.id.value;
}).get();
},
/**
* Returns the index of the row in the list of rows.
*
* @param {Object} row the selected row
* @returns {Number} the position of the row in this.rows
*/
row_position: function (row) {
return $(row).prevAll().length;
},
/**
* Returns the identifier of the object displayed in the provided table
* row
*
* @param {Object} row the selected table row
* @returns {Number|String} the identifier of the row's object
*/
row_id: function (row) {
return this.rows[this.row_position(row)].data.id.value;
}
// drag and drop
// editable?
});
openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Groups# */{
/**
* Grouped display for the ListView. Handles basic DOM events and interacts
* with the :js:class:`~openerp.base.DataGroup` bound to it.
*
* Provides events similar to those of
* :js:class:`~openerp.base.ListView.List`
*/
init: function (opts) {
this.options = opts.options;
this.columns = opts.columns;
this.datagroup = {};
},
make_level: function (datagroup) {
var self = this, $root = $('<dl>');
datagroup.list().then(function (list) {
_(list).each(function (group, index) {
var $title = $('<dt>')
.text(group.grouped_on + ': ' + group.value + ' (' + group.length + ')')
.appendTo($root);
$title.click(function () {
datagroup.get(index, function (new_dataset) {
var $content = $('<ul>').appendTo(
$('<dd>').insertAfter($title));
new_dataset.read_slice([], null, null, function (records) {
_(records).each(function (record) {
$('<li>')
.appendTo($content)
.text(_(record).map(function (value, key) {
return key + ': ' + value;
}).join(', '));
});
});
}, function (new_datagroup) {
console.log(new_datagroup);
$('<dd>')
.insertAfter($title)
.append(self.make_level(new_datagroup));
});
});
});
});
return $root;
},
render: function () {
return this.make_level(this.datagroup);
}
});
openerp.base.TreeView = openerp.base.Controller.extend({
});
};
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:

View File

@ -0,0 +1,114 @@
openerp.base.m2o = function(openerp){
openerp.base.m2o = openerp.base.Controller.extend({
init: function(view_manager, element_id, model, dataset, session){
this._super(element_id, model, dataset, session);
this.view_manager = view_manager
this.session = session;
this.element = element_id.find('input');
this.button = element_id.find('div');
this.dataset = dataset;
var cache = {};
var lastXhr;
this.relation = model;
this.result_ids = [];
var self = this;
var $input = this.element.autocomplete({
source: function(request, response){
var search_val = request.term;
if (search_val in cache) {
response(cache[search_val]);
return;
}
//pass request to server
lastXhr = self.dataset.name_search(search_val, function(obj, status, xhr){
var result = obj.result;
var values = [];
if (!result.length) {
values.push({'value': 'Create...', id: 'create', orig_val: ''});
}
$.each(result, function(i, val){
values.push({
value: val[1],
id: val[0],
orig_val: val[1]
});
self.result_ids.push(result[i][0]);
});
if (values.length > 7) {
values = values.slice(0, 7);
}
values.push({'value': 'More...', id: 'more', orig_val:''});
cache[search_val] = values;
response(values);
});
return;
},
select: function(event, ui){
ui.item.value = ui.item.orig_val? ui.item.orig_val : self.element.data( "autocomplete" ).term;
if (ui.item.id == 'more') {
self.dataset.ids = self.result_ids;
self.dataset.count = self.dataset.ids.length;
self.dataset.domain = self.result_ids.length ? [["id", "in", self.dataset.ids]] : []
self.element.val('');
var pop = new openerp.base.form.Many2XSelectPopup(null, self.session);
pop.select_element(self.relation, self.dataset);
return;
}
if (ui.item.id == 'create') {
var val = self.element.val();
self.dataset.create({'name': ui.item.value},
function(r){}, function(r){
var element_id = _.uniqueId("act_window_dialog");
var dialog = jQuery('<div>',
{'id': element_id
}).dialog({
modal: true,
minWidth: 800
});
self.element.val('');
var event_form = new openerp.base.FormView(self.view_manager, self.session, element_id, self.dataset, false);
event_form.start();
});
$input.val(self.element.data( "autocomplete" ).term);
return true;
}
self.element.attr('m2o_id', ui.item.id);
},
minLength: 0,
focus: function(event, ui) {
if (ui.item.id == ('create')) {
return true;
}
ui.item.value = self.element.data("autocomplete").term.length ? self.element.val() + '[' + ui.item.orig_val.substring(self.element.data("autocomplete").term.length) + ']' : this.lastSearch;
}
});
this.button.button({
icons: {
primary: "ui-icon-triangle-1-s"},
text: false
})
.click(function() {
// close if already visible
if ($input.autocomplete("widget").is(":visible")) {
$input.autocomplete( "close" );
return;
}
$(this).blur();
$input.autocomplete("search", "" );
$input.focus();
});
}
});
}

View File

@ -121,7 +121,10 @@ openerp.base.SearchView = openerp.base.Controller.extend({
'lines': lines,
'defaults': this.defaults
});
this.$element.html(render);
// We don't understand why the following commented line does not work in Chrome but
// the non-commented line does. As far as we investigated, only God knows.
//this.$element.html(render);
jQuery(render).appendTo(this.$element);
var f = this.$element.find('form');
this.$element.find('form')
@ -146,7 +149,6 @@ openerp.base.SearchView = openerp.base.Controller.extend({
do_search: function (e) {
if (e && e.preventDefault) { e.preventDefault(); }
//debugger;
var domains = [], contexts = [];
var errors = [];

View File

@ -0,0 +1,26 @@
/*---------------------------------------------------------
* OpenERP base library
*---------------------------------------------------------*/
openerp.base.view_tree = function(openerp) {
openerp.base.views.add('tree', 'openerp.base.TreeView');
openerp.base.TreeView = openerp.base.Controller.extend({
/**
* Genuine tree view (the one displayed as a tree, not the list)
*/
start: function () {
this._super();
this.$element.append('Tree view');
},
do_show: function () {
this.$element.show();
},
do_hide: function () {
this.$element.hide();
}
});
};
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:

View File

@ -19,14 +19,21 @@ openerp.base.ActionManager = openerp.base.Controller.extend({
* Supported actions: act_window
*/
do_action: function(action) {
this.log(action);
var self = this;
action.flags = _.extend({
sidebar : true,
search_view : true,
new_window : false,
views_switcher : true,
toolbar : true,
pager : true
}, action.flags || {});
// instantiate the right controllers by understanding the action
switch (action.type) {
case 'ir.actions.act_window':
if (action.target == 'new') {
var element_id = _.uniqueId("act_window_dialog");
var dialog = $('<div id="'+element_id+'"></div>');
var dialog = $('<div id="' + element_id + '"></div>');
dialog.dialog({
title: action.name,
modal: true,
@ -36,14 +43,21 @@ 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);
viewmanager.start();
this.dialog_stack.push(viewmanager);
} else if (action.target == "current") {
} else if (action.flags.new_window) {
action.flags.new_window = false;
this.rpc("/base/session/save_session_action", { the_action : action}, function(key) {
var url = window.location.protocol + "//" + window.location.host +
window.location.pathname + "?" + jQuery.param({ s_action : "" + key });
window.open(url);
});
} else {
if (this.viewmanager) {
this.viewmanager.stop();
}
this.viewmanager = new openerp.base.ViewManagerAction(this.session,this.element_id, action, true);
this.viewmanager = new openerp.base.ViewManagerAction(this.session, this.element_id, action);
this.viewmanager.start();
}
break;
@ -58,11 +72,6 @@ openerp.base.ActionManager = openerp.base.Controller.extend({
}
});
/**
* Registry for all the main views
*/
openerp.base.views = new openerp.base.Registry();
openerp.base.ViewManager = openerp.base.Controller.extend({
init: function(session, element_id, dataset, views) {
this._super(session, element_id);
@ -72,6 +81,7 @@ openerp.base.ViewManager = openerp.base.Controller.extend({
this.active_view = null;
this.views_src = views;
this.views = {};
this.flags = this.flags || {};
},
/**
* @returns {jQuery.Deferred} initial view loading promise
@ -86,6 +96,9 @@ openerp.base.ViewManager = openerp.base.Controller.extend({
_.each(this.views_src, function(view) {
self.views[view[1]] = { view_id: view[0], controller: null };
});
if (this.flags.views_switcher === false) {
this.$element.find('.oe_vm_switch').hide();
}
// switch to the first one in sequence
return this.on_mode_switch(this.views_src[0][1]);
},
@ -109,7 +122,7 @@ openerp.base.ViewManager = openerp.base.Controller.extend({
this.views[view_type].controller = controller;
}
if(this.searchview) {
if (this.searchview) {
if (view.controller.searchable === false) {
this.searchview.hide();
} else {
@ -145,6 +158,9 @@ openerp.base.ViewManager = openerp.base.Controller.extend({
this.searchview.stop();
}
this.searchview = new openerp.base.SearchView(this, this.session, this.element_id + "_search", this.dataset, view_id, search_defaults);
if (this.flags.search_view === false) {
this.searchview.hide();
}
this.searchview.on_search.add(function(domains, contexts, groupbys) {
self.views[self.active_view].controller.do_search.call(
self, domains.concat(self.domains()),
@ -183,13 +199,21 @@ 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);
init: function(session, element_id, action) {
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;
if (sidebar)
this.flags = this.action.flags || {};
if (this.flags.sidebar) {
this.sidebar = new openerp.base.Sidebar(null, this);
}
},
start: function() {
var inital_view_loaded = this._super();
@ -282,7 +306,10 @@ openerp.base.Sidebar = openerp.base.BaseWidget.extend({
var i = $this.attr("data-i");
var j = $this.attr("data-j");
var action = self.sections[i].elements[j];
(new openerp.base.ExternalActionManager(self.view_manager.session, null)) .handle_action(action);
action.flags = {
new_window : true
}
self.session.action_manager.do_action(action);
e.stopPropagation();
e.preventDefault();
});
@ -293,89 +320,51 @@ openerp.base.Sidebar = openerp.base.BaseWidget.extend({
}
});
openerp.base.ExternalActionManager = openerp.base.Controller.extend({
handle_action: function(action) {
if(action.type=="ir.actions.act_window") {
if(action.target=="new") {
var element_id = _.uniqueId("act_window_dialog");
var dialog = $('<div id="'+element_id+'"></div>');
dialog.dialog({
title: action.name
});
var viewmanager = new openerp.base.ViewManagerAction(this.session ,element_id, action, false);
viewmanager.start();
} else if (action.target == "current") {
this.rpc("/base/session/save_session_action", {the_action:action}, function(key) {
var url = window.location.protocol + "//" + window.location.host +
window.location.pathname + "?" + jQuery.param({s_action:""+key});
window.open(url);
});
openerp.base.View = openerp.base.Controller.extend({
/**
* 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);
}
}
// TODO: show an error like "not implemented" here
// since we don't currently have any way to handle errors do you have any better idea
// than using todos?
}
});
openerp.base.views.add('calendar', 'openerp.base.CalendarView');
openerp.base.CalendarView = openerp.base.Controller.extend({
start: function () {
this._super();
this.$element.append('Calendar view');
},
do_show: function () {
this.$element.show();
},
do_hide: function () {
this.$element.hide();
}
});
openerp.base.views.add('gantt', 'openerp.base.GanttView');
openerp.base.GanttView = openerp.base.Controller.extend({
start: function () {
this._super();
this.$element.append('Gantt view');
},
do_show: function () {
this.$element.show();
},
do_hide: function () {
this.$element.hide();
}
});
openerp.base.views.add('tree', 'openerp.base.TreeView');
openerp.base.TreeView = openerp.base.Controller.extend({
/**
* Genuine tree view (the one displayed as a tree, not the list)
* Registry for all the main views
*/
start: function () {
this._super();
this.$element.append('Tree view');
},
do_show: function () {
this.$element.show();
},
do_hide: function () {
this.$element.hide();
}
});
openerp.base.views.add('graph', 'openerp.base.GraphView');
openerp.base.GraphView = openerp.base.Controller.extend({
start: function () {
this._super();
this.$element.append('Graph view');
},
do_show: function () {
this.$element.show();
},
do_hide: function () {
this.$element.hide();
}
});
openerp.base.views = new openerp.base.Registry();
openerp.base.ProcessView = openerp.base.Controller.extend({
});

View File

@ -18,11 +18,7 @@
</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')">
<table border="0" cellpadding="0" cellspacing="0" width="100%" height="100%" class="main_table">
<tr>
<td colspan="2">
<div id="oe_header" class="header"></div>
@ -32,11 +28,8 @@
<tr>
<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>
<td valign="top" height="100%">
<div id="oe_app" class="oe-application"></div>
</td>
</tr>
<tr>
@ -61,8 +54,7 @@
</t>
<t t-name="Header">
<a href="/" class="company_logo_link">
<img t-att-src="typeof kitten == 'undefined' ? '/base/static/src/img/logo.png' :
'http://placekitten.com/g/179/46'" border="0" class="company_logo"/>
<div class="company_logo" />
</a>
<h1 class="header_title" t-if="session.session_is_valid()">
<span class="company">$company</span> - (<span class="database">$database</span>)<br/>
@ -93,13 +85,17 @@
</div>
</t>
<t t-name="Menu">
<ul>
<li t-foreach="data.children" t-as="menu">
<a href="#" t-att-data-menu="menu.id">
<span><t t-esc="menu.name"/></span>
</a>
</li>
</ul>
<table align="center">
<tr>
<t t-foreach="data.children" t-as="menu">
<td>
<a href="#" t-att-data-menu="menu.id">
<span><t t-esc="menu.name"/></span>
</a>
</td>
</t>
</tr>
</table>
</t>
<t t-name="Menu.secondary">
<div style="display: none" class="menu_accordion" t-att-data-menu-parent="menu.id">
@ -135,23 +131,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 +183,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>
@ -212,12 +210,13 @@
<t t-foreach="columns" t-as="column">
<t t-set="attrs" t-value="column.attrs_for(row.data)"/>
<td t-if="column.invisible !== '1'" t-att-title="column.help"
class="oe-field-cell">
class="oe-field-cell" t-att-data-field="column.id">
<t t-if="!attrs.invisible">
<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">
@ -262,7 +261,7 @@
<t t-foreach="row" t-as="td">
<td t-att-colspan="td.colspan gt 1 ? td.colspan : undefined"
t-att-width="td.width ? td.width : undefined"
t-att-nowrap="td.is_field_label ? 'true' : undefined"
t-att-nowrap="td.is_field_label or td.is_field_m2o? 'true' : undefined"
t-att-valign="td.table ? 'top' : undefined"
t-att-id="td.element_id"
t-att-class="'oe_form_' + (td.is_field_label ? 'label' : (td.field ? 'field_' + td.type : td.type))"
@ -299,7 +298,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">
@ -309,6 +308,20 @@
t-att-class="'field_' + widget.type" style="width: 100%"
/>
</t>
<t t-name="FieldEmail">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<td width="100%">
<t t-call="FieldChar"/>
</td>
<td width="16">
<button class="button" title="Send an e-mail with your default e-mail client">
<img src="/base/static/src/img/icons/terp-mail-message-new.png"/>
</button>
</td>
</tr>
</table>
</t>
<t t-name="FieldText">
<textarea rows="6" style="width: 100%;"
t-att-name="widget.name"
@ -323,13 +336,6 @@
t-att-class="'field_' + widget.type"
/>
</t>
<t t-name="FieldDatetime">
<input type="text" style="width: 100%"
t-att-name="widget.name"
t-att-id="widget.element_id + '_field'"
t-att-class="'field_' + widget.type"
/>
</t>
<t t-name="FieldSelection">
<select
t-att-name="widget.name"
@ -348,7 +354,9 @@
t-att-name="widget.name"
t-att-id="widget.element_id + '_field'"
t-att-class="'field_' + widget.type"
t-att-type="widget.type"
style="width: 100%;"/>
<div type='button' class='ui_combo' tabindex="'-1'" title="'Show All Items'"></div>
</t>
<t t-name="FieldOne2Many">
<div t-att-id="widget.element_id">
@ -356,9 +364,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"/>
@ -370,7 +376,18 @@
t-att-class="'field_' + widget.type"/>
</t>
<t t-name="FieldProgressBar">
<div t-opentag="true"></div>
<div t-opentag="true" class="oe-progressbar">
<span></span>
</div>
</t>
<t t-name="FieldImage">
<img align="left" border="1" style="display: none"
t-att-id="widget.element_id + '_field'"
t-att-name="widget.name"
t-att-class="'field_' + widget.type"
t-att-width="widget.node.attrs.img_width || widget.node.attrs.width"
t-att-height="widget.node.attrs.img_height || widget.node.attrs.height"
/>
</t>
<t t-name="WidgetButton">
<button type="button"
@ -385,7 +402,7 @@
<h2 class="oe_view_title"><t t-esc="view.attrs['string']"/></h2>
<form>
<t t-call="SearchView.render_lines"/>
<div style="text-align:right;">
<div class="oe_search-view-buttons" style="text-align:right;">
<input type="submit" value="Search"/>
<input type="reset" value="Clear"/>
</div>
@ -421,7 +438,7 @@
<span t-if="attrs.help">(?)</span>
</label>
<div style="white-space: nowrap;">
<input type="text" t-att-name="attrs.name"
<input type="text" size="15" t-att-name="attrs.name"
t-att-autofocus="attrs.default_focus === '1' ? 'autofocus' : undefined"
t-att-id="element_id"
t-att-value="defaults[attrs.name] || ''"/>
@ -493,9 +510,10 @@
<t t-set="expand" t-value="false"/>
<t t-set="label" t-value="'Custom Filters'"/>
<t t-set="content">
<div class="searchview_extended_groups_list"/>
<div class="searchview_extended_groups_list">
</div>
<button class="searchview_extended_add_group"
type="button">Add group of conditions</button>
type="button"><span>Add group of conditions</span></button>
</t>
</t>
</t>
@ -507,10 +525,10 @@
<option value="none">None of the following conditions must match</option>
</select>
<button class="searchview_extended_delete_group"
type="button">Delete this group of conditions</button>
type="button"><span>Delete this group of conditions</span></button>
<div class="searchview_extended_propositions_list">
</div>
<button class="searchview_extended_add_proposition" type="button">Add condition</button>
<button class="searchview_extended_add_proposition" type="button"><span>Add condition</span></button>
</div>
</t>
<t t-name="SearchView.extended_search.proposition">
@ -526,7 +544,7 @@
<select class="searchview_extended_prop_op"/>
<span class="searchview_extended_prop_value"/>
<button class="searchview_extended_delete_prop"
type="button">Delete this condition</button>
type="button"><span>Delete this condition</span></button>
</div>
</t>
<t t-name="SearchView.extended_search.proposition.char">
@ -538,18 +556,21 @@
</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">
<div class="sidebar-displaying-div">
<t t-set="i" t-value="0"/>
<t t-set="i" t-value="1-1"/> <!-- al do stupid things -->
<t t-foreach="sections" t-as="section">
<t t-if="section.elements.length &gt; 0">
<h2><t t-esc="section.label"/></h2>
<ul>
<t t-set="j" t-value="0"/>
<t t-set="j" t-value="1-1"/>
<t t-foreach="section.elements" t-as="element">
<li><a t-att-data-i="i" t-att-data-j="j" href="#"><t t-esc="element.name"/></a></li>
<t t-set="j" t-value="j+1"/>
@ -570,4 +591,19 @@
</p>
</div>
</t>
<t t-name="Many2XSelectPopup">
<div t-att-id="element_id">
<div t-att-id="element_id + '_search'"></div>
<div t-att-id="element_id + '_view_list'"></div>
<div t-att-id="element_id + '_view_form'"></div>
</div>
</t>
<t t-name="Many2XSelectPopup.search.buttons">
<button type="button" class="oe_many2xselectpopup-search-new">New</button>
<button type="button" class="oe_many2xselectpopup-search-close">Close</button>
</t>
<t t-name="Many2XSelectPopup.form.buttons">
<button type="button" class="oe_many2xselectpopup-form-save">Save</button>
<button type="button" class="oe_many2xselectpopup-form-close">Close</button>
</t>
</templates>

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);
}
});

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)')

View File

@ -17,10 +17,12 @@
<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>
<script src="/base/static/src/js/search.js"></script>
<script src="/base/static/src/js/m2o.js"></script>
<script src="/base/static/src/js/form.js"></script>
<script src="/base/static/src/js/list.js"></script>
<script type="text/javascript">

View File

@ -0,0 +1 @@
import controllers

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
}

View File

@ -0,0 +1 @@
import main

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}

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

View File

@ -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;
}

View File

@ -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;
};
})();

View File

@ -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>

View File

@ -0,0 +1,105 @@
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.flags = {
search_view : false,
sidebar : false,
views_switcher : 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');
}

View File

@ -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>

View File

@ -3,8 +3,8 @@
"version": "2.0",
"depends": ['base'],
"js": [
'static/lib/dhtmlxGantt/codebase/dhtmlxcommon.js',
'static/lib/dhtmlxGantt/codebase/dhtmlxgantt.js',
'static/lib/dhtmlxGantt/sources/dhtmlxcommon.js',
'static/lib/dhtmlxGantt/sources/dhtmlxgantt.js',
'static/src/js/gantt.js'
],
"css": ['static/lib/dhtmlxGantt/codebase/dhtmlxgantt.css'],

View File

@ -1,184 +1,11 @@
import glob, os
from xml.etree import ElementTree
import math
import simplejson
import openerpweb
import time
import datetime
from base.controllers.main import Xml2Json
from base.controllers.main import View
COLOR_PALETTE = ['#f57900', '#cc0000', '#d400a8', '#75507b', '#3465a4', '#73d216', '#c17d11', '#edd400',
'#fcaf3e', '#ef2929', '#ff00c9', '#ad7fa8', '#729fcf', '#8ae234', '#e9b96e', '#fce94f',
'#ff8e00', '#ff0000', '#b0008c', '#9000ff', '#0078ff', '#00ff00', '#e6ff00', '#ffff00',
'#905000', '#9b0000', '#840067', '#510090', '#0000c9', '#009b00', '#9abe00', '#ffc900', ]
_colorline = ['#%02x%02x%02x' % (25 + ((r + 10) % 11) * 23, 5 + ((g + 1) % 11) * 20, 25 + ((b + 4) % 11) * 23) for r in range(11) for g in range(11) for b in range(11) ]
def choice_colors(n):
if n > len(COLOR_PALETTE):
return _colorline[0:-1:len(_colorline) / (n + 1)]
elif n:
return COLOR_PALETTE[:n]
return []
class GanttView(openerpweb.Controller):
class GanttView(View):
_cp_path = "/base_gantt/ganttview"
date_start = None
date_delay = None
date_stop = None
color_field = None
day_length = 8
fields = {}
events = []
calendar_fields = {}
ids = []
model = ''
domain = []
context = {}
event_res = []
colors = {}
color_values = []
@openerpweb.jsonrequest
def load(self, req, model, view_id):
m = req.session.model(model)
r = m.fields_view_get(view_id, 'gantt')
r["arch"] = Xml2Json.convert_to_structure(r["arch"])
return {'fields_view':r}
@openerpweb.jsonrequest
def get_events(self, req, **kw):
fields_view = self.fields_view_get(req, model, view_id, 'gantt')
return {'fields_view':fields_view}
self.model = kw['model']
self.fields = kw['fields']
self.day_length = kw['day_length']
self.calendar_fields = kw['calendar_fields']
self.color_field = kw.get('color_field') or self.color_field or None
self.fields[self.color_field] = ""
self.colors = kw.get('colors') or {}
self.text = self.calendar_fields['text']['name']
self.date_start = self.calendar_fields['date_start']['name']
self.fields[self.date_start] = ""
if self.calendar_fields.get('date_stop'):
self.date_stop = self.calendar_fields['date_stop']['name']
self.fields[self.date_stop] = ""
if self.calendar_fields.get('date_delay'):
self.date_delay = self.calendar_fields['date_delay']['name']
self.fields[self.date_delay] = ""
if self.calendar_fields.get('parent'):
self.parent = self.calendar_fields['parent']['name']
self.fields[self.parent] = ""
model = req.session.model(self.model)
event_ids = model.search([])
return self.create_event(event_ids, model)
def create_event(self, event_ids, model):
self.events = model.read(event_ids, self.fields.keys())
result = []
for evt in self.events:
event_res = {}
key = evt[self.color_field]
name = key
value = key
if isinstance(key, list): # M2O, XMLRPC returns List instead of Tuple
name = key[0]
value = key[-1]
evt[self.color_field] = key = key[-1]
if isinstance(key, tuple): # M2O
value, name = key
self.colors[key] = (name, value, None)
st_date = evt.get(self.date_start)
if st_date:
self.set_format(st_date)
if self.date_delay:
duration = evt.get(self.date_delay)
else:
en_date = evt.get(self.date_stop)
duration = (time.mktime(time.strptime(en_date, self.format))-\
time.mktime(time.strptime(st_date, self.format)))/ (60 * 60)
if duration > self.day_length :
d = math.floor(duration / 24)
h = duration % 24
duration = d * self.day_length + h
event_res = {}
event_res['start_date'] = st_date
event_res['duration'] = duration
event_res['text'] = evt.get(self.text)
event_res['id'] = evt['id']
event_res['parent'] = evt.get(self.parent)
result.append(event_res)
colors = choice_colors(len(self.colors))
for i, (key, value) in enumerate(self.colors.items()):
self.colors[key] = [value[0], value[1], colors[i]]
return {'result': result,'sidebar': self.colors}
def set_format(self, st_date):
if len(st_date) == 10 :
self.format = "%Y-%m-%d"
else :
self.format = "%Y-%m-%d %H:%M:%S"
return
def check_format(self, date):
if self.format == "%Y-%m-%d %H:%M:%S":
date = date + " 00:00:00"
return date
@openerpweb.jsonrequest
def on_event_resize(self, req, **kw):
if self.date_delay:
key = self.date_delay
value = kw['duration']
else:
key = self.date_stop
value = self.check_format(kw['end_date'])
try:
model = req.session.model(self.model)
res = model.write(kw['id'], {key : value})
except Exception, e:
print "eeeeeeeeeeeeeeeeeeeeeeeeeee",e
return True
@openerpweb.jsonrequest
def on_event_drag(self, req, **kw):
start_date = self.check_format(kw['start_date'])
if self.date_delay:
key = self.date_delay
value = kw['duration']
else:
key = self.date_stop
value = self.check_format(kw['end_date'])
try:
model = req.session.model(self.model)
res = model.write(kw['id'], {self.date_start : start_date, key : value})
except Exception, e:
print "eeeeeeeeeeeeeeeeeeeeeeeeeee",e
return True
@openerpweb.jsonrequest
def reload_gantt(self, req, **kw):
model = req.session.model(kw['model'])
if (kw['domain']):
domain = (kw['color_field'],'in', kw['domain'])
event_ids = model.search([domain])
else:
event_ids = model.search([])
return self.create_event(event_ids, model)

View File

@ -173,8 +173,9 @@ GanttProjectInfo.prototype.getTaskByIdInTree = function(parentTask, id)
* @type: public
* @topic: 0
*/
function GanttTaskInfo(id, name, est, duration, percentCompleted, predecessorTaskId)
function GanttTaskInfo(id, name, est, duration, percentCompleted, predecessorTaskId, color)
{
this.color = color || "white";
this.Id = id;
this.Name = name;
this.EST = est;
@ -348,7 +349,7 @@ GanttProject.prototype.create = function()
* @type: public
* @topic: 0
*/
function GanttChart()
function GanttChart(day_length)
{
this.Error = new GanttError();
this.dhtmlXMLSenderObject = new dhtmlXMLSenderObject(this);
@ -356,7 +357,7 @@ function GanttChart()
//settings
this.heightTaskItem = 12;
this.dayInPixels = 24;
this.hoursInDay = 8;
this.hoursInDay = day_length;
this._showTreePanel = true;
this._showTooltip = true;
this.isShowDescTask = false;
@ -4869,11 +4870,13 @@ GanttTask.prototype.createTaskItem = function()
cellTblTask.height = this.Chart.heightTaskItem + "px";
cellTblTask.width = this.TaskInfo.PercentCompleted + "%";
cellTblTask.style.lineHeight = "1px";
cellTblTask.style.backgroundColor = self.TaskInfo.color;
var imgPr = document.createElement("img");
imgPr.style.width = (this.TaskInfo.PercentCompleted * this.TaskInfo.Duration * this.Chart.hourInPixelsWork) / 100 + "px";
imgPr.style.height = this.Chart.heightTaskItem + "px";
cellTblTask.appendChild(imgPr);
imgPr.src = this.Chart.imgs + "progress_filled.png";
//imgPr.src = this.Chart.imgs + "progress_filled.png";
}
if (this.TaskInfo.PercentCompleted != 100)
@ -4943,9 +4946,9 @@ GanttTask.prototype.createTaskItem = function()
}
var taskClick = function() {
self.Chart.callEvent("onTaskClick", [self]);
self.Chart.callEvent("onTaskDblClick", [self]);
};
this.addEvent(divMove, 'click', taskClick, false);
this.addEvent(divMove, 'dblclick', taskClick, false);
if (this.Chart.isEditable)
{

View File

@ -7,7 +7,8 @@ QWeb.add_template('/base_gantt/static/src/xml/base_gantt.xml');
openerp.base.views.add('gantt', 'openerp.base_gantt.GanttView');
openerp.base_gantt.GanttView = openerp.base.Controller.extend({
init: function(view_manager, session, element_id, dataset, view_id) {
init: function(view_manager, session, element_id, dataset, view_id) {
this._super(session, element_id);
this.view_manager = view_manager;
this.dataset = dataset;
@ -16,207 +17,326 @@ openerp.base_gantt.GanttView = openerp.base.Controller.extend({
this.fields_views = {};
this.widgets = {};
this.widgets_counter = 0;
this.fields = {};
this.datarecord = {};
this.fields = this.dataset.fields ? this.dataset.fields: {};
this.ids = this.dataset.ids;
this.name = "";
this.date_start = "";
this.date_delay = "";
this.date_stop = "";
this.color_field = "";
this.day_lenth = 8;
this.colors = [];
this.color_values = [];
this.calendar_fields = {};
this.info_fields = [];
this.domain = this.dataset._domain ? this.dataset._domain: [];
this.context = {};
},
do_show: function () {
// TODO: re-trigger search
this.$element.show();
},
do_hide: function () {
this.$element.hide();
},
start: function() {
this.rpc("/base_gantt/ganttview/load", {"model": this.model, "view_id": this.view_id}, this.on_loaded);
this.rpc("/base_gantt/ganttview/load",
{"model": this.model, "view_id": this.view_id}, this.on_loaded);
},
on_loaded: function(data) {
this.fields_view = data.fields_view;
var self = this;
this.fields_view = data.fields_view;
this.name = this.fields_view.name || this.fields_view.arch.attrs.string;
this.view_id = this.fields_view.view_id;
this.date_start = this.fields_view.arch.attrs.date_start;
this.date_delay = this.fields_view.arch.attrs.date_delay;
this.date_stop = this.fields_view.arch.attrs.date_stop;
this.color_field = this.fields_view.arch.attrs.color;
this.color_field = this.fields_view.arch.attrs.color;
this.day_length = this.fields_view.arch.attrs.day_length || 8;
this.colors = this.fields_view.arch.attrs.colors;
this.fields = this.fields_view.fields;
this.text = this.fields_view.arch.children[0].children[0].attrs.name;
this.parent = this.fields_view.arch.children[0].attrs.link;
this.calendar_fields['parent'] = {'name': this.parent};
this.calendar_fields['date_start'] = {'name': this.date_start};
this.calendar_fields['text'] = {'name': this.text};
if(this.date_delay)
this.calendar_fields['date_delay'] = {'name': this.date_delay};
if(this.date_stop)
this.calendar_fields['date_stop'] = {'name': this.date_stop};
this.format = "yyyy-MM-dd";
self.create_gantt();
self.get_events();
this.calendar_fields['day_length'] = this.day_length;
this.rpc('/base_gantt/ganttview/get_events',
{'model': this.model,
'fields': this.fields,
'color_field': this.color_field,
'day_length': this.day_length,
'calendar_fields': this.calendar_fields,
'colors': this.colors,
'info_fields': this.info_fields
},
function(res) {
self.create_gantt();
self.load_event(res);
})
this.$element.html(QWeb.render("GanttView", {"view": this, "fields_view": this.fields_view}));
},
convert_date_format: function(date) {
date=date+"";
if(typeof (date)!="string"||date.length===0){
return null;
}
var iso=date.split("-");
if(iso.length===0){
return null;
}
var day = iso[2];
var iso_hours = day.split(' ');
if (iso_hours.length > 1) {
day = iso_hours[0];
var iso_date_hours = iso_hours[1].split(':')
var new_date = new Date(iso[0], iso[1] - 1, day);
new_date.setHours(iso_date_hours[0]);
new_date.setMinutes(iso_date_hours[1]);
new_date.setSeconds(iso_date_hours[2]);
}
else {
var new_date = new Date(iso[0], iso[1] - 1, day);
}
new_date.setFullYear(iso[0]);
new_date.setMonth(iso[1]-1);
new_date.setDate(day);
return new_date;
},
create_gantt: function() {
ganttChartControl = new GanttChart();
ganttChartControl = new GanttChart(this.day_length);
ganttChartControl.setImagePath("/base_gantt/static/lib/dhtmlxGantt/codebase/imgs/");
ganttChartControl.setEditable(true);
ganttChartControl.showTreePanel(true);
ganttChartControl.showContextMenu(true);
ganttChartControl.showDescTask(true,'d,s-f');
ganttChartControl.showDescProject(true,'n,d');
},
load_event: function(res) {
var self = this
var result = res.result;
var sidebar = res.sidebar;
var project_id = new Array();
var project = new Array();
var j = -1;
var self = this;
for (i in result) {
var parent_id = result[i]['parent'][0];
var parent_name = result[i]['parent'][1];
if (jQuery.inArray(parent_id, project_id) == -1){
if (parent_id == undefined){
parent_name = "";
}
j = j + 1;
project[j] = new GanttProjectInfo(parent_id, parent_name, new Date(2011, 1, 1));
project_id[j] = parent_id;
}
var id = result[i]['id'];
var text = result[i]['text'];
var start_date = this.convert_date_format(result[i]['start_date']);
var duration = result[i]['duration'];
var task = new GanttTaskInfo(id, text, start_date, duration, 100, "");
k = project_id.indexOf(parent_id);
project[k].addTask(task);
}
for (i in project_id){
ganttChartControl.addProject(project[i]);
}
ganttChartControl.create("GanttDiv");
ganttChartControl.attachEvent("onTaskEndResize", function(task) {self.on_task_end_resize(task);})
ganttChartControl.attachEvent("onTaskEndDrag", function(task) {self.on_task_end_drag(task);})
//Create Sidebar
if (jQuery('#cal-sidebar-option').length == 0){
jQuery('#gantt-sidebar').append(
jQuery('<table>',{'width':'100%','cellspacing': 0, 'cellpadding': 0, 'id':'cal-sidebar-option'})
)
for(s in sidebar) {
jQuery('#cal-sidebar-option').append(
jQuery('<tr>').append(
jQuery('<td>').append(
jQuery('<div>')
.append(
jQuery('<input>',
{
'type': 'checkbox',
'id':sidebar[s][0],
'value':sidebar[s][0]
}).bind('click',function(){
self.reload_gantt(self.color_field,self.model)
}),
sidebar[s][1]
)
.css('background-color',sidebar[s][sidebar[s].length-1])
)
)
)
},
get_events: function() {
var self = this;
this.dataset.read_ids(this.dataset.ids, {}, function(result) {
self.load_event(result);
});
},
load_event: function(events) {
var self = this;
var result = events;
var project = {};
var project_smalldate = {};
var proj_id = [];
var proj_id_text = [];
COLOR_PALETTE = ['#ccccff', '#cc99ff', '#75507b', '#3465a4', '#73d216', '#c17d11', '#edd400',
'#fcaf3e', '#ef2929', '#ff00c9', '#ad7fa8', '#729fcf', '#8ae234', '#e9b96e', '#fce94f',
'#ff8e00', '#ff0000', '#b0008c', '#9000ff', '#0078ff', '#00ff00', '#e6ff00', '#ffff00',
'#905000', '#9b0000', '#840067', '#510090', '#0000c9', '#009b00', '#9abe00', '#ffc900']
//Smallest date of child is parent start date
for (i in result){
var res = result[i];
if (res[this.date_start] != false){
var parent_id = res[this.parent][0] || res[this.parent];
var parent_name = res[this.parent][1];
var start_date = this.convert_str_date(res[this.date_start]);
if (project_smalldate[parent_id] == undefined){
project_smalldate[parent_id] = start_date;
proj_id.push(parent_id);
if (parent_name != undefined){
proj_id_text.push(res[this.parent]);
}
}
else{
if (start_date < project_smalldate[parent_id]){
project_smalldate[parent_id] = start_date;
}
}
}
}
//get parent text
if (parent_name == undefined){
var ajax = {
url: '/base/dataset/call',
async: false
};
this.rpc(ajax, {
model: this.dataset.model,
method: "name_get",
ids: proj_id,
args: ""
}, function(response) {
proj_id_text = response.result;
});
}
//create parents
for (i in proj_id){
var id = proj_id_text[i][0];
var text = proj_id_text[i][1];
project[id] = new GanttProjectInfo(id, text, project_smalldate[id]);
}
//create childs
var k = 0;
var color_box = {};
for (i in result) {
var res = result[i];
if (res[this.date_start] != false){
var parent_id = res[this.parent][0] || res[this.parent];
var id = res['id'];
var text = res[this.text];
var start_date = this.convert_str_date(res[this.date_start]);
var color = res[this.color_field][0] || res[this.color_field];
if (color_box[color] == undefined){
color_box[color] = COLOR_PALETTE[k];
k = k + 1;
}
if (this.date_stop != undefined){
if (res[this.date_stop] != false){
var stop_date = this.convert_str_date(res[this.date_stop]);
var duration= self.hours_between(start_date, stop_date);
}
else{
var duration = 0;
}
}
else{
var duration = res[this.date_delay];
}
if (duration == false)
duration = 0
task = new GanttTaskInfo(id, text, start_date, duration, 100, "", color_box[color]);
project[parent_id].addTask(task);
}
}
//Add parent
for (i in proj_id){
ganttChartControl.addProject(project[proj_id[i]]);
}
ganttChartControl.create("GanttDiv");
ganttChartControl.attachEvent("onTaskStartDrag", function(task) {self.on_drag_start(task);});
ganttChartControl.attachEvent("onTaskEndResize", function(task) {self.on_resize_drag_end(task, "resize");});
ganttChartControl.attachEvent("onTaskEndDrag", function(task) {self.on_resize_drag_end(task, "drag");});
ganttChartControl.attachEvent("onTaskDblClick", function(task) {self.open_popup(task);});
},
reload_gantt: function(color_field, model) {
var domain = [];
hours_between: function(date1, date2) {
var ONE_DAY = 1000 * 60 * 60 * 24;
var date1_ms = date1.getTime();
var date2_ms = date2.getTime();
var difference_ms = Math.abs(date1_ms - date2_ms);
d = Math.round(difference_ms / ONE_DAY);
h = Math.round((difference_ms % ONE_DAY)/(1000 * 60 * 60));
return (d * this.day_length) + h;
},
open_popup : function(task) {
var event_id = task.getId();
if (event_id) {
event_id = parseInt(event_id, 10);
var dataset_event_index = jQuery.inArray(event_id, this.ids);
} else {
var dataset_event_index = null;
}
this.dataset.index = dataset_event_index;
var element_id = _.uniqueId("act_window_dialog");
var dialog = jQuery('<div>',
{'id': element_id
}).dialog({
title: 'Gantt Chart',
modal: true,
minWidth: 800,
position: 'top'
});
var event_form = new openerp.base.FormView(this.view_manager, this.session, element_id, this.dataset, false);
event_form.start();
},
on_drag_start : function(task){
st_date = task.getEST();
if(st_date.getHours()){
self.hh = st_date.getHours();
self.mm = st_date.getMinutes();
}
},
on_resize_drag_end : function(task, evt){
var event_id = task.getId();
var data = {};
if (evt == "drag"){
full_date = task.getEST().set({hour: self.hh, minute : self.mm, second:0});
data[this.date_start] = this.convert_date_str(full_date);
}
if (this.date_stop != undefined){
tm = (task.getDuration() % this.day_length);
stp = task.getFinishDate().add(tm).hour();
data[this.date_stop] = this.convert_date_str(stp);
}else{
data[this.date_delay] = task.getDuration();
}
this.dataset.write(event_id, data, function(result) {});
},
do_show: function () {
this.$element.show();
},
do_hide: function () {
this.$element.hide();
},
convert_str_date: function (str){
if (str.length == 19){
this.format = "yyyy-MM-dd HH:mm:ss";
return openerp.base.parse_datetime(str);
} else if (str.length == 10){
this.format = "yyyy-MM-dd";
return openerp.base.parse_date(str);
} else if (str.length == 8){
this.format = "HH:mm:ss";
return openerp.base.parse_time(str);
}
throw "Unrecognized date/time format";
},
convert_date_str: function(full_date) {
if (this.format == "yyyy-MM-dd HH:mm:ss"){
return openerp.base.format_datetime(full_date);
} else if (this.format == "yyyy-MM-dd"){
return openerp.base.format_date(full_date);
} else if (this.format == "HH:mm:ss"){
return openerp.base.format_time(full_date);
}
throw "Unrecognized date/time format";
},
reload_gantt: function(domain) {
var self = this;
jQuery('input[type=checkbox]:checked','#cal-sidebar-option').each(function() {
domain.push(parseInt(jQuery(this).attr('id')))
var ajax = {
url: '/base/dataset/search_read',
async: false
};
this.rpc(ajax, {
model: this.dataset.model,
domain: self.dataset.domain,
context :self.dataset.context
}, function(response) {
ganttChartControl.clearAll();
jQuery("#GanttDiv").children().remove();
self.load_event(response);
});
},
do_search: function (domains, contexts, groupbys) {
var self = this;
return this.rpc('/base/session/eval_domain_and_context', {
domains: domains,
contexts: contexts,
group_by_seq: groupbys
}, function (results) {
self.dataset.context = results.context;
self.dataset.domain = results.domain;
return self.reload_gantt(self.dataset.domain);
});
this.rpc('/base_gantt/ganttview/reload_gantt',{
'domain':domain,
'color_field':color_field,
'model': model
},function(res) {
ganttChartControl.clearAll();
jQuery("#GanttDiv").children().remove();
self.load_event(res);
});
},
reverse_convert_date_format: function(date) {
return date.getFullYear()+"-"+(date.getMonth()+1)+"-"+date.getDate();
},
on_task_end_resize : function(task) {
this.rpc('/base_gantt/ganttview/on_event_resize',
{'id' : task.getId(),
'end_date' : this.reverse_convert_date_format(task.getFinishDate()),
'duration' : task.getDuration()
},
function(result) {
})
},
on_task_end_drag : function(task) {
this.rpc('/base_gantt/ganttview/on_event_drag',
{'id' : task.getId(),
'start_date' : this.reverse_convert_date_format(task.getEST()),
'end_date' : this.reverse_convert_date_format(task.getFinishDate()),
'duration' : task.getDuration()
},
function(result) {
})
}
});
@ -224,5 +344,4 @@ openerp.base_gantt.GanttView = openerp.base.Controller.extend({
// here you may tweak globals object, if any, and play with on_* or do_* callbacks on them
};
// vim:et fdc=0 fdl=0:

View File

@ -3,12 +3,9 @@
<h3 class="title"><t t-esc="view.fields_view.arch.attrs.string"/></h3>
<table class="gantt-view" width="100%" height="100%" cellspacing="0" cellpadding="0">
<tr>
<td style="width:85%">
<td>
<div style="width:100%;height:300px;position:relative" id="GanttDiv"/>
</td>
<td valign = "top">
<div id="gantt-sidebar"/>
</td>
</tr>
</table>
</t>

View File

@ -111,7 +111,6 @@ openerp.base_graph.GraphView = openerp.base.Controller.extend({
if(this.chart == 'bar') {
return this.schedule_bar(result);
} else if(this.chart == "pie") {
console.log("test");
return this.schedule_pie(result);
}
}
@ -125,7 +124,7 @@ openerp.base_graph.GraphView = openerp.base.Controller.extend({
var res = [];
var COLOR_PALETTE = ['#ff8e00', '#ff0000', '#b0008c', '#9000ff', '#0078ff', '#00ff00', '#e6ff00', '#ffff00',
var COLOR_PALETTE = ['#cc99ff', '#75507b', '#ccccff', '#b0008c', '#ff0000', '#ff8e00', '#9000ff', '#0078ff', '#00ff00', '#e6ff00', '#ffff00',
'#905000', '#9b0000', '#840067', '#9abe00', '#ffc900', '#510090', '#0000c9', '#009b00',
'#75507b', '#3465a4', '#73d216', '#c17d11', '#edd400', '#fcaf3e', '#ef2929', '#ff00c9',
'#ad7fa8', '#729fcf', '#8ae234', '#e9b96e', '#fce94f', '#f57900', '#cc0000', '#d400a8'];
@ -245,8 +244,8 @@ openerp.base_graph.GraphView = openerp.base.Controller.extend({
var val = obj[self.operator_field] / sum * 100 ;
return Math.round(val * 10)/10 + "%";
},
label:"<b>#"+self.chart_info_fields[0]+"#</b>",
gradient:"3d",
height: 20,
legend: {
width: 300,
align:"right",

View File

@ -10,5 +10,6 @@
'static/src/js/web_chat.js'
],
"css": [],
# 'active': True,
'active': False,
}

View File

@ -307,8 +307,6 @@ Javascript
:returns: the dataset's current active id
.. js:class:: openerp.base.DataRecord(session, model, fields, values)
Ad-hoc objects and structural types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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".

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

View File

@ -1,27 +1,3 @@
[global]
server.environment = "development"
# Some server parameters that you may want to tweak
server.socket_host = "0.0.0.0"
server.socket_port = 8080
# Sets the number of threads the server uses
server.thread_pool = 10
tools.sessions.on = True
tools.sessions.persistent = False
# logging
#log.access_file = "/var/log/openerp-web/access.log"
#log.error_file = "/var/log/openerp-web/error.log"
log.access_level = "INFO"
log.error_level = "INFO"
# OpenERP Server
openerp.server.host = 'localhost'
openerp.server.port = '8070'
openerp.server.protocol = 'socket'
openerp.server.timeout = 450
# Regex for dbname %{subdomain}s for the subdomain
#openerp.web.database.list = "%{subdomain}s_.*"
#openerp.web.database.manager = False

88
openerpweb/dates.py Normal file
View File

@ -0,0 +1,88 @@
# -*- 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/>.
#
##############################################################################
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):
"""
Converts a string to a datetime object using OpenERP's
datetime string format (exemple: '2011-12-01 15:12:35').
No timezone information is added, the datetime is a naive instance, but
according to OpenERP 6.1 specification the timezone is always UTC.
"""
if not str:
return str
return datetime.datetime.strptime(str, DEFAULT_SERVER_DATETIME_FORMAT)
def parse_date(str):
"""
Converts a string to a date object using OpenERP's
date string format (exemple: '2011-12-01').
"""
if not str:
return str
return datetime.datetime.strptime(str, DEFAULT_SERVER_DATE_FORMAT).date()
def parse_time(str):
"""
Converts a string to a time object using OpenERP's
time string format (exemple: '15:12:35').
"""
if not str:
return str
return datetime.datetime.strptime(str, DEFAULT_SERVER_TIME_FORMAT).time()
def format_datetime(obj):
"""
Converts a datetime object to a string using OpenERP's
datetime string format (exemple: '2011-12-01 15:12:35').
The datetime instance should not have an attached timezone and be in UTC.
"""
if not obj:
return False
return obj.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
def format_date(obj):
"""
Converts a date object to a string using OpenERP's
date string format (exemple: '2011-12-01').
"""
if not obj:
return False
return obj.strftime(DEFAULT_SERVER_DATE_FORMAT)
def format_time(obj):
"""
Converts a time object to a string using OpenERP's
time string format (exemple: '15:12:35').
"""
if not obj:
return False
return obj.strftime(DEFAULT_SERVER_TIME_FORMAT)

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]

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}
"""
@ -396,6 +422,8 @@ class HttpRequest(object):
self.applicationsession = applicationsession
self.httpsession_id = "cookieid"
self.httpsession = cherrypy.session
self.context = kw.get('context', {})
self.session = self.httpsession.setdefault(kw.get('session_id', None), OpenERPSession())
self.result = ""
print "GET/POST --> %s.%s %s %r" % (controller.__class__.__name__, f.__name__, request, kw)
r = f(controller, self, **kw)
@ -471,30 +499,45 @@ class Root(object):
default.exposed = True
def main(argv):
# Parse config
op = optparse.OptionParser()
op.add_option("-p", "--port", dest="socket_port", help="listening port", metavar="NUMBER", default=8002)
op.add_option("-s", "--session-path", dest="storage_path",
help="directory used for session storage", metavar="DIR",
default=os.path.join(tempfile.gettempdir(), "cpsessions"))
(o, args) = op.parse_args(argv[1:])
# Prepare cherrypy config from options
if not os.path.exists(o.storage_path):
os.mkdir(o.storage_path, 0700)
config = {
'server.socket_port': int(o.socket_port),
# change the timezone of the program to the OpenERP server's assumed timezone
os.environ["TZ"] = "UTC"
DEFAULT_CONFIG = {
'server.socket_port': 8002,
'server.socket_host': '0.0.0.0',
#'server.thread_pool' = 10,
'tools.sessions.on': True,
'tools.sessions.storage_type': 'file',
'tools.sessions.storage_path': o.storage_path,
'tools.sessions.storage_path': os.path.join(tempfile.gettempdir(), "cpsessions"),
'tools.sessions.timeout': 60
}
# Parse config
op = optparse.OptionParser()
op.add_option("-p", "--port", dest="server.socket_port", help="listening port",
type="int", metavar="NUMBER")
op.add_option("-s", "--session-path", dest="tools.sessions.storage_path",
help="directory used for session storage", metavar="DIR")
(o, args) = op.parse_args(argv[1:])
o = vars(o)
for k in o.keys():
if o[k] == None:
del(o[k])
# Setup and run cherrypy
cherrypy.tree.mount(Root())
cherrypy.config.update(config)
cherrypy.config.update(config=DEFAULT_CONFIG)
if os.path.exists(os.path.join(os.path.dirname(
os.path.dirname(__file__)),'openerp-web.cfg')):
cherrypy.config.update(os.path.join(os.path.dirname(
os.path.dirname(__file__)),'openerp-web.cfg'))
if os.path.exists(os.path.expanduser('~/.openerp_webrc')):
cherrypy.config.update(os.path.expanduser('~/.openerp_webrc'))
cherrypy.config.update(o)
if not os.path.exists(cherrypy.config['tools.sessions.storage_path']):
os.mkdir(cherrypy.config['tools.sessions.storage_path'], 0700)
cherrypy.server.subscribe()
cherrypy.engine.start()
cherrypy.engine.block()

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()