We could directly create a simple form by inheriting forms.Form. However, I want forms to be dynamically created based on the key-value specified by the admin. Consider, user provides a map as :
[
{
"key": "name",
"value": "CharField"
},
{
"key": "url",
"value": "URLField"
}
]
Based on the map given above how can I create a form which is equivalent to the class based form creation below:
from django import forms
class CommentForm(forms.Form):
name = forms.CharField(label='name')
url = forms.URLField(label='url')
My current approach might be to iterate across each key-value pair and make a HTML form manually. Is it possible to create dynamic forms in django without having to write a class definition?
Yes, you can do this in the init function of the form.
Pass in the mapping array as an extra keyword argument when creating the form, then pop it from the kwargs before calling the init method from super.
You can then use this to dynamically add fields to the form.
class CommentForm(forms.Form):
def __init__(self , *args, **kwargs):
field_mapping_array = kwargs.pop('field_mapping_array')
super(CommentFrom, self).__init__(*args, **kwargs)
for field_type in field_mapping_array:
field_name = field_type['key'] # make sure keys are unique
if field_type[value] == "CharField":
self.fields[field_name] = form.CharField(...extra args here)
elif field_type['value'] == 'UrlField' :
self.fields[field_name] = form.UrlField(...extra args here)
elif .... map other field types here....
When creating the form pass the field_mapping_array into the form class.
field_mapping = [
{
"key": "name",
"value": "CharField"
},
{
"key": "url",
"value": "URLField"
}
]
comment_form = CommentForm(field_mapping_array=field_mapping)
Related
I am looking for the good architecture for my problem. I am using django rest framework for building an API. I receive a list of dict which contains an id and a list of values. The list of values need to be validated according to the id.
Example of my code:
class AttributesSerializer(serializers.Serializer):
id = serializers.PrimaryKeyRelatedField(queryset=Attribute.objects.all(), source="attribute", required=True)
values = serializers.ListField()
def validate(self, validated_data):
attribute = validated_data["attribute"]
values = validated_data["values"]
# This function returns the corresponding field according to attribute
values_child_field = get_values_field(attribute)
self.fields["values"].child = values_child_fields
new_values = self.fields["values"].run_child_validation(values)
set_value(validated_data, "values", new_values)
return validated_data
class BaseObjectApiInputSerializer(serializers.Serializer):
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all()
)
attributes = AttributesSerializer(many=True)
I want to parse json like this:
{
"categorty_id": 42, # Category pk of the baseobject. which defines some constraints about attributes available
"attributes": [
{"id": 124, "values": ["value"]},
{"id": 321, "values": [42]},
{
"id": 18,
"values": [
{
"location": {"type": "Point", "geometry": {...}},
"address": "an address",
}
],
},
]
}
Currently, this code does not work. DRF seems to try to revalidate all values entries for each iteration with each child field. I do not understand why... I guess I could make it work without using this fields["values"] for making the validation and just retrieve the field and use it directly, but i need this field for making the save later.
Do you think my architecture is ok? What is the good way for parsing this type of data with DRF?
EDIT:
Structure of models are complex but a version simplified following:
class Attribute(models.Model):
class DataType(models.TextChoices):
TEXT = "TEXT", _("datatype_text")
INTEGER = "INTEGER", _("datatype_integer")
DATETIME = "DATETIME", _("datatype_datetime")
BOOL = "BOOL", _("datatype_bool")
# Some examples, but there are about 30 items with
# type very complicated like RecurrenceRule (RFC2445)
# or GeoJSON type
label = models.CharField()
category = models.ForeignKey(Category)
attribute_type = models.CharField(choices=DataType.choices)
class AttributeValue(models.Model):
attribute = models.ForeignKey(Attribute)
# a model which represents an object with list of attributes
baseobject = models.ForeignKey(BaseObject)
value = models.TextField()
AttributeValue is like a through table for manytomany relation between BaseObject model and Attribute model.
My JSON represents the list of attribute/values attached to a baseobject.
In fact I don't understand why DRf doesn't allow delegating registration in the child serializers of the parent serializer. This would allow much greater flexibility in code architecture and separation of responsibilities.
EDIT 2 :
My urls.py
router = routers.DefaultRouter()
router.register("baseobjects", BaseObjectViewSet, basename="baseobjects")
I am using the default router and url for DRF viewset.
The view looks like:
class BaseObjectViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
authentication_classes = [TokenAuthentication]
def create(self, request, *args, **kwargs):
serializer = BaseObjectApiInputSerializer(
data=request.data
)
if not serializer.is_valid():
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
baseobject: BaseObject = serializer.save()
return Response(
{"results": [{"id": baseobject.pk}]}, status=HTTP_200_OK
)
I think you should use ListField with JSONField as child argument for values field.
validators = {
TinyurlShortener.DataType.TEXT: serializers.CharField(),
TinyurlShortener.DataType.INTEGER: serializers.IntegerField(),
TinyurlShortener.DataType.DATETIME: serializers.DateTimeField(),
TinyurlShortener.DataType.BOOL: serializers.BooleanField(),
}
class AttributesSerializer(serializers.Serializer):
id = serializers.PrimaryKeyRelatedField(queryset=Attribute.objects.all(), source="attribute", required=True)
values = serializers.ListField(
child=serializers.JSONField()
)
def validate(self, attrs):
attribute = attrs.get('id')
field = validators[attribute.attribute_type]
for v in attrs['values']:
field.run_validation(json.loads(v.replace("'", '"')))
return super().validate(attrs)
class BaseObjectApiInputSerializer(serializers.Serializer):
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all()
)
attributes = AttributesSerializer(many=True)
I'm creating a django rest framework application with this structure (assuming imports are correct, so I omit them from the code below.
models.py:
class Door(models.Model):
type = models.CharField(max_length=40)
color = models.CharField(max_length=40)
serializers.py:
class DoorSerializer(serializers.ModelSerializer):
class Meta:
model = Door
fields = ['type', 'color']
views.py:
class DoorViewSet(viewsets.ModelViewSet):
serializer_class = DoorSerializer
queryset = Door.objects.all()
def get_queryset(self, *args, **kwargs):
queryset = Door.objects.all()
parameter = self.request.query_params.get('type', '')
if parameter:
return queryset.filter(type=parameter)
else:
return queryset
So far this behaves as intended, when I make an api call to localhost/Doors it lists all the doors. And when I make an api call to localhost/Doors/?type=big it lists all the doors that have the value "big" in their "type" field.
The addition I would like to make is another parameter check which would return a list of all the unique door types that exist in the database. This can be achieved in the manage.py shell by using: Door.objects.all().values('type').distinct()
My attempt was the following modifications to views.py:
...
parameter = self.request.query.params.get('type', '')
unique = self.request.query.params.get('unique', '')
if parameter:
...
elif unique:
return Door.objects.all().values('type').distinct()
...
My assumption was that this would return the same as Door.objects.all().values('type').distinct() when I make a call to localhost/Doors/?unique=whatever
However I am getting the error: "Got KeyError when attempting to get a value for field color on serializer DoorSerializer.\nThe serializer field might be named incorrectly and not match any attribute or key on the dict instance.\nOriginal exception text was: 'color'."
I assume this means that the serializer expects an object or a list of objects that contains all the fields of the corresponding model.
Is there some way I could circumvent this by fixing the view or should I create a different serializer? In either case, since I've gotten pretty confused with DRF / django differences and it is possible I won't be able to follow abstract instructions, could you provide a code solution that addresses the issue? Also, in the very likely case that my assumption is completely off, could you also explain what is causing the problem? Thank you for your time!
Edit for clarifying the desired result:
Assuming my database has 4 doors which are:
{
"id": 1,
"type": "big",
"color": "blue"
},
{
"id": 2,
"type": "big",
"color": "yellow"
},
{
"id": 3,
"type": "small",
"color": "green"
},
{
"id": 4,
"type": "big",
"color": "red"
},
I would like to make a get request to some url, for instance localhost/Doors/?unique=Yes and have the api return to me the list {"big", "small}
WRITING YOUR OWN VIEW: Short view that returns the list of type. You need to set up a new path here. I'd personally go for this option as the response you expect is way different to what the rest of your view does.
from rest_framework.decorators import api_view
from rest_framework.response import Response
#api_view()
def Unique_door_types(request):
types = Door.objects.values_list('type', flat=True).distinct()
return Response({"types": list(types)})
WITHOUT AN ADDITIONAL VIEW:
No need for additional view or serializer. Override the list method. Note that this is closer to a trick than to a good way of programming.
from rest_framework.response import Response
class DoorViewSet(viewsets.ModelViewSet):
serializer_class = DoorSerializer
def get_queryset(self, *args, **kwargs):
queryset = Door.objects.all()
parameter = self.request.query_params.get('type', '')
if parameter:
return queryset.filter(type=parameter)
else:
return queryset
def list(self, request):
unique = self.request.query_params.get('unique', '')
if unique:
types = Door.objects.values_list('type', flat=True).distinct()
return Response({"types": list(types)})
return super().list()
My suggestion would be to create a separate route like /doors/types/. You do this by adding a method to your DoorViewSet class with a #action decorator. See https://www.django-rest-framework.org/api-guide/viewsets/#marking-extra-actions-for-routing for more details about how to do this.
I'm writing a view generator for my Django project. I have a large number of models from a legacy application (~150 models), that all need the same basic CRUD operations (providing Admin access isn't enough apparently).
So I'm writing a generator that returns 5 Views for each model, and of course each view can potentially take a large number of options, and I'm trying to define sane API/default parameter format for my generator.
My current generator:
def generate_views(model_class, **kwargs):
"""
For a given model, returns a dict of generic class-based views
"""
###
# Forms
# Optionally generate form classes if not already provided
###
# Append these fields with either "create_" or "update_" to have them only
# apply to that specific type of form
form_override_args = ['fields', 'exclude', 'form_method', 'form_class',
'form_layout', 'widgets', 'media_css', 'media_js']
if 'form_class' not in kwargs and 'create_form_class' not in kwargs:
create_form_kwargs = kwargs.copy()
for arg in form_override_args:
if f'create_{arg}' in kwargs:
create_form_kwargs[arg] = kwargs[f'create_{arg}']
kwargs['create_form_class'] = forms.FormFactory(model_class, **create_form_kwargs).form()
if 'form_class' not in kwargs and 'update_form_class' not in kwargs:
update_form_kwargs = kwargs.copy()
for arg in form_override_args:
if f'update_{arg}' in kwargs:
update_form_kwargs[arg] = kwargs[f'update_{arg}']
kwargs['update_form_class'] = forms.FormFactory(model_class, **update_form_kwargs).form()
if 'form_class' not in kwargs:
kwargs['form_class'] = forms.FormFactory(model_class, **kwargs).form()
###
# Tables
# Optionally generate table classes if not already provided
###
# Append these fields with "table_" to have them only
# apply to the table view
table_override_args = ['fields', 'exclude']
if 'table_class' not in kwargs:
update_table_kwargs = kwargs.copy()
for arg in table_override_args:
if f'table_{arg}' in kwargs:
update_table_kwargs[arg] = kwargs[f'table_{arg}']
kwargs['table_class'] = tables.TableFactory(model_class, **update_table_kwargs).table()
###
# Views
# Generate 5 generic views based on the provided model
###
view_factory = views.ViewFactory(model_class, **kwargs)
return {
'list_view': view_factory.list_view(),
'detail_view': view_factory.detail_view(),
'create_view': view_factory.create_view(),
'update_view': view_factory.update_view(),
'delete_view': view_factory.delete_view()
}
I'm currently relying on kwargs, and I wanted to define what a fully filled-out kwargs dict should look like. Something like
{
'forms': {
'all': {
},
'create': {
},
'update': {
}
},
'tables': {
'all': {
},
'list': {
}
},
'views': {
'all': {
},
'list': {
},
'detail': {
},
'create': {
},
'update': {
},
'delete': {
}
}
}
And it's just seeming a bit overworked. I'm mostly looking for recommendations on a potentially better design (because I'm going cross eyed from just working on it).
It seems that you are fighting the way how Django structures discrete functionalities/configurations in class-based views.
Django’s generic class-based views are built out of mixins providing discrete functionality.
So, my suggestion is: using mixins to incoporate the table and form classes into your views for the CRUD operation. In the generator, all configurable parameters should be passed only to the views.
Backgrounds knowledge
Let's look at how django.views.generic.edit.CreateView is designed. It inherits methods and attributes from:
SingleObjectTemplateResponseMixin,
BaseCreateView and
ModelFormMixin.
It can be bound to a model simply with a few lines of codes:
from myapp.models import Author
class AuthorCreateView(CreateView):
model = Author
fields = ['FirstName','FamilyName','BirthDay']
def form_valid(self, form):
# Saves the form instance, sets the current object for the view, and redirects to get_success_url().
Here the model attribute is shared by all the mixins to do their jobs, while fields and form_valid are specific to ModelFormMixin.
Although all configurable parameters/methods are put together under the View class, each mixin just picks up those it needs.
Redesign the API
Keeping this in mind, let's begin to simplify your view generator/factory. For this example, let's say you have the following base classes that include common (default) settings:
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.views.generic import ListView, DetailView
from django_tables2 as SingleTableMixin
class TableListView(SingleTableMixin, ListView):
table_pagination = { 'per_page': 10 }
# add common configurable parameters here
class MyOwnCreateView(CreateView):
success_url = "/yeah"
# Introduce a configurable method `form_valid_hook`
def form_valid(self, form):
if hasattr(self,'form_valid_hook'):
self.form_valid_hook(form)
return super().form_valid(form)
Below is the simplified generator function for all 5 views.
BaseViews= {'create': MyOwnCreateView,
'delete': DeleteView,
'update': UpdateView,
'list' : TableListView,
'detail': DetailView }
def generate_views(model_class, **kwargs):
"""
Generate views for `model_class`
Keyword parameters:
{action}=dict(...)
{action}_mixins=tuple(...)
where `action` can be 'list', 'detail', 'create', 'update', 'delete'.
"""
NewViews = {}
for action, baseView in BaseViews.items():
viewName = model_class.__name__ + baseView.__name__
viewAttributes = kwargs.get(action,{})
viewBaseCls = (baseView,) + kwargs.get(f"{action}_mixins",tuple())
v = type(viewName, viewBaseCls, viewAttributes) # create a subclass of baseView
v.model = model_class # bind the view to the model
NewViews[f'{action}_view'] = v
return NewViews
You see, the generator function is simplified to only 10 lines of code.
Moreover, the API will become much cleaner:
def validate_author(self, form):
send_email(form)
AuthorViews = generate_views(Author,
create=dict(
success_url='/thanks/',
form_valid_hook=validate_author),
... )
How to use mixins in this API
In the above example, I use a hook/callback function form_valid_hook to inject an email-sending procedure before the form data are saved. This is ugly because the configurables for the email will be in the module scope. It's better to refactor it into a mixin class.
from django.core.mail import send_mail
class FormEmailMixin:
from_email = 'info#example.com'
subject_template = 'We hear you'
message_template = 'Hi {username}, ...'
def form_valid(self, form):
user_info = dict( username = self.request.user.username
to_email = ... )
send_mail(subject_template.format(**user_info),
message_template.format(**user_info)
self.from_email , [user_info['to_email'],] )
return super().form_valid(form)
Then you can use this mixin class in the API call.
AuthorViews = generate_views( Author,
create={ 'message_template': 'Dear Author {username}, ...' },
create_mixins = (FormEmailMixin,) )
Right now with the current Views functions I am getting the data given below:
{"item": "zxmnb",
"category": "zxc",
"price": "zxc",
"restaurant": 1}
Here is my views file:
class RestaurantMenuView(generics.RetrieveAPIView):
lookup_field = 'item'
serializer_class = MenuSerializer
def get_queryset(self):
return Menu.objects.all()
But the issue is I want the data to be in a format as:
{"restaurant": "name"
"item":"some item",
"category": "some category",
"price": "some price"
}
I want to mention that Restaurant is another model in my models class,Now I know that if I use restaurant I will only get the pk. But what I want is the JSON to be displayed like that.
You need to modify your MenuSerializer. Specifically, you need to change the restaurant field to be a CharField and also provide a source attribute. Something like the following:
class MenuSerializer(serializers.ModelSerializer):
restaurant = serializers.CharField(source='restaurant.name')
# ... other stuff in the serializer
Here, I am assuming that your Restaurant model has a name field.
You can read more about Serializer fields here: https://www.django-rest-framework.org/api-guide/fields/
Why don't you redefine the to_representation() function. Something like this:
class MenuSerializer(serializers.ModelSerializer):
def to_representation(self, obj):
restaurants = RestaurantSerializer(instance=obj,
context=self.context).data
data = []
for restaurant in restaurants:
data.append(
{
"restaurant": {
"item": obj.item,
"category": obj.category,
"price": obj.price,
"name": restaurant.name,
}
}
)
return data
Without looking at your models or why you want restaurant in there, I added the for loop in order to show you that you can pretty much access any of the data in your to_representation() and put it in any sort of format you want. I use this when I'm trying to render my JSON objects into XML in a specific way. Hope this helps.
Also check out the documentation:
https://www.django-rest-framework.org/api-guide/serializers/#overriding-serialization-and-deserialization-behavior
Another solution that you could consider is adding a Foreign Key on your Menu model back to the Restaurant, then you could define your serializer like this:
class RestaurantSerializer(serializers.ModelSerializer):
menu = MenuViewSerializer(read_only=True, many=True)
class Meta:
model = Restaurant
fields = [
"id",
"name",
"menu",
]
extra_kwargs = {
"menu": {"read_only": True},
}
I have this order form that allows my users to create an order. An order consists of multiple tuples of (producetype, quantity). Producetype should be rendered in a <select> form while quantity can just be an input. The choices of producetype should be dynamically added because that could change. Currently, I've written this in bare html
I would like to use WTForm for this because WTForm really simplifies my code. However, I am unable to do so:
Code:
class OrderEntryForm(Form):
quantity = IntegerField('Quantity',
[validators.Required(), validators.NumberRange(min=1)])
# we will be dynamically adding choices
producetype = SelectField('Produce',
[validators.Required()],
choices=[])
class OrderForm(Form):
name = TextField('Crop', [validators.Length(min=3, max=60)])
due_date = DateField('Due Date', [validators.required()])
order_entries = FieldList(FormField(OrderEntryForm))
I have the following questions:
How can I dynamically add choices to the order_entries field of the OrderForm?
If I have an order, order = { name:hello, due_date:2014-06-18, order_entries:[ {producetype_id: 1, quantity: 2}, {producetype_id: 3, quantity: 4}] }, how can populate my OrderForm with the right OrderEntryForm values?
How can I dynamically add choices to the order_entries field of the OrderForm
This depends on what you mean
If you mean you want to add lines items to the form after render. You have to use DOM manipulation to add these on the client side. WTForms has a naming convention for addressing indices on form fields. Its just name="<form-field-name>-<index>". If you add a name using javascript that follows this convention WTForms will know how to handle it on the backend.
If you mean you want to have a dynamic choice list then you can just iterate over the FieldList in the form after its instantiated. You can assign choices any iterable of 2-tuples. In the example below I am assigning them directly but they could just as easily have been retrieved from storage.
order_form = OrderForm()
for sub_form in order_form.order_entries:
sub_form.producetype.choices = [('2', 'apples'), ('2', 'oranges')]
how can populate my OrderForm with the right OrderEntryForm values?
You can just bind objects directly to the form using the obj keyword argument. WTForms is smart enough to dynamically build the form from the object's attributes.
from wtforms import Form, IntegerField, SelectField, TextField, FieldList, FormField
from wtforms import validators
from collections import namedtuple
OrderEntry = namedtuple('OrderEntry', ['quantity', 'producetype'])
Order = namedtuple('Order', ['name', 'order_entries'])
class OrderEntryForm(Form):
quantity = IntegerField('Quantity',
[validators.Required(), validators.NumberRange(min=1)])
# we will be dynamically adding choices
producetype = SelectField('Produce',
[validators.Required()],
choices=[
(1, 'carrots'),
(2, 'turnips'),
])
class OrderForm(Form):
name = TextField('Crop', [validators.Length(min=3, max=60)])
order_entries = FieldList(FormField(OrderEntryForm))
# Test Print of just the OrderEntryForm
o_form = OrderEntryForm()
print o_form.producetype()
# Create a test order
order_entry_1 = OrderEntry(4, 1)
order_entry_2 = OrderEntry(2, 2)
order = Order('My First Order', [order_entry_1, order_entry_2])
order_form = OrderForm(obj=order)
print order_form.name
print order_form.order_entries
The above example creates a sample Order and supplies it to the obj keyword. On render this will generate the following(unstyled):
For anyone else stumped by SelectFields + FieldLists, this is how I implemented a FieldList with SelectFields (inspired by nsfyn55's answer). This approach dynamically renders an arbitrary number of SelectFields to the /home route based on JSON data.
The route (routes.py):
#app.route('/home', methods=['POST', 'GET'])
def home():
custom_metadata = data
select_metadata_form_list = SelectFormList()
select_metadata_form_list.select_entries = get_select_entries()
context = {
"select_metadata_form_list": select_metadata_form_list,
}
return render_template('home.html', **context)
The forms (forms.py):
class SelectForm(FlaskForm):
select = SelectField("Placeholder", choices=[])
class SelectFormList(FlaskForm):
select_entries = FieldList(FormField(SelectForm))
The template (home.html):
{% for select_form in select_metadata_form_list.select_entries %}
{{select_form.select.label}}: {{ select_form.select}}
{% endfor %}
Helper methods (app.py):
def get_select_entries():
"""
Converts custom metadata to a forms.SelectForm(), which can then be
used by SelectFormlist() to dynamically render select items.
:return: <forms.SelectForm object>
"""
select_data = get_select_data_from_custom_metadata()
select_data_labeled = get_labled_select_data(select_data=select_data)
all_select_items = []
for select_dict in select_data_labeled:
for k, v in select_dict.items():
select_id = uuid.uuid1() # allows for multiple selects
select_entry = SelectForm()
select_entry.select.label = k
select_entry.id = select_id
select_entry.select.choices = v
all_select_items.append(select_entry)
return all_select_items
def get_select_data_from_custom_metadata():
"""
[
{"Seniority": ["Intern", "Associate", "Senior"]}
]
:return: List of dictionaries containing key and list of select values
"""
type = "select"
select_data = []
custom_metadata = get_custom_metadata()
for field in custom_metadata["fields"]:
if field["type"] == type:
select_data.append({field["key"]: field["data_list"]})
return select_data
The 'data' (custom_metadata.json):
{
"fields": [
{
"type": "text",
"key": "Position"
},
{
"type": "select",
"key": "Seniority",
"data_list": ["Intern", "Associate", "Senior", "Executive"]
},
{
"type": "select",
"key": "Company",
"data_list": ["Ford", "Chevy", "Toyota"]
},
{
"type": "select",
"key": "Team",
"data_list": ["Bucks", "Raptors", "Thunder"]
}
]
}
The result:
Add a SubmitField to your to OrderForm:
submit_something = SubmitField((u'Add something'))
and then call it from your view, and use the append_entry method of FieldList:
if form.submit_something.data:
form.order_entries.append_entry()
return render_template('yourtemplate.html', form=form)
Hope that helps !