django-crispy-forms: Change name of input - django-crispy-forms

I am running into some minor problems with django-crispy-form.
I currently have a form, which successfully displays all fields from my model.
However, I would like to change the html-attribute name to something else. I realize this might be difficult, since django-crispy-forms probably expects the name to be the same as the field in the model.
I managed to change the id, but the name will not update. Any chance that there is a fix for this, other than changing the name of my columns in my database?
Example code
from django import forms
from crispy_forms.helper import FormHelper
class CompanyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(CompanyForm, self).__init__(*args, **kwargs)
self.fields['name'].widget.attrs['id'] = 'company_n' # works
self.fields['name'].widget.attrs['name'] = 'company_n' # does not work
self.helper = FormHelper()

Changing the column name in the model is the way to change the name attribute of fields of a ModelForm form.
In my experience, that shouldn't be a problem for sanely designed models.
A good guide to efficient data modeling in django is:
https://simpleisbetterthancomplex.com/tips/2018/02/10/django-tip-22-designing-better-models.html

This is the top google search result, so here is a method that works for me. Note that this is actually not a crispy forms question, it applies to all Django forms.
Of course there are times when you need to change the label on a form - one example would be a different label for an admin user or a regular user.
Just set the label property on self.fields. Here's a complete example:
from crispy_forms.bootstrap import Div, Field, PrependedAppendedText
from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit
class MyModelEditForm(forms.ModelForm):
"""Make changes to my model"""
class Meta:
model = MyModel
fields = ['my_field', ]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# this is the magic right here:
self.fields['my_field'].label = "Foo"
# this should answer your next question, which is how do I change the text under the field
self.fields['my_field'].help_text = "Bar"
self.helper = FormHelper
self.helper.form_method = 'post'
# I have found that it's important to be explicit about the form layout, or you'll get all kinds of CSS issues
self.helper.form_id = 'my-model-form'
self.helper.form_class = 'form'
self.helper.form_tag = True
self.helper.layout = Layout(
Div(
PrependedAppendedText('my_field', '$', '.00',wrapper_class='col-md-4', ),
),
Submit('submit', 'Save', css_class='btn-success'),
)

Related

How to pass custom error message in django_filters fields?

I am using django_filter package for custom filtering in my django rest framework API,
Below is the given code
import django_filters
from src.core.models.rough_management import DocumentDetails
class DocumentDetailsFilter(django_filters.FilterSet):
my_date = django_filters.DateFromToRangeFilter()
class Meta:
model = DocumentDetails
fields = ['my_date']
Here i am getting "Enter a valid date" as an exception message when i enter invalid date range so my question is how to pass custom exception message to "No records found"?
Django filters use native django form fields to validate data.
DateFromToRangeFilter use django.forms.fields.DateField.
(Django DateField code)
You need to implement your own custom filter with little changes.
Not that from my perspective it's bad practice because that code interferes DateField error which should return default error as it is "Enter a valid date" and it's good information from api if you enter invalid date value like text "abcd".
Anyway for now i don't see better place to implement that and that is simpler than interference in FilterSet class.
You can also look on original code here to see how it's designed.
class CustomDateField(django.forms.fields.DateField):
default_error_messages = {
"invalid": "No records found",
}
class CustomDateRangeField(RangeField):
widget = DateRangeWidget
def __init__(self, *args, **kwargs):
fields = (CustomDateField(), CustomDateField()) # Important change here only
super().__init__(fields, *args, **kwargs)
def compress(self, data_list):
# Compress method stay the same as it is in original code
if data_list:
start_date, stop_date = data_list
if start_date:
start_date = handle_timezone(
datetime.combine(start_date, time.min), False
)
if stop_date:
stop_date = handle_timezone(
datetime.combine(stop_date, time.max), False
)
return slice(start_date, stop_date)
return None
class CustomDateFromToRangeFilter(RangeFilter):
field_class = CustomDateRangeField

django rest framework: SlugRelatedField options- limit by user

