Example #1
0
class Form(object):
    moya_render_targets = ['html']

    def __init__(self,
                 element,
                 context,
                 app,
                 style,
                 template,
                 action,
                 enctype,
                 _class=None):
        self.element = element
        self.context = context
        self.app = app
        self.style = style
        self.template = template
        self.action = action
        self.enctype = enctype
        setattr(self, 'class', _class)

        self._fields = OrderedDict()
        self.bound = False
        self.validated = False
        self.raw_data = {}
        self._data = {}
        self.error = None
        self.errors = ErrorContainer(self._fields)
        self.id = make_id()
        self.current_field_id = 1

        self.legend = element.legend(context)

        self.root = RootFormRenderable(self, template)
        self.current_node = self.root
        self.content = Content(app, template)
        self.content.new_section('fields', None)

        self.field_validators = defaultdict(list)
        self.field_adapters = defaultdict(list)
        self.field_applyers = defaultdict(list)

    def __repr__(self):
        return "<form '{}'>".format(self.element.libid)

    def reset(self):
        """Reset the form to a blank state"""
        self._data = {}
        self.raw_data = {}
        for field in self.all_fields:
            field.value = None

    @property
    def renderables(self):
        return self.root.children

    @property
    def ok(self):
        return self.validated and not self.errors and not self.error

    @property
    def fields(self):
        return [field_list[0] for field_list in itervalues(self._fields)]

    @property
    def fields_map(self):
        return {k: v[0] for k, v in self._fields.items()}

    def get_value(self):
        return self

    @property
    def data(self):
        return self._data

    @property
    def current_section(self):
        return self

    def on_content_insert(self, content):
        content.merge(self.content, ignore_sections=["fields"])

    def update_field_value(self, name, value):
        for field in self._fields[name]:
            field.value = value

    def set_field_data(self, name, value):
        self._data[name] = value

    def update(self, value_map):
        if value_map:
            for k, v in value_map.items():
                if k in self._fields:
                    self.update_field_value(k, v)

    __moyaupdate__ = update

    def __contains__(self, key):
        return key in self._fields and self._fields[key] is not None

    @property
    def all_fields(self):
        for field_list in itervalues(self._fields):
            for field in field_list:
                yield field

    def add_field(self,
                  params,
                  enctype=None,
                  style=None,
                  template=None,
                  default=None,
                  process_value=None,
                  adapt_value=None,
                  data=None):
        if enctype is not None and self.enctype is None:
            self.enctype = enctype
        style = style or params.pop('style', None) or self.style
        field = Field(self.app,
                      default=default,
                      template=template,
                      process_value=process_value,
                      adapt_value=adapt_value,
                      style=style,
                      data=data,
                      **params)
        field.id = "field{}_{}".format(self.id, self.current_field_id)
        self.current_field_id += 1
        if params.get('name'):
            name = params['name']
            self._fields.setdefault(name, []).append(field)
        return field

    def add_field_validator(self, field_name, element):
        self.field_validators[field_name].append(element)

    def add_field_adapter(self, field_name, element):
        self.field_adapters[field_name].append(element)

    def add_field_applyer(self, field_name, element):
        self.field_applyers[field_name].append(element)

    def get_field_validators(self, field_name):
        return self.field_validators[field_name]

    def get_field_adapters(self, field_name):
        return self.field_adapters[field_name]

    def push_node(self):
        self.current_node = self.current_node.children[-1]

    def pop_node(self):
        self.current_node = self.current_node.parent

    def add_renderable(self, name, renderable):
        self.current_node.add_child(renderable)

    def add_fail(self, field, msg):
        """Add a message to a list of errors for a given field"""
        self._fields[field][0].errors.append(msg)
        self.errors[field].append(msg)

    def set_fail(self, field, msg):
        """Replace the current list of errors for a field"""
        self._fields[field][0].errors[:] = [msg]
        self.errors[field][:] = [msg]

    def __moyaconsole__(self, console):
        if self.bound:
            console.text("Bound form", fg="cyan", bold=True)
        else:
            console.text("Unbound form", fg="blue", bold=True)

        table = []
        for field in self.all_fields:
            if field.value is None:
                v = Cell('None', dim=True)
            else:
                v = field.value
            table.append([field.name, v])
        console.table(table, ['name', 'value'])

        if self.validated:
            if self.errors:
                console.text("Form errors", fg="red", bold=True)
                error_table = [(field, '\n'.join('* %s' % e.strip()
                                                 for e in _errors))
                               for field, _errors in iteritems(self.errors)]
                console.table(error_table, ["field", "error"])
            else:
                if self.error:
                    console.text('Form error "%s"' % self.error.strip(),
                                 fg="red",
                                 bold=True)
                else:
                    console.text("Validated, no errors", fg="green", bold=True)

    def fill_initial(self, context):
        for field in self.all_fields():
            if field.initial is not None:
                value = field.initial
                field.value = self.raw_data[field.value] = value

    def get_initial_binding(self, context):
        binding = {}
        for field in self.all_fields:
            if field.initial is not None:
                value = field.initial
                binding[field.name] = field.adapt_value(context, value=value)
        return binding

    def fill(self, obj):
        for field in self:
            field.value = obj.get(field.name, None)

    def get_binding(self, context, bind):
        binding = {}
        if not bind:
            return binding

        with context.data_frame(bind):
            for field in self.all_fields:
                if field.name:
                    binding[field.name] = field.process_context_value(context)
        return binding

    def get_src_binding(self, context, bind_root):
        binding = {}
        with context.frame(bind_root):
            for field in self.all_fields:
                if field.src and field.src in context:
                    value = context[field.src]
                    value = field.process_value(context, value=value)
                    binding[field.name] = value
        return binding

    def bind(self, context, *bindings):
        for field in self.all_fields:
            for binding in reversed(bindings):
                if field.name in binding:
                    value = binding[field.name]
                    if value is not None:
                        field.value = self.raw_data[field.name] = value
                        break
        self.bound = True

    def moya_render(self, archive, context, target, options):
        form_template = self.template
        if form_template is None:
            form_template = "/moya.forms/styles/%s/form.html" % self.style
        self.content.template = form_template
        self.content.td['form'] = self
        return self.content.moya_render(archive, context, target, options)
