Why does this OneToOneField reverse relationship not persist to the database? - django

Relevant frameworks: Django, Django REST Framework
On POST everything seems fine: a 201 Created response is returned and the JSON representation of what should have been persisted to the database in the response body is correct.
In reality, the reverse relationship between Flag and Product has not been established (see relevant models, serializer, and view below) and Product.flag is actually None (serialized to null by DRF).
Relevant models:
class Product(models.Model):
...
flag = models.OneToOneField('Flag', null=True, blank=True, related_name='product')
class Flag(models.Model):
...
reasons = ManyToManyField('FlagReason')
severity = models.CharField(...)
notes = models.TextField(...)
...
Relevant DRF serializer:
class FlagSerializer(serializers.ModelSerializer):
class Meta:
model = models.Flag
fields = ('id', 'reasons', 'notes', 'created_timestamp',
'modified_timestamp', 'severity', 'product',)
Relevant DRF view:
class FlagListCreate(generics.ListCreateAPIView):
queryset = Flag.objects.all()
serializer_class = FlagSerializer
filter_fields = ('severity',)
POST request JSON body:
{
"product": 1328,
"severity": "mid",
"reasons": [1, 2],
"notes": "Test note",
}
201 Created response to the above POST request (note product pk):
{
"id": 31,
"reasons": [
1,
2
],
"notes": "Test note",
"created_timestamp": "2018-04-30T11:54:50.762054+01:00",
"modified_timestamp": "2018-04-30T11:54:50.762084+01:00",
"severity": "mid",
"product": 1328
}
Response to GET request for above Flag (note product value is null):
{
"id": 31,
"reasons": [
1,
2
],
"notes": "Test note",
"created_timestamp": "2018-04-30T11:54:50.762054+01:00",
"modified_timestamp": "2018-04-30T11:54:50.762084+01:00",
"severity": "mid",
"product": null
}

Related

Django rest elasticsearch filter range in url query params

I am using elasticsearch with django rest framework. I am using this lib:
https://github.com/barseghyanartur/django-elasticsearch-dsl-drf/
I am trying to filter by price range according to these docs: https://github.com/barseghyanartur/django-elasticsearch-dsl-drf/
This is my views:
class TestAPIView(DocumentViewSet):
document = TestDocument
serializer_class = TestSerializer
queryset = TestModel.objects.all()
filter_backends = [
FilteringFilterBackend
]
filter_fields = {
'price': {
'field': 'price',
'lookups': [
LOOKUP_FILTER_RANGE,
LOOKUP_QUERY_IN,
],
},
}
and this is my document.py file
#registry.register_document
class TestDocument(Document):
price = fields.IntegerField(attr='price')
class Index:
name = 'TestModel'
settings = {
'number_of_shards': 1,
'number_of_replicas': 0,
}
class Django:
model = TestModel
fields = [
'id',
]
When I hit on browser this url: http://127.0.0.1:8000/search/api/v1/test-model/?price=12 It works very well, even when I try with this url : http://127.0.0.1:8000/search/api/v1/test-model/?price=55 it works,
I am facing a problem to filter by range, like I want to filter price 12 to price 90, in this case how can I pass query parameter for the range? can anyone help me in this case?
The source codeĀ [GitHub] provides an example for this:
# Example: {"query": {"range": {"age": {"gte": "16", "lte": "67"}}}}
# Example: http://localhost:8000/api/users/?age__range=16__67
You thus can filter with:
http://127.0.0.1:8000/search/api/v1/test-model/?price__range=12__90
to retrieve items with a price between 12 and 90.

Testing Django Wagtail - assert that a child of the given Page type can be created under the parent, using the supplied POST data

