"""field classes for form construction:organization: Logilab:copyright: 2009 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"fromwarningsimportwarnfromdatetimeimportdatetimefromlogilab.mtconverterimportxml_escapefromyams.constraintsimport(SizeConstraint,StaticVocabularyConstraint,FormatConstraint)fromcubicweb.utilsimportustrftimefromcubicweb.commonimporttags,uilibfromcubicweb.webimportINTERNAL_FIELD_VALUEfromcubicweb.web.formwidgetsimport(HiddenInput,TextInput,FileInput,PasswordInput,TextArea,FCKEditor,Radio,Select,DateTimePicker)defvocab_sort(vocab):"""sort vocabulary, considering option groups"""result=[]partresult=[]forlabel,valueinvocab:ifvalueisNone:# opt group startifpartresult:result+=sorted(partresult)partresult=[]result.append((label,value))else:partresult.append((label,value))result+=sorted(partresult)returnresultclassField(object):"""field class is introduced to control what's displayed in forms. It makes the link between something to edit and its display in the form. Actual display is handled by a widget associated to the field. Attributes ---------- all the attributes described below have sensible default value which may be overriden by value given to field's constructor. :name: name of the field (basestring), should be unique in a form. :id: dom identifier (default to the same value as `name`), should be unique in a form. :label: label of the field (default to the same value as `name`). :help: help message about this field. :widget: widget associated to the field. Each field class has a default widget class which may be overriden per instance. :required: bool flag telling if the field is required or not. :initial: initial value, used when no value specified by other means. :choices: static vocabulary for this field. May be a list of values or a list of (label, value) tuples if specified. :sort: bool flag telling if the vocabulary (either static vocabulary specified in `choices` or dynamic vocabulary fetched from the form) should be sorted on label. :internationalizable: bool flag telling if the vocabulary labels should be translated using the current request language. :eidparam: bool flag telling if this field is linked to a specific entity :role: when the field is linked to an entity attribute or relation, tells the role of the entity in the relation (eg 'subject' or 'object') :fieldset: optional fieldset to which this field belongs to """# default widget associated to this class of fields. May be overriden per# instancewidget=TextInput# does this field requires a multipart formneeds_multipart=False# class attribute used for ordering of fields in a form__creation_rank=0def__init__(self,name=None,id=None,label=None,help=None,widget=None,required=False,initial=None,choices=None,sort=True,internationalizable=False,eidparam=False,role='subject',fieldset=None):self.name=nameself.id=idornameself.label=labelornameself.help=helpself.required=requiredself.initial=initialself.choices=choicesself.sort=sortself.internationalizable=internationalizableself.eidparam=eidparamself.role=roleself.fieldset=fieldsetself.init_widget(widget)# ordering number for this field instanceself.creation_rank=Field.__creation_rankField.__creation_rank+=1def__unicode__(self):returnu'<%s name=%r label=%r id=%r initial=%r visible=%r @%x>'%(self.__class__.__name__,self.name,self.label,self.id,self.initial,self.is_visible(),id(self))def__repr__(self):returnself.__unicode__().encode('utf-8')definit_widget(self,widget):ifwidgetisnotNone:self.widget=widgetelifself.choicesandnotself.widget.vocabulary_widget:self.widget=Select()ifisinstance(self.widget,type):self.widget=self.widget()defset_name(self,name):"""automatically set .id and .label when name is set"""assertnameself.name=nameifnotself.id:self.id=nameifnotself.label:self.label=namedefis_visible(self):"""return true if the field is not an hidden field"""returnnotisinstance(self.widget,HiddenInput)defactual_fields(self,form):"""return actual fields composing this field in case of a compound field, usually simply return self """yieldselfdefformat_value(self,req,value):"""return value suitable for display where value may be a list or tuple of values """ifisinstance(value,(list,tuple)):return[self.format_single_value(req,val)forvalinvalue]returnself.format_single_value(req,value)defformat_single_value(self,req,value):"""return value suitable for display"""ifvalueisNoneorvalueisFalse:returnu''ifvalueisTrue:returnu'1'returnunicode(value)defget_widget(self,form):"""return the widget instance associated to this field"""returnself.widgetdefexample_format(self,req):"""return a sample string describing what can be given as input for this field """returnu''defrender(self,form,renderer):"""render this field, which is part of form, using the given form renderer """widget=self.get_widget(form)try:returnwidget.render(form,self,renderer)exceptTypeError:warn('[3.3] %s: widget.render now take the renderer as third argument, ''please update implementation'%widget,DeprecationWarning)returnwidget.render(form,self)defvocabulary(self,form):"""return vocabulary for this field. This method will be called by widgets which desire it."""ifself.choicesisnotNone:ifcallable(self.choices):try:vocab=self.choices(form=form)exceptTypeError:warn('[3.3] vocabulary method (eg field.choices) should now take ''the form instance as argument',DeprecationWarning)vocab=self.choices(req=form.req)else:vocab=self.choicesifvocabandnotisinstance(vocab[0],(list,tuple)):vocab=[(x,x)forxinvocab]else:vocab=form.form_field_vocabulary(self)ifself.internationalizable:# the short-cirtcuit 'and' boolean operator is used here to permit# a valid empty string in vocabulary without attempting to translate# it by gettext (which can lead to weird strings display)vocab=[(labelandform.req._(label),value)forlabel,valueinvocab]ifself.sort:vocab=vocab_sort(vocab)returnvocabdefform_init(self,form):"""method called before by build_context to trigger potential field initialization requiring the form instance """passclassStringField(Field):widget=TextAreadef__init__(self,max_length=None,**kwargs):self.max_length=max_length# must be set before super callsuper(StringField,self).__init__(**kwargs)definit_widget(self,widget):ifwidgetisNone:ifself.choices:widget=Select()elifself.max_lengthandself.max_length<257:widget=TextInput()super(StringField,self).init_widget(widget)ifisinstance(self.widget,TextArea):self.init_text_area(self.widget)elifisinstance(self.widget,TextInput):self.init_text_input(self.widget)definit_text_input(self,widget):ifself.max_length:widget.attrs.setdefault('size',min(45,self.max_length))widget.attrs.setdefault('maxlength',self.max_length)definit_text_area(self,widget):ifself.max_length<513:widget.attrs.setdefault('cols',60)widget.attrs.setdefault('rows',5)classRichTextField(StringField):widget=Nonedef__init__(self,format_field=None,**kwargs):super(RichTextField,self).__init__(**kwargs)self.format_field=format_fielddefinit_text_area(self,widget):passdefget_widget(self,form):ifself.widgetisNone:ifself.use_fckeditor(form):returnFCKEditor()widget=TextArea()self.init_text_area(widget)returnwidgetreturnself.widgetdefget_format_field(self,form):ifself.format_field:returnself.format_field# we have to cache generated field since it's use as key in the# context dictionnaryreq=form.reqtry:returnreq.data[self]exceptKeyError:fkwargs={'eidparam':self.eidparam}ifself.use_fckeditor(form):# if fckeditor is used and format field isn't explicitly# deactivated, we want an hidden field for the formatfkwargs['widget']=HiddenInput()fkwargs['initial']='text/html'else:# else we want a format selectorfkwargs['widget']=Select()fcstr=FormatConstraint()fkwargs['choices']=fcstr.vocabulary(form=form)fkwargs['internationalizable']=Truefkwargs['initial']=lambdaf:f.form_field_format(self)field=StringField(name=self.name+'_format',**fkwargs)req.data[self]=fieldreturnfielddefactual_fields(self,form):yieldselfformat_field=self.get_format_field(form)ifformat_field:yieldformat_fielddefuse_fckeditor(self,form):"""return True if fckeditor should be used to edit entity's attribute named `attr`, according to user preferences """ifform.req.use_fckeditor():returnform.form_field_format(self)=='text/html'returnFalsedefrender(self,form,renderer):format_field=self.get_format_field(form)ifformat_field:# XXX we want both fields to remain vertically alignedifformat_field.is_visible():format_field.widget.attrs['style']='display: block'result=format_field.render(form,renderer)else:result=u''returnresult+self.get_widget(form).render(form,self,renderer)classFileField(StringField):widget=FileInputneeds_multipart=Truedef__init__(self,format_field=None,encoding_field=None,name_field=None,**kwargs):super(FileField,self).__init__(**kwargs)self.format_field=format_fieldself.encoding_field=encoding_fieldself.name_field=name_fielddefactual_fields(self,form):yieldselfifself.format_field:yieldself.format_fieldifself.encoding_field:yieldself.encoding_fieldifself.name_field:yieldself.name_fielddefrender(self,form,renderer):wdgs=[self.get_widget(form).render(form,self,renderer)]ifself.format_fieldorself.encoding_field:divid='%s-advanced'%form.context[self]['name']wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>'%(xml_escape(uilib.toggle_action(divid)),form.req._('show advanced fields'),xml_escape(form.req.build_url('data/puce_down.png')),form.req._('show advanced fields')))wdgs.append(u'<div id="%s" class="hidden">'%divid)ifself.name_field:wdgs.append(self.render_subfield(form,self.name_field,renderer))ifself.format_field:wdgs.append(self.render_subfield(form,self.format_field,renderer))ifself.encoding_field:wdgs.append(self.render_subfield(form,self.encoding_field,renderer))wdgs.append(u'</div>')ifnotself.requiredandform.context[self]['value']:# trick to be able to delete an uploaded filewdgs.append(u'<br/>')wdgs.append(tags.input(name=u'%s__detach'%form.context[self]['name'],type=u'checkbox'))wdgs.append(form.req._('detach attached file'))returnu'\n'.join(wdgs)defrender_subfield(self,form,field,renderer):return(renderer.render_label(form,field)+field.render(form,renderer)+renderer.render_help(form,field)+u'<br/>')classEditableFileField(FileField):editable_formats=('text/plain','text/html','text/rest')defrender(self,form,renderer):wdgs=[super(EditableFileField,self).render(form,renderer)]ifform.form_field_format(self)inself.editable_formats:data=form.form_field_value(self,load_bytes=True)ifdata:encoding=form.form_field_encoding(self)try:form.context[self]['value']=unicode(data.getvalue(),encoding)exceptUnicodeError:passelse:ifnotself.required:msg=form.req._('You can either submit a new file using the browse button above'', or choose to remove already uploaded file by checking the ''"detach attached file" check-box, or edit file content online ''with the widget below.')else:msg=form.req._('You can either submit a new file using the browse button above'', or edit file content online with the widget below.')wdgs.append(u'<p><b>%s</b></p>'%msg)wdgs.append(TextArea(setdomid=False).render(form,self,renderer))# XXX restore form context?return'\n'.join(wdgs)classIntField(Field):def__init__(self,min=None,max=None,**kwargs):super(IntField,self).__init__(**kwargs)self.min=minself.max=maxifisinstance(self.widget,TextInput):self.widget.attrs.setdefault('size',5)self.widget.attrs.setdefault('maxlength',15)classBooleanField(Field):widget=Radiodefvocabulary(self,form):ifself.choices:returnself.choicesreturn[(form.req._('yes'),'1'),(form.req._('no'),'')]classFloatField(IntField):defformat_single_value(self,req,value):formatstr=req.property_value('ui.float-format')ifvalueisNone:returnu''returnformatstr%float(value)defrender_example(self,req):returnself.format_single_value(req,1.234)classDateField(StringField):format_prop='ui.date-format'widget=DateTimePickerdefformat_single_value(self,req,value):returnvalueandustrftime(value,req.property_value(self.format_prop))oru''defrender_example(self,req):returnself.format_single_value(req,datetime.now())classDateTimeField(DateField):format_prop='ui.datetime-format'classTimeField(DateField):format_prop='ui.time-format'widget=TextInputclassHiddenInitialValueField(Field):def__init__(self,visible_field):name='edit%s-%s'%(visible_field.role[0],visible_field.name)super(HiddenInitialValueField,self).__init__(name=name,widget=HiddenInput,eidparam=True)self.visible_field=visible_fielddefformat_single_value(self,req,value):returnself.visible_field.format_single_value(req,value)classRelationField(Field):# XXX (syt): iirc, we originaly don't sort relation vocabulary since we want# to let entity.unrelated_rql control this, usually to get most recently# modified entities in the select box instead of by alphabetical order. Now,# we first use unrelated_rql to get the vocabulary, which may be limited# (hence we get the latest modified entities) and we can sort here for# better readability## def __init__(self, **kwargs):# kwargs.setdefault('sort', False)# super(RelationField, self).__init__(**kwargs)@staticmethoddeffromcardinality(card,**kwargs):kwargs.setdefault('widget',Select(multiple=cardin'*+'))returnRelationField(**kwargs)defvocabulary(self,form):entity=form.edited_entityreq=entity.req# first see if its specified by __linkto form parameterslinkedto=entity.linked_to(self.name,self.role)iflinkedto:entities=(req.entity_from_eid(eid)foreidinlinkedto)return[(entity.view('combobox'),entity.eid)forentityinentities]# it isn't, check if the entity provides a method to get correct valuesres=[]ifnotself.required:res.append(('',INTERNAL_FIELD_VALUE))# vocabulary doesn't include current values, add themifentity.has_eid():rset=entity.related(self.name,self.role)relatedvocab=[(e.view('combobox'),e.eid)foreinrset.entities()]else:relatedvocab=[]vocab=res+form.form_field_vocabulary(self)+relatedvocabifself.sort:vocab=vocab_sort(vocab)returnvocabdefformat_single_value(self,req,value):returnvalueclassCompoundField(Field):def__init__(self,fields,*args,**kwargs):super(CompoundField,self).__init__(*args,**kwargs)self.fields=fieldsdefsubfields(self,form):returnself.fieldsdefactual_fields(self,form):return[self]+list(self.fields)defguess_field(eschema,rschema,role='subject',skip_meta_attr=True,**kwargs):"""return the most adapated widget to edit the relation 'subjschema rschema objschema' according to information found in the schema """fieldclass=Nonecard=eschema.cardinality(rschema,role)ifrole=='subject':targetschema=rschema.objects(eschema)[0]help=rschema.rproperty(eschema,targetschema,'description')ifrschema.final:ifrschema.rproperty(eschema,targetschema,'internationalizable'):kwargs.setdefault('internationalizable',True)defget_default(form,es=eschema,rs=rschema):returnes.default(rs)kwargs.setdefault('initial',get_default)else:targetschema=rschema.subjects(eschema)[0]help=rschema.rproperty(targetschema,eschema,'description')kwargs['required']=cardin'1+'kwargs['name']=rschema.typeifrole=='object':kwargs.setdefault('label',(eschema.type,rschema.type+'_object'))else:kwargs.setdefault('label',(eschema.type,rschema.type))kwargs.setdefault('help',help)ifrschema.final:ifskip_meta_attrandrschemaineschema.meta_attributes():returnNonefieldclass=FIELDS[targetschema]iffieldclassisStringField:iftargetschema=='Password':# special case for Password field: specific PasswordInput widgetkwargs.setdefault('widget',PasswordInput())returnStringField(**kwargs)ifeschema.has_metadata(rschema,'format'):# use RichTextField instead of StringField if the attribute has# a "format" metadata. But getting information from constraints# may be useful anyway...constraints=rschema.rproperty(eschema,targetschema,'constraints')forcstrinconstraints:ifisinstance(cstr,StaticVocabularyConstraint):raiseException('rich text field with static vocabulary')returnRichTextField(**kwargs)constraints=rschema.rproperty(eschema,targetschema,'constraints')# init StringField parameters according to constraintsforcstrinconstraints:ifisinstance(cstr,StaticVocabularyConstraint):kwargs.setdefault('choices',cstr.vocabulary)breakforcstrinconstraints:ifisinstance(cstr,SizeConstraint)andcstr.maxisnotNone:kwargs['max_length']=cstr.maxreturnStringField(**kwargs)iffieldclassisFileField:formetadatain('format','encoding','name'):metaschema=eschema.has_metadata(rschema,metadata)ifmetaschemaisnotNone:kwargs['%s_field'%metadata]=guess_field(eschema,metaschema,skip_meta_attr=False)returnfieldclass(**kwargs)kwargs['role']=rolereturnRelationField.fromcardinality(card,**kwargs)FIELDS={'Boolean':BooleanField,'Bytes':FileField,'Date':DateField,'Datetime':DateTimeField,'Int':IntField,'Float':FloatField,'Decimal':StringField,'Password':StringField,'String':StringField,'Time':TimeField,}