diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7d4082be3..52e99cf5f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,15 +9,11 @@ jobs: steps: - uses: actions/checkout@v7 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: "3.12" + - name: Install uv + uses: astral-sh/setup-uv@v9.0.0 - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install -q -r requirements.txt -c constraints.txt sudo snap install --edge vale - name: Run tests with make test (includes clean and linkcheckbroken) diff --git a/Makefile b/Makefile index 491842e38..30b3c2af2 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ distclean: ## Clean docs build directory and Python virtual environment cd $(DOCS_DIR) && rm -rf $(BUILDDIR)/ venv/bin/python: ## Set up training: Install requirements - python3 -m venv venv + uv venv --seed venv venv/bin/python -m pip install --upgrade pip venv/bin/pip install -r requirements.txt @echo diff --git a/docs/conf.py b/docs/conf.py index b7387b0d5..b2a147bf9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -180,7 +180,7 @@ }, ], "logo": { - "text": "Plone Training 2025", + "text": "Plone Training 2026", }, "navigation_with_keys": True, "path_to_docs": "docs", diff --git a/docs/contributing/writing-docs-guide.md b/docs/contributing/writing-docs-guide.md index 12a692876..f891ec619 100644 --- a/docs/contributing/writing-docs-guide.md +++ b/docs/contributing/writing-docs-guide.md @@ -46,10 +46,10 @@ The following are frequently used snippets and examples. ##### Link to a Chapter or Page ```md -We created an add-on in the last chapter {doc}`/mastering-plone/volto_custom_addon`. +We created an add-on in the last chapter {doc}`/mastering-plone/add-ons`. ``` -We created an add-on in the last chapter {doc}`/mastering-plone/volto_custom_addon`. +We created an add-on in the last chapter {doc}`/mastering-plone/add-ons`. (writing-docs-guide-link-heading-label)= diff --git a/docs/mastering-plone/_static/addons.png b/docs/mastering-plone/_static/addons.png new file mode 100644 index 000000000..8c94777db Binary files /dev/null and b/docs/mastering-plone/_static/addons.png differ diff --git a/docs/mastering-plone/_static/behaviors_frontend.png b/docs/mastering-plone/_static/behaviors_frontend.png index ba9a4c2c6..69a2f6dab 100644 Binary files a/docs/mastering-plone/_static/behaviors_frontend.png and b/docs/mastering-plone/_static/behaviors_frontend.png differ diff --git a/docs/mastering-plone/_static/features_control_panel.png b/docs/mastering-plone/_static/features_control_panel.png index 827a7e877..2eb676384 100644 Binary files a/docs/mastering-plone/_static/features_control_panel.png and b/docs/mastering-plone/_static/features_control_panel.png differ diff --git a/docs/mastering-plone/_static/frontpage_volto_logged_in.png b/docs/mastering-plone/_static/frontpage_volto_logged_in.png index 93803a2f2..bd9cd154e 100644 Binary files a/docs/mastering-plone/_static/frontpage_volto_logged_in.png and b/docs/mastering-plone/_static/frontpage_volto_logged_in.png differ diff --git a/docs/mastering-plone/_static/instructions_plone_running.png b/docs/mastering-plone/_static/instructions_plone_running.png index 9ad020be7..fff218390 100644 Binary files a/docs/mastering-plone/_static/instructions_plone_running.png and b/docs/mastering-plone/_static/instructions_plone_running.png differ diff --git a/docs/mastering-plone/about_mastering.md b/docs/mastering-plone/about_mastering.md index d91fc3001..578659487 100644 --- a/docs/mastering-plone/about_mastering.md +++ b/docs/mastering-plone/about_mastering.md @@ -3,45 +3,44 @@ myst: html_meta: "description": "What to expect. Trainings on site. Training videos." "property=og:description": "What to expect. Trainings on site. Training videos." - "property=og:title": "About Mastering Plone development" + "property=og:title": "About Mastering Plone Development" "keywords": "Plone, training, develop" --- (about-mastering-label)= -# About Mastering Plone development +# About Mastering Plone Development -The *Mastering Plone Development* training is both: an online training and a handout for on-site trainings. +The *Mastering Plone Development* training is both an online training and a handout for on-site trainings. We address developers. You are interested in best practice guides and want to learn the why and where, so read on! ## The beginning and the continuation -This training was initially started as a Plone Classic training and evolved now to a roundtrip on both: Plone backend, React frontend and the interplay between backend and frontend. +This training was initially started as a Plone Classic training and evolved now to a roundtrip including the Plone backend, the Plone Volto frontend, and the interplay between backend and frontend. This training is published on [training.plone.org](https://training.plone.org/). -We update the training ongoing to teach the best practices. +We update the training over time to teach the best practices. You are welcome to contribute and give feedback on [GitHub issues](https://github.com/plone/training/issues). -Thank you for reading and your charity with non-native English speakers / writers. +Thank you for reading and your charity with non-native English speakers and writers. (about-upcoming-label)= ## Upcoming trainings -Next Plone Conference, usually in October. -Announced on [plone.org](https://plone.org) +The Mastering Plone Development training will be offered September 21-22, 2026 as part of the [2026 Plone Conference](https://2026.ploneconf.org) in Maastricht. -If you want to have an on-site training or want to attend a public training, please ask for trainings on [community.plone.org](https://community.plone.org) or one of the {ref}`trainers`. +If you want to have an on-site training or want to attend a public training, please ask for trainings on [community.plone.org](https://community.plone.org) or contact one of the {ref}`trainers`. (video-label)= ## Watch the training video -At Ploneconf 2021 the Mastering Plone Development training has been recorded. +At Ploneconf 2021 the Mastering Plone Development training was recorded. It's a shortened version of the training, given by Philip Bauer and Katja Süss. [Part 1 – Mastering Plone Development Training](https://www.youtube-nocookie.com/embed/Jzuw3uWtzzI?privacy_mode=1) diff --git a/docs/mastering-plone/add-ons.md b/docs/mastering-plone/add-ons.md index 525f953c2..df4ca0326 100644 --- a/docs/mastering-plone/add-ons.md +++ b/docs/mastering-plone/add-ons.md @@ -3,22 +3,37 @@ myst: html_meta: "description": "Extending Plone with features via existing backend add-ons" "property=og:description": "Extending Plone with features via existing backend add-ons" - "property=og:title": "Extending Plone with add-on packages" + "property=og:title": "Extend Plone with add-on packages" "keywords": "Plone, Volto, add-on, customizing" --- (add-ons-label)= -# Extending Plone with add-on packages +# Extend Plone with add-on packages ```{card} -Backend chapter -For frontend add-ons see chapter {ref}`volto-addon-label`. +In this chapter you will learn how to select and install Plone add-ons. -The creation of a custom backend add-on is explained in {doc}`voting-story/index` +The creation of a custom add-on is explained in {doc}`voting-story/index` ``` +````{card} + +Check out `mastering-plone-project` at tag `initial`: + +```shell +git checkout initial +``` + +The code at the end of the chapter: + +```shell +git checkout addons +``` + +More info in {doc}`code` +```` Plone add-ons enrich the CMS by @@ -29,7 +44,7 @@ Plone add-ons enrich the CMS by - customizing the editor experience - adding content assembling features for overview pages -Plone 6 knows two groups of add-ons: add-ons for Plone and such for the frontend Volto. +Plone has two groups of add-ons: add-ons for the Plone backend and add-ons for the frontend Volto. Plone backend add-ons provide @@ -37,20 +52,14 @@ Plone backend add-ons provide - behaviors to enrich content types Plone frontend add-ons provide + - new blocks - variations and enhancements of blocks - a theme to design the layout of a site - components independent of blocks like a dropdown navigation -Both can be coupled, if a frontend feature depends on a new content type, a new behavior or any other change needed in data structure. -For example an add-on that has the goal to provide a bookmarking feature depends on a backend add-on that handles the storing of the bookmarks. - -Have a look at the curated lists of add-ons: - - -[Awesome Plone: backend add-ons](https://github.com/collective/awesome-plone/blob/main/README.md) -[Awesome Volto: frontend add-ons](https://github.com/collective/awesome-volto/blob/main/README.md) - +Both can be coupled, if a frontend feature depends on a new content type, a new behavior, or any other change needed in data structure. +For example, an add-on that has the goal to provide a bookmarking feature depends on a backend add-on that handles the storing of the bookmarks. (add-ons-find-label)= @@ -64,8 +73,8 @@ Here are some tips. - Find candidates on PyPI, npm or GitHub: - - curated list of [backend add-ons](https://github.com/collective/awesome-plone/blob/main/README.md) - - curated list of [frontend add-ons](https://github.com/collective/awesome-volto#readme) + - curated list of [awesome backend add-ons](https://github.com/collective/awesome-plone/blob/main/README.md) + - curated list of [awesome frontend add-ons](https://github.com/collective/awesome-volto#readme) - Python packages on PyPI: - Plone add-ons on GitHub: - Plone core packages on GitHub: @@ -93,93 +102,104 @@ Here are some tips. - Either extend an existing add-on to fit your needs or create a new add-on that does exactly what you need. -(add-ons-installing-label)= +(add-ons-install-form-block-label)= -## Installing Plone add-ons +## Example: the form block add-on -We have two groups of add-ons: backend and frontend. +For our case study, it would be nice to have a contact form to send questions to the conference organizers. +We can use the [Plone form block add-on](https://github.com/plone/form-block) for this. -The training setup starts without any frontend add-on. -Later on we will add features via a frontend add-on. -See chapter {doc}`volto_addon` how to install a frontend add-on. +It is released in two packages: -The training setup starts with one backend add-on `ploneconf.site`. -Let's see how it is installed. +- [`plone.formblock`](https://pypi.org/project/plone.formblock/) is the backend add-on +- [`@plone/volto-form-block`](https://www.npmjs.com/package/@plone/volto-form-block) is the frontend add-on +We have to add both of these to our project. -### Making a backend add-on package available to Zope +## Install the backend add-on -First, we must make the add-on package available to Zope. -This means that Zope can import the code. +First, we must add the backend add-on as a dependency, so that its code will be available. -A backend add-on is a Python package. -Therefore we install it with pip. +Edit the file {file}`backend/pyproject.toml` and add `plone.formblock` to the `dependencies`: -Look at the {file}`requirements.txt` file. -You add a package to the configuration by adding a new line containing the package name. +```{code-block} toml +:linenos: +:emphasize-lines: 6 -If the add-on is not released on [PyPI](https://pypi.org/), we tell Zope where to find the package on `Github` or another repository platform by including the necessary information in {file}`mx.ini`. - -```ini -[training.votable] -url=git@github.com:collective/training.votable.git -branch=main -; tag=volto +dependencies = [ + "Products.CMFPlone==6.2.1", + "plone.api", + "plone.restapi", + "plone.volto", + "plone.formblock==1.0.0a3", +] ``` -Adding the package to {file}`instance.yaml` causes the generation of the Zope configuration to make the package available in a Zope app. - -```yaml -zcml_package_includes: training.votable, ploneconf.site -``` +It's a good idea to "pin" the add-on to a specific version, to make sure that it won't get accidentally upgraded when you don't expect it, if there is a new release of the add-on in the future. -Running `make build` has three effects: -- The build installs the python package with `pip`. -- The build generates in `instance/` a Zope instance configuration that makes the package available in our Zope app. -- As soon as the Zope app is started via `make start`, the add-on can be enabled per Plone instance. - A Zope app can include multiple Plone instances. - So an add-on can be enabled per Plone instance. +Now re-install the project with the new dependencies: -```{seealso} -Documentation {doc}`plone6docs:admin-guide/add-ons` +```shell +make backend-install ``` +This runs `uv sync` which updates the Python virtual environment with the dependencies listed in `pyproject.toml`. +Now when the backend is restarted, the code for the add-on is available. -### Enabling add-ons in your Plone site - -An add-on can be enabled per Plone instance. +Backend add-ons usually also need to be installed in a specific Plone site. -In your browser, go to `Site Setup` at `http://localhost:3000/controlpanel`, and open the `Add-ons` control panel. You will see a list of available add-ons. +In your browser, go to `Site Setup` at `http://localhost:3000/controlpanel`, and open the `Add-ons` control panel. +You will see a list of available add-ons. +Click to install the form block add-on. ````{card} -```{image} ../_static/site_setup.png +```{image} _static/addons.png :alt: Plone `Add-ons` control panel, showing available configuration options -:target: ../_static/site_setup.png ``` +++ -_Plone `Add-ons` control panel, showing available configuration options._ +_Add-ons control panel, showing available configuration options._ ```` -Enable `ploneconf.site` now if you haven't done already. +```{seealso} +Documentation {doc}`plone6docs:admin-guide/add-ons` +``` -This is what happens: The GenericSetup profile of the product gets loaded. This does things like: +## Install the frontend add-on -- Registering new content types -- Registering behaviors -- Configuring new actions -- Create catalog indexes +We also need to install the code for the frontend add-on. +Update the `addons` and `dependencies` in {file}`frontend/packages/volto-ploneconf-site/package.json`: -All this is configured in the default GenericSetup profile, which can be found in `backend/sources//src//profiles/default`. -In the next chapters we will add here our content type `talk`, configure a catalog index, and some more. +```{code-block} json +:emphasize-lines: 2, 5 +"addons": [ + "@plone/volto-form-block" +], +"dependencies": { + "@plone/volto-form-block": "^1.0.0-alpha.0", +}, +``` -(add-ons-summary-label)= +```{tip} +`dependencies` tells the package manager `pnpm` to install the code. +`addons` tells Volto to load the add-on's configuration. +``` -## Summary +Now re-install the frontend with the new dependencies: -We have seen in short how to extend a vanilla Plone website with third party add-ons to add new functionality. -Even if you do not use many of these, they are nonetheless an inspiration on how to implement features in Plone. +```shell +make frontend-install +``` + +After you restart the frontend, you should be able to add a form block. + +```{tip} +To confirm that the frontend add-on is installed, go to http://localhost:3000/controlpanel and look at the list of Add-ons at the bottom. +``` +(add-ons-summary-label)= +## Summary -For frontend add-ons see chapter {ref}`volto-addon-label` +We have seen in short how to extend a vanilla Plone website with third party add-ons to add new functionality. +Even if you do not use many of these, they can be useful examples of how to implement features in Plone. diff --git a/docs/mastering-plone/api.md b/docs/mastering-plone/api.md index 53df399d1..a5b884049 100644 --- a/docs/mastering-plone/api.md +++ b/docs/mastering-plone/api.md @@ -11,7 +11,7 @@ myst: # Programming Plone -```{card} Backend chapter +```{card} In this part you will: @@ -28,21 +28,25 @@ Tools and techniques covered: (api-api-label)= -## plone.api +## `plone.api` -The most important tool nowadays for plone developers is the add-on {doc}`plone6docs:plone.api/index` that covers 20% of the tasks any Plone developer does 80% of the time. If you are not sure how to handle a certain task, be sure to first check if `plone.api` has a solution for you. +The most important tool nowadays for backend Plone developers is the add-on {doc}`plone6docs:plone.api/index` that covers 20% of the tasks any Plone developer does 80% of the time. +If you are not sure how to handle a certain task, be sure to first check if `plone.api` has a solution for you. -The API is divided in five sections. Here is one example from each: +The API is divided in a number of sections. Here is one example from each: -- `Content:` {ref}`plone6docs:content-create-example` -- `Portal:` {ref}`plone6docs:portal-send-email-example` -- `Groups:` {ref}`plone6docs:group-grant-roles-example` -- `Users:` {ref}`plone6docs:user-get-roles-example` -- `Environment:` {ref}`plone6docs:env-adopt-roles-example` +- Content: {ref}`plone6docs:content-create-example` +- Portal: {ref}`plone6docs:portal-send-email-example` +- Groups: {ref}`plone6docs:group-grant-roles-example` +- Users: {ref}`plone6docs:user-get-roles-example` +- Environment: {ref}`plone6docs:env-adopt-roles-example` +- Add-ons: {ref}`plone6docs:addons-get-addons` +- Relations: {ref}`plone6docs:relation-get-example` -{py:mod}`plone.api` is a great tool for integrators and developers that is included when you install Plone, though for technical reasons it is not used by the code of Plone itself. +{py:mod}`plone.api` is a great tool for integrators and developers that is included when you install Plone, though for technical reasons it is not used by the internal code of Plone itself. -In existing code you'll often encounter methods that don't mean anything to you. You'll have to use the source to find out what they do. +In existing code you'll often encounter methods that don't mean anything to you. +You'll have to use the source to find out what they do. Some of these methods are replaced by {py:mod}`plone.api`: @@ -51,28 +55,29 @@ Some of these methods are replaced by {py:mod}`plone.api`: (api-portal-tools-label)= -## portal tools +## Portal tools -Some parts of Plone are very complex modules in themselves (e.g. the versioning machinery of {py:mod}`Products.CMFEditions`). +Some parts of Plone are very complex modules in themselves (for example, the versioning machinery of {py:mod}`Products.CMFEditions`). Most of them have an API of themselves that you will have to look up when you need to implement a feature that is not covered by {py:mod}`plone.api`. Here are a few examples: -portal_catalog +`portal_catalog` : {py:meth}`unrestrictedSearchResults()` returns search results without checking if the current user has the permission to access the objects. : {py:meth}`uniqueValuesFor()` returns all entries in an index -portal_setup +`portal_setup` : {py:meth}`runAllExportSteps()` generates a tarball containing artifacts from all export steps. -Products.CMFPlone.utils +`Products.CMFPlone.utils` : {py:meth}`is_product_installed()` checks if a product is installed. -Usually the best way to learn about the API of a tool is to look in the {file}`interfaces.py` in the respective package and read the `docstrings`. But sometimes the only way to figure out which features a tool offers is to read its code. +Usually the best way to learn about the API of a tool is to look in the {file}`interfaces.py` in the respective package and read the docstrings. +But sometimes the only way to figure out which features a tool offers is to read its code. To use a tool, you usually first get the tool with {py:mod}`plone.api` and then invoke the method. @@ -84,7 +89,11 @@ mt.logoutUser(request) ``` ```{note} -The code for {py:meth}`logoutUser()` is in {py:meth}`Products.PlonePAS.tools.membership.MembershipTool.logoutUser`. Many tools that are used in Plone are actually subclasses of tools from the package {py:mod}`Products.CMFCore`. For example `portal_membership` is subclassing and extending the same tool from {py:class}`Products.CMFCore.MembershipTool.MembershipTool`. That can make it hard to know which options a tool has. There is an ongoing effort by the Plone Community to consolidate tools to make it easier to work with them as a developer. +The code for {py:meth}`logoutUser()` is in {py:meth}`Products.PlonePAS.tools.membership.MembershipTool.logoutUser`. +Many tools that are used in Plone are actually subclasses of tools from the package {py:mod}`Products.CMFCore`. +For example `portal_membership` is subclassing and extending the same tool from {py:class}`Products.CMFCore.MembershipTool.MembershipTool`. +That can make it hard to know which options a tool has. +There is an ongoing effort by the Plone Community to consolidate tools to make it easier to work with them as a developer. ``` (api-debugging-label)= @@ -97,7 +106,7 @@ We use some of them in various situations during the training. tracebacks and the log : The log (and the console when running in foreground) collects all log messages Plone prints. -When an exception occurs, Plone throws a traceback. +When an exception occurs, Plone logs a traceback. Most of the time the traceback is everything you need to find out what is going wrong. Also adding your own information to the log is very simple. : ```python @@ -112,8 +121,8 @@ Also adding your own information to the log is very simple. pdb : The `Python` debugger `pdb` is the single most important tool for us when programming. -Just add `import pdb; pdb.set_trace()` in your code and debug away! -The code execution stops at the line you added `import pdb; pdb.set_trace()`. +Just add `breakpoint()` in your code and debug away! +The code execution stops at the line you added `breakpoint()`. Switch to your terminal and step through your code. @@ -123,13 +132,16 @@ pdbpp ipdb -: Another enhanced pdb with the power of IPython, e.g. tab completion, syntax highlighting, better tracebacks and introspection. It also works nicely with {py:mod}`Products.PDBDebugMode`. Needs to be invoked with `import ipdb; ipdb.set_trace()`. +: Another enhanced pdb with the power of IPython, e.g. tab completion, syntax highlighting, better tracebacks and introspection. +It also works nicely with {py:mod}`Products.PDBDebugMode`. +Needs to be invoked with `import ipdb; ipdb.set_trace()`. Products.PDBDebugMode : An add-on that has two killer features. - **Post-mortem debugging**: throws you in a pdb whenever an exception occurs. This way you can find out what is going wrong. + **Post-mortem debugging**: throws you in a pdb whenever an exception occurs. + This way you can find out what is going wrong. **pdb view**: simply adding `/pdb` to a url drops you in a pdb session with the current context as {py:obj}`self.context`. From there you can do just about anything. @@ -137,16 +149,17 @@ Interactive debugger : Start your instance in debug mode with ```shell - venv/bin/zconsole debug instance/etc/zope.conf + cd backend + make console ``` You have an interactive debugger at your fingertips. `app.Plone` is your Plone instance object which you can inspect on the command line. -: To list the ids of the objects in a folderish object: +: To list the ids of the objects inside a container: ```shell >>> app.Plone.talks.keys() ['whats-new-in-python-3.10', 'plone-7', 'zope', 'betty-white', 'new-years-day', 'journey-band'] ``` -: To list the items of a folderish object: +: To list the items of a container: ```shell >>> from zope.component.hooks import setSite >>> setSite(app.Plone) @@ -161,7 +174,8 @@ The component registry is needed for methods like `contentItems` which look up u plone.app.debugtoolbar -: An add-on that allows you to inspect nearly everything. It even has an interactive console, a tester for TALES-expressions and includs a reload-feature like {py:mod}`plone.reload`. +: An add-on that allows you to inspect nearly everything. +It even has an interactive console, a tester for TALES-expressions and includes a reload-feature like {py:mod}`plone.reload`. plone.reload @@ -180,7 +194,7 @@ Products.PrintingMailHost Sentry : [Sentry](https://github.com/getsentry/sentry) is an error logging application you can host yourself. -It aggregates tracebacks from many sources and (here comes the killer feature) even the values of variables in the traceback. We use it in all our production sites. +It aggregates tracebacks from many sources and (here comes the killer feature) even the values of variables in the traceback. ```{seealso} @@ -191,26 +205,27 @@ It aggregates tracebacks from many sources and (here comes the killer feature) e ## Exercise 1 -Knowing that `venv/bin/zconsole debug instance/etc/zope.conf` basically offers you a Python prompt to inspect your Plone instance, how would you start to explore Plone? +Knowing that `make console` basically offers you a Python prompt to inspect your Plone instance, how would you start to explore Plone? ```{admonition} Solution :class: toggle Use `locals()` or `locals().keys()` to see Python objects available in Plone -You will get notified that `app` is automatically bound to your Zope application, so you can use dictionary-access or attribute-access as explained in {doc}`what_is_plone` to inspect the application: +You will get notified that `app` is automatically bound to your Zope application, so you can use dictionary access or attribute access as explained in {doc}`what_is_plone` to inspect the application. ``` ## Exercise 2 -The `app` object you encountered in the previous exercise can be seen as the root of Plone. Once again using Python, can you find your newly created Plone site? +The `app` object you encountered in the previous exercise can be seen as the root of Plone. +Once again using Python, can you find your newly created Plone site? `````{admonition} Solution :class: toggle `app.keys()` will show `app`'s attribute names - there is one called `Plone`, this is your Plone site object. Use `app.Plone` to access and further explore it. -```pycon +```python >>> app >>> app.keys() @@ -262,7 +277,9 @@ You have been warned. Use the documentation at {doc}`plone6docs:backend/global-utils` to find an overview of `plone_view` helpers. ```{note} -- Do not try everything at once, work in small iterations, restart your Plone instance to check your results frequently. +- Do not try everything at once. + Work in small iterations. + Restart your Plone instance to check your results frequently. - Use `pdb` during development to experiment. ``` @@ -334,7 +351,7 @@ class DemoContent(BrowserView): Some notes: -- Since calling view is a GET and not a POST we need {py:meth}`alsoProvides(self.request, IDisableCSRFProtection)` to allow write-on-read without Plone complaining. +- Since requesting the view is a GET and not a POST we need {py:meth}`alsoProvides(self.request, IDisableCSRFProtection)` to allow write-on-read without Plone complaining. Alternatively we could create a simple form and create the content on submit. - {ref}`plone6docs:content-transition-example` has two modes of operation: diff --git a/docs/mastering-plone/appendix.md b/docs/mastering-plone/appendix.md new file mode 100644 index 000000000..ca80ed060 --- /dev/null +++ b/docs/mastering-plone/appendix.md @@ -0,0 +1,18 @@ +--- +myst: + html_meta: + "description": "Meta information about the Mastering Plone Development training" + "property=og:description": "Meta information about the Mastering Plone Development training" + "property=og:title": "Appendix" + "keywords": "Plone, backend" +--- + +(appendix-label)= + +# Appendix + +```{toctree} + +code +trainer +``` diff --git a/docs/mastering-plone/behaviors_1.md b/docs/mastering-plone/behaviors_1.md index d62d5969a..bd4ef5523 100644 --- a/docs/mastering-plone/behaviors_1.md +++ b/docs/mastering-plone/behaviors_1.md @@ -11,8 +11,6 @@ myst: # Behaviors -Enhance content types to be selectable for presentation on the front page. - ```{card} In this part you will: @@ -25,12 +23,12 @@ Tools and techniques covered: - Catalog indexes and catalog metadata columns ``` -````{card} Backend chapter +````{card} -Checkout `ploneconf.site` at tag "talks": +Check out `mastering-plone-project` at tag `talkview`: ```shell -git checkout talks +git checkout talkview ``` The code at the end of the chapter: @@ -42,25 +40,24 @@ git checkout behaviors_1 More info in {doc}`code` ```` + +## Why behaviors? + ```{only} not presentation A first approach would be to extend the functionality of a content type by writing an adapter that adapts an object of this type to add an additional attribute or feature. This would mean to write an adapter for an interface the respective content types provides. But for which interface shall we write the adapter? Do we want to write it for the general {py:class}`Products.CMFCore.interfaces.IContentish` which is implemented by all content types? -We want to be more specific and provide the behavior only for some selected content types. +No, we want to be more specific and provide the behavior only for some selected content types. ``` -(behaviors1-dexterity-label)= - -## Dexterity Approach - -Dexterity has special adapters that are called and registered by the name behavior. +Plone has special adapters that are called and registered by the name _behavior_. A behavior can be enabled for any content type through the web UI and at runtime. -All default views (for example the add and edit forms) know about the concept of behaviors. -When rendering forms, the views check whether there are behaviors referenced with the current context and if these behaviors have a schema of their own, these fields get shown in addition. +All default forms (for example the add and edit forms) know about the concept of behaviors. +When rendering forms, they check whether there are behaviors referenced with the current context and if these behaviors have a schema of their own, these fields are also added to the form. (behaviors1-names-label)= @@ -73,14 +70,14 @@ But it is a good idea to think of a behavior as an aspect. You are adding an aspect to your content type and you want to write your aspect in such a way that it works independently of the content type on which the aspect is applied. You should not have dependencies to specific fields of your type or to other behaviors. -Such an object allows you to apply the [open/closed principle](https://en.wikipedia.org/wiki/Open/closed_principle) to your dexterity objects. +Such an object allows you to apply the [open/closed principle](https://en.wikipedia.org/wiki/Open/closed_principle) to your content types. ``` (behaviors1-example-label)= ## Practical example -```{note} +```{tip} We write the behavior code step by step, but you can also use the Plone Command Line Tool `plonecli` to initially create a behavior and edit it afterwards. ``` @@ -92,28 +89,28 @@ We want some selected talks, news items or other content to be presented on the So for now, our behavior just adds a new field to store the information if an object should be listed on the front page. ``` -We want to keep a clean structure, so we create a {file}`behaviors` directory first, and include it into the ZCML declarations of our {file}`configure.zcml`. +We want to keep a clean structure, so we create a {file}`backend/src/ploneconf/site/behaviors` directory first, and include it into the ZCML declarations of our {file}`backend/src/ploneconf/site/configure.zcml`. ```xml ``` -Then, we add an empty {file}`behaviors/__init__.py` and a {file}`behaviors/configure.zcml` containing +Then, we add an empty {file}`backend/src/ploneconf/site/behaviors/__init__.py` and a {file}`backend/src/ploneconf/site/behaviors/configure.zcml` containing (featured-behavior-zcml-label)= ```{code-block} xml -:emphasize-lines: 6-11 :linenos: + i18n_domain="ploneconf.site" + > @@ -121,31 +118,31 @@ Then, we add an empty {file}`behaviors/__init__.py` and a {file}`behaviors/confi ``` -And a {file}`behaviors/featured.py` containing: +And a {file}`backend/src/ploneconf/site/behaviors/featured.py` containing: (featured-behavior-python-label)= ```{code-block} python :linenos: +from plone import schema from plone.autoform.interfaces import IFormFieldProvider from plone.supermodel import model from plone.supermodel.directives import fieldset -from plone import schema from zope.interface import provider + @provider(IFormFieldProvider) class IFeatured(model.Schema): - featured = schema.Bool( - title='Show this item on the frontpage', + title="Show this item on the frontpage", required=False, ) fieldset("Options", fields=["featured"]) ``` This is exactly the same type of schema as the one in the talk content-type. -The only addition is `@provider(IFormFieldProvider)` that makes sure that the fields in the schema are displayed in the add- and edit-forms. +The only addition is `@provider(IFormFieldProvider)` that makes sure that the fields in the schema are displayed in the add and edit forms. Let's go through this step by step. @@ -154,34 +151,40 @@ Let's go through this step by step. You do this through the web or in the GenericSetup profile. 2. We create an interface in {ref}`behaviors/featured.py ` for our behavior. We make it also a schema containing the fields we want to declare. - We could just define schema fields on a zope.interface class, but we use an extended form from {py:mod}`plone.supermodel`, else we could not use the fieldset features. + We could just define schema fields on a zope.interface class, but we use an extended form from {py:mod}`plone.supermodel`. + Otherwise we could not use the fieldset features. 3. We mark our schema as a class that also provides the {py:class}`IFormFieldProvider` interface using a decorator. The schema class itself provides the interface, not its instance! 4. We also add a `fieldset` so that our field is not mixed with the normal fields of the object. 5. We add a normal [Bool](https://zopeschema.readthedocs.io/en/latest/api.html#zope.schema.interfaces.IBool) schema field to control if an item should be displayed on the front page. ```{note} -For simplicity we do not use the so called `AnnotationStorage`. -The value of the field "featured" is saved on the object. -Imagine an add-on that unfortunately uses the same field name "featured" for another purpose than `ploneconf.site`. -Here the AnnotationStorage comes in. -The object is equipped by a storage where behaviors do store values with a key unique per behavior. +For simplicity we do not add an adapter to change where the field values are stored. +The value of the field `featured` is saved on the object. +Imagine an add-on with another behavior that unfortunately uses the same field name `featured` for another purpose than `ploneconf.site`. +That problem could be solved by using an adapter to get and set the field value in a different place. -Furthermore a `marker interface` is needed as soon as we want to register components for objects that do adapt this behavior, e.g. REST API endpoints. +Furthermore a _marker interface_ is needed as soon as we want to register components for objects that do adapt this behavior, e.g. REST API endpoints. -We will see `marker interfaces` and `AnnotationStorages` in chapter {doc}`./voting-story/behaviors_2`. +We will see marker interfaces and behavior adapters in chapter {doc}`./voting-story/behaviors_2`. ``` (behaviors1-adding-label)= -## Enabling the behavior on our talk +## Enable the behavior on our talk -```{only} not presentation -We could add this behavior now via the plone control panel "content types". -But instead, we will do it directly and properly in a content types `GenericSetup` profile. +````{only} not presentation +We could add this behavior now via the Content Types control panel in Site Setup. +But instead, we will do it programmatically in a Generic Setup profile. + +```{tip} +Making changes programmatically is preferred for all but the simplest Plone sites. +It helps keep things consistent between your development copy of the site and a production deployment. ``` -We add the behavior to {file}`profiles/default/types/talk.xml`: +```` + +We add the behavior to {file}`backend/src/ploneconf/site/profiles/default/types/talk.xml`: ```{code-block} xml :emphasize-lines: 8 @@ -209,76 +212,90 @@ After a restart and the reinstallation of the product we now have the new field (behaviors1-index-label)= -## Add an index for the new field +## Add an index -To use this new "featured" information in searches and listings, we have to add an index to the `plone_catalog`. +To use this new "featured" information in searches and listings, we have to add an index to the `portal_catalog`. Indexing is the action to make object data searchable. Plone stores available catalog indexes in the database. ```{note} -You can inspect existing indexes in `portal_catalog` on "Index" tab . +You can inspect existing indexes in the ZMI at `portal_catalog` on the "Index" tab . ``` First of all we have to decide which kind of index we need for our new field. Common index types are: -- FieldIndex stores values as is -- BooleanIndex stores boolean values as is -- KeywordIndex allows keyword-style look-ups (query term is matched against all the values of a stored list) -- DateIndex and DateRangeIndex store dates in searchable format. +- **FieldIndex** stores values as is +- **BooleanIndex** stores boolean values as is +- **KeywordIndex** allows keyword-style look-ups (query term is matched against all the values of a stored list) +- **DateIndex** and **DateRangeIndex** store dates efficiently. The latter provides ranged searches. Because we have a boolean field for the featured information, it is obvious to use the BooleanIndex for this. -To add a new index we have to change the `catalog.xml` in the `profiles/default` folder of our product. Without changes the file does look like this: +To add a new index we have to change the `catalog.xml` in the `backend/src/ploneconf/site/profiles/default` folder of our project. +Without changes the file looks like this: ```{code-block} xml :linenos: - + - + + ``` To add the new BooleanIndex to the file we have to change the file as following: ```{code-block} xml -:emphasize-lines: 3-5 +:emphasize-lines: 3-7 :linenos: - + - - + + ``` To understand this snippet we have to understand the tags and information we are using: -- The `index` tag will tell the `plone_catalog` that we want to add a new index. +- The `index` tag will tell the `portal_catalog` that we want to add a new index. - `name` will be shown in the overview of `portal_catalog` and can be used in listings and searches later on. - `meta_type` determines the type of index we want to use. - The `indexed_attr` includes the field name of the information we are going to save in the index. + It happens to be the same as the index name in this case, but it doesn't have to be. After a restart and reinstallation of the product, a new index is created in the `portal_catalog`. -```{note} -Instead of de-installing and installing in the `Add-Ons` control panel, we can import new or altered XML files in the `ZMI`. To do so go to `portal_setup`, switch to the `Import`-Tab and search for the profile to import like in this case: `ploneconf.site`. +```{tip} +Instead of uninstalling and re-installing in the `Add-Ons` control panel, we can import new or altered XML files in the `ZMI`. +To do so go to `portal_setup`, switch to the `Import` tab and search for the profile to import. In this case: `ploneconf.site`. ``` To see if the adding was successful, we open the ZMI of our Plone site and navigate to the `portal_catalog` and click the `Indexes` tab. The new index `featured` should now be listed. -As soon as you edit content, you can also see the values of "featured" listed on "Browse" tab. +As soon as you edit content, you can also see the values of "featured" listed on the catalog's "Browse" tab. (behaviors1-metadata-label)= -## Add a metadata column for the new field +## Add a metadata column The same rules and methods shown above for indexes apply for metadata columns. -The difference with metadata is that it is not used as criteria for searching the catalog, but is mandatory for displaying of search results returned from the catalog. +The difference with metadata is that it is not used as criteria for searching the catalog, but is mandatory for displaying search results returned from the catalog. We will see that in fact every attribute of an object can be accessed in search results by explicitly requesting objects. A way more performant search is requesting what is stored in the catalog. @@ -287,13 +304,15 @@ And this is exactly the metadata. To add a metadata column for "featured", we have to add one more line in the `catalog.xml` like this: ```{code-block} xml -:emphasize-lines: 6 +:emphasize-lines: 8 :linenos: - + - - + + diff --git a/docs/mastering-plone/case.md b/docs/mastering-plone/case.md index 8cf4ba730..242006625 100644 --- a/docs/mastering-plone/case.md +++ b/docs/mastering-plone/case.md @@ -3,13 +3,13 @@ myst: html_meta: "description": "Our training story" "property=og:description": "Our training story" - "property=og:title": "The Case Study" + "property=og:title": "The case study" "keywords": "Plone, training" --- (case-label)= -# The Case Study +# The case study (case-background-label)= @@ -62,6 +62,7 @@ During the course of the training you will solve the following tasks. - Create users and organize them - Configure some basic settings of the website - Create content with info about the conference using the default features +- Create a contact form using the form block add-on - Create a Plone add-on to hold our own python code in a backend add-on - Create a content type 'talk' to store all the data required for a talk - Create a view to display a talk in a nice way diff --git a/docs/mastering-plone/code.md b/docs/mastering-plone/code.md index 622abfb53..36ec54ea5 100644 --- a/docs/mastering-plone/code.md +++ b/docs/mastering-plone/code.md @@ -14,58 +14,40 @@ myst: You can get the complete code for this training from GitHub. See {doc}`installation`. -The backend add-on [ploneconf.site](https://github.com/collective/ploneconf.site) is included in the backend setup of [Training setup Mastering Plone Development](https://github.com/collective/training_buildout). +The main training project is [mastering-plone-project](https://github.com/collective/mastering-plone-project). -The frontend code can be found at [volto-ploneconf](https://github.com/collective/volto-ploneconf). +The add-on developed in chapter 31 is [mastering-plone-votable-add-on](https://github.com/collective/mastering-plone-votable-add-on). -Further add-ons are build or used while stepping through advanced training chapters. -For the sake of completion we are mentioning them here. -There is no need to check them out as they are dependencies in backend or frontend. -They will be added by name in backend configuration or frontend configuration, than fetched by building the backend or the frontend. +Both of these repositories are monorepos which contain: +- a backend package +- a frontend package +- Makefile commands to use during development +- configuration for automatic continuous integration workflows on GitHub -- [training.votable](https://github.com/collective/training.votable) -- [volto-training-votable](https://github.com/collective/volto-training-votable) - -## The code-packages - -The add-on package [ploneconf.site](https://github.com/collective/ploneconf.site) contains the complete backend code for this training excluding exercises. -It is automatically downloaded from GitHub when you run `make build` in your Plone backend set up from {doc}`installation`. - -The frontend app [volto-ploneconf](https://github.com/collective/volto-ploneconf) holds the code for the frontend excluding exercises. -As explained in {doc}`installation`, it is to be installed side by side with the backend in a folder `/frontend/`. -Optional frontend add-ons are configured here in `packages.json`. +## Get the code for a particular chapter The default branches of these repositories hold the code of the final chapter of the training. Each chapter that adds code to the package has a tag that can be used to get the code for that chapter. -## Getting the code for a certain chapter - -To use the code for a certain chapter you need to checkout the appropriate tag for the chapter. -The package will then contain the complete code for that chapter excluding exercises. +To use the code for a certain chapter, you need to checkout the appropriate tag for the chapter. +The package will then contain the complete code for that chapter (excluding exercises). -If you want to add the code for the chapter yourself you have to checkout the tag of the previous chapter. - -Here is an example: - -```shell -git checkout views_2 -``` +If you want to add the code for the chapter yourself, you have to check out the tag of the previous chapter. +Each chapter has a card at the top which tells you which tags to use. +The full list of tags is also shown in a table below. -The names of the tags are the same as the URL of the chapter. -The tag for the chapter {doc}`/mastering-plone/registry` is `registry`. -You can get it with {command}`git checkout registry`. ### How to check out a tag of a git repository -You have two options: console or source editor (for example VSCode). +You have two options: terminal or source editor (for example VSCode). -1. Console - - Change to the location of the repository (`backend/sources/ploneconf.site` or `frontend`). - - To checkout tag 'talks': `git checkout talks` +1. Terminal + - `cd` to the folder which contains the repository (`mastering-plone-project`). + - To check out the tag `talks`: `git checkout talks` 2. VSCode - - Open `backend/sources/ploneconf.site` or `frontend` in VSCode. - - If not installed, install extension "GitHub Pull Requests". + - Open `mastering-plone-project` in VSCode. + - If not installed, install the extension "GitHub Pull Requests". - Open the source control sidebar. - Select the tag. ```{figure} _static/vscode_git.png @@ -73,18 +55,18 @@ You have two options: console or source editor (for example VSCode). ``` -## Moving from chapter to chapter +## Move from chapter to chapter -To change the code to the state of the next chapter checkout the tag for the next chapter: +To change the code to the state of the next chapter, check out the tag for the next chapter: ```shell git checkout views_3 ``` -If you made any changes to the code you have to get them out of the way first. This involves two things. +If you made any changes to the code, you have to get them out of the way first. This involves two things. ```{warning} -Make sure you have no new files or changes in the folder structure of `ploneconf.site` that you want to keep because the following will delete them!!! +Make sure you have no new files or changes in the folder structure of `mastering-plone-project` that you want to keep, because the following will delete them! ``` ```shell @@ -101,7 +83,7 @@ This does two things: ## Tags -These are the tags of the backend add-on 'ploneconf.site' and frontend add-on 'volto-ploneconf' for which there is code: +These are the tags of the repositories `mastering-plone-project` and `mastering-plone-votable-add-on` for which there is code: | Chapter | Tag name | Package | @@ -110,86 +92,35 @@ These are the tags of the backend add-on 'ploneconf.site' and frontend add-on 'v | {doc}`intro` | | | {doc}`case` | | | {doc}`what_is_plone` | | -| {doc}`installation` | | +| {doc}`installation` | initial | mastering-plone-project | | {doc}`features` | | | {doc}`configuring_customizing` | | -| {doc}`add-ons` | | +| {doc}`add-ons` | addons | mastering-plone-project | | {doc}`extending` | | | {doc}`dexterity` | | | {doc}`volto_development` | | -| {doc}`dexterity_2_talk` | talks | ploneconf.site | +| {doc}`dexterity_2_talk` | talks | mastering-plone-project | | {doc}`dexterity_reference` | | -| {doc}`volto_overrides` | overrides | volto-ploneconf | -| {doc}`volto_talkview` | talkview | volto-ploneconf | -| {doc}`behaviors_1` | behaviors_1 | ploneconf.site | -| {doc}`volto_frontpage` | frontpage | ploneconf.site | +| {doc}`volto_overrides` | overrides | mastering-plone-project | +| {doc}`volto_talkview` | talkview | mastering-plone-project | +| {doc}`behaviors_1` | behaviors_1 | mastering-plone-project | +| {doc}`volto_frontpage` | frontpage | mastering-plone-project | | {doc}`api` | | -| {doc}`events` | events | ploneconf.site and volto-ploneconf | -| {doc}`registry` | vocabularies | ploneconf.site and volto-ploneconf | -| {doc}`custom_search` | search | ploneconf.site | -| {doc}`volto_testing` | testing | volto-ploneconf | -| {doc}`dexterity_3` | schema | ploneconf.site | -| {doc}`upgrade_steps` | upgrade_steps | ploneconf.site | -| {doc}`volto_listing_variation` | listing_variation | volto-ploneconf | -| {doc}`searchable` | searchable | ploneconf.site | -| {doc}`volto_components_sponsors` | sponsors | volto-ploneconf | -| {doc}`volto_addon` | | -| {doc}`volto_custom_addon` | | -| {doc}`volto_custom_addon2` | | -| {doc}`user_generated_content` | user_generated_content | ploneconf.site | -| {doc}`relations` | relations | ploneconf.site and volto-ploneconf | -| {doc}`voting-story/index` | | training.votable, volto-training-votable | +| {doc}`events` | events | mastering-plone-project | +| {doc}`registry` | vocabularies | mastering-plone-project | +| {doc}`custom_search` | search | mastering-plone-project | +| {doc}`volto_testing` | testing | mastering-plone-project | +| {doc}`dexterity_3` | schema | mastering-plone-project | +| {doc}`upgrade_steps` | upgrade_steps | mastering-plone-project | +| {doc}`volto_listing_variation` | listing_variation | mastering-plone-project | +| {doc}`searchable` | searchable | mastering-plone-project | +| {doc}`volto_components_sponsors` | sponsors | mastering-plone-project | +| {doc}`custom_block` | block | mastering-plone-project | +| {doc}`user_generated_content` | user_generated_content | mastering-plone-project | +| {doc}`relations` | relations | mastering-plone-project | +| {doc}`voting-story/index` | initial | mastering-plone-votable-add-on | +| {doc}`voting-story/behaviors_2` | behaviors | mastering-plone-votable-add-on | +| {doc}`voting-story/endpoints` | endpoints | mastering-plone-votable-add-on | +| {doc}`voting-story/volto_actions` | actions | mastering-plone-votable-add-on | +| {doc}`voting-story/permissions` | permissions | mastering-plone-votable-add-on | | {doc}`deployment_code` | | -| {doc}`code` | | -| {doc}`trainer` | | - - -## Updating the code-package - -This section is for **trainers** who want to update the code after changing something in the training documentation. - -The current model uses only one branch of commits and maintains the integrity through rebases. - -It goes like this: - -- Only one branch (main) - -- Write the code for chapter 1 and commit. - -- Write the code for chapter 2 and commit. - -- Add the code for chapter 3 and commit. - -- You realize that something is wrong in chapter 1. - -- You branch off at the commit id for chapter 1. - `git checkout -b temp 123456` - -- You change the code and do a commit. - `git commit -am 'Changed foo to also do bar'` - -- Switch to master and rebase on the branch holding the fix which will inject the new commit into master at the right place: - `git checkout master` - `git rebase temp` - That inserts the changes into master in the right place. You only maintain a master branch that is a sequence of commits. - -- Then you might need to update your training documentation to point to the corresponding commit ids: - - - chapter one: `git checkout 121431243` - - chapter two: `git checkout 498102980` - -Additionally you can - -- set tags on the respective commits and move these tags. This way the docs do not need to be changed when the code changes. -- squash the commits between the chapters to every chapter is one commit. - -To move tags after changes you do: - -- Move tag to another commit: `git tag -a -f` -- Push the changed tags to the server: `git push --tags -f` - -The final result should look like this: - -```{figure} _static/code_tree.png -:align: center -``` diff --git a/docs/mastering-plone/configuring_customizing.md b/docs/mastering-plone/configuring_customizing.md index 6905ef993..1d1572a53 100644 --- a/docs/mastering-plone/configuring_customizing.md +++ b/docs/mastering-plone/configuring_customizing.md @@ -3,21 +3,21 @@ myst: html_meta: "description": "What you can do through the web without touching the code" "property=og:description": "What you can do through the web without touching the code" - "property=og:title": "Configuring and Customizing Plone 'Through The Web'" + "property=og:title": "Configure Plone 'through the web'" "keywords": "Plone, configuration" --- (configuring-customizing-label)= -# Configuring and Customizing Plone "Through The Web" +# Configure Plone "through the web" (customizing-controlpanel-label)= -## The site setup +## Site Setup -Important parts of Plone can be configured in the site setup, often still called `control panel`. +Important parts of Plone can be configured in the Site Setup area, often still called the "control panel". -Follow the menu in the left bottom of your site via {guilabel}`Site Setup` +As an admin user, open the menu at the bottom left of your site and choose {guilabel}`Site Setup`. ```{figure} _static/features_control_panel.png :alt: Site Setup @@ -28,12 +28,14 @@ Site Setup We'll explain every page and mention some of the actions you can perform here. ```{note} -Not all control panels known from Plone Classic are available in Volto yet. -Switch to the backend if you need to configure your site: `http://localhost:8080/Plone/@@overview-controlpanel`. +A few control panels are not available in Volto yet. +Switch to the backend if you need to configure your site: http://localhost:8080/Plone/@@overview-controlpanel. ``` ### General +1. Add-ons +1. Database 1. Date and time 1. Language 1. Mail @@ -41,11 +43,9 @@ Switch to the backend if you need to configure your site: `http://localhost:8080 1. Search 1. Site 1. Social Media -1. Volto settings -1. Add-ons -1. Database +1. URL Management 1. Undo -1. URL management +1. Volto Settings The following control panels are so far only available in the backend: @@ -55,12 +55,12 @@ The following control panels are so far only available in the backend: ### Content -1. Content types +1. Block Types +1. Content Rules +1. Content Types 1. Editing -1. Image handling -1. Content rules +1. Image Handling 1. Relations -1. Moderate comments The following control panels are so far only available in the backend: @@ -68,10 +68,10 @@ The following control panels are so far only available in the backend: ### Users -1. User and group settings -1. Editing users -1. Editing groups -1. Editing group memberships +1. Groups +1. User Group Membership +1. User and Group Settings +1. Users ### Security @@ -100,7 +100,7 @@ Below the links to panels you will find information on your Plone, Zope and Pyth (customizing-zmi-label)= -## ZMI (Zope management interface) +## Zope Management Interface (ZMI) Zope is the foundation of Plone. Here you can access the inner workings of Zope and Plone alike. @@ -112,16 +112,16 @@ You can easily break your site here. So you should know what you are doing! Back up your site, just to be sure. ``` -Examples of what can be configured in the `ZMI` are +Examples of what can be configured in the `ZMI` are: -{guilabel}`portal_workflow` where you can inspect and manage existing and applied workflows, their states and transitions. +* {guilabel}`portal_workflow` where you can inspect and manage existing and applied workflows, their states and transitions. -{guilabel}`portal_catalog` where you can inspect existing indices. +* {guilabel}`portal_catalog` where you can inspect existing indices. ## Summary You can configure and customize a lot in Plone through the web. The most important options are accessible in the [Plone control panel](http://localhost:3000/controlpanel) but some are hidden away in the [ZMI](http://localhost:8080/Plone/manage). -The amount and presentation of information may be overwhelming and the differences between the Volto frontend and the Classic Plone frontend adds even more complexity. +The amount and presentation of information may be overwhelming and the differences between the Volto frontend and the Classic Plone frontend add even more complexity. Don't worry, you'll get the hang of it through practice. diff --git a/docs/mastering-plone/custom_block.md b/docs/mastering-plone/custom_block.md new file mode 100644 index 000000000..9ff9d1fa9 --- /dev/null +++ b/docs/mastering-plone/custom_block.md @@ -0,0 +1,357 @@ +--- +myst: + html_meta: + "description": "Simple block architecture" + "property=og:description": "Simple block architecture" + "property=og:title": "Create a custom block" + "keywords": "Plone, Volto, block, add-on" +--- + +(volto-custom-block-label)= + +# Create a custom block + +````{card} + +In this part you will create a new block for the Volto frontend. +```` + +````{card} + +Check out `mastering-plone-project` at tag `sponsors`: + +```shell +git checkout sponsors +``` + +The code at the end of the chapter: + +```shell +git checkout block +``` + +More info in {doc}`code` +```` + +We want to provide some information for speakers of the conference: +Which topics are possible? +What do I have to consider for speaking at an online conference? +A FAQ section would come in handy. +This could be done by creating a block type that offers a form for question and answer pairs and displays an accordion. + +```{figure} _static/volto_addon_accordion_display.png +:alt: Volto add-on volto-accordion-block +``` + +```{figure} _static/volto_addon_accordion_sidebar.png +:alt: Editing Volto add-on volto-accordion-block +``` + +## The block schema + +Let's first define the schema for the data that will be stored for this block. +We want to store a list of question and answer pairs, like this: + +```json +[ + { + "question": "What is Plone?", + "answer": "Plone is a CMS..." + }, + { + "question": "Where is the conference?", + "answer": "Maastricht" + } +] +``` + +Create a folder {file}`src/frontend/volto-ploneconf-site/src/components/Blocks/FAQ` containing {file}`schema.js`. + +```{code-block} jsx +:linenos: + +export const QuestionAnswerPairSchema = { + title: 'Question and Answer Pair', + fieldsets: [ + { + id: 'default', + title: 'QA pair', + fields: ['question', 'answer'], + }, + ], + properties: { + question: { + title: 'Question', + type: 'string', + widget: 'textarea', + }, + answer: { + title: 'Answer', + type: 'string', + widget: 'richtext', + }, + }, + required: ['question', 'answer'], +}; + +export const FAQBlockSchema = { + title: 'FAQ', + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: ['faqs'], + }, + ], + properties: { + faqs: { + title: 'Question and Answers', + type: 'array', + widget: 'object_list', + schema: QuestionAnswerPairSchema, + }, + }, + required: [], +}; +``` + +`QuestionAnswerPairSchema` is the schema for a single question-answer pair, and `FAQBlockSchema` is the schema for the entire block, with a list of those pairs. + +## Block view + +We need a view for the block. +The BlockView is a simple function component that displays a FAQ component with the data stored in the block. + +Create the file {file}`src/frontend/volto-ploneconf-site/src/components/Blocks/FAQ/BlockView.jsx`. + +```{code-block} jsx +:linenos: + +import FAQ from './FAQ'; + +const View = ({ data }) => { + return ( +
+ +
+ ); +}; + +export default View; +``` + +We outsource the FAQ component to file {file}`src/packages/volto-ploneconf-site/src/components/Blocks/FAQ/FAQ.jsx` and make heavy use of Semantic UI components, especially the accordion with its behavior of expanding and collapsing. + +```{code-block} jsx +:linenos: + +import { useState } from 'react'; + +import Icon from '@plone/volto/components/theme/Icon/Icon'; +import rightSVG from '@plone/volto/icons/right-key.svg'; +import downSVG from '@plone/volto/icons/down-key.svg'; +import AnimateHeight from 'react-animate-height'; + +import { Accordion, Grid, Divider, Header } from 'semantic-ui-react'; + +const FAQ = ({ data }) => { + const [activeIndex, setActiveIndex] = useState(new Set()); + + return data.faqs ? ( + <> + + {data.faqs.map(({ '@id': id, question, answer }) => ( + + { + const newSet = new Set(activeIndex); + activeIndex.has(id) ? newSet.delete(id) : newSet.add(id); + setActiveIndex(newSet); + }} + > + + + + {activeIndex.has(id) ? ( + + ) : ( + + )} + + +
{question}
+
+
+
+
+
+ + + + + +
+ +
+ +
+ + + + +
+ + + ))} + + ) : ( + '' + ); +}; + +export default FAQ; +``` +```` + +## Edit form + +We also need an edit form. +The edit form also uses the same `FAQ` component to show the current data, along with the `FAQSidebar` with the form for editing the data. + +Create the file {file}`frontend/packages/volto-ploneconf-site/src/components/Block/FAQ/BlockEdit.jsx`. + +```{code-block} jsx +:linenos: + +import SidebarPortal from '@plone/volto/components/manage/Sidebar/SidebarPortal'; + +import FAQSidebar from './FAQSidebar'; +import FAQ from './FAQ'; + +const Edit = ({ data, onChangeBlock, block, selected }) => { + return ( +
+ + + + + +
+ ); +}; + +export default Edit; +``` + +```{tip} +Everything inside the `SidebarPortal` is rendered in the sidebar instead of inside the block. +``` + +We outsource the edit form to {file}`FAQSidebar.jsx` which displays a form using the block schema. +The _onChangeBlock_ prop is a function we can use to store changes to the block data. + +```{code-block} jsx +:linenos: + +import { FAQBlockSchema } from './schema'; +import InlineForm from '@plone/volto/components/manage/Form/InlineForm'; + +const FAQSidebar = ({ data, block, onChangeBlock }) => { + return ( + { + onChangeBlock(block, { + ...data, + [id]: value, + }); + }} + formData={data} + /> + ); +}; + +export default FAQSidebar; +``` + +## Register the block in Volto config + +What's left to do? +You created a block type with view and edit form and even a nice widget for the editor to fill in questions and answers. +We still need to register the block type in the Volto configuration so that Volto knows it exists. + +Add the file {file}`frontend/volto-ploneconf-site/src/config/blocks.ts`. + +```{code-block} tsx +:linenos: + +import icon from '@plone/volto/icons/list-bullet.svg'; + +import FAQBlockEdit from '../components/Blocks/FAQ/BlockEdit'; +import FAQBlockView from '../components/Blocks/FAQ/BlockView'; +import { FAQBlockSchema } from '../components/Blocks/FAQ/schema'; + +import type { ConfigType } from '@plone/registry'; + +export default function install(config: ConfigType) { + config.blocks.blocksConfig.faq = { + id: 'faq', + title: 'FAQ', + blockSchema: FAQBlockSchema, + edit: FAQBlockEdit, + view: FAQBlockView, + icon: icon, + group: 'text', + restricted: false, + mostUsed: false, + sidebarTab: 1, + }; + return config; +} +``` + +Update {file}`frontend/src/volto-ploneconf-site/src/index.ts` to include the block configuration. + +```{code-block} tsx +:linenos: +:emphasize-lines: 3, 7 + +import type { ConfigType } from '@plone/registry'; +import installSettings from './config/settings'; +import installBlocks from './config/blocks'; + +function applyConfig(config: ConfigType) { + installSettings(config); + installBlocks(config); + + return config; +} + +export default applyConfig; +``` + +Restart the frontend, and now the FAQ block should be available. + +```{figure} _static/volto_addon_accordion_add.png +:alt: "@rohberg/volto-accordion-block" +``` + +```{seealso} + +[@rohberg/volto-accordion-block](https://www.npmjs.com/package/@rohberg/volto-accordion-block) is a released add-on similar to the one from this chapter. +``` diff --git a/docs/mastering-plone/custom_search.md b/docs/mastering-plone/custom_search.md index 9fdec651b..bbe3e1d65 100644 --- a/docs/mastering-plone/custom_search.md +++ b/docs/mastering-plone/custom_search.md @@ -9,10 +9,28 @@ myst: (custom-search-label)= -# Custom Search +# Custom search Volto has a search block that allows you to build custom searches without additional add-ons. +````{card} + +Check out `mastering-plone-project` at tag `vocabularies`: + +```shell +git checkout vocabularies +``` + +The code at the end of the chapter: + +```shell +git checkout search +``` + +More info in {doc}`code` +```` + + ## Exercise Show talks on the talks page with a search block. @@ -44,32 +62,39 @@ Check the entries by switching to the "Browse" tab. :animate: fade-in-slide-down :icon: question -{file}`catalog.xml` +{file}`backend/src/ploneconf/site/profiles/default/catalog.xml` ```xml - - + + - + ``` -{file}`querystring.xml` +{file}`backend/src/ploneconf/site/profiles/default/querystring.xml` ```xml - Type of Talk - True - False - - plone.app.querystring.operation.selection.any - plone.app.querystring.operation.selection.all - plone.app.querystring.operation.selection.none - - Metadata - ploneconf.types_of_talk + prefix="plone.app.querystring.field.type_of_talk" + > + Type of Talk + True + False + + plone.app.querystring.operation.selection.any + plone.app.querystring.operation.selection.all + plone.app.querystring.operation.selection.none + + Metadata + ploneconf.types_of_talk ``` -Checkout `ploneconf.site` at tag "search" to see the updated code. +Check out `mastering-plone-project` at tag `search` to see the updated code. ```` diff --git a/docs/mastering-plone/dexterity.md b/docs/mastering-plone/dexterity.md index 39b29c599..4fbc7465d 100644 --- a/docs/mastering-plone/dexterity.md +++ b/docs/mastering-plone/dexterity.md @@ -11,23 +11,27 @@ myst: # Content types I +```{card} In this part you will: - Learn about content types - Customize existing types - Create a content type through the web - +``` (dexterity1-what-label)= ## What is a content type? -A content type instance is a kind of object that can store information and is editable by users. +A content type is a kind of object that can store information and is editable by users. We have different content types to reflect the different kinds of information about which we need to collect and display information. Pages, events, news items, files (binary) and images are all content types. -See "Features of Plone – {ref}`features-content-types-label`" for an overview. +```{seealso} + +See {ref}`features-content-types-label` for an overview of Plone's built-in content types. +``` It is common in developing a web site that you'll need customized versions of common content types, or perhaps even entirely new types. @@ -46,28 +50,29 @@ Every Plone content type has the following parts: Schema -: A definition of fields that comprise a content type, properties of an object. +: A definition of fields that can be stored and edited for a content item with this type. -FTI +Factory Type Information (FTI) -: The "Factory Type Information" configures the content type in Plone, assigns it a name, additional features and available views to it. +: The "Factory Type Information" is a specification stored in the `portal_types` tool which configures the content type in Plone, assigns it a name, additional features and available views to it. Views : A view is a representation of the object and the content of its fields that may be rendered in response to a request. You may have _one or more_ views for an object. -Some may be _visual_ — intended for display as web pages — others may be intended to satisfy AJAX requests and render content in formats like JSON or XML. +Some may be _visual_, intended for display as web pages. +Others may be intended to satisfy AJAX requests and render content in formats like JSON or XML. ## Schemas, Fields and Values In a schema you can model fields that are used to store data. -Plone automatically creates forms bases on the schemata of a content type to add and edit content. +Plone automatically creates forms to add and edit content based on the schemas of a content type. Values of these fields are attributes on content objects. -Here is a example that shows how to access and modify these values in python: +Here is a example that shows how to access and modify these values in Python: -```pycon +```python >>> obj.title 'A Newsitem' >>> obj.description @@ -83,25 +88,33 @@ b'\x89PNG\r\n\x1a\n\x00\x00\x00\...' ## Behaviors -Content types can have additional schemata. These are called behaviors. +Content types can have additional schemas. +These are called behaviors. They are meant to be used across content types to add shared functionality. One example is the ability of most content types to allow them to be excluded from the navigation. The field is available on all types even though it is not defined in their schema. Instead it is provided by the behavior `plone.excludefromnavigation` that most content types use. -Each behavior schema can define fields. The values of these fields are again attributes on content objects. +Each behavior schema can define fields. +The values of these fields are again attributes on content objects. + +The behavior `plone.excludefromnavigation` adds a field `exclude_from_nav` to each object. +The value is either `True` or `False` because it is a boolean field. -The behavior `plone.excludefromnavigation` adds a attribute `exclude_from_nav` to each object. The value is either `True` or `False` because it is a boolean field. (dexterity1-modify-label)= -## Modifying existing types +## Modify an existing content type schema For now, we will not code anything. We will only use the Plone web interface features. -- Go to the {guilabel}`Content Types` control panel at http://localhost:3000/controlpanel/dexterity-types. +- Go to the {guilabel}`Content Types` control panel at . + + ```{note} + "Dexterity" is the internal name of Plone's content type system. + ``` - Inspect some of the existing default types. @@ -148,32 +161,40 @@ We will only use the Plone web interface features. (dexterity1-create-ttw-label)= -## Creating content types TTW +## Create a content type through the web + +In this step we will create a content type called `Talk` and try it out. +When it's ready, we will move the code from the web to the filesystem and into our own add-on. +Later we will extend that content type. + +- Go to the {guilabel}`Content Types` control panel: . +- Use the add button at upper left to add a new content type "Talk". +- Edit the schema of the Talk content type and add some fields to it: -In this step we will create a content type called `Talk` and try it out. When it's ready we will move the code from the web to the file system and into our own add-on. Later we will extend that content type. + - **Type of talk**, type "Choice". Add possible values: Talk, Training, Keynote. + - **Details**, type "Rich Text" with a maximum length of 2000. + - **Audience**, type "Multiple Choice". Add possible values: Beginner, Advanced, Professional. -- Add new content type "Talk" and some fields for it: +- Save the schema. - - {guilabel}`Add new field` "Type of talk", type "Choice". Add options: talk, keynote, training. - - {guilabel}`Add new field` "Details", type "Rich Text" with a maximal length of 2000. - - {guilabel}`Add new field` "Audience", type "Multiple Choice". Add options: beginner, advanced, pro. - - Check the behaviors that are enabled: _Dublin Core metadata_, _Name from title_. Do we need them all? +- Check the behaviors that are enabled: _Dublin Core metadata_, _Name from title_. Do we need them all? - Test the content type. -- Return to the control panel +- Return to the control panel: . -- Extend the new type: add the following fields: +- Extend the new type by adding the following fields: - - "Speaker", type: "Text line" - - "Email", type: "Email" - - "Image", type: "Image", not required - - "Speaker Biography", type: "Rich Text" + - **Speaker**, type: "Text line" + - **Email**, type: "Email" + - **Image**, type: "Image", not required + - **Speaker Biography**, type: "Rich Text" - Test again. ````{note} -The schema you created through the web is stored as XML in the database. Here is the complete XML schema created by our actions: +The schema you created through the web is stored as XML in the database. +Here is the complete XML schema created by our actions: ```{code-block} xml :linenos: @@ -215,7 +236,7 @@ The schema you created through the web is stored as XML in the database. Here is Speaker - Adress of the speaker + Email of the speaker Email @@ -234,7 +255,8 @@ The schema you created through the web is stored as XML in the database. Here is ``` ```` -It's awesome that we can do so much through the web and great for prototyping or small projects. But it's also a dead end if we want to reuse this content type in other sites. +It's awesome that we can do so much through the web and great for prototyping or small projects. +But it's also a dead end if we want to reuse this content type in other sites. Also, for professional development, we want to be able to use version control for our work, and we'll want to be able to add the kind of business logic that will require programming. @@ -247,26 +269,24 @@ Using Python to define the schema gives us much more control (e.g. for validatio ### Exercise 1 -Modify Pages to allow uploading an image as decoration (like News Items do). +Modify the Page content type to allow uploading an image as decoration (like News Items do). ```{dropdown} Solution :animate: fade-in-slide-down :icon: question -- Go to the dexterity control panel () -- Click on *Page* () -- Select the tab *Behaviors* () +- Go to the Content Types control panel () +- Click on *Page* () +- Select the tab *Behaviors* - Check the box next to {guilabel}`Lead Image` and save. The images are displayed above the title. ``` ## Further reading -- Documentation {doc}`plone6docs:backend/fields` -- [Example content type](https://github.com/collective/example.contenttype) A Plone content type with all available fields - -Plone 5 documentation -- [Dexterity Developer Manual](https://5.docs.plone.org/external/plone.app.dexterity/docs/index.html) -- [The standard behaviors](https://5.docs.plone.org/external/plone.app.dexterity/docs/reference/standard-behaviours.html) -- [Dexterity XML](https://5.docs.plone.org/external/plone.app.dexterity/docs/reference/dexterity-xml.html) -- [Model-driven types](https://5.docs.plone.org/external/plone.app.dexterity/docs/model-driven-types.html#model-driven-types) +- Plone documentation about + - {doc}`plone6docs:backend/content-types/index` + - {doc}`plone6docs:backend/schemas` + - {doc}`plone6docs:backend/fields` + - {doc}`plone6docs:backend/behaviors` +- [Example content type](https://github.com/collective/example.contenttype) - A Plone content type with all available fields diff --git a/docs/mastering-plone/dexterity_2_talk.md b/docs/mastering-plone/dexterity_2_talk.md index fa0900e0a..db4fbf3e8 100644 --- a/docs/mastering-plone/dexterity_2_talk.md +++ b/docs/mastering-plone/dexterity_2_talk.md @@ -12,9 +12,7 @@ myst: # Content types II: Talk ```{card} -In this part you will: - -Create a content type 'talk' to store all the data required for a talk. +In this part you will create a content type "Talk" to store all the data required for a talk. Tools and techniques covered: @@ -24,9 +22,9 @@ Tools and techniques covered: - Widgets ``` -````{card} Backend chapter +````{card} -Checkout `ploneconf.site` at tag "initial": +Check out `mastering-plone-project` at tag `initial`: ```shell git checkout initial @@ -43,78 +41,109 @@ More info in {doc}`code` ## The type registration -Add a new file {file}`types.xml` to your add-on package in {file}`profiles/default/`. - -The following lines will tell Plone that there is a new content type defined. +Edit the file {file}`backend/src/ploneconf/site/profiles/default/types.xml` and add the `talk` object. ```xml - - - + + + ``` -Plone will now expect a file {file}`profiles/default/types/talk.xml` and will register that as a new content type. +Plone will now expect a file {file}`backend/src/ploneconf/site/profiles/default/types/talk.xml` and will register that as a new content type. ## The FTI -Add the file `profiles/default/types/talk.xml`. -Note there is a file _types_ and a folder _types_. +Add the file `backend/src/ploneconf/site/profiles/default/types/talk.xml`. +Note there is a file `types.xml` and a folder `types`. -This is the **Factory Type Information** that holds the configuration for the content type **talk**. +This is the **Factory Type Information** that holds the configuration for the content type **Talk**. ```{code-block} xml :linenos: - - - Talk - - string:${portal_url}/document_icon.png - talk - string:${folder_url}/++add++talk - - view - True - True - - False - view - - - - False - cmf.AddPortalContent - ploneconf.site.content.talk.Talk - ploneconf.site.content.talk.ITalk - - - - - - - - dexterity - - - - - - - - - - + + + Talk + + string:${portal_url}/document_icon.png + talk + string:${folder_url}/++add++talk + + view + True + True + + False + view + + + + False + cmf.AddPortalContent + ploneconf.site.content.talk.Talk + ploneconf.site.content.talk.ITalk + + + + + + + + dexterity + + + + + + + + + + ``` Now our package has a new configuration for Generic Setup. -Generic Setup loads a lot of different types of configuration for the site from folder {file}`profiles/`. +Generic Setup loads a lot of different types of configuration for the site from the folder {file}`profiles/`. This configuration is applied to your site upon installing the package. This also means that you will need to re-install the package once we are finished with the talk. @@ -128,11 +157,10 @@ It is also the place where you would add widget options per field to control the In the FTI we referenced the Python path `ploneconf.site.content.talk.ITalk`. -The module {py:mod}`content` does not exist. -Create a folder {file}`content` and add an empty {file}`__init__.py` in it. -From the training root that is {file}`backend/sources/ploneconf.site/src/ploneconf/site/content/__init__.py`. +The package {py:mod}`ploneconf.site.content` already exists. +Find it in at this path: {file}`backend/src/ploneconf/site/content`. -In this new folder add a file {file}`talk.py` with the following content: +In this folder add a new file {file}`talk.py` with the following content: ```{code-block} python :linenos: @@ -147,12 +175,10 @@ from plone.supermodel import model from z3c.form.browser.checkbox import CheckBoxFieldWidget from z3c.form.browser.radio import RadioFieldWidget from zope.interface import implementer -from zope.schema.vocabulary import SimpleTerm -from zope.schema.vocabulary import SimpleVocabulary class ITalk(model.Schema): - """Dexterity-Schema for Talks""" + """Dexterity schema for Talks""" directives.widget(type_of_talk=RadioFieldWidget) type_of_talk = schema.Choice( @@ -172,7 +198,7 @@ class ITalk(model.Schema): audience = schema.Set( title="Audience", value_type=schema.Choice( - values=['beginner', 'advanced', 'professional'], + values=["beginner", "advanced", "professional"], ), required=False, ) @@ -190,7 +216,7 @@ class ITalk(model.Schema): email = Email( title="Email", - description="Email adress of the speaker", + description="Email address of the speaker", required=False, ) @@ -199,11 +225,6 @@ class ITalk(model.Schema): required=False, ) - twitter = schema.TextLine( - title="Twitter name", - required=False, - ) - github = schema.TextLine( title="Github username", required=False, @@ -231,10 +252,10 @@ The first class {py:class}`ITalk` is the schema for talks and defines quite a lo - The fields in the schema are mostly from {py:mod}`zope.schema`. - The most basic field is `schema.TextLine` which can store text. -- In the next chapter you will find a reference of all field-types available in Plone. -- The widget directives can be ignored by now, as we are implementing for a frontend app. - The widget directives like above do control the rendering of the fields in Plone Classic. - In the rare case that you need to tweak the rendering of a field in frontend, this can be done like described in {doc}`plone6docs:volto/development/widget` +- In the next chapter you will find a reference of all field types available in Plone. +- The widget directives can be ignored by now, as we are implementing for a Volto frontend. + The widget directives do control the rendering of the fields in Plone Blicca. + In the rare case that you need to tweak the rendering of a field in the frontend, this can be done as described in {doc}`plone6docs:volto/development/widget`. ## The instance class @@ -242,16 +263,22 @@ The first class {py:class}`ITalk` is the schema for talks and defines quite a lo The second class {py:class}`Talk` in {file}`talk.py` will be the class of instances for each talk. It inherits from {py:class}`Container` which is one of the default classes of dexterity. {py:class}`Container` is used for items that can contain other items. -It does nothing so far but it can be useful later when we want to add methods or properties to it that can be used directly from a talk instance. +It does nothing special so far, but it can be useful later when we want to add methods or properties to it that can be used directly from a talk instance. + ## Try the new type -Now all pieces should be in place and you can enable the new type `Talk`. +Now all pieces should be in place and you can enable the new type `talk`. -- Restart Plone (to load the new Python code and the changed ZCML) -- You do not need to restart the Volto frontend since we did not do any changes there. -- Re-install the package ploneconf.site (deactivate and activate) to load the type registration and type configuration: - Follow the link "SITE SETUP" in the bottom of the toolbar and switch to "Add-Ons". +1. Restart Plone (to load the new Python code). + You do not need to restart the Volto frontend, since we did not do any changes there. +2. Reinstall the package ploneconf.site to apply the updated profile. + + 1. Go to {guilabel}`Site Setup`. + 2. Open the {guilabel}`Add-Ons` control panel. + 3. Find `PLONECONF SITE: INSTALL` in the list of installed add-ons and click to open its details. + 4. Click the {guilabel}`Uninstall` button. + 5. Find it again and click the {guilabel}`Install` button. Now instances of the new type can be added. Please check that you can add a talk to your site. @@ -260,17 +287,16 @@ Please check that you can add a talk to your site. Adding a talk in the frontend ``` -- Test the type by adding a talk. Add some values in the fields, save it, look at the view and edit it again. +- Test the type by adding a talk. Add some values in the fields, save it, look at the view, and edit it again. - Compare all the fields you see to the code in the schema. -- You can also make changes in the schema. After restarting the backend these changes are effective immediately. -- Find the tool `portal_types` in the ZMI http://localhost:8080/manage. +- You can also make changes in the schema. After restarting the backend, these changes are effective immediately. +- Find the tool `portal_types` in the ZMI: . Look at the FTI for type `talk` and inspect the configuration taken from the FTI. - - You can make changes to the FTI here. -- A part of the configuration is also available in Plone control panels (SITE SETUP). - For example the content types control panel `http://localhost:3000/controlpanel/dexterity-types` allows to add behaviors to content types. - Please be aware that these changes are done on your site instance. - You can use the browser UI, but it's the add-on package where you configure your content types. + You can make changes to the FTI here. +- A part of the configuration is also available in Site Setup. + For example the [Content Types control panel](http://localhost:3000/controlpanel/dexterity-types) allows to add behaviors to content types. + Please be aware that these changes are done in your site database, but not on the filesystem. + You can use the browser UI, but then the configuration can get out of sync with your filesystem add-on package. The field values of your talk instance are listed. In one of the next chapters we will create a custom view for the new type. @@ -280,12 +306,15 @@ In one of the next chapters we will create a custom view for the new type. - You created a custom content type. - You can now control the data that will be stored for talks. -- You can reuse and adapt these examples to model data for your own use-cases. +- You can reuse and adapt these examples to model data for your own use cases. - Next up: After looking at even more fields that are available in Plone, you will learn to change how talks are displayed. ```{seealso} -- Documentation {doc}`plone6docs:backend/fields` -- [Example content type](https://github.com/collective/example.contenttype/tree/training-mastering-plone-development) -A Plone content type with all available fields -``` \ No newline at end of file +- Plone documentation about + - {doc}`plone6docs:backend/content-types/index` + - {doc}`plone6docs:backend/schemas` + - {doc}`plone6docs:backend/fields` + - {doc}`plone6docs:backend/behaviors` +- [Example content type](https://github.com/collective/example.contenttype) - A Plone content type with all available fields +``` diff --git a/docs/mastering-plone/dexterity_3.md b/docs/mastering-plone/dexterity_3.md index 9e56162f0..4c0219007 100644 --- a/docs/mastering-plone/dexterity_3.md +++ b/docs/mastering-plone/dexterity_3.md @@ -3,34 +3,35 @@ myst: html_meta: "description": "Organize and protect fields of content types" "property=og:description": "Organize and protect fields of content types" - "property=og:title": "Content types III: Sponsors" + "property=og:title": "Content types III: Sponsor" "keywords": "schema, permission" --- (dexterity-3-label)= -# Content types III: Sponsors +# Content types III: Sponsor -Without sponsors, a conference would be hard to finance! Plus it is a good opportunity for Plone companies to advertise their services. +Without sponsors, a conference would be hard to finance! +Plus it is a good opportunity for Plone companies to advertise their services. ```{card} In this part we will: -- Create a sponsor contenttype to manage sponsors -- Store non-visible information about the sponsor in the sponsor-type +- Create a sponsor content type to manage sponsors +- Store non-visible information about the sponsor in the sponsor type Tools and techniques covered: -- Schema hint and directives +- Schema hints and directives - Field permissions ``` -````{card} Backend chapter +````{card} -Checkout `ploneconf.site` at tag "search": +Check out `mastering-plone-project` at tag `testing`: ```shell -git checkout search +git checkout testing ``` The code at the end of the chapter: @@ -47,7 +48,7 @@ More info in {doc}`code` First we create the schema for the new content type. -Add a new file {file}`content/sponsor.py`. +Add a new file {file}`backend/src/ploneconf/site/content/sponsor.py`. ```{code-block} python :linenos: @@ -87,7 +88,7 @@ class ISponsor(model.Schema): directives.read_permission(notes="plone.app.controlpanel.Site") directives.write_permission(notes="plone.app.controlpanel.Site") notes = RichText( - title="Secret Notes (only for site-administrators and managers)", required=False + title="Secret Notes (only for site administrators and managers)", required=False ) @@ -105,12 +106,11 @@ Some things are notable here: See the {doc}`dexterity_reference` for a reference of all field-types and directives you can use in dexterity. ``` -## The Factory Type Information, or FTI +## The Factory Type Information (FTI) -Next, we create the factory type information ("FTI") for the new type in {file}`profiles/default/types/sponsor.xml` +Next, we create the factory type information ("FTI") for the new type in {file}`backend/src/ploneconf/site/profiles/default/types/sponsor.xml` ```{code-block} xml -:emphasize-lines: 21 :linenos: @@ -189,16 +189,22 @@ Next, we create the factory type information ("FTI") for the new type in {file}` ``` -Then we register the FTI in {file}`profiles/default/types.xml` +Then we register the FTI in {file}`backend/src/ploneconf/site/profiles/default/types.xml` ```{code-block} xml -:emphasize-lines: 4 +:emphasize-lines: 8-10 :linenos: - - - - + + + + ``` @@ -210,6 +216,6 @@ See the next chapter. ## Summary -- You created a new content type to store information on sponsors -- You learned how to protect individual fields from being edited with permissions -- Next you will learn how to display the sponsors at the bottom of every page +- You created a new content type to store information on sponsors. +- You learned how to protect individual fields from being edited with permissions. +- Next you will learn how to apply updates without completely reinstalling the add-on. diff --git a/docs/mastering-plone/dexterity_reference.md b/docs/mastering-plone/dexterity_reference.md index e9d035e02..1e4a3fc4e 100644 --- a/docs/mastering-plone/dexterity_reference.md +++ b/docs/mastering-plone/dexterity_reference.md @@ -3,23 +3,27 @@ myst: html_meta: "description": "Fields and widgets" "property=og:description": "Fields and widgets" - "property=og:title": "Content types: Reference" + "property=og:title": "Content types reference" "keywords": "field, widget, schema" --- (dexterity-reference-label)= -# Content types: Reference +# Content types reference This chapter documents common fields, widgets, directives that you can use with content types. -Content types are often called dexterity types which refers to the rework of the content type concept by dexterity and abandoning the Archetypes system. + +```{note} +You might see references to "Dexterity" which is the internal name of Plone's content type system. +``` ## Fields included in Plone -This is a schema with examples for all field-types that are shipped with Plone by default. They are arranged in fieldsets: +This is a schema with examples for all field types that are shipped with Plone by default. +They are arranged in fieldsets: -Text, boolean, email +Text, Boolean, Email : Textline, RichText, Boolean, Email, URI @@ -68,63 +72,8 @@ from zope.interface import implementer class IExample(model.Schema): - """Dexterity-Schema with common field-types.""" - - # fieldset( - # "default", - # label="Text, Boolean, Email", - # fields=( - # "title", - # "description", - # "richtext_field", - # "bool_field", - # "email_field", - # "uri_field", - # ), - # ) - - fieldset( - "numberfields", - label="Number", - fields=("int_field", "float_field"), - ) - - fieldset( - "datetimefields", - label="Date and time", - fields=( - "datetime_field", - "date_field", - ), - ) - - fieldset( - "choicefields", - label="Choice", - fields=( - "choice_field", - "list_field", - "tuple_field", - "set_field", - ), - ) - - fieldset( - "relationfields_volto", - label="Relation fields – Volto", - fields=( - "relationchoice_field_named_staticcatalogvocabulary", - "relationlist_field_named_staticcatalogvocabulary", - ), - ) - - fieldset( - "filefields", - label="File", - fields=("file_field", "image_field"), - ) + """Dexterity schema with common field types.""" - # Default fields primary("title") title = schema.TextLine( title="Primary Field (Textline)", @@ -138,20 +87,6 @@ class IExample(model.Schema): required=False, ) - # text_field = schema.Text( - # title="Text Field", - # description="zope.schema.Text", - # required=False, - # missing_value="", - # default="", - # ) - - # textline_field = schema.TextLine( - # title="Textline field", - # description="A simple input field (zope.schema.TextLine)", - # required=False, - # ) - richtext_field = RichText( title="RichText field", description="This uses a richtext editor. (plone.app.textfield.RichText)", @@ -177,7 +112,59 @@ class IExample(model.Schema): required=False, ) + # Number fields + fieldset( + "numberfields", + label="Number", + fields=("int_field", "float_field"), + ) + + int_field = schema.Int( + title="Integer Field (e.g. 12)", + description="zope.schema.Int", + required=False, + ) + + float_field = schema.Float( + title="Float field, e.g. 12.7", + description="zope.schema.Float", + required=False, + ) + + # Date and Time fields + fieldset( + "datetimefields", + label="Date and time", + fields=( + "datetime_field", + "date_field", + ), + ) + + datetime_field = schema.Datetime( + title="Datetime field", + description="Uses a date and time picker (zope.schema.Datetime)", + required=False, + ) + + date_field = schema.Date( + title="Date field", + description="Uses a date picker (zope.schema.Date)", + required=False, + ) + # Choice fields + fieldset( + "choicefields", + label="Choice", + fields=( + "choice_field", + "list_field", + "tuple_field", + "set_field", + ), + ) + choice_field = schema.Choice( title="Choice field", description="zope.schema.Choice", @@ -218,32 +205,6 @@ class IExample(model.Schema): default=set(), ) - # File and image fields - image_field = NamedBlobImage( - title="Image field", - description="A upload field for images (plone.namedfile.field.NamedBlobImage)", - required=False, - ) - - file_field = NamedBlobFile( - title="File field", - description="A upload field for files (plone.namedfile.field.NamedBlobFile)", - required=False, - ) - - # Date and Time fields - datetime_field = schema.Datetime( - title="Datetime field", - description="Uses a date and time picker (zope.schema.Datetime)", - required=False, - ) - - date_field = schema.Date( - title="Date field", - description="Uses a date picker (zope.schema.Date)", - required=False, - ) - """Relation fields like Volto likes it RelationChoice and RelationList with named StaticCatalogVocabulary @@ -251,6 +212,14 @@ class IExample(model.Schema): StaticCatalogVocabulary registered with same name as field/relation. This allowes Volto relations control panel to restrict potential targets. """ + fieldset( + "relationfields_volto", + label="Relation fields – Volto", + fields=( + "relationchoice_field_named_staticcatalogvocabulary", + "relationlist_field_named_staticcatalogvocabulary", + ), + ) relationchoice_field_named_staticcatalogvocabulary = RelationChoice( title="RelationChoice – named StaticCatalogVocabulary – Select widget", @@ -282,16 +251,22 @@ class IExample(model.Schema): }, ) - # Number fields - int_field = schema.Int( - title="Integer Field (e.g. 12)", - description="zope.schema.Int", + # File and image fields + fieldset( + "filefields", + label="File", + fields=("file_field", "image_field"), + ) + + image_field = NamedBlobImage( + title="Image field", + description="A upload field for images (plone.namedfile.field.NamedBlobImage)", required=False, ) - float_field = schema.Float( - title="Float field, e.g. 12.7", - description="zope.schema.Float", + file_field = NamedBlobFile( + title="File field", + description="A upload field for files (plone.namedfile.field.NamedBlobFile)", required=False, ) @@ -588,7 +563,7 @@ directives.widget( specialfield = schema.TextLine(title="Field with special frontend widget") ``` -Then register your frontend widget in your apps configuration. +Then register your frontend widget in your Volto configuration. ```jsx import { MySpecialWidget } from './components'; @@ -616,7 +591,8 @@ The props will be injected into the corresponding widget component, configuring ## Directives -Directives can be placed anywhere in the class body (annotations are made directly on the class). By convention they are kept next to the fields they apply to. +Directives can be placed anywhere in the class body (annotations are made directly on the class). +By convention, they are kept next to the fields they apply to. For example, here is a schema that omits a field: @@ -646,7 +622,8 @@ With the directive "mode" you can set fields to 'input', 'display' or 'hidden'. directives.mode(additionalInfo='hidden') ``` -You can apply directives to certain forms only. Here we drop a field from the add-form, it will still show up in the edit-form. +You can apply directives to certain forms only. +Here we drop a field from the add form, but it will still show up in the edit form. ```python from z3c.form.interfaces import IAddForm @@ -664,7 +641,10 @@ class ITask(model.Schema): The same works for custom forms. -With the directive {py:meth}`widget` you can not only change the widget used for a field. With {py:data}`pattern_options` you can pass additional parameters to the widget. Here, we configure the datetime widget powered by the JavaScript library [pickadate](https://amsul.ca/pickadate.js/) by adding options that are used by it. Plone passes the options to the library. +With the directive {py:meth}`widget` you can not only change the widget used for a field. +With {py:data}`pattern_options` you can pass additional parameters to the widget. +Here, we configure the datetime widget powered by the JavaScript library [pickadate](https://amsul.ca/pickadate.js/) by adding options that are used by it. +Plone passes the options to the library. ```python class IMeeting(model.Schema): @@ -712,7 +692,8 @@ class IMeeting(model.Schema): Validators and defaults can also be made aware of the context (i.e. to check against the values of other fields). -For context aware defaults you need to use a {py:class}`IContextAwareDefaultFactory`. It will be passed the container for which the add form is being displayed: +For context-aware defaults you need to use a {py:class}`IContextAwareDefaultFactory`. +It will be passed the container for which the add form is being displayed: ```python from zope.interface import provider diff --git a/docs/mastering-plone/events.md b/docs/mastering-plone/events.md index bcc7e0ddc..a75452e71 100644 --- a/docs/mastering-plone/events.md +++ b/docs/mastering-plone/events.md @@ -3,41 +3,33 @@ myst: html_meta: "description": "Add event features to a content type" "property=og:description": "Add event features to a content type" - "property=og:title": "Turning Talks into Events" + "property=og:title": "Add event dates to talks" "keywords": "Plone, event, content type, date" --- (events-label)= -# Turning Talks into Events +# Add event dates to talks -Save and show date and time of a talk. +```{card} +In this chapter you will -````{card} Frontend chapter +- Enable the event behavior for talks +- Display the date & time in the talk view -Check out the code at the relevant tags! +``` -Code for the beginning of this chapter: +````{card} -```shell -# frontend -git checkout talkview -``` +Check out `mastering-plone-project` at tag `frontpage`: ```shell -# backend git checkout frontpage ``` Code for the end of this chapter: ```shell -# frontend -git checkout events -``` - -```shell -# backend git checkout events ``` @@ -45,21 +37,16 @@ More info in {doc}`code` ```` -We need a schedule and for this we need to store the information when a talk will happen. +We need a schedule and for this we need to store the date and time when a talk will happen. Luckily the default type _Event_ is based on reusable behaviors from the package {py:mod}`plone.app.event` that we can reuse. -In this chapter you will - -- Enable the event behavior for talks -- Display the date of event in the talkview - ## Add date fields -Instead of adding Datetime-fields to the talk schema we will use the behavior `plone.eventbasic`. +Instead of adding datetime fields to the talk schema, we will use the behavior `plone.eventbasic`. -Enable the behavior `plone.eventbasic` for talks in {file}`profiles/default/types/talk.xml`. +Enable the behavior `plone.eventbasic` for talks in {file}`backend/src/ploneconf/site/profiles/default/types/talk.xml`. ```{code-block} xml :emphasize-lines: 6 @@ -74,32 +61,40 @@ Enable the behavior `plone.eventbasic` for talks in {file}`profiles/default/type ``` -After you activate the behavior by hand or you reinstalled the add-on you will now have some additional fields for `start`, `end`, `open_end` and `whole_day`. +After you activate the behavior by hand or reinstalled the add-on, you will now have some additional fields for `start`, `end`, `open_end` and `whole_day`. ````{note} -While we're editing behaviors we can also add our own featured-behavior to News Items. +While we're editing behaviors we can also add our own "featured" behavior to News Items. -Add {file}`profiles/default/types/News_Item.xml`: +Add {file}`backend/src/ploneconf/site/profiles/default/types/News_Item.xml`: ```xml - - - - - + + + + ``` + +Because the behaviors are specified with `purge="false"`, +the new one will be added without removing any existing behaviors. + ```` ## Display the dates Now we need to update the event view to show this information. -Unfortuanely displaying dates and times is not as simple as it might sound since we'd have to account for different use cases that all look different: +Unfortunately displaying dates and times is not as simple as it might sound, since we have to account for different use cases that all look different. -Here are some examples how dates might be displayed if they are full-day events, open-ended events or events with a defined end-time. +Here are some examples of how dates might be displayed if they are full-day events, open-ended events, or events with a defined end-time. - Apr 22, 2020 from 3:00 PM to 5:00 PM - Apr 22, 2020 @@ -113,7 +108,7 @@ So it would be a good idea to reuse a component that already deals with these us Since we use the same behavior as the default content type Event in Plone, the default event view might have what we need. Add an event und use the React Developer Tools to inspect the component displaying the date. -The component is called `When` and is defined in `frontend/node_modules/@plone/volto/src/components/theme/View/EventDatesInfo.jsx`. +The component is called `When` and is defined in `frontend/core/packages/volto/src/components/theme/View/EventDatesInfo.jsx`. ```jsx ``` -We'll reuse it in {file}`frontend/src/components/Views/Talk.jsx`. We'll let us inspire by the event-view and add a `` that will contain the date and the audience. In this box we will also use `
` from [seamantic-ui](https://react.semantic-ui.com/elements/header/#types-subheaders) to separate the data. - -{file}`frontend/src/components/Views/Talk.jsx`: +We'll reuse it in {file}`frontend/packages/volto-ploneconf-site/src/components/Views/TalkView.jsx`. +Inspired by the event view, we'll add a right-floated segment containing the date and the audience. ```{code-block} jsx -:emphasize-lines: 9,30-58 - -import { - Container as SemanticContainer, - Header, - Image, - Label, - Segment, -} from 'semantic-ui-react'; -import { flattenToAppURL } from '@plone/volto/helpers'; -import { When } from '@plone/volto/components/theme/View/EventDatesInfo'; +:emphasize-lines: 3,25-49 + +import { Container as SemanticContainer } from 'semantic-ui-react'; import config from '@plone/volto/registry'; +import { When } from '@plone/volto/components/theme/View/EventDatesInfo'; + +const colorMapping = { + beginner: 'green', + advanced: 'yellow', + professional: 'purple', +}; const TalkView = (props) => { const { content } = props; const Container = config.getComponent({ name: 'Container' }).component || SemanticContainer; - const color_mapping = { - beginner: 'green', - advanced: 'yellow', - professional: 'purple', - }; + const Image = config.getComponent({ name: 'Image' }).component; return (

@@ -160,12 +149,10 @@ const TalkView = (props) => { {content.description && (

{content.description}

)} - +
{content.start && !content.hide_date && ( <> -
- When -
+
When
{ )} {content.audience && ( -
- Audience -
+
Audience
)} {content.audience?.map((item) => { - let audience = item.title || item.token; - let color = color_mapping[audience] || 'green'; + let audience = item.token; + let color = colorMapping[audience] || 'green'; return ( -
- - {content.speaker &&
{content.speaker}
} +
+ {content.speaker && ( +
{content.speaker}
+ )} {content.website ? (

{content.company || content.website} @@ -204,16 +191,6 @@ const TalkView = (props) => { Email: {content.email}

)} - {content.twitter && ( -

- X:{' '} - - {content.twitter.startsWith('@') - ? content.twitter - : '@' + content.twitter} - -

- )} {content.github && (

Github:{' '} @@ -223,11 +200,9 @@ const TalkView = (props) => {

)} {content.speaker} {content.speaker_biography && (
{ }} /> )} - +
); }; @@ -249,7 +224,7 @@ The result should look like this: ``` -## Hiding fields from certain users +## Hide fields from certain users ```{note} This chapter is about displaying, not editing. So setting values is not the topic here. @@ -262,7 +237,7 @@ At least in this case we can take the easy way out since the field does not cont We can simply hide the fields from contributors using CSS and show them for reviewers. ```{warning} -This trick does not yet work in Volto because some css-classes are still missing from the body-tag (see ). Skip ahead! +This trick does not yet work in Volto because some CSS classes are still missing from the body-tag (see ). Skip ahead! ``` Modify {file}`frontend/theme/extras/custom.overrides` and add: @@ -299,7 +274,7 @@ Find out where the event behavior is defined and which fields it offers. The name you used to enable the behavior {file}`Talk.xml` is registered in zcml. So `name="plone.eventbasic"` should be easy to find. -You will find it in {file}`backend/packages/plone/app/event/dx/configure.zcml` and it points to `IEventBasic` in {file}`packages/plone.app.event/plone/app/event/dx/behaviors.py` +You will find it in `plone.app.event` in {file}`src/plone/app/event/dx/configure.zcml` and it points to `IEventBasic` in {file}`src/plone/app/event/dx/behaviors.py` ```python class IEventBasic(model.Schema, IDXEvent): @@ -386,6 +361,6 @@ Note how it uses `defaultFactory` to set an initial value. ## Summary -- You applied an existing behavior to a content type to add new fields -- You benefited of an existing Volto component to display the date -- You did not have to write your own datetime fields and indexers o/ +- You applied an existing behavior to a content type to add new fields. +- You reused an existing Volto component to display the date. +- You did not have to write your own datetime fields and indexers. diff --git a/docs/mastering-plone/extending.md b/docs/mastering-plone/extending.md index d510d43af..ff052315c 100644 --- a/docs/mastering-plone/extending.md +++ b/docs/mastering-plone/extending.md @@ -3,65 +3,67 @@ myst: html_meta: "description": "Plone architecture and concepts" "property=og:description": "Plone architecture and concepts" - "property=og:title": "Extending Plone" + "property=og:title": "Extend and customize Plone" "keywords": "Plone, architecture, zcml, Generic Setup" --- (extending-label)= -# Extending Plone +# Extend and customize Plone -```{card} Backend chapter +```{card} -In this part you will: - -- Get an overview over the technologies used to extend Plone +In this part you will get an overview over the technologies used to extend the Plone backend. Topics covered: -- Overriding Python components +- Extension packages - Component architecture - ZCML -- GenericSetup +- Generic Setup ``` -As a developer you want to go further than simply configuring Plone, you want to extend and customize it. +As a developer you want to go further than simply configuring Plone. +You want to extend and customize it. Plone is built to be extended. Extendability is not an afterthought but is the core of Plone and the systems it is based on. -Instead it is the core of its architecture. -> Plone consists of a Python backend and a React frontend. -> They are connected via the REST API. -> Thus you have two different layers that you can customize. +How do you extend Plone? -Therefore we create two different extension packages to customize and extend Plone: +This depends on what type of extension you want to create. -1. One is a Python package that holds for example content types, behaviors and configuration. -2. The other is a JavaScript package that hold views, styling and customization of the frontend. +```{only} not presentation +- You can create extensions with new types of objects to add to your Plone site. Usually these are content types. +- You can create an extension that changes or extends functionality. For example to change the way Plone displays search results, or to make pictures searchable by adding a transformer from image to text. +``` -Sometimes it is easy to know, which layer needs to be customized to achieve a certain result. +For most projects you combine multiple kinds of methods to extend Plone. -- All styling and JavaScript-based interaction is customized on the Volto side of Plone. -- Content types and other persistent data is customized or created in a Python package. -For more complex use cases you will need to add code to both parts of our customization story. -For example a content type is defined in the Python package and its visualization is defined in the JavaScript package. +(extending-packages-label)= +## Extension packages -(extending-technologies-label)= +Plone consists of a Python backend and a React frontend. +They are connected via the REST API. +Thus you have two different layers that you can customize. -## Extension technologies +Therefore we create two different extension packages to customize and extend Plone. +(These are similar to add-ons, but they are located in the project repository.) -How do you extend Plone? +1. One is a Python package that holds for example content types, behaviors and configuration. + (For the training project, this is located in {file}`backend/src/ploneconf/site`.) +2. The other is a JavaScript package that hold views, styling and customization of the frontend. + (For the training project, this is located in {file}`frontend/packages/volto-ploneconf-site`.) -This depends on what type of extension you want to create. +Sometimes it is easy to know which layer needs to be customized to achieve a certain result. -```{only} not presentation -- You can create extensions with new types of objects to add to your Plone site. Usually these are content types. -- You can create an extension that changes or extends functionality. For example to change the way Plone displays search results, or to make pictures searchable by adding a transformer from image to text. -``` +- All styling and JavaScript-based interaction is customized on the frontend side of Plone. +- Content types and other persistent data are customized or created in a Python package. + +For more complex use cases you will need to add code to both parts of our customization story. +For example a content type is defined in the Python package and its visualization is defined in the JavaScript package. -For most projects you combine multiple kinds of methods to extend Plone. (extending-technologies-component-architecture-label)= @@ -75,65 +77,85 @@ For most projects you combine multiple kinds of methods to extend Plone. - Powerful and flexible ``` -```{only} not presentation -The best way to extend Plone is via *Components*. - -A bit of history is in order. +````{only} not presentation -When Zope started, object-oriented design was **the** silver bullet. +Plone uses a component architecture to provide loose coupling between different parts of the system. -Object-oriented design is good at modeling inheritance, but breaks down when an object has multiple aspects that are part of multiple taxonomies. +What does that mean? +There is a central registry of components that can fulfill predefined contracts, called interfaces. +If some code wants to make a call to another part of Plone, it should not do so directly. +Instead, it should ask the registry for a component that can provide the interface it is designed to use. -Some object-oriented programming languages like Python handle this through multiple inheritance. But it's not a good way to do it. Zope objects have more than 10 base classes. Too many namespaces makes code that's hard to maintain. Where did that method/attribute come from? +There are several kinds of components: -After a while, XML and Components became the next silver bullet (Does anybody remember J2EE?). +* *Utilities* provide a standalone service. +* *Adapters* provide a new way to access an existing object. +* *Subscribers* execute actions in response to events triggered on a different object. -Based on their experiences with Zope in the past, Zope developers thought that a component system configured via XML might be the way to go to keep the code more maintainable. +For example, there is an interface `INameFromTitle` which defines how to get the title for a content item. +(Get an adapter which is registered for the `INameFromTitle` interface, and get its `title` attribute.) +If you as a developer want to change how the title is calculated for a specific content type, you can register an `INameFromTitle` adapter for that content type. -Before Zope Components functionality was often extended by a practice called Monkey Patching: Changing code in other modules by importing and then modifying it at runtime. +This is the basis for Plone's extensibility. +Add-on packages can easily change core Plone functionality by adding or replacing components in the registry, without needing to directly change the code that uses those components. -Monkey Patching, like subclassing via multiple inheritance, does not scale. Multiple plugins might overwrite each other, you would explain to people that they have to reorder the imports, and then, suddenly, you will be forced to import feature A before B, B before C and C before A, or else your application won't work. +```{tip} +Many of the interfaces used by Plone core are defined in the `plone.base` package. +You can explore them here: https://github.com/plone/plone.base/tree/main/src/plone/base/interfaces -As the new concepts were radically different from the old Zope concepts, the Zope developers renamed the new project to Zope 3. -But it did not gain traction, was eventually renamed to Bluebream and then died out. +However, Plone is made up of many packages, so there are also a lot of interfaces defined elsewhere. +``` -But the component architecture itself is quite successful and the Zope developers extracted it into the Zope Toolkit. The Zope toolkit is part of Zope, and Plone developers use it extensively. +```{note} +Earlier versions of Zope and Plone relied more on other ways of composing software, such as object-oriented inheritance. +But this led to very complicated objects that were difficult to reason about and override. -This is what you want to use. +Over time, many parts have been updated to use the component architecture. +But there are still some inner parts which use inheritance. +Sometimes it is necessary to use more invasive techniques like monkey-patching to override core Plone functionality. ``` +```` + + (extending-components-label)= -## Configuring Zope Components with ZCML +## Configure Zope Components with ZCML ```{only} presentation - zcml (Zope Component Markup Language) is used to register components - components are distingushed by interfaces (contracts) that they require or provide ``` -```{only} not presentation -ZCML, the Zope Configuration Mark-up Language is an XML based language used to configure Zope Components. With ZCML you declare utilities, adapters and browser views. +````{only} not presentation +The Zope Configuration Markup Language (ZCML) is an XML-based language used to configure Zope components. +With ZCML you register utilities, adapters and browser views using ZCML. Components are distinguished from one another by the interfaces (formal definitions of functionality) that they require or provide. -During startup, Zope reads all these ZCML statements, validates that there are not two declarations trying to register the same components and registers everything. All components are registered by interfaces required and provided. Components with the same interfaces may optionally also be named. +During startup, Zope reads all these ZCML statements, validates that there are not two declarations trying to register the same components, and registers everything. +All components are registered by interfaces required and provided. +Components with the same interfaces may optionally also be named. -It may seem a little cumbersome that you have to register all components. But thanks to ZCML, you hardly ever have a hard time to find what and where extensions or customizations are defined. ZCML files are like a phone book. +```{tip} +ZCML is only processed at startup time. +If you make changes to a `.zcml` file, you have to restart the backend in order for the changes to take effect. ``` -```{eval-rst} -.. epigraph:: - - Explicit is better than implicit - - -- The Zen of Python +It may seem a little cumbersome that you have to register all components. +But thanks to ZCML, you hardly ever have a hard time to find what and where extensions or customizations are defined. +ZCML files are like a phone book. +```` +```{epigraph} + Explicit is better than implicit + + -- The Zen of Python ``` - (extending-technologies-generic-setup-label)= -## GenericSetup +## Generic Setup ```{only} presentation - Old style @@ -141,15 +163,24 @@ It may seem a little cumbersome that you have to register all components. But th ``` ```{only} not presentation -The next thing is {py:mod}`Products.GenericSetup`. +Another tool for configuring Plone using XML files is {term}`Generic Setup`. + +Generic Setup organizes XML configuration files in a _profile_. +When the profile is applied, it will update persistent settings stored in the database. -*GenericSetup* lets you define persistent configuration in XML files. *GenericSetup* parses the XML files and updates the persistent configuration according to the configuration. This is a step you have to run on your own! +Unlike ZCML, Generic Setup profiles are not read automatically. +You have to apply the profile on your own, usually by installing or upgrading an add-on. +When you do this, Generic Setup reads the XML files and updates the persistent configuration accordingly. -You will see many objects in Zope or the ZMI that you can customize through the web. If they are well behaving, they can export their configuration via *GenericSetup* and import it again. +Generic Setup profiles are a useful way to programmatically configure the same things that can be changed through the web in a control panel. +You will see many objects in Zope or the ZMI that you can customize through the web. +If they are well behaving, they can export their configuration via Generic Setup and import it again. -Typically you use *GenericSetup* to change workflows or add new content type definitions. +For example, you can use Generic Setup to change workflows or add new content type definitions. -GenericSetup profiles may also be built into Python packages. Every package that is listed on the add-on package list inside a Plone installation has a GS profile that details how it fits into Plone. Packages that are part of Plone itself may have GS profiles, but are excluded from the active/inactive listing. +Generic Setup profiles may also be built into Python packages. +Every package that is listed in the Add-ons control panel in Site Setup has a Generic Setup profile that defines how it fits into Plone. +(Packages that are part of Plone itself may also have Generic Setup profiles, but are not shown in the Add-ons control panel unless they are optional.) ``` Examples of a profile of an add-on in `profile/default/` @@ -168,7 +199,8 @@ Examples of a profile of an add-on in `profile/default/` ``` -Most settings are stored in a tool called `portal_registry`. Since it has great import/export handlers for GenericSetup it can be configured with {file}`registry/main.xml`: +Most settings are stored in a tool called `portal_registry`. +Since it has great import/export handlers for Generic Setup, it can be configured with {file}`registry/main.xml`: {file}`registry/main.xml`: diff --git a/docs/mastering-plone/features.md b/docs/mastering-plone/features.md index 2ac5293a5..80ec86563 100644 --- a/docs/mastering-plone/features.md +++ b/docs/mastering-plone/features.md @@ -3,138 +3,37 @@ myst: html_meta: "description": "Plone from an integrators view" "property=og:description": "Plone from an integrators view" - "property=og:title": "The Features of Plone" + "property=og:title": "Features of Plone" "keywords": "Plone, content type, user, group, workflow, content rule, history" --- (features-label)= -# The Features of Plone +# Features of Plone -Now we create a Plone instance and take a look at all the features you can use as an integrator. -Developers get a glimpse on the features that can be modified easily. +Now let's take a look at all the features you can use as an integrator. +Developers get a glimpse of the features that can be modified easily. -(features-start-stop-label)= -## Starting and stopping Zope +(features-start-plone)= -We control Plone with `make`. Start your Zope instance with: +## Start Plone -```shell -cd training/backend/ -make start -``` - -The Zope instance starts up with the message `Ready to handle requests`. -Later on the instance can be stopped by {kbd}`ctrl c`. - -A standard installation listens on port 8080, so let's have a look at - -```{figure} _static/features_plone_running.png -:alt: Zope instance is up and running, ready to create a Plone instance. - -Zope instance is up and running, ready to create a Plone instance. -``` - - -(features-create-plonesite-label)= - -## Creating a Plone site - -We now have a running Zope with a database, but no content. - -Push the button {guilabel}`Create` in Plone Site (Volto). -Log in with `admin` and password `admin`. -The initial login is defined in file `instance.yaml`. -You should change your password in production sites via `http://localhost:8080/acl_users/users/manage_users`. - -If you ever have the need to create an emergency user, create one with: - -```shell -venv/bin/addzopeuser masterofdesaster mypassworD£xyz2 -c ./instance/etc/zope.conf -``` - -```{figure} _static/features_create_site_form.png -:alt: Create a Plone site - -Create a Plone site -``` - -You will be automatically redirected to the new site. - -This is how the front page should look like: - -```{figure} _static/frontpage_plone.png -:alt: The front page of your site - -The front page of your site -``` +First make sure that Plone is running and that you're logged in. +You can refer to these instructions from the previous chapter: +* {ref}`installation-start-backend-label` +* {ref}`installation-start-frontend-label` +* {ref}`installation-log-in-label` -## Starting and stopping the frontend -Start the frontend of your new Plone site by switching to directory `frontend` and enter: - -```shell -make start -``` - -Opening `http://localhost:3000`, you are facing the front page of your Plone site. - -```{figure} _static/frontpage_volto.png -``` - -You can stop the frontend any time using {kbd}`ctrl c`. - -While developing it's not necessary to restart the frontend unless you're adding a new file. +(features-walkthrough-label)= -Login to your new site with `admin` and password `admin`. +## Walk through the user interface ```{figure} _static/frontpage_volto_logged_in.png ``` - -### Change ports - -#### backend - -If you want Plone to listen on port 9080 instead of the default 8080, open the file {file}`backend/instance.yml` in your favorite editor. - -```yaml -wsgi_listen: localhost:8080 -``` - -Change the address to `localhost:9080` and restart your instance. - -You will also have to tell the frontend that the backend is now running on a different port! - -You need to change the environment variable `RAZZLE_DEV_PROXY_API_PATH` to the base URL of the backend: - -```shell -RAZZLE_DEV_PROXY_API_PATH=http://localhost:9080/Plone pnpm start -``` - -When your Plone instance isn't called `Plone` you can use the same approach: - -```shell -RAZZLE_DEV_PROXY_API_PATH=http://localhost:8080/mysite pnpm start -``` - -#### frontend - -Change the port of the frontend to 1234 - -By default the frontend will start on port 3000. You can change the port and/or host name for the frontend by specifying the environment variables `PORT` and/or `HOST`: - -```shell -HOST=localhost PORT=1234 pnpm start -``` - - -(features-walkthrough-label)= - -## Walk through the user interface - These are the main elements of the user interface: - {guilabel}`header`: @@ -172,32 +71,6 @@ Some toolbar options only show when appropriate. For example, {guilabel}`edit` is only shown if the current user has the permission to edit the current page. -(features-mailserver-label)= - -## Configure a mail server - -```{only} not presentation -For production level deployments you have to configure a mail server. -Later in the training we will create some content rules that send emails when new content is put on our site. - -For the training you don't have to configure a working mailserver since the Plone add-on `Products.PrintingMailHost` is installed which will redirect all emails to the console. -``` - -Head over to the mail control panel http://localhost:8080/Plone/@@mail-controlpanel and apply the following configuration: - -- Server: {samp}`localhost` -- Username: leave blank -- Password: leave blank -- Site 'From' name: Your name -- Site 'From' address: Your email address - - -Click on {guilabel}`Save and send test e-mail`. -You will see the mail content in the console output of your instance. -Plone will not actually send the email to the receiver's address unless you remove or deactivate [Products.PrintingMailHost](https://pypi.org/project/Products.PrintingMailHost/). - - - ## The site structure We are creating the following structure: @@ -247,7 +120,7 @@ Create a site structure: The view of the newly created site structure ``` -Additional to these conference pages we also want some news and events. +In addition to these conference pages, we also want some news and events. We want a registration page and a protected section for the conference team. - Add a page "News" @@ -263,14 +136,14 @@ We want a registration page and a protected section for the conference team. ## Default content types -The most used default Plone content types are Page, News item, and Event. +The most used default Plone content types are Page, News Item, and Event. ### Page -A Page is the most flexible content type. +A page is the most flexible content type. You can use the editor to create, edit and arrange blocks on a page. You can choose from blocks for text, an image, a video, a list of existing content and many more. -Pages are folderish, they can contain other content. +Pages are folderish (they can contain other content items). This means you can use pages to structure your site. ```{figure} _static/features_add_a_page.png @@ -278,42 +151,42 @@ This means you can use pages to structure your site. ### News Item -Basically a page with an image and an image caption to be used for press releases and such. +A news item is basically a page with an image and an image caption to be used for press releases and such. ```{figure} _static/features_add_a_news_item.png ``` ### Event -Basically a page with start and end dates and some additional fields for whole day or recurring events. +An event is basically a page with start and end dates and some additional fields for whole day or recurring events. ```{figure} _static/features_add_a_event.png ``` ### Other available content types -There are more content types per default available: file, image, link. +There are more content types available by default: File, Image, Link. -### Content editing +### Editing content For more information on how to edit content, see the training {doc}`/content-editing/index`. (features-containers-label)= -## folderish content +## Folderish content Go to "News". -Earlier we created this page with its title "News". -Therefore this page has the id "news" which we can see as part of its url `http://localhost:3000/news`. +Earlier we created this page with the title "News". +Therefore this page has the id `news` which we can see as part of its URL: `http://localhost:3000/news`. A page is folderish. To inspect its contained items, we change to `/contents` by clicking the folder icon. We can change the order of the two contained items by dragging and dropping. -We can modify their title and id, publish them, etc.. in one step by selecting them and applying a bulk action. +We can modify their title and id, publish them, etc. in one step by selecting them and applying a bulk action. ```{figure} _static/contents.png :alt: page contents @@ -321,12 +194,12 @@ We can modify their title and id, publish them, etc.. in one step by selecting t `/contents` ``` -A page has per default the view displaying the blocks of the page. +By default, a page has the view displaying the blocks of the page. As for all content types, you as a developer can provide multiple views or replace the default view. This is useful for adding components that should be shown, regardless of how an editor assembles a page with blocks. By default, the page doesn't show its contained items but only the title and blocks created by an editor. -To reveal contained items, you can create a listing block. +To reveal contained items, you can create a listing block or a search block. If you use a listing block without any specific criteria, it lists all contained items. @@ -337,63 +210,6 @@ listing contained content items with a listing block ``` -(features-content-rules-label)= - -## Content Rules - -Content rules allow to subscribe actions to events. -We can access the UI by switching to the site setup. -Select the menu in the left bottom of your page. -In the site setup we select the content rules panel. - -Each content rule created here is a contract on the site as a whole or just a section to apply an event subscriber to. -The content rule therefore defines an action that subscribes to an event. - -### Exercise - -The goal of this exercise is to implement a content rule that will notify a moderator of a new news item. - -From within the site setup menu, click "Content Rules". This will open a window containing the content rules and several options to filter the rules by event trigger. For now, there are no rules yet, but we will create one now. -Click the button "Add content rule" and enter the title "Notify moderators on new news items". -Select the triggering event "Object added to this container". Later on, we will apply the rule to a specific container. -Enable the "Enabled" check box to make sure the rule will run when applied. -Click Save to save the new content rule and return to the content rules overview. - -At this point, we have to configure the rule and tell it to send an e-mail to a moderator when a news item has been added to the container. -Click "Configure" and start by selecting the "Content type" condition. -Click the "Add" button below the content type, select "News Item" and click the right arrow. -Select "Send email" in the Action selection field. -Click the "Add" button below the action selection field and fill out the form. Make sure the mandatory fields (subject, Email recipients, Message) have content and click the right arrow to save the changes. - -The rule has been created, but has not been applied to a container yet. Let's do that now. -Click the left arrow button in the top left corner three times to return to the site. -If you want to apply the rule to all news items globally for your site, go to your home page, click the context menu button (three dots in the sidebar) and click "Rules". -From the "Available content rules" dropdown list, select the rule you created earlier and click "Add". - -Verify that your rule works by creating a new news item. -See your backend log or your mail for a notification. -Did you receive a notification? - - -```{figure} _static/features_add_rule_1.png -:alt: Create a new content rule for an event. - -Create a new content rule for an event. -``` - -```{figure} _static/features_add_rule_2.png -:alt: Configure the content rule with conditions and actions. - -Configure the content rule with conditions and actions. -``` - -```{figure} _static/features_add_rule_3.png -:alt: Assign your rule to a page (with or without sub pages) or globally. - -Assign your rule to a page (with or without sub pages) or globally. -``` - - (features-history-label)= ## History @@ -568,12 +384,66 @@ You can find the UI for adding alternative URLs following the context menu {guil (features-wc-label)= -## Working copy -```{warning} -The working copy feature is not yet implemented in Volto UI. +(features-content-rules-label)= + +## Content rules + +Content rules automatically perform actions triggered by events. +We can access the UI by switching to {guilabel}`Site Setup`. +Select the menu in the bottom left of your page. +In Site Setup we select the content rules panel. + +Each content rule created here is a contract on the site as a whole or just a section to apply an event subscriber to. +The content rule therefore defines an action that subscribes to an event. + +### Exercise + +The goal of this exercise is to implement a content rule that will notify a moderator of a new news item. + +From within the site setup menu, click "Content Rules". This will open a window containing the content rules and several options to filter the rules by event trigger. For now, there are no rules yet, but we will create one now. +Click the button "Add content rule" and enter the title "Notify moderators on new news items". +Select the triggering event "Object added to this container". Later on, we will apply the rule to a specific container. +Enable the "Enabled" check box to make sure the rule will run when applied. +Click Save to save the new content rule and return to the content rules overview. + +At this point, we have to configure the rule and tell it to send an e-mail to a moderator when a news item has been added to the container. +Click "Configure" and start by selecting the "Content type" condition. +Click the "Add" button below the content type, select "News Item" and click the right arrow. +Select "Send email" in the Action selection field. +Click the "Add" button below the action selection field and fill out the form. Make sure the mandatory fields (subject, Email recipients, Message) have content and click the right arrow to save the changes. + +The rule has been created, but has not been applied to a container yet. Let's do that now. +Click the left arrow button in the top left corner three times to return to the site. +If you want to apply the rule to all news items globally for your site, go to your home page, click the context menu button (three dots in the sidebar) and click "Rules". +From the "Available content rules" dropdown list, select the rule you created earlier and click "Add". + +Verify that your rule works by creating a new news item. +See your backend log or your mail for a notification. +Did you receive a notification? + + +```{figure} _static/features_add_rule_1.png +:alt: Create a new content rule for an event. + +Create a new content rule for an event. ``` +```{figure} _static/features_add_rule_2.png +:alt: Configure the content rule with conditions and actions. + +Configure the content rule with conditions and actions. +``` + +```{figure} _static/features_add_rule_3.png +:alt: Assign your rule to a page (with or without sub pages) or globally. + +Assign your rule to a page (with or without sub pages) or globally. +``` + + +## Working copy + Published content, even in an intranet setting, can pose a special problem for editing. It may need to be reviewed before changes are made available. In fact, the original author may not even have permission to change the document without review. diff --git a/docs/mastering-plone/index.md b/docs/mastering-plone/index.md index d16db4fe7..b733cf8db 100644 --- a/docs/mastering-plone/index.md +++ b/docs/mastering-plone/index.md @@ -3,35 +3,35 @@ myst: html_meta: "description": "The power of the Plone backend – best practice" "property=og:description": "The power of the Plone backend – best practice" - "property=og:title": "Mastering Plone 6 Development" + "property=og:title": "Mastering Plone Development" "keywords": "Plone, backend" --- (mastering-plone-label)= -# Mastering Plone 6 development +# Mastering Plone Development *Mastering Plone Development* is intended as a training to learn proven practices of Plone development. -It's both, an online course and a sketch for an on-the-spot training. +It's both an online course and a sketch for an on-site training. -The story of a conference platform provides a training of several development topics that can be split in two trainings: +The story of a conference platform provides a training of several development topics that can be split into two trainings: - A beginner training (2 to 3 days) covering the essentials of Plone development. - An advanced training (3 to 5 days) with additional advanced topics concerning the Plone backend. ```{note} -*Mastering Plone 6 Development Training* focuses on Plone backend architecture and topics of Volto development as long as the backend is involved. +*Mastering Plone Development* focuses on Plone backend architecture and topics of Volto development as long as the backend is involved. We do not deal with pure Volto topics here. For Volto development see training {doc}`training2024:effective-volto/index`. -We expect you to work with the Plone React frontend called Volto. -Therefore we do not deal with Plone Classic topics here. -For Plone Classic see training {doc}`training2024:theming/index`, training {doc}`training2023:mastering-plone-5/index` and documentation {doc}`plone6docs:classic-ui/index` +We expect you to work with the Plone frontend called Volto. +Therefore we do not deal with Plone Blicca (Classic) topics here. +For Plone Blicca see training {doc}`training2024:theming/index`, training {doc}`training2023:mastering-plone-5/index` and documentation {doc}`plone6docs:classic-ui/index` ``` ```{toctree} -:caption: Mastering Plone 6 Development +:caption: Mastering Plone Development :name: toc-mastering :maxdepth: 3 :numbered: 2 @@ -63,17 +63,15 @@ upgrade_steps volto_listing_variation searchable volto_components_sponsors -volto_addon -volto_custom_addon -volto_custom_addon2 +custom_block user_generated_content relations voting-story/index deployment_code -code -trainer +appendix ``` Please note that this document is *not complete* without the spoken word of a trainer. -We attempt to include the most important parts of what we teach in the training. But reading it here can not be considered equal to attending a training. +We attempt to include the most important parts of what we teach in the training. +But reading it here can not be considered equal to attending a training. diff --git a/docs/mastering-plone/installation.md b/docs/mastering-plone/installation.md index 5e5e4b458..68fee1b72 100644 --- a/docs/mastering-plone/installation.md +++ b/docs/mastering-plone/installation.md @@ -13,93 +13,100 @@ This chapter provides instructions on how to install **Plone with the training c For general Plone installation, deployment, and hosting instructions, refer to the authoritative Plone 6 documentation in {doc}`plone6docs:install/index`. -(installation-tools-label)= +We encourage you to install and run `Plone` on your own machine, as you will have important benefits: + +- You can work with your favorite editor. +- You have all the code of Plone at your fingertips in `site-packages` or `node_modules`. + -## Technologies and Tools +(installation-prerequisites-label)= - - [Linux (Ubuntu)](https://ubuntu.com/) - - [Python](https://www.python.org) - - [React](https://react.dev/) +## Prerequisites We recommend to work on Linux or Mac, not Windows. +You will need some other software in order to install and run Plone. +Follow {ref}`create-project-cookieplone-prerequisites-for-installation-label` to install the following tools, if you don't have them already: -(installation-label)= + - uv + - nvm + - Node.js + - GNU make + - Git -## Set up -We install the `Plone` backend and its `React`-based frontend `Volto`, starting with the following folder structure: +(installation-project-structure)= + +## The project structure + +The main project for this training is structured as a {term}`monorepo`. +It is a single Git repository which includes both the Plone backend and its React-based frontend Volto, in the following folder structure: ```text -training +mastering-plone-project ├── backend └── frontend ``` -In {file}`backend` we install Plone and add our custom Python code. -In {file}`frontend` we install Volto and add our custom React code. - - -(installation-install-backend-label)= - -## Installing the backend - -We encourage you to install and run `Plone` on your own machine, as you will have important benefits: - -- You can work with your favorite editor. -- You have all the code of Plone at your fingertips in `site-packages` tree. - +The {file}`backend` folder contains Plone and our custom backend code and configuration. +The {file}`frontend` folder contains Volto and our custom frontend code and configuration. -### Prerequisites -- `make`. We recommend upgrading to at least `make` 4. +(installation-project-label)= +## Install the project -### Installation - -Set up the [backend with the training code](https://github.com/collective/training_buildout): add-ons `ploneconf.site` and `training.votable`. +Clone the [`mastering-plone-project` repository](https://github.com/collective/mastering-plone-project). ```shell -mkdir training -cd training -git clone https://github.com/collective/training_buildout.git backend -cd backend +git clone git@github.com:collective/mastering-plone-project.git +cd mastering-plone-project +``` + +```{tip} +The initial structure for this project repository was created by following {ref}`create-project-cookieplone-create-volto-project-label`. ``` -Build your backend with: +Install the project with: ```shell -make build +make install ``` -This build executes multiple tasks. -The build -- creates a Python virtual environment and installs prerequisites -- generates a file structure to be prepared to install Plone packages with pip -- generates Zope configuration with Cookiecutter +The install command executes multiple tasks. +It will: +- create a Python virtual environment and install Python dependencies using uv +- generate configuration for a Zope instance using `cookiecutter-zope-instance` +- install Node dependencies using pnpm +- create a new Plone site with our `ploneconf.site` add-on installed -By creating and working with a **Python virtual environment**, we are independent of the system Python installation. We install packages and its version according to our needs. +By creating and working with a **Python virtual environment**, we are independent of the system Python installation. +The correct version of Python and dependency packages are installed in an isolated path (`backend/.venv`). -The build generates a file structure to be prepared to install **Plone from packages** with `pip` and `mxdev`. The tool `mxdev` helps with configuration files to define which add-ons and which versions to install. -It also allows to override Plone core package versions or force a checkout from `github`. -The documentation {doc}`plone6docs:admin-guide/add-ons` provides information on common tasks. +The specification of which Python packages to install comes from {file}`backend/pyproject.toml`. The build generates **Zope configuration** files with Cookiecutter `cookiecutter-zope-instance`. -The file we will modify to update our Zope / Plone configuration is `instance.yaml`. +The file we will modify to update our Zope / Plone configuration is {file}`backend/instance.yaml`. In this file we will add add-ons that are installed as Python packages and shall be loaded in our instance. `instance.yaml` is the one configuration file for our Zope / Plone instance. -The documentation of [`cookiecutter-zope-instance`](https://github.com/plone/cookiecutter-zope-instance) explains a lot more that can be configured like the port or another storage. +The [documentation of `cookiecutter-zope-instance`](https://plone.github.io/cookiecutter-zope-instance/) explains a lot more that can be configured like the port or another storage. -After changes in configuration files, a re-build is necessary: +The specification of which Node.js packages to install comes from {file}`frontend/package.json` and {file}`frontend/mrs.developer.json`. + +After changes in configuration files, a reinstall is necessary: ```shell -make build +make install ``` +(installation-start-backend-label)= + +## Start the backend + We are now ready to start the backend with: ```shell -make start +make backend-start ``` Voilà, your Plone is up and running on http://localhost:8080. @@ -107,12 +114,17 @@ Voilà, your Plone is up and running on http://localhost:8080. The output should be similar to: ```shell -katjasuss@purpur training % make start - -2022-09-27 08:57:23,961 INFO [Zope:42][MainThread] Ready to handle requests -Starting server in PID 28745. -2022-09-27 08:57:23,963 INFO [waitress:486][MainThread] Serving on http://[::1]:8080 -2022-09-27 08:57:23,963 INFO [waitress:486][MainThread] Serving on http://127.0.0.1:8080 +% make backend-start + +2026-07-21 15:12:15,604 INFO [chameleon.config:40][MainThread] directory cache: /Users/davisagli/Plone/mastering-plone-project/backend/instance/var/cache. +2026-07-21 15:12:16,003 WARNING [ZODB.FileStorage:409][MainThread] Ignoring index for /Users/davisagli/Plone/mastering-plone-project/backend/instance/var/filestorage/Data.fs +2026-07-21 15:12:16,155 INFO [plone.restapi.patches:15][MainThread] PATCH: Disabled ZPublisher.HTTPRequest.ZopeFieldStorage.VALUE_LIMIT. This enables file uploads larger than 1MB. +2026-07-21 15:12:16,364 INFO [plone.app.event:17][MainThread] icalendar has been set up to use pytz instead of zoneinfo. +2026-07-21 15:12:17,232 INFO [plone.volto:22][MainThread] Aliasing collective.folderish classes to plone.volto classes. +2026-07-21 15:12:17,630 INFO [Zope:42][MainThread] Ready to handle requests +Starting server in PID 87938. +2026-07-21 15:12:17,633 INFO [waitress:449][MainThread] Serving on http://127.0.0.1:8080 +2026-07-21 15:12:17,633 INFO [waitress:449][MainThread] Serving on http://[::1]:8080 ``` Troubleshooting: We are here to help: Please file an issue in [training repository](https://github.com/plone/training/issues). @@ -123,58 +135,117 @@ Point your browser to to see `Plone` running. :alt: Plone is running. :scale: 50 % -`Plone`, up and running. +Plone, up and running. ``` -There is no Plone site yet. -We will create one in the next chapter. +You can see that there is already an existing Plone site that was created by the install command. You can stop the running instance anytime using {kbd}`ctrl c`. -(installation-install-frontend-label)= +(installation-start-frontend-label)= -## Installing the frontend +## Start the frontend -You have two options: +The frontend must be run as a separate process. +Open a second terminal and start it with: -> 1. Create the frontend from scratch using {term}`Cookieplone`. -> 2. Use the prepared Volto project [volto-ploneconf](https://github.com/collective/volto-ploneconf) with all the code for the training. +```shell +make frontend-start +``` +Point your browser to and see that the site is up and running. -### Option 1: Frontend from scratch +You can stop the frontend anytime using {kbd}`ctrl c`. -Follow the instructions of `docs.plone.org`: {doc}`plone6docs:volto/development/creating-project`. +(installation-log-in-label)= +## Log in -### Option 2. Start with prepared training project `volto-ploneconf` with all code for the training +Log in to your new site with Login Name `admin` and Password `admin`. -Prepare the prerequisites explained in {ref}`plone6docs:create-project-cookieplone-prerequisites-for-installation-label`. -Be aware that Volto 18 recommends `pnpm` instead of `yarn` as package manager and project manager. +````{tip} +The initial password for the admin user is defined in {file}`backend/instance.yaml`. +You should change your password in production sites via `http://localhost:8080/acl_users/users/manage_users`. -Get the code for the frontend from GitHub and install: +If you ever have the need to create an emergency user, create one with: ```shell -cd training -git clone https://github.com/collective/volto-ploneconf.git frontend -cd frontend -make install +backend/.venv/bin/addzopeuser masterofdisaster mypassworD£xyz2 -c backend/instance/etc/zope.conf ``` -Now you can start the app with: +```` + +After logging in, the site should look like this: + +```{figure} _static/frontpage_volto_logged_in.png +``` + + +(installation-change-ports-label)= + +## Change ports + +If you want the Plone backend to listen on port 9080 instead of the default 8080, open the file {file}`backend/instance.yml` in your favorite editor. + +Add the `wsgi_listen` option. + +```yaml +default_context: + initial_user_password: 'admin' + zcml_package_includes: 'ploneconf.site' + wsgi_listen: localhost:9080 +``` + +Now restart your backend. ```shell -make start +make backend-start ``` -which is a +You will also have to tell the frontend that the backend is now running on a different port! + +You need to change the environment variable `RAZZLE_INTERNAL_API_PATH` to the base URL of the backend: ```shell -pnpm start +RAZZLE_INTERNAL_API_PATH=http://localhost:9080/Plone make frontend-start ``` -Point your browser to and see that the app is up and running, albeit without content. -We will create a Plone instance in the next chapter. +If your Plone instance isn't called `Plone`, you can use the same approach: -You can stop the frontend anytime using {kbd}`ctrl c`. +```shell +RAZZLE_INTERNAL_API_PATH=http://localhost:8080/mysite make frontend-start +``` + +By default the frontend will listen on port 3000. +You can change the port for the frontend by specifying the environment variable `PORT`: + +```shell +PORT=1234 make frontend-start +``` + + +(installation-mailserver-label)= + +## Configure a mail server + +````{only} not presentation +For production-level deployments you have to configure a mail server. +Later in the training we will create some content rules that send emails when new content is put on our site. + +For the training you don't have to configure a working mail server. +The Plone add-on `Products.PrintingMailHost` is installed, which will print all outgoing emails to the console instead of sending them. +```` + +Head over to the mail control panel http://localhost:8080/Plone/@@mail-controlpanel and apply the following configuration: + +- Server: {samp}`localhost` +- Username: leave blank +- Password: leave blank +- Site 'From' name: Your name +- Site 'From' address: Your email address + +Click on {guilabel}`Save and send test e-mail`. +You will see the mail content in the console output of your instance. +Plone will not actually send the email to the receiver's address unless you remove or deactivate [Products.PrintingMailHost](https://pypi.org/project/Products.PrintingMailHost/). diff --git a/docs/mastering-plone/intro.md b/docs/mastering-plone/intro.md index 945e84f74..450280a50 100644 --- a/docs/mastering-plone/intro.md +++ b/docs/mastering-plone/intro.md @@ -32,7 +32,7 @@ Tell us about yourselves: - Take notes. - If you have questions later on, https://community.plone.org is the Plone forum with many experienced developers. - For coaching, please contact us. - See {ref}`trainers` section below. + See {ref}`trainers` in the previous chapter. (intro-docs-label)= diff --git a/docs/mastering-plone/registry.md b/docs/mastering-plone/registry.md index 80dc1563a..fabfa9e45 100644 --- a/docs/mastering-plone/registry.md +++ b/docs/mastering-plone/registry.md @@ -3,13 +3,13 @@ myst: html_meta: "description": "How to make your Plone add-on configurable" "property=og:description": "How to make your Plone add-on configurable" - "property=og:title": "Registry, control panels and vocabularies" + "property=og:title": "Registry, control panels, and vocabularies" "keywords": "registry, control panel, vocabulary, select, options, configuration, settings" --- (registry-label)= -# Registry, control panels and vocabularies +# Registry, control panels, and vocabularies ```{card} In this part you will: @@ -26,9 +26,9 @@ Topics covered: - Control panels ``` -````{card} Backend chapter +````{card} -Checkout `ploneconf.site` at tag "events": +Check out `mastering-plone-project` at tag `events`: ```shell git checkout events @@ -63,12 +63,11 @@ To achieve this you first need to get to know the registry. The registry stores and retrieves values in records. Each record consists of the actual value, along with a field that describes the record in more detail. -You can interact with the registry using Python dictionary-style operations to get and set values. +You can interact with the registry in Python using dictionary-style operations to get and set values. -Since Plone 5 the registry stores all global settings. Plone provides the registry through [plone.registry](https://pypi.org/project/plone.registry) and offers a user interface for interaction via [plone.app.registry](https://pypi.org/project/plone.app.registry). -Most settings in {guilabel}`Site Setup` reside in the registry. +Most settings in Site Setup reside in the registry. You can modify them directly through its UI. Open http://localhost:8080/Plone/portal_registry and filter for `displayed_types`. @@ -81,7 +80,7 @@ This UI for the registry is not yet available in the frontend. ## Registry records -In {doc}`volto_frontpage` you already added a criterion usable for listing blocks in {file}`profiles/default/registry/querystring.xml`. +In {doc}`volto_frontpage` you already added a criterion usable for listing blocks in {file}`backend/src/ploneconf/site/profiles/default/registry/querystring.xml`. This setting is stored in the registry. Examine the existing values in the registry. @@ -91,7 +90,7 @@ Talks in the root will now show up in the navigation. This setting is stored in the registry record `plone.displayed_types`. -## Accessing and modifying records in the registry +## Access and modify records in the registry In Python you can access the registry record with the key `plone.displayed_types` via `plone.api.portal`. It holds convenience functions to get and set a record: @@ -106,7 +105,9 @@ api.portal.set_registry_record('plone.smtp_host', 'my.mail.server') For more information see `plone.api.portal` documentation: {ref}`plone6docs:portal-get-registry-record-example`. -The access of the registry by `zope.component.getUtility` is often seen in code from before the time of `plone.api`. +````{note} + +Code that cannot use `plone.api`, such as in Plone core, can access records via the `IRegistry` utility. ```{code-block} python @@ -117,8 +118,11 @@ registry = getUtility(IRegistry) displayed_types = registry.get('plone.displayed_types') ``` +```` + The value of the record `displayed_types` is the tuple `('Image', 'File', 'Link', 'News Item', 'Folder', 'Document', 'Event', 'talk')`. + ## Custom registry records Now add custom settings: @@ -130,7 +134,7 @@ Additionally, new settings `types_of_talk` and `audiences` can be added for use To define custom records, you write the same type of schema as you already did for content types or for behaviors: -Add a file {file}`controlpanel/controlpanel.py`: +Add a file {file}`backend/src/ploneconf/site/controlpanels/controlpanel.py`: ```{code-block} python :linenos: @@ -319,7 +323,7 @@ If the name `Lightning-Talk` needs to be updated to `Short talks`, the talks cat This is because the value stored in the talks is the token `lightning-talk`, which remains unchanged. A new field `JSONField` has been introduced. -This field is used to store JSON data for the content. +This field is used to store structured JSON data for the content. A schema defines the valid structure of the field values. ```python @@ -332,29 +336,28 @@ A schema defines the valid structure of the field values. ``` The `frontendOptions` forces Volto to display on editing the field with a widget prepared for vocabulary terms. -More correct, it forces Volto to lookup the widget in `Volto's` widget mapping to find the corresponding widget. - +It tells Volto to look up the widget named `vocabularyterms` in `Volto's` widget mapping to find the corresponding widget. The schema `IPloneconfSettings` is now registered for the registry. -Add the following to {file}`profiles/default/registry/main.xml`. +Add the following to {file}`backend/src/ploneconf/site/profiles/default/registry/main.xml`. Each field in the `IPloneconfSettings` schema adds a corresponding record to the registry. ```xml - - + + - + ``` ```{note} The `prefix` allows you to access these records with a shortcut: -You can use `ploneconf.rooms` instead of `ploneconf.site.controlpanel.controlpanel.IPloneconfSettings.rooms`. +You can use `ploneconf.rooms` instead of `ploneconf.site.controlpanels.controlpanel.IPloneconfSettings.rooms`. ``` After reinstalling the package to apply the registry changes, you can access and modify these registry records as described before. @@ -367,7 +370,7 @@ api.portal.get_registry_record('ploneconf.rooms') ``` `````{note} -In training code `ploneconf.site`, we use Python to define the registry records. +In the training project `mastering-plone-project`, we use Python to define the registry records. Alternatively you could add these registry entries with Generic Setup. The following creates a new entry `ploneconf.talk_submission_open` with Generic Setup: @@ -396,7 +399,7 @@ See https://github.com/plone/Products.CMFPlone/blob/master/Products/CMFPlone/pro Now you'll add a custom control panel to edit all settings related to the package with a user-friendly interface. -To register a control panel for the frontend, add the following `RegistryConfigletPanel` to {file}`controlpanel/controlpanel.py`. +To register a control panel for the frontend, add the following `RegistryConfigletPanel` to {file}`backend/src/ploneconf/site/controlpanels/controlpanel.py`. The `RegistryConfigletPanel` uses the schema and will serve as a factory for a control panel configlet. ```{code-block} python @@ -431,24 +434,26 @@ class PloneConfRegistryConfigletPanel(RegistryConfigletPanel): ``` -If you want to use this control panel in Classic UI as well, see https://2022.training.plone.org/mastering-plone/registry.html#add-a-custom-control-panel, which also handles the Classic UI version. +```{tip} -The factory is used in {file}`controlpanel/configure.zcml` for a named adapter: +If you want to use this control panel in Blicca as well, see https://2022.training.plone.org/mastering-plone/registry.html#add-a-custom-control-panel, which also shows the Blicca version. +``` + +Update {file}`backend/src/ploneconf/site/controlpanels/configure.zcml` to register `PloneConfRegistryConfigletPanel` as a named adapter: ```{code-block} xml :linenos: + factory=".controlpanel.PloneConfRegistryConfigletPanel" + name="ploneconf-controlpanel" + /> ``` -Finally register in {file}`profiles/default/controlpanel.xml` the configlet with Generic Setup so that it gets listed in the {guilabel}`Site Setups` panels list (often called 'control panel'). -Therefore the named adapter "ploneconf-controlpanel" provides the schema for the form of the control panel configlet. +Finally, in {file}`backend/src/ploneconf/site/profiles/default/controlpanel.xml`, register the configlet with Generic Setup so that it gets listed in Site Setup. ```{code-block} xml :linenos: -:emphasize-lines: 9 @@ -464,7 +469,7 @@ Therefore the named adapter "ploneconf-controlpanel" provides the schema for the ``` -After applying the profile (for example, by reinstalling the package), your control panel configlet shows up on http://localhost:3000/controlpanel/controlpanel +After applying the profile (for example, by reinstalling the package), your control panel shows up on http://localhost:3000/controlpanel. ```{figure} _static/volto_ploneconf_controlpanel_overview.png ``` @@ -473,15 +478,15 @@ After applying the profile (for example, by reinstalling the package), your cont ```{figure} _static/volto_ploneconf_controlpanel.png ``` -As you can see in the control panel configlet for the `ploneconf.site` package, the entries can be modified and reordered. -Changes are reflected in the registry because the configlet is registered with the schema of the registry fields. +As you can see in the control panel for the `ploneconf.site` package, the entries can be modified and reordered. +Changes are stored in the registry because the configlet is registered with the schema of the registry fields. ````{note} **Frontend widgets** A short remark on the frontend widget. We want the `VocabularyTermsWidget` to be applied. -Thus we specify a hint, using a so-called "tagged value", the name of the frontend widget to be applied for the three control panel fields in our backend schema. +Thus we use the widget "directive" to specify the name of the frontend widget to be used for the three control panel fields in our backend schema. Thus no widget registration in the frontend app is needed. ```python @@ -497,7 +502,7 @@ This is also the way you would configure a content type schema, where you may wa A widget component in your frontend package would be mapped to a key "mywidget". In your content type schema you would add a widget directive with -`frontendOptions={"widget": "mywidget"}` +`frontendOptions={"widget": "mywidget"}`. ```` (vocabularies-label)= @@ -517,11 +522,10 @@ They have many benefits: - Developers can set vocabularies dynamically. The available options may vary based on existing content, the user's role, or even the time of day. -Create a file {file}`vocabularies/talk.py` and write code that generates vocabularies from these settings: +Create a file {file}`backend/src/ploneconf/site/vocabularies/talk.py` and write code that generates vocabularies from these settings: ```{code-block} python :linenos: -:emphasize-lines: 13-15 from plone import api from zope.interface import provider @@ -586,22 +590,24 @@ The `SimpleVocabulary.fromItems()` is a method that takes the list of dictionari and creates a Zope vocabulary. This `SimpleVocabulary` instance has methods that Plone uses to display select widgets, display the rendered content type instance according the user language, etc.. -You can now register these vocabularies as named utilities in {file}`vocabularies/configure.zcml`: +You can now register these vocabularies as named utilities in {file}`backend/src/ploneconf/site/vocabularies/configure.zcml`: ```xml - - - - + + + ``` -From now on you can use these vocabulary by referring to their name, for example, `ploneconf.rooms`. +From now on you can use these vocabularies by referring to their name, for example, `ploneconf.rooms`. ```{note} - Plone comes with many useful named vocabularies that you can use in your own projects, for example `plone.app.vocabularies.Users` or `plone.app.vocabularies.PortalTypes`. @@ -614,10 +620,10 @@ From now on you can use these vocabulary by referring to their name, for example ``` ```{seealso} -Plone documentation [Vocabularies](https://5.docs.plone.org/external/plone.app.dexterity/docs/advanced/vocabularies.html). +Plone documentation: {doc}`plone6docs:backend/vocabularies` ``` -## Using vocabularies in a schema +## Use vocabularies in a schema To use a vocabulary in a schema field, replace the attribute `values` with `vocabulary`, and point to a vocabulary by its name: @@ -632,12 +638,10 @@ type_of_talk = schema.Choice( ) ``` -Don't forget to add the new field `room`. - -Edit {file}`content/talk.py`: +Edit {file}`backend/src/ploneconf/site/content/talk.py`. Don't forget to add the new field `room`. ```{code-block} python -:emphasize-lines: 19, 33, 81-85 +:emphasize-lines: 19, 34, 78-82 :linenos: from plone.app.textfield import RichText @@ -700,11 +704,6 @@ class ITalk(model.Schema): required=False, ) - twitter = schema.TextLine( - title="Twitter name", - required=False, - ) - github = schema.TextLine( title="Github username", required=False, @@ -734,36 +733,37 @@ class Talk(Container): """Talk instance class""" ``` +Now, after restarting, these fields on the talk content type should show the options that are configured in the control panel. + + ## Adjust frontend according schema changes -With the new key value pairs (token/title) we adjust the component accordingly: +Now that we have vocabulary terms with a title that can be different from the token, we adjust {file}`frontend/packages/volto-ploneconf-site/src/components/views/TalkView.jsx` to show the title: -```jsx - {content.audience?.map((item) => { - let color = color_mapping[item.token] || 'green'; - return ( - - ); - })} +```{code-block} jsx +:emphasize-lines: 6 + + {content.audience?.map((item) => { + const audience = item.token; + const color = colorMapping[audience] || 'green'; + return ( +
+ {item.title} +
+ ); + })} ``` One tiny thing is still missing: We should display the room. +In the same file, add this after the `When` component: -Modify {file}`frontend/src/components/Views/Talk.jsx` an add this after the `When` component: - -```{code-block} -:emphasize-lines: 6 - - {content.room && ( - <> -
- Where -
-

{content.room.title}

- - )} +```{code-block} jsx + {content.room && ( + <> +
Where
+

{content.room.title}

+ + )} ``` ````{dropdown} The complete TalkView @@ -771,26 +771,21 @@ Modify {file}`frontend/src/components/Views/Talk.jsx` an add this after the `Whe :icon: question ```jsx -import { - Container as SemanticContainer, - Header, - Image, - Label, - Segment, -} from 'semantic-ui-react'; -import { flattenToAppURL } from '@plone/volto/helpers'; -import { When } from '@plone/volto/components/theme/View/EventDatesInfo'; +import { Container as SemanticContainer } from 'semantic-ui-react'; import config from '@plone/volto/registry'; +import { When } from '@plone/volto/components/theme/View/EventDatesInfo'; + +const colorMapping = { + beginner: 'green', + advanced: 'yellow', + professional: 'purple', +}; const TalkView = (props) => { const { content } = props; const Container = config.getComponent({ name: 'Container' }).component || SemanticContainer; - const color_mapping = { - beginner: 'green', - advanced: 'yellow', - professional: 'purple', - }; + const Image = config.getComponent({ name: 'Image' }).component; return (

@@ -800,12 +795,10 @@ const TalkView = (props) => { {content.description && (

{content.description}

)} - +
{content.start && !content.hide_date && ( <> -
- When -
+
When
{ )} {content.room && ( <> -
- Where -
+
Where

{content.room.title}

)} {content.audience && ( - <> -
- Audience -
- {content.audience?.map((item) => { - let color = color_mapping[item.token] || 'green'; - return ( - - ); - })} - +
Audience
)} - + {content.audience?.map((item) => { + const audience = item.token; + const color = colorMapping[audience] || 'green'; + return ( +
+ {item.title} +
+ ); + })} +
- - {content.speaker &&
{content.speaker}
} +
+ {content.speaker && ( +
{content.speaker}
+ )} {content.website ? (

{content.company || content.website} @@ -853,16 +843,6 @@ const TalkView = (props) => { Email: {content.email}

)} - {content.twitter && ( -

- X:{' '} - - {content.twitter.startsWith('@') - ? content.twitter - : '@' + content.twitter} - -

- )} {content.github && (

Github:{' '} @@ -872,11 +852,9 @@ const TalkView = (props) => {

)} {content.speaker} {content.speaker_biography && (
{ }} /> )} - +
); }; diff --git a/docs/mastering-plone/relations.md b/docs/mastering-plone/relations.md index b1d3d9bef..c485b79ea 100644 --- a/docs/mastering-plone/relations.md +++ b/docs/mastering-plone/relations.md @@ -12,41 +12,25 @@ myst: # Relations -You can model relationships between content items by placing them in a hierarchy (for example a (folderish) page _speakers_ containing the (folderish) speakers and within each speaker the talks) or by linking them to each other in blocks. -But where would you then store a talk that two speakers give together? - -Relations allow developers to model relationships between objects without using links or a hierarchy. -The behavior {py:class}`plone.app.relationfield.behavior.IRelatedItems` provides the field {guilabel}`Related Items` in the section {guilabel}`Categorization`. -That field simply says `a` is somehow related to `b`. - -By using custom relations you can model your data in a much more meaningful way. +```{card} +In this part you will use relations to connect talks to other content items. +Tools and techniques covered: -````{card} Backend and frontend chapter - -Check out the code at the relevant tags! +- relation fields +``` -Code for the beginning of this chapter: +````{card} -```shell -# frontend -git checkout sponsors -``` +Check out `mastering-plone-project` at tag `user_generated_content`: ```shell -# backend git checkout user_generated_content ``` -Code for the end of this chapter: - -```shell -# frontend -git checkout relations -``` +The code at the end of the chapter: ```shell -# backend git checkout relations ``` @@ -54,16 +38,24 @@ More info in {doc}`code` ```` -## Creating and configuring relations in a schema +You can model relationships between content items by placing them in a hierarchy — for example a (folderish) page _speakers_ containing the speakers and within each speaker their talks — or by linking them to each other in blocks. +But where would you then store a talk that two speakers give together? + +Relations allow developers to model relationships between objects without using links or a hierarchy. +The behavior `plone.relateditems` provides the field {guilabel}`Related Items` in the section {guilabel}`Categorization`. +That field simply says `a` is somehow related to `b`. + +By using custom relations you can model your data in a much more meaningful way. + + +## Use relation fields in a schema -Relate to **one** item only with `RelationChoice`. +Relate to **one** item only with a `RelationChoice` field. ```{code-block} python -:linenos: from z3c.relationfield.schema import RelationChoice - speaker = RelationChoice( title="Speaker", description="The speaker of the talk", @@ -72,16 +64,15 @@ from z3c.relationfield.schema import RelationChoice ) ``` -Relate to **multiple** items with `RelationList`. +Relate to **multiple** items with a `RelationList` field. ```{code-block} python -:linenos: from z3c.relationfield.schema import RelationChoice from z3c.relationfield.schema import RelationList - speaker = RelationList( + speakers = RelationList( title="Speaker", description="Speakers of the talk", value_type=RelationChoice( @@ -93,19 +84,16 @@ from z3c.relationfield.schema import RelationList ``` ```{seealso} -[Relation fields in docs.plone.org](https://6.docs.plone.org/volto/development/widget.html?highlight=staticcatalogvocabulary#relation-fields) +Plone documentation {ref}`widget-relation-field-label` ``` - -### Controlling what to relate to - -The vocabulary controls which content instances can be related to from the field. +The vocabulary controls which content items can be the target of the relation. ```{code-block} python :linenos: :emphasize-lines: 5 - speaker = RelationList( + speakers = RelationList( title="Speaker", description="Speakers of the talk", value_type=RelationChoice( @@ -119,7 +107,7 @@ The vocabulary controls which content instances can be related to from the field We want to relate to content instances of type 'speaker'. So we define a vocabulary of speakers. -{file}`src/ploneconf/site/vocabularies/configure.zcml` +{file}`backend/src/ploneconf/site/vocabularies/configure.zcml` ```{code-block} xml :linenos: @@ -130,7 +118,7 @@ So we define a vocabulary of speakers. /> ``` -{file}`src/ploneconf/site/vocabularies/speaker.py` +{file}`backend/src/ploneconf/site/vocabularies/speaker.py` ```{code-block} python @@ -157,26 +145,48 @@ def SpeakerVocabularyFactory(context=None): The widget allows the editor to edit the relations. -The default and only widget by now in Volto is the `select` widget. -It opens the tree of content to be selected by the editor. -On saving the talk, the selection is validated according the vocabulary. +The default widget for relation fields in Volto is the object browser widget, which opens the tree of content for the editor to browse and select. +On saving the talk, the selection is validated against the vocabulary. +That also means that if you select anything that is not a published speaker, you will get an error message. + +One way to work around this is to use a select widget that only allows you to choose from the field's vocabulary. + +```{code-block} python +:linenos: +:emphasize-lines: 9-14 + textindexer.searchable("speakers") + speakers = RelationList( + title="Speakers", + description="Speakers of the talk", + value_type=RelationChoice(vocabulary="ploneconf.speakers"), + required=False, + default=[], + ) + directives.widget( + "speakers", + frontendOptions={ + "widget": "select", + }, + ) -For a more sophisticated widget see the explanation on how to write a custom widget in {doc}`plone6docs:volto/development/widget`. +``` +For more info on widgets see {doc}`plone6docs:volto/development/widget`. -## Accessing and displaying related items + +## Access and display related items The values of a relation 'speaker' can be displayed in 'TalkView' by iterating over the values. ```{code-block} jsx -{content.speaker?.length > 0 && - content.speaker.map((el) => ( +{content.speakers?.length > 0 && + content.speakers.map((el) => ( {el.title} ))} ``` -Available attributes of the speakers are: +The target items of the relation are serialized with the "summary serialization" which includes a limited number of fields: ```{code-block} js @@ -254,9 +264,9 @@ Available attributes of the speakers are: ``` -## Inspecting relations +## Inspect relations -In Plone 6 Volto you can inspect all relations and inverse relations in your site using the control panel `relations` . +You can inspect all relations and inverse relations in your site using the Relations control panel at . You can even edit the relations. ```{figure} _static/inspect-relations_volto_annotations.png @@ -284,7 +294,7 @@ Links and references ## Programming with relations -Since Plone 6 `plone.api` has methods to create, read, and delete relations and inverse relations. +`plone.api` has methods to create, read, and delete relations. ```{code-block} python :linenos: @@ -312,7 +322,7 @@ List all relations of name "speaker": ```{code-block} python >>> for rel in api.relation.get(relationship="speaker"): rel.from_object, rel.to_object, rel.from_attribute -... +... (, , 'speaker') (, , 'speaker') ``` @@ -320,31 +330,29 @@ List all relations of name "speaker": See the chapter {ref}`plone6docs:chapter-relation` of the docs for `plone.api` for more details. -### Plone 5.2 and older - -In older Plone versions you can use [collective.relationhelpers](https://pypi.org/project/collective.relationhelpers) to create and read relations and inverse relations in a very similar way. - - ## Exercise 1 -Add a content type speaker and modify the content type talk to relate to speakers. -Write an upgrade step for the change of the field 'speaker'. +Add a Speaker content type and modify the Talk content type to relate to speakers. +Write an upgrade step for the change of the field `speaker`. -The code can be found in backend add-on `ploneconf.site` at tag `relations`. +```{dropdown} Solution +:animate: fade-in-slide-down +:icon: question + +The code can be found in the `mastering-plone-project` repository at tag `relations`. +``` ## Exercise 2 The speaker is now a relation on talk. -Available on the TalkView is a subset of attributes of the speaker. -How would you achieve to show the GitHub handle of the speaker? -It is by now not included in the available attributes. - +The TalkView includes a subset of attributes of the speaker. +How would you achieve showing the GitHub handle of the speaker? +So far, it is not included in the available attributes. ```{dropdown} Solution :animate: fade-in-slide-down :icon: question -Add the name of the field to the relevant serializer implementing `IJSONSummarySerializerMetadata` in `src/ploneconf/site/serializers/summary.py` +Add the name of the field to the relevant serializer implementing `IJSONSummarySerializerMetadata` in `backend/src/ploneconf/site/serializers/summary.py` ``` - diff --git a/docs/mastering-plone/searchable.md b/docs/mastering-plone/searchable.md index b44901a0b..21110fb8b 100644 --- a/docs/mastering-plone/searchable.md +++ b/docs/mastering-plone/searchable.md @@ -12,17 +12,15 @@ myst: # Search for additional fields ```{card} -In this part you will: - -- Ensure that the detailed description of a talk and its speaker are included in search. +In this part you will ensure that the detailed description of a talk and its speaker are included in search. ``` -````{card} Backend chapter +````{card} -Checkout `ploneconf.site` at tag "upgrade_steps": +Check out `mastering-plone-project` at tag `listing_variation`: ```shell -git checkout upgrade_steps +git checkout listing_variation ``` The code at the end of the chapter: @@ -36,16 +34,15 @@ More info in {doc}`code` In the previous chapter we prepared a search block to search for talks. You may have noticed that a search for speaker names does not show the expected results. -We add the two fields of a talk "speaker" and "details" to the `SearchableText` index. -This is the index that's addressed when making a full text search. - +We need to add the two fields of a talk `speaker` and `details` to the `SearchableText` index. +This is the index that's used when making a full text search. -The package `plone.app.dexterity.textindexer` allows to add fields to the `SearchableText`index. +The package `plone.app.dexterity.textindexer` allows to add fields to the `SearchableText` index. The package is already installed with Plone. Add the behavior `plone.textindexer` to the list of behaviors of your content type. -{file}`src/ploneconf/site/profiles/default/types/talk.xml` +{file}`backend/src/ploneconf/site/profiles/default/types/talk.xml` ```{code-block} xml :emphasize-lines: 7 @@ -63,13 +60,15 @@ Add the behavior `plone.textindexer` to the list of behaviors of your content ty Now you need to mark the fields you want to include in your `SearchableText` index. This can be done with the `searchable` directive. -{file}`src/ploneconf/site/profiles/default/types/talk.xml` +{file}`backend/src/ploneconf/site/content/talk.py` ```{code-block} python -:emphasize-lines: 1, 3, 11 +:emphasize-lines: 1, 5, 13 from plone.app.dexterity import textindexer +# ... + textindexer.searchable("details") details = RichText( title="Details", @@ -91,7 +90,7 @@ The `SearchableText` index now includes your fields. To be able to search for text in the detailed description of a talk and its speaker we need to restart Plone and update the catalog. This can be done with an upgrade step. Do you remember the earlier chapter {doc}`upgrade_steps`? -You can use the function `update_indexes` for a new upgrade step. +You need an upgrade step which runs the `typeinfo` import step to add the new behavior, and runs the `update_indexes` function to reindex existing talks. ```{figure} _static/searchable.png diff --git a/docs/mastering-plone/trainer.md b/docs/mastering-plone/trainer.md index ed712e7bf..cd02c37de 100644 --- a/docs/mastering-plone/trainer.md +++ b/docs/mastering-plone/trainer.md @@ -7,9 +7,29 @@ You are about to give a training. Here are some tips what to prepare before. ## Technical set up to do before a training -- Prepare a mail server for the user registration mail (See {ref}`features-mailserver-label`) -- Update the Plone version in [training_buildout](https://github.com/collective/training_buildout/blob/main/constraints.txt) of collective/training_buildout. -- Update the Volto version in [volto-ploneconf](https://github.com/collective/volto-ploneconf/blob/main/mrs.developer.json). +- Update the Plone and Volto versions in [mastering-plone-project](https://github.com/collective/mastering-plone-project). - If you do only a part of the training, prepare an installation with the steps of the previous chapters. -See {doc}`/mastering-plone/code` for more information. +See {doc}`code` for more information. + +## Update the example code + +This section is for trainers who want to update the example project code to match a change in the training documentation. + +The tags for each chapter described in {doc}`code` are actually branches, so that they can more easily be updated. + +If you need to change code in the middle of the training, you have to rebase all the branches that come after it, so that they include the updated code. + +That looks like this: + +1. Check out the branch you need to update. +2. Commit the changes there. (You can use `git commit --amend` if you want to rewrite the commit instead of adding a new one.) +3. Check out the next branch. Rebase it onto the previous one. +4. Force push the updated branch to GitHub. +5. Continue checking out and rebasing each subsequent branch until you get to `main`. + +The final result should look like this: + +```{figure} _static/code_tree.png +:align: center +``` diff --git a/docs/mastering-plone/upgrade_steps.md b/docs/mastering-plone/upgrade_steps.md index 821f3b0ff..c47851a7d 100644 --- a/docs/mastering-plone/upgrade_steps.md +++ b/docs/mastering-plone/upgrade_steps.md @@ -15,8 +15,6 @@ myst: In this part you will: - Write code to update, create and move content -- Create custom catalog indexes -- Create criteria for search and listing blocks - Enable features with upgrade steps Tools and techniques covered: @@ -24,9 +22,9 @@ Tools and techniques covered: - upgrade steps ``` -````{card} Backend chapter +````{card} -Checkout `ploneconf.site` at tag "schema": +Check out `mastering-plone-project` at tag `schema`: ```shell git checkout schema @@ -41,39 +39,48 @@ git checkout upgrade_steps More info in {doc}`code` ```` - -(upgrade-steps-upgrades-label)= - -## Upgrade steps - -You recently changed existing content, when you added the behavior `ploneconf.featured` or when you turned talks into events in the chapter {doc}`events`. +You recently changed the site configuration, when you added the behavior `ploneconf.featured` or when you turned talks into events in the chapter {doc}`events`. When projects evolve you sometimes want to modify various things while the site is already up and brimming with content and users. Upgrade steps are pieces of code that run when upgrading from one version of an add-on to a newer one. They can do just about anything. We will use an upgrade step to enable the new behavior instead of reinstalling the add-on. +Upgrade steps help make sure that changes are applied to multiple instances of the site in a consistent way. +For example, you might have multiple environments (development, staging, production) or multiple developers working on their own local copies of the site. +Once an upgrade step is defined, it can be applied in the same way to all of these, instead of making changes manually through the web. + + +(upgrade-steps-add-steps-label)= + +## Add upgrade steps + We will create an upgrade step that: -- runs the typeinfo step, i.e. loads the GenericSetup configuration stored in `profiles/default/types.xml` and `profiles/default/types/...` so we don't have to reinstall the add-on to have our changes from above take effect and +- runs the typeinfo step, i.e. loads the GenericSetup configuration stored in `profiles/default/types.xml` and `profiles/default/types/...` so we don't have to reinstall the add-on to have our changes from above take effect. - cleans up existing talks that might be scattered around the site in the early stages of creating it. We will move all talks to a (folderish) page `talks` (unless they already are there). Upgrade steps can be registered in their own ZCML file to prevent cluttering the main {file}`configure.zcml`. -Update the {file}`upgrades/configure.zcml`: +Update the file {file}`backend/src/ploneconf/site/upgrades/configure.zcml`: ```{code-block} xml -:emphasize-lines: 9, 14 +:linenos: + + - + + ``` -The upgrade steps bumps the version number of the GenericSetup profile of {py:mod}`ploneconf.site` from 1000 to 1001. -The version is stored in {file}`profiles/default/metadata.xml`. +The `upgradeDepends` directive runs the normal `typeinfo` import step, which is the one that processes the files in the profile's `types` folder. +The `upgradeStep` directive runs a custom handler that we will add below. + +````{tip} +Have a look at Generic Setup import steps in the ZMI at http://localhost:8080/Plone/portal_setup/manage_importSteps to find the import step id. + +```{figure} _static/import_steps.png +:alt: Import steps + +Import step ids for upgradeDepends +``` +```` -Change it to +The upgrade step is registered to run when the profile version increased from 1000 to 1001. +The current version is stored in {file}`profiles/default/metadata.xml`. +Change it to: ```xml 1001 ``` -`GenericSetup` now expects the code as a method {py:meth}`cleanup_site_structure` and {py:meth}`update_types` in the file {file}`upgrades/v1001.py`. -Let's create it. +Now let's add a file {file}`backend/src/ploneconf/site/upgrades/v1001.py` with our custom `cleanup_site_structure` handler code. (upgrade-steps-pycode-label)= -{file}`upgrades/v1001.py` - ```{code-block} python :linenos: from plone import api -from plone.app.upgrade.utils import loadMigrationProfile import logging -default_profile = "profile-ploneconf.site:default" logger = logging.getLogger(__name__) -def reload_gs_profile(setup_tool): - """Load default profile""" - loadMigrationProfile( - setup_tool, - default_profile, - ) - - -def update_types(setup_tool): - setup_tool.runImportStepFromProfile(default_profile, "typeinfo") - - def cleanup_site_structure(setup_tool): - # Load default profile including new type info - # This makes 'update_types' superfluous. - reload_gs_profile(setup_tool) - portal = api.portal.get() # Create the expected site structure @@ -172,21 +172,18 @@ def cleanup_site_structure(setup_tool): logger.info(f"{obj.absolute_url()} moved to {schedule_folder_url}") ``` +We create the required site structure if it does not exist yet. +The code makes extensive use of `plone.api` as discussed in the chapter {doc}`api`. -We create the required site structure if it does not exist yet making extensive use of `plone.api` as discussed in the chapter {doc}`api`. -Have a look at ZMI import steps http://localhost:8080/Plone/portal_setup/manage_importSteps to find the upgrade step id for the type upgrade. +(upgrade-steps-run-steps-label)= -```{figure} _static/import_steps.png -:alt: Import steps - -Import step ids for runImportStepFromProfile -``` +## Run upgrade steps After restarting the site we can run the upgrade step: - Go to the {guilabel}`Add-ons` control panel . - The add-on `ploneconf.site` should now be marked with an {guilabel}`Upgrade` label and have a button to upgrade from 1000 to 1001. + The add-on `ploneconf.site` should now be marked with an {guilabel}`Update` label and have a button to upgrade from 1000 to 1001. - Run the upgrade step by clicking on it. On the console you should see logging messages like: @@ -206,216 +203,10 @@ Alternatively you can also select which upgrade steps to run like this: ``` -(upgrade-steps-catalog-label)= - -## Add catalog index - -For the next chapter we need to search for sponsors and get the results with the values of field 'url' and 'level. -We add a metadata column for these fields to not wake up objects on search request. -This would be OK, but time consuming. -The search request gets an attribute from catalog brains unless the attribute is not available, then fetches the real object. - -Add the new meta data columns 'level' and 'url' to {file}`profiles/default/catalog.xml` - -```{code-block} xml - - - -``` - -While we are at it, we also add some more indexes and criteria for fields of type talk. -With these indexes and criteria we can create listing and search blocks with facets. - -```{code-block} xml -:emphasize-lines: 18-36 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -This adds new indexes for the three fields we want to show in the listing. -Note that _audience_ is a {py:class}`KeywordIndex` because the field is multi-valued, but we want a separate index entry for every value in an object. - -A reinstallation of the add-on would leave the new catalog indexes empty. -Therefore we write an upgrade step to not only add indexes and criteria, but also reindex all talks: - -`src/ploneconf/site/upgrades/v1001.py`: - -```python -def update_indexes(setup_tool): - # Indexes and metadata - setup_tool.runImportStepFromProfile(default_profile, "catalog") - # Criteria - setup_tool.runImportStepFromProfile(default_profile, "plone.app.registry") - # Reindexing content - for brain in api.content.find(portal_type=["talk", "sponsor"]): - obj = brain.getObject() - obj.reindexObject() - logger.info(f"{obj.id} reindexed.") -``` - -`src/ploneconf/site/upgrades/configure.zcml`: - -```{code-block} xml -:emphasize-lines: 21-25 - - - - - - - - - -``` - -From time to time you may want to update the catalog manually. To do so, go to , select the new indexes and click {guilabel}`Reindex`. -You can also rebuild the whole catalog by going to the {guilabel}`Advanced` tab and clicking {guilabel}`Clear and Rebuild`. - - -(upgrade-steps-collection-criteria-label)= - -## Add collection criteria - -The following additional criteria allow us to create a search block constrained to talks with facets to filter for audience, speaker and room. - -`profiles/default/registry/querystring.xml` - -```{code-block} xml - - - Speaker - True - True - - plone.app.querystring.operation.string.is - plone.app.querystring.operation.string.contains - - Metadata - - - - Audience - True - False - - plone.app.querystring.operation.selection.any - plone.app.querystring.operation.selection.all - plone.app.querystring.operation.selection.none - - Metadata - ploneconf.audiences - - - - Room - True - False - - plone.app.querystring.operation.selection.any - plone.app.querystring.operation.selection.all - plone.app.querystring.operation.selection.none - - Metadata - ploneconf.rooms - -``` - -```{seealso} -For a full list of all existing QueryField declarations see https://github.com/plone/plone.app.querystring/blob/master/plone/app/querystring/profiles/default/registry.xml#L197. - -For a full list of all existing operations see https://github.com/plone/plone.app.querystring/blob/master/plone/app/querystring/profiles/default/registry.xml#L1. -``` - - -(upgrade-steps-search-block-label)= - -## Apply the new criterion to create a search block for talks - -As soon as you run the upgrade steps, you can now add a search block to your 'schedule' page that provides facets to filter for audience, et cetera. - -```{figure} _static/search_block.png -:alt: search block - -search block -``` - (upgrade-steps-summary-label)= ## Summary -- You wrote your first upgrade step - - to enable changes on types - - update content - - prepare your site for search and listing blocks for your custom types +- You wrote your first upgrade step. +- You ran the upgrade step to apply changes to the existing site. diff --git a/docs/mastering-plone/user_generated_content.md b/docs/mastering-plone/user_generated_content.md index 8ecfab9a3..40aca5316 100644 --- a/docs/mastering-plone/user_generated_content.md +++ b/docs/mastering-plone/user_generated_content.md @@ -3,38 +3,35 @@ myst: html_meta: "description": "Configure who can edit what" "property=og:description": "Configure who can edit what" - "property=og:title": "Workflow, Roles and Permissions" + "property=og:title": "Workflow, roles and permissions" "keywords": "Plone, Volto, workflow, role, local role, permission" --- (user-content-label)= -# Workflow, Roles and Permissions - -How do prospective speakers submit talks? -We let them register on the site and grant right to create talks. -For this we go back to changing the site through-the-web. +# Workflow, roles and permissions ```{card} In this part you will: - Allow self-registration -- Constrain which content types can be added to the (folderish) talk page +- Constrain which content types can be added to the schedule folder - Grant local roles - Create a custom workflow for talks Tools and techniques covered: -- workflow +- folder constraints - local roles +- workflow ``` -````{card} Backend chapter +````{card} -Checkout `ploneconf.site` at tag "searchable": +Check out `mastering-plone-project` at tag `block`: ```shell -git checkout searchable +git checkout block ``` The code at the end of the chapter: @@ -46,22 +43,26 @@ git checkout user_generated_content More info in {doc}`code` ```` +How do prospective speakers submit talks? +We let them register on the site and grant the right to create talks. +For this we go back to changing the site through the web. + (user-content-self-reg-label)= ## Self-registration -- Go to the control panel {guilabel}`security` at and enable self-registration. -- Leave "Enable User Folders" off unless you want a community site, in which users can create any content they want in their home folder. -- Select the option 'Use email address as login name'. +- Go to the {guilabel}`Security` control panel at and enable self-registration. +- Leave {guilabel}`Enable User Folders` off unless you want a community site, in which users can create any content they want in their home folder. +- Select the option {guilabel}`Use email address as login name`. (user-content-constrain-types-label)= ## Constrain types to be addable -On the page `schedule` select {guilabel}`Restrictions…` from the {guilabel}`Add new_` menu. -Restrict to adding only talks. +On the schedule page, select {guilabel}`Restrictions…` from the {guilabel}`Add new` menu. +Restrict to only allow adding talks. ```{note} This action is only available in Plone's Classic UI frontend, and not its Volto frontend. @@ -72,10 +73,13 @@ This action is only available in Plone's Classic UI frontend, and not its Volto ## Grant local roles -- Go to {guilabel}`Sharing` and grant the role _Can add_ to the group _logged-in users_. - Now every logged-in user can add content in this folder (and only this folder). +On the schedule page, go to {guilabel}`Sharing`. +Check the box for {guilabel}`Can add` for the group {guilabel}`Logged-in users`, and save. +Now every logged-in user can add content in this folder (and only this folder). -By combining the constrain types and the local roles on this folder, we have achieved, that only logged-in users can create and submit talks in this folderish page. +The {guilabel}`Can add` column grants the `Contributor` role to this group within this folder. + +By combining the type constraints and the local roles on this folder, we have made it so that non-admin users can create and submit talks inside the schedule. (user-content-custom-workflow-label)= @@ -86,45 +90,51 @@ We still need to fix a problem: Authenticated users can see all talks, including Since we do not want this, we will create a modified workflow for talks. The new workflow will only let them see and edit talks they created themselves and not the ones of other users. -- Go to the {menuselection}`ZMI --> portal_workflow` +- Go to the {menuselection}`ZMI --> portal_workflow`: http://localhost:8080/Plone/portal_workflow/manage - See how talks have the same workflow as most content, namely {guilabel}`(Default)` - Go to the tab {guilabel}`Contents`, check the box next to {guilabel}`simple_publication_workflow`, click {guilabel}`copy` and {guilabel}`paste`. -- Rename the new workflow from _copy_of_simple_publication_workflow_ to _talks_workflow_. -- Edit the workflow by clicking on it: Change the Title to _Talks Workflow_. -- Click on the tab {guilabel}`States` and click on {guilabel}`private` to edit this state. In the next view select the tab {guilabel}`Permissions`. -- Find the table column for the role {guilabel}`Contributor` and remove the permissions for {guilabel}`Access contents information` and {guilabel}`View`. Note that the {guilabel}`Owner` (that's the creator) still has some permissions. +- Rename the new workflow from `copy_of_simple_publication_workflow` to `talks_workflow`. +- Edit the workflow by clicking on it: Change the Title to `Talks Workflow`. +- Click on the tab {guilabel}`States` and click on {guilabel}`private` to edit this state. + In the next view select the tab {guilabel}`Permissions`. +- Find the table column for the role {guilabel}`Contributor` and remove the permissions for {guilabel}`Access contents information` and {guilabel}`View`. Note that the {guilabel}`Owner` role (that's the creator) still has some permissions. - Do the same for the state {guilabel}`pending` -- Go back to {file}`portal_workflow` and set the new workflow {file}`talks_workflow` for talks. Click {file}`Change` and then {file}`Update security settings`. +- Go back to {guilabel}`portal_workflow` and set the new workflow {file}`talks_workflow` for talks. + Click {file}`Change` and then {file}`Update security settings`. -The new workflow allows contributors to see and edit talks they created themselves but not the ones of other contributors. +The new workflow allows contributors to see and edit talks they created themselves, but not talks submitted by other contributors until they are published. (user-content-fs-label)= ## Move the changes to the file system -We don't want to do these steps for every new conference by hand so we move the changes into our package. +We don't want to do these steps for every new conference by hand so we move the changes into our Generic Setup profile. -### Export and import the workflow +### Export the workflow -- Export the GenericSetup step _Workflow Tool_ in . +- Export the Generic Setup step _Workflow Tool_ in . -- Drop the file {file}`workflows.xml` into {file}`src/ploneconf/site/profiles/default` an clean out everything that is not related to talks. +- Copy the file {file}`workflows.xml` into {file}`backend/src/ploneconf/site/profiles/default` and clean out everything that is not related to talks. ```xml - - - - + + + + - + - - - ``` + + +``` -- Drop {file}`workflows/talks_workflow/definition.xml` in {file}`src/ploneconf/site/profiles/default/workflows/talks_workflow/definition.xml`. - The other files are just definitions of the default-workflows and we only want things in our package that changes Plone. +- Copy {file}`workflows/talks_workflow/definition.xml` into {file}`backend/src/ploneconf/site/profiles/default/workflows/talks_workflow/definition.xml`. + (The other files are just definitions of the default workflows, and we only want things in our package that changes Plone.) ### Enable self-registration @@ -139,19 +149,64 @@ Most global setting are stored in the registry. You can modify it by adding the ### Grant local roles and constrain types to be addable -Since the granting of local roles applies only to a certain folder in the site we would not always write code for it but do it by hand. -But for testability and repeatability (there is a conference every year!) we should create the initial content structure automatically and also apply needed local roles. +Since the granting of local roles applies only to a certain folder in the site, we could easily do it by hand instead of writing code for it. +But for testability and repeatability (there is a conference every year!), we should create the initial content structure automatically and also apply needed local roles. -We are setting up the initial content of a conference site in an upgrade step explained in {ref}`upgrade step code `. -Let's enhance this by setting local roles and constrain types. -Add the following lines to `cleanup_site_structure`. +Let's add an upgrade step to do this as well as importing the workflow and new registry setting. + +Update the profile version in {file}`backend/src/ploneconf/site/profiles/default/metadata.xml`: + +```{code-block} xml +:linenos: +:emphasize-lines: 3 + + + + 1004 + + profile-plone.volto:default + profile-plone.app.caching:default + profile-plone.app.caching:with-caching-proxy + + +``` + +Register the new upgrade step in {file}`backend/src/ploneconf/site/upgrades/configure.zcml`: + +```{code-block} xml + + + + +``` + +Create the file {file}`backend/src/ploneconf/site/upgrades/v1004.py`: ```{code-block} python :linenos: +from plone import api from Products.CMFPlone.interfaces import constrains +import logging + +logger = logging.getLogger(__name__) +def configure_talk_permissions(context): + talks_folder = api.content.get("/schedule") + # Allow logged-in users to create content api.group.grant_roles( groupname='AuthenticatedUsers', @@ -164,21 +219,23 @@ from Products.CMFPlone.interfaces import constrains behavior.setLocallyAllowedTypes(['talk']) behavior.setImmediatelyAddableTypes(['talk']) logger.info(f'Added and configured {talks_folder.absolute_url()}') - ``` -Once we apply the upgrade step or reinstall our package a page {file}`talks` is created with the appropriate local roles and constraints. +Once we apply the upgrade step, the schedule page is updated with the appropriate local roles and constraints. ## Exercise -We wrote similar code to create the pages in {doc}`upgrade_steps`. -We need it to make sure a sane structure gets created when we create a new site by hand or in tests. +In {doc}`upgrade_steps` we wrote an upgrade step to create the basic page structure of the site. +But we want that to be created not only during an upgrade, but also when a new site is created by hand or in tests. + +One way to do this is to create a list of dictionaries containing the type, parent and title plus optionally workflow state etc. to create an initial structure. +In some projects it could also make sense to have additional profiles besides `default`: -You would usually create a list of dictionaries containing the type, parent and title plus optionally workflow state etc. to create an initial structure. -In some projects it could also make sense to have a separate profile besides `default` which might be called `demo` or `content` that creates an initial structure and maybe another `testing` that creates dummy content (talks, speakers etc) for tests. +- a `demo` or `content` profile that creates the initial structure +- a `testing` profile that creates dummy content (talks, speakers etc) for tests -> Create an optional GenericSetup profile `content` that creates the content, grants local roles and sets constraints. +Create an optional Generic Setup profile `content` that creates the content, grants local roles and sets constraints. ````{dropdown} Solution :animate: fade-in-slide-down diff --git a/docs/mastering-plone/volto_addon.md b/docs/mastering-plone/volto_addon.md deleted file mode 100644 index 660b12e66..000000000 --- a/docs/mastering-plone/volto_addon.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -myst: - html_meta: - "description": "How to enhance your Plone site with additional features from existing add-ons" - "property=og:description": "How to enhance your Plone site with additional features from existing add-ons" - "property=og:title": "Using Volto add-ons" - "keywords": "Plone, frontend, add-on" ---- - -(volto-addon-label)= - -# Using Volto add-ons - -````{card} Frontend chapter - -For Plone backend add-ons see chapter {ref}`add-ons-label` -```` - - -(add-ons-volto-overview-label)= - -## Awesome Volto add-ons - -add-ons enrich a Volto app with specialized blocks, themes, integration of non-Volto Node packages, and more. -A selection of add-ons can be found on: - -- [Awesome Volto](https://github.com/collective/awesome-volto/blob/main/README.md#addons) -- [npm #volto-addon](https://www.npmjs.com/search?q=keywords:volto-addon) -- [GitHub #volto-addon](https://github.com/search?o=desc&q=%23volto-addon&s=&type=Repositories) - -Some add-ons do require a backend add-on, some do not. -A backend add-on is needed for a content type or a `REST API endpoint`. - -Two examples: - -[`@eeacms/volto-matomo`](https://www.npmjs.com/package/@eeacms/volto-matomo) integrates Matomo with Volto sites. -No backend add-on is needed. -The add-on integrates the code snippet from `Matomo` into your pages to ping your web stats installation. - -[`@rohberg/volto-slate-glossary`](https://github.com/rohberg/volto-slate-glossary) adds tooltips to selected pages according a given glossary. -As it depends on a permanent glossary, it communicates with the backend add-on [`collective.glossary`](https://pypi.org/project/collective.glossary/). -The Volto add-on provides, aside from the tooltips, a UI to edit the glossary. - - -## Install an add-on - -Here is how you would install a Volto add-on in your app: - -add-ons that are already released on [npm](https://www.npmjs.com): - -: Update `package.json`: - ```{code-block} json - :emphasize-lines: 5 - - "dependencies": { - "@plone/volto": "workspace:*", - "@plone/registry": "workspace:*", - "volto-ploneconf": "workspace:*", - "@eeacms/volto-matomo": "^5.0.0" - }, - ``` - -: Update `volto.config.js`: - ```{code-block} js - :emphasize-lines: 1 - - const addons = ['@eeacms/volto-matomo', 'volto-ploneconf']; - const theme = ''; - - module.exports = { - addons, - theme, - }; - ``` - -Add-ons that are **not yet released** on `npm` but available on `Github`: - -: Update `package.json`: - ```{code-block} json - :emphasize-lines: 5 - - "dependencies": { - "@plone/volto": "workspace:*", - "@plone/registry": "workspace:*", - "volto-ploneconf": "workspace:*", - "@foo/volto-bar-block": "github:foo/volto-bar-block#x.y.z" - }, - ``` - -: Update `volto.config.js`: - ```{code-block} js - :emphasize-lines: 1 - - const addons = ['@foo/volto-bar-block', 'volto-ploneconf']; - const theme = ''; - - module.exports = { - addons, - theme, - }; - ``` - -Install the new add-on and restart Volto: - -```shell -make install -make start -``` - - -(add-ons-volto-backedupbyplone-label)= - -## Complementing Volto with Plone add-ons - -With some additional features of Volto add-ons in place, where do we need to work if there is more that add-ons do not already implement? -With the split of Plone in backend and frontend, the backend Plone is still the place to shape your data model. -For our training story 'Platform for a Plone Conference' we need to model the content type talk. -So in an earlier {doc}`dexterity` chapter we created a **new Plone Python add-on** `ploneconf.site` that adds the content type `talk`. -And in chapter {doc}`volto_talkview` we created the view for a talk in our custom Volto add-on `volto-ploneconf`. diff --git a/docs/mastering-plone/volto_components_sponsors.md b/docs/mastering-plone/volto_components_sponsors.md index 2702cd2a8..6fc145249 100644 --- a/docs/mastering-plone/volto_components_sponsors.md +++ b/docs/mastering-plone/volto_components_sponsors.md @@ -3,39 +3,36 @@ myst: html_meta: "description": "How to fetch data from the backend" "property=og:description": "How to fetch data from the backend" - "property=og:title": "The Sponsors Component, how to fetch data from the backend" + "property=og:title": "The Sponsors component" "keywords": "REST API, Semantic UI" --- (volto-sponsors-component-label)= -# The Sponsors Component +# The Sponsors component -In a previous chapter {doc}`dexterity_3` you created the `sponsor` content type. +In a previous chapter, {doc}`dexterity_3`, you created the sponsor content type. Now let's learn how to display content of this type. ```{card} -To be solved task in this part: - -- Advert to sponsors on all pages, sorted by level - In this part you will: +- Add a component advertising sponsors at the bottom of all pages - Display data from fetched content Topics covered: -- Create React component -- Use React action of Volto to fetch data from Plone backend via REST API -- Style component with Semantic UI +- Display a React component in a slot +- Use a Volto Redux action to fetch data from Plone backend via the REST API +- Style the component with Semantic UI ``` -````{card} Frontend chapter +````{card} -Checkout `volto-ploneconf` at tag "listing_variation": +Check out `mastering-plone-project` at tag `searchable`: ```shell -git checkout listing_variation +git checkout searchable ``` The code at the end of the chapter: @@ -47,71 +44,39 @@ git checkout sponsors More info in {doc}`code` ```` +```{only} not presentation +For sponsors we will stay with the default view as we will only display the sponsors in the footer and not modify their own pages. +Using what you learned in {doc}`volto_talkview` you should be able to write a view for sponsors if you want to. +``` ```{figure} _static/volto_component_sponsors.png :alt: Sponsors component ``` -```{only} not presentation -For sponsors we will stay with the default view as we will only display the sponsors in the footer and do not modify their own pages. -Using what you learned in {doc}`volto_talkview` you should be able to write a view for sponsors if you wanted to. -``` - (volto-component-component-label)= -## A React component +## Slots React components let you split the UI into independent, reusable pieces, and think about each piece in isolation. +Volto comes with several components like header, footer, and sidebar. +In fact everything in the UI is built of nested components. -- You can write a view component for the current context - like the `TalkView`. -- You can also write components that are visible on all views of content objects. -- Volto comes with several components like header, footer, sidebar. In fact everything of the UI is build of nested components. -- Inspect existing components with the React Developer Tools. - - -(volto-component-sponsors-label)= - -## The Sponsors Component - -**Steps to take** +We've already seen how to register a custom component as the view for a content type. +In this case we'll do something different, and add a component to a slot. +A slot is a defined plugin point in the existing Volto components where add-ons can show their own components. +Adding a sponsors component to the `belowContent` slot will let it be displayed below every content view. -- Copy and customize the Footer component. -- Create component to fetch data from backend and to display the fetched data. - -(volto-component-customizing-label)= - -### Customizing the Footer - -You can override any component that lives inside Volto's source folder `core/packages/volto/src/components` and adapt it to your needs, without touching the original component. - -The sponsors shall live in the footer of a page. To customize the given footer component we copy the `Footer.jsx` file {file}`core/packages/volto/src/components/theme/Footer/Footer.jsx` from Volto. We insert the copied file to our app regarding the original folder structure but inside our customizations folder {file}`packages/ploneconf.site/src/customizations/components/theme/Footer/Footer.jsx`. - -In this file `Footer.jsx` we can now modify the to be rendered html by adding a subcomponent `Sponsors`. - -Be aware that the following code is JSX. JSX is Javascript that can handle html in a handy way. What you see is a component defined as an arrow function. The function returns markup consisting of enriched html: The tag `` forces a rendering of the Sponsors component. +```{seealso} +{doc}`plone6docs:volto/configuration/slots` +``` -```{code-block} jsx -:emphasize-lines: 4 -:linenos: -const Footer = ({ intl }) => ( - - - - -``` +(volto-component-sponsors-label)= -This will show an additional component. -It is visible on all pages as it is a subcomponent of the `Footer` component. -Later on it can be made conditional if necessary. +## The Sponsors component -To create the component `Sponsors` we add a folder {file}`src/components/Sponsors/` with a file {file}`Sponsors.jsx`. In this file we can now define our new component. +To create the component `Sponsors` we add a folder {file}`frontend/packages/volto-ploneconf-site/src/components/Sponsors/` with a file {file}`Sponsors.jsx`. +In this file we can now define our new component. Start with a placeholder to see that your registration actually works: @@ -125,109 +90,132 @@ const Sponsors = () => { export default Sponsors; ``` -A component is just a function that returns markup. - -Go back to your modified `Footer` component. -The `Footer` component needs to know where to find the added `Sponsor` component. -We import the `Sponsor` component at the top of our modified `Footer` component. +A component is just a function that returns JSX markup. -{file}`src/customizations/components/theme/Footer/Footer.jsx`: +Now we can configure this component to be shown in the `belowContent` slot. +Edit the file {file}`frontend/packages/volto-ploneconf-site/config/settings.ts`. -```{code-block} jsx +```{code-block} tsx :linenos: - -import { Sponsors } from '@package/components'; +:emphasize-lines: 5, 31-35 + +import type { ConfigType } from '@plone/registry'; +import type { BlockExtension, ViewsConfig } from '@plone/types'; +import TalkView from '../components/Views/TalkView'; +import TalkListingBlockVariation from '../components/variations/TalkListingBlockVariation'; +import Sponsors from '../components/Sponsors/Sponsors'; + +export default function install(config: ConfigType) { + // Language settings + config.settings.defaultLanguage = 'en'; + // Additional language settings for Volto 19 and above, add as many supported languages as needed + // Languages not added to supportedLanguages will not be included in the build + // config.settings.supportedLanguages = ['en']; + + config.views = { + ...(config.views as ViewsConfig), + contentTypesViews: { + ...config.views.contentTypesViews, + talk: TalkView, + }, + }; + + config.blocks.blocksConfig.listing.variations = [ + ...(config.blocks.blocksConfig.listing.variations as BlockExtension[]), + { + id: 'talks', + title: 'Talks', + template: TalkListingBlockVariation, + }, + ]; + + config.registerSlotComponent({ + slot: 'belowContent', + name: 'sponsors', + component: Sponsors, + }); + + return config; +} ``` -For an import using the alias '@package/components', we need to add a conveniance import in the root `src/components/index.js`. +After restarting the frontend with `make frontend-start`, we are now ready to visit an arbitrary page to see the new component. -```{code-block} jsx -import TalkView from './Views/Talk'; -import Sponsors from './Sponsors/Sponsors'; - -export { Sponsors, TalkView }; +```{tip} +A restart is necessary for Volto to find newly added files. +As long as you just edit existing files of your app, your browser will update automatically. ``` -After restarting the frontend with `make start`, we are now ready to visit an arbitrary page to see the new component. -A restart is necessary on newly added files. As long as you just edit existing files of your app, your browser is updating automagically by app configuration. - (volto-component-datafetching-label)= -### Getting the sponsors data +## Fetch the sponsors data With our `Sponsors` component in place we can take the next step and explore Volto some more to figure out how it does data fetching. -As the data is in the backend, we need to find a way to address it. -Volto provides various predefined actions to communicate with the backend (fetching data, creating content, editing content, etc.). -A Redux action communicates with the backend and has a common pattern: -It addresses the backend via REST API and updates the global app store according to the response of the backend. -A component calls an action and has hereupon access to the global app store (shortened: store) with the fetched data. +As the data is in the backend, we need to find a way to fetch it. +Volto provides various predefined actions to communicate with the backend (fetching content, creating content, editing content, etc.). +A Redux action communicates with the backend and has a common pattern. +It makes a request to the backend via the REST API and updates the global app store (in browser memory) according to the response of the backend. +A component can call actions, and select data from the store. -For more information which actions are already provided by Volto have a look at {file}`core/packages/volto/src/actions`. +For more information which actions are already provided by Volto have a look at {file}`frontend/core/packages/volto/src/actions`. -Our component will use the action `searchContent` to fetch data of all sponsors. -It takes as arguments the path where to search, the information what to search and an argument with which key the data should be stored in the store. +Our component will use the action `searchContent` to fetch the data of all sponsors. +It takes as arguments the path where to search, the search parameters, and an argument with which key the data should be stored in the store. Remember: the result is stored in the global app store. -So if we call the action `searchContent` to fetch data of sponsors, that means data of the instances of content type `sponsor`, then we can access this data from the store. +So if we call the action `searchContent` to fetch data of sponsors (that is, all content items with type `sponsor`), then we can access this data from the store. -The Hook `useEffect` lets you perform side effects in `function components`. We use it to fetch the sponsors data from the backend. +The React hook `useEffect` lets you perform side effects after a component is rendered. +We use it to fetch the sponsors data from the backend when the component is first loaded. ```{code-block} jsx :linenos: +:emphasize-lines: 1-3, 6-20 -const dispatch = useDispatch(); - -useEffect(() => { - dispatch( - searchContent( - '/', - { - portal_type: ['sponsor'], - review_state: 'published', - fullobjects: true, - }, - 'sponsors', - ), - ); -}, [dispatch]); -``` - -#### Search options +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { searchContent } from '@plone/volto/actions/search/search'; -- The default representation for search results is a summary that contains only the most basic information like **title, review state, type, path and description**. -- With the option `fullobjects` all available field values are present in the fetched data. -- Another option is `metadata_fields`, which allows to get more attributes (selection of Plone catalog metadata columns) than the default search. The search is done without a performance expensive fetch via option `fullobjects` as soon as the attributes are available from catalog as metadata. +const Sponsors = () => { + const dispatch = useDispatch(); -Possible **sort criteria** are indices of the Plone catalog. + useEffect(() => { + dispatch( + searchContent( + '/', + { + portal_type: ['sponsor'], + review_state: 'published', + sort_on: 'effective', + metadata_fields: ['level', 'url'], + }, + 'sponsors', + ), + ); + }, [dispatch]); + return

Our sponsors

; +}; -```{code-block} jsx -:linenos: -:emphasize-lines: 10 +export default Sponsors; +``` -const dispatch = useDispatch(); +### Search options -useEffect(() => { - dispatch( - searchContent( - '/', - { - portal_type: ['News Items'], - review_state: 'published', - sort_on: "effective", - }, - 'sponsors', - ), - ); -}, [dispatch]); -``` +- The default representation for search results is a summary that contains only the most basic information like **title, review state, type, path and description**. +- `metadata_fields` lets us specify additional catalog metadata columns that we'd like to include in the results. +- We could also pass the option `fullobjects: true` to get the full serialization of all fields from the content item. + However this requires loading each object fully instead of just using data in the catalog, so it is slower. +- `sort_on` specifies which catalog index should be used to sort the results. -Check which info you get with the search request in Google developer tools: +````{tip} +Check which results you get in the Network tab of the browser developer tools: ```{figure} _static/search_response.png :alt: search response ``` +```` ```{seealso} REST API Documentation {doc}`plone6docs:plone.restapi/docs/source/endpoints/searching` @@ -236,13 +224,16 @@ REST API Documentation {doc}`plone6docs:plone.restapi/docs/source/endpoints/sear (volto-component-store-label)= -### Connection of component and store +## Use the fetched data -Let's connect the store to our component. The Selector Hook `useSelector` allows a `function component` to connect to the store. +Let's connect our component to the data which has been fetched into the store. +The hook `useSelector` allows a component to select a specific part of the data in the store. -It's worth exploring the store of our app with the Redux Dev Tools which are additional Dev Tools to React Dev Tools. +```{tip} +It's worth exploring the store of our app with the Redux Dev Tools. There you can see what is stored in `state.search.subrequests.sponsors`. And you can walk through time and watch how the store is changing. +``` ```{code-block} jsx :linenos: @@ -252,20 +243,22 @@ const sponsors = useSelector((state) => ); ``` -With these both: dispatching the action and a connection to the state in place, the component can call the predefined action `searchContent` and has access to the fetched data via its constant `sponsors`. +Now the component has access to the sponsors data, and will re-render when it is updated after the `searchContent` action has finished. + +````{note} +This note is advanced and can be skipped on a first reading. -The next step is advanced and can be skipped on a first reading. -As by now we fetch the sponsors data on mounting event of the component. +So far we fetch the sponsors data once, after the component is first mounted. The mounting is done once on the first visit of a page of our app. What if a new sponsor is added or a sponsor is published? -We want to achieve a re-rendering of the component on changed sponsorship. -To subscribe to these changes in sponsorship, we extend our already defined connection. +We want to achieve a re-rendering of the component when the user publishes a new sponsor. +To subscribe to these changes in workflow status, we extend the dependencies of the `useEffect` hook. ```{code-block} jsx :emphasize-lines: 1,15 :linenos: -const content = useSelector((state) => state.workflow.transition); +const workflowTransition = useSelector((state) => state.workflow.transition); useEffect(() => { dispatch( @@ -274,24 +267,27 @@ useEffect(() => { { portal_type: ['sponsor'], review_state: 'published', - fullobjects: true, + sort_on: 'effective', + metadata_fields: ['level', 'url'], }, 'sponsors', ), ); -}, [dispatch, content]); +}, [dispatch, workflowTransition]); ``` Listening to this subscription the component fetches the data from the store if a workflow state changes. +```` + (volto-component-presentation-label)= -### Presentation of the prepared data +## Render the sponsor data -With the data fetched and accessible in the component constant `sponsors` we can -now render the sponsors data. +With the data fetched and accessible in the variable `sponsors`, we can +now render the sponsors data. -We prepare the sponsors data as a dictionary grouped by sponsor level: groupedSponsorsByLevel. +We prepare the sponsors data as a dictionary grouped by sponsor level: ```js const groupedSponsorsByLevel = (array = []) => @@ -300,7 +296,7 @@ const groupedSponsorsByLevel = (array = []) => obj[token] ? obj[token].push(item) : (obj[token] = [item]); return obj; }, {}); - ``` +``` Which results in an dictionary Object available with our subscription `sponsors`: @@ -310,7 +306,7 @@ Which results in an dictionary Object available with our subscription `sponsors` } ``` -With the subscription `sponsors` we can now show a nested list. +Now we can show a nested list. ```{code-block} jsx :linenos: @@ -319,21 +315,24 @@ With the subscription `sponsors` we can now show a nested list. return (

{level.toUpperCase()}

- - +
+
{sponsors[level].map((item) => ( - +
- +
))} - - +
+
); })} @@ -348,10 +347,8 @@ With the subscription `sponsors` we can now show a nested list. import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { Segment, Grid } from 'semantic-ui-react'; - -import { ConditionalLink, Component } from '@plone/volto/components'; -import { searchContent } from '@plone/volto/actions'; +import Component from '@plone/volto/components/theme/Component/Component'; +import { searchContent } from '@plone/volto/actions/search/search'; const groupedSponsorsByLevel = (array = []) => array.reduce((obj, item) => { @@ -382,33 +379,38 @@ const Sponsors = () => { }, [dispatch]); return sponsors && Object.keys(sponsors).length > 0 ? ( - -
-

SPONSORS

+
+
+
+

SPONSORS

+
+ {Object.keys(sponsors).map((level) => { + return ( +
+

{level.toUpperCase()}

+
+
+ {sponsors[level].map((item) => ( +
+ +
+ ))} +
+
+
+ ); + })}
- {Object.keys(sponsors).map((level) => { - return ( -
-

{level.toUpperCase()}

- - - {sponsors[level].map((item) => ( - - - - ))} - - -
- ); - })} - +
) : ( <> ); @@ -420,14 +422,15 @@ export default Sponsors; We group the sponsors by sponsorship level. -An Object `sponsors` using the sponsorship level as key helps to build rows with sponsors by sponsorship level. +An object `sponsors` using the sponsorship level as key helps to build rows with sponsors by sponsorship level. -The Volto component `Image` is used to display the logo. -It cares about the markup of an html image node with all necessary attributes in place. +The Volto component `PreviewImage` is used to display the logo. -We also benefit from [Semantic UI React](https://react.semantic-ui.com/) component `Grid` to build our list of sponsors. The styling can be customized but these predefined components help simplifying the code and achieve an app wide harmonic style. +We also benefit from the [Semantic UI](https://semantic-ui.com/) grid component to build our list of sponsors. +The styling can be customized but these predefined components help simplify the code and achieve an app-wide harmonic style. -See the new footer. A restart is not necessary as we didn't add a new file. The browser updates automagically by configuration. +See the new footer. +A restart is not necessary, as we didn't add a new file. ```{figure} _static/volto_component_sponsors.png :align: left @@ -438,14 +441,15 @@ See the new footer. A restart is not necessary as we didn't add a new file. The ## Exercise -Modify the component to display a sponsor logo as a link to the sponsors website. The address is set in sponsor field "url". +Modify the component to display a sponsor logo as a link to the sponsors website. +The address is stored in sponsor field `url`. ````{dropdown} Solution :animate: fade-in-slide-down :icon: question ```{code-block} jsx -:emphasize-lines: 1-5, 13 +import ConditionalLink from '@plone/volto/components/manage/ConditionalLink/ConditionalLink'; @@ -483,8 +490,9 @@ The image component is now rendered with a wrapping anchor tag. ``` ```` -% volto-component-summary-label: +(volto-component-summary-label)= ## Summary -You know how to fetch data from backend. With the data you are able to create a component displayed at any place in the website. +You know how to fetch data from backend. +With the data you are able to create a component displayed at any place in the website. diff --git a/docs/mastering-plone/volto_custom_addon.md b/docs/mastering-plone/volto_custom_addon.md deleted file mode 100644 index dab48a59c..000000000 --- a/docs/mastering-plone/volto_custom_addon.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -myst: - html_meta: - "description": "Write your own Volto add-on" - "property=og:description": "Write your own Volto add-on" - "property=og:title": "Extending Volto with a custom add-on package" - "keywords": "Plone, Volto, add-on, development" ---- - -(volto-custom-addon-label)= - -# Extending Volto with a custom add-on package - -````{card} Frontend chapter - -See {ref}`voting-story-backend-package-label` for extending Plone with backend add-ons. -```` - -As soon as you have repeating needs in Volto projects, you will want to move the code to an add-on that can be applied to multiple projects. One of several ways to start with a new add-on is the [cookieplone](https://github.com/plone/cookieplone) Cookiecutter template. - - -(volto-custom-addon-preparation-label)= - -If you haven't prepared 'cookieplone' already, see https://github.com/plone/cookieplone?tab=readme-ov-file#installation- for installation. - - -'cookieplone' creates an add-on that comes with a Volto app. -So you can start a Volto app right after creation of the add-on and proceed with developing your add-on. - -```shell -pipx run cookieplone -``` - -Go to the app folder and run the embedding app with: - -```shell -make start -``` - -You now have a Volto app running which includes the new add-on. - -Open `packages/your-add-on` and start coding. - - -(volto-custom-addon-final-label)= - -```{note} Step to the next chapter and come back here for a release. -We will create a new block type in the next chapter {doc}`volto_custom_addon2`. -We will do this in an add-on to apply the feature to multiple projects. -``` - -````{note} -Coming back here with the new block type, you can now release the new add-on to npm. - -```{code-block} console -make release -```` - -(volto-custom-addon-include-label)= - -## Enrich an existing project with your new released add-on - -You already released your add-on. Go on with {file}`package.json` and {file}`volto.config.js` and include your new add-on. - -: Update `package.json`: - ```{code-block} json - :emphasize-lines: 5 - - "dependencies": { - "@plone/volto": "workspace:*", - "@plone/registry": "workspace:*", - "volto-ploneconf": "workspace:*", - "@greenthumb/volto-qa-block": "^1.0.0" - }, - ``` - -: Update `volto.config.js`: - ```{code-block} js - :emphasize-lines: 1 - - const addons = ['@greenthumb/volto-qa-block', 'volto-ploneconf']; - const theme = ''; - - module.exports = { - addons, - theme, - }; - ``` - -Modify versions as necessary. - -Install new add-on and restart Volto: - -```shell -make install -make start -``` - - -## Create a new project with your new released add-on - -This is the same procedure as creating an add-on. - -```shell -pipx run cookieplone -``` - -Follow the steps from previous section {ref}`volto-custom-addon-include-label`. diff --git a/docs/mastering-plone/volto_custom_addon2.md b/docs/mastering-plone/volto_custom_addon2.md deleted file mode 100644 index a2407a983..000000000 --- a/docs/mastering-plone/volto_custom_addon2.md +++ /dev/null @@ -1,556 +0,0 @@ ---- -myst: - html_meta: - "description": "Simple block architecture" - "property=og:description": "Simple block architecture" - "property=og:title": "Creating a custom block" - "keywords": "Plone, Volto, block, add-on" ---- - -(volto-custom-addon2-label)= - -# Creating a custom block - -````{card} Frontend chapter - -Creating a new block type -```` - -We want to provide some information for speakers of the conference: Which topics are possible? What do I have to consider speaking at an online conference? FAQ section would come in handy. This could be done by creating a block type that offers a form for question and answer pairs and displays an accordion. - -Let's start with our fresh add-on we created in the last chapter {doc}`volto_custom_addon`. - -```{figure} _static/volto_addon_accordion_display.png -:alt: Volto add-on volto-accordion-block -``` - -```{figure} _static/volto_addon_accordion_sidebar.png -:alt: Editing Volto add-on volto-accordion-block -``` - -We need a view and an edit form for the block. Create a {file}`src/FAQ/BlockView.jsx` and {file}`src/FAQ/BlockEdit.jsx`. - -The BlockView is a simple function component that displays a FAQ component with the data stored on the block. - -```{code-block} jsx -:linenos: - -import React from 'react'; -import FAQ from './FAQ'; - -const View = ({ data }) => { - return ( -
- -
- ); -}; - -export default View; -``` - -We outsource the FAQ component to file {file}`srch/FAQ/FAQ.jsx` and make heavy use of Semantic UI components especially of an accordion with its respective behavior of expanding and collapsing. - -```{code-block} jsx -:linenos: - -const FAQ = ({ data }) => { - const [activeIndex, setActiveIndex] = useState(new Set()); - - return data.faq_list?.faqs ? ( - {data.faq_list.faqs.map((id_qa) => ( -``` - -We primarily loop over the accordion elements and we remember the extended (not collapsed) elements. - -````{dropdown} Complete code of the FAQ component -:animate: fade-in-slide-down -:icon: question - -```{code-block} jsx -:linenos: - -import React, { useState } from 'react'; - -import { Icon } from '@plone/volto/components'; -import rightSVG from '@plone/volto/icons/right-key.svg'; -import downSVG from '@plone/volto/icons/down-key.svg'; -import AnimateHeight from 'react-animate-height'; - -import { Accordion, Grid, Divider, Header } from 'semantic-ui-react'; - -const FAQ = ({ data }) => { - const [activeIndex, setActiveIndex] = useState(new Set()); - - return data.faq_list?.faqs ? ( - <> - - {data.faq_list.faqs.map((id_qa) => ( - - { - const newSet = new Set(activeIndex); - activeIndex.has(id_qa) ? newSet.delete(id_qa) : newSet.add(id_qa); - setActiveIndex(newSet); - }} - > - - - - {activeIndex.has(id_qa) ? ( - - ) : ( - - )} - - -
{data.faq_list.faqs_layout[id_qa][0]}
-
-
-
-
-
- - - - - -
- -
- -
- - - - -
- - - ))} - - ) : ( - '' - ); -}; - -export default FAQ; -``` -```` - -Let's see how the data is stored on the block. Open your BlockEdit. See the helper component `SidebarPortal`. Everything inside is displayed in the Sidebar. - -```{code-block} jsx -:linenos: - -import React from 'react'; -import { SidebarPortal } from '@plone/volto/components'; - -import FAQSidebar from './FAQSidebar'; -import FAQ from './FAQ'; - -const Edit = ({ data, onChangeBlock, block, selected }) => { - return ( -
- - - - - -
- ); -}; - -export default Edit; -``` - -We outsource the edit form in a file {file}`FAQSidebar.jsx` which displays the form according a schema of question and answers. The _onChangeBlock_ event handler is inherited, it stores the value on the block. - -```{code-block} jsx -:linenos: - -import React from 'react'; -import { FAQSchema } from './schema'; -import InlineForm from '@plone/volto/components/manage/Form/InlineForm'; - -const FAQSidebar = ({ data, block, onChangeBlock }) => { - return ( - { - onChangeBlock(block, { - ...data, - [id]: value, - }); - }} - formData={data} - /> - ); -}; - -export default FAQSidebar; -``` - -We define the schema in {file}`schema.js`. - -```{code-block} jsx -:emphasize-lines: 11-14 -:linenos: - -export const FAQSchema = { - title: 'FAQ', - fieldsets: [ - { - id: 'default', - title: 'Default', - fields: ['faq_list'], - }, - ], - properties: { - faq_list: { - title: 'Question and Answers', - type: 'faqlist', - }, - }, - required: [], -}; -``` - -The field _faq_list_ has a type _'faqlist'_. This has to be registered as a _widget_ in {file}`src/index.js`. This configuration is the central place where your add-on can customize the hosting Volto app. It's the place where we later also register our new block type with information about its view and edit form. - -```{code-block} jsx -:linenos: - -import FAQListEditWidget from './FAQ/FAQListEditWidget'; - -export default function applyConfig(config) { - config.widgets.type.faqlist = FAQListEditWidget; - - return config; -} -``` - -Now we will code the important part of the whole block type: the widget `FAQListEditWidget`. -We need a form that consists of a list of existing questions and answers. -The text should be editable. -Additional pairs of questions and answers should be addable. -Next step will be to let the list be drag- and droppable to reorder the items. -Also should an item be deletable. -That's a lot. Let's start with the list of fields displaying the existing values. - -Create a {file}`FAQListEditWidget.jsx`. - -```{code-block} jsx -:linenos: - -import { Form as VoltoForm } from '@plone/volto/components'; - -const FAQListEditWidget = (props) => { - const { value = {}, id, onChange } = props; - // id is the field name: faq_list - // value is the form data (see example in schema.js) - - // qaList: array of [id_question, [question, answer]] - const qaList = (value.faqs || []).map((key) => [key, value.faqs_layout[key]]); - - return ( - // loop over question answer pairs *qaList* - { - onSubmitQAPair(childId, question, answer); - }} - formData={{ - question: value.faqs_layout[childId][0], - answer: value.faqs_layout[childId][1], - }} - schema={QuestionAnswerPairSchema( - props.intl.formatMessage(messages.question), - props.intl.formatMessage(messages.answer), - )} - /> -``` - -You see the Volto `Form` component with its onSubmit event, the form data and the schema to be used. - -````{dropdown} Complete code of the FAQListEditWidget component -:animate: fade-in-slide-down -:icon: question - -```{code-block} jsx -:emphasize-lines: 112-124 -:linenos: - -import React from 'react'; -import { defineMessages, injectIntl } from 'react-intl'; -import { v4 as uuid } from 'uuid'; -import { omit, without } from 'lodash'; -import move from 'lodash-move'; -import { FormFieldWrapper, DragDropList, Icon } from '@plone/volto/components'; -import { Form as VoltoForm } from '@plone/volto/components'; - -import dragSVG from '@plone/volto/icons/drag.svg'; -import trashSVG from '@plone/volto/icons/delete.svg'; -import plusSVG from '@plone/volto/icons/circle-plus.svg'; - -import { QuestionAnswerPairSchema } from './schema.js'; - -const messages = defineMessages({ - question: { - id: 'Question', - defaultMessage: 'Question', - }, - answer: { - id: 'Answer', - defaultMessage: 'Answer', - }, - add: { - id: 'add', - defaultMessage: 'add', - }, -}); - -export function moveQuestionAnswerPair(formData, source, destination) { - return { - ...formData, - faqs: move(formData.faqs, source, destination), - }; -} - -const empty = () => { - return [uuid(), ['', {}]]; -}; - -const FAQListEditWidget = (props) => { - const { value = {}, id, onChange } = props; - // id is the field name: faq_list - // value is the form data (see example in schema.js) - - const onSubmitQAPair = (id_qa, question, answer) => { - onChange(id, { - ...value, - faqs_layout: { - ...(value.faqs_layout || {}), - [id_qa]: [question, answer], - }, - }); - }; - - const addQA = () => { - const [newId, newData] = empty(); - onChange(id, { - ...value, - faqs: [...(value.faqs || []), newId], - faqs_layout: { - ...(value.faqs_layout || {}), - [newId]: newData, - }, - }); - }; - - // qaList array of [id_question, [question, answer]] - const qaList = (value.faqs || []).map((key) => [key, value.faqs_layout[key]]); - - const showAdd = true; - return ( - -
- { - const { source, destination } = result; - if (!destination) { - return; - } - const newFormData = moveQuestionAnswerPair( - value, - source.index, - destination.index, - ); - onChange(id, newFormData); - return true; - }} - > - {(dragProps) => { - const { childId, draginfo } = dragProps; - return ( -
-
-
- -
-
- { - onSubmitQAPair(childId, question, answer); - }} - formData={{ - question: value.faqs_layout[childId][0], - answer: value.faqs_layout[childId][1], - }} - schema={QuestionAnswerPairSchema( - props.intl.formatMessage(messages.question), - props.intl.formatMessage(messages.answer), - )} - /> - {qaList?.length > 1 ? ( - - ) : ( - '' - )} -
-
-
- ); - }} -
- {showAdd ? ( - - ) : ( - '' - )} -
-
- ); -}; - -export default injectIntl(FAQListEditWidget); -``` - -```` - -The form is fructified by the schema QuestionAnswerPairSchema. It's simple, just a string field with a TextArea widget for the question and a such for the answer, but with a RichText widget to have some editing and styling tools available. - -{file}`src/FAQ/schema.js` - -```{code-block} jsx -:emphasize-lines: 12,17 -:linenos: - -export const QuestionAnswerPairSchema = (title_question, title_answer) => { - return { - title: 'Question and Answer Pair', - fieldsets: [ - { - id: 'default', - title: 'QA pair', - fields: ['question', 'answer'], - }, - ], - properties: { - question: { - title: title_question, - type: 'string', - widget: 'textarea', - }, - answer: { - title: title_answer, - type: 'string', - widget: 'richtext', - }, - }, - required: ['question', 'answer'], - }; -}; -``` - -What's left to do? -You created a block type with view and edit form and even a nice widget for the editor to fill in questions and answers. Register the block type and you are good to start your app and create an FAQ for the conference speakers. - -Go to {file}`src/index.js` and register your block type. - -```{code-block} jsx -:emphasize-lines: 8-22 -:linenos: - -import icon from '@plone/volto/icons/list-bullet.svg'; - -import FAQBlockEdit from './FAQ/BlockEdit'; -import FAQBlockView from './FAQ/BlockView'; -import FAQListEditWidget from './FAQ/FAQListEditWidget'; - -export default function applyConfig(config) { - config.blocks.blocksConfig.faq_viewer = { - id: 'faq_viewer', - title: 'FAQ', - edit: FAQBlockEdit, - view: FAQBlockView, - icon: icon, - group: 'text', - restricted: false, - mostUsed: false, - sidebarTab: 1, - security: { - addPermission: [], - view: [], - }, - }; - - config.widgets.type.faqlist = FAQListEditWidget; - - return config; -} -``` - -As we now apply our configuration of the new block type, the app is enriched with an accordion block. - -Run - -```shell -make start -``` - -```{figure} _static/volto_addon_accordion_add.png -:alt: "@rohberg/volto-accordion-block" -``` - -See the complete add-on code @rohberg/volto-accordion-block [^id3] - -## Save your work to Github - -Your add-on is ready to use. As by now your repository is on GitHub. As soon as it is published, you can share it with others. - -An official release is done on npm. Switch to section {ref}`Release a Volto add-on `. - -[^id3]: [Volto accordion block](https://www.npmjs.com/package/@rohberg/volto-accordion-block) - Started as an example for the training it is ready to use for creating a questions and answer sections. diff --git a/docs/mastering-plone/volto_development.md b/docs/mastering-plone/volto_development.md index 3f67ae665..0d0486266 100644 --- a/docs/mastering-plone/volto_development.md +++ b/docs/mastering-plone/volto_development.md @@ -3,18 +3,18 @@ myst: html_meta: "description": "Plone development tools" "property=og:description": "Plone development tools" - "property=og:title": "Plone development" + "property=og:title": "Development tools" "keywords": "Plone, Volto, development, React, Redux" --- (volto-development-label)= -# Develop +# Development tools (editors)= -## IDE: Integrated development environment +## Integrated development environment (IDE) You are about to write code in Python and React / JavaScript. An appropriate integrated development environment supports both writing code and accessing our coding base: Plone Python code and JavaScript / React code. @@ -22,8 +22,8 @@ An appropriate integrated development environment supports both writing code and Some of the most used editors in the Plone community are listed here. - [VSCode](https://code.visualstudio.com/) -- [Sublime](https://www.sublimetext.com/) - [PyCharm](https://www.jetbrains.com/pycharm/) +- [Sublime](https://www.sublimetext.com/) - [Wing IDE](https://wingware.com/) Some features that most editors have in one form or another, are essential when developing with Plone. @@ -38,8 +38,7 @@ Some features that most editors have in one form or another, are essential when | git diff | sidebar tab 'source control' | | file diff | select via sidebar tab 'explorer' | -The capability of performing a _full text search_ through the complete Plone frontend code is invaluable. -Thanks to `omelette` mapping the Volto code in your project, you can search through the complete Plone frontend code base quickly. +The capability of performing a _full text search_ through the complete Plone codebase is invaluable. IDEs nowadays have plenty of features. Beyond the existing features, many extensions offer multiple practical features. @@ -48,33 +47,38 @@ Here are some extensions we recommend when using VSCode: - `Easy Snippet`: Turn your selection into a snippet. - `autoDocstring`: Generates python docstrings. - JSON Crack: Seamlessly visualize JSON data instantly into graphs. -- MyST-Markdown:Oofficial markdown syntax extension for MyST (Markedly Structured Text) +- MyST-Markdown: Official markdown syntax extension for MyST (Markedly Structured Text) - Plone Snippets - `EsLint`: Statically analyzes your code to find problems. Editor support for `ReactJS` development is explained in {doc}`Effective Volto training: VSCode extensions and helpers `. -Checkout VSCode documentation for topics like [code navigation](https://code.visualstudio.com/docs/editing/editingevolved), [Keyboard shortcuts and Multiple selections](https://code.visualstudio.com/docs/editing/codebasics) and many more that makes your everyday work easier. +Check VSCode documentation for topics like [code navigation](https://code.visualstudio.com/docs/editing/editingevolved), [Keyboard shortcuts and Multiple selections](https://code.visualstudio.com/docs/editing/codebasics) and many more that make your everyday work easier. (volto-development-tools-label)= -## Tools +## Browser development tools -### Browser Development Tools +Most browsers have built-in developer tools which can be used to: -React components can be inspected with `React Developer Tools`: props, hierarchy, and a lot more. +- explore the document object model (DOM) +- debug JavaScript code +- inspect styles +- inspect network requests + +The **React Developer Tools** add a tab to inspect React components: props, hierarchy, and a lot more. - [React Developer Tools Chrome](https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) - [React Developer Tools Firefox](https://addons.mozilla.org/de/firefox/addon/react-devtools/) -The Redux store and actions can be inspected with `Redux Developer Tools`. +The **Redux Developer Tools** add a tab to inspect the Redux store and actions. - [Redux Developer Tools Chrome](https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) - [Redux Developer Tools Firefox](https://addons.mozilla.org/de/firefox/addon/reduxdevtools/) -### Postman +## Postman [Postman](https://www.postman.com/) is an app that lets you execute and save requests. We will request REST API endpoints of the backend later with actions. @@ -98,5 +102,4 @@ Just include `{JSON.stringify(var_name_object)}` in your components `html` code. Select the Python of your project `backend/venv/bin/python`. -In VSCode this can be done by following the menu on selecting the current Python in the bottom of the IDE. - +In VSCode this can be done by opening the Python menu at the bottom of the IDE. diff --git a/docs/mastering-plone/volto_frontpage.md b/docs/mastering-plone/volto_frontpage.md index b1272602e..1e3e3efe6 100644 --- a/docs/mastering-plone/volto_frontpage.md +++ b/docs/mastering-plone/volto_frontpage.md @@ -3,15 +3,13 @@ myst: html_meta: "description": "How to use the listing block with a custom criterion" "property=og:description": "How to use the listing block with a custom criterion" - "property=og:title": "Creating a dynamic front page with Volto blocks" + "property=og:title": "Create a dynamic front page listing" "keywords": "Volto, catalog, index, listing, criteria" --- (volto-frontpage-label)= -# Creating a dynamic front page with Volto blocks - -Show selected content on the front page by criterion. +# Create a dynamic front page listing ```{card} In this part you will: @@ -24,11 +22,9 @@ Tools and techniques covered: - listing criterion ``` -````{card} Frontend chapter - -Despite this is a frontend chapter, we are working on **backend** code in `ploneconf.site`. +````{card} -Checkout `ploneconf.site` at tag "initial": +Check out `mastering-plone-project` at tag `behaviors_1`: ```shell git checkout behaviors_1 @@ -44,16 +40,16 @@ More info in {doc}`code` ```` -We prepared a behavior for content types to store the information if the content should be featured in the previous chapter. +In the previous chapter, we prepared a behavior for content types to store the choice of whether the content should be featured. And we enhanced the catalog search by adding index and metadata "featured". Now we turn this into a criterion for a listing block that shows featured content. (volto-frontpage-criterion-label)= -## Add Index as collection criterion +## Query criteria -To understand why we need a collection criterion for a dynamic front page in Volto and what a collection criterion is, we have to look at the listing block of Volto. +To understand what a query criterion is, we have to look at the listing block of Volto. ```{figure} _static/volto_frontpage.png :alt: Listing Block sidebar @@ -61,32 +57,43 @@ To understand why we need a collection criterion for a dynamic front page in Vol In the sidebar, we see the {guilabel}`Criteria` select menu, and if we click there, it'll show some of the selectable criteria ordered in categories like the following: -- `Metadata` contains indexes that are counting as metadata like Type (means Portal Types) and Review State -- `Text` contains indexes that are counting as text-data like Description and Searchable Text -- `Dates` contains indexes which are working with date-data like Effective Date and Creation Date +- `Metadata` contains indexes that are counting as metadata like Type (meaning content type) and Review State +- `Text` contains indexes that are counting as text data like Description and Searchable Text +- `Dates` contains indexes which are working with date data like Effective Date and Creation Date + +These criteria control how the listing is filtered to include the content items we want to show. -To get all talks we marked as `featured` we have to get the listing block to recognize our newly created index. +## Add an index to the query criteria + +To get all talks we marked as `featured`, we have to get the listing block to recognize our newly created index. This means we have to add our index to the collection criteria, to be selectable by the editor. -To add our new index as a criterion to be applicable in a listing block or a collection, we have to switch to our `backend`. There we have to create a plone.app.registry record for our index. This can be achieved by adding a new file {file}`profiles/default/registry/querystring.xml`: +To add our new index as a criterion to be applicable in a listing block or a collection, we have to create a plone.app.registry record for our index. +This can be achieved by adding a new file {file}`backend/src/ploneconf/site/profiles/default/registry/querystring.xml`: ```{code-block} xml :linenos: - + + i18n:domain="plone" +> - Featured - True - False - - plone.app.querystring.operation.boolean.isTrue - plone.app.querystring.operation.boolean.isFalse - - Metadata + prefix="plone.app.querystring.field.featured" + > + Featured + True + False + + plone.app.querystring.operation.boolean.isTrue + plone.app.querystring.operation.boolean.isFalse + + Metadata @@ -95,11 +102,11 @@ To add our new index as a criterion to be applicable in a listing block or a col To understand this code snippet, we have to know the information and tags we are using: - The prefix `plone.app.querystring.field.featured` refers to the featured index we just created. -- The operations elements define the provided criterion values to filter the listing. +- The operations elements define which operations can be used when filtering by this index. - The group value defines the group under which the entry shows up in the selection widget, in our case `Metadata`. -```{note} -For a list of existing QueryField declarations and operations see https://github.com/plone/plone.app.querystring/blob/master/plone/app/querystring/profiles/default/registry.xml +```{tip} +For a list of Plone's default querystring criteria and declarations and operations, see https://github.com/plone/plone.app.querystring/blob/master/plone/app/querystring/profiles/default/registry.xml ``` We can now restart the instance and re-install the add-on. @@ -108,8 +115,8 @@ We can now restart the instance and re-install the add-on. ## Add a listing block to show the featured content Now we go back to our frontend. -To create a new listing block on the front-page we have to click on `edit` and then create one new block. -Now you choose the block `Listing` from the menu: +To create a new listing block on the front page we have to click on `edit` and then create one new block. +Choose the block `Listing` from the menu: ```{figure} _static/volto_frontpage_1.png :alt: Most used blocks in Volto @@ -123,9 +130,4 @@ You can select the 'featured' criterion: :align: left ``` -## Outlook: Block variations - -The listing block comes with default variations for the display. -The editor can choose from these variations to change the template for the listing: with thumbnail image or without, etc.. -Your project maybe needs a custom template. The way to go would be to create an additional variation of the listing block. -See {ref}`plone6docs:extensions-block-variations` for more information. +Now the listing shows only the items in the site that have the `featured` checkbox checked. diff --git a/docs/mastering-plone/volto_listing_variation.md b/docs/mastering-plone/volto_listing_variation.md index 8bfb7c6e4..500f0662b 100644 --- a/docs/mastering-plone/volto_listing_variation.md +++ b/docs/mastering-plone/volto_listing_variation.md @@ -16,16 +16,17 @@ myst: In this part you will: -- Create a search block variation to display also the talks event date, room, audience and speaker name +- Add more indexes and criteria +- Create a search block variation Topics covered: - block variation ``` -````{card} Frontend chapter +````{card} -Checkout `volto-ploneconf` at tag "upgrade_steps": +Check out `mastering-plone-project` at tag `upgrade_steps`: ```shell git checkout upgrade_steps @@ -40,91 +41,285 @@ git checkout listing_variation More info in {doc}`code` ```` +We've already created a {doc}`custom_search` that lists all the talks, but it would be nice to improve it to show more information about each talk and allow filtering by additional facets. +Let's add a variation of the search block to display talks with their event date, room, audience and speaker name. +The result will look like this: + ```{figure} _static/listing_variation.png :alt: block variation for the search block to show more than title and description block variation for the search block to show more than title and description ``` -```{figure} _static/listing_variation_edit.png -:alt: Apply listing variation -Apply listing variation +(search-block-variation-indexes-label)= + +## Add catalog indexes + +In order to add facets for the audience and room, we have to make sure they are indexed. +We'll also add them as metadata columns so they are included in the search results data. + +Update {file}`backend/src/ploneconf/site/profiles/default/catalog.xml`. + +```{code-block} xml +:emphasize-lines: 17-38 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ``` +This adds new indexes for the three fields we want to show in the listing. +Note that _audience_ is a {py:class}`KeywordIndex` because the field is multi-valued, but we want a separate index entry for every value in an object. + +While we're at it, we also added more metadata columns that we'll need in one of the next chapters. +We will need to search for sponsors and get the results with the values of the `url` and `level` fields. + +We add a metadata column for these fields to avoid loading full content objects while searching. +This would be okay, but slower. +The search request gets an attribute from catalog brains unless the attribute is not available, then fetches the real object. + + +(search-block-variation-criteria-label)= + +## Add collection criteria + +The following additional criteria allow us to create a search block constrained to talks with facets to filter for audience, speaker and room. + +`backend/src/ploneconf/site/profiles/default/registry/querystring.xml` + +```{code-block} xml + + + Speaker + True + True + + plone.app.querystring.operation.string.is + plone.app.querystring.operation.string.contains + + Metadata + + + + Audience + True + False + + plone.app.querystring.operation.selection.any + plone.app.querystring.operation.selection.all + plone.app.querystring.operation.selection.none + + Metadata + ploneconf.audiences + + + + Room + True + False + + plone.app.querystring.operation.selection.any + plone.app.querystring.operation.selection.all + plone.app.querystring.operation.selection.none + + Metadata + ploneconf.rooms + +``` -(search-block-variation-registration-label)= +```{seealso} +For a full list of all existing QueryField declarations see https://github.com/plone/plone.app.querystring/blob/master/plone/app/querystring/profiles/default/registry.xml#L197. -## Create and register new block variation +For a full list of all existing operations see https://github.com/plone/plone.app.querystring/blob/master/plone/app/querystring/profiles/default/registry.xml#L1. +``` -Each block can be enhanced with variations of its layout. -We are writing a block variation for the search block. -First step is to create a new component `packages/volto-ploneconf/src/components/variations/TalkListingBlockVariation.jsx` with the code of an existing block variation `core/packages/volto/src/components/manage/Blocks/Listing/SummaryTemplate.jsx`. +## Add upgrade step -We register our new variation on `config.blocks.blocksConfig.listing.variations`, as listing blocks and search blocks share their variations via this setting. +A reinstallation of the add-on would leave the new catalog indexes empty. +Therefore we write an upgrade step to not only add indexes and criteria, but also reindex all talks. -registration `packages/volto-ploneconf/src/index.js`: +`backend/src/ploneconf/site/profiles/default/metadata.xml`: -```{code-block} jsx -:linenos: -:emphasize-lines: 12-20 +```{code-block} xml +:emphasize-lines: 3 -import { TalkView, TalkListingBlockVariation } from './components'; + + + 1002 + + profile-plone.volto:default + profile-plone.app.caching:default + profile-plone.app.caching:with-caching-proxy + + +``` -const applyConfig = (config) => { - config.views = { - ...config.views, - contentTypesViews: { - ...config.views.contentTypesViews, - talk: TalkView, - }, - }; +`backend/src/ploneconf/site/upgrades/v1002.py`: - config.blocks.blocksConfig.listing.variations = [ - ...config.blocks.blocksConfig.listing.variations, - { - id: 'talks', - title: 'Talks', - template: TalkListingBlockVariation, - }, - ]; +```python +from plone import api - return config; -}; +import logging + + +logger = logging.getLogger(__name__) + + +def update_indexes(setup_tool): + # Reindexing content + for brain in api.content.find(portal_type=["talk", "sponsor"]): + obj = brain.getObject() + obj.reindexObject() + logger.info(f"{obj.id} reindexed.") +``` -export default applyConfig; +`backend/src/ploneconf/site/upgrades/configure.zcml`: + +```{code-block} xml +:emphasize-lines: 23-38 + + + + + + + + + + + + + + ``` +```{tip} +From time to time you may want to reindex catalog indexes manually. +To do so, go to , select the new indexes and click {guilabel}`Reindex`. +You can also rebuild the whole catalog by going to the {guilabel}`Advanced` tab and clicking {guilabel}`Clear and Rebuild`. +This can take some time in a large site! +``` + + +(upgrade-steps-search-block-label)= + +## Create a search block for talks + +As soon as you run the upgrade steps, you can now add a search block to your 'schedule' page that provides facets to filter for audience, et cetera. + +```{figure} _static/search_block.png +:alt: search block + +search block +``` + + +(search-block-variation-registration-label)= + +## Create and register a new block variation + +Some blocks can be enhanced with variations of their layout. +We are writing a block variation for the search block. -variation component `packages/volto-ploneconf/src/components/variations/TalkListingBlockVariation.jsx`: +First, create a new component `frontend/packages/volto-ploneconf-site/src/components/variations/TalkListingBlockVariation.jsx` by copying the code of an existing block variation, `frontend/core/packages/volto/src/components/manage/Blocks/Listing/SummaryTemplate.jsx`. ```{code-block} jsx :linenos: -:emphasize-lines: 37 +:emphasize-lines: 4, 8-12, 36-41, 43-65 -import React from 'react'; import PropTypes from 'prop-types'; -import { Label, Segment } from 'semantic-ui-react'; -import { ConditionalLink, Component } from '@plone/volto/components'; +import ConditionalLink from '@plone/volto/components/manage/ConditionalLink/ConditionalLink'; +import Component from '@plone/volto/components/theme/Component/Component'; import { When } from '@plone/volto/components/theme/View/EventDatesInfo'; -import { flattenToAppURL } from '@plone/volto/helpers'; -import { isInternalURL } from '@plone/volto/helpers/Url/Url'; +import { flattenToAppURL, isInternalURL } from '@plone/volto/helpers/Url/Url'; + +const colorMapping = { + beginner: 'green', + advanced: 'yellow', + professional: 'purple', +}; -const TalkListingBlockVariation = ({ - items, - linkTitle, - linkHref, - isEditMode, -}) => { +const SummaryTemplate = ({ items, linkTitle, linkHref, isEditMode }) => { let link = null; let href = linkHref?.[0]?.['@id'] || ''; - const color_mapping_audience = { - beginner: 'green', - advanced: 'yellow', - professional: 'purple', - }; if (isInternalURL(href)) { link = ( @@ -140,47 +335,44 @@ const TalkListingBlockVariation = ({ <>
{items.map((item) => ( - -
- - -
- -

{item.title || item.id}

-

{item.speaker}

-

- {item.room && ( - <> - Room: - {item.room} -
- - )} - {item.audience?.length > 0 && ( - <> - Audience: - {item.audience?.map((audience) => { - let color = - color_mapping_audience[audience] || 'green'; - return ( - - ); - })} - - )} -

-

{item.description}

-
-
-
-
+
+ + +
+ +

{item.title || item.id}

+

{item.speaker}

+

+ {item.room && ( + <> + Room: + {item.room} +
+ + )} + {item.audience?.length > 0 && ( + <> + Audience: + {item.audience?.map((audience) => { + let color = colorMapping[audience] || 'green'; + return ( +

+ {audience} +
+ ); + })} + + )} +

+

{item.description}

+
+
+
))}
@@ -189,14 +381,66 @@ const TalkListingBlockVariation = ({ ); }; -TalkListingBlockVariation.propTypes = { +SummaryTemplate.propTypes = { items: PropTypes.arrayOf(PropTypes.any).isRequired, linkMore: PropTypes.any, isEditMode: PropTypes.bool, }; -export default TalkListingBlockVariation; +export default SummaryTemplate; +``` + +We register our new variation in `config.blocks.blocksConfig.listing.variations`. +(Listing blocks and search blocks share their variations via this setting.) + +Update `frontend/packages/volto-ploneconf-site/src/config/settings.ts`: + +```{code-block} jsx +:linenos: +:emphasize-lines: 2, 4, 21-28 + +import type { ConfigType } from '@plone/registry'; +import type { BlockExtension, ViewsConfig } from '@plone/types'; +import TalkView from '../components/Views/TalkView'; +import TalkListingBlockVariation from '../components/variations/TalkListingBlockVariation'; + +export default function install(config: ConfigType) { + // Language settings + config.settings.defaultLanguage = 'en'; + // Additional language settings for Volto 19 and above, add as many supported languages as needed + // Languages not added to supportedLanguages will not be included in the build + // config.settings.supportedLanguages = ['en']; + + config.views = { + ...(config.views as ViewsConfig), + contentTypesViews: { + ...config.views.contentTypesViews, + talk: TalkView, + }, + }; + + config.blocks.blocksConfig.listing.variations = [ + ...(config.blocks.blocksConfig.listing.variations as BlockExtension[]), + { + id: 'talks', + title: 'Talks', + template: TalkListingBlockVariation, + }, + ]; + + return config; +} +``` + + + +Now select the new variation to apply it to the search block. + +```{figure} _static/listing_variation_edit.png +:alt: Apply listing variation + +Apply listing variation ``` This is a basic block variation. -Block variations can have variations: See {doc}`plone6docs:volto/blocks/extensions` for advanced techniques. +See {doc}`plone6docs:volto/blocks/extensions` for advanced techniques. diff --git a/docs/mastering-plone/volto_overrides.md b/docs/mastering-plone/volto_overrides.md index b932c1a41..e30f93109 100644 --- a/docs/mastering-plone/volto_overrides.md +++ b/docs/mastering-plone/volto_overrides.md @@ -3,31 +3,29 @@ myst: html_meta: "description": "Small customizations by overriding existing code" "property=og:description": "Small customizations by overriding existing code" - "property=og:title": "Customizing Volto components" + "property=og:title": "Customize Volto components" "keywords": "Plone, Volto, customization, shadowing, component" --- (volto-overrides-label)= -# Customizing Volto components +# Customize Volto components ```{card} -In this part you will: - -Customize existing components and views +In this part you will customize existing components and views. Tools and techniques covered: - Component shadowing -- View for a content type +- Content type views ``` -````{card} Frontend chapter +````{card} -Checkout `volto-ploneconf` at tag "initial": +Check out `mastering-plone-project` at tag `talks`: ```shell -git checkout initial +git checkout talks ``` The code at the end of the chapter: @@ -45,17 +43,17 @@ More info in {doc}`code` ## Component shadowing We use a technique called **component shadowing** to override an existing Volto component with our local custom version, without having to modify Volto's source code at all. -You have to place the replacing file in the same folder path inside the {file}`packages/volto-ploneconf/src/customizations/` folder of your app as the original file in {file}`core/packages/volto/src/`. +You have to create a new file inside {file}`frontend/packages/volto-ploneconf-site/src/customizations/` in the same subpath where the original file is found inside {file}`frontend/core/packages/volto/src/`. -Every time you add a file to your app, you have to restart Volto for changes taking effect. -From that point on, the hot module reloading should kick in and reload the page automatically on changes. +Every time you add a file to your app, you have to restart Volto in order for it to discover the new file. +From that point on, hot module reloading should kick in and reload the page automatically on changes. You can customize any module in Volto, including actions and reducers, not only components. -The Volto code can be found in {file}`core/packages/volto/`. +The Volto code can be found in {file}`frontend/core/packages/volto/`. -## The footer +### Example: Customize the footer The React developer tools provide a selector to find the component we need to override. @@ -67,11 +65,11 @@ The React developer tools provide a selector to find the component we need to ov :alt: Footer component file. ``` -Customize the footer by copying {file}`core/packages/volto/src/components/theme/Footer/Footer.jsx` to your customization folder at {file}`packages/volto-ploneconf/src/customizations/components/theme/Footer/Footer.jsx`. +Customize the footer by copying {file}`frontend/core/packages/volto/src/components/theme/Footer/Footer.jsx` to your customization folder at {file}`frontend/packages/volto-ploneconf-site/src/customizations/components/theme/Footer/Footer.jsx`. After a restart you can change this Footer component and the changes are shown immediately due to hot module reloading. -## The news item view +## Customize the News Item view We want to show the date a News Item is published. This way visitors can see at a glance if they are looking at current news. @@ -80,12 +78,15 @@ So you need to customize the way a News Item is rendered. A News Item has date attributes. The attributes of a content type instance are defined by the schema of a content type and possible behaviors. -We had a look at schemas in {doc}`dexterity` and {doc}`dexterity_2_talk`. -Behaviors are being described in {doc}`behaviors_1`. These date attributes are available when the content is fetched by the frontend. But let's first have a look how these attributes are used in a Volto component. -The Volto view component to render a News Item is in {file}`core/packages/volto/src/components/theme/View/NewsItemView.jsx`. +```{seealso} +We had a look at schemas in {doc}`dexterity` and {doc}`dexterity_2_talk`. +We will add a behavior later in {doc}`behaviors_1`. +``` + +The Volto view component to render a News Item is in {file}`frontend/core/packages/volto/src/components/theme/View/NewsItemView.jsx`. ```{code-block} jsx :linenos: @@ -97,7 +98,8 @@ The Volto view component to render a News Item is in {file}`core/packages/volto/ import React from 'react'; import PropTypes from 'prop-types'; import { Container as SemanticContainer } from 'semantic-ui-react'; -import { hasBlocksData, flattenHTMLToAppURL } from '@plone/volto/helpers'; +import { hasBlocksData } from '@plone/volto/helpers/Blocks/Blocks'; +import { flattenHTMLToAppURL } from '@plone/volto/helpers/Url/Url'; import RenderBlocks from '@plone/volto/components/theme/View/RenderBlocks'; import config from '@plone/volto/registry'; @@ -167,19 +169,19 @@ export default NewsItemView; ``` ````{note} -- `content` is passed to `NewsItemView` and represents the content item as it is serialized by the REST API. +- `content` is passed to `NewsItemView` and represents the content item as it was serialized by the REST API. The `content` data has been fetched by an action on navigating to route `http://localhost:3000/my-news-item`. - The view displays various attributes of the News Item using `content.title`, `content.description` or `content.text.data`. -- You can inspect all data hold by `content` using the React Developer Tools for [Firefox](https://addons.mozilla.org/de/firefox/addon/react-devtools/) or [Chrome](https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi): +- You can inspect all data in the `content` object using the React Developer Tools for [Firefox](https://addons.mozilla.org/de/firefox/addon/react-devtools/) or [Chrome](https://chromewebstore.google.com/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi): ```{figure} _static/volto_react_devtools.png :align: center ``` ```` -Copy this file from {file}`core/packages/volto/src/components/theme/View/NewsItemView.jsx` to {file}`packages/volto-ploneconf/src/customizations/components/theme/View/NewsItemView.jsx`. +Copy this file from {file}`frontend/core/packages/volto/src/components/theme/View/NewsItemView.jsx` to {file}`frontend/packages/volto-ploneconf-site/src/customizations/components/theme/View/NewsItemView.jsx`. After restarting Volto, the new file is used when displaying a News Item. To make sure your file is taken into effect, add a small change before the blocks ``. @@ -204,7 +206,7 @@ Let's use one of many helpers available in Volto. Import the component `FormattedDate` from `@plone/volto/components` at the top of the file and use it to format the date in a human readable format. ```{code-block} jsx -:emphasize-lines: 10,27-29 +:emphasize-lines: 10,28-30 :linenos: /** @@ -215,8 +217,9 @@ Import the component `FormattedDate` from `@plone/volto/components` at the top o import React from 'react'; import PropTypes from 'prop-types'; import { Container as SemanticContainer } from 'semantic-ui-react'; -import { hasBlocksData, flattenHTMLToAppURL } from '@plone/volto/helpers'; -import { FormattedDate } from '@plone/volto/components'; +import { hasBlocksData } from '@plone/volto/helpers/Blocks/Blocks'; +import FormattedDate from '@plone/volto/components/theme/FormattedDate/FormattedDate'; +import { flattenHTMLToAppURL } from '@plone/volto/helpers/Url/Url'; import RenderBlocks from '@plone/volto/components/theme/View/RenderBlocks'; import config from '@plone/volto/registry'; @@ -235,7 +238,7 @@ const NewsItemView = ({ content }) => {

-

+

{' '}
) : ( @@ -294,14 +297,15 @@ The result should look like this: :alt: A News Item with publishing date. ``` -Now another issue appears. There are various dates associated with any content object: +Now another issue appears. +There are various dates associated with any content object: -- The date the item is created: `content.created` -- The date the item is last modified `content.modified` -- The date the item is published `content.effective` +- The date the item was created: `content.created` +- The date the item was last modified: `content.modified` +- The date the item was published: `content.effective` In fact you most likely want to show the date when the item has been published. -But while the item isn't yet published, this value isn't yet set and you will get an error. +But while the item isn't yet published, this value isn't yet set, and you will get an error. So we'll add some simple logic to show the effective date only if it exists. ```jsx @@ -313,14 +317,13 @@ So we'll add some simple logic to show the effective date only if it exists. ``` As we're in the HTML part of our React component, we surround the JavaScript code with curly braces. -Inside JavaScript we embrace HTML in rounded braces. ## Summary -- With component shadowing views and other components in Volto can be modified and extended. -- Component shadowing a powerful mechanism making changes without the need of complex configuration or maintaining a fork of the code. -- You need to restart Volto when you add a new overriding. +- With component shadowing, views and other components in Volto can be modified and extended. +- Component shadowing a powerful mechanism to make changes without the need of complex configuration or maintaining a fork of the code. +- You need to restart Volto when you add a new override. ```{seealso} Volto Hands-On training: {ref}`voltohandson-header-component-label` diff --git a/docs/mastering-plone/volto_talkview.md b/docs/mastering-plone/volto_talkview.md index 7cb9ed64b..c5f93c6ab 100644 --- a/docs/mastering-plone/volto_talkview.md +++ b/docs/mastering-plone/volto_talkview.md @@ -3,31 +3,31 @@ myst: html_meta: "description": "Display content type" "property=og:description": "Display content type" - "property=og:title": "Volto View Component: A Default View for a Talk" + "property=og:title": "Add a custom view" "keywords": "view, content type" --- (volto-talkview-label)= -# Volto view component: A default view for a "Talk" +# Add a custom view ```{card} In this part we will: -- Create a view to display a talk -- Register a React view component for content type talk +- Create a component to display a talk +- Register the component as the default view for the talk content type - Write the view component Tools and techniques covered: - View for a content type - Displaying data stored in fields of a content type -- React Basics +- React basics ``` -````{card} Frontend chapter +````{card} -Checkout `volto-ploneconf` at tag "overrides": +Check out `mastering-plone-project` at tag "overrides": ```shell git checkout overrides @@ -46,9 +46,9 @@ More info in {doc}`code` The default visualization for the new content type `talk` lists the field values according to the type schema. -To show the talk data in a nice way, display the speaker portrait and add some components, we create a custom view for type talk. +We will create a custom view for the talk content type in order to show the talk data in a nice way, display the speaker portrait, and add some components. -In the folder {file}`frontend` you need to add a new file {file}`packages/volto-ploneconf/src/components/Views/Talk.jsx`. +In the folder {file}`frontend` you need to add a new file {file}`packages/volto-ploneconf-site/src/components/Views/Talk.jsx`. Create the folder {file}`Views` first. As a first step, the file will hold only a placeholder. @@ -64,35 +64,32 @@ export default TalkView; ``` -Also add a convenience import of the new component to {file}`src/components/index.js`: +Now register the new component as the default view for `talk` in {file}`packages/volto-ploneconf-site/src/config/settings.ts`. -```jsx -import TalkView from './Views/Talk'; - -export { TalkView }; -``` +```{code-block} tsx +:emphasize-lines: 2-3,12-18 -This is a common practice and allows to import the new view component as `import { TalkView } from './components';` instead of `import { TalkView } from './components/Views/Talk';`. +import type { ConfigType } from '@plone/registry'; +import type { ViewsConfig } from '@plone/types'; +import TalkView from '../components/Views/TalkView'; -Now register the new component as the default view for `talks` in {file}`packages/volto-ploneconfig/src/index.js`. - -```{code-block} jsx -:emphasize-lines: 1,6-8 +export default function install(config: ConfigType) { + // Language settings + config.settings.defaultLanguage = 'en'; + // Additional language settings for Volto 19 and above, add as many supported languages as needed + // Languages not added to supportedLanguages will not be included in the build + // config.settings.supportedLanguages = ['en']; -import { TalkView } from './components'; - -const applyConfig = (config) => { config.views = { - ...config.views, + ...(config.views as ViewsConfig), contentTypesViews: { ...config.views.contentTypesViews, talk: TalkView, }, }; - return config; -}; -export default applyConfig; + return config; +} ``` - This extends the Volto default setting `config.views.contentTypesViews` with the key/value pair `talk: TalkView`. @@ -101,15 +98,15 @@ export default applyConfig; A restarted Volto (with `make start`) picks up these configuration modifications and displays the placeholder in place of the previously used default view. -## Enhancing the view +## Enhance the view -Now we're improving this view step by step. +Now let's improve this view step by step. First we reuse the component `DefaultView.jsx` in our custom view: ```{code-block} jsx :emphasize-lines: 1,4 -import { DefaultView } from '@plone/volto/components'; +import DefaultView from '@plone/volto/components/theme/View/DefaultView'; const TalkView = (props) => { return ; @@ -122,7 +119,7 @@ We will now add the content from the field `details` after the `DefaultView`. ```{code-block} jsx :emphasize-lines: 5,7,8 -import { DefaultView } from '@plone/volto/components'; +import DefaultView from '@plone/volto/components/theme/View/DefaultView'; const TalkView = (props) => { return ( @@ -135,11 +132,10 @@ const TalkView = (props) => { export default TalkView; ``` -- `<> ` is a fragment. The return value of React needs to be one single element. +- `<> ` is a fragment. The return value from a React component needs to be one single element. - The variable `props` receives data from the parent component. As the TalkView component is registered as a content type view, it receives the content data and some more. We will use the content part. - So we introduce a constant `content` to be more explicit. - `content.details` is the value of the RichText field `details` with mime type, encoding and the data: ```jsx @@ -154,16 +150,16 @@ export default TalkView; - `content.details.data` holds the raw HTML. To render it, we use `dangerouslySetInnerHTML` (see https://legacy.reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml). -Please check the 'components' tab of Google developer tools for property `content` of the `TalkView` component to see the field values of your talk instance. +Please check the 'components' tab of Google developer tools for the property `content` of the `TalkView` component to see the field values of your talk instance. The result isn't beautiful, because the text sticks to the left border of the page. You need to wrap it in a `Container` to get the same styling as the content of `DefaultView`: ```{code-block} jsx -:emphasize-lines: 1,11,13 +:emphasize-lines: 1,6-7,11,13 import { Container as SemanticContainer } from 'semantic-ui-react'; -import { DefaultView } from '@plone/volto/components'; +import DefaultView from '@plone/volto/components/theme/View/DefaultView'; import config from '@plone/volto/registry'; const TalkView = (props) => { @@ -179,10 +175,9 @@ const TalkView = (props) => { ); }; export default TalkView; - ``` -`Container` is either a registered component or a component from [Semantic UI React](https://react.semantic-ui.com/elements/container/) and needs to be imported before it is used. +Container is either a registered component or a component from [Semantic UI React](https://react.semantic-ui.com/elements/container/) and needs to be imported before it is used. We now decide to display the type of talk in the title (for example: "Keynote: The Future of Plone"). This means we cannot use `DefaultView` anymore since that displays the title like this: `

{content.title}

`. @@ -196,7 +191,7 @@ This has multiple benefits: The speaker portrait is picked up by the DefaultView because the fields name is `image`, which is the same as the image from the behavior `plone.leadimage`. ```{code-block} jsx -:emphasize-lines: 9,18 +:emphasize-lines: 9-18 :linenos: import { Container as SemanticContainer } from 'semantic-ui-react'; @@ -229,19 +224,14 @@ export default TalkView; Next we add a segment with info on the speaker: ```{code-block} jsx -:emphasize-lines: 3-4,22-37 +:emphasize-lines: 17-34 -import { - Container as SemanticContainer, - Header, - Segment, -} from 'semantic-ui-react'; +import { Container as SemanticContainer } from 'semantic-ui-react'; import config from '@plone/volto/registry'; const TalkView = (props) => { const { content } = props; - const Container = - config.getComponent({ name: 'Container' }).component || SemanticContainer; + const Container = config.getComponent({ name: 'Container' }).component || SemanticContainer; return (

@@ -252,8 +242,10 @@ const TalkView = (props) => {

{content.description}

)}
- - {content.speaker &&
{content.speaker}
} +
+ {content.speaker && ( +
{content.speaker}
+ )}

{content.company || content.website}

{content.email && (

@@ -267,34 +259,29 @@ const TalkView = (props) => { }} /> )} - +

); }; export default TalkView; ``` -- We use the component [Segment](https://react.semantic-ui.com/elements/segment/#variations-clearing) for the box. -- `` {`mailto:${content.email}`} `` is a [template literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) +- We use CSS classes from [Semantic UI](https://semantic-ui.com/) for the segment and header. +- `` `mailto:${content.email}` `` is a [template literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) Next we add the image: ```{code-block} jsx -:emphasize-lines: 4,32-38 - -import { - Container as SemanticContainer, - Header, - Image, - Segment, -} from 'semantic-ui-react'; -import { flattenToAppURL } from '@plone/volto/helpers'; +:emphasize-lines: 8,29-33 + +import { Container as SemanticContainer } from 'semantic-ui-react'; import config from '@plone/volto/registry'; const TalkView = (props) => { const { content } = props; const Container = config.getComponent({ name: 'Container' }).component || SemanticContainer; + const Image = config.getComponent({ name: 'Image' }).component; return (

@@ -305,21 +292,21 @@ const TalkView = (props) => {

{content.description}

)}
- - {content.speaker &&
{content.speaker}
} +
+ {content.speaker && ( +
{content.speaker}
+ )}

{content.company || content.website}

{content.email && (

Email: {content.email}

)} - {content.speaker} + className="ui small right floated image" + />} {content.speaker_biography && (
{ }} /> )} - +
); }; export default TalkView; ``` -- We use the Semantic UI component [Image](https://react.semantic-ui.com/elements/image/#variations-avatar) -- We use `flattenToAppURL` to turn the Plone URL of the image to the Volto URL, for example it turns into . -- Open the React Developer Tools in your browser and inspect the property `content` of the TalkView component, its attribute `image` and its attribute `scales`. If you look at the [documentation for the serialization of image-fields](https://6.docs.plone.org/plone.restapi/docs/source/usage/serialization.html#file-image-fields) you can find out where that information comes from. -- To deal with talks without speaker image, we check for the existence of the image with `content.image?.scales?.preview?.download`. - The expression with question marks returns `undefined` if `content` has no `image` key or `content.image` has no `scales` key and so forth. - `?.` is the [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) operator. +- We use the Volto Image component (see {doc}`plone6docs:volto/development/images`), which renders an `img` element with multiple image sizes in a `srcset` so the browser can download the most appropriate one. +- Open the React Developer Tools in your browser and inspect the property `content` of the TalkView component, its attribute `image` and its attribute `scales`. If you look at the [documentation for the serialization of image fields](https://6.docs.plone.org/plone.restapi/docs/source/usage/serialization.html#file-image-fields) you can find out where that information comes from. +- To deal with talks without a speaker image, we check for the existence of the image with `content.image &&`. Next we add the audience: ```{code-block} jsx -:emphasize-lines: 5, 15-19, 29-36 - -import { - Container as SemanticContainer, - Header, - Image, - Label, - Segment, -} from 'semantic-ui-react'; -import { flattenToAppURL } from '@plone/volto/helpers'; +:emphasize-lines: 4-8, 24-31 + +import { Container as SemanticContainer } from 'semantic-ui-react'; import config from '@plone/volto/registry'; +const colorMapping = { + beginner: 'green', + advanced: 'yellow', + professional: 'purple', +}; + const TalkView = (props) => { const { content } = props; const Container = config.getComponent({ name: 'Container' }).component || SemanticContainer; - const color_mapping = { - beginner: 'green', - advanced: 'yellow', - professional: 'purple', - }; + const Image = config.getComponent({ name: 'Image' }).component; return (

@@ -375,16 +354,18 @@ const TalkView = (props) => {

{content.description}

)} {content.audience?.map((item) => { - let color = color_mapping[item.token] || 'green'; + let color = colorMapping[item.token] || 'green'; return ( -