-
Notifications
You must be signed in to change notification settings - Fork 4
Plugin mechanism #345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Plugin mechanism #345
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| from django.test import TestCase | ||
|
|
||
| from formidable.register import FieldSerializerRegister, load_serializer | ||
| from formidable.serializers import FormidableSerializer | ||
| from formidable.serializers.fields import BASE_FIELDS, FieldSerializer | ||
|
|
||
|
|
||
| class TestCustomFieldSerializer(TestCase): | ||
| def setUp(self): | ||
| field_register = FieldSerializerRegister.get_instance() | ||
| custom_type_id = 'custom_type_id' | ||
| self.custom_type_id = custom_type_id | ||
|
|
||
| @load_serializer(field_register) | ||
| class CustomFieldSerializer(FieldSerializer): | ||
| type_id = custom_type_id | ||
|
|
||
| class Meta(FieldSerializer.Meta): | ||
| fields = BASE_FIELDS + ('parameters', ) | ||
| config_fields = ('meta_info', 'some_another_data') | ||
|
|
||
| self.custom_field_serializer_class = CustomFieldSerializer | ||
| self.field_register = field_register | ||
| self.schema = { | ||
| "label": "test", | ||
| "description": "test", | ||
| "fields": [ | ||
| { | ||
| "slug": "custom-type-id", | ||
| "label": "Custom field", | ||
| "placeholder": None, | ||
| "description": None, | ||
| "defaults": [], | ||
| "multiple": False, | ||
| "config_field": "Test test test", | ||
| "values": [], | ||
| "required": False, | ||
| "disabled": False, | ||
| "isVisible": True, | ||
| "type_id": "custom_type_id", | ||
| "validations": [], | ||
| "order": 1, | ||
| "meta_info": "meta", | ||
| "some_another_data": "some_another_data", | ||
| "accesses": [ | ||
| { | ||
| "id": "field-access868", | ||
| "level": "EDITABLE", | ||
| "access_id": "padawan" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
|
|
||
| def test_register(self): | ||
| self.assertIn(self.custom_type_id, self.field_register) | ||
|
|
||
| def test_custom_field_serialize(self): | ||
| serializer = FormidableSerializer(data=self.schema) | ||
| serializer.is_valid() | ||
| serializer.save() | ||
| # get field instance | ||
| self.instance = serializer.instance | ||
| custom_field = serializer.instance.fields.first() | ||
| # test field instance | ||
| self.assertIn('meta_info', custom_field.parameters) | ||
| self.assertEqual(custom_field.parameters['meta_info'], "meta") | ||
| self.assertIn('some_another_data', custom_field.parameters) | ||
| self.assertEqual( | ||
| custom_field.parameters['some_another_data'], | ||
| "some_another_data" | ||
| ) | ||
| # get serialized data | ||
| data = FormidableSerializer(serializer.instance).data['fields'][0] | ||
| # test serialized data | ||
| self.assertIn('meta_info', data) | ||
| self.assertIn('some_another_data', data) | ||
| self.assertNotIn('parameters', data) | ||
| # remove instance | ||
| self.instance.delete() | ||
|
|
||
| def tearDown(self): | ||
| self.field_register.pop(self.custom_type_id) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| =============================== | ||
| External Field Plugin Mechanism | ||
| =============================== | ||
|
|
||
| We've included a mechanism to add your own fields to the collection of available fields in ``django-formidable``. | ||
|
|
||
| It'll be possible to: | ||
|
|
||
| * define a new form using this new type of field, | ||
| * store their definition and parameters in a Formidable object instance (and thus, in the database), | ||
| * using this form definition, validate the end-user data when filling this form against your field business logic mechanism. | ||
|
|
||
| For the sake of the example, let's say you want to add a "Color Picker" field in django-formidable. You'll have to create a django library project that we'll call ``django-formidable-color-picker``. Let's say that this module has its own ``setup.py`` with the appropriate scripts to be installed in dev mode using ``pip install -e ./``. | ||
|
|
||
| Let's also say that you have added it in your ``INSTALLED_APPS``. | ||
|
|
||
| Tree structure | ||
| ============== | ||
|
|
||
| :: | ||
|
|
||
| . | ||
| ├── formidable_color_picker | ||
| │ ├── apps.py | ||
| │ ├── __init__.py | ||
| │ ├── serializers.py | ||
| ├── setup.cfg | ||
| └── setup.py | ||
|
|
||
| Loading the field for building time | ||
| =================================== | ||
|
|
||
| The first file we're going to browse is :file:`serializers.py`. Here's a minimal version of it: | ||
|
|
||
|
|
||
| .. code-block:: python | ||
|
|
||
| from formidable.register import load_serializer, FieldSerializerRegister | ||
| from formidable.serializers.fields import FieldSerializer, BASE_FIELDS | ||
|
|
||
| field_register = FieldSerializerRegister.get_instance() | ||
|
|
||
|
|
||
| @load_serializer(field_register) | ||
| class ColorPickerFieldSerializer(FieldSerializer): | ||
|
|
||
| type_id = 'color_picker' | ||
|
|
||
| class Meta(FieldSerializer.Meta): | ||
| fields = BASE_FIELDS | ||
|
|
||
| Then you're going to need to make sure that Django would catch this file at startup, and thus load the Serializer. It's done via the :file:`apps.py` file. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| from __future__ import absolute_import | ||
| from django.apps import AppConfig | ||
|
|
||
|
|
||
| class FormidableColorPickerConfig(AppConfig): | ||
| """ | ||
| Formidable Color Picker configuration class. | ||
| """ | ||
| name = 'formidable_color_picker' | ||
|
|
||
| def ready(self): | ||
| """ | ||
| Load external serializer when ready | ||
| """ | ||
| from . import serializers # noqa | ||
|
|
||
| As you'd do for any other Django application, you can now add this line to your :file:`__init__.py` file at the root of the python module: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| default_app_config = 'formidable_color_picker.apps.FormidableColorPickerConfig' | ||
|
|
||
| Check that it's working | ||
| ----------------------- | ||
|
|
||
| Loading the Django shell: | ||
|
|
||
| .. code-block:: pycon | ||
|
|
||
| >>> from formidable.serializers import FormidableSerializer | ||
| >>> data = { | ||
| "label": "Color picker test", | ||
| "description": "May I help you pick your favorite color?", | ||
| "fields": [{ | ||
| "slug": "color", | ||
| "label": "What is your favorite color?", | ||
| "type_id": "color_picker", | ||
| "accesses": [], | ||
| }] | ||
| } | ||
| >>> instance = FormidableSerializer(data=data) | ||
| >>> instance.is_valid() | ||
| True | ||
| >>> formidable_instance = instance.save() | ||
|
|
||
| This means that you can create a form with a field whose type is not in ``django-formidable`` code, but in your module's. | ||
|
|
||
| Then you can also retrieve this instance JSON defintion | ||
|
|
||
| .. code-block:: pycon | ||
|
|
||
| >>> import json | ||
| >>> print(json.dumps(formidable_instance.to_json(), indent=2)) | ||
| { | ||
| "label": "Color picker test", | ||
| "description": "May I help you pick your favorite color?", | ||
| "fields": [ | ||
| { | ||
| "slug": "color", | ||
| "label": "What is your favorite color?", | ||
| "type_id": "color_picker", | ||
| "placeholder": null, | ||
| "description": null, | ||
| "accesses": [], | ||
| "validations": [], | ||
| "defaults": [], | ||
| } | ||
| ], | ||
| "id": 42, | ||
| "conditions": [], | ||
| "version": 5 | ||
| } | ||
|
|
||
| Making your field a bit more clever | ||
| =================================== | ||
|
|
||
| Let's say that colors can be expressed in two ways: RGB tuple (``rgb``) or Hexadecimal expression (``hex``). This means your field has to be parametrized in order to store this information at the builder step. Let's imagine your JSON payload would look like: | ||
|
|
||
| .. code-block:: json | ||
|
|
||
| { | ||
| "label": "Color picker test", | ||
| "description": "May I help you pick your favorite color?", | ||
| "fields": [{ | ||
| "slug": "color", | ||
| "label": "What is your favorite color?", | ||
| "type_id": "color_picker", | ||
| "accesses": [], | ||
| "color_format": "hex" | ||
| }] | ||
| } | ||
|
|
||
| You want then to make sure that your user would not send a wrong parameter, as in these BAD examples: | ||
|
|
||
| .. code-block:: json | ||
|
|
||
| "color_format": "" | ||
| "color_format": "foo" | ||
| "color_format": "wrong" | ||
|
|
||
| For this specific field, you only want one parameter and its key is ``format`` and its values are only ``hex`` or ``rgb`` | ||
|
|
||
| Let's add some validation in your Serializer, then. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| from rest_framework import serializers | ||
| from formidable.register import load_serializer, FieldSerializerRegister | ||
| from formidable.serializers.fields import FieldSerializer, BASE_FIELDS | ||
|
|
||
| field_register = FieldSerializerRegister.get_instance() | ||
|
|
||
|
|
||
| @load_serializer(field_register) | ||
| class ColorPickerFieldSerializer(FieldSerializer): | ||
|
|
||
| type_id = 'color_picker' | ||
|
|
||
| allowed_formats = ('rgb', 'hex') | ||
| default_error_messages = { | ||
| "missing_parameter": "You need a `format` parameter for this field", | ||
| "invalid_format": "Invalid format: `{format}` is not one of {formats}." | ||
| } | ||
|
|
||
| class Meta(FieldSerializer.Meta): | ||
| config_fields = ('color_format', ) | ||
| fields = BASE_FIELDS + ('parameters',) | ||
|
|
||
| def to_internal_value(self, data): | ||
| data = super(ColorPickerFieldSerializer, self).to_internal_value(data) | ||
| # Check if the parameters are compliant | ||
| format = data.get('color_format') | ||
| if format is None: | ||
| self.fail('missing_parameter') | ||
|
|
||
| if format not in self.allowed_formats: | ||
| self.fail("invalid_format", | ||
| format=format, formats=self.allowed_formats) | ||
|
|
||
| return super(ColorPickerFieldSerializer, self).to_internal_value(data) | ||
|
|
||
| .. note:: Full example | ||
|
|
||
| You may browse this as a complete directly usable example in `the following repository: "django-formidable-color-picker" <https://github.com/peopledoc/django-formidable-color-picker>`_ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # -*- coding: utf-8 -*- | ||
| # Generated by Django 1.11.6 on 2018-07-23 10:37 | ||
| from __future__ import unicode_literals | ||
|
|
||
| from django.db import migrations | ||
| import jsonfield.fields | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('formidable', '0008_formidable_item_value_field_size'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name='field', | ||
| name='parameters', | ||
| field=jsonfield.fields.JSONField( | ||
| blank=True, default={}, null=True | ||
| ), | ||
| ), | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need blank and null while there is a default value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a habit for the Django models. When you use it from the admin panel, you can't save a blank field if you haven't specified
null=True, blank=Trueeven with a declared default value. Maybe it's an overkill here but I'd like to keep it.