Exemplo n.º 1
0
Arquivo: auth.py Projeto: misl6/py4web
    def __init__(
        self,
        session,
        db,
        define_tables=True,
        sender=None,
        use_username=True,
        use_phone_number=False,
        registration_requires_confirmation=True,
        registration_requires_approval=False,
        inject=True,
        extra_fields=[],
        login_expiration_time=3600,  # seconds
        password_complexity={"entropy": 50},
        block_previous_password_num=None,
        allowed_actions=["all"],
        use_appname_in_redirects=None,
    ):

        self.param = Param(
            registration_requires_confirmation=registration_requires_confirmation,
            registration_requires_approval=registration_requires_approval,
            login_after_registration=False,
            login_expiration_time=login_expiration_time,  # seconds
            password_complexity=password_complexity,
            block_previous_password_num=block_previous_password_num,
            allowed_actions=allowed_actions,
            use_appname_in_redirects=use_appname_in_redirects,
            formstyle=FormStyleDefault,
            messages=copy.deepcopy(self.MESSAGES),
            button_classes=copy.deepcopy(self.BUTTON_CLASSES),
        )

        """Creates and Auth object responsible for handling
        authentication and authorization"""
        self.__prerequisites__ = []
        self.inject = inject
        if session:
            self.__prerequisites__.append(session)
        if db:
            self.__prerequisites__.append(db)

        self.onsuccess = {}
        self.next = {}

        self.db = db
        self.session = session
        self.sender = sender
        self.route = None
        self.use_username = use_username  # if False, uses email only
        self.use_phone_number = use_phone_number
        # The self._link variable is not thread safe (only intended for testing)
        self.extra_auth_user_fields = extra_fields
        self._link = None
        self.extra_auth_user_fields = extra_fields
        if db and define_tables:
            self.define_tables()
        self.plugins = {}
        self.form_source = DefaultAuthForms(self)
        self.flash = Flash()
Exemplo n.º 2
0
    def __init__(self,
                 table,
                 record=None,
                 readonly=False,
                 deletable=True,
                 noncreate=True,
                 formstyle=FormStyleDefault,
                 dbio=True,
                 keep_values=False,
                 form_name=None,
                 hidden=None,
                 validation=None,
                 csrf_session=None,
                 csrf_protection=True,
                 lifespan=None,
                 signing_info=None,
                 submit_value="Submit",
                 show_id=True,
                 **kwargs):
        self.param = Param(
            formstyle=formstyle,
            hidden=hidden,
            submit_value=submit_value,
            sidecar=[],
        )

        if isinstance(table, list):
            dbio = False
            # Mimic a table from a list of fields without calling define_table
            form_name = form_name or "none"
            for field in table:
                field.tablename = getattr(field, "tablename", form_name)

        if isinstance(record, (int, str)):
            record_id = int(str(record))
            self.record = table[record_id]
            if not self.record:
                raise HTTP(404)
        else:
            self.record = record

        # computed from input and not changed
        self.table = table
        self.deletable = self.record and deletable and not readonly
        self.dbio = dbio
        self.keep_values = True if keep_values or self.record else False
        self.form_name = form_name or table._tablename
        self.csrf_session = csrf_session
        self.signing_info = signing_info
        self.validation = validation
        self.lifespan = lifespan
        self.csrf_protection = csrf_protection
        self.show_id = show_id
        # initialized and can change
        self.vars = {}
        self.errors = {}
        self.readonly = readonly
        self.noncreate = noncreate
        self.submitted = False
        self.deleted = False
        self.accepted = False
        self.formkey = None
        self.cached_helper = None
        self.action = None

        self.kwargs = kwargs if kwargs else {}

        if self.record:
            self.vars = self._read_vars_from_record(table)
        if not readonly and request.method != "GET":
            post_vars = request.POST
            form_vars = copy.deepcopy(request.forms)
            for k in form_vars:
                self.vars[k] = form_vars[k]
            self.submitted = True
            process = False

            # We only a process a form if it is POST and the formkey matches (correct formname and crsf)
            # Notice: we never expose the crsf uuid, we only use to sign the form uuid
            if request.method == "POST":
                if not self.csrf_protection or self._verify_form(post_vars):
                    process = True
            if process:
                record_id = self.record and self.record.get("id")
                if not post_vars.get("_delete"):
                    validated_vars = {}
                    uploaded_files = []
                    for field in self.table:
                        if field.writable and field.type != "id":
                            original_value = post_vars.get(field.name)
                            if isinstance(original_value, list):
                                if len(original_value) == 1:
                                    original_value = original_value[0]

                                elif len(original_value) == 0:
                                    original_value = None
                            if field.type.startswith("list:") and isinstance(
                                    original_value, str):
                                try:
                                    original_value = json.loads(original_value
                                                                or "[]")
                                except json.decoder.JSONDecodeError:
                                    # this happens if posting a single value
                                    pass
                            (value,
                             error) = field.validate(original_value, record_id)
                            if field.type == "password" and record_id and value is None:
                                continue
                            if field.type == "upload":
                                value = request.files.get(field.name)
                                print(str(value)[:100])
                                delete = post_vars.get("_delete_" + field.name)
                                if value is not None:
                                    if field.uploadfolder:
                                        uploaded_files.append(
                                            tuple((field, value)))
                                    validated_vars[field.name] = value
                                elif self.record:
                                    if not delete:
                                        validated_vars[
                                            field.name] = self.record.get(
                                                field.name)
                                    else:
                                        validated_vars[
                                            field.name] = value = None
                            elif field.type == "boolean":
                                validated_vars[field.name] = value is not None
                            else:
                                validated_vars[field.name] = value
                            if error:
                                self.errors[field.name] = error
                    self.vars.update(validated_vars)
                    if validation:
                        validation(self)
                    if self.record and dbio:
                        self.vars["id"] = self.record.id
                    if not self.errors:
                        for file in uploaded_files:
                            field, value = file
                            value = field.store(value.file, value.filename,
                                                field.uploadfolder)
                            if value is not None:
                                validated_vars[field.name] = value
                        self.accepted = True
                        self.vars.update(validated_vars)
                        if dbio:
                            self.update_or_insert(validated_vars)
                elif dbio:
                    self.deleted = True
                    self.record.delete_record()
            elif self.record:
                # This form should not be processed.  We return the same as for GET.
                self.vars = self._read_vars_from_record(table)
        if self.csrf_protection:
            self._sign_form()