Note: I'm new to django rest framework. First project with it. My model has a foreign key which in the serializer I link by a SlugRelatedField. This related model is a small list of options that can be selected however what is available for selection depends on the user (or more specifically the user group).
I have found this question that says how to get the user into a serilaizer. But this doesn't seem to help as the field definition is static right?
Removing the irrelevant parts I have:
class MyModelSerializer(serializers.ModelSerializer):
sequence = serializers.SlugRelatedField(
many=False,
read_only=False,
slug_field='prefix',
queryset=Sequence.objects.filter(active=True,
sequence_groups__sequence_group_id__in=SequenceGroup.objects.filter(users=serializers.CurrentUserDefault()))
)
This query works as I also use it in a normal form. When I start the dev. server I get an exception:
TypeError: int() argument must be a string, a bytes-like object or a number, not 'CurrentUserDefault'
So my question is how can I get the current user into the queryset of the SlugRelatedField?
It's funny how after hours of trying, writing down the question leads one to a working solution.
Add request to sterilizer context in ModelViewSet
Simply add below method to the ModelViewSet:
def get_serializer_context(self):
return {"request": self.request}
Adjust the queryset of the SlugRelatedField in the constructor of the Serializer
def __init__(self, *args, **kwargs):
super(MyModelSerializer, self).__init__(*args, **kwargs)
# superuser can choose from all sequences, normal users can only choose from
# active sequences he is assigned to
request = self.context.get("request")
if request and hasattr(request, "user"):
sequence = self.fields['sequence']
if request.user.is_superuser:
sequence.queryset = Sequence.objects.all()
else:
sequence.queryset = Sequence.objects.filter(active=True,
sequence_groups__sequence_group_id__in=SequenceGroup.objects.filter(users=request.user))
In my case the admin should be able to select any of the available options hence the extra condition.

Django REST Framework (DRF): ModelSerializer does not validate models on serialization

I want to ask how to use Django REST Framework (DRF) ModelSerializers correctly for serializing from model.
I have Django model with two required fields:
class Book(models.Model):
title = models.CharField()
desc = models.CharField()
I have DRF ModelSerializer:
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ['title', 'desc']
I can deseralize and validate incoming request using:
serializer = BookSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
But how to serialize and send response? DRF allows me to break contact built using ModelSerializer. If I forgot to set one of mandatory Book fields, it will still still pass through BookSerializer!
invalid_book = Book(title="Foo") # but forgotten to set "desc"
serializer = BookSerializer(instance=invalid_book)
serializer.data # it contains book without required "desc"
Serialized created using instance parameter throws error if I try is_validate().
Why ModelSerializer can validate incoming data, but cannot outgoing?
Validation is only performed when deserializing. As per the documentation:
Serializers also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data.
That makes sense (edit: in the way Django Rest Framework seems to be construed). Because it isn't the 'role' of the Serializer to make sure that your complex data such as querysets and model instances (eg. your Book instance) that you are going to serialize is construed 'legitimately', thus they also don't validate while serializing.
So if you would save the instance like invalid_book.save(), Django would throw an error because of the missing field.
Edit
After a comment about being 'a point of view' and thus being opiniated I want to stress and make clear that this seems to be the way that Django Rest Framework (DRF) is construed. After digging deeper on SO I link this answer in support.
Also if you read the documentation of DRF, it is somewhat implied that serialization and validation are two separate concepts.
Furthermore, analyzing serializers.py makes clear that validation is only run when calling is_valid() and the validation is only run on the provided data flag. In fact, it can't even be run when only an instance is provided:
def __init__(self, instance=None, data=empty, **kwargs):
self.instance = instance
if data is not empty:
self.initial_data = data
self.partial = kwargs.pop('partial', False)
self._context = kwargs.pop('context', {})
kwargs.pop('many', None)
super().__init__(**kwargs)
...
def is_valid(self, raise_exception=False):
assert hasattr(self, 'initial_data'), (
'Cannot call `.is_valid()` as no `data=` keyword argument was '
'passed when instantiating the serializer instance.'
)
if not hasattr(self, '_validated_data'):
try:
self._validated_data = self.run_validation(self.initial_data)
except ValidationError as exc:
self._validated_data = {}
self._errors = exc.detail
else:
self._errors = {}
if self._errors and raise_exception:
raise ValidationError(self.errors)
return not bool(self._errors)
You are under a very wrong assumption. A serializer (not de-serializer) does one thing. Convert an Object to JSON. Here, you are creating an object Book(name='sad book'). This is just a regular Python Object. Django Serializers will attempt to serialize any Object that is passed to it.
What you might be wondering is the field is required in Model but why doesn't the serializer validate? Because of the way DRF handles serialization. I will show some excerpts from DRF Source code.
This is how the data property is calulated.
class BaseSerializer():
...
...
#property
def data(self):
if hasattr(self, 'initial_data') and not hasattr(self, '_validated_data'):
msg = (
'When a serializer is passed a `data` keyword argument you '
'must call `.is_valid()` before attempting to access the '
'serialized `.data` representation.\n'
'You should either call `.is_valid()` first, '
'or access `.initial_data` instead.'
)
raise AssertionError(msg)
if not hasattr(self, '_data'):
if self.instance is not None and not getattr(self, '_errors', None):
# THIS IS WHERE WE GO. THE to_representation() CAN BE FOUND IN THE IMPLEMENTATION
# OF ModelSerializer() which inherits from this class BaseSerializer
self._data = self.to_representation(self.instance)
elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None):
self._data = self.to_representation(self.validated_data)
else:
self._data = self.get_initial()
return self._data
What happens in ModelSerializer.to_representation() ?
class ModelSerializer(BaseSerializer):
...
...
def to_representation(self, instance):
"""
Object instance -> Dict of primitive datatypes.
"""
ret = OrderedDict()
fields = self._readable_fields
for field in fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
# We skip `to_representation` for `None` values so that fields do
# not have to explicitly deal with that case.
#
# For related fields with `use_pk_only_optimization` we need to
# resolve the pk value.
check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
if check_for_none is None:
ret[field.field_name] = None
else:
ret[field.field_name] = field.to_representation(attribute)
return ret
As you can see, in this case the serializer only maps the fields from the Object being passed. So, there is no validation during serialization. For more info, check the source code of DRF. It's pretty easy if you use Pycharm Pro.

