new field's responsibility POC: EditableURLWidget allow to edit Bookmark.path in two separated fields, displaying unquoted values which are requoted on form post processing
"""widget classes for form construction:organization: Logilab:copyright: 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.deprecationimportdeprecatedfromcubicwebimporttags,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# does this widget expect a vocabularyvocabulary_widget=Falsedef__init__(self,attrs=None,setdomid=None,settabindex=None):ifattrsisNone:attrs={}self.attrs=attrsifsetdomidisnotNone:# override class's default valueself.setdomid=setdomidifsettabindexisnotNone:# override class's default valueself.settabindex=settabindexdefadd_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):"""render the widget for the given `field` of `form`. To override in concrete class """raiseNotImplementedErrordeftyped_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,qname):try:returnform.formvalues[key]exceptKeyError:continueiffield.name!=qnameandfield.nameinform.formvalues:returnform.formvalues[field.name]returnfield.typed_value(form)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 """qname=field.input_name(form)ifqnameinform.form_previous_values:values=form.form_previous_values[qname]elifqnameinform._cw.form:values=form._cw.form[qname]eliffield.name!=qnameandfield.nameinform._cw.form:# compat: accept attr=value in req.form to specify value of attr-subjectvalues=form._cw.form[field.name]else:values=self.typed_value(form,field)ifvalues!=INTERNAL_FIELD_VALUE:values=self.format_value(form,field,values)ifnotisinstance(values,(tuple,list)):values=(values,)attrs=dict(self.attrs)ifself.setdomid:attrs['id']=field.dom_id(form)ifself.settabindexandnot'tabindex'inattrs:attrs['tabindex']=form._cw.next_tabindex()returnvalues,attrsdefprocess_field_data(self,form,field):posted=form._cw.formval=posted.get(field.input_name(form))ifisinstance(val,basestring):val=val.strip()orNonereturnval@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),values,attrsclassInput(FieldWidget):"""abstract widget class for <input> tag based widgets"""type=Nonedefrender(self,form,field,renderer):"""render the widget for the given `field` of `form`. Generate one <input> tag for each field's value """self.add_media(form)values,attrs=self.values_and_attributes(form,field)# ensure something is renderedifnotvalues:values=(INTERNAL_FIELD_VALUE,)inputs=[tags.input(name=field.input_name(form),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'defrender(self,form,field,renderer):self.add_media(form)values,attrs=self.values_and_attributes(form,field)assertlen(values)==1id=attrs.pop('id')inputs=[tags.input(name=field.input_name(form),value=values[0],type=self.type,id=id,**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>"""defrender(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),**attrs)classFCKEditor(TextArea):"""FCKEditor enabled <textarea>"""def__init__(self,*args,**kwargs):super(FCKEditor,self).__init__(*args,**kwargs)self.attrs['cubicweb:type']='wysiwyg'defrender(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):super(Select,self).__init__(attrs)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),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),type=self.type,value=value,**iattrs)options.append(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))# ajax widgets ################################################################definit_ajax_attributes(attrs,wdgtype,loadtype=u'auto'):try:attrs['klass']+=u' widget'exceptKeyError:attrs['klass']=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']=inputiddefrender(self,form,field,renderer):self.add_media(form)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'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('klass','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(TextInput):"""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). """type='text'defrender(self,form,field,renderer):"""render the widget for the given `field` of `form`. Generate one <input> tag for each field's value """self.add_media(form)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)try:path,qs=value.split('?',1)exceptValueError:path=valueqs=''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>']returnu'\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))