I've defined a custom page model (a blog post) as a child of a parent model (a blog index page) and I want to test that the child can be created under its parent.
The BlogPage and BlogIndexPage models are copied from the wagtail "basic blog" example in the documentation, and works as expected.
I'm trying to follow the documentation but I get the following validation error:
AssertionError: Validation errors found when creating a cms.blogpage:
E date:
E This field is required.
E intro:
E This field is required.
E slug:
E This field is required.
E title:
E This field is required.
I suspect that I'm defining my fixture incorrectly, but I am not what the correct form is. Any help is greatly appreciated! Can someone explain why it isn't working?
fixture (apps.cms.tests.fixtures.blogPage.json):
[
{
"model":"wagtailcore.page",
"pk": 1,
"fields":{
"date":"2022-02-28",
"intro":"intro to the post...",
"slug":"slug/",
"title":"the title",
"body":"body of the post...",
"categories":[
1
],
"content_type": ["cms", "blogpage"],
"depth": 2
}
},
{
"model": "cms.blogpage",
"pk": 1,
"fields": {}
}
]
the test class (apps.cms.tests.test_pages.py):
class MyPageTests(WagtailPageTests):
def setUp(self):
self.login()
page = BlogIndexPage(title="Home page", slug="home", path="foo", depth=1)
page.save()
def test_create_blog_post(self):
cwd = Path.cwd()
root_page = BlogIndexPage.objects.first()
with open(f"{cwd}/lettergun/apps/cms/tests/fixtures/BlogPage.json") as json_file:
fixture = json.load(json_file)
# Assert that a ContentPage can be made here, with this POST data
self.assertCanCreate(root_page, BlogPage, nested_form_data(fixture))
the models (apps.cms.models.py):
class BlogIndexPage(Page):
template = "blog.html"
intro = models.TextField(blank=True)
def get_context(self, request):
# Update context to include only published posts, ordered by reverse-chron
context = super().get_context(request)
blogpages = self.get_children().live().order_by("-first_published_at")
context["blogpages"] = blogpages
return context
content_panels = Page.content_panels + [FieldPanel("intro", classname="full")]
class BlogPageTag(TaggedItemBase):
content_object = ParentalKey("BlogPage", related_name="tagged_items", on_delete=models.CASCADE)
class BlogPage(Page):
template = "blog-post.html"
date = models.DateField("Post date")
intro = models.CharField(max_length=250)
body = RichTextField(blank=True)
tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
categories = ParentalManyToManyField("cms.BlogCategory", blank=True)
def main_image(self):
gallery_item = self.gallery_images.first()
if gallery_item:
return gallery_item.image
else:
return None
search_fields = Page.search_fields + [
index.SearchField("intro"),
index.SearchField("body"),
]
content_panels = Page.content_panels + [
MultiFieldPanel(
[
FieldPanel("date"),
FieldPanel("tags"),
FieldPanel("categories", widget=forms.CheckboxSelectMultiple),
],
heading="Blog information",
),
FieldPanel("intro"),
FieldPanel("body"),
InlinePanel("gallery_images", label="Gallery images"),
]
The last argument to self.assertCanCreate is a dictionary of HTTP POST data to be submitted to the 'create page' admin view. This is an entirely different thing to a fixture (which is a representation of the page data as stored in the database), and the data structures are not compatible.
At its simplest, the POST dictionary can just consist of the required fields date, intro, slug and title:
self.assertCanCreate(root_page, BlogPage, {
'date': '2022-02-28',
'intro': "intro to the post...",
'slug': 'my-blog-page',
'title': 'My blog page',
})
The nested_form_data helper is only needed if your test is creating a page with data that's more complex than a flat list of fields - for example, if you wanted your page data to contain some gallery images, you'd need to use it along with the inline_formset helper. As you have an InlinePanel on your page, you need to account for that even if you're not passing it any data - see https://stackoverflow.com/a/71356332/1853523 for details.

How to get image url or download url of images in pages API where image is created by a streamfield?

