I've been developing a web app on top of Django and I use the Django Rest Framework for my API. There's a model class named Events and my EventsSerializer in DRF is a pretty common serializer without any special configuration. It just dumps data returned by the EventManager.
There is a field "type" in the Event model class.
My json returned now is:
{
events: [
{object1},
{object2},
.....
]
}
, as anything dumped in a DRF api and returned by django.
For some reason, I need my events objects returned categorized by the "type" field. For example, I need to get this:
{
events: [
type1: [{object1}, {object2},...],
type2: [{object3}, {object4}, ...],
.......
]
}
I have literally searched anything related to that but couldn't find a proper solution. Do you have anything to suggest about that?
Thanks in advance
You can use SerializerMethodField and provide custom serialization logic there:
class EventSerializer(serializers.Serializer):
events = serializers.SerializerMethodField(source="get_events")
def get_events(self, events):
event_list = {}
return [event_list[e.type].add({e}) if event.type in event_list else event_list[event.type] = [] for event in events]
I had a model similar to the following:
class Book(models.Model):
title = models.CharField(max_length=200)
class Author(models.Model):
name = models.CharField(max_length=200)
books = models.ManyToManyField(Book)
The JSON that was being generated for an Author looks like this:
{
"name": "Sir Arthur C. Clarke",
"books": [
{
"title": "Rendezvous with Rama",
},
{
"title": "Childhood's End",
}
]
}
In the JSON wanted the books to be sorted by title. Since the books are pulled into the queryset via a prefetch_related adding an order_by to the View's queryset had no effect (generated SQL didn't have a join to the Books table). The solution I came up with was to override the get method. In my version of the get method, I have the super class generate the Response and I modify it's data (a Python dict) before returning it as shown below.
I'm not too worried about performance for two reasons:
because of the prefetch_related the join is already being done in Python rather than in the database
In my case the number of Books per Author is relatively small
class AuthorView(RetrieveUpdateAPIView):
queryset = Author.objects.prefetch_related(
'books',
)
serializer_class = AuthorSerializer
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
def key_func(book_json):
return book_json.get('title', '')
books = response.data.get('books', [])
books = sorted(books, key=key_func)
response.data['books'] = books
return response
Related
I have a django-rest-framework model viewset (for tests) that is using a serialize like this:
class ProcessSerializer(serializers.Serializer):
class Meta:
model = Process.draft_model
fields = ['id', 'name']
class TestSerializer(serializers.ModelSerializer):
process = ProcessSerializer()
class Meta:
model = ConfigurationTest
fields = [
'id',
'name',
'process',
]
This works great when retrieving tests, but doesn't work for creation / updates, where I would ideally like to just provide the ID with a request like this:
{
process: 1
name: 'A new test'
}
When sending that request to the server I get an error like Invalid data. Expected a dictionary, but got int
What I tried:
Have process and process_id as included fields and just sending process_id in a POST request. In that case I get an error like process_id cannot be null, which is plain confusing.
Use a different serializer for the create action, that uses process as a plain PrimaryKeyRelatedField without a nested serializer. This works nicely for getting the request, but also obviously means the server reponse to that POST request doesn't include the nicely nested serializer.
Models for reference
class ConfigurationTest(...):
name = CharField(max_length=120)
process = ForeignKey(Process)
class Process(...):
name = CharField(max_length=120)
I would give a serializer like this. One serializer field for read_only where it uses ProcessSerializer and process_id for write_only as integer.
class TestSerializer(serializers.ModelSerializer):
process = ProcessSerializer(read_only=True)
process_id = IntegerField(write_only=True)
class Meta:
model = ConfigurationTest
fields = [
'id',
'name',
'process',
'process_id',
]
And POST this:
{
process_id: 1
name: 'A new test'
}
I am not 100% sure you don't need to override create/update but this should work fine.
N.B.:
I see that you tried something with similar logic. Was it the same code though ?
Given the general structure:
class Article(Page):
body = RichTextField(...)
search_fields = Page.search_fields + [index.SearchField('body')]
class ArticleFilter(FilterSet):
search = SearchFilter()
class Meta:
model = Article
fields = ['slug']
class Query(ObjectType):
articles = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleFilter)
I thought to create a "SearchFilter" to expose the wagtail search functionality, since I ultimately want to perform full text search via graphql like so:
query {
articles (search: "some text in a page") {
edges {
nodes {
slug
}
}
}
}
"search" is not a field on the Django model which is why I created a custom field in the Django FilterSet. My thought was to do something like:
class SearchFilter(CharFilter):
def filter(self, qs, value):
search_results = [r.pk for r in qs.search(value)]
return self.get_method(qs)(pk__in=search_results)
however, I'm curious if there's a better pattern that's more efficient. At the bare minimum I'd want to ensure the SearchFilter is added last (so the query searched is filtered first).
Should the "search" be moved outside of the FilterSet and into the Query/Node/custom connection, and if so, how can I add an additional field to "articles" to see it as the final step in resolving articles (i.e. tack it on to the end of the filter queryset)? If this does belong in a separate Connection, is it possible to combine that connection with the django filter connection?
I would think this pattern of accessing Wagtail search via graphene already exists, however I've had no luck on finding this in the documentation.
I have the following serializer that retrieves pages in a set of books:
class PagesDataSerializer(Serializer):
books = Book.objects.all()
data = PagesSerializer(many=True,data=books)
I would like to introduce a filter (query parameter type received in the get request)
In order to access self.request content I have to add a custom function
I tried something like this:
class PagesDataSerializer(Serializer):
books = Book.objects.all()
data = PagesSerializer(source='get_pages_set', many=True)
def get_pages_set(self, obj):
selected_books = Book.objects.filter(type=self.request.type)
return PagesSerializer(many=True,data=selected_books).data
But this doesn't work for some reason (I get empty results).... could anyone enlighten me please ?
Thank you very much
We are creating an API that needs to allow a user to update a record. In many cases, like a status update or name change, only one field will change. This seems an appropriate use-case scenario for a PATCH request. As I understand it this is a 'partial' update.
We've implemented Django's REST Framework and run into this issue. For a record such as a "AccountUser" I want to change a name field only so I send the following request:
PATCH /api/users/1/ HTTP/1.1
Host: localhost
X-CSRFToken: 111122223333444455556666
Content-Type: application/json;charset=UTF-8
Cache-Control: no-cache
{ "fullname": "John Doe" }
The record obviously has other attributes including a couple of 'related' fields such as 'account' which is required for a new record. When submit the request, the response is a 400 error with the following body:
{ "account": [ "This field is required." ] }
The serializer for the user looks like this:
class AccountUserSerializer(serializers.ModelSerializer):
account = serializers.PrimaryKeyRelatedField()
class Meta:
model = AccountUser
fields = ('id', 'account', 'fullname', ... )
depth = 1
And the model looks like this:
class AccountUser(models.Model):
''' Account User'''
fullname = models.CharField(max_length=200,
null=True,blank=True)
account = models.ForeignKey(Account,
on_delete=models.PROTECT
)
objects = AccountUserManager()
def __unicode__(self):
return self.email
class Meta:
db_table = 'accounts_account_user'
Am I doing something wrong here or is it wrong to expect to be able to update a single field on a record this way. Thanks! This community rocks!
EDIT:
Requested - AccountUserManager:
class AccountUserManager(BaseUserManager):
def create_user(self, email, account_name):
username = hash_email_into_username(email)
...
account = Account.objects.get(name=account_name)
account_user = AccountUser(email=email,user=user,account=account)
account_user.save()
return account_user
It doesn't look like your manager is filtering the user. I'd encourage you to use pdb and set a breakpoint in your view code and step through to see why its attempting to create a new record. I can vouch that we use PATCH to complete partial updates all the time and only send a few fields to update without issue.
Only other thought is that you're some how sending a value for account (like null) that's triggering the validation error even though you're listed example only shows sending the fullname field.
See my answer about partial updates. Also you can see the drf docs and this one docs
In my case, all I had to do was add required=False to my serializer field's arguments.
A novice mistake to be sure, but I thought I'd mention it here in case it helps anyone.
I came here because I got into a similar problem. CREATE is fine but PATCH isn't.
Since OP doesn't post the ViewSets and I suspend if he is using a custom ViewSets, which I got a mistake when override the update function:
class MyViewSet(ModelViewSet):
def update(self, request, *args, **kwargs):
// Do some checking
return super().update(request, args, kwargs)
My mistake is in the call to super(). Missing the asterisks. This fix my problem
return super().update(request, *args, **kwargs)
you can simply add this line after the fields variable in serializers.py
class AccountUserSerializer(serializers.ModelSerializer):
account = serializers.PrimaryKeyRelatedField()
class Meta:
model = AccountUser
fields = ('id', 'account', 'fullname', ... )
depth = 1
extra_kwargs = {
'account' : {'required' : False}
}
How do you load a Django fixture so that models referenced via natural keys don't conflict with pre-existing records?
I'm trying to load such a fixture, but I'm getting IntegrityErrors from my MySQL backend, complaining about Django trying to insert duplicate records, which doesn't make any sense.
As I understand Django's natural key feature, in order to fully support dumpdata and loaddata usage, you need to define a natural_key method in the model, and a get_by_natural_key method in the model's manager.
So, for example, I have two models:
class PersonManager(models.Manager):
def get_by_natural_key(self, name):
return self.get(name=name)
class Person(models.Model):
objects = PersonManager()
name = models.CharField(max_length=255, unique=True)
def natural_key(self):
return (self.name,)
class BookManager(models.Manager):
def get_by_natural_key(self, title, *person_key):
person = Person.objects.get_by_natural_key(*person_key)
return self.get(title=title, person=person)
class Book(models.Model):
objects = BookManager()
author = models.ForeignKey(Person)
title = models.CharField(max_length=255)
def natural_key(self):
return (self.title,) + self.author.natural_key()
natural_key.dependencies = ['myapp.Person']
My test database already contains a sample Person and Book record, which I used to generate the fixture:
[
{
"pk": null,
"model": "myapp.person",
"fields": {
"name": "bob"
}
},
{
"pk": null,
"model": "myapp.book",
"fields": {
"author": [
"bob"
],
"title": "bob's book",
}
}
]
I want to be able to take this fixture and load it into any instance of my database to recreate the records, regardless of whether or not they already exist in the database.
However, when I run python manage.py loaddata myfixture.json I get the error:
IntegrityError: (1062, "Duplicate entry '1-1' for key 'myapp_person_name_uniq'")
Why is Django attempting to re-create the Person record instead of reusing the one that's already there?
Turns out the solution requires a very minor patch to Django's loaddata command. Since it's unlikely the Django devs would accept such a patch, I've forked it in my package of various Django admin related enhancements.
The key code change (lines 189-201 of loaddatanaturally.py) simply involves calling get_natural_key() to find any existing pk inside the loop that iterates over the deserialized objects.
Actually loaddata is not supposed to work with existing data in database, it is normally used for initial load of models.
Look at this question for another way of doing it: Import data into Django model with existing data?