Exemplo n.º 3
0
class Form(object):
    """
    Usage in py4web controller:

       def index():
           form = Form(db.thing, record=1)
           if form.accepted: ...
           elif form.errors: ...
           else: ...
           return dict(form=form)

    Arguments:
    :param table: a DAL table or a list of fields (equivalent to old SQLFORM.factory)
    :param record: a DAL record or record id
    :param readonly: set to True to make a readonly form
    :param deletable: set to False to disallow deletion of record
    :param noncreate: make sure when you use a form with a list of fields that does not contain the id field, does not always render the create form.
    :param formstyle: a function that renders the form using helpers (FormStyleDefault)
    :param dbio: set to False to prevent any DB writes
    :param keep_values: if set to true, it remembers the values of the previously submitted form
    :param form_name: the optional name of this form
    :param csrf_session: if None, no csrf token is added.  If a session, then a CSRF token is added and verified.
    :param lifespan: lifespan of CSRF token in seconds, to limit form validity.
    :param signing_info: information that should not change between when the CSRF token is signed and
        verified.  This information is not leaked to the form.  For instance, if you wish to verify
        that the identity of the logged in user has not changed, you can do as below.
        signing_info = session.get('user', {}).get('id', '')
        The content of the field should be convertible to a string via json.
    """
    def __init__(self,
                 table,
                 record=None,
                 readonly=False,
                 deletable=True,
                 noncreate=True,
                 formstyle=FormStyleDefault,
                 dbio=True,
                 keep_values=False,
                 form_name=None,
                 hidden=None,
                 validation=None,
                 csrf_session=None,
                 csrf_protection=True,
                 lifespan=None,
                 signing_info=None,
                 submit_value="Submit",
                 show_id=True,
                 **kwargs):
        self.param = Param(
            formstyle=formstyle,
            hidden=hidden,
            submit_value=submit_value,
            sidecar=[],
        )

        if isinstance(table, list):
            dbio = False
            # Mimic a table from a list of fields without calling define_table
            form_name = form_name or "none"
            for field in table:
                field.tablename = getattr(field, "tablename", form_name)

        if isinstance(record, (int, str)):
            record_id = int(str(record))
            self.record = table[record_id]
            if not self.record:
                raise HTTP(404)
        else:
            self.record = record

        # computed from input and not changed
        self.table = table
        self.deletable = self.record and deletable and not readonly
        self.dbio = dbio
        self.keep_values = True if keep_values or self.record else False
        self.form_name = form_name or table._tablename
        self.csrf_session = csrf_session
        self.signing_info = signing_info
        self.validation = validation
        self.lifespan = lifespan
        self.csrf_protection = csrf_protection
        self.show_id = show_id
        # initialized and can change
        self.vars = {}
        self.errors = {}
        self.readonly = readonly
        self.noncreate = noncreate
        self.submitted = False
        self.deleted = False
        self.accepted = False
        self.formkey = None
        self.cached_helper = None
        self.action = None

        self.kwargs = kwargs if kwargs else {}

        if self.record:
            self.vars = self._read_vars_from_record(table)
        if not readonly and request.method != "GET":
            post_vars = request.POST
            form_vars = copy.deepcopy(request.forms)
            for k in form_vars:
                self.vars[k] = form_vars[k]
            self.submitted = True
            process = False

            # We only a process a form if it is POST and the formkey matches (correct formname and crsf)
            # Notice: we never expose the crsf uuid, we only use to sign the form uuid
            if request.method == "POST":
                if not self.csrf_protection or self._verify_form(post_vars):
                    process = True
            if process:
                record_id = self.record and self.record.get("id")
                if not post_vars.get("_delete"):
                    validated_vars = {}
                    uploaded_files = []
                    for field in self.table:
                        if field.writable and field.type != "id":
                            original_value = post_vars.get(field.name)
                            if isinstance(original_value, list):
                                if len(original_value) == 1:
                                    original_value = original_value[0]

                                elif len(original_value) == 0:
                                    original_value = None
                            if field.type.startswith("list:") and isinstance(
                                    original_value, str):
                                try:
                                    original_value = json.loads(original_value
                                                                or "[]")
                                except json.decoder.JSONDecodeError:
                                    # this happens if posting a single value
                                    pass
                            (value,
                             error) = field.validate(original_value, record_id)
                            if field.type == "password" and record_id and value is None:
                                continue
                            if field.type == "upload":
                                value = request.files.get(field.name)
                                print(str(value)[:100])
                                delete = post_vars.get("_delete_" + field.name)
                                if value is not None:
                                    if field.uploadfolder:
                                        uploaded_files.append(
                                            tuple((field, value)))
                                    validated_vars[field.name] = value
                                elif self.record:
                                    if not delete:
                                        validated_vars[
                                            field.name] = self.record.get(
                                                field.name)
                                    else:
                                        validated_vars[
                                            field.name] = value = None
                            elif field.type == "boolean":
                                validated_vars[field.name] = value is not None
                            else:
                                validated_vars[field.name] = value
                            if error:
                                self.errors[field.name] = error
                    self.vars.update(validated_vars)
                    if validation:
                        validation(self)
                    if self.record and dbio:
                        self.vars["id"] = self.record.id
                    if not self.errors:
                        for file in uploaded_files:
                            field, value = file
                            value = field.store(value.file, value.filename,
                                                field.uploadfolder)
                            if value is not None:
                                validated_vars[field.name] = value
                        self.accepted = True
                        self.vars.update(validated_vars)
                        if dbio:
                            self.update_or_insert(validated_vars)
                elif dbio:
                    self.deleted = True
                    self.record.delete_record()
            elif self.record:
                # This form should not be processed.  We return the same as for GET.
                self.vars = self._read_vars_from_record(table)
        if self.csrf_protection:
            self._sign_form()

    def _read_vars_from_record(self, table):
        if isinstance(table, list):
            # The table is just a list of fields.
            return {field.name: self.record.get(field.name) for field in table}
        else:
            return {
                name: table[name].formatter(self.record[name])
                for name in table.fields if name in self.record
            }

    def _make_key(self):
        if self.csrf_session is not None:
            key = str(uuid.uuid1())
            self.csrf_session["_formkey"] = key
        else:
            key = str(uuid.uuid1())
            response.set_cookie("_formkey", key, same_site="Strict")
        return key

    def _get_key(self):
        if self.csrf_session is not None:
            key = self.csrf_session.get("_formkey")
        else:
            key = request.get_cookie("_formkey")
        return key

    def _sign_form(self):
        """Signs the form, for csrf"""
        # Adds a form key.  First get the signing key from the session.
        payload = {"ts": str(time.time())}
        if self.lifespan is not None:
            payload["exp"] = time.time() + self.lifespan
        key = self._get_key() or self._make_key()
        self.formkey = to_native(jwt.encode(payload, key, algorithm="HS256"))

    def _verify_form(self, post_vars):
        """Verifies the csrf signature and form name."""
        if post_vars.get("_formname") != self.form_name:
            return False
        token = post_vars.get("_formkey")
        key = self._get_key()
        if not key:
            return False
        try:
            jwt.decode(token, key, algorithms=["HS256"])
            return True
        except:
            return False

    def update_or_insert(self, validated_vars):
        if self.record:
            self.record.update_record(**validated_vars)
        else:
            # warning, should we really insert if record
            self.vars["id"] = self.table.insert(**validated_vars)

    def clear(self):
        self.errors.clear()
        if not self.record and not self.keep_values:
            self.vars.clear()
            for field in self.table:
                self.vars[field.name] = (field.default() if callable(
                    field.default) else field.default)

    def helper(self):
        if self.accepted:
            self.clear()
        if not self.cached_helper:
            helper = self.param.formstyle(
                self.table,
                self.vars,
                self.errors,
                self.readonly,
                self.deletable,
                self.noncreate,
                show_id=self.show_id,
                kwargs=self.kwargs,
            )
            for item in self.param.sidecar:
                helper["form"][-1][-1].append(item)

                button_attributes = item.attributes
                button_attributes["_label"] = item.children[0]
                button_attributes["_type"] = (button_attributes.pop("_role")
                                              if "_role" in button_attributes
                                              else None)
                helper["json_controls"]["form_buttons"] += [button_attributes]

            if self.action:
                helper["form"]["_action"] = self.action

            if self.param.submit_value:
                helper["controls"]["submit"][
                    "_value"] = self.param.submit_value

            if self.form_name:
                helper["controls"]["hidden_widgets"]["formname"] = INPUT(
                    _type="hidden", _name="_formname", _value=self.form_name)
                helper["form"].append(
                    helper["controls"]["hidden_widgets"]["formname"])

                helper["json_controls"]["form_values"][
                    "_formname"] = self.form_name

            if self.formkey:
                helper["controls"]["hidden_widgets"]["formkey"] = INPUT(
                    _type="hidden", _name="_formkey", _value=self.formkey)
                helper["form"].append(
                    helper["controls"]["hidden_widgets"]["formkey"])

                helper["json_controls"]["form_values"][
                    "_formkey"] = self.formkey

            for key in self.param.hidden or {}:
                helper["controls"]["hidden_widgets"][key] = INPUT(
                    _type="hidden", _name=key, _value=self.param.hidden[key])
                helper["form"].append(
                    helper["controls"]["hidden_widgets"][key])

            helper["controls"]["begin"] = XML("".join(
                str(helper["controls"]["begin"]) +
                str(helper["controls"]["hidden_widgets"][hidden_field])
                for hidden_field in helper["controls"]["hidden_widgets"]))
            self.cached_helper = helper

        return self.cached_helper

    @staticmethod
    def is_image(value):
        """
        Tries to check if the filename provided references to an image

        Checking is based on filename extension. Currently recognized:
           gif, png, jp(e)g, bmp

        Args:
            value: filename
        """
        if value:
            (_, extension) = os.path.splitext(value)
            if extension in [".gif", ".png", ".jpg", ".jpeg", ".bmp"]:
                return True
        return False

    @property
    def custom(self):
        return self.helper()["controls"]

    @property
    def structure(self):
        return self.helper()["form"]

    def as_json(self):
        return self.helper()["json_controls"]

    def xml(self):
        return self.structure.xml()

    def __str__(self):
        return self.xml()