In my wagtail application I have a streamfield that is used to upload an image using ImageChooserBlock along with a title and text. That means in the single streamfield I have a title, a text and an image upload inputs. I'm trying to get the image url in the rest framework's pages API (localhost:8000/api/v2/pages/[page-id]). But this pages api only gives the image id of the uploaded images as follows
{
"type": "avengers",
"value": {
"title": "Tony Stark",
"avengers": [
{
"image": 1, /******* this is the image id returned ********/
"title": "Iron Man",
"text": "Iron man is now in framework"
}
]
},
"id": "2f27cb24"
}
If I access the images api(http://localhost:8000/api/v2/images/1/) I'm getting the download_url as follows
{
"id": 1,
"meta": {
"type": "wagtailimages.Image",
"detail_url": "http://localhost/api/v2/images/1/",
"tags": [],
"download_url": "/media/original_images/avenger.jpeg"
},
"title": "avenger.jpeg",
"width": 400,
"height": 400
}
My question is how I can get the download_url or the image url in the pages API (localhost:8000/api/v2/pages/[page-id])
My streamfields blocks.py for the avengers block is as follows
class AvengersBlock(blocks.StructBlock):
title = blocks.CharBlock(required=True, help_text="Add your title")
Avengers = blocks.ListBlock(
blocks.StructBlock(
[
("image", ImageChooserBlock(required=True)),
("title", blocks.CharBlock(required=True, max_length=40)),
("text", blocks.TextBlock(required=True, max_length=200))
]
)
)
class Meta: # noqa
template = "streams/Avengers_block.html"
icon = "placeholder"
label = "Avengers"
This stream field is used in a content types model.py as follows
from django.db import models
from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel
from wagtail.core.fields import StreamField
from wagtail.core.models import Page
from wagtail.api import APIField
from apps.common.streams import blocks
class AvengersPage(Page):
tempalte = "avengers/avengers_page.html"
content = StreamField(
[
("avengers", blocks.AvengersBlock())
],
null=True,
blank=True,
)
subtitle = models.CharField(max_length=100, null=True, blank=True)
content_panels = Page.content_panels + [
FieldPanel("subtitle"),
StreamFieldPanel("content"),
]
api_fields = [
APIField("subtitle"),
APIField("content")
]
class Meta: # noqa
verbose_name = "Avengers Page"
Add this to your AvengersBlock and when you call your API at
/api/v2/pages/?type=home.AvengersPage&fields=content
you should see the JSON you're looking for.
def get_api_representation(self, value, context=None):
""" Recursively call get_api_representation on children and return as a plain dict """
dict_list = []
for item in value["Avengers"]:
temp_dict = {
'title': item.get("title"),
'text': item.get("text"),
'image_url': item.get("image").file.url
# any other relevant fields of your model...
}
dict_list.append(temp_dict)
return dict_list

Using views to change DRF response

I would like to alter the response from an API.
However, it does not alter the result properly. I get a KeyError: 'game'.
I am not sure why, as my API response (via URL) seems to have the value game in it. I may be getting confused with the JSON response, and the python object.
I have a sample of the API response below
results from API
{
"pk": 995,
"game": [
{
"name": "Finance",
"gamelevel": 3
},
{
"name": "Data",
"gamelevel": 1
}
]
},
views.py
class TagList(viewsets.ModelViewSet):
queryset = Task.objects.filter(game__isnull=False).all()
serializer_class = TagSortSerializer
def get_queryset(self):
test = self.queryset.values('title', 'game__name')
result = defaultdict(set)
for item in queryset:
parent = {'name': 'NoLevel_1'}
children = []
for game in item['game']:
if game['gamelevel'] == 1:
parent = game
else:
children.append((game['gamelevel'], game['name']))
result[parent['name']].update(children)
result = [
{
'name': parent,
'game_child': [
{'name': name, 'gamelevel': gamelevel} for gamelevel, name in games
],
'gamelevel': 1,
} for parent, games in result.items()
]
return result
You're using the values queryset method to get only a selection of fields from the model, and the only fields you've specified are title and tag__name. So you won't get game or any of the other keys you've used.
You certainly don't want to use values here in the first place; just do a normal query and access fields via dot lookup rather than dictionary.

Enumerating model choices in a Django Rest Framework serializer

I have a model that uses a Django choices field, like this:
class Question(models.Model):
QUESTION_TYPES = (
(10,'Blurb'),
(20,'Group Header'),
(21,'Group Footer'),
(30,'Sub-Group Header'),
(31,'Sub-Group Footer'),
(50,'Save Button'),
(100,'Standard Question'),
(105,'Text-Area Question'),
(110,'Multiple-Choice Question'),
(120,'Standard Sub-Question'),
(130,'Multiple-Choice Sub-Question')
)
type = models.IntegerField(default=100,choices=QUESTION_TYPES)
I'm using Django Rest Framework to present this model as an API to an Angular web app. In my Angular web app, I want a combo box widget that drops down with all those choices. Not the integers, but the text choices, like "blurb", "standard question" and so on.
Now, I could hand code the combo box into the Angular app, but in the spirit of DRY, is it possible to write a DRF serializer that just returns those choices (ie the QUESTION_TYPES object), so I can populate the combo box with a ReST query?
And by "possible", I guess I mean "simple and elegant". And maybe I also mean "ReSTful". (Is it ReSTful to do it that way?)
Just wondering . . .
Thanks
John
I would probably try something like the following:
# models.py
class Question(models.Model):
QUESTION_NAMES = (
'Blurb',
'Group Header',
'Group Footer',
'Sub-Group Header',
'Sub-Group Footer',
'Save Button',
'Standard Question',
'Text-Area Question',
'Multiple-Choice Question',
'Standard Sub-Question',
'Multiple-Choice Sub-Question')
QUESTION_VALS = (10, 20, 21, 30,
31, 50, 100, 105, 110,
120, 130)
QUESTION_TYPES = tuple(zip(QUESTION_VALS, QUESTION_NAMES))
# Personal choice here: I never name attribs after Python built-ins:
qtype = models.IntegerField(default=100,choices=QUESTION_TYPES)
The following doesn't work as I thought it should
(Following was my original intuition on serializing a list of objects, but it did not work. I'm leaving it in here anyway, because it seems like it should work.)
Okay, so we have a way to access the strings on their own, now we just need to serialize them, and for that, I'd probably try to use the ListField in DRF3, which should support the source kwarg, I would think?
# serializers.py
from .models import Question
class YourSerializer(ModelSerializer):
names = serializers.ListField(
child=serializers.CharField(max_length=40),
source=Question.QUESTION_NAMES
)
class Meta:
model = Question
fields = ('names', etc.)
The following does return a list of results
Fallback: use a SerializerMethodField:
from .models import Question
class YourSerializer(serializers.ModelSerializer):
...
names = serializers.SerializerMethodField()
def get_names(self, obj):
return Question.QUESTION_NAMES
class Meta:
model = Question
Demo:
In [1]: q = Question.objects.create()
Out[1]: <Question: Question object>
In [2]: ser = YourSerializer(q)
In [3]: ser.data
Out[3]: {'id': 1, 'names': ['Blurb', 'Group Header', 'Group Footer', 'Sub-Group Header', 'Sub-Group Footer', 'Save Button', 'Standard Question', 'Text-Area Question', 'Multiple-Choice Question', 'Standard Sub-Question', 'Multiple-Choice Sub-Question'], 'qtype': 100}
if you use a ModelViewSet in combination with a ModelSerializer, the OPTIONS request will return metadata that you can use to get the choice options.
from models import Question
from rest_framework import serializers
class QuestionSerializer(serializers.ModelSerializer):
class Meta:
model = Question
from rest_framework.viewsets import ModelViewSet
class QuestionChoicesViewSet(ModelViewSet):
queryset = Question.objects.all()
serializer_class = QuestionSerializer
This will give you a response that includes the actions attribute, that might look something like this:
"actions": {
"POST": {
"id": {
"type": "integer",
"required": false,
"read_only": true,
"label": "ID"
},
"qtype": {
"type": "choice",
"required": false,
"read_only": false,
"label": "Qtype",
"choices": [
{
"display_name": "Blurb",
"value": 10
},
{
"display_name": "Group Header",
"value": 20
},
{
"display_name": "Group Footer",
"value": 21
},
{
"display_name": "Sub-Group Header",
"value": 30
},
//...
}
}
}
You can iterate over the choices attribute on qtype to get all of the available choices.
To get more familiar with this topic you can read: Metadata
I accomplished this by making an API endpoint for the choices which only use the GET verb.
models.py
QUESTION_TYPES = (
(10,'Blurb'),
(20,'Group Header'),
(21,'Group Footer'),
(30,'Sub-Group Header'),
(31,'Sub-Group Footer'),
(50,'Save Button'),
(100,'Standard Question'),
(105,'Text-Area Question'),
(110,'Multiple-Choice Question'),
(120,'Standard Sub-Question'),
(130,'Multiple-Choice Sub-Question')
)
class Question(models.Model):
type = models.IntegerField(default=100,choices=QUESTION_TYPES)
viewsets.py
from models import QUESTION_NAMES, Question
from rest_framework import serializers
class QuestionSerializer(serializers.ModelSerializer):
type = serializers.ChoiceField(choices=QUESTION_NAMES, default=100)
class Meta:
model = Question
from rest_framework.response import Response
from rest_framework.views import APIView
class QuestionChoicesViewSet(APIView):
def get(self, request):
return Response(QUESTION_NAMES)
from rest_framework import viewsets
class QuestionViewSet(viewsets.ModelViewSet):
queryset = Question.objects.all()
serializer_class = QuestionSerializer