I have a CMS based on wagtail, and have recently re-written it in a more sensible fashion. I have written a script to migrate the old content to this new version, which is based on wagtail 2.3 (old version was on wagtail 1.11). I have written the migration script (to re-construct various foreign keys etc) and all content has been populated and seems to be working except for the render of StreamFields.
Frustratingly, when I switch back to my test db for v2, this works fine (content is rendered) - I've been scouring my databases for differences between the two rows (in wagtailcore_page or blog_blogpostpage) and can't see any difference. There's obviously something I'm missing in the way wagtail fetches StreamField content, can anyone enlighten me as to what I might have missed in the migration? Many thanks!!
models.py
class BlogPostPage(Page): # Individual blog post
template = 'blog/post_page.html'
parent_page_types = ['blog.BlogIndexPage']
show_in_menus_default = True
author = models.ForeignKey(
User, on_delete=models.PROTECT, default=1,
)
description = models.CharField(
max_length=300, blank=False,
help_text="Add a brief (max 300 characters) description for this blog post."
)
date = models.DateField(
"Post date",
help_text="This date may be displayed on the blog post. "
"It is not used to schedule posts to go live at a later date."
)
body = StreamField([
('heading', blocks.CharBlock(classname="full title")),
('paragraph', blocks.RichTextBlock()),
('embed', EmbedBlock()),
('image', ImageChooserBlock(classname='img-responsive')),
('code', CodeBlock(label='Code')),
('table', TableBlock(label='Table'))
], help_text="Create content by adding new blocks.")
table blog_blogpostpage entry:
"page_ptr_id","description","date","body","author_id"
23,"Now including Blog!","2018-12-06","[{""type"": ""paragraph"", ""value"": ""<p>Since the first release we've made some improvements and upgrades...</p>"", ""id"": ""25fe32be-2090-42dd-8e3e-4df53c494227""}]",15
migration_script.sh
INSERT INTO "public"."wagtailcore_page"("path","depth","numchild","title","slug","live","has_unpublished_changes","url_path","seo_title","show_in_menus","search_description","go_live_at","expire_at","expired","content_type_id","owner_id","locked","latest_revision_created_at","first_published_at","live_revision_id","last_published_at","draft_title")
VALUES
(E'00010002000O0001',4,0,E'Release: version 2',E'release-version-2',TRUE,FALSE,E'/home/blog/release-version-2/',E'',TRUE,E'',NULL,NULL,FALSE,6,15,FALSE,E'2018-12-06 16:58:10.897348+08',E'2018-12-06 16:58:10.926032+08',NULL,E'2018-12-06 16:58:10.926032+08',E'Release: version 2');
INSERT INTO "public"."blog_blogpostpage"("page_ptr_id","description","date","body","author_id")
VALUES
((SELECT id FROM wagtailcore_page WHERE path='00010002000O0001'),E'Now including Blog!',E'2018-12-06',E'[{"type": "paragraph", "value": "<p>Since the first release we've made some improvements and upgrades...</p>", "id": "25fe32be-2090-42dd-8e3e-4df53c494227"}]',15);
template.html
{% include_block page.body %}
^^^ Nothing is shown for the page.body field, but description, date and author are rendered.
Your data migration created invalid JSON:
[{""type"": ""paragraph"", ""value"": ""<p>S ...
Should have single quotes:
[{"type": "paragraph", "value": "<p>S ...
Related
Edit: Seems like I can't add images :(, added links to imgur instead.
I'm trying to implement settings in order to enter social media accounts.
#register_setting
class SocialMediaSettings(BaseGenericSetting):
discord_url = models.URLField(
help_text="Link used."
)
discord_name = models.CharField(
max_length=100,
help_text="Name to be displayed."
)
gitlab_url = models.URLField(
help_text="Link used."
)
gitlab_name = models.CharField(
max_length=100,
help_text="Name to be displayed."
)
panels = [
FieldPanel("discord_url"),
FieldPanel("discord_name"),
FieldPanel("gitlab_url"),
FieldPanel("gitlab_name")
]
Which works totally fine, but as it feels like a super bad idea to do it this way, I wanted to use a streamfield like this:
#register_setting
class SocialMediaSettings(BaseGenericSetting):
body = StreamField([
("social_account", blocks.SocialBlock()),
], null=True, blank=True, use_json_field=True)
panels = [
FieldPanel("body")
]
class SocialBlock(blocks.StructBlock):
name = blocks.CharBlock(
max_length=100,
help_text="Name used for tooltips."
)
url = blocks.URLBlock(
help_text="URL"
)
Image of wagtail admin (image)
This way it shows up partly. In the admin panel I can only enter the name, not the url. And saving doesn't work eighter (saving will reset what I entered in the admin panel).
Adding the SocialBlock to a normal page works as expected:
SocialBlock working in another Page (image)
Which is why I ran out of ideas. Are streamfields not supported in snippets / settings? (the above exapmles are done with #register_setting but I tried #register_snippet with the same result.)
Any idea how I could get this up and running?
Wagtail 4.0
Django 4.1
Well, wagtail itself just showed me the solution :x
It was a bug in Wagtail 4.0, which is solved in 4.0.1
Release Notes Wagtail 4.0.1
Is it possible to translate a non snipppet/page model in Wagtail >= 2.11 and wagtail-localize >= 0.9.3 ?
I've set up my model as follows:
class TrainingPlace(TranslatableMixin, models.Model):
name = models.CharField()
description = models.TextField()
class Meta(TranslatableMixin.Meta):
verbose_name = "Training Places"
translatable_fields = [
TranslatableField("description")
]
I have a "Training" submenu in my wagtail admin page, with Trainings, Training Photos, Trainers, Training Places, where I can add Trainers, new Trainings etc.
If I register a TraningPlace with a #register_snipppet I can translate it, but at the moment there are some problems:
I see many Trainings (menu) -> TrainingPlace recoreds in admin listing with no "Translate" option
If I go to Snippets (menu) => Training Place snippets I can see snipppets and have option to translate/create/sync translations, but when I click on change language button in admin I am getting a blank page (url points to a snippet with different ID) which may be a bug.
Can non snippet/page models be translated in admin when using TranslatableMixin on model ?
Situation
I have a custom image and rendition model, and have followed the wagtail v2.4 guide to implement them:
class AccreditedImage(AbstractImage):
"""
AccreditedImage - Customised image model with optional caption and accreditation
"""
caption = models.CharField(max_length=255, blank=True)
accreditation = models.CharField(max_length=255, blank=True, null=True)
admin_form_fields = Image.admin_form_fields + (
'caption',
'accreditation',
)
class Meta:
verbose_name = 'Accredited image'
verbose_name_plural = 'Accredited images'
def __str__(self):
credit = ' ({})'.format(self.accreditation) if (self.accreditation is not None) and (len(self.accreditation) > 0) else ''
return '{}{}'.format(self.title, credit)
class AccreditedRendition(AbstractRendition):
"""
AccreditedRendition - stores renditions for the AccreditedImage model
"""
image = models.ForeignKey(AccreditedImage, on_delete=models.CASCADE, related_name='renditions')
class Meta:
unique_together = (('image', 'filter_spec', 'focal_point_key'),)
verbose_name = 'Accredited Image Rendition'
verbose_name_plural = 'Accredited Image Renditions'
In settings I have:
WAGTAILIMAGES_IMAGE_MODEL = 'cms.AccreditedImage'
But, I have two third party plugins installed: puput and wagtail_events, each of which use a foreign key to wagtail images.
When I run `manage.py makemigrations, additional migrations are created in the puput and wagtail_events site_packages folders to handle the change in FK. The migrations look like this:
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('puput', '0005_blogpage_main_color'),
]
operations = [
migrations.AlterField(
model_name='blogpage',
name='header_image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='cms.AccreditedImage', verbose_name='Header image'),
),
migrations.AlterField(
model_name='entrypage',
name='header_image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='cms.AccreditedImage', verbose_name='Header image'),
),
]
The Problem
If I actually apply these migrations, then puput or wagtail_events releases a new version, then the migration history becomes corrupted - e.g. my autogenerated 0006* migration on puput and their new 0006* migration fork the history
The Question
Is there a way of overcoming this? Or a recommended practice for what to do?
At this point I'm in very early beta, so I could dump the entire DB and start again if the recommended strategy is to set this up from the outset to avoid the issue.
Thanks for any help, folks!
Answer 1 - if you have control over your third party libraries
The initial migration in the third party library should define a swappable dependency, for example:
from wagtail.images import get_image_model_string
dependencies = [
migrations.swappable_dependency(get_image_model_string()),
]
operations = [
migrations.CreateModel(
name='ThirdPartyModel',
fields=[
...
('image', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to=get_image_model_string())),
],
...
),
This is not automatically created by makemigrations. get_image_model_string needs to be used like this in every migration affecting that FK, made through the entire migration history of the library.
If you change the setting at some point in the project, you will still need to do a data migration ('Migrate an existing swappable dependency' might help), but this solves the forking problem described above if starting clean.
It has the drawback of requiring control over the third party library. I'm not holding my breath for a project like puput to go back and alter their early migration history to allow for a swappable image model (puput's initial migration hard-codes wagtailimages.Image). But I've implemented this for wagtail_events (my own project) to save other people this hassle.
Answer 2 - if you don't have control
Ugh. I've been working on this a while and all candidate solutions are all pretty horrible. I considered getting my custom image class to impersonate wagtail.images.model.Image via the db_table meta attributes, and even by creating another app which essentially duplicates wagtail images. It's all either a lot of work or super hacky.
I've chosen to take over migrations manually using the MIGRATION_MODULES setting.
For my scenario, I've taken the entire migration history of puput and copied all the files into a separate folder, root/custom_puput_migrations/. I set
MIGRATION_MODULES = {
'puput': 'custom_puput_migrations'
}
WAGTAILIMAGES_IMAGE_MODEL = 'otherapp.AccreditedImage'
Then I pull the ol' switcharoo by editing 0001_initial.py in that folder to refer to the model via the setting, rather than by hard coding:
...
from wagtail.images import get_image_model_string
class Migration(migrations.Migration):
dependencies = [
...
migrations.swappable_dependency(get_image_model_string())
]
operations = [
migrations.CreateModel(
name='BlogPage',
fields=[
...
('header_image', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, verbose_name='Header image', blank=True, to=get_image_model_string(), null=True)),
],
...
Drawbacks
1) The actual table relation created is not strictly determined by the migration file, but by the setting, which could change independently. If you prefer to avoid this, you could instead simply hard-code your referred model in the custom migration.
2) This approach leaves you pretty vulnerable to developers upgrading the library version requirement without realising that they also have to manually copy across the migrations files. I suggest a check (f/ex ensuring that the number of files in the default migrations folder is the same as the number of files in the custom one) before allowing the app to boot, to ensure your development and production databases all run on the same set of migrations.
I would like to allow a dropdown within wagtail admin to select from values (blocks) contained within another model's streamfield, is that possible? I imagined something like:
Feedback(page):
paper = models.ForeignKey('PaperPage', on_delete=models.CASCADE, null=True, blank=False, help_text="The paper associated with this feedback. Auto assigned.")
content_panels = [
InlinePanel('paper__drafts_id', label='Draft') <--- this doesn't work
]
where
Paper(page):
drafts = StreamField(
[
('draft_block', blocks.ListBlock(blocks.StructBlock([
('date', blocks.DateTimeBlock(required=True, label='Date draft uploaded')),
('uploaded_by', MemberBlock(required=False, label='Uploaded by',
help_text="Who is uploading this draft.")),
('draft_file', DocumentChooserBlock(required=False, label='Upload file')),
]), template='papers/blocks/drafts.html')),
],
blank=True
)
but I'm not sure if this is even possible? Any suggestions would be most appreciated. Thanks!
This is one downside of StreamField - the data is not stored as "true" database objects, but only as JSON text stored against the page, so there's no way to define relations such as ForeignKeys pointing to individual items in that data.
If there's only one block type in the stream, as in your example code, then it would be a better fit to define 'draft' as a child object (with an InlinePanel) on the Paper model instead; it will then exist as a true database model.
I have a case that I am curious about for a long, long time. Say I have 3 models:
class Product(models.Model):
manufacturer = models.CharField(max_length=100)
description = models.TextField(blank=True)
class PurchasedProduct(models.Model):
product = models.ForeignKey(Product)
purchase = models.ForeignKey('Purchase')
quantity = models.PositiveIntegerField()
class Purchase(models.Model):
customer = models.ForeignKey('customers.Customer')
products = models.ManyToManyField(Product, through=PurchasedProduct)
comment = models.CharField(max_length=200)
I have an API and client application written in some JavaScript framework. So now I need to communicate between them! I am not sure how should I handle this situation in DRF, naturally I would expect to get something like this when accessing /purchase/1/
{
"id": 1,
"customer": 1,
"comment": "Foobar",
"products": [
{
"id": 1,
"product": {
"id": 1,
....
},
....
},
....
]
}
So I created proper serializer specifying that products field should use PurchasedProductSerializer which in turn uses nested ProductSerializer. It is fine cause I get all necessary info to, say, display what specific products where purchased and in what quantity during shopping using appropriate components in say React.
The problem for me is however when I need to POST new PurchasedProduct. I would expect the most convenient form to be:
{
"quantity": 10,
"purchase": 1,
"product": 1
}
As it carries all necessary info and has the smallest footprint. However I can't be accomplished using PurchasedProductSerializer as it requires product to be object instead of id.
So my question here, is this a good approach (it seems very natural to me), should I use two separate serializers for GET and POST? Should I perform this differently? Could you point me to some best practices/books how to write APIs and client apps?
I had the exact same problem a few months back and would've been more than happy if someone would've told me. I ended up with the exact solution that you proposed to add products to a purchase. I do agree that your proposed POST request it is the most natural way with the minimal required footprint.
In order to correctly process the POST request's data correctly, though, I ended up using two separate Serializers, just as you described. If you're using DRF viewsets, one way to select the correct serializer on GET and POST is to override the get_serializer_class method as described here.
The deserializer for POST requests could look like this:
class PurchasedProductDeserializer(serializers.ModelSerializer):
product = serializers.PrimaryKeyRelatedField(queryset=Product.objects.all())
purchase = serializers.PrimaryKeyRelatedField(queryset=Purchase.objects.all())
class Meta:
model = PurchasedProduct
fields = ('id', 'product', 'purchase', 'quantity')
write_only_fields = ('product', 'purchase', 'quantity')
That deserializer can then be used for input validation and finally to add a product to a purchase (or increase its quantity).
E.g., inside your viewset:
def create(self, request, *args, **kwargs):
# ...
# init your serializer here
serializer = self.get_serializer(data=request.data)
if serializer.is_valid(raise_exception=True):
# now check if the same item is already in the cart
try:
# try to find the product in the list of purchased products
purchased_product = serializer.validated_data['purchase'].purchasedproduct_set.get(product=serializer.validated_data['product'])
# if so, simply increase its quantity, else add the product as a new item to the cart (see except case)
purchased_product.quantity += serializer.validated_data['quantity']
purchased_product.save()
# update the serializer so it knows the id of the existing instance
serializer.instance = purchased_product
except PurchasedProduct.DoesNotExist:
# product is not yet part of the purchase cart, add it now
self.perform_create(serializer)
# ...
# do other stuff here
As for best practices, there's a ton of documentation available on the internet, but if you're looking for books you might wanna look at some of the ones posted here. When you get bored of REST you might even wanna look into GraphQL.