Exemplo n.º 4
0
    def __call__(
        self,
        table,
        vars,
        errors,
        readonly,
        deletable,
        noncreate,
        show_id,
        kwargs=None,
    ):
        kwargs = kwargs if kwargs else {}

        form_method = "POST"
        form_action = request.url.split(":", 1)[1]
        form_enctype = "multipart/form-data"

        form = FORM(_method=form_method,
                    _action=form_action,
                    _enctype=form_enctype,
                    **kwargs)

        controls = Param(
            labels=dict(),
            widgets=dict(),
            wrappers=dict(),
            comments=dict(),
            hidden_widgets=dict(),
            placeholders=dict(),
            titles=dict(),
            errors=dict(),
            begin=XML(form.xml().split("</form>")[0]),
            submit="",
            delete="",
            end=XML("</form>"),
        )

        json_controls = dict(form_fields=[],
                             form_values=dict(),
                             form_buttons=[],
                             form_method=form_method,
                             form_action=form_action,
                             form_enctype=form_enctype,
                             **kwargs)

        class_label = self.classes["label"]
        class_outer = self.classes["outer"]
        class_inner = self.classes["inner"]
        class_error = self.classes["error"]
        class_info = self.classes["info"]

        all_fields = [x for x in table]
        if "_virtual_fields" in dir(table):
            all_fields += table._virtual_fields
        for field in all_fields:

            # Reset the json control fields.
            field_attributes = dict()
            field_value = None

            field_name = field.name
            field_type = field.type
            field_comment = field.comment if field.comment else ""
            field_label = field.label
            input_id = "%s_%s" % (field.tablename, field.name)
            if isinstance(field, FieldVirtual):
                value = None
            else:
                value = vars.get(
                    field.name,
                    field.default()
                    if callable(field.default) else field.default,
                )
            error = errors.get(field.name)
            field_class = "type-" + field.type.split()[0].replace(":", "-")
            placeholder = (field._placeholder
                           if "_placeholder" in field.__dict__ else None)

            title = field._title if "_title" in field.__dict__ else None
            field_disabled = False

            # only display field if readable or writable
            if not field.readable and not field.writable:
                continue

            # if this is a readonly field only show readable fields
            if readonly:
                if not field.readable:
                    continue

            # do not show the id if not desired
            if field.type == "id" and not show_id:
                continue

            #  if this is an create form (unkown id) then only show writable fields.
            #  Some if an edit form was made from a list of fields and noncreate=True
            elif not vars.get("id") and noncreate:
                if not field.writable:
                    continue

            # ignore blob fields
            if field.type == "blob":  # never display blobs (mistake?)
                continue

            # ignore fields of type id its value is equal to None
            if field.type == "id" and value is None:
                field.writable = False
                continue

            # if the form is readonly or this is an id type field, display it as readonly
            if (readonly or not field.writable or field.type == "id"
                    or isinstance(field, FieldVirtual)):
                # for boolean readonly we use a readonly checbox
                if field.type == "boolean":

                    control = CheckboxWidget().make(field,
                                                    value,
                                                    error,
                                                    title,
                                                    readonly=True)
                # for all othe readonly fields we use represent or a string
                else:
                    if isinstance(field, FieldVirtual):
                        field_value = field.f(vars)
                    else:
                        field_value = (field.represent
                                       and field.represent(value) or value
                                       or "")
                    field_type = "represent"
                    control = DIV(field_value)

                field_disabled = True

            # if we have a field.widget for the field use it but this logic is deprecated
            elif field.widget:
                control = field.widget(table, vars)
            # else we pick the right widget
            else:
                if field.name in self.widgets:
                    widget = self.widgets[field.name]
                elif field.type == "text":
                    widget = TextareaWidget()
                elif field.type == "datetime":
                    widget = DateTimeWidget()
                elif field.type == "boolean":
                    widget = CheckboxWidget()
                elif field.type == "upload":
                    widget = FileUploadWidget()
                    url = getattr(field, "download_url",
                                  lambda value: "#")(value)
                    # Set the download url.
                    field_attributes["_download_url"] = url
                    # Set the flag determining whether the file is an image.
                    field_attributes["_is_image"] = (
                        url != "#") and Form.is_image(value)
                    # do we need the variables below?
                    delete_field_attributes = dict()
                    delete_field_attributes["_label"] = "Remove"
                    delete_field_attributes["_value"] = "ON"
                    delete_field_attributes["_type"] = "checkbox"
                    delete_field_attributes["_name"] = "_delete_" + field.name
                    json_controls["form_fields"] += [delete_field_attributes]
                    json_controls["form_values"]["_delete_" +
                                                 field.name] = None
                elif get_options(field.requires) is not None:
                    widget = SelectWidget()
                elif field.type == "password":
                    widget = PasswordWidget()
                elif field.type.startswith("list:"):
                    widget = ListWidget()
                else:
                    widget = Widget()

                control = widget.make(field, value, error, title, placeholder)

            key = control.name.rstrip("/")

            if key == "input":
                key += "[type=%s]" % (control["_type"] or "text")

            control["_class"] = join_classes(control.attributes.get("_class"),
                                             self.classes.get(key))

            # Set the form controls.
            controls["labels"][field_name] = field_label
            controls["widgets"][field_name] = control
            controls["comments"][field_name] = field_comment
            controls["titles"][field_name] = title
            controls["placeholders"][field_name] = placeholder

            # Set the remain json field attributes.
            field_attributes["_title"] = title
            field_attributes["_label"] = field_label
            field_attributes["_comment"] = field_comment
            field_attributes["_id"] = to_id(field)
            field_attributes["_class"] = field_class
            field_attributes["_name"] = field.name
            field_attributes["_type"] = field.type
            field_attributes["_placeholder"] = placeholder
            field_attributes["_error"] = error
            field_attributes["_disabled"] = field_disabled

            # Add to the json controls.
            json_controls["form_fields"] += [field_attributes]
            json_controls["form_values"][field_name] = field_value

            if error:
                controls["errors"][field.name] = error

            if field.type == "boolean":
                controls.wrappers[field.name] = wrapped = SPAN(
                    control, _class=class_inner)
                form.append(
                    DIV(
                        wrapped,
                        LABEL(
                            " " + field.label,
                            _for=input_id,
                            _class=class_label,
                            _style="display: inline !important",
                        ),
                        P(error, _class=class_error) if error else "",
                        P(field.comment or "", _class=class_info),
                        _class=class_outer,
                    ))
            else:
                controls.wrappers[field.name] = wrapped = DIV(
                    control,
                    _class=self.class_inner_exceptions.get(
                        control.name, class_inner),
                )

                form.append(
                    DIV(
                        LABEL(field.label, _for=input_id, _class=class_label),
                        wrapped,
                        P(error, _class=class_error) if error else "",
                        P(field.comment or "", _class=class_info),
                        _class=class_outer,
                    ))

        if vars.get("id"):
            form.append(INPUT(_name="id", _value=vars["id"], _hidden=True))

        if deletable:

            deletable_record_attributes = dict()

            deletable_field_name = "_delete"
            deletable_field_type = "checkbox"

            # Set the deletable json field attributes.
            deletable_record_attributes["_label"] = " check to delete"
            deletable_record_attributes["_name"] = deletable_field_name
            deletable_record_attributes["_type"] = deletable_field_type
            deletable_record_attributes["_class"] = self.classes[
                "input[type=checkbox]"]
            deletable_record_attributes["_value"] = "ON"

            # Add to the json controls.
            json_controls["form_fields"] += [deletable_record_attributes]
            json_controls["form_values"][deletable_field_name] = None

            controls["delete"] = INPUT(
                _type=deletable_field_type,
                _value=deletable_record_attributes["_value"],
                _name=deletable_field_name,
                _class=self.classes["input[type=checkbox]"],
            )

            form.append(
                DIV(
                    SPAN(
                        controls["delete"],
                        _class=class_inner,
                        _stye="vertical-align: middle;",
                    ),
                    P(
                        deletable_record_attributes["_label"],
                        _class="help",
                        _style="display: inline !important",
                    ),
                    _class=class_outer,
                ))

        submit_button_attributes = dict()

        submit_button_field_type = "submit"

        # Set the deletable json field attributes.
        submit_button_attributes["_label"] = "Submit"
        submit_button_attributes["_type"] = submit_button_field_type
        submit_button_attributes["_class"] = self.classes["input[type=submit]"]

        # Add to the json controls.
        json_controls["form_buttons"] += [submit_button_attributes]

        controls["submit"] = INPUT(
            _type=submit_button_field_type,
            _value="Submit",
            _class=self.classes["input[type=submit]"],
        )

        submit = DIV(
            DIV(
                controls["submit"],
                _class=class_inner,
            ),
            _class=class_outer,
        )
        form.append(submit)

        return dict(form=form, controls=controls, json_controls=json_controls)
