How to add a static User field to django query - python

I need to perform some special serialization / deserialization with DRF, but one of the required parameters for those is the user that generated the query.
AFAIK a serializers.Field instance does not have access to ViewSet and thus no access to ViewSet.request.user.
So I thought I'd just add the user as a static field into the queryset so that each record would have access to it.
qry = qry.annotate(user=Value(user, models.ForeignKey(settings.AUTH_USER_MODEL)))
However, this gives me
ValueError: Related model 'auth.User' cannot be resolved
I also tried
q.annotate(user=Value(user, models.ForeignKey(user.__class__)))
but that also excepts.
Exactly what do I have to include so that this will resolve as needed?

Have a look at the CurrentUserDefault:
A default class that can be used to represent the current user. In order to use this, the 'request' must have been provided as part of the context dictionary when instantiating the serializer.
owner = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
To use this, you need to pass the request in the context as following:
serializer = AccountSerializer(account, context={'request': request})

Related

Add a virtual field to a django query

I want to add an extra field to a query set in Django.
The field does not exist in the model but I want to add it to the query set.
Basically I want to add an extra field called "upcoming" which should return "True"
I already tried adding a #property method to my model class. This does not work because apparently django queries access the DB directly.
models.py
class upcomingActivity(models.Model):
title = models.CharField (max_length=150)
address = models.CharField (max_length=150)
Views.py
def get(self, request):
query = upcomingActivity.objects.all()
feature_collection = serialize('geojson', query ,
geometry_field='location',
fields= ( 'upcoming','title','address','pk' )
)
This answer is for the case that you do not want to add a virtual property to the model (the model remains as is).
To add an additional field to the queryset result objects:
from django.db.models import BooleanField, Value
upcomingActivity.objects.annotate(upcoming=Value(True, output_field=BooleanField())).all()
Each object in the resulting queryset will have the attribute upcoming with the boolean value True.
(Performance should be nice because this is easy work for the DB, and Django/Python does not need to do much additional work.)
EDIT after comment by Juan Carlos:
The Django serializer is for serializing model objects and thus will not serialize any non-model fields by default (because basically, the serializer in this case is for loading/dumping DB data).
See https://docs.djangoproject.com/en/2.2/topics/serialization/
Django’s serialization framework provides a mechanism for “translating” Django models into other formats.
See also: Django - How can you include annotated results in a serialized QuerySet?
From my own experience:
In most cases, we are using json.dumps for serialization in views and this works like a charm. You can prepare the data very flexibly for whatever needs arize, either by annotations or by processing the data (list comprehension etc.) before dumping it via json.
Another possibility in your situation could be to customize the serializer or the input to the serializer after fetching the data from the DB.
You can use a class function to return the upcoming value like this:
def upcoming(self):
is_upcoming = # some logic query or just basically set it to true.
return is_upcoming
then call it normally in your serializer the way you did it.
fields= ( 'upcoming','title','address','pk' )

Django. Check `unique_together` only if both Model fields were provided for create and update calls

I need to check unique_together only in case both fields were supplied to admin creation Form or via API request for both create and update calls. In case this happens and fields are not unique_together I need to propagate Exception to django-admin creation Form and RestFramework's Serializer/ViewSet.
Here's a simplified example of my model:
class MyModel(models.Model):
time = models.TimeField(null=True, blank=True)
date = models.DateField(null=True, blank=True)
some_other_field = models.BooleanField(default=False)
...
As you can see at the level of model both time and date are not required and are nullable.
Application logic on the other hand requires at least one of those fields to be provided and if both are supplied – this pair should be unique for entire table to avoid duplication of datetime composition.
This model is accessed through these nodes: django-admin and DjangoRestFramework's endpoint using ViewSet.
Creation of new record does not seem to be a problem from API perspective, I can override validate method of Serializer:
def validate(self, data):
if not data.get('time') and not data.get('date'):
raise serializers.ValidationError(
"At least one of this fields is required: time, date"
)
if OpenTable.objects.filter(
time=data.get('time'),
date=data.get('date')).exists():
raise serializers.ValidationError('Duplicates for date or datetime not allowed')
return data
But this becomes a problem when I receive PUT or PATCH request, because then despite this record being the only one in table I raise ValidationError precisely because there is a record with this date-time pair.
Overriding validate method also does not solve problem of preventing creation of such a pair from Django-Admin.
At the moment the closest I got to validating this is using save method of MyModel, but I can't understand how exactly to handle this case there to comply with create/update flow.
For Django Rest Framework validation, you can use self.instance check to use different validation flows for creation and modification of an object;
def validate(self, data):
if self.instance:
# Modifiying an existing instance, run validations accordingly
else:
# Creading a new instance, run validations accordingly
As for admin site, you can use Django's ModelForm structure and their validations to enforce validtion for admin site. You'll need to set you admin site to use your custom form for MyModel

Django admin change list view disable sorting for some fields

