Wagtail API - show image URL on json output - django

Fairly new to Wagtail - I'm currently creating a Wagtail API for my React app. Have installed successfully and am getting a json output, but not getting a url for images that are uploaded in the Wagtail admin panel. I have searched online, but not having much joy.
This is the basic home page model I have created
class BarsHomePage(Previewable, Themable, Page):
bars_site_homepage_test = models.CharField(max_length=255, blank=True)
feed_image = models.ForeignKey(
'DemoImage',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
api_fields = ['bars_site_homepage_test','feed_image']
class DemoImage(Image):
#property
def fullwidth_url(self):
return generate_image_url(self, 'width-800')
#property
def halfwidth_url(self):
return generate_image_url(self, 'width-400')
api_fields = (
'fullwidth_url',
'halfwidth_url',
)
class Meta:
proxy = True
Json output
{
"id": 504,
"meta": {
"type": "wagtailimages.Image",
"detail_url": "http://www.lv.local/api/v1/images/504/"
},
"title": "Lighthouse.jpg",
"tags": [],
"width": 1365,
"height": 2048
}
Thanks

As of Wagtail 1.10, you can use ImageRenditionField in your page's api_fields definition to include the URL for an image, rendered at a size of your choosing:
from wagtail.api import APIField
from wagtail.wagtailimages.api.fields import ImageRenditionField
class BarsHomePage(Previewable, Themable, Page):
# ...
api_fields = [
APIField('bars_site_homepage_test'),
APIField('feed_image_fullwidth', serializer=ImageRenditionField('width-800', source='feed_image')),
]

Related

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

How to integrate Haystack with Django Rest Framework for making GET REST API for searching?

model.py
class Item(models.Model):
name=models.CharField(max_length=50)
company=models.CharField(max_length=100)
search_indexes.py
class ItemIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
name=indexes.CharField(model_attr='name')
company=indexes.CharField(model_attr='company')
def get_model(self):
return Item
def index_queryset(self, using=None):
return self.get_model().objects.all()
serializer.py
class ItemSearchSerializer(serializers.Serializer):
text = serializers.CharField()
name=serializers.CharField()
company=serializers.CharField()
views.py
class ItemSearchViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
serializer_class = ItemSearchSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def get_queryset(self):
request = self.request
queryset = EmptySearchQuerySet()
if request.GET.get('q', ''):
query = request.GET.get('q', '')
queryset =SearchQuerySet().filter(content=query);
return queryset
And in url.py I added :
router.register(r'searchquery', views.ItemSearchViewSet, base_name='searchquery')
Now on making GET request from postman like :
http://127.0.0.1:8000/searchquery/?q=app, I am getting the response as desired as show below.
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"text": "apple\ndjflkj",
"id": 14,
"name": "apple",
"mrp": "45.000000",
"company": "djflkj",
"imageid": "jfhi",
"edible": false,
"discount": "0.000000",
"deliverable": true,
"seller_uid": "ljhkh",
"category": "ldjhgjfdk"
},
{
"text": "app\nhuhiu",
"id": 16,
"name": "app",
"mrp": "78.000000",
"company": "huhiu",
"imageid": "iyiugiy",
"edible": false,
"discount": "45.000000",
"deliverable": true,
"seller_uid": "hjh",
"category": "hhl"
}
]
}
But the reponse time is very slow it takes around 2700 ms everytime ,
and I want to make it fast. As response of elastic search is much fast
but I don't know what I am doing wrong. Not sure but may be due to
these reasons I am getting this delay : 1) Haystack is made for
django, so on integrating it with django rest framework , it may be
getting slow. 2) I am using free Bonsai Elastic search heroku add on
and it has just 125 mb memory.
This is how I am connecting to Bonsai elastic search (setting.py)
ES_URL = urlparse('https://******#pine-1455731.us-east1.bonsaisearch.net')
print ES_URL
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
'URL': ES_URL.scheme + '://' + ES_URL.hostname + ':443',
'INDEX_NAME': 'haystack',
},
}
if ES_URL.username:
HAYSTACK_CONNECTIONS['default']['KWARGS'] = {"http_auth": ES_URL.username + ':' + ES_URL.password}
Any help will be appreciated. I am new to elastic search. I want to do elastic search to search products by name for my android application.
I even don't know whether this is the correct approach to do searching. I thought I would enter name of product I want to search and then i will send a GET request and get all the products which are related.
I did Python Profile please look it here: gist
If any one could suggest me any other way of achieving this I will appreciate your help.
The search response is slow because of this code:
def index_queryset(self, using=None):
return self.get_model().objects.all()
index_queryset is supposed to return query set, you are actually returning all model objects. This method is called for every item which is returned in search.

Testing related page in Wagtail

I've got a ContentPage model in wagtail and a RelatedPost model that links other ContentPage models to ContentPage a bit like this:
class ContentPage(Page):
summary = RichTextField(blank=True)
body = RichTextField(blank=True)
published = models.DateTimeField(default=timezone.now())
content_panels = Page.content_panels + [
FieldPanel('summary'),
FieldPanel('body', classname="full"),
InlinePanel('related_page', label="Related Content"),
]
settings_panels = Page.settings_panels + [
FieldPanel('published'),
]
class RelatedPost(Orderable):
post = ParentalKey(
'ContentPage',
related_name='related_page'
)
page = models.ForeignKey(
'ContentPage',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+"
)
panels = [
FieldPanel('page')
]
When I run this test:
class ContentPageTests(WagtailPageTests):
def test_can_create_article_page(self):
self.assertCanCreateAt(ContentIndexPage, ContentPage)
# content_index is just a parent page
content_index = self.create_content_index_page()
self.assertCanCreate(content_index, ContentPage, {
'title': 'Test Article',
'published': datetime.datetime.now()
})
I get an error saying:
django.core.exceptions.ValidationError: ['ManagementForm data is missing or has been tampered with']
The admin works fine. I can save related pages etc and when I comment out the InlinePanel line it works fine.
The data passed to assertCanCreate needs to match the format of a form submission being posted to the 'edit page' form in the Wagtail admin. For a child model in an InlinePanel, Wagtail handles this with a Django formset - see https://docs.djangoproject.com/en/1.10/topics/forms/formsets/#formset-validation - and so you need to supply all the fields that the Django formset logic would expect, including the management form. The simplest case that passes validation is a management form that simply reports that there are no child forms:
self.assertCanCreate(content_index, ContentPage, {
'title': 'Test Article',
'published': datetime.datetime.now(),
'related_page-TOTAL_FORMS': 0,
'related_page-INITIAL_FORMS': 0,
'related_page-MAX_NUM_FORMS': 999,
})

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