Skip to content

Commit

Permalink
Merge branch 'main' into effectiveVolto2023
Browse files Browse the repository at this point in the history
  • Loading branch information
sneridagh authored Oct 1, 2023
2 parents 097f6f5 + 32f9910 commit 99e4a21
Show file tree
Hide file tree
Showing 18 changed files with 2,487 additions and 23 deletions.
7 changes: 6 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mastering-plone/index
mastering-plone-5/index
voltohandson/index
voltoaddons/index
volto_customization/index
effective-volto/index
testing/index
theming/index
Expand Down Expand Up @@ -64,6 +65,10 @@ teaching/index

: Build custom Volto add-ons, explore more advanced Volto topics.

{doc}`volto_customization/index`

: Are you new to JavaScript development and eager to explore the world of Volto customization? Unlock the power of Volto, the modern React-based CMS framework for Plone, by joining our comprehensive half day training designed specifically for JavaScript beginners.

{doc}`effective-volto/index`

: Learn proven practices of Plone frontend development.
Expand Down Expand Up @@ -124,7 +129,7 @@ Because we began this practice in 2022, all previous trainings that have documen

#### Volto, React, and Javascript

- [Volto](https://2022.training.plone.org/volto/index.html)
- [Volto](https://2022.training.plone.org/volto/index.html)
- [Angular SDK for Plone](https://2022.training.plone.org/angular/index.html)
- [GatsbyJS](https://2022.training.plone.org/gatsby/index.html)
- [JavaScript For Plone Developers (up to Plone 5)](https://2022.training.plone.org/javascript/index.html)
Expand Down
171 changes: 160 additions & 11 deletions docs/migrations/exportimport.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,12 @@ It also does the following:

It can be used in:

* Plone 4 - 6
* Plone 4 - 6 (and beyond)

Most of the additional exports and imports are separate from the content because they may require two or more content items.
During import, you cannot be certain that both items already exist when one is imported.
It also makes it easier to adapt and/or skip these exports and imports to your need.
Example: Imagine a relation from A to B. When you try to create the relation while importing A the item B may not have been imported yet.
It also makes it easier to adapt and/or skip these exports and imports to your needs.


## Step 1: Cleanup
Expand Down Expand Up @@ -121,12 +122,12 @@ eggs +=

[versions]
# always use the newest version!
collective.exportimport = 1.6
collective.exportimport = 1.9
# Use the latest 7.x version for py2 and at support
plone.restapi = 7.8.0
plone.restapi = 7.8.3
pyrsistent = 0.15.7
hurry.filesize = 0.9
ijson = 3.1.4
ijson = 3.2.3
```


Expand All @@ -146,12 +147,12 @@ eggs +=

[versions]
# always use the newest version!
collective.exportimport = 1.6
collective.exportimport = 1.9
# Use the version that is shipped with your Plone version
# plone.restapi = 8.22.0
pyrsistent = 0.18.1
# plone.restapi = 9.0.0
pyrsistent = 0.19.3
hurry.filesize = 0.9
ijson = 3.1.4
ijson = 3.2.3
```


Expand Down Expand Up @@ -339,7 +340,7 @@ contentexport = fs contentexport

[versions]
# always use the newest version!
collective.exportimport = 1.6
collective.exportimport = 1.9
# Use the latest 7.x version for py2 and at support
plone.restapi = 7.8.0
pyrsistent = 0.15.7
Expand Down Expand Up @@ -452,15 +453,163 @@ Here are some of the discussed examples:
* Alternative ways to handle items without parent
* Export/Import registry settings
* Export/Import Zope Users
* Export/Import properties, registry-settings and installed addons
* Migrate `PloneFormGen` to `Easyform`
* Export and import `collective.cover` content
* Fixing invalid collection queries
* Migrate very old Plone Versions with data created by collective.jsonify

## Fix html

The html generated by Plone 6 Classic differs from Plone 4 and 5 version in various ways. Here are some examples:

* Image-scales in Archetypes were created using urls like `.../myimage/image_large`. In Dexterity we use `.../myimage/@@images/[fieldname]/large` instead.
* The image-tag generated by TinyMCE now requires a data-attributes `data-scale`
* Link- and Image-tags require the uuid of the target as `data-val` to be able to find it during editing.
* Image-Tags now support picture-variants and srcsets.

Exportimport deals with these changes by modifying the html of all richtext-fields and static portlets.
It uses [Beautiful Soup](https://www.crummy.com/software/BeautifulSoup/bs4/doc/) to parse and update the html and does a pretty good job.

But sometimes you may need additional changes in the html.
For this exportimport allows you to pass additional fixers (methods that modify the html).

Here are examples for some additional fixers and how to use them:

```python
from collective.exportimport.fix_html import fix_html_in_content_fields

class ImportAll(BrowserView):
def __call__(self):
# ...
fixers = [anchor_fixer, table_class_fixer, scale_unscaled_images, remove_spans]
results = fix_html_in_content_fields(fixers=fixers)
# ...


def anchor_fixer(text, obj=None):
"""Remove anchors and links without text.
Volto does not support anchors (yet).
"""
soup = BeautifulSoup(text, "html.parser")
for link in soup.find_all("a"):
if not link.get("href") and not link.text:
# drop empty links (e.g. anchors)
link.decompose()
elif not link.get("href") and link.text:
# drop links without a href but keep the text
link.unwrap()
return soup.decode()


def table_class_fixer(text, obj=None):
"""Cleanup some table-classes"""
if "table" not in text:
return text
dropped_classes = [
"MsoNormalTable",
"MsoTableGrid",
]
replaced_classes = {
"invisible": "invisible-grid",
}
soup = BeautifulSoup(text, "html.parser")
for table in soup.find_all("table"):
table_classes = table.get("class", [])
for dropped in dropped_classes:
if dropped in table_classes:
table_classes.remove(dropped)
for old, new in replaced_classes.items():
if old in table_classes:
table_classes.remove(old)
table_classes.append(new)
# all tables get the default bootstrap table class
if "table" not in table_classes:
table_classes.insert(0, "table")

return soup.decode()


def scale_unscaled_images(text, obj=None):
"""Scale unscaled image"""
if not text:
return text
fallback_scale = "huge"
soup = BeautifulSoup(text, "html.parser")
for tag in soup.find_all("img"):
if "data-val" not in tag.attrs:
# maybe external image
continue
scale = tag["data-scale"]
# Prevent unscaled images!
if not scale:
scale = fallback_scale
tag["data-scale"] = fallback_scale
if not tag["src"].endswith(scale):
tag["src"] = tag["src"] + "/" + scale
return soup.decode()


def remove_spans(text, obj=None):
"""Remove obsolete spans and smoothify html"""
if not text:
return text
soup = BeautifulSoup(text, "html.parser")
for tag in soup.find_all("span"):
style = tag.get("style", "")
if "font-style:italic" in style or "font-weight:bold" in style:
continue
logger.info("Removing %s: %s", tag.name, tag.attrs)
tag.unwrap()
soup.smooth()
return soup.decode()
```

## Migrate to Volto using exportimport

You can reuse the migration-code provided by the form `@@migrate_to_volto` (it is in `plone.volto`) in a migration.

You need to have the Blocks Conversion Tool (https://github.com/plone/blocks-conversion-tool) running that takes care of migrating richtext-values to Volto-blocks.


```python
class ImportAll(BrowserView):
def __call__(self):
# ...
logger.info("Start migrating richtext to blocks...")
migrate_richtext_to_blocks()
transaction.commit()
logger.info("Finished migrating richtext to blocks!")

view = api.content.get_view("migrate_to_volto", portal, request)
view.migrate_default_pages = True
view.slate = True
view.service_url = "http://localhost:5000/html"
logger.info("Start migrating Folders to Documents...")
view.do_migrate_folders()
transaction.commit()
logger.info("Finished migrating Folders to Documents!")
logger.info("Start migrating Collections to Documents...")
view.migrate_collections()
transaction.commit()
logger.info("Finished migrating Collections to Documents!")

```

## Final steps

* reset_dates
After everyting else was done you can reset the modification- and creation-dates of all migrated content.

```python
class ImportAll(BrowserView):
def __call__(self):
# ...
reset_dates = api.content.get_view("reset_dates", portal, request)
reset_dates()
```

Keep in mind though that in a very strict sense you are lying to the users since the migrated content was actually created and modified by the migration...


## Further reading

Expand Down
8 changes: 4 additions & 4 deletions docs/migrations/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ These changes are reflected in different releases of Plone.

The [release-schedule](https://plone.org/download/release-schedule) specifies, which versions are supported and for how long.

```{figure} https://plone.org/download/plonereleaseschedule-2022-09-12.png/@@images/image
```{figure} https://plone.org/download/release-schedule/plone-release-schedule-2022-12-13.png/@@images/image
:alt: The Plone release schedule
```

Expand All @@ -39,10 +39,10 @@ Security support
: Plone security hotfixes will be made available for this series.
For more information, see the [security update policy](https://plone.org/security/update-policy).

From the moment Plone 6 is released, Plone 6.0.x will be under maintenance support, and Plone 5.2.x will be under security support.
That means new features are being developed for Plone 6.1.x, and bugfixes are being developed for Plone 6.0.x.
From the moment Plone 6 was released, Plone 6.0.x was under maintenance support, and Plone 5.2.x will be under security support when Plone 6.1 is released.
That means new features are being developed for Plone 6.1.x while bugfixes are being developed for Plone 6.0.x.

Plone intends to provide stable releases (for example, 5.2.8 is a stable release of the 5.2.x series).
Plone intends to provide stable releases (for example, 5.2.14 is a stable release of the 5.2.x series).
Only bugfixes and changes that extend or improve an existing feature—as long as they don't break anything—make it into a bugfix release.


Expand Down
9 changes: 2 additions & 7 deletions docs/migrations/volto.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@ myst:

See the chapter {ref}`plone6docs:backend-migrate-to-volto-label` of the Plone Upgrade Guide.

Issues:
This explains mostly why you need to do what and how to use the form `@@migrate_to_volto` by hand.

* Complex HTML, such as in tables, is transformed to HTML blocks.
* There is no migration from DraftJS to Slate.
You can (and should) use the power of that feature in your exportimport-based migration as described in https://github.com/collective/collective.exportimport#migrate-to-volto

Alternatives:

* Start fresh
* Use `collective.exportimport` (TODO: https://github.com/collective/collective.exportimport/issues/133).
Binary file added docs/volto_customization/_static/variations.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 81 additions & 0 deletions docs/volto_customization/blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
myst:
html_meta:
"description": "How to extend volto blocks"
"property=og:description": "How to extend volto blocks"
"property=og:title": "Extend volto blocks"
"keywords": "Volto, Training, Extend block"
---

# Extend volto blocks

There are various ways of extending Volto blocks.
Component shadowing (see last chapter) is a very basic to customize components in volto.
But it comes with its own problems like keeping the shadowed component up to date with latest fixes and features of newer Volto versions.
Instead of shadowing components we can:

- Change the block-config
- Extend blocks by adding new block-variations
- Write add schemaEnhancer to modify blocks schema

Let us first change the View of the teaser block which we already have in volto core by changing the block-configuration.
In our addon `volto-teaser-tutorial` we will step by step extend each component that we have in volto core.

The most simple customization is the View of the Teaser. The volto core teaser block configration (in `omelette/src/config/Blocks.jsx`) looks like:

```{code-block} js
teaser: {
id: 'teaser',
title: 'Teaser',
icon: imagesSVG,
group: 'common',
view: TeaserViewBlock,
edit: TeaserEditBlock,
restricted: false,
mostUsed: true,
sidebarTab: 1,
blockSchema: TeaserSchema,
dataAdapter: TeaserBlockDataAdapter,
variations: [
{
id: 'default',
isDefault: true,
title: 'Default',
template: TeaserBlockDefaultBody,
},
],
},
```

Every block in Volto has Edit and View components.
You can customize these individually by either shadowing or directly in the confuguration (`index.js` of your addon) like this:

```{code-block} js
import MyTeaserView from 'volto-teaser-tutorial/components/Blocks/Teaser/View'
const applyConfig = (config) => {
config.blocks.blocksConfig.teaser.view = MyTeaserView
return config;
}
export default applyConfig;
```

Of course we need to add our custom `MyTeaserView` component in our addon.
From the root of the project that is `src/addon/volto-teaser-tutorial/src/components/Blocks/Teaser/View.jsx`:

```{code-block} jsx
import React from 'react';
import TeaserBody from '@plone/volto/components/manage/Blocks/Teaser/Body';
import { withBlockExtensions } from '@plone/volto/helpers';
const TeaserView = (props) => {
return <TeaserBody {...props} extraProps={{ foo: 'bar' }} />;
};
export default withBlockExtensions(TeaserView);
```

Here, the View component renders a TeaserBody which will be a result of an active variation, we will come to that in later chapters.
Loading

0 comments on commit 99e4a21

Please sign in to comment.