Exemplo n.º 5
0
    def produce(self, table, vars, errors, readonly, deletable, classes=None):
        self.classes.update(classes or {})
        form = FORM(_method="POST", _action=request.url, _enctype="multipart/form-data")
        controls = Param(
            labels=dict(),
            widgets=dict(),
            comments=dict(),
            hidden_widgets=dict(),
            placeholders=dict(),
            titles=dict(),
            errors=dict(),
            begin=XML(form.xml().split("</form>")[0]),
            submit="",
            delete="",
            end=XML("</form>"),
        )
        class_label = self.classes["label"]
        class_outer = self.classes["outer"]
        class_inner = self.classes["inner"]
        class_error = self.classes["error"]
        class_info = self.classes["info"]

        for field in table:

            input_id = "%s_%s" % (field.tablename, field.name)
            value = vars.get(field.name, field.default)
            error = errors.get(field.name)
            field_class = "type-" + field.type.split()[0].replace(":", "-")
            placeholder = (
                field._placeholder if "_placeholder" in field.__dict__ else None
            )
            title = field._title if "_title" in field.__dict__ else None
            # only diplay field if readable or writable
            if not field.readable and not field.writable:
                continue
            # if this is a reaonly field only show readable fields
            if readonly:
                if not field.readable:
                    continue
            # if this is an create form (unkown id) then only show writable fields
            elif not vars.get("id"):
                if not field.writable:
                    continue
            # ignore blob fields
            if field.type == "blob":  # never display blobs (mistake?)
                continue
            # ignore fields of type id its value is equal to None
            if field.type == "id" and value is None:
                field.writable = False
                continue
            # if the form is readonly or this is an id type field, display it as readonly
            if readonly or not field.writable or field.type == "id":
                if field.type == "boolean":
                    control = INPUT(
                        _type="checkbox",
                        _id=input_id,
                        _name=field.name,
                        _value="ON",
                        _disabled="",
                        _checked=value,
                        _title=title,
                    )
                else:
                    control = DIV(
                        field.represent and field.represent(value) or value or ""
                    )
            # if we have a widget for the field use it
            elif field.widget:
                control = field.widget(table, value)
            # else pick the proper default widget
            elif field.type == "text":
                control = TEXTAREA(
                    value or "",
                    _id=input_id,
                    _name=field.name,
                    _placeholder=placeholder,
                    _title=title,
                )
            elif field.type == "date":
                control = INPUT(
                    _value=value,
                    _type="date",
                    _id=input_id,
                    _name=field.name,
                    _placeholder=placeholder,
                    _title=title,
                )
            elif field.type == "datetime":
                if isinstance(value, str):
                    value = value.replace(" ", "T")
                control = INPUT(
                    _value=value,
                    _type="datetime-local",
                    _id=input_id,
                    _name=field.name,
                    _placeholder=placeholder,
                    _title=title,
                )
            elif field.type == "time":
                control = INPUT(
                    _value=value,
                    _type="time",
                    _id=input_id,
                    _name=field.name,
                    _placeholder=placeholder,
                    _title=title,
                )
            elif field.type == "boolean":
                control = INPUT(
                    _type="checkbox",
                    _id=input_id,
                    _name=field.name,
                    _value="ON",
                    _checked=value,
                    _title=title,
                )
            elif field.type == "upload":
                control = DIV()
                if value and field.download_url is not None and not error:
                    download_div = DIV()
                    download_div.append(
                        LABEL(
                            "Currently:  ",
                        )
                    )
                    download_div.append(
                        A(" download ", _href=field.download_url(value))
                    )
                    download_div.append(
                        INPUT(
                            _type="checkbox",
                            _value="ON",
                            _name="_delete_" + field.name,
                            _title=title,
                        )
                    )
                    download_div.append(" (check to remove)")
                    control.append(download_div)
                control.append(LABEL("Change: "))
                control.append(INPUT(_type="file", _id=input_id, _name=field.name))
            elif get_options(field.requires) is not None and field.writable == True:
                multiple = field.type.startswith("list:")
                value = list(map(str, value if isinstance(value, list) else [value]))
                option_tags = [
                    OPTION(v, _value=k, _selected=(not k is None and k in value))
                    for k, v in get_options(field.requires)
                ]
                control = SELECT(
                    *option_tags,
                    _id=input_id,
                    _name=field.name,
                    _multiple=multiple,
                    _title=title
                )
            else:
                field_type = "password" if field.type == "password" else "text"
                if field.type.startswith("list:"):
                    value = json.dumps(value or [])
                control = INPUT(
                    _type=field_type,
                    _id=input_id,
                    _name=field.name,
                    _value=None if field.type == "password" else value,
                    _class=field_class,
                    _placeholder=placeholder,
                    _title=title,
                    _autocomplete="off" if field_type == "password" else "on",
                )

            key = control.name.rstrip("/")
            if key == "input":
                key += "[type=%s]" % (control["_type"] or "text")
            control["_class"] = (
                control.attributes.get("_class", "") + " " + self.classes.get(key, "")
            ).strip()
            controls["labels"][field.name] = field.label
            controls["widgets"][field.name] = control
            controls["comments"][field.name] = field.comment if field.comment else ""
            controls["titles"][field.name] = title
            controls["placeholders"][field.name] = placeholder
            if error:
                controls["errors"][field.name] = error

            if field.type == "boolean":
                form.append(
                    DIV(
                        SPAN(control, _class=class_inner),
                        LABEL(
                            " " + field.label,
                            _for=input_id,
                            _class=class_label,
                            _style="display: inline !important",
                        ),
                        P(error, _class=class_error) if error else "",
                        P(field.comment or "", _class=class_info),
                        _class=class_outer,
                    )
                )
            else:
                form.append(
                    DIV(
                        LABEL(field.label, _for=input_id, _class=class_label),
                        DIV(control, _class=class_inner),
                        P(error, _class=class_error) if error else "",
                        P(field.comment or "", _class=class_info),
                        _class=class_outer,
                    )
                )

            if vars.get("id"):
                form.append(INPUT(_name="id", _value=vars["id"], _hidden=True))
        if deletable:
            controls["delete"] = INPUT(
                _type="checkbox",
                _value="ON",
                _name="_delete",
                _class=self.classes["input[type=checkbox]"],
            )
            form.append(
                DIV(
                    SPAN(
                        controls["delete"],
                        _class=class_inner,
                        _stye="vertical-align: middle;",
                    ),
                    P(
                        " check to delete",
                        _class="help",
                        _style="display: inline !important",
                    ),
                    _class=class_outer,
                )
            )
        controls["submit"] = INPUT(
            _type="submit",
            _value="Submit",
            _class=self.classes["input[type=submit]"],
        )
        submit = DIV(
            DIV(
                controls["submit"],
                _class=class_inner,
            ),
            _class=class_outer,
        )
        form.append(submit)
        return dict(form=form, controls=controls)