Is there are way(s) to disable the sorting function for some fields in django admin change list so for those fields, users cannot click the column header to sort the list.
I tried on the following method, but it doesn't work.
https://djangosnippets.org/snippets/2580/
I also tired to override the changelist_view in ModelAdmin but also nothing happen.
def changelist_view(self, request, extra_context=None):
self.ordering_fields = ['id']
return super(MyModelAdmin, self).changelist_view(request, extra_context)
In the above case, I would like to only allow user to sort the list by ID.
Anyone has suggestion? Thanks.
For Django 1.7 (or the version that I last use) do not support such things. One possible dirty work-around could be defining a model class method and using that method instead of model field.
class TestClass(Model):
some_field = (.....)
other_field = (........)
def show_other_field(self):
return self.other_field
class TestClassAdmin(ModelAdmin):
list_display = ("some_field", "show_other_field")
Since show_other_field is a model class method, django do not knows how to sort (or process) the return result of that method.
But as I said, this is a dirty hack that might require more processing (and maybe more database calls) according to use-case than displaying a field of a model.
Extra: If you want to make a model method sortable, you must pass admin_order_field value like:
def show_other_field(self):
return self.other_field
show_other_field.admin_order_field = "other_field"
That will make your model method sortable in admin list_display. But you have to pass a field or relation that is usable in the order_by method of database api.
TestClass.objects.filter(....).order_by(<admin_order_field>)

Override serializer.data in Django REST Framework

I've been trying to alter the value of a form field the Django REST Framework's admin panel and for some reason the change never takes place. I have the serializer below
class SomeView(ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
# I Want to override this and change the POST data
def perform_create(self, serializer):
user = self.request.user.id
# this was a form field where I manually entered the user ID
# I now want it to default to the logged in user
serializer.data['user'] = user
# This returns the original user that was entered into the form field
print serializer.data
I checked out serializer.data with dir() and it's just a python dictionary so I can't figure out why I can't modify the value. As a test, I tried to add extra values but that doesn't work either
# this doesnt work
serializer.data['some_new_field'] = 'test'
EDIT
On another note, I can copy the data and edit it
fake_data = serializer.data.copy()
fake_data['old_value'] = 'new value'
However, it always fails to validate
serializer = MyModelSerializer(data=fake_data)
serializer.is_valid() # returns false
EDIT EDIT:
Ok, so the validation error was caused by Django returning a SimpleLazyObject. Everything works now when I perform a copy of the data, but I'm really curious as to why I can't edit serializer.data directly without copying it. The problem is solved now, but if anyone can provide insight on the issue just for curiosity, that would be awesome.
I checked out serializer.data with dir() and it's just a python dictionary so I can't figure out why I can't modify the value.
While the value returned from Serializer.data is indeed a dictionary, Serializer.data is not a simple instance variable.
If you look at rest_framework/serializers.py:
class Serializer(BaseSerializer, metaclass=SerializerMetaclass):
# [...]
#property
def data(self):
ret = super().data
return ReturnDict(ret, serializer=self)
ReturnDict inherits from OrderedDict, but you still get a new dictionary every time you access Serializer.data.
The real data is in _data, however as noted by the underscore you might not want to modify that either as it is not intended to be public. The values are filled by Serializer.to_representation() which you could override on the viewset.
As for the second part: ModelViewSet defines get_serializer() that is called with the request POST data to create the serializer you want to modify. I'd suggest try to change the input data before the serializer is created, instead.

Can django-tastypie display a different set of fields in the list and detail views of a single resource?

I would like for a particular django-tastypie model resource to have only a subset of fields when listing objects, and all fields when showing a detail. Is this possible?
You can also now use the use_in attribute on a field to specify the relevant resource to show the field in. This can either be list or detail, or a callback.
You would have to specify all fields in the actual ModelResource then override the get_list method to filter out only the fields you want to show. See the internal implementation of get_list on Resource to see how to override it.
However, note this will only apply on GET requests, you should still be able to POST/PUT/PATCH on the resource with all fields if you authorization limits allow you to do so.
In a nut shell, you want to hot patch the internal field list before full_dehydrate is called on all ORM objects returned by obj_get_list.
Alternatively, you can let the full dehydrate mechanism take place and just at the end of it remove the fields you don't want to show if you don't care about squeezing out as much as speed as possible. Of course you would need to do this only if the URL is invoked as a consequence of get_list call. There is a convenience method for this alter_list_data_to_serialize(request, to_be_serialized).
Just do:
class SomeResource(Resource):
class Meta(...):
...
field_list_to_remove = [ 'field1', 'field2' ]
...
def alter_list_data_to_serialize(request, to_be_serialized):
for obj in to_be_serialized['objects']:
for field_name in self._meta.field_list_to_remove:
del obj.data[field_name]
return to_be_serialized
There is an open issue for this on GitHub, with a number of workarounds suggested there.
Can also use the dehydrate(self, bundle) method.
def dehydrate(self, bundle):
del bundle.data['attr-to-del]
return bundle

Categories