Dealing with unique constraints that should be replaced in django_rest_framework

tl;dr: How can I ignore (turn off) a unique constraint in django_rest_framework Create calls with a ListCreateAPIView, because I'm going to deal with it manually in the perform_create method?
Im using a third party library django-push-notifications. It has a nice model for APNSDevice (apple push notification service device) that has a unique constraint on a registration_id field.
My problem is that sometimes I want to manually delete old values in the table that have the registration ID, so that I can insert a new value. I'd like to use this serializer:
class APNSDeviceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = APNSDevice
fields = ('name', 'active', 'device_id', 'registration_id')
along with this code for PUT
class MyAppleDevices(generics.ListCreateAPIView):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = APNSDeviceSerializer
model = APNSDevice
def get_queryset(self):
return APNSDevice.objects.filter(user = self.request.user)
def perform_create(self, serializer):
print "Looking for old devices with registration id "+str(self.request.registration_id)
oldDevices = APNSDevice.objects.filter(registration_id = self.request.registration_id)
for oldDevice in oldDevices:
oldDevice.delete()
apnsDevice = serializer.save(user=self.request.user)
In other words, I'm trying to manually delete other entries that have the unique constraint in this particular PUT, so that I can insert the new one without violating the unique constraint. The problem is the validator runs before the perform_create method is called, and I can't figure out how to turn off the validator's unique constraint. I tried adding this to the Serializer
def get_validation_exclusions(self, instance = None):
exclusions = super(APNSDeviceSerializer, self).get_validation_exclusions(instance)
return exclusions + ['registration_id']
but it doesn't help so obviously I have no clue even though I've been pouring through the documentation and Stack Overflow posts. Any help appreciated, thanks. I suppose as a last resort I could remove the unique constraint from the model, but it is a valid constraint so I'd rather leave it in.
I found this question because I had this exact problem with that exact library. You can get around it by subclassing the serializer and manually overriding the field definition:
class APNSDeviceSerializerWithNonUniqueRegistrationId(APNSDeviceSerializer):
registration_id = serializers.CharField(min_length=64, max_length=64)
class Meta(APNSDeviceSerializer.Meta):
fields = ("name", "registration_id", "device_id", "active", "date_created")
Then, if you're using django-push-notifications, you'll also need to override the ViewSet that uses that serializer:
class APNSDeviceAuthorizedViewSetWithNonUniqueRegistrationId(AuthorizedMixin, APNSDeviceViewSet):
"""
The out of the box viewset/serializer combo require the registration ID to be unique and won't
allow setting a registration ID to a new user (which is useful if we have potentially more than
one account on a device.)
"""
serializer_class = APNSDeviceSerializerWithNonUniqueRegistrationId
def perform_create(self, serializer):
if self.request.user.is_authenticated():
try:
existing_registration = APNSDevice.objects.get(
registration_id=serializer.validated_data['registration_id'])
existing_registration.delete()
except APNSDevice.DoesNotExist:
pass
serializer.save(user=self.request.user)
return super(DeviceViewSetMixin, self).perform_create(serializer)

Exposing "virtual" field in a tastypie view?