Example #2
0
class Form(AttributeExposer):
    moya_render_targets = ['html']

    __moya_exposed_attributes__ = sorted(['action',
                                          'app',
                                          'bound',
                                          'content',
                                          'id',
                                          'template',
                                          'csrf_check',
                                          'class',
                                          'csrf',
                                          'csrf_token',
                                          'data',
                                          'element',
                                          'enctype',
                                          'raw_data',
                                          'error',
                                          'errors',
                                          'fields',
                                          'legend',
                                          'ok',
                                          'style',
                                          'validated'])

    def __init__(self, element, context, app, style, template, action, enctype, csrf=True, _class=None):
        super(Form, self).__init__()
        self.element = element
        self.context = context
        self.app = app
        self.style = style
        self.template = template
        self.action = action
        self.enctype = enctype
        self.csrf = csrf
        setattr(self, 'class', _class)

        self._fields = OrderedDict()
        self.bound = False
        self.validated = False
        self.raw_data = {}
        self._data = {}
        self.error = None
        self.errors = ErrorContainer(self._fields)
        self.id = make_id()
        self.current_field_id = 1

        self.legend = element.legend(context)

        self.root = RootFormRenderable(self, template)
        self.current_node = self.root
        self.content = Content(app, template)
        self.content.new_section('fields', None)

        self.field_validators = defaultdict(list)
        self.field_adapters = defaultdict(list)
        self.field_applyers = defaultdict(list)

    def add_csrf(self):
        context = self.context
        field_data = {
            'name': '_moya_csrf',
            'fieldname': 'hidden',
            'src': None,
            'dst': None,
            'initial': self.csrf_token,
            'required': True,
            'label': 'csrf',
            'visible': False,
            'type': 'text',
            '_if': True
        }
        field = self.add_field(field_data, template=None)
        content = context['.content']
        context['field'] = field
        content.add_renderable('hiddeninput', field)

    def __repr__(self):
        return "<form '{}'>".format(self.element.libid)

    def reset(self):
        """Reset the form to a blank state"""
        self._data = {}
        self.raw_data = {}
        for field in self.all_fields:
            field.value = None

    @property
    def csrf_token(self):
        """Return a csrf token"""
        context = self.context
        user_id = text_type(context['.session_key'] or '')
        form_id = self.element.libid
        secret = text_type(self.element.archive.secret)
        raw_token = "{}{}{}".format(user_id, secret, form_id).encode('utf-8', 'ignore')
        m = hashlib.md5()
        m.update(raw_token)
        token_hash = m.hexdigest()
        return token_hash

    def validate_csrf(self, context):
        """Validate CSRF token and raise forbidden error if it fails"""
        if not self.csrf:
            return
        if context['.user'] and context['.request.method'] in ('POST', 'PUT', 'DELETE'):
            csrf = context['.request.POST._moya_csrf']
            if csrf != self.csrf_token:
                request = context['.request']
                if request:
                    security_log.info('''CSRF detected on request "%s %s" referer='%s' user='******'''',
                                      request.method, request.url, request.referer, context['.user.username'])
                raise logic.EndLogic(http.RespondForbidden())

    @property
    def csrf_check(self):
        if not self.csrf:
            return True
        context = self.context
        if context['.user'] and context['.request.method'] in ('POST', 'PUT', 'DELETE'):
            csrf = context['.request.POST._moya_csrf']
            if csrf != self.csrf_token:
                request = context['.request']
                if request:
                    security_log.info('''CSRF detected on request "%s %s" referer='%s' user='******'''',
                                      request.method, request.url, request.referer, context['.user.username'])
                return False
        return True

    @property
    def renderables(self):
        return self.root.children

    @property
    def ok(self):
        return self.validated and not self.errors and not self.error

    @property
    def fields(self):
        return [field_list[0] for field_list in itervalues(self._fields)]

    @property
    def fields_map(self):
        return {k: v[0] for k, v in self._fields.items()}

    def get_value(self):
        return self

    @property
    def data(self):
        return self._data

    @property
    def current_section(self):
        return self

    def on_content_insert(self, content):
        content.merge(self.content, ignore_sections=["fields"])

    def update_field_value(self, name, value):
        for field in self._fields[name]:
            field.value = value

    def set_field_data(self, name, value):
        self._data[name] = value

    def update(self, value_map):
        if value_map:
            for k, v in value_map.items():
                if k in self._fields:
                    self.update_field_value(k, v)
    __moyaupdate__ = update

    def __contains__(self, key):
        return key in self._fields and self._fields[key] is not None

    @property
    def all_fields(self):
        for field_list in itervalues(self._fields):
            for field in field_list:
                yield field

    def add_field(self,
                  params,
                  enctype=None,
                  style=None,
                  template=None,
                  default=None,
                  process_value=None,
                  adapt_value=None,
                  data=None):
        if enctype is not None and self.enctype is None:
            self.enctype = enctype
        style = style or params.pop('style', None) or self.style
        field = Field(self.app,
                      default=default,
                      template=template,
                      process_value=process_value,
                      adapt_value=adapt_value,
                      style=style,
                      data=data,
                      **params)

        field.id = "field{}_{}".format(self.id, self.current_field_id)
        self.current_field_id += 1
        if params.get('name'):
            name = params['name']
            self._fields.setdefault(name, []).append(field)
        return field

    def add_field_validator(self, field_name, element):
        self.field_validators[field_name].append(element)

    def add_field_adapter(self, field_name, element):
        self.field_adapters[field_name].append(element)

    def add_field_applyer(self, field_name, element):
        self.field_applyers[field_name].append(element)

    def get_field_validators(self, field_name):
        return self.field_validators[field_name]

    def get_field_adapters(self, field_name):
        return self.field_adapters[field_name]

    def push_node(self):
        self.current_node = self.current_node.children[-1]

    def pop_node(self):
        self.current_node = self.current_node.parent

    def add_renderable(self, name, renderable):
        self.current_node.add_child(renderable)

    def add_fail(self, field, msg):
        """Add a message to a list of errors for a given field"""
        self._fields[field][0].errors.append(msg)
        self.errors[field].append(msg)

    def set_fail(self, field, msg):
        """Replace the current list of errors for a field"""
        self._fields[field][0].errors[:] = [msg]
        self.errors[field][:] = [msg]

    def __moyaconsole__(self, console):
        if self.bound:
            console.text("Bound form", fg="cyan", bold=True)
        else:
            console.text("Unbound form", fg="blue", bold=True)

        table = []
        for field in self.all_fields:
            if field.value is None:
                v = Cell('None', dim=True)
            else:
                v = field.value
            table.append([field.name, v])
        console.table(table, ['name', 'value'])

        if self.validated:
            if self.errors:
                console.text("Form errors", fg="red", bold=True)
                error_table = [(field, '\n' .join('* %s' % e.strip() for e in _errors))
                               for field, _errors in iteritems(self.errors)]
                console.table(error_table, ["field", "error"])
            else:
                if self.error:
                    console.text('Form error "%s"' % self.error.strip(), fg="red", bold=True)
                else:
                    console.text("Validated, no errors", fg="green", bold=True)

        if not self.csrf_check:
            console.text('CSRF check failed -- form did not originate from here!', fg="red", bold=True)

    # def fill_initial(self, context):
    #     for field in self.all_fields:
    #         if field.initial is not None:
    #             value = field.initial
    #             field.value = self.raw_data[field.value] = value

    def get_initial_binding(self, context):
        binding = {}
        for field in self.all_fields:
            if field.initial is not None:
                value = field.initial
                binding[field.name] = field.adapt_value(context, value=value)
        return binding

    def fill(self, obj):
        for field in self:
            field.value = obj.get(field.name, None)

    def get_binding(self, context, bind):
        binding = {}
        if not bind:
            return binding

        with context.data_frame(bind):
            for field in self.all_fields:
                if field.name:
                    binding[field.name] = field.process_context_value(context)
        return binding

    def get_src_binding(self, context, bind_root):
        binding = {}
        with context.frame(bind_root):
            for field in self.all_fields:
                if field.src and field.src in context:
                    value = context[field.src]
                    value = field.process_value(context, value=value)
                    binding[field.name] = value
        return binding

    def bind(self, context, *bindings):
        self.set_data(context, *bindings)
        self.bound = True

    def set_data(self, context, *data):
        for field in self.all_fields:
            for binding in reversed(data):
                if field.name in binding:
                    value = binding[field.name]
                    if value is not None:
                        field.value = self.raw_data[field.name] = value
                        break

    def moya_render(self, archive, context, target, options):
        form_template = self.template
        if form_template is None:
            form_template = "/moya.forms/styles/%s/form.html" % self.style
        self.content.template = form_template
        self.content.td['form'] = self
        return self.content.moya_render(archive, context, target, options)