Exemplo n.º 6
0
    def __init__(
        self,
        table,
        record=None,
        readonly=False,
        deletable=True,
        formstyle=FormStyleDefault,
        dbio=True,
        keep_values=False,
        form_name=None,
        hidden=None,
        validation=None,
        csrf_session=None,
        lifespan=None,
        signing_info=None,
        submit_value="Submit",
    ):
        self.param = Param(
            formstyle=formstyle, hidden=hidden, submit_value=submit_value, sidecar=[],
        )

        if isinstance(table, list):
            dbio = False
            # Mimic a table from a list of fields without calling define_table
            form_name = form_name or "none"
            for field in table:
                field.tablename = getattr(field, "tablename", form_name)

        if isinstance(record, (int, str)):
            record_id = int(str(record))
            self.record = table[record_id]
        else:
            self.record = record

        # computed from input and not changed
        self.table = table
        self.deletable = deletable and not readonly and self.record
        self.dbio = dbio
        self.keep_values = True if keep_values or self.record else False
        self.form_name = form_name or table._tablename
        self.csrf_session = csrf_session
        self.signing_info = signing_info
        self.validation = validation
        self.lifespan = lifespan

        # initialized and can change
        self.vars = {}
        self.errors = {}
        self.readonly = readonly
        self.submitted = False
        self.deleted = False
        self.accepted = False
        self.formkey = None
        self.cached_helper = None
        self.action = None

        if readonly or request.method == "GET":
            if self.record:
                self.vars = self._read_vars_from_record(table)
        else:
            post_vars = request.forms
            self.vars = copy.deepcopy(request.forms)
            self.submitted = True
            process = False

            # We only a process a form if it is POST and the formkey matches (correct formname and crsf)
            # Notice: we never expose the crsf uuid, we only use to sign the form uuid
            if request.method == "POST":
                if self._verify_form(post_vars):
                    process = True
            if process:
                record_id = self.record and self.record.get("id")
                if not post_vars.get("_delete"):
                    validated_vars = {}
                    for field in self.table:
                        if field.writable and field.readable and field.type != "id":
                            original_value = post_vars.getall(field.name)
                            if (
                                isinstance(original_value, list)
                                and len(original_value) == 1
                            ):
                                original_value = original_value[0]
                            if field.type.startswith("list:") and isinstance(original_value, str):
                                original_value = json.loads(original_value or "[]")
                            (value, error) = field.validate(original_value, record_id)
                            if field.type == "password" and record_id and value is None:
                                continue
                            if field.type == "upload":
                                value = request.files.get(field.name)
                                delete = post_vars.get("_delete_" + field.name)
                                if value is not None and hasattr(value, "file"):
                                    value = field.store(
                                        value.file, value.filename, field.uploadfolder
                                    )
                                elif self.record and not delete:
                                    value = self.record.get(field.name)
                                else:
                                    value = None
                            validated_vars[field.name] = value
                            if error:
                                self.errors[field.name] = error
                    self.vars.update(validated_vars)
                    if validation:
                        validation(self)
                    if self.record and dbio:
                        self.vars["id"] = self.record.id
                    if not self.errors:
                        self.accepted = True
                        if dbio:
                            self.update_or_insert(validated_vars)
                elif dbio:
                    self.deleted = True
                    self.record.delete_record()
            elif self.record:
                # This form should not be processed.  We return the same as for GET.
                self.vars = self._read_vars_from_record(table)
        self._sign_form()
