"""widget classes for form construction:organization: Logilab:copyright: 2009-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses"""__docformat__="restructuredtext en"fromdatetimeimportdatefromwarningsimportwarnfromlogilab.mtconverterimportxml_escapefromlogilab.common.deprecationimportdeprecatedfromlogilab.common.dateimporttodatetimefromcubicwebimporttags,uilibfromcubicweb.webimportstdmsgs,INTERNAL_FIELD_VALUE,ProcessFormErrorclassFieldWidget(object):"""abstract widget class"""# javascript / css files required by the widgetneeds_js=()needs_css=()# automatically set id and tabindex attributes ?setdomid=Truesettabindex=True# to ease usage as a sub-widgets (eg widget used by another widget)suffix=None# does this widget expect a vocabularyvocabulary_widget=Falsedef__init__(self,attrs=None,setdomid=None,settabindex=None,suffix=None):ifattrsisNone:attrs={}self.attrs=attrsifsetdomidisnotNone:# override class's default valueself.setdomid=setdomidifsettabindexisnotNone:# override class's default valueself.settabindex=settabindexifsuffixisnotNone:self.suffix=suffixdefadd_media(self,form):"""adds media (CSS & JS) required by this widget"""ifself.needs_js:form._cw.add_js(self.needs_js)ifself.needs_css:form._cw.add_css(self.needs_css)defrender(self,form,field,renderer=None):self.add_media(form)returnself._render(form,field,renderer)def_render(self,form,field,renderer):raiseNotImplementedError()defformat_value(self,form,field,value):returnfield.format_value(form._cw,value)defvalues_and_attributes(self,form,field):"""found field's *string* value in: 1. previously submitted form values if any (eg on validation error) 2. req.form 3. extra form values given to render() 4. field's typed value values found in 1. and 2. are expected te be already some 'display' value while those found in 3. and 4. are expected to be correctly typed. 3 and 4 are handle by the .typed_value(form, field) method """attrs=dict(self.attrs)ifself.setdomid:attrs['id']=field.dom_id(form,self.suffix)ifself.settabindexandnot'tabindex'inattrs:attrs['tabindex']=form._cw.next_tabindex()returnself.values(form,field),attrsdefvalues(self,form,field):values=Noneifnotfield.ignore_req_params:qname=field.input_name(form,self.suffix)# value from a previous post that has raised a validation errorifqnameinform.form_previous_values:values=form.form_previous_values[qname]# value specified using form parameterselifqnameinform._cw.form:values=form._cw.form[qname]eliffield.name!=qnameandfield.nameinform._cw.form:# XXX compat: accept attr=value in req.form to specify value of# attr-subjectvalues=form._cw.form[field.name]ifvaluesisNone:values=self.typed_value(form,field)ifvalues!=INTERNAL_FIELD_VALUE:values=self.format_value(form,field,values)ifnotisinstance(values,(tuple,list)):values=(values,)returnvaluesdeftyped_value(self,form,field):"""return field's *typed* value specified in: 3. extra form values given to render() 4. field's typed value """qname=field.input_name(form)forkeyin((field,form),qname):try:returnform.formvalues[key]exceptKeyError:continueiffield.name!=qnameandfield.nameinform.formvalues:returnform.formvalues[field.name]returnfield.typed_value(form)defprocess_field_data(self,form,field):posted=form._cw.formval=posted.get(field.input_name(form,self.suffix))ifisinstance(val,basestring):val=val.strip()returnval@deprecated('[3.6] use values_and_attributes')def_render_attrs(self,form,field):"""return html tag name, attributes and a list of values for the field """values,attrs=self.values_and_attributes(form,field)returnfield.input_name(form,self.suffix),values,attrsclassInput(FieldWidget):"""abstract widget class for <input> tag based widgets"""type=Nonedef_render(self,form,field,renderer):"""render the widget for the given `field` of `form`. Generate one <input> tag for each field's value """values,attrs=self.values_and_attributes(form,field)# ensure something is renderedifnotvalues:values=(INTERNAL_FIELD_VALUE,)inputs=[tags.input(name=field.input_name(form,self.suffix),type=self.type,value=value,**attrs)forvalueinvalues]returnu'\n'.join(inputs)# basic html widgets ###########################################################classTextInput(Input):"""<input type='text'>"""type='text'classPasswordInput(Input):"""<input type='password'> and its confirmation field (using <field's name>-confirm as name) """type='password'def_render(self,form,field,renderer):assertself.suffixisNone,'suffix not supported'values,attrs=self.values_and_attributes(form,field)assertlen(values)==1domid=attrs.pop('id')inputs=[tags.input(name=field.input_name(form),value=values[0],type=self.type,id=domid,**attrs),'<br/>',tags.input(name=field.input_name(form,'-confirm'),value=values[0],type=self.type,**attrs),' ',tags.span(form._cw._('confirm password'),**{'class':'emphasis'})]returnu'\n'.join(inputs)defprocess_field_data(self,form,field):passwd1=super(PasswordInput,self).process_field_data(form,field)passwd2=form._cw.form.get(field.input_name(form,'-confirm'))ifpasswd1==passwd2:ifpasswd1isNone:returnNonereturnpasswd1.encode('utf-8')raiseProcessFormError(form._cw._("password and confirmation don't match"))classPasswordSingleInput(Input):"""<input type='password'> without a confirmation field"""type='password'defprocess_field_data(self,form,field):value=super(PasswordSingleInput,self).process_field_data(form,field)ifvalueisnotNone:returnvalue.encode('utf-8')returnvalueclassFileInput(Input):"""<input type='file'>"""type='file'defvalues_and_attributes(self,form,field):# ignore value which makes no sense here (XXX even on form validation error?)values,attrs=super(FileInput,self).values_and_attributes(form,field)return('',),attrsclassHiddenInput(Input):"""<input type='hidden'>"""type='hidden'setdomid=False# by default, don't set id attribute on hidden inputsettabindex=FalseclassButtonInput(Input):"""<input type='button'> if you want a global form button, look at the Button, SubmitButton, ResetButton and ImgButton classes below. """type='button'classTextArea(FieldWidget):"""<textarea>"""def_render(self,form,field,renderer):values,attrs=self.values_and_attributes(form,field)attrs.setdefault('onkeyup','autogrow(this)')ifnotvalues:value=u''eliflen(values)==1:value=values[0]else:raiseValueError('a textarea is not supposed to be multivalued')lines=value.splitlines()linecount=len(lines)forlineinlines:linecount+=len(line)/80attrs.setdefault('cols',80)attrs.setdefault('rows',min(15,linecount+2))returntags.textarea(value,name=field.input_name(form,self.suffix),**attrs)classFCKEditor(TextArea):"""FCKEditor enabled <textarea>"""def__init__(self,*args,**kwargs):super(FCKEditor,self).__init__(*args,**kwargs)self.attrs['cubicweb:type']='wysiwyg'def_render(self,form,field,renderer):form._cw.fckeditor_config()returnsuper(FCKEditor,self)._render(form,field,renderer)classSelect(FieldWidget):"""<select>, for field having a specific vocabulary"""vocabulary_widget=Truedef__init__(self,attrs=None,multiple=False,**kwargs):super(Select,self).__init__(attrs,**kwargs)self._multiple=multipledefrender(self,form,field,renderer):curvalues,attrs=self.values_and_attributes(form,field)ifnot'size'inattrs:attrs['size']=self._multipleand'5'or'1'options=[]optgroup_opened=Falseforoptioninfield.vocabulary(form):try:label,value,oattrs=optionexceptValueError:label,value=optionoattrs={}ifvalueisNone:# handle separatorifoptgroup_opened:options.append(u'</optgroup>')oattrs.setdefault('label',labelor'')options.append(u'<optgroup %s>'%uilib.sgml_attributes(oattrs))optgroup_opened=Trueelifvalueincurvalues:options.append(tags.option(label,value=value,selected='selected',**oattrs))else:options.append(tags.option(label,value=value,**oattrs))ifoptgroup_opened:options.append(u'</optgroup>')returntags.select(name=field.input_name(form,self.suffix),multiple=self._multiple,options=options,**attrs)classCheckBox(Input):"""<input type='checkbox'>, for field having a specific vocabulary. One input will be generated for each possible value. """type='checkbox'vocabulary_widget=Truedefrender(self,form,field,renderer):curvalues,attrs=self.values_and_attributes(form,field)domid=attrs.pop('id',None)sep=attrs.pop('separator',u'<br/>\n')options=[]fori,optioninenumerate(field.vocabulary(form)):try:label,value,oattrs=optionexceptValueError:label,value=optionoattrs={}iattrs=attrs.copy()iattrs.update(oattrs)ifi==0anddomidisnotNone:iattrs.setdefault('id',domid)ifvalueincurvalues:iattrs['checked']=u'checked'tag=tags.input(name=field.input_name(form,self.suffix),type=self.type,value=value,**iattrs)options.append(u'%s %s'%(tag,label))returnsep.join(options)classRadio(CheckBox):"""<input type='radio'>, for field having a specific vocabulary. One input will be generated for each possible value. """type='radio'# compound widgets #############################################################classIntervalWidget(FieldWidget):"""custom widget to display an interval composed by 2 fields. This widget is expected to be used with a CompoundField containing the two actual fields. Exemple usage::from uicfg import autoform_field, autoform_sectionautoform_field.tag_attribute(('Concert', 'minprice'), CompoundField(fields=(IntField(name='minprice'), IntField(name='maxprice')), label=_('price'), widget=IntervalWidget() ))# we've to hide the other field manually for nowautoform_section.tag_attribute(('Concert', 'maxprice'), 'generated') """defrender(self,form,field,renderer):actual_fields=field.fieldsassertlen(actual_fields)==2returnu'<div>%s%s%s%s</div>'%(form._cw._('from_interval_start'),actual_fields[0].render(form,renderer),form._cw._('to_interval_end'),actual_fields[1].render(form,renderer),)classHorizontalLayoutWidget(FieldWidget):"""custom widget to display a set of fields grouped together horizontally in a form. See `IntervalWidget` for example usage. """defrender(self,form,field,renderer):ifself.attrs.get('display_label',True):subst=self.attrs.get('label_input_substitution','%(label)s%(input)s')fields=[subst%{'label':renderer.render_label(form,f),'input':f.render(form,renderer)}forfinfield.subfields(form)]else:fields=[f.render(form,renderer)forfinfield.subfields(form)]returnu'<div>%s</div>'%' '.join(fields)# javascript widgets ###########################################################classDateTimePicker(TextInput):"""<input type='text' + javascript date/time picker for date or datetime fields """monthnames=('january','february','march','april','may','june','july','august','september','october','november','december')daynames=('monday','tuesday','wednesday','thursday','friday','saturday','sunday')needs_js=('cubicweb.calendar.js',)needs_css=('cubicweb.calendar_popup.css',)@classmethoddefadd_localized_infos(cls,req):"""inserts JS variables defining localized months and days"""# import here to avoid dependancy from cubicweb to simplejson_=req._monthnames=[_(mname)formnameincls.monthnames]daynames=[_(dname)fordnameincls.daynames]req.html_headers.define_var('MONTHNAMES',monthnames)req.html_headers.define_var('DAYNAMES',daynames)defrender(self,form,field,renderer):txtwidget=super(DateTimePicker,self).render(form,field,renderer)self.add_localized_infos(form._cw)cal_button=self._render_calendar_popup(form,field)returntxtwidget+cal_buttondef_render_calendar_popup(self,form,field):value=field.typed_value(form)ifnotvalue:value=date.today()inputid=field.dom_id(form)helperid='%shelper'%inputidyear,month=value.year,value.monthreturn(u"""<a onclick="toggleCalendar('%s', '%s', %s, %s);" class="calhelper"><img src="%s" title="%s" alt="" /></a><div class="calpopup hidden" id="%s"></div>"""%(helperid,inputid,year,month,form._cw.external_resource('CALENDAR_ICON'),form._cw._('calendar'),helperid))classJQueryDatePicker(FieldWidget):"""use jquery.ui.datepicker to define a date time picker"""needs_js=('jquery.ui.js',)needs_css=('jquery.ui.css',)def__init__(self,datestr=None,**kwargs):super(JQueryDatePicker,self).__init__(**kwargs)self.datestr=datestrdef_render(self,form,field,renderer):req=form._cwdomid=field.dom_id(form,self.suffix)# XXX find a way to understand every formatfmt=req.property_value('ui.date-format')fmt=fmt.replace('%Y','yy').replace('%m','mm').replace('%d','dd')req.add_onload(u'jqNode("%s").datepicker(''{buttonImage: "%s", dateFormat: "%s", firstDay: 1,'' showOn: "button", buttonImageOnly: true})'%(domid,req.external_resource('CALENDAR_ICON'),fmt))ifself.datestrisNone:value=self.values(form,field)[0]else:value=self.datestrreturntags.input(id=domid,name=domid,value=value,type='text',size='10')classJQueryTimePicker(FieldWidget):"""use jquery.timePicker.js to define a js time picker"""needs_js=('jquery.timePicker.js',)needs_css=('jquery.timepicker.css',)def__init__(self,timestr=None,timesteps=30,separator=u':',**kwargs):super(JQueryTimePicker,self).__init__(**kwargs)self.timestr=timestrself.timesteps=timestepsself.separator=separatordef_render(self,form,field,renderer):req=form._cwdomid=field.dom_id(form,self.suffix)req.add_onload(u'jqNode("%s").timePicker({selectedTime: "%s", step: %s, separator: "%s"})'%(domid,self.timestr,self.timesteps,self.separator))ifself.timestrisNone:value=self.values(form,field)[0]else:value=self.timestrreturntags.input(id=domid,name=domid,value=value,type='text',size='5')classJQueryDateTimePicker(FieldWidget):def__init__(self,initialtime=None,timesteps=15,**kwargs):super(JQueryDateTimePicker,self).__init__(**kwargs)self.initialtime=initialtimeself.timesteps=timestepsdef_render(self,form,field,renderer):"""render the widget for the given `field` of `form`. Generate one <input> tag for each field's value """req=form._cwdateqname=field.input_name(form,'date')timeqname=field.input_name(form,'time')ifdateqnameinform.form_previous_values:datestr=form.form_previous_values[dateqname]timestr=form.form_previous_values[timeqname]else:datestr=timestr=u''iffield.nameinreq.form:value=req.parse_datetime(req.form[field.name])else:value=self.typed_value(form,field)ifvalue:datestr=req.format_date(value)timestr=req.format_time(value)elifself.initialtime:timestr=req.format_time(self.initialtime)datepicker=JQueryDatePicker(datestr=datestr,suffix='date')timepicker=JQueryTimePicker(timestr=timestr,timesteps=self.timesteps,suffix='time')returnu'<div id="%s">%s%s</div>'%(field.dom_id(form),datepicker.render(form,field),timepicker.render(form,field))defprocess_field_data(self,form,field):req=form._cwdatestr=req.form.get(field.input_name(form,'date')).strip()orNonetimestr=req.form.get(field.input_name(form,'time')).strip()orNoneifdatestrisNone:returnNonedate=todatetime(req.parse_datetime(datestr,'Date'))iftimestrisNone:returndatetime=req.parse_datetime(timestr,'Time')returndate.replace(hour=time.hour,minute=time.minute,second=time.second)# ajax widgets ################################################################definit_ajax_attributes(attrs,wdgtype,loadtype=u'auto'):try:attrs['class']+=u' widget'exceptKeyError:attrs['class']=u'widget'attrs.setdefault('cubicweb:wdgtype',wdgtype)attrs.setdefault('cubicweb:loadtype',loadtype)classAjaxWidget(FieldWidget):"""simple <div> based ajax widget"""def__init__(self,wdgtype,inputid=None,**kwargs):super(AjaxWidget,self).__init__(**kwargs)init_ajax_attributes(self.attrs,wdgtype)ifinputidisnotNone:self.attrs['cubicweb:inputid']=inputiddef_render(self,form,field,renderer):attrs=self.values_and_attributes(form,field)[-1]returntags.div(**attrs)classAutoCompletionWidget(TextInput):"""ajax widget for StringField, proposing matching existing values as you type. """needs_js=('cubicweb.widgets.js','jquery.autocomplete.js')needs_css=('jquery.autocomplete.css',)wdgtype='SuggestField'loadtype='auto'def__init__(self,*args,**kwargs):try:self.autocomplete_initfunc=kwargs.pop('autocomplete_initfunc')exceptKeyError:warn('use autocomplete_initfunc argument of %s constructor ''instead of relying on autocomplete_initfuncs dictionary on ''the entity class'%self.__class__.__name__,DeprecationWarning)self.autocomplete_initfunc=Nonesuper(AutoCompletionWidget,self).__init__(*args,**kwargs)defvalues_and_attributes(self,form,field):values,attrs=super(AutoCompletionWidget,self).values_and_attributes(form,field)init_ajax_attributes(attrs,self.wdgtype,self.loadtype)# XXX entity form specificattrs['cubicweb:dataurl']=self._get_url(form.edited_entity,field)ifnotvalues:values=('',)returnvalues,attrsdef_get_url(self,entity,field):ifself.autocomplete_initfuncisNone:# XXX for bw compatfname=entity.autocomplete_initfuncs[field.name]else:fname=self.autocomplete_initfuncreturnentity._cw.build_url('json',fname=fname,mode='remote',pageid=entity._cw.pageid)classStaticFileAutoCompletionWidget(AutoCompletionWidget):"""XXX describe me"""wdgtype='StaticFileSuggestField'def_get_url(self,entity,field):ifself.autocomplete_initfuncisNone:# XXX for bw compatfname=entity.autocomplete_initfuncs[field.name]else:fname=self.autocomplete_initfuncreturnentity._cw.datadir_url+fnameclassRestrictedAutoCompletionWidget(AutoCompletionWidget):"""XXX describe me"""wdgtype='RestrictedSuggestField'classLazyRestrictedAutoCompletionWidget(RestrictedAutoCompletionWidget):"""remote autocomplete """wdgtype='LazySuggestField'defvalues_and_attributes(self,form,field):self.add_media(form)"""override values_and_attributes to handle initial displayed values"""values,attrs=super(LazyRestrictedAutoCompletionWidget,self).values_and_attributes(form,field)assertlen(values)==1,"multiple selection is not supported yet by LazyWidget"ifnotvalues[0]:values=form.cw_extra_kwargs.get(field.name,'')ifnotisinstance(values,(tuple,list)):values=(values,)try:values=list(values)values[0]=int(values[0])attrs['cubicweb:initialvalue']=values[0]values=(self.display_value_for(form,values[0]),)except(TypeError,ValueError):passreturnvalues,attrsdefdisplay_value_for(self,form,value):entity=form._cw.entity_from_eid(value)returnentity.view('combobox')classAddComboBoxWidget(Select):defvalues_and_attributes(self,form,field):values,attrs=super(AddComboBoxWidget,self).values_and_attributes(form,field)init_ajax_attributes(self.attrs,'AddComboBox')# XXX entity form specificentity=form.edited_entityattrs['cubicweb:etype_to']=entity.e_schemaetype_from=entity.e_schema.subjrels[field.name].objects(entity.e_schema)[0]attrs['cubicweb:etype_from']=etype_fromreturnvalues,attrsdefrender(self,form,field,renderer):returnsuper(AddComboBoxWidget,self).render(form,field,renderer)+u'''<div id="newvalue"> <input type="text" id="newopt" /> <a href="javascript:noop()" id="add_newopt"> </a></div>'''# buttons ######################################################################classButton(Input):"""<input type='button'>, base class for global form buttons note label is a msgid which will be translated at form generation time, you should not give an already translated string. """type='button'def__init__(self,label=stdmsgs.BUTTON_OK,attrs=None,setdomid=None,settabindex=None,name='',value='',onclick=None,cwaction=None):super(Button,self).__init__(attrs,setdomid,settabindex)ifisinstance(label,tuple):self.label=label[0]self.icon=label[1]else:self.label=labelself.icon=Noneself.name=nameself.value=''self.onclick=onclickself.cwaction=cwactionself.attrs.setdefault('class','validateButton')defrender(self,form,field=None,renderer=None):label=form._cw._(self.label)attrs=self.attrs.copy()ifself.cwaction:assertself.onclickisNoneattrs['onclick']="postForm('__action_%s', \'%s\', \'%s\')"%(self.cwaction,self.label,form.domid)elifself.onclick:attrs['onclick']=self.onclickifself.name:attrs['name']=self.nameifself.setdomid:attrs['id']=self.nameifself.settabindexandnot'tabindex'inattrs:attrs['tabindex']=form._cw.next_tabindex()ifself.icon:img=tags.img(src=form._cw.external_resource(self.icon),alt=self.icon)else:img=u''returntags.button(img+xml_escape(label),escapecontent=False,value=label,type=self.type,**attrs)classSubmitButton(Button):"""<input type='submit'>, main button to submit a form"""type='submit'classResetButton(Button):"""<input type='reset'>, main button to reset a form. You usually don't want this. """type='reset'classImgButton(object):"""<img> wrapped into a <a> tag with href triggering something (usually a javascript call) note label is a msgid which will be translated at form generation time, you should not give an already translated string. """def__init__(self,domid,href,label,imgressource):self.domid=domidself.href=hrefself.imgressource=imgressourceself.label=labeldefrender(self,form,field=None,renderer=None):label=form._cw._(self.label)imgsrc=form._cw.external_resource(self.imgressource)return'<a id="%(domid)s" href="%(href)s">'\'<img src="%(imgsrc)s" alt="%(label)s"/>%(label)s</a>'%{'label':label,'imgsrc':imgsrc,'domid':self.domid,'href':self.href}# more widgets #################################################################classEditableURLWidget(FieldWidget):"""custom widget to edit separatly an url path / query string (used by default for Bookmark.path for instance), dealing with url quoting nicely (eg user edit the unquoted value). """def_render(self,form,field,renderer):"""render the widget for the given `field` of `form`. Generate one <input> tag for each field's value """assertself.suffixisNone,'not supported'req=form._cwpathqname=field.input_name(form,'path')fqsqname=field.input_name(form,'fqs')# formatted query stringifpathqnameinform.form_previous_values:path=form.form_previous_values[pathqname]fqs=form.form_previous_values[fqsqname]else:iffield.nameinreq.form:value=req.form[field.name]else:value=self.typed_value(form,field)ifvalue:try:path,qs=value.split('?',1)exceptValueError:path=valueqs=''else:path=qs=''fqs=u'\n'.join(u'%s=%s'%(k,v)fork,vinreq.url_parse_qsl(qs))attrs=dict(self.attrs)ifself.setdomid:attrs['id']=field.dom_id(form)ifself.settabindexandnot'tabindex'inattrs:attrs['tabindex']=req.next_tabindex()# ensure something is renderedinputs=[u'<table><tr><th>',req._('i18n_bookmark_url_path'),u'</th><td>',tags.input(name=pathqname,type='string',value=path,**attrs),u'</td></tr><tr><th>',req._('i18n_bookmark_url_fqs'),u'</th><td>']ifself.setdomid:attrs['id']=field.dom_id(form,'fqs')ifself.settabindex:attrs['tabindex']=req.next_tabindex()attrs.setdefault('onkeyup','autogrow(this)')inputs+=[tags.textarea(fqs,name=fqsqname,**attrs),u'</td></tr></table>']# surrounding div necessary for proper error localizationreturnu'<div id="%s">%s</div>'%(field.dom_id(form),u'\n'.join(inputs))defprocess_field_data(self,form,field):req=form._cwvalues={}path=req.form.get(field.input_name(form,'path'))ifisinstance(path,basestring):path=path.strip()orNonefqs=req.form.get(field.input_name(form,'fqs'))ifisinstance(fqs,basestring):fqs=fqs.strip()orNoneiffqs:fori,lineinenumerate(fqs.split('\n')):line=line.strip()ifline:try:key,val=line.split('=',1)exceptValueError:raiseProcessFormError(req._("wrong query parameter line %s")%(i+1))# value will be url quoted by build_url_paramsvalues.setdefault(key.encode(req.encoding),[]).append(val)ifnotvalues:returnpathreturnu'%s?%s'%(path,req.build_url_params(**values))