{"id":13707476,"url":"https://github.com/stevelittlefish/easyforms","last_synced_at":"2026-03-06T06:03:21.479Z","repository":{"id":19841354,"uuid":"88056263","full_name":"stevelittlefish/easyforms","owner":"stevelittlefish","description":"Forms library for Flask and Jinja2","archived":false,"fork":false,"pushed_at":"2023-02-16T01:29:33.000Z","size":2039,"stargazers_count":9,"open_issues_count":8,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-09-27T20:16:18.184Z","etag":null,"topics":["flask","jinja2","python"],"latest_commit_sha":null,"homepage":null,"language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stevelittlefish.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2017-04-12T13:41:16.000Z","updated_at":"2024-08-01T01:45:47.000Z","dependencies_parsed_at":"2024-01-14T20:48:43.977Z","dependency_job_id":"baea0aaf-1a5d-4b5e-b615-02b343a2bc14","html_url":"https://github.com/stevelittlefish/easyforms","commit_stats":{"total_commits":139,"total_committers":2,"mean_commits":69.5,"dds":0.03597122302158273,"last_synced_commit":"7944b83ec94a29ca55777d16536e13cbf8f6996f"},"previous_names":[],"tags_count":57,"template":false,"template_full_name":null,"purl":"pkg:github/stevelittlefish/easyforms","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevelittlefish%2Feasyforms","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevelittlefish%2Feasyforms/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevelittlefish%2Feasyforms/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevelittlefish%2Feasyforms/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stevelittlefish","download_url":"https://codeload.github.com/stevelittlefish/easyforms/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevelittlefish%2Feasyforms/sbom","scorecard":{"id":852287,"data":{"date":"2025-08-11","repo":{"name":"github.com/stevelittlefish/easyforms","commit":"7944b83ec94a29ca55777d16536e13cbf8f6996f"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":1.6,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: pipCommand not pinned by hash: testapp/create_env.sh:8","Warn: pipCommand not pinned by hash: testapp/run_test_app.sh:10","Info:   0 out of   2 pipCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE.txt:0","Info: FSF or OSI recognized license: Apache License 2.0: LICENSE.txt:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":0,"reason":"72 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: PYSEC-2023-120 / GHSA-45c4-8wx5-qw6w","Warn: Project is vulnerable to: PYSEC-2024-24 / GHSA-5h86-8mv2-jq9f","Warn: Project is vulnerable to: GHSA-5m98-qgg9-wh84","Warn: Project is vulnerable to: GHSA-7gpw-8wmc-pm8g","Warn: Project is vulnerable to: GHSA-8495-4g3g-x7pr","Warn: Project is vulnerable to: PYSEC-2024-26 / GHSA-8qpw-xqxj-h4r2","Warn: Project is vulnerable to: GHSA-9548-qrrj-x5pj","Warn: Project is vulnerable to: PYSEC-2023-246 / GHSA-gfw2-4jvh-wgfg","Warn: Project is vulnerable to: GHSA-pjjw-qhg8-p2p9","Warn: Project is vulnerable to: PYSEC-2023-250 / GHSA-q3qx-c6g2-7pw2","Warn: Project is vulnerable to: PYSEC-2023-251 / GHSA-qvrw-v9rv-5rjx","Warn: Project is vulnerable to: PYSEC-2021-76 / GHSA-v6wp-4m6f-gcjg","Warn: Project is vulnerable to: PYSEC-2023-247 / GHSA-xx9p-xxvh-7g8j","Warn: Project is vulnerable to: PYSEC-2021-858 / GHSA-q4xr-rc97-m4xx","Warn: Project is vulnerable to: PYSEC-2022-42986 / GHSA-43fp-rhv2-5gv8","Warn: Project is vulnerable to: PYSEC-2023-135 / GHSA-xqr8-7jwr-rhp7","Warn: Project is vulnerable to: PYSEC-2023-62 / GHSA-m2qf-hxjv-5gpq","Warn: Project is vulnerable to: GHSA-hc5x-x2vx-497g","Warn: Project is vulnerable to: GHSA-w3h3-4rj7-4ph4","Warn: Project is vulnerable to: PYSEC-2024-60 / GHSA-jjg7-2v4v-x38h","Warn: Project is vulnerable to: GHSA-cpwx-vrp4-4pq7","Warn: Project is vulnerable to: PYSEC-2021-66 / GHSA-g3rq-g295-4j3m","Warn: Project is vulnerable to: GHSA-h5c8-rqwp-cp95","Warn: Project is vulnerable to: GHSA-h75v-3vvj-5mfj","Warn: Project is vulnerable to: GHSA-q2x7-8rv6-6q7h","Warn: Project is vulnerable to: GHSA-55x5-fj6c-h6m8","Warn: Project is vulnerable to: PYSEC-2021-19 / GHSA-jq4v-f5q6-mjqq","Warn: Project is vulnerable to: PYSEC-2022-230 / GHSA-wrxv-2j5q-m38w","Warn: Project is vulnerable to: GHSA-3f63-hfp8-52jq","Warn: Project is vulnerable to: PYSEC-2021-41 / GHSA-3wvg-mj6g-m9cv","Warn: Project is vulnerable to: GHSA-44wm-f244-xhp3","Warn: Project is vulnerable to: GHSA-4fx9-vc88-q2xc","Warn: Project is vulnerable to: PYSEC-2021-35 / GHSA-57h3-9rgr-c24m","Warn: Project is vulnerable to: PYSEC-2021-331 / GHSA-7534-mm45-c74v","Warn: Project is vulnerable to: PYSEC-2021-137 / GHSA-77gc-v2xv-rvvh","Warn: Project is vulnerable to: PYSEC-2021-92 / GHSA-7r7m-5h27-29hp","Warn: Project is vulnerable to: PYSEC-2023-227 / GHSA-8ghj-p4vj-mr35","Warn: Project is vulnerable to: PYSEC-2022-10 / GHSA-8vj2-vxx3-667w","Warn: Project is vulnerable to: PYSEC-2021-36 / GHSA-8xjq-8fcg-g5hw","Warn: Project is vulnerable to: PYSEC-2021-42 / GHSA-95q3-8gr9-gm8w","Warn: Project is vulnerable to: PYSEC-2021-317 / GHSA-98vv-pw6r-q6q4","Warn: Project is vulnerable to: PYSEC-2021-38 / GHSA-9hx2-hgq2-2g4f","Warn: Project is vulnerable to: PYSEC-2022-168 / GHSA-9j59-75qj-795w","Warn: Project is vulnerable to: PYSEC-2021-40 / GHSA-f4w8-cv6p-x6r5","Warn: Project is vulnerable to: PYSEC-2021-139 / GHSA-g6rj-rv7j-xwp4","Warn: Project is vulnerable to: PYSEC-2021-94 / GHSA-hjfx-8p6c-g7gx","Warn: Project is vulnerable to: GHSA-j7hp-h8jx-5ppr","Warn: Project is vulnerable to: GHSA-jgpv-4h4c-xhw3","Warn: Project is vulnerable to: PYSEC-2022-42979 / GHSA-m2vv-5vj5-2hm7","Warn: Project is vulnerable to: PYSEC-2021-37 / GHSA-mvg9-xffr-p774","Warn: Project is vulnerable to: PYSEC-2021-39 / GHSA-p43w-g3c5-g5mq","Warn: Project is vulnerable to: PYSEC-2022-8 / GHSA-pw3c-h7wp-cvhx","Warn: Project is vulnerable to: PYSEC-2021-93 / GHSA-q5hq-fp76-qmrc","Warn: Project is vulnerable to: PYSEC-2021-138 / GHSA-rwv7-3v45-hg29","Warn: Project is vulnerable to: PYSEC-2022-9 / GHSA-xrcv-f9gm-v42c","Warn: Project is vulnerable to: PYSEC-2023-175","Warn: Project is vulnerable to: GHSA-9hjg-9r4m-mvj7","Warn: Project is vulnerable to: GHSA-9wx4-h78v-vm56","Warn: Project is vulnerable to: PYSEC-2023-74 / GHSA-j8r2-6x86-q33q","Warn: Project is vulnerable to: GHSA-34jh-p97f-mpxf","Warn: Project is vulnerable to: PYSEC-2021-59 / GHSA-5phf-pp7p-vc2r","Warn: Project is vulnerable to: PYSEC-2023-212 / GHSA-g4mx-q9vg-27p4","Warn: Project is vulnerable to: GHSA-pq67-6m6q-mj2v","Warn: Project is vulnerable to: PYSEC-2021-108 / GHSA-q2q7-5pp4-w6pg","Warn: Project is vulnerable to: PYSEC-2023-192 / GHSA-v845-jxx5-vc9f","Warn: Project is vulnerable to: GHSA-2g68-c3qc-8985","Warn: Project is vulnerable to: GHSA-f9vj-2wh5-fj8j","Warn: Project is vulnerable to: PYSEC-2023-221 / GHSA-hrfv-mqp8-q5rw","Warn: Project is vulnerable to: PYSEC-2023-57 / GHSA-px8h-6qxv-m22q","Warn: Project is vulnerable to: GHSA-q34m-jh98-gwm2","Warn: Project is vulnerable to: PYSEC-2023-58 / GHSA-xg9f-g7g7-2323","Warn: Project is vulnerable to: PYSEC-2022-203"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-23T22:51:19.341Z","repository_id":19841354,"created_at":"2025-08-23T22:51:19.341Z","updated_at":"2025-08-23T22:51:19.341Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30164532,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-06T04:43:31.446Z","status":"ssl_error","status_checked_at":"2026-03-06T04:40:30.133Z","response_time":250,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["flask","jinja2","python"],"created_at":"2024-08-02T22:01:32.557Z","updated_at":"2026-03-06T06:03:21.422Z","avatar_url":"https://github.com/stevelittlefish.png","language":"HTML","readme":"# Easy Forms\n\nForm processing library using Flask and Jinja2.\n\nThis library makes it very simple to create forms with field by field validation, and render them\ninto Bootstrap 3 style forms.\n\nIf you want to use this with Bootstrap 4 contact me - I will gladly add Bootstrap 4 support!\n\nIncluded in this repository is a sample application in the `testapp` folder.  Have a look through\nthe code to see some examples of the library being used.\n\nYou can view examples of the forms with this library, including most of the included fields at the\nfollowing url: [http://littlefish.solutions/easyforms/](http://littlefish.solutions/easyforms/)\n\n## Basic Usage\n\nTo create a form and pass into the template:\n\n```python\nimport easyforms\n\n@app.route('/some-url', method=['GET', 'POST'])\ndef some_view_function():\n    form = easyforms.Form([\n        easyforms.TextInput('some-text', required=True),\n        easyforms.IntegerInput('a-number', min_value=1, required=True)\n     ])\n   \n     return render_template('some-template.html', form=form)\n```\n\nThen to render it, inside the template:\n\n```html\n\u003chtml\u003e\n    \u003cbody\u003e\n        \u003ch1\u003eMy Page\u003c/h1\u003e\n        {{ form.render() }}\n    \u003c/body\u003e\n\u003c/html\u003e\n```\n\nIf the form has been submitted, and there are no validation error, form.ready will be `True`\nTo read from the form access it like a dictionary, using the name of each field to index:\n\n```python\n@app.route('/some-url', method=['GET', 'POST'])\ndef some_view_function():\n    form = easyforms.Form([\n        easyforms.TextInput('some-text', required=True),\n        easyforms.IntegerInput('a-number', min_value=1, required=True)\n    ])\n\n    if form.ready:\n        text = form['some-text']\n        number = form['a-number']\n        print('Submitted values: {} {}'.format(text, number))\n\n    return render_template('some-template.html', form=form)\n```\n\n## Multi Section Forms\n\nYou can create forms in multiple sections like this:\n\n```python\n@app.route('/some-url', method=['GET', 'POST'])\ndef some_view_function():\n    # read_form_data=False will ensure that we don't read form data until we have\n    # finished adding all of the sections\n    form = easyforms.Form([], read_form_data=False)\n\n    form.add_section('Section 1', [\n        easyforms.TextField('text-field', required=True,\n                            help_text='Any text will be accepted'),\n        easyforms.PasswordField('password-field', required=True)\n    ])\n    \n    form.add_section('Section 2', [\n        easyforms.EmailField('email-address', required=True),\n        easyforms.BooleanCheckbox('confirm', help_text='I confirm I want to do this!')\n    ])\n\n    return render_template('some-template.html', form=form)\n```\n\nIf we simply call `form.render()` in the template, the entire form will be rendered with\nheadings separating each section, but this isn't usually what you want to do. You can\nalso render each of the form sections yourself to create customer layouts:\n\n```html\n\u003chtml\u003e\n    \u003cbody\u003e\n        \u003ch1\u003eTwo Column Form\u003c/h1\u003e\n        {{ form.render_before_sections() }}\n            \u003cdiv class=\"row\"\u003e\n                \u003cdiv class=\"col-md-6\"\u003e\n                    {{ form.render_section('Section 1') }}\n                \u003c/div\u003e\n                \u003cdiv class=\"col-md-6\"\u003e\n                    {{ form.render_section('Section 2') }}\n                \u003c/div\u003e\n            \u003c/div\u003e\n            \u003chr\u003e\n        {{ form.render_after_sections() }}\n    \u003c/body\u003e\n\u003c/html\u003e\n```\n\nThe first call to `form.render_before_sections()` is equivalent to opening a form tag. Then\nwe can render each section by name using `form.render_section('Section Name')`.  Finally\nwe call `form.render_after_sections()` which will render the submit button and close the form.\n\nWhen we're processing our form data, we have to manually tell the form to read the form data\nonce we've finished adding our sections (remember that parameter read_form_data=False?).\nThen we can access all of the fields from all sections as if it were a single dictionary:\n\n```python\n@app.route('/some-url', method=['GET', 'POST'])\ndef some_view_function():\n    # read_form_data=False will ensure that we don't read form data until we have\n    # finished adding all of the sections\n    form = easyforms.Form([], read_form_data=False)\n\n    form.add_section('Section 1', [\n        easyforms.TextField('text-field', required=True,\n                            help_text='Any text will be accepted'),\n        easyforms.PasswordField('password-field', required=True)\n    ])\n    \n    form.add_section('Section 2', [\n        easyforms.EmailField('email-address', required=True),\n        easyforms.BooleanCheckbox('confirm', help_text='I confirm I want to do this!')\n    ])\n    \n    form.read_form_data()\n\n    if form.submitted:\n        text = form['text-field']\n        confirmed = form['confirm']\n        if confirmed:\n            print('Confirmed with this text: {}'.format(text))\n\n    return render_template('some-template.html', form=form)\n```\n\n## Custom Validation\n\nThere are 2 approaches to adding customer validation. The first method is to manually do the\nvalidation in the view function:\n\n```python\n@main.route('/custom-validation-1', methods=['GET', 'POST'])\ndef custom_validation_1():\n    form = easyforms.Form([\n        easyforms.IntegerField('an-integer', help_text='Any whole number!', required=True),\n        easyforms.TextField('two-or-three', required=True)\n    ])\n    \n    if form.submitted:\n        # The form was submitted, but not necessarily validated\n        # If there is a value in the two-or-three field, and it doen't already have an error set\n        if form['two-or-three'] and not form.has_error('two-or-three'):\n            # Check if the string contains \"two\" or \"three\" and set an error if it doesn't\n            two_or_three = form['two-or-three']\n            if two_or_three != 'two' and two_or_three != 'three':\n                form.set_error('two-or-three', 'Please enter \"two\" or \"three\" (lower case)')\n    \n    # If we set an error, form.ready will no longer be True\n    if form.ready:\n        print('The form was submitted!')\n\n    return render_template('custom_validation_1.html', form=form)\n\n```\n\nThis works, and is good for one-offs, but is a bit tedious and ugly.  Another method we can use is\nto define validation functions and pass them into the form fields:\n\n```python\n@main.route('/custom-validation-2', methods=['GET', 'POST'])\ndef custom_validation_2():\n    def is_two_or_three(val):\n        if val and val != 'two' and val != 'three':\n            return 'Please enter \"two\" or \"three\" (lower case)'\n\n    form = easyforms.Form([\n        easyforms.IntegerField('an-integer', help_text='Any whole number!', required=True),\n        easyforms.TextField('two-or-three', required=True, validators=[is_two_or_three])\n    ])\n    \n    if form.ready:\n        print('The form was submitted!')\n\n    return render_template('custom_validation_2.html', form=form)\n```\n\nHere we define a function `is_two_or_three` that takes a single argument, and if that argument is\nnot valid, returns an error message. It's usually a good idea not to return an error message if\nthe value is not set, otherwise it's impossible to use the validator on an optional field.\n\nThis is a little bit neater and has the advantage of making the validator functions reusable.\n\n## Custom Fields\n\nSooner or later you are going to want to add some custom fields, either to add fields that\nlook different to the standard fields, or to add fields which handle different types of data.\nThis involves a little bit of setup.  Look at the `testapp.customfields` package for an example.\n\n### Custom field to handle different data\n\nThis first example is a very simple example that doesn't affect the rendering of the field it's\nbased on, but changes the behaviour.  In this example, any text that's submitted into the field\nis converted to upper case.\n\nDefine the following class for the field:\n\n```python\nfrom easyforms import basicfields\n\n\nclass UpperCaseTextField(basicfields.TextField):\n    def __init__(self, name, value=None, **kwargs):\n        super().__init__(name, value.upper() if value is not None else None)\n    \n    def convert_value(self):\n        # self.value is the raw string\n        if self.value is not None:\n            # The string is present - convert to upper case\n            self.value = self.value.upper()\n```\n\nWhen a field is submitted, the raw string is taken from the form data and placed in the `value`\nattribute.  Then, for each field in the form, `convert_value` is called, which should convert\nthe value to whatever data-type it's supposed to be and store it back in `value`.\n\nIn this example, the submitted value is simply converted to upper case, but you could convert it\nto any data type you wanted, or even query a database and place a model in value, or return any\nkind of complex object.\n\nIt should be noted that it's normally best to leave `None` values as `None` so that optional fields\ncan be created.\n\n### Custom field with customer renderer - without using jinja2\n\nThis next example shows how to create a field that looks different to the standard fields.  You'd\nprobably never want to actually create a field in quite this manner, but it does demonstrate how\nto override the `render` method of a field, and that you don't have to use Jinja2:\n\n```python\nclass BasicCustomTextField(basicfields.TextField):\n    def render(self):\n        html = '[BASIC FIELD]\u003cbr\u003e {label}: \u003cinput type=\"text\" name=\"{name}\" value=\"{value}\"\u003e\u003cbr\u003e\u003cbr\u003e'.format(\n            label=self.label,\n            name=self.name,\n            value=self.value if self.value else ''\n        )\n\n        if self.error:\n            html = '\u003cp\u003e\u003cstrong\u003eERROR: {}\u003c/strong\u003e\u003c/p\u003e{}'.format(self.error, html)\n\n        return html\n```\n\nHere we simply override the `render` method and output some html which we construct manually.\n\n### Custom Field with Jinja2 Renderer\n\nThis is a bit more in depth and could probably be acheived in any number of ways.\n\nVery briefly, this is what you'll probably want to do:\n\n#### 1. Create a package and a jinja2 environment for your fields\n\nCreate a package, and inside that package create a python file called env.py with the jinja2\nenvironment like this:\n\n```python\nimport logging\nimport os\n\nfrom jinja2 import Environment, FileSystemLoader\nimport jinja2\n\nfrom easyforms import formtype\n\n__author__ = 'Stephen Brown (Little Fish Solutions LTD)'\n\nlog = logging.getLogger(__name__)\n\n\ndef _suppress_none(val):\n    \"\"\"Returns an empty string if None is passed in, otherwide returns what was passed in\"\"\"\n    if val is None:\n        return ''\n    return val\n\n# Create the jinja2 environment\n_current_path = os.path.dirname(os.path.realpath(__file__))\n_template_path = os.path.join(_current_path, 'templates')\n\nenv = Environment(loader=FileSystemLoader(_template_path), autoescape=True)\n# Don't allow undefined variables to be ignored\nenv.undefined = jinja2.StrictUndefined\n# Custom filter to replace None with empty string\nenv.filters['sn'] = _suppress_none\nenv.globals['formtype'] = formtype\n```\n\nYou can base this off of the env.py in the easyforms package.\n\n#### 2. Create a templates directory and optionally copy in the easyform macros\n\nInside you package create a folder called `templates`. If you want to use the easyform macros\n(hint: you probably do) copy in 'easyforms/templates/macros.html` so that you can import it\nin your templates.\n\nNow create your template for your new field.  I created `text_field.html` to create a field\nwith a custom label with the text \"[ CUSTOM FIELD ]\" in green as part of the label text:\n\n```html\n{% import 'macros.html' as macros %}\n\n\u003cdiv {{ field.form_group_attributes }}\u003e\n\t{% if field.label_width \u003e 0 %}\n\t\t\u003clabel for=\"{{ field.id }}\" class=\"{{ field.label_column_class }}\"\u003e\n\t\t\t\u003cstrong class=\"green\"\u003e[ CUSTOM FIELD ]\u003c/strong\u003e\n\t\t\t{{ field.label_html }}\n\t\t\u003c/label\u003e\n\t{% endif %}\n\n\t\u003cdiv {{ field.input_column_attributes }}\u003e\n\t\t\u003cinput type=\"{{ field.type }}\"\n\t       class=\"form-control {{ field.css_class|sn }}\"\n\t       name=\"{{ field.name }}\"\n\t       id=\"{{ field.id }}\" value=\"{{ field.value|sn }}\" placeholder=\"{{ field.placeholder|sn }}\"\n\t       {% if field.readonly %}readonly{% endif %}\u003e\n\n\t\u003c/div\u003e\n\n\t{{ macros.standard_error(field) }}\n\t{{ macros.standard_help_text(field) }}\n\u003c/div\u003e\n\n```\n\n\n#### 3. Create a module in you package with your customer fields\n\nHere we're going to create our `CustomTextField` class:\n\n```python\nfrom easyforms import basicfields\n\nfrom .env import env\n\nclass CustomTextField(basicfields.TextField):\n    def render(self):\n        return env.get_template('text_field.html').render(field=self)\n```\n\nNow we can use this in our form:\n\n```python\n\nimport easyforms\nfrom customfields.fields import CustomTextField\n\ndef some_view():\n    form = easyforms.Form([\n        easyforms.TextField('normal-text-field'),\n        CustomTextField('custom-field')\n    ])\n```\n\n### A Custom Template and Custom Behaviour\n\nThis final example will implement a field where the user can type in a comma separated list of\nstrings, and it will be converted into a list.  This also means that the developer can pass in\na list of strings into the `value` parameter of the form field.\n\nFirst the template in `templates/comma_separated_list.html`:\n\n```html\n{% import 'macros.html' as macros %}\n\n\u003cdiv {{ field.form_group_attributes }}\u003e\n\t{{ macros.standard_label(field) }}\n\n\t\u003cdiv {{ field.input_column_attributes }}\u003e\n\t\t\u003cinput type=\"{{ field.type }}\"\n\t       class=\"form-control {{ field.css_class|sn }}\"\n\t       name=\"{{ field.name }}\"\n\t       id=\"{{ field.id }}\" value=\"{{ ', '.join(field.value) if field.value else '' }}\" placeholder=\"{{ field.placeholder|sn }}\"\n\t       {% if field.readonly %}readonly{% endif %}\u003e\n\n\t\u003c/div\u003e\n\n\t{{ macros.standard_error(field) }}\n\t{{ macros.standard_help_text(field) }}\n\u003c/div\u003e\n```\n\nLook at what we did with the `value` attribute.\n\nNow the python class:\n\n```python\nclass CommaSeparatedListField(basicfields.TextField):\n    def render(self):\n        return env.get_template('comma_separated_list.html').render(field=self)\n\n    def convert_value(self):\n        # self.value is the raw string\n        if self.value is not None:\n            # The string is present - convert it into a list\n            parts = self.value.split(',')\n            self.value = [x.strip() for x in parts if x.strip()]\n```\n\nNow we can use it like this:\n\n```python\nimport easyforms\nfrom customfields.fields import CommaSeparatedListField\n\ndef some_view():\n    form = easyforms.Form([\n        CommaSeparatedListField('list', value=['hello', 'goodbye', 'testing')\n    ])\n\n    if form.ready:\n        list = form['list']\n        # list is a list of strings instead of the raw string\n        print(list)\n        # prints something like: ['hello', 'goodbye', 'testing']\n```\n\n","funding_links":[],"categories":["HTML"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevelittlefish%2Feasyforms","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstevelittlefish%2Feasyforms","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevelittlefish%2Feasyforms/lists"}