Exemplo n.º 7
0
class Form(object):
    """
    Usage in py4web controller:

       def index():
           form = Form(db.thing, record=1)
           if form.accepted: ...
           elif form.errors: ...
           else: ...
           return dict(form=form)

    Arguments:
    :param table: a DAL table or a list of fields (equivalent to old SQLFORM.factory)
    :param record: a DAL record or record id
    :param readonly: set to True to make a readonly form
    :param deletable: set to False to disallow deletion of record
    :param formstyle: a function that renders the form using helpers (FormStyleDefault)
    :param dbio: set to False to prevent any DB writes
    :param keep_values: if set to true, it remembers the values of the previously submitted form
    :param form_name: the optional name of this form
    :param csrf_session: if None, no csrf token is added.  If a session, then a CSRF token is added and verified.
    :param lifespan: lifespan of CSRF token in seconds, to limit form validity.
    :param signing_info: information that should not change between when the CSRF token is signed and
        verified.  This information is not leaked to the form.  For instance, if you wish to verify
        that the identity of the logged in user has not changed, you can do as below.
        signing_info = session.get('user', {}).get('id', '')
        The content of the field should be convertible to a string via json.
    """

    def __init__(
        self,
        table,
        record=None,
        readonly=False,
        deletable=True,
        formstyle=FormStyleDefault,
        dbio=True,
        keep_values=False,
        form_name=None,
        hidden=None,
        validation=None,
        csrf_session=None,
        lifespan=None,
        signing_info=None,
        submit_value="Submit",
    ):
        self.param = Param(
            formstyle=formstyle, hidden=hidden, submit_value=submit_value, sidecar=[],
        )

        if isinstance(table, list):
            dbio = False
            # Mimic a table from a list of fields without calling define_table
            form_name = form_name or "none"
            for field in table:
                field.tablename = getattr(field, "tablename", form_name)

        if isinstance(record, (int, str)):
            record_id = int(str(record))
            self.record = table[record_id]
        else:
            self.record = record

        # computed from input and not changed
        self.table = table
        self.deletable = deletable and not readonly and self.record
        self.dbio = dbio
        self.keep_values = True if keep_values or self.record else False
        self.form_name = form_name or table._tablename
        self.csrf_session = csrf_session
        self.signing_info = signing_info
        self.validation = validation
        self.lifespan = lifespan

        # initialized and can change
        self.vars = {}
        self.errors = {}
        self.readonly = readonly
        self.submitted = False
        self.deleted = False
        self.accepted = False
        self.formkey = None
        self.cached_helper = None
        self.action = None

        if readonly or request.method == "GET":
            if self.record:
                self.vars = self._read_vars_from_record(table)
        else:
            post_vars = request.forms
            self.vars = copy.deepcopy(request.forms)
            self.submitted = True
            process = False

            # We only a process a form if it is POST and the formkey matches (correct formname and crsf)
            # Notice: we never expose the crsf uuid, we only use to sign the form uuid
            if request.method == "POST":
                if self._verify_form(post_vars):
                    process = True
            if process:
                record_id = self.record and self.record.get("id")
                if not post_vars.get("_delete"):
                    validated_vars = {}
                    for field in self.table:
                        if field.writable and field.readable and field.type != "id":
                            original_value = post_vars.getall(field.name)
                            if (
                                isinstance(original_value, list)
                                and len(original_value) == 1
                            ):
                                original_value = original_value[0]
                            if field.type.startswith("list:") and isinstance(original_value, str):
                                original_value = json.loads(original_value or "[]")
                            (value, error) = field.validate(original_value, record_id)
                            if field.type == "password" and record_id and value is None:
                                continue
                            if field.type == "upload":
                                value = request.files.get(field.name)
                                delete = post_vars.get("_delete_" + field.name)
                                if value is not None and hasattr(value, "file"):
                                    value = field.store(
                                        value.file, value.filename, field.uploadfolder
                                    )
                                elif self.record and not delete:
                                    value = self.record.get(field.name)
                                else:
                                    value = None
                            validated_vars[field.name] = value
                            if error:
                                self.errors[field.name] = error
                    self.vars.update(validated_vars)
                    if validation:
                        validation(self)
                    if self.record and dbio:
                        self.vars["id"] = self.record.id
                    if not self.errors:
                        self.accepted = True
                        if dbio:
                            self.update_or_insert(validated_vars)
                elif dbio:
                    self.deleted = True
                    self.record.delete_record()
            elif self.record:
                # This form should not be processed.  We return the same as for GET.
                self.vars = self._read_vars_from_record(table)
        self._sign_form()

    def _read_vars_from_record(self, table):
        if isinstance(table, list):
            # The table is just a list of fields.
            return {field.name: self.record.get(field.name) for field in table}
        else:
            return {
                name: table[name].formatter(self.record[name])
                for name in table.fields
                if name in self.record
            }

    def _get_key(self):
        if self.csrf_session is not None:
            key = self.csrf_session.get("_form_key")
            if key is None:
                key = str(uuid.uuid1())
                self.csrf_session["_form_key"] = key
        else:
            key = request.get_cookie("_form_key")
            if key is None:
                key = str(uuid.uuid1())
                response.set_cookie("_form_key", key, same_site="Strict")
        additional_info = {
            "signing_info": self.signing_info,
            "form_name": self.form_name,
        }
        return key + "." + json.dumps(additional_info)

    def _sign_form(self):
        """Signs the form, for csrf"""
        # Adds a form key.  First get the signing key from the session.
        payload = {"ts": str(time.time())}
        if self.lifespan is not None:
            payload["exp"] = time.time() + self.lifespan
        self.formkey = jwt.encode(payload, self._get_key(), algorithm="HS256").decode(
            "utf-8"
        )

    def _verify_form(self, post_vars):
        """Verifies the csrf signature and form name."""
        if post_vars.get("_formname") != self.form_name:
            return False
        if not self.csrf_session:
            return True
        token = post_vars.get("_formkey")
        try:
            jwt.decode(token, self._get_key(), algorithms=["HS256"])
            return True
        except:
            return False

    def update_or_insert(self, validated_vars):
        if self.record:
            self.record.update_record(**validated_vars)
        else:
            # warning, should we really insert if record
            self.vars["id"] = self.table.insert(**validated_vars)

    def clear(self):
        self.errors.clear()
        if not self.record and not self.keep_values:
            self.vars.clear()
            for field in self.table:
                self.vars[field.name] = field.default

    def helper(self):
        if self.accepted:
            self.clear()
        if not self.cached_helper:
            helper = self.param.formstyle(
                self.table, self.vars, self.errors, self.readonly, self.deletable
            )
            for item in self.param.sidecar:
                helper["form"][-1][-1].append(item)
            if self.action:
                helper["_action"] = self.action
            if self.param.submit_value:
                helper["controls"]["submit"]["_value"] = self.param.submit_value
            if self.form_name:
                helper["controls"]["hidden_widgets"]["formname"] = INPUT(
                    _type="hidden", _name="_formname", _value=self.form_name
                )
                helper["form"].append(helper["controls"]["hidden_widgets"]["formname"])
            if self.formkey:
                helper["controls"]["hidden_widgets"]["formkey"] = INPUT(
                    _type="hidden", _name="_formkey", _value=self.formkey
                )
                helper["form"].append(helper["controls"]["hidden_widgets"]["formkey"])
            for key in self.param.hidden or {}:
                helper["controls"]["hidden_widgets"][key] = INPUT(
                    _type="hidden", _name=key, _value=self.param.hidden[key]
                )
                helper["form"].append(helper["controls"]["hidden_widgets"][key])
            helper["controls"]["begin"] = XML(
                "".join(
                    str(helper["controls"]["begin"])
                    + str(helper["controls"]["hidden_widgets"][hidden_field])
                    for hidden_field in helper["controls"]["hidden_widgets"]
                )
            )
            self.cached_helper = helper

        return self.cached_helper

    @property
    def custom(self):
        return self.helper()["controls"]

    @property
    def structure(self):
        return self.helper()["form"]

    def xml(self):
        return self.structure.xml()

    def __str__(self):
        return self.xml()
    def __init__(
        self,
        path,
        query,
        search_form=None,
        search_queries=None,
        fields=None,
        show_id=False,
        orderby=None,
        left=None,
        headings=None,
        create=True,
        details=True,
        editable=True,
        deletable=True,
        pre_action_buttons=None,
        post_action_buttons=None,
        auto_process=True,
        rows_per_page=15,
        include_action_button_text=True,
        search_button_text="Filter",
        formstyle=FormStyleDefault,
        grid_class_style=GridClassStyle,
    ):
        """
        Grid is a searchable/sortable/pageable grid

        :param path: The part of the URL to be parsed by this grid
        :param query: the query used to filter the data
        :param search_form: py4web FORM to be included as the search form
        :param search_queries: future use - pass a dict of name and a search query
        :param fields: list of fields to display on the list page, if blank, glean tablename from first query
        :              and use all fields of that table
        :param show_id: show the record id field on list page - default = False
        :param orderby: pydal orderby field or list of fields
        :param left: if joining other tables, specify the pydal left expression here
        :param headings: list of headings to be used for list page - if not provided use the field label
        :param create: URL to redirect to for creating records - set to False to not display the button
        :param editable: URL to redirect to for editing records - set to False to not display the button
        :param deletable: URL to redirect to for deleting records - set to False to not display the button
        :param pre_action_buttons: list of action_button instances to include before the standard action buttons
        :param post_action_buttons: list of action_button instances to include after the standard action buttons
        :param auto_process: True/False - automatically process the sql for the form - if False, user is
                              responsible for calling process().
        """

        # in case the query is a Table instead
        if isinstance(query, query._db.Table):
            query = query._id != None

        self.path = path or ""
        self.db = query._db
        self.param = Param(
            query=query,
            fields=fields,
            show_id=show_id,
            orderby=orderby,
            left=left,
            search_form=search_form,
            search_queries=search_queries,
            headings=headings or [],
            create=create,
            details=details,
            editable=editable,
            deletable=deletable,
            pre_action_buttons=pre_action_buttons,
            post_action_buttons=post_action_buttons,
            rows_per_page=rows_per_page,
            include_action_button_text=include_action_button_text,
            search_button_text=search_button_text,
            formstyle=formstyle,
            grid_class_style=grid_class_style,
            new_sidecar=None,
            new_submit_value=None,
            new_action_button_text="New",
            details_sidecar=None,
            details_submit_value=None,
            details_action_button_text="Details",
            edit_sidecar=None,
            edit_submit_value=None,
            edit_action_button_text="Edit",
            delete_action_button_text="Delete",
            htmx_target=None,
        )

        #  instance variables that will be computed
        self.action = None
        self.current_page_number = None
        self.endpoint = request.fullpath
        if self.path:
            self.endpoint = self.endpoint[:-len(self.path)].rstrip("/")
        self.hidden_fields = None
        self.form = None
        self.number_of_pages = None
        self.page_end = None
        self.page_start = None
        self.query_parms = request.params
        self.readonly_fields = None
        self.record_id = None
        self.rows = None
        self.tablename = None
        self.total_number_of_rows = None
        self.use_tablename = self.is_join()
        self.formatters = {}
        self.formatters_by_type = copy.copy(Grid.FORMATTERS_BY_TYPE)

        if auto_process:
            self.process()
