web/formfields.py
author sylvain.thenault@logilab.fr
Thu, 12 Mar 2009 19:39:34 +0100
branchtls-sprint
changeset 1101 0c067de38e46
parent 1095 6917ebe281e9
child 1104 58f27c3c0167
permissions -rw-r--r--
unification of meta-attributes handling: * remove has_format, has_text_encoding, format, text_encoding methods on Entity * remove rich_text_fields method on EntitySchema * use instead has_metadata / meta_attribute on EntitySchema (from yams) and attribute_metadata on Entity

"""field classes for form construction

:organization: Logilab
:copyright: 2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"

from datetime import datetime

from logilab.mtconverter import html_escape
from yams.constraints import SizeConstraint, StaticVocabularyConstraint

from cubicweb.schema import FormatConstraint
from cubicweb.utils import ustrftime
from cubicweb.common import tags, uilib
from cubicweb.web import INTERNAL_FIELD_VALUE
from cubicweb.web.formwidgets import (HiddenInput, TextInput, FileInput, PasswordInput,
                                      TextArea, FCKEditor, Radio, Select,
                                      DateTimePicker) 

class Field(object):
    """field class is introduced to control what's displayed in edition form
    """
    widget = TextInput
    needs_multipart = False
    creation_rank = 0

    def __init__(self, name=None, id=None, label=None,
                 widget=None, required=False, initial=None,
                 choices=None, help=None, eidparam=False, role='subject'):
        self.required = required
        if widget is not None:
            self.widget = widget
        if isinstance(self.widget, type):
            self.widget = self.widget()
        self.name = name
        self.label = label or name
        self.id = id or name
        self.initial = initial
        self.choices = choices
        self.help = help
        self.eidparam = eidparam
        self.role = role
        # global fields ordering in forms
        self.creation_rank = Field.creation_rank
        Field.creation_rank += 1

    def __unicode__(self):
        return u'<%s name=%r label=%r id=%r initial=%r>' % (
            self.__class__.__name__, self.name, self.label,
            self.id, self.initial)

    def __repr__(self):
        return self.__unicode__().encode('utf-8')

    def set_name(self, name):
        assert name
        self.name = name
        if not self.id:
            self.id = name
        if not self.label:
            self.label = name
            
    def is_visible(self):
        return not isinstance(self.widget, HiddenInput)
    
    def actual_fields(self, form):
        yield self
    
    def format_value(self, req, value):
        if isinstance(value, (list, tuple)):
            return [self.format_single_value(req, val) for val in value]
        return self.format_single_value(req, value)
    
    def format_single_value(self, req, value):
        if value is None:
            return u''
        return unicode(value)

    def get_widget(self, form):
        return self.widget
    
    def example_format(self, req):
        return u''

    def render(self, form, renderer):
        return self.get_widget(form).render(form, self)

    def vocabulary(self, form):
        if self.choices is not None:
            return self.choices
        return form.form_field_vocabulary(self)

    
class StringField(Field):
    def __init__(self, max_length=None, **kwargs):
        super(StringField, self).__init__(**kwargs)
        self.max_length = max_length


class TextField(Field):
    widget = TextArea
    def __init__(self, rows=10, cols=80, **kwargs):
        super(TextField, self).__init__(**kwargs)
        self.rows = rows
        self.cols = cols


class RichTextField(TextField):
    widget = None
    def __init__(self, format_field=None, **kwargs):
        super(RichTextField, self).__init__(**kwargs)
        self.format_field = format_field

    def get_widget(self, form):
        if self.widget is None:
            if self.use_fckeditor(form):
                return FCKEditor()
            return TextArea()
        return self.widget

    def get_format_field(self, form):
        if self.format_field:
            return self.format_field
        # we have to cache generated field since it's use as key in the
        # context dictionnary
        req = form.req
        try:
            return req.data[self]
        except KeyError:
            if self.use_fckeditor(form):
                # if fckeditor is used and format field isn't explicitly
                # deactivated, we want an hidden field for the format
                widget = HiddenInput()
                choices = None
            else:
                # else we want a format selector
                # XXX compute vocabulary
                widget = Select
                fcstr = FormatConstraint()
                choices = [(req._(fmt), fmt) for fmt in fcstr.vocabulary(req=req)]
            field = StringField(name=self.name + '_format', widget=widget,
                                choices=choices)
            req.data[self] = field
            return field
    
    def actual_fields(self, form):
        yield self
        format_field = self.get_format_field(form)
        if format_field:
            yield format_field
            
    def use_fckeditor(self, form):
        """return True if fckeditor should be used to edit entity's attribute named
        `attr`, according to user preferences
        """
        if form.req.use_fckeditor():
            return form.form_field_format(self) == 'text/html'
        return False

    def render(self, form, renderer):
        format_field = self.get_format_field(form)
        if format_field:
            result = format_field.render(form, renderer)
        else:
            result = u''
        return result + self.get_widget(form).render(form, self)

    
class FileField(StringField):
    widget = FileInput
    needs_multipart = True
    
    def __init__(self, format_field=None, encoding_field=None, **kwargs):
        super(FileField, self).__init__(**kwargs)
        self.format_field = format_field
        self.encoding_field = encoding_field
        
    def actual_fields(self, form):
        yield self
        if self.format_field:
            yield self.format_field
        if self.encoding_field:
            yield self.encoding_field

    def render(self, form, renderer):
        wdgs = [self.get_widget(form).render(form, self)]
        if self.format_field or self.encoding_field:
            divid = '%s-advanced' % form.context[self]['name']
            wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' %
                        (html_escape(uilib.toggle_action(divid)),
                         form.req._('show advanced fields'),
                         html_escape(form.req.build_url('data/puce_down.png')),
                         form.req._('show advanced fields')))
            wdgs.append(u'<div id="%s" class="hidden">' % divid)
            if self.format_field:
                wdgs.append(self.render_subfield(form, self.format_field, renderer))
            if self.encoding_field:
                wdgs.append(self.render_subfield(form, self.encoding_field, renderer))
            wdgs.append(u'</div>')            
        if not self.required and form.context[self]['value']:
            # trick to be able to delete an uploaded file
            wdgs.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'))
        return u'\n'.join(wdgs)

    def render_subfield(self, form, field, renderer):
        return (renderer.render_label(form, field)
                + field.render(form, renderer)
                + renderer.render_help(form, field)
                + u'<br/>')

        
class EditableFileField(FileField):
    editable_formats = ('text/plain', 'text/html', 'text/rest')
    
    def render(self, form, renderer):
        wdgs = [super(EditableFileField, self).render(form, renderer)]
        if form.form_field_format(self) in self.editable_formats:
            data = form.form_field_value(self, {}, load_bytes=True)
            if data:
                encoding = form.form_field_encoding(self)
                try:
                    form.context[self]['value'] = unicode(data.getvalue(), encoding)
                except UnicodeError:
                    pass
                else:
                    if not self.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))
                    # XXX restore form context?
        return '\n'.join(wdgs)

        
class IntField(Field):
    def __init__(self, min=None, max=None, **kwargs):
        super(IntField, self).__init__(**kwargs)
        self.min = min
        self.max = max


class BooleanField(Field):
    widget = Radio
        
    def vocabulary(self, form):
        if self.choices:
            return self.choices
        return [(form.req._('yes'), '1'), (form.req._('no'), '')]


class FloatField(IntField):    
    def format_single_value(self, req, value):
        formatstr = req.property_value('ui.float-format')
        if value is None:
            return u''
        return formatstr % float(value)

    def render_example(self, req):
        return self.format_value(req, 1.234)


class DateField(StringField):
    format_prop = 'ui.date-format'
    widget = DateTimePicker
    
    def format_single_value(self, req, value):
        return value and ustrftime(value, req.property_value(self.format_prop)) or u''

    def render_example(self, req):
        return self.format_value(req, datetime.now())


class DateTimeField(DateField):
    format_prop = 'ui.datetime-format'


class TimeField(DateField):
    format_prop = 'ui.datetime-format'


class HiddenInitialValueField(Field):
    def __init__(self, visible_field, name):
        super(HiddenInitialValueField, self).__init__(name=name,
                                                      widget=HiddenInput,
                                                      eidparam=True)
        self.visible_field = visible_field
    
                 
class RelationField(Field):
    def __init__(self, **kwargs):
        super(RelationField, self).__init__(**kwargs)

    @staticmethod
    def fromcardinality(card, **kwargs):
        return RelationField(widget=Select(multiple=card in '*+'), **kwargs)
        
    def vocabulary(self, form):
        entity = form.entity
        req = entity.req
        # first see if its specified by __linkto form parameters
        linkedto = entity.linked_to(self.name, self.role)
        if linkedto:
            entities = (req.eid_rset(eid).get_entity(0, 0) for eid in linkedto)
            return [(entity.view('combobox'), entity.eid) for entity in entities]
        # it isn't, check if the entity provides a method to get correct values
        res = []
        if not self.required:
            res.append(('', INTERNAL_FIELD_VALUE))
        # vocabulary doesn't include current values, add them
        if entity.has_eid():
            rset = entity.related(self.name, self.role)
            relatedvocab = [(e.view('combobox'), e.eid) for e in rset.entities()]
        else:
            relatedvocab = []
        return res + form.form_field_vocabulary(self) + relatedvocab
    
    def format_single_value(self, req, value):
        return value


def stringfield_from_constraints(constraints, **kwargs):
    field = None
    for cstr in constraints:
        if isinstance(cstr, StaticVocabularyConstraint):
            return StringField(widget=Select(vocabulary=cstr.vocabulary),
                               **kwargs)
        if isinstance(cstr, SizeConstraint) and cstr.max is not None:
            if cstr.max > 257:
                rows_cols_from_constraint(cstr, kwargs)
                field = TextField(**kwargs)
            else:
                field = StringField(max_length=cstr.max, **kwargs)
    return field or TextField(**kwargs)


def rows_cols_from_constraint(constraint, kwargs):
    if constraint.max < 513:
        rows, cols = 5, 60
    else:
        rows, cols = 10, 80
    kwargs.setdefault('rows', rows)
    kwargs.setdefault('cols', cols)


def guess_field(eclass, rschema, role='subject', **kwargs):
    """return the most adapated widget to edit the relation
    'subjschema rschema objschema' according to information found in the schema
    """
    fieldclass = None
    eschema = eclass.e_schema
    if role == 'subject':
        targetschema = rschema.objects(eschema)[0]
        card = rschema.rproperty(eschema, targetschema, 'cardinality')[0]
    else:
        targetschema = rschema.subjects(eschema)[0]
        card = rschema.rproperty(targetschema, eschema, 'cardinality')[1]
    kwargs['required'] = card in '1+'
    kwargs['name'] = rschema.type
    if rschema.is_final():
        if rschema in eschema.format_fields:
            return None
        if targetschema == 'Password':
            return StringField(widget=PasswordInput(), **kwargs)
        if eschema.has_metadata(rschema, 'format'):
            constraints = rschema.rproperty(eschema, targetschema, 'constraints')
            for cstr in constraints:
                if isinstance(cstr, StaticVocabularyConstraint):
                    raise Exception('rich text field with static vocabulary')
                if isinstance(cstr, SizeConstraint) and cstr.max is not None:
                    rows_cols_from_constraint(cstr, kwargs)
            return RichTextField(**kwargs)
        fieldclass = FIELDS[targetschema]
        if fieldclass is StringField:
            constraints = rschema.rproperty(eschema, targetschema, 'constraints')
            return stringfield_from_constraints(constraints, **kwargs)
        return fieldclass(**kwargs)
    kwargs['role'] = role
    return RelationField.fromcardinality(card, **kwargs)

FIELDS = {
    'Boolean':  BooleanField,
    'Bytes':    FileField,
    'Date':     DateField,
    'Datetime': DateTimeField,
    'Int':      IntField,
    'Float':    FloatField,
    'Decimal':  StringField,
    'Password': StringField,
    'String' :  StringField,
    'Time':     TimeField,
    }