I want to create a view using tastypie to expose certain objects of the same type, but with the following two three twists:
I need to get the objects using three separate queries;
I need to add a field which doesn't exist in the underlying model, and the value of that field depends on which of the queries it came from; and
The data will be per-user (so I need to hook in to one of the methods that gets a request).
I'm not clear on how to hook into the tastypie lifecycle to accomplish this. The recommended way for adding a "virtual" field is in the dehydrate method, which only knows about the bundle it's operating on.
Even worse, there's no official way to join querysets.
My problem would go away if I could get tastypie to accept something other than a queryset. In that case I could pass it a list of subclasses of my object, with the additional field added.
I'm open to any other sensible solution.
Edit: Added twist 3 - per-user data.
In the last version you should override the dehydrate method, e.g.
def dehydrate(self, bundle):
bundle.data['full_name'] = bundle.obj.get_full_name()
return bundle
Stumbled over similar problem here. In my case, items in the list could be "checked" by user.
When an item is retrieved by AJAX, its checked status is returned with the resource as a normal field.
When an item is saved to the server, "checked" field from the resource is stored in user's session.
First I thought hydrate() and dehydrate() methods to be the best match for this job, but turned out there are problems with accessing request object in these. So I went with alter_data_to_serialize() and obj_update(). I think there's no need to override obj_create(), since item can't be checked when it's first created, I think.
Here is the code, but note that it hasn't been properly tested yet.
class ItemResource(ModelResource):
def get_object_checked_status(self, obj, request):
if hasattr(request, 'session'):
session = request.session
session_data = session.get(get_item_session_key(obj), dict())
return session_data.get('checked', False)
return False
def save_object_checked_status(self, obj, data, request):
if hasattr(request, 'session'):
session_key = get_item_session_key(obj)
session_data = request.session.get(session_key, dict())
session_data['checked'] = data.pop('checked', False)
request.session[session_key] = session_data
# Overridden methods
def alter_detail_data_to_serialize(self, request, bundle):
# object > resource
bundle.data['checked'] = \
self.get_object_checked_status(bundle.obj, request)
return bundle
def alter_list_data_to_serialize(self, request, to_be_serialized):
# objects > resource
for bundle in to_be_serialized['objects']:
bundle.data['checked'] = \
self.get_object_checked_status(bundle.obj, request)
return to_be_serialized
def obj_update(self, bundle, request=None, **kwargs):
# resource > object
save_object_checked_status(bundle.obj, bundle.data, request)
return super(ItemResource, self)\
.obj_update(bundle, request, **kwargs)
def get_item_session_key(obj): return 'item-%s' % obj.id
OK, so this is my solution. Code is below.
Points to note:
The work is basically all done in obj_get_list. That's where I run my queries, having access to the request.
I can return a list from obj_get_list.
I would probably have to override all of the other obj_* methods corresponding to the other operations (like obj_get, obj_create, etc) if I wanted them to be available.
Because I don't have a queryset in Meta, I need to provide an object_class to tell tastypie's introspection what fields to offer.
To expose my "virtual" attribute (which I create in obj_get_list), I need to add a field declaration for it.
I've commented out the filters and authorisation limits because I don't need them right now. I'd need to implement them myself if I needed them.
Code:
from tastypie.resources import ModelResource
from tastypie import fields
from models import *
import logging
logger = logging.getLogger(__name__)
class CompanyResource(ModelResource):
role = fields.CharField(attribute='role')
class Meta:
allowed_methods = ['get']
resource_name = 'companies'
object_class = CompanyUK
# should probably have some sort of authentication here quite soon
#filters does nothing. If it matters, hook them up
def obj_get_list(self, request=None, **kwargs):
# filters = {}
# if hasattr(request, 'GET'):
# # Grab a mutable copy.
# filters = request.GET.copy()
# # Update with the provided kwargs.
# filters.update(kwargs)
# applicable_filters = self.build_filters(filters=filters)
try:
#base_object_list = self.get_object_list(request).filter(**applicable_filters)
def add_role(role):
def add_role_company(link):
company = link.company
company.role = role
return company
return add_role_company
director_of = map(add_role('director'), DirectorsIndividual.objects.filter(individual__user=request.user))
member_of = map(add_role('member'), MembersIndividual.objects.filter(individual__user=request.user))
manager_of = map(add_role('manager'), CompanyManager.objects.filter(user=request.user))
base_object_list = director_of + member_of + manager_of
return base_object_list #self.apply_authorization_limits(request, base_object_list)
except ValueError, e:
raise BadRequest("Invalid resource lookup data provided (mismatched type).")
You can do something like this (not tested):
def alter_list_data_to_serialize(self, request, data):
for index, row in enumerate(data['objects']):
foo = Foo.objects.filter(baz=row.data['foo']).values()
bar = Bar.objects.all().values()
data['objects'][index].data['virtual_field'] = bar
return data

Resources