Exemplo n.º 9
0
    def produce(
        self,
        table,
        vars,
        errors,
        readonly,
        noncreate,
        deletable,
        classes=None,
        class_inner_exceptions=None,
        kwargs=None,
    ):
        self.classes.update(classes or {})
        self.class_inner_exceptions.update(class_inner_exceptions or {})
        kwargs = kwargs if kwargs else {}

        form_method = "POST"
        form_action = request.url
        form_enctype = "multipart/form-data"

        form = FORM(
            _method=form_method, _action=form_action, _enctype=form_enctype, **kwargs
        )

        controls = Param(
            labels=dict(),
            widgets=dict(),
            comments=dict(),
            hidden_widgets=dict(),
            placeholders=dict(),
            titles=dict(),
            errors=dict(),
            begin=XML(form.xml().split("</form>")[0]),
            submit="",
            delete="",
            end=XML("</form>"),
        )

        json_controls = dict(
            form_fields=[],
            form_values=dict(),
            form_buttons=[],
            form_method=form_method,
            form_action=form_action,
            form_enctype=form_enctype,
            **kwargs
        )

        class_label = self.classes["label"]
        class_outer = self.classes["outer"]
        class_inner = self.classes["inner"]
        class_error = self.classes["error"]
        class_info = self.classes["info"]

        for field in table:

            # Reset the json control fields.
            field_attributes = dict()
            field_value = None

            field_name = field.name
            field_type = field.type
            field_comment = field.comment if field.comment else ""
            field_label = field.label
            input_id = "%s_%s" % (field.tablename, field.name)
            value = vars.get(
                field.name,
                field.default() if callable(field.default) else field.default,
            )
            error = errors.get(field.name)
            field_class = "type-" + field.type.split()[0].replace(":", "-")
            placeholder = (
                field._placeholder if "_placeholder" in field.__dict__ else None
            )
            title = field._title if "_title" in field.__dict__ else None
            field_disabled = False

            # only diplay field if readable or writable
            if not field.readable and not field.writable:
                continue

            # if this is a reaonly field only show readable fields
            if readonly:
                if not field.readable:
                    continue

            # if this is an create form (unkown id) then only show writable fields. Some if an edit form was made from a list of fields and noncreate=True
            elif not vars.get("id") and noncreate:
                if not field.writable:
                    continue

            # ignore blob fields
            if field.type == "blob":  # never display blobs (mistake?)
                continue

            # ignore fields of type id its value is equal to None
            if field.type == "id" and value is None:
                field.writable = False
                continue

            # if the form is readonly or this is an id type field, display it as readonly
            if readonly or not field.writable or field.type == "id":
                if field.type == "boolean":

                    field_value = value
                    field_type = "checkbox"

                    control = INPUT(
                        _type=field_type,
                        _id=input_id,
                        _name=field_name,
                        _value="ON",
                        _disabled="",
                        _checked=value,
                        _title=title,
                    )
                else:

                    field_value = (
                        field.represent and field.represent(value) or value or ""
                    )
                    field_type = "represent"

                    control = DIV(field_value)

                field_disabled = True

            # if we have a widget for the field use it
            elif field.widget:
                control = field.widget(table, value)

                # Grab the custom widget attributes.
                field_attributes = control.attributes
                field_type = "widget"
                field_value = value

            # else pick the proper default widget
            elif field.type == "text":

                field_value = value or ""
                field_type = "text"

                control = TEXTAREA(
                    field_value,
                    _id=input_id,
                    _name=field_name,
                    _placeholder=placeholder,
                    _title=title,
                )

            elif field.type == "date":

                field_value = value
                field_type = "date"

                control = INPUT(
                    _value=field_value,
                    _type="date",
                    _id=input_id,
                    _name=field_name,
                    _placeholder=placeholder,
                    _title=title,
                )

            elif field.type == "datetime":
                helpervalue = str(value)
                helpervalue = helpervalue.replace(" ", "T")

                field_value = helpervalue
                field_type = "datetime-local"

                control = INPUT(
                    _value=helpervalue,
                    _type="datetime-local",
                    _id=input_id,
                    _name=field_name,
                    _placeholder=placeholder,
                    _title=title,
                )
            elif field.type == "time":

                field_value = value
                field_type = "time"

                control = INPUT(
                    _value=value,
                    _type="time",
                    _id=input_id,
                    _name=field_name,
                    _placeholder=placeholder,
                    _title=title,
                )

            elif field.type == "boolean":

                field_value = value
                field_type = "checkbox"
                field_attributes["_value"] = "ON"

                control = INPUT(
                    _type="checkbox",
                    _id=input_id,
                    _name=field_name,
                    _value=field_attributes["_value"],
                    _checked=value,
                    _title=title,
                )

            elif field.type == "upload":
                control = DIV()
                if value and not error:
                    download_div = DIV()

                    download_div.append(
                        LABEL(
                            "Currently:  ",
                        )
                    )
                    if getattr(field, "download_url", None):
                        url = field.download_url(value)
                    else:
                        url = "#"
                    download_div.append(A(" download ", _href=url))

                    # Set the download url.
                    field_attributes["_download_url"] = url

                    # Set the flag determining whether the file is an image.
                    field_attributes["_is_image"] = (url != "#") and Form.is_image(
                        value
                    )

                    delete_checkbox_name = "_delete_" + field_name

                    download_div.append(
                        INPUT(
                            _type="checkbox",
                            _value="ON",
                            _name=delete_checkbox_name,
                            _title=title,
                        )
                    )
                    download_div.append(" (check to remove)")

                    delete_field_attributes = dict()

                    delete_field_attributes["_label"] = "Remove"
                    delete_field_attributes["_value"] = "ON"
                    delete_field_attributes["_type"] = "checkbox"
                    delete_field_attributes["_name"] = delete_checkbox_name

                    json_controls["form_fields"] += [delete_field_attributes]
                    json_controls["form_values"][delete_checkbox_name] = None

                    control.append(download_div)

                control.append(LABEL("Change: "))
                control.append(INPUT(_type="file", _id=input_id, _name=field_name))

                field_value = None
                field_type = "file"

            elif get_options(field.requires) is not None and field.writable == True:
                multiple = field.type.startswith("list:")
                value = list(map(str, value if isinstance(value, list) else [value]))

                field_options = [
                    [k, v, (not k is None and k in value)]
                    for k, v in get_options(field.requires)
                ]
                option_tags = [
                    OPTION(v, _value=k, _selected=_selected)
                    for (k, v, _selected) in field_options
                ]

                control = SELECT(
                    *option_tags,
                    _id=input_id,
                    _name=field_name,
                    _multiple=multiple,
                    _title=title
                )

                field_value = value
                field_type = "options"
                field_attributes["_multiple"] = multiple
                field_attributes["_options"] = field_options

            else:

                field_type = "password" if field.type == "password" else "text"

                if field.type.startswith("list:"):
                    value = json.dumps(value or [])

                field_value = None if field.type == "password" else value
                field_autocomplete = "off" if field_type == "password" else "on"

                control = INPUT(
                    _type=field_type,
                    _id=input_id,
                    _name=field_name,
                    _value=field_value,
                    _class=field_class,
                    _placeholder=placeholder,
                    _title=title,
                    _autocomplete=field_autocomplete,
                )

                field_attributes["_autocomplete"] = field_autocomplete

            key = control.name.rstrip("/")

            if key == "input":
                key += "[type=%s]" % (control["_type"] or "text")

            control["_class"] = (
                control.attributes.get("_class", "") + " " + self.classes.get(key, "")
            ).strip()

            # Set the form controls.
            controls["labels"][field_name] = field_label
            controls["widgets"][field_name] = control
            controls["comments"][field_name] = field_comment
            controls["titles"][field_name] = title
            controls["placeholders"][field_name] = placeholder

            # Set the remain json field attributes.
            field_attributes["_title"] = title
            field_attributes["_label"] = field_label
            field_attributes["_comment"] = field_comment
            field_attributes["_id"] = input_id
            field_attributes["_class"] = field_class
            field_attributes["_name"] = field_name
            field_attributes["_type"] = field_type
            field_attributes["_placeholder"] = placeholder
            field_attributes["_error"] = error
            field_attributes["_disabled"] = field_disabled

            # Add to the json controls.
            json_controls["form_fields"] += [field_attributes]
            json_controls["form_values"][field_name] = field_value

            if error:
                controls["errors"][field.name] = error

            if field.type == "boolean":
                form.append(
                    DIV(
                        SPAN(control, _class=class_inner),
                        LABEL(
                            " " + field.label,
                            _for=input_id,
                            _class=class_label,
                            _style="display: inline !important",
                        ),
                        P(error, _class=class_error) if error else "",
                        P(field.comment or "", _class=class_info),
                        _class=class_outer,
                    )
                )
            else:
                form.append(
                    DIV(
                        LABEL(field.label, _for=input_id, _class=class_label),
                        DIV(
                            control,
                            _class=self.class_inner_exceptions.get(
                                control.name, class_inner
                            ),
                        ),
                        P(error, _class=class_error) if error else "",
                        P(field.comment or "", _class=class_info),
                        _class=class_outer,
                    )
                )

        if vars.get("id"):
            form.append(INPUT(_name="id", _value=vars["id"], _hidden=True))

        if deletable:

            deletable_record_attributes = dict()

            deletable_field_name = "_delete"
            deletable_field_type = "checkbox"

            # Set the deletable json field attributes.
            deletable_record_attributes["_label"] = " check to delete"
            deletable_record_attributes["_name"] = deletable_field_name
            deletable_record_attributes["_type"] = deletable_field_type
            deletable_record_attributes["_class"] = self.classes["input[type=checkbox]"]
            deletable_record_attributes["_value"] = "ON"

            # Add to the json controls.
            json_controls["form_fields"] += [deletable_record_attributes]
            json_controls["form_values"][deletable_field_name] = None

            controls["delete"] = INPUT(
                _type=deletable_field_type,
                _value=deletable_record_attributes["_value"],
                _name=deletable_field_name,
                _class=self.classes["input[type=checkbox]"],
            )

            form.append(
                DIV(
                    SPAN(
                        controls["delete"],
                        _class=class_inner,
                        _stye="vertical-align: middle;",
                    ),
                    P(
                        deletable_record_attributes["_label"],
                        _class="help",
                        _style="display: inline !important",
                    ),
                    _class=class_outer,
                )
            )

        submit_button_attributes = dict()

        submit_button_field_type = "submit"

        # Set the deletable json field attributes.
        submit_button_attributes["_label"] = "Submit"
        submit_button_attributes["_type"] = submit_button_field_type
        submit_button_attributes["_class"] = self.classes["input[type=submit]"]

        # Add to the json controls.
        json_controls["form_buttons"] += [submit_button_attributes]

        controls["submit"] = INPUT(
            _type=submit_button_field_type,
            _value="Submit",
            _class=self.classes["input[type=submit]"],
        )

        submit = DIV(
            DIV(
                controls["submit"],
                _class=class_inner,
            ),
            _class=class_outer,
        )
        form.append(submit)

        return dict(form=form, controls=controls, json_controls=json_controls)