Porównaj commity

...

38 Commity

Autor SHA1 Wiadomość Data
Andy Babic fa82c16a15
Merge 020c3a5e0d into 196cb02d10 2024-05-02 09:35:49 +00:00
Matt Westcott 196cb02d10 Generate new strings for translation 2024-05-02 10:06:13 +01:00
Matt Westcott 1874350fbf Update latest.txt for 6.1 / 5.2.5 2024-05-02 09:46:31 +01:00
Storm Heg 2031eb3d24
Give translators more context about how density is used (#11918) 2024-05-02 09:44:53 +01:00
Matt Westcott 4ac42a723f Release note for CopyView fix (cherry-picked from e3d9233f4d) 2024-05-01 13:37:39 +01:00
Matt Westcott ef57f9b2c9 Fill in release date for 6.1 final 2024-05-01 12:41:22 +01:00
Matt Westcott 955703fba6 Fill in release date for 6.0.3 2024-05-01 12:40:43 +01:00
Matt Westcott 42b7c9bcde Release note for CVE-2024-32882 in 6.1 2024-05-01 12:20:00 +01:00
Matt Westcott 9d24ac4e39 Release note for CVE-2024-32882 in 6.0.3 2024-05-01 12:18:47 +01:00
Jake Howard ee57f6d4dc
Merge pull request from GHSA-w2v8-php4-p8hc
* Pass user to settings form to enable permissions checks

* Add tests for settings creation and editing

* Ensure all generic create / edit view forms receive `for_user`

Co-authored-by: Sage Abdullah <sage.abdullah@torchbox.com>

* Test field permissions on ModelViewTest

---------

Co-authored-by: Sage Abdullah <sage.abdullah@torchbox.com>
2024-05-01 12:14:16 +01:00
Matt Westcott 932402fd28 Fetch new translations from Transifex 2024-05-01 11:32:29 +01:00
Matt Westcott 7de6872277 Release note for #11912 in 6.1 2024-05-01 11:20:58 +01:00
Matt Westcott 95d23fdf7d Typo - this features -> these features 2024-05-01 11:20:58 +01:00
Matt Westcott 72edc09851 Release note for #11912 in 6.0.3 2024-05-01 11:20:58 +01:00
Matt Westcott fac768c076 Fill in release date for 5.2.5 2024-05-01 11:20:58 +01:00
Matt Westcott 81a11d63c6 Release note for #11912 in 5.2.5 2024-05-01 11:20:58 +01:00
Matt Westcott 617e5129c5
Add management command to fix UUID fields under MariaDB / Django 5.0 (#11912) 2024-05-01 11:07:20 +01:00
Thibaud Colas cae0002afe Add more sections to 6.1 release notes 2024-04-30 17:29:18 +01:00
Sage Abdullah 08ee15a358 Remove --inline-actions variant of listing tables
This was added in f322e9d868, when the
snippets listing view was briefly redesigned to put the "Edit", "Delete"
etc. actions in the same line as the title instead of in a new line.

With the universal listings design, all listing actions are put inside a
three-dot dropdown menu.

This style is no longer used anywhere in Wagtail, especially now that
the HistoryView has been reimplemented using the dropdown menu for the
actions.
2024-04-30 16:13:33 +01:00
Sage Abdullah b8dd7f484f Fix icon alignment in page listings 2024-04-30 16:13:33 +01:00
Sage Abdullah 56e69bc3ea
Use DjangoJSONEncoder instead of custom LazyStringEncoder 2024-04-30 15:17:08 +07:00
Jake Howard afbafd657d Remove duplication on performance page for frontend caching proxies (#11871) 2024-04-29 19:10:07 +01:00
Benjamin Bach b266e54ba9 Bug: Enable template tag `richtext` to convert lazy text strings (#11901)
Fixes #11900
2024-04-29 15:34:37 +01:00
Matt Westcott 763c990490 Release note for #11902 in 6.1 2024-04-29 14:18:02 +01:00
Matt Westcott 207d5dafd5 Release note for #11902 in 6.0.3 2024-04-29 14:18:02 +01:00
Sage Abdullah c3a52a6fdb Fix missing static files in the styleguide 2024-04-29 14:18:02 +01:00
Sage Abdullah 4302bed1b1
Release note for #11860 2024-04-29 14:41:49 +07:00
Sage Abdullah ae28020195
Render breadcrumbs in redirects edit view 2024-04-29 14:35:42 +07:00
rohitsrma 6f28aa9d8b
Refactor redirects edit view to extend generic EditView 2024-04-29 14:35:41 +07:00
rohitsrma 3d63d0da4f
Use pk_url_kwarg to resolve pk in generic EditView 2024-04-29 14:23:33 +07:00
Andy Babic 020c3a5e0d Rewrite streamfield tests 2024-04-22 10:00:29 +01:00
Andy Babic bb28daf65a Allow value_class to be overridden at the field level 2024-04-22 09:12:51 +01:00
Andy Babic c75b7f9404 Move value_class attribute to BaseStreamBlock.Meta 2024-04-20 16:01:32 +01:00
Andy Babic b2c79f21b6 Define StreamValue before BaseStreamBlock so that we can lose the delayed attribute assignment 2024-04-20 15:52:22 +01:00
Andy Babic 0c9bb707cb Add comments RE 'default_value_class' and update the BaseStreamBlock class instead of just StreamBlock (it's a sensible default for both) 2024-04-20 15:45:15 +01:00
Andy Babic 91930e1152 Add tests 2024-04-20 15:45:15 +01:00
Andy Babic 4cf2b8fd64 Respect block-level value_class at the field level 2024-04-20 15:45:12 +01:00
Andy Babic bda27a5691 Support custom value classes on StreamBlocks 2024-04-20 15:44:12 +01:00
71 zmienionych plików z 1483 dodań i 804 usunięć

Wyświetl plik

@ -6,10 +6,14 @@ Changelog
* Optimize and consolidate redirects report view into the index view (Jake Howard, Dan Braghis)
* Support a `HOSTNAMES` parameter on `WAGTAILFRONTENDCACHE` to define which hostnames a backend should respond to (Jake Howard, sponsored by Oxfam America)
* Refactor redirects edit view to use the generic `EditView` and breadcrumbs (Rohit Sharma)
* Fix: Make `WAGTAILIMAGES_CHOOSER_PAGE_SIZE` setting functional again (Rohit Sharma)
* Fix: Enable `richtext` template tag to convert lazy translation values (Benjamin Bach)
* Docs: Remove duplicate section on frontend caching proxies from performance page (Jake Howard)
* Maintenance: Use `DjangoJSONEncoder` instead of custom `LazyStringEncoder` to serialize Draftail config (Sage Abdullah)
6.1 (xx.xx.xxxx) - IN DEVELOPMENT
6.1 (01.05.2024)
~~~~~~~~~~~~~~~~
* Refine wording of page & collection privacy using password is a shared password and should not be used for secure content (Rohit Sharma, Jake Howard)
@ -49,6 +53,7 @@ Changelog
* Populate django-treebeard cache during page routing to improve performance of `get_parent` (Nigel van Keulen)
* Add a new user profile preference to configure user interface information density (Thibaud Colas)
* Add additional field types to Elasticsearch mapping (scott-8)
* Fix: CVE-2024-32882: Permission check bypass when editing a model with per-field restrictions through `wagtail.contrib.settings` or `ModelViewSet` (Ben Morse, Joshua Munn, Jake Howard, Sage Abdullah)
* Fix: Fix typo in `__str__` for MySQL search index (Jake Howard)
* Fix: Ensure that unit tests correctly check for migrations in all core Wagtail apps (Matt Westcott)
* Fix: Correctly handle `date` objects on `human_readable_date` template tag (Jhonatan Lopes)
@ -69,6 +74,8 @@ Changelog
* Fix: Improve exception handling when generating image renditions concurrently (Andy Babic)
* Fix: Respect `WAGTAIL_ALLOW_UNICODE_SLUGS` setting when auto-generating slugs (LB (Ben) Johnston)
* Fix: Use correct URL when redirecting back to page search results after an AJAX search (Sage Abdullah)
* Fix: Reinstate missing static files in style guide (Sage Abdullah)
* Fix: Provide `convert_mariadb_uuids` management command to assist with upgrading to Django 5.0+ on MariaDB (Matt Westcott)
* Docs: Add contributing development documentation on how to work with a fork of Wagtail (Nix Asteri, Dan Braghis)
* Docs: Make sure the settings panel is listed in tabbed interface examples (Tibor Leupold)
* Docs: Update content and page names to their US spelling instead of UK spelling (Victoria Poromon)
@ -111,11 +118,15 @@ Changelog
* Maintenance: Refactor the Django port of `urlify` to use TypeScript, officially deprecate `window.URLify` global util (LB (Ben) Johnston)
6.0.3 (xx.xx.xxxx) - IN DEVELOPMENT
6.0.3 (01.05.2024)
~~~~~~~~~~~~~~~~~~
* Fix: CVE-2024-32882: Permission check bypass when editing a model with per-field restrictions through `wagtail.contrib.settings` or `ModelViewSet` (Ben Morse, Joshua Munn, Jake Howard, Sage Abdullah)
* Fix: Respect `WAGTAIL_ALLOW_UNICODE_SLUGS` setting when auto-generating slugs (LB (Ben) Johnston)
* Fix: Use correct URL when redirecting back to page search results after an AJAX search (Sage Abdullah)
* Fix: Reinstate missing static files in style guide (Sage Abdullah)
* Fix: Provide `convert_mariadb_uuids` management command to assist with upgrading to Django 5.0+ on MariaDB (Matt Westcott)
* Fix: Fix generic CopyView for models with primary keys that need to be quoted (Sage Abdullah)
6.0.2 (03.04.2024)
@ -309,11 +320,12 @@ Changelog
* Maintenance: Remove support for Django 4.1 and below (Sage Abdullah)
5.2.5 (xx.xx.xxxx) - IN DEVELOPMENT
5.2.5 (01.05.2024)
~~~~~~~~~~~~~~~~~~
* Fix: Respect `WAGTAIL_ALLOW_UNICODE_SLUGS` setting when auto-generating slugs (LB (Ben) Johnston)
* Fix: Use correct URL when redirecting back to page search results after an AJAX search (Sage Abdullah)
* Fix: Provide `convert_mariadb_uuids` management command to assist with upgrading to Django 5.0+ on MariaDB (Matt Westcott)
5.2.4 (03.04.2024)

Wyświetl plik

@ -223,6 +223,10 @@ ul.listing {
.title {
word-break: break-word;
display: flex;
align-items: center;
justify-content: space-between;
gap: theme('spacing.2');
.title-wrapper,
h2 {
@ -231,6 +235,7 @@ ul.listing {
gap: theme('spacing.2');
margin: 0;
vertical-align: middle;
align-items: center;
a {
color: inherit;
@ -242,11 +247,6 @@ ul.listing {
}
}
}
.icon-folder {
margin: 3px 0.3em 0 0;
vertical-align: top;
}
}
.actions {
@ -265,36 +265,6 @@ ul.listing {
}
}
&--inline-actions td.title {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5rem;
.title-wrapper {
margin-inline-end: 2.5em;
}
.w-status {
margin: 0;
}
}
&--inline-actions .actions {
display: inline-block;
margin-top: 0;
vertical-align: inherit;
li {
margin-bottom: 0;
}
.button {
vertical-align: inherit;
}
}
.moderate-actions form {
float: inline-start;
margin: 0 1em 1em 0;

Wyświetl plik

@ -4,6 +4,7 @@
border-radius: 2px;
text-align: center;
display: inline-block;
word-break: normal;
// stylelint-disable-next-line property-disallowed-list
text-transform: uppercase;
padding: 0 0.5em;

Wyświetl plik

@ -123,7 +123,7 @@ urlpatterns = [
[django-sendfile](https://github.com/johnsensible/django-sendfile) offloads the job of transferring the image data to the web
server instead of serving it directly from the Django application. This could
greatly reduce server load in situations where your site has many images being
downloaded but you're unable to use a [](caching_proxy) or a CDN.
downloaded but you're unable to use a [caching proxy](performance_frontend_caching) or a CDN.
You first need to install and configure django-sendfile and configure your
web server to use it. If you haven't done this already, please refer to the

Wyświetl plik

@ -60,16 +60,18 @@ The same can be achieved in Python using [`generate_image_url`](dynamic_image_ur
When using a queryset to render a list of images or objects with images, you can [prefetch the renditions](prefetching_image_renditions) needed with a single additional query. For long lists of items, or where multiple renditions are used for each item, this can provide a significant boost to performance.
(performance_page_urls)=
(performance_frontend_caching)=
## Frontend caching
## Frontend caching proxy
Many websites use a frontend cache such as Varnish, Squid, Cloudflare or CloudFront to gain extra performance. The downside of using a frontend cache though is that they don't respond well to updating content and will often keep an old version of a page cached after it has been updated.
Many websites use a frontend cache such as [Varnish](https://varnish-cache.org/), [Squid](http://www.squid-cache.org/), [Cloudflare](https://www.cloudflare.com/) or [CloudFront](https://aws.amazon.com/cloudfront/) to support high volumes of traffic with excellent response times. The downside of using a frontend cache though is that they don't respond well to updating content and will often keep an old version of a page cached after it has been updated.
Wagtail supports being [integrated](frontend_cache_purging) with many CDNs, so it can inform them when a page changes, so the cache can be cleared immediately and users see the changes sooner.
If you have multiple frontends configured (eg Cloudflare for one site, CloudFront for another), it's recommended to set the [`HOSTNAMES`](frontendcache_multiple_backends) key to the list of hostnames the backend can purge, to prevent unnecessary extra purge requests.
(performance_page_urls)=
## Page URLs
To fully resolve the URL of a page, Wagtail requires information from a few different sources.
@ -90,14 +92,6 @@ Wagtail is tested on PostgreSQL, SQLite, and MySQL. It may work on some third-pa
We recommend PostgreSQL for production use, however, the choice of database ultimately depends on a combination of factors, including personal preference, team expertise, and specific project requirements. The most important aspect is to ensure that your selected database can meet the performance and scalability requirements of your project.
(caching_proxy)=
## Caching proxy
To support high volumes of traffic with excellent response times, we recommend a caching proxy. Both [Varnish](https://varnish-cache.org/) and [Squid](http://www.squid-cache.org/) have been tested in production. Hosted proxies like [Cloudflare](https://www.cloudflare.com/) should also work well.
Wagtail supports automatic cache invalidation for Varnish/Squid. See [](frontend_cache_purging) for more information.
### Image attributes
For some images, it may be beneficial to lazy load images, so the rest of the page can continue to load. It can be configured site-wide [](adding_default_attributes_to_images) or per-image [](image_tag_alt). For more details you can read about the [`loading='lazy'` attribute](https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading#images_and_iframes) and the [`'decoding='async'` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-decoding) or this [web.dev article on lazy loading images](https://web.dev/lazy-loading-images/).

Wyświetl plik

@ -8,6 +8,8 @@
Wagtail provides several generic views for handling common tasks such as creating / editing model instances and chooser modals. For convenience, these views are bundled in [viewsets](viewsets_reference).
(modelviewset)=
## ModelViewSet
The {class}`~wagtail.admin.viewsets.model.ModelViewSet` class provides the views for listing, creating, editing, and deleting model instances. For example, if we have the following model:

Wyświetl plik

@ -164,3 +164,13 @@ Options:
- `--purge-only` :
This argument will purge all image renditions without regenerating them. They will be regenerated when next requested.
(convert_mariadb_uuids)=
## convert_mariadb_uuids
```sh
./manage.py convert_mariadb_uuids
```
For sites using MariaDB, this command must be run once when upgrading to Django 5.0 and MariaDB 10.7 from any earlier version of Django or MariaDB. This is necessary because Django 5.0 introduces support for MariaDB's native UUID type, breaking backwards compatibility with `CHAR`-based UUIDs used in earlier versions of Django and MariaDB. New sites created under Django 5.0+ and MariaDB 10.7+ are unaffected.

Wyświetl plik

@ -1,6 +1,6 @@
# Wagtail 5.2.5 release notes - IN DEVELOPMENT
# Wagtail 5.2.5 release notes
_Unreleased_
_May 1, 2024_
```{contents}
---
@ -15,3 +15,16 @@ depth: 1
* Respect `WAGTAIL_ALLOW_UNICODE_SLUGS` setting when auto-generating slugs (LB (Ben) Johnston)
* Use correct URL when redirecting back to page search results after an AJAX search (Sage Abdullah)
* Provide [`convert_mariadb_uuids`](convert_mariadb_uuids) management command to assist with upgrading to Django 5.0+ on MariaDB (Matt Westcott)
## Upgrade considerations
### Changes to UUID fields on MariaDB when upgrading to Django 5.0
Django 5.0 introduces support for MariaDB's native UUID type on MariaDB 10.7 and above. This breaks backwards compatibility with `CHAR`-based UUIDs created on earlier versions of Django and MariaDB, and so upgrading a site to Django 5.0+ and MariaDB 10.7+ is liable to result in errors such as `Data too long for column 'translation_key' at row 1` or `Data too long for column 'uuid' at row 1` when creating or editing pages. To fix this, it is necessary to run the [`convert_mariadb_uuids`](convert_mariadb_uuids) management command (available as of Wagtail 5.2.5) after upgrading:
```sh
./manage.py convert_mariadb_uuids
```
This will convert all existing UUID fields used by Wagtail to the new format. New sites created under Django 5.0+ and MariaDB 10.7+ are unaffected.

Wyświetl plik

@ -219,6 +219,18 @@ As part of our [adoption of Stimulus](https://github.com/wagtail/rfcs/blob/main/
* Add better deprecation warnings to the `search.Query` & `search.QueryDailyHits` model, move final set of templates from the admin search module to the search promotions contrib module (LB (Ben) Johnston)
## Upgrade considerations - changes affecting all projects
### Changes to UUID fields on MariaDB when upgrading to Django 5.0
Django 5.0 introduces support for MariaDB's native UUID type on MariaDB 10.7 and above. This breaks backwards compatibility with `CHAR`-based UUIDs created on earlier versions of Django and MariaDB, and so upgrading a site to Django 5.0+ and MariaDB 10.7+ is liable to result in errors such as `Data too long for column 'translation_key' at row 1` or `Data too long for column 'uuid' at row 1` when creating or editing pages. To fix this, it is necessary to run the [`convert_mariadb_uuids`](convert_mariadb_uuids) management command (available as of Wagtail 5.2.5) after upgrading:
```sh
./manage.py convert_mariadb_uuids
```
This will convert all existing UUID fields used by Wagtail to the new format. New sites created under Django 5.0+ and MariaDB 10.7+ are unaffected.
## Upgrade considerations - deprecation of old functionality
### Legacy moderation system is deprecated

Wyświetl plik

@ -1,6 +1,6 @@
# Wagtail 6.0.3 release notes - IN DEVELOPMENT
# Wagtail 6.0.3 release notes
_Unreleased_
_May 1, 2024_
```{contents}
---
@ -11,7 +11,30 @@ depth: 1
## What's new
### CVE-2024-32882: Permission check bypass when editing a model with per-field restrictions through `wagtail.contrib.settings` or `ModelViewSet`
This release addresses a permission vulnerability in the Wagtail admin interface. If a model has been made available for editing through the [`wagtail.contrib.settings`](/reference/contrib/settings) module or [ModelViewSet](modelviewset), and the permission argument on FieldPanel has been used to further restrict access to one or more fields of the model, a user with edit permission over the model but not the specific field can craft an HTTP POST request that bypasses the permission check on the individual field, allowing them to update its value.
The vulnerability is not exploitable by an ordinary site visitor without access to the Wagtail admin, or by a user who has not been granted edit access to the model in question. The editing interfaces for pages and snippets are also unaffected.
Many thanks to Ben Morse and Joshua Munn for reporting this issue, and Jake Howard and Sage Abdullah for the fix. For further details, please see [the CVE-2024-32882 security advisory](https://github.com/wagtail/wagtail/security/advisories/GHSA-w2v8-php4-p8hc).
### Bug fixes
* Respect `WAGTAIL_ALLOW_UNICODE_SLUGS` setting when auto-generating slugs (LB (Ben) Johnston)
* Use correct URL when redirecting back to page search results after an AJAX search (Sage Abdullah)
* Reinstate missing static files in style guide (Sage Abdullah)
* Provide [`convert_mariadb_uuids`](convert_mariadb_uuids) management command to assist with upgrading to Django 5.0+ on MariaDB (Matt Westcott)
* Fix generic CopyView for models with primary keys that need to be quoted (Sage Abdullah)
## Upgrade considerations
### Changes to UUID fields on MariaDB when upgrading to Django 5.0
Django 5.0 introduces support for MariaDB's native UUID type on MariaDB 10.7 and above. This breaks backwards compatibility with `CHAR`-based UUIDs created on earlier versions of Django and MariaDB, and so upgrading a site to Django 5.0+ and MariaDB 10.7+ is liable to result in errors such as `Data too long for column 'translation_key' at row 1` or `Data too long for column 'uuid' at row 1` when creating or editing pages. To fix this, it is necessary to run the [`convert_mariadb_uuids`](convert_mariadb_uuids) management command (available as of Wagtail 6.0.3) after upgrading:
```sh
./manage.py convert_mariadb_uuids
```
This will convert all existing UUID fields used by Wagtail to the new format. New sites created under Django 5.0+ and MariaDB 10.7+ are unaffected.

Wyświetl plik

@ -283,6 +283,16 @@ The `use_json_field` argument to `StreamField` is no longer required, and can be
## Upgrade considerations - changes affecting all projects
### Changes to UUID fields on MariaDB when upgrading to Django 5.0
Django 5.0 introduces support for MariaDB's native UUID type on MariaDB 10.7 and above. This breaks backwards compatibility with `CHAR`-based UUIDs created on earlier versions of Django and MariaDB, and so upgrading a site to Django 5.0+ and MariaDB 10.7+ is liable to result in errors such as `Data too long for column 'translation_key' at row 1` or `Data too long for column 'uuid' at row 1` when creating or editing pages. To fix this, it is necessary to run the [`convert_mariadb_uuids`](convert_mariadb_uuids) management command (available as of Wagtail 6.0.3) after upgrading:
```sh
./manage.py convert_mariadb_uuids
```
This will convert all existing UUID fields used by Wagtail to the new format. New sites created under Django 5.0+ and MariaDB 10.7+ are unaffected.
### `SnippetViewSet` & `ModelViewSet` copy view enabled by default
The newly introduced copy view will be enabled by default for all `ModelViewSet` and `SnippetViewSet` classes.

Wyświetl plik

@ -1,6 +1,6 @@
# Wagtail 6.1 release notes - IN DEVELOPMENT
# Wagtail 6.1 release notes
_Unreleased_
_May 1, 2024_
```{contents}
---
@ -24,8 +24,12 @@ Continuing work on the Universal Listings project, this release rolls out univer
* Groups
* Users
* Workflow and task views
* Search promotions index views
* Redirects index
Universal listing components like header buttons have also been tweaked to improve usability. This feature was developed by Ben Enright and Sage Abdullah.
Universal listing components like header buttons have also been tweaked to improve usability, and the `PageListingViewSet` now includes `ChooseParentView` to allow creating pages from custom page listings.
Thank you to everyone who worked on these features: Ben Enright, Sage Abdullah, Rohit Sharma, Storm Heg, Temidayo Azeez, and Abdelrahman Hamada.
### Information-dense admin interface
@ -44,21 +48,38 @@ A new viewset class `PageListingViewSet` has been introduced, allowing developer
A new dialog is available from the help menu, providing an overview of keyboard shortcuts available in the Wagtail admin. This feature was developed by Karthik Ayangar and Rohit Sharma.
### Better guidance for password-protected content
Wagtail now includes extra guidance in its [private pages](private_pages) and [private collections (documents)](private_collections) forms, to warn users about the pitfalls of the "shared password" option.
For projects with higher security requirements, it's now possible to disable the shared password option entirely.
Thank you to Rohit Sharma, Salvo Polizzi, and Jake Howard for implementing those changes.
### Favicon images generation
For sites managing favicons via the CMS, Wagtail now supports [`.ico` favicon generation](favicon_generation), with `format-ico`:
```html+django
<link rel="icon" href="{% image favicon_image format-ico %}" />
```
This feature was developed by Jake Howard.
### CVE-2024-32882: Permission check bypass when editing a model with per-field restrictions through `wagtail.contrib.settings` or `ModelViewSet`
This release addresses a permission vulnerability in the Wagtail admin interface. If a model has been made available for editing through the [`wagtail.contrib.settings`](/reference/contrib/settings) module or [ModelViewSet](modelviewset), and the permission argument on FieldPanel has been used to further restrict access to one or more fields of the model, a user with edit permission over the model but not the specific field can craft an HTTP POST request that bypasses the permission check on the individual field, allowing them to update its value.
The vulnerability is not exploitable by an ordinary site visitor without access to the Wagtail admin, or by a user who has not been granted edit access to the model in question. The editing interfaces for pages and snippets are also unaffected.
Many thanks to Ben Morse and Joshua Munn for reporting this issue, and Jake Howard and Sage Abdullah for the fix. For further details, please see [the CVE-2024-32882 security advisory](https://github.com/wagtail/wagtail/security/advisories/GHSA-w2v8-php4-p8hc).
### Other features
* Refine wording of page & collection privacy using password is a shared password and should not be used for secure content (Rohit Sharma, Jake Howard)
* Add `RelatedObjectsColumn` to the table UI framework (Matt Westcott)
* Reduce memory usage when rebuilding search indexes (Jake Howard)
* Support creating images in `.ico` format (Jake Howard)
* Add the ability to disable the usage of a shared password for enhanced security for the [private pages](private_pages) and [collections (documents)](private_collections) feature (Salvo Polizzi, Jake Howard)
* Add system checks to ensure that `WAGTAIL_DATE_FORMAT`, `WAGTAIL_DATETIME_FORMAT`, `WAGTAIL_TIME_FORMAT` are [correctly configured](wagtail_date_time_formats) (Rohit Sharma, Coen van der Kamp)
* Allow custom permissions with the same prefix as built-in permissions (Sage Abdullah)
* Allow displaying permissions linked to the Admin model's content type (Sage Abdullah)
* Add support for Draftail's JavaScript to use chooserUrls provided by entity options & for the Draftail widget to encode lazy URLs/ translations (Elhussein Almasri)
* Reimplement search promotions `IndexView` using the `generic.IndexView` (Rohit Sharma, Sage Abdullah, Storm Heg)
* Reimplement redirects `IndexView` using the `generic.IndexView` (Rohit Sharma, Sage Abdullah, Temidayo Azeez)
* Add `ChooseParentView` to `PageListingViewSet` to allow creating pages from custom page listings (Abdelrahman Hamada, Sage Abdullah)
* Added `AbstractGroupApprovalTask` to simplify [customizing behavior of custom `Task` models](../extending/custom_tasks) (John-Scott Atlakson)
* Add ability to bulk toggle permissions in the user group editing view, including shift+click for multiple selections (LB (Ben) Johnston, Kalob Taulien)
* Update the minimum version of `djangorestframework` to 3.15.1 (Sage Abdullah)
@ -97,6 +118,8 @@ A new dialog is available from the help menu, providing an overview of keyboard
* Improve exception handling when generating image renditions concurrently (Andy Babic)
* Respect `WAGTAIL_ALLOW_UNICODE_SLUGS` setting when auto-generating slugs (LB (Ben) Johnston)
* Use correct URL when redirecting back to page search results after an AJAX search (Sage Abdullah)
* Reinstate missing static files in style guide (Sage Abdullah)
* Provide [`convert_mariadb_uuids`](convert_mariadb_uuids) management command to assist with upgrading to Django 5.0+ on MariaDB (Matt Westcott)
### Documentation

Wyświetl plik

@ -16,19 +16,22 @@ depth: 1
* Optimize and consolidate redirects report view into the index view (Jake Howard, Dan Braghis)
* Support a [`HOSTNAMES` parameter on `WAGTAILFRONTENDCACHE`](frontendcache_multiple_backends) to define which hostnames a backend should respond to (Jake Howard, sponsored by Oxfam America)
* Refactor redirects edit view to use the generic `EditView` and breadcrumbs (Rohit Sharma)
### Bug fixes
* Make `WAGTAILIMAGES_CHOOSER_PAGE_SIZE` setting functional again (Rohit Sharma)
* Enable `richtext` template tag to convert lazy translation values (Benjamin Bach)
### Documentation
* ...
* Remove duplicate section on frontend caching proxies from performance page (Jake Howard)
### Maintenance
* ...
* Use `DjangoJSONEncoder` instead of custom `LazyStringEncoder` to serialize Draftail config (Sage Abdullah)
## Upgrade considerations - changes affecting all projects

Wyświetl plik

@ -442,6 +442,8 @@ You can encode the image into lossless AVIF or WebP format by using `format-avif
{% image page.photo width-400 format-webp-lossless %}
```
(favicon_generation)=
### Favicon generation
You can save images as a `.ico` file using `format-ico`, which is especially useful when managing a site's favicon through the Admin.

Wyświetl plik

@ -1,10 +1,10 @@
{
"version": "6.0.2",
"url": "https://docs.wagtail.org/en/stable/releases/6.0.2.html",
"minorUrl": "https://docs.wagtail.org/en/stable/releases/6.0.html",
"version": "6.1",
"url": "https://docs.wagtail.org/en/stable/releases/6.1.html",
"minorUrl": "https://docs.wagtail.org/en/stable/releases/6.1.html",
"lts": {
"version": "5.2.4",
"url": "https://docs.wagtail.org/en/stable/releases/5.2.4.html",
"version": "5.2.5",
"url": "https://docs.wagtail.org/en/stable/releases/5.2.5.html",
"minorUrl": "https://docs.wagtail.org/en/stable/releases/5.2.html"
}
}

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -50,7 +50,7 @@ msgstr ""
#: action_menu.py:196 templates/wagtailadmin/generic/confirm_unpublish.html:5
#: templates/wagtailadmin/pages/bulk_actions/confirm_bulk_unpublish.html:7
#: views/pages/bulk_actions/unpublish.py:8 wagtail_hooks.py:341
#: views/pages/bulk_actions/unpublish.py:8 wagtail_hooks.py:342
msgid "Unpublish"
msgstr ""
@ -199,7 +199,7 @@ msgstr ""
msgid "You cannot have multiple permission records for the same collection."
msgstr ""
#: forms/collections.py:378 views/generic/models.py:459
#: forms/collections.py:378 views/generic/models.py:460
msgid "Add"
msgstr ""
@ -207,21 +207,20 @@ msgstr ""
msgid "Add collections"
msgstr ""
#: forms/collections.py:379 rich_text/editors/draftail/__init__.py:24
#: templates/wagtailadmin/generic/inspect.html:31
#: forms/collections.py:379 templates/wagtailadmin/generic/inspect.html:31
#: templates/wagtailadmin/home/locked_pages.html:46
#: templates/wagtailadmin/home/recent_edits.html:35
#: templates/wagtailadmin/home/user_objects_in_workflow_moderation.html:29
#: templates/wagtailadmin/home/workflow_objects_to_moderate.html:29
#: templates/wagtailadmin/home/workflow_objects_to_moderate.html:52
#: templates/wagtailadmin/shared/workflow_history/index.html:12
#: views/generic/history.py:133 views/generic/history.py:269
#: views/generic/models.py:384 views/generic/models.py:589
#: views/generic/models.py:847 views/generic/models.py:1152
#: views/generic/models.py:1294 views/generic/models.py:1403
#: views/generic/history.py:133 views/generic/history.py:268
#: views/generic/models.py:385 views/generic/models.py:590
#: views/generic/models.py:873 views/generic/models.py:1178
#: views/generic/models.py:1320 views/generic/models.py:1429
#: views/generic/usage.py:88 views/pages/create.py:172 views/pages/edit.py:291
#: views/pages/move.py:116 views/workflows.py:236 views/workflows.py:352
#: viewsets/chooser.py:30 wagtail_hooks.py:244
#: viewsets/chooser.py:30 wagtail_hooks.py:245
msgid "Edit"
msgstr ""
@ -236,9 +235,9 @@ msgstr ""
#: templates/wagtailadmin/panels/inline_panel_child.html:11
#: templates/wagtailadmin/permissions/includes/collection_member_permissions_form.html:16
#: templates/wagtailadmin/workflows/includes/workflow_pages_form.html:8
#: views/generic/models.py:429 views/generic/models.py:699
#: views/generic/models.py:914 views/pages/bulk_actions/delete.py:8
#: wagtail_hooks.py:311
#: views/generic/models.py:430 views/generic/models.py:718
#: views/generic/models.py:940 views/pages/bulk_actions/delete.py:8
#: wagtail_hooks.py:312
msgid "Delete"
msgstr ""
@ -538,7 +537,7 @@ msgstr ""
msgid "Promote"
msgstr ""
#: panels/page_utils.py:68 wagtail_hooks.py:125
#: panels/page_utils.py:68 wagtail_hooks.py:126
msgid "Settings"
msgstr ""
@ -835,7 +834,7 @@ msgstr ""
#: templates/wagtailadmin/page_privacy/set_privacy.html:12
#: templates/wagtailadmin/workflows/edit.html:60
#: templates/wagtailadmin/workflows/edit_task.html:44
#: views/generic/models.py:702
#: views/generic/models.py:721
msgid "Save"
msgstr ""
@ -1085,7 +1084,7 @@ msgstr ""
#: templates/wagtailadmin/home/recent_edits.html:42
#: templates/wagtailadmin/shared/page_status_tag_new.html:50
#: templates/wagtailadmin/shared/side_panels/includes/status/workflow.html:18
#: views/generic/models.py:1186
#: views/generic/models.py:1212
msgid "Live"
msgstr ""
@ -1108,7 +1107,7 @@ msgid "Status"
msgstr ""
#: templates/wagtailadmin/home/recent_edits.html:14 views/generic/history.py:58
#: views/generic/history.py:226 views/reports/audit_logging.py:66
#: views/generic/history.py:225 views/reports/audit_logging.py:66
msgid "Date"
msgstr ""
@ -1674,7 +1673,7 @@ msgstr[1] ""
#: templates/wagtailadmin/pages/bulk_actions/confirm_bulk_move.html:8
#: templates/wagtailadmin/pages/confirm_move.html:5
#: templates/wagtailadmin/pages/move_choose_destination.html:5
#: views/pages/bulk_actions/move.py:31 wagtail_hooks.py:289
#: views/pages/bulk_actions/move.py:31 wagtail_hooks.py:290
msgid "Move"
msgstr ""
@ -2016,8 +2015,8 @@ msgid "Copy %(title)s"
msgstr ""
#: templates/wagtailadmin/pages/copy.html:5
#: templatetags/wagtailadmin_tags.py:1328 views/generic/models.py:398
#: wagtail_hooks.py:300
#: templatetags/wagtailadmin_tags.py:1328 views/generic/models.py:399
#: wagtail_hooks.py:301
msgid "Copy"
msgstr ""
@ -2099,7 +2098,7 @@ msgstr ""
#: templates/wagtailadmin/pages/listing/_navigation_explore.html:17
#: templates/wagtailadmin/pages/listing/_navigation_explore.html:18
#: wagtail_hooks.py:280
#: wagtail_hooks.py:281
#, python-format
msgid "Add a child page to '%(title)s'"
msgstr ""
@ -2197,7 +2196,7 @@ msgid "Confirm"
msgstr ""
#: templates/wagtailadmin/pages/page_listing_header.html:12
#: wagtail_hooks.py:278
#: wagtail_hooks.py:279
msgid "Add child page"
msgstr ""
@ -2322,7 +2321,7 @@ msgid "App"
msgstr ""
#: templates/wagtailadmin/reports/listing/_list_page_types_usage.html:19
#: wagtail_hooks.py:102 wagtail_hooks.py:143
#: wagtail_hooks.py:103 wagtail_hooks.py:144
msgid "Pages"
msgstr ""
@ -2368,7 +2367,7 @@ msgstr ""
#: templates/wagtailadmin/reports/site_history.html:10
#: templates/wagtailadmin/workflows/task_chooser/includes/results.html:19
#: views/account.py:152 views/collections.py:27 views/generic/models.py:254
#: views/account.py:152 views/collections.py:27 views/generic/models.py:255
#: views/generic/usage.py:103 views/reports/audit_logging.py:68
#: views/reports/audit_logging.py:114 views/workflows.py:125
#: views/workflows.py:520
@ -2376,7 +2375,7 @@ msgid "Name"
msgstr ""
#: templates/wagtailadmin/reports/site_history.html:16
#: views/generic/history.py:48 views/generic/history.py:214
#: views/generic/history.py:48 views/generic/history.py:213
#: views/reports/audit_logging.py:57
msgid "Action"
msgstr ""
@ -2502,8 +2501,8 @@ msgstr ""
#: templates/wagtailadmin/shared/headers/_history_icon_link.html:5
#: templates/wagtailadmin/shared/headers/_history_icon_link.html:7
#: views/generic/history.py:194 views/generic/history.py:259
#: views/generic/models.py:1153 wagtail_hooks.py:352
#: views/generic/history.py:194 views/generic/history.py:258
#: views/generic/models.py:1179 wagtail_hooks.py:353
msgid "History"
msgstr ""
@ -2949,7 +2948,7 @@ msgstr ""
#: templates/wagtailadmin/workflows/create.html:36
#: templates/wagtailadmin/workflows/create_task.html:32
#: templates/wagtailadmin/workflows/task_chooser/includes/create_form.html:30
#: views/generic/chooser.py:302 views/generic/models.py:508
#: views/generic/chooser.py:302 views/generic/models.py:509
#: viewsets/chooser.py:82
msgid "Create"
msgstr ""
@ -3050,7 +3049,7 @@ msgid "Choose a task"
msgstr ""
#: templates/wagtailadmin/workflows/task_chooser/chooser.html:8
#: views/generic/models.py:502 views/pages/create.py:67
#: views/generic/models.py:503 views/pages/create.py:67
msgid "New"
msgstr ""
@ -3241,11 +3240,11 @@ msgstr ""
msgid "Text formatting"
msgstr ""
#: templatetags/wagtailadmin_tags.py:1351 wagtail_hooks.py:693
#: templatetags/wagtailadmin_tags.py:1351 wagtail_hooks.py:694
msgid "Bold"
msgstr ""
#: templatetags/wagtailadmin_tags.py:1352 wagtail_hooks.py:715
#: templatetags/wagtailadmin_tags.py:1352 wagtail_hooks.py:716
msgid "Italic"
msgstr ""
@ -3261,11 +3260,11 @@ msgstr ""
msgid "Strike-through"
msgstr ""
#: templatetags/wagtailadmin_tags.py:1356 wagtail_hooks.py:785
#: templatetags/wagtailadmin_tags.py:1356 wagtail_hooks.py:786
msgid "Superscript"
msgstr ""
#: templatetags/wagtailadmin_tags.py:1357 wagtail_hooks.py:806
#: templatetags/wagtailadmin_tags.py:1357 wagtail_hooks.py:807
msgid "Subscript"
msgstr ""
@ -3391,7 +3390,7 @@ msgstr ""
msgid "You have been successfully logged out."
msgstr ""
#: views/collections.py:21 wagtail_hooks.py:174
#: views/collections.py:21 wagtail_hooks.py:175
msgid "Collections"
msgstr ""
@ -3455,7 +3454,7 @@ msgstr ""
msgid "Cancel scheduled publish"
msgstr ""
#: views/generic/history.py:356
#: views/generic/history.py:355
msgid "Workflow progress"
msgstr ""
@ -3469,12 +3468,12 @@ msgstr ""
msgid "The %(model_name)s could not be saved as it is locked"
msgstr ""
#: views/generic/mixins.py:418 views/generic/models.py:700
#: views/generic/mixins.py:418 views/generic/models.py:719
#, python-format
msgid "%(model_name)s '%(object)s' updated."
msgstr ""
#: views/generic/mixins.py:420 views/generic/models.py:504
#: views/generic/mixins.py:420 views/generic/models.py:505
#, python-format
msgid "%(model_name)s '%(object)s' created."
msgstr ""
@ -3553,97 +3552,97 @@ msgid ""
"for publishing."
msgstr ""
#: views/generic/models.py:284
#: views/generic/models.py:285
#, python-format
msgid "%(related_model_name)s %(field_label)s"
msgstr ""
#: views/generic/models.py:388 wagtail_hooks.py:246
#: views/generic/models.py:389 wagtail_hooks.py:247
#, python-format
msgid "Edit '%(title)s'"
msgstr ""
#: views/generic/models.py:402
#: views/generic/models.py:403
#, python-format
msgid "Copy '%(title)s'"
msgstr ""
#: views/generic/models.py:411 views/generic/models.py:1056
#: views/generic/models.py:412 views/generic/models.py:1082
msgid "Inspect"
msgstr ""
#: views/generic/models.py:415
#: views/generic/models.py:416
#, python-format
msgid "Inspect '%(title)s'"
msgstr ""
#: views/generic/models.py:433
#: views/generic/models.py:434
#, python-format
msgid "Delete '%(title)s'"
msgstr ""
#: views/generic/models.py:447 wagtail_hooks.py:236
#: views/generic/models.py:448 wagtail_hooks.py:237
#, python-format
msgid "More options for '%(title)s'"
msgstr ""
#: views/generic/models.py:457
#: views/generic/models.py:458
#, python-format
msgid "Add %(model_name)s"
msgstr ""
#: views/generic/models.py:506
#: views/generic/models.py:507
#, python-format
msgid "The %(model_name)s could not be created due to errors."
msgstr ""
#: views/generic/models.py:543 views/workflows.py:644
#: views/generic/models.py:544 views/workflows.py:644
#, python-format
msgid "New: %(model_name)s"
msgstr ""
#: views/generic/models.py:695
#: views/generic/models.py:714
msgid "Editing"
msgstr ""
#: views/generic/models.py:701
#: views/generic/models.py:720
#, python-format
msgid "The %(model_name)s could not be saved due to errors."
msgstr ""
#: views/generic/models.py:915
#: views/generic/models.py:941
#, python-format
msgid "%(model_name)s '%(object)s' deleted."
msgstr ""
#: views/generic/models.py:974
#: views/generic/models.py:1000
#, python-format
msgid "Are you sure you want to delete this %(model_name)s?"
msgstr ""
#: views/generic/models.py:1019
#: views/generic/models.py:1045
msgid "Inspecting"
msgstr ""
#: views/generic/models.py:1191
#: views/generic/models.py:1217
msgid "Earliest"
msgstr ""
#: views/generic/models.py:1194
#: views/generic/models.py:1220
msgid "Latest"
msgstr ""
#: views/generic/models.py:1257
#: views/generic/models.py:1283
#, python-format
msgid "'%(object)s' unpublished."
msgstr ""
#: views/generic/models.py:1364
#: views/generic/models.py:1390
#, python-format
msgid "Version %(revision_id)s of \"%(object)s\" unscheduled."
msgstr ""
#: views/generic/models.py:1420
#: views/generic/models.py:1446
#, python-format
msgid "revision %(revision_id)s of \"%(object)s\""
msgstr ""
@ -3819,11 +3818,11 @@ msgstr ""
msgid "Page '%(page_title)s' copied."
msgstr ""
#: views/pages/create.py:178 views/pages/edit.py:297 wagtail_hooks.py:255
#: views/pages/create.py:178 views/pages/edit.py:297 wagtail_hooks.py:256
msgid "View draft"
msgstr ""
#: views/pages/create.py:183 views/pages/edit.py:302 wagtail_hooks.py:267
#: views/pages/create.py:183 views/pages/edit.py:302 wagtail_hooks.py:268
msgid "View live"
msgstr ""
@ -4030,7 +4029,7 @@ msgstr ""
msgid "Last published before"
msgstr ""
#: views/reports/aging_pages.py:33 wagtail_hooks.py:946
#: views/reports/aging_pages.py:33 wagtail_hooks.py:947
msgid "Aging pages"
msgstr ""
@ -4042,7 +4041,7 @@ msgstr ""
msgid "Hide commenting actions"
msgstr ""
#: views/reports/audit_logging.py:108 wagtail_hooks.py:935
#: views/reports/audit_logging.py:108 wagtail_hooks.py:936
msgid "Site history"
msgstr ""
@ -4058,11 +4057,11 @@ msgstr ""
msgid "Date/Time"
msgstr ""
#: views/reports/locked_pages.py:38 wagtail_hooks.py:902
#: views/reports/locked_pages.py:38 wagtail_hooks.py:903
msgid "Locked pages"
msgstr ""
#: views/reports/page_types_usage.py:100 wagtail_hooks.py:957
#: views/reports/page_types_usage.py:100 wagtail_hooks.py:958
msgid "Page types usage"
msgstr ""
@ -4075,7 +4074,7 @@ msgid "Awaiting my review"
msgstr ""
#: views/reports/workflows.py:139 views/workflows.py:119 views/workflows.py:588
#: wagtail_hooks.py:205 wagtail_hooks.py:913
#: wagtail_hooks.py:206 wagtail_hooks.py:914
msgid "Workflows"
msgstr ""
@ -4091,8 +4090,8 @@ msgstr ""
msgid "Page/Snippet Title"
msgstr ""
#: views/reports/workflows.py:211 views/workflows.py:514 wagtail_hooks.py:216
#: wagtail_hooks.py:924
#: views/reports/workflows.py:211 views/workflows.py:514 wagtail_hooks.py:217
#: wagtail_hooks.py:925
msgid "Workflow tasks"
msgstr ""
@ -4222,98 +4221,98 @@ msgstr ""
msgid "Choose another"
msgstr ""
#: wagtail_hooks.py:257
#: wagtail_hooks.py:258
#, python-format
msgid "Preview draft version of '%(title)s'"
msgstr ""
#: wagtail_hooks.py:269
#: wagtail_hooks.py:270
#, python-format
msgid "View live version of '%(title)s'"
msgstr ""
#: wagtail_hooks.py:291
#: wagtail_hooks.py:292
#, python-format
msgid "Move page '%(title)s'"
msgstr ""
#: wagtail_hooks.py:302
#: wagtail_hooks.py:303
#, python-format
msgid "Copy page '%(title)s'"
msgstr ""
#: wagtail_hooks.py:313
#: wagtail_hooks.py:314
#, python-format
msgid "Delete page '%(title)s'"
msgstr ""
#: wagtail_hooks.py:343
#: wagtail_hooks.py:344
#, python-format
msgid "Unpublish page '%(title)s'"
msgstr ""
#: wagtail_hooks.py:354
#: wagtail_hooks.py:355
#, python-format
msgid "View page history for '%(title)s'"
msgstr ""
#: wagtail_hooks.py:363
#: wagtail_hooks.py:364
msgid "Sort menu order"
msgstr ""
#: wagtail_hooks.py:365
#: wagtail_hooks.py:366
#, python-format
msgid "Change ordering of child pages of '%(title)s'"
msgstr ""
#: wagtail_hooks.py:497 wagtail_hooks.py:518 wagtail_hooks.py:539
#: wagtail_hooks.py:560 wagtail_hooks.py:581 wagtail_hooks.py:602
#: wagtail_hooks.py:498 wagtail_hooks.py:519 wagtail_hooks.py:540
#: wagtail_hooks.py:561 wagtail_hooks.py:582 wagtail_hooks.py:603
#, python-format
msgid "Heading %(level)d"
msgstr ""
#: wagtail_hooks.py:623
#: wagtail_hooks.py:624
msgid "Bulleted list"
msgstr ""
#: wagtail_hooks.py:647
#: wagtail_hooks.py:648
msgid "Numbered list"
msgstr ""
#: wagtail_hooks.py:671
#: wagtail_hooks.py:672
msgid "Blockquote"
msgstr ""
#: wagtail_hooks.py:738
#: wagtail_hooks.py:739
msgid "Link"
msgstr ""
#: wagtail_hooks.py:827
#: wagtail_hooks.py:828
msgid "Strikethrough"
msgstr ""
#: wagtail_hooks.py:848
#: wagtail_hooks.py:849
msgid "Code"
msgstr ""
#: wagtail_hooks.py:968
#: wagtail_hooks.py:969
msgid "Reports"
msgstr ""
#: wagtail_hooks.py:980
#: wagtail_hooks.py:981
#, python-format
msgid "What's new in Wagtail %(version)s"
msgstr ""
#: wagtail_hooks.py:992
#: wagtail_hooks.py:993
msgid "Editor Guide"
msgstr ""
#: wagtail_hooks.py:1009
#: wagtail_hooks.py:1010
msgid "Shortcuts"
msgstr ""
#: wagtail_hooks.py:1026
#: wagtail_hooks.py:1027
msgid "Help"
msgstr ""

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -98,6 +98,9 @@ msgstr "Raccolta"
msgid "Tag"
msgstr "Tag"
msgid "Filter by up to ten most popular tags."
msgstr "Filtra fino a dieci tag più popolari."
msgid "Preferred language"
msgstr "Lingua preferita"
@ -263,6 +266,20 @@ msgstr "Seleziona un nuovo genitore per questa pagina."
msgid "Parent page"
msgstr "Pagina padre"
msgid "The new page will be a child of this given parent page."
msgstr "Questa nuova pagina sarà un figlio della pagina padre in questione."
#, python-format
msgid "You do not have permission to create a page under \"%(page_title)s\"."
msgstr "Non hai i permessi per creare una pagina sotto \"%(page_title)s\""
#, python-format
msgid ""
"You cannot create a page of type \"%(page_type)s\" under \"%(page_title)s\"."
msgstr ""
"Non puoi creare una pagina del tipo \"%(page_type)s\" sotto "
"\"%(page_title)s\"."
msgid "Search…"
msgstr "Cerca..."
@ -659,13 +676,12 @@ msgid ""
"No collections have been created. Why not <a "
"href=\"%(add_collection_url)s\">add one</a>?"
msgstr ""
"Nessuna raccolta presente. Perché non ne <a "
"href=\"%(add_collection_url)s\">aggiungi una</a>?"
"Non ci sono raccolte presenti. Perché non ne <a "
"href=\"%(add_collection_url)s\">crei una</a>?"
#, python-format
msgid "Sorry, there are no matches for \"<em>%(search_query)s</em>\""
msgstr ""
"Spiacente, non ci sono corrispondenze per \"<em>%(search_query)s</em>\""
msgstr "Non ci sono corrispondenze per \"<em>%(search_query)s</em>\""
msgid "There are no results."
msgstr "Non ci sono risultati."
@ -1479,6 +1495,25 @@ msgstr[2] ""
"L'eliminazione di questa pagina eliminerà anche 1 traduzione e le sue "
"%(translation_descendant_count)s pagine figlie tradotte combinate."
#, python-format
msgid ""
"Deleting this page will also delete %(translation_count)s translations and "
"their combined %(translation_descendant_count)s translated child page."
msgid_plural ""
"Deleting this page will also delete %(translation_count)s translations and "
"their combined %(translation_descendant_count)s translated child pages."
msgstr[0] ""
"L'eliminazione di questa pagina eliminerà anche %(translation_count)s "
"traduzioni e la loro pagina figlia tradotta %(translation_descendant_count)s."
msgstr[1] ""
"L'eliminazione di questa pagina eliminerà anche %(translation_count)s "
"traduzioni e le relative %(translation_descendant_count)s pagine figlie "
"tradotte."
msgstr[2] ""
"L'eliminazione di questa pagina eliminerà anche %(translation_count)s "
"traduzioni e le relative %(translation_descendant_count)s pagine figlie "
"tradotte."
#, python-format
msgid "This action will delete total <b>%(total_pages)s</b> pages."
msgstr "Questa azione eliminerà un totale di <b>%(total_pages)s</b> pagine."
@ -1897,6 +1932,18 @@ msgstr "Torna indietro"
msgid "History"
msgstr "Storico"
msgid "Keyboard shortcuts"
msgstr "Scorciatoie da tastiera"
msgid "All keyboard shortcuts"
msgstr "Tuttle le scorciatoie da dastiera"
msgid "Section"
msgstr "Sezione"
msgid "Keyboard shortcut"
msgstr "Scorciatoia da tastiera"
msgid "Current page status:"
msgstr "Stato attuale della pagina:"
@ -2426,18 +2473,48 @@ msgstr "Inviato a %(task_name)s %(started_at)s"
msgid "%(status_display)s %(task_name)s %(started_at)s"
msgstr "%(status_display)s %(task_name)s %(started_at)s"
msgid "Common actions"
msgstr "Azioni comuni"
msgid "Cut"
msgstr "Taglia"
msgid "Paste"
msgstr "Incolla"
msgid "Paste and match style"
msgstr "Incolla e mantieni lo stile"
msgid "Paste without formatting"
msgstr "Incolla senza formattazione"
msgid "Undo"
msgstr "Annulla"
msgid "Redo"
msgstr "Ripristina"
msgid "Save changes"
msgstr "Salva modifiche"
msgid "Text content"
msgstr "Contenuto testo"
msgid "Insert or edit a link"
msgstr "Inserisci o modifica un link"
msgid "Text formatting"
msgstr "Formattazione testo"
msgid "Bold"
msgstr "Grassetto"
msgid "Italic"
msgstr "Corsivo"
msgid "Underline"
msgstr "Sottolineatura"
msgid "Superscript"
msgstr "Apice"
@ -2610,6 +2687,10 @@ msgstr "Cancella programma pubblicazione"
msgid "Workflow progress"
msgstr "Andamento flusso di lavoro"
#, python-format
msgid "%(model_name)s '%(title)s' is now unlocked."
msgstr "%(model_name)s '%(title)s' è sbloccata ora."
#, python-format
msgid "%(model_name)s '%(object)s' updated."
msgstr "%(model_name)s '%(object)s' aggiornato."
@ -2622,6 +2703,10 @@ msgstr "%(model_name)s '%(object)s' creato."
msgid "Edit '%(title)s'"
msgstr "Modifica '%(title)s'"
#, python-format
msgid "Copy '%(title)s'"
msgstr "Copia '%(title)s'"
msgid "Inspect"
msgstr "Controlla"
@ -2902,6 +2987,9 @@ msgstr "La pagina non può esser salvata a causa di errori di validazione"
msgid "Owner"
msgstr "Proprietario"
msgid "Edited by"
msgstr "Modificata da"
msgid "Site"
msgstr "Sito"
@ -2911,6 +2999,9 @@ msgstr "Qualsiasi"
msgid "Yes"
msgstr "Sì"
msgid "Page type"
msgstr "Tipologia pagina"
#, python-format
msgid "Page '%(page_title)s' is now unlocked."
msgstr "Pagina '%(page_title)s' sbloccata."
@ -2984,6 +3075,9 @@ msgstr "Data/Ora"
msgid "Locked pages"
msgstr "Pagine bloccate"
msgid "Page types usage"
msgstr "Utilizzo tipologie pagine"
msgid "Show"
msgstr "Mostra"
@ -3008,6 +3102,9 @@ msgstr "Attività nei flussi di lavoro"
msgid "Requested By"
msgstr "Richiesto da"
msgid "Show disabled"
msgstr "Mostra disabilitate"
msgid "Add a workflow"
msgstr "Aggiungi un flusso di lavoro"
@ -3041,6 +3138,26 @@ msgstr "Disabilita flusso di lavoro"
msgid "Workflow '%(object)s' disabled."
msgstr "Flusso di lavoro '%(object)s' disabilitato."
#, python-format
msgid ""
"This workflow is in progress on %(states_in_progress)d page/snippet. "
"Disabling this workflow will cancel moderation on this page/snippet."
msgid_plural ""
"This workflow is in progress on %(states_in_progress)d pages/snippets. "
"Disabling this workflow will cancel moderation on these pages/snippets."
msgstr[0] ""
"Questo flusso di lavoro è in corso sulla pagina/snippet "
"%(states_in_progress)d. Se disabiliti questo flusso di lavoro verrà rimossa "
"la moderazione da questa pagina/snippet."
msgstr[1] ""
"Questo flusso di lavoro è in corso sulle pagine/snippet "
"%(states_in_progress)d. Se disabiliti questo flusso di lavoro verrà rimossa "
"la moderazione da queste pagine/snippet."
msgstr[2] ""
"Questo flusso di lavoro è in corso sulle pagine/snippet "
"%(states_in_progress)d. Se disabiliti questo flusso di lavoro verrà rimossa "
"la moderazione da queste pagine/snippet."
#, python-format
msgid "Workflow '%(workflow_name)s' enabled."
msgstr "Flusso di lavoro '%(workflow_name)s' abilitato."
@ -3148,6 +3265,12 @@ msgstr "Cosa c'è di nuovo in Wagtail %(version)s"
msgid "Editor Guide"
msgstr "Guida editor"
msgid "Shortcuts"
msgstr "Scorciatoie"
msgid "Help"
msgstr "Aiuto"
msgid "Choose an item"
msgstr "Scegli un elemento"

Wyświetl plik

@ -1,10 +1,9 @@
import json
import warnings
from django.core.serializers.json import DjangoJSONEncoder
from django.forms import Media, widgets
from django.urls import reverse_lazy
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy
from wagtail.admin.rich_text.converters.contentstate import ContentstateConverter
from wagtail.admin.staticfiles import versioned_static
@ -13,23 +12,6 @@ from wagtail.telepath import register
from wagtail.widget_adapters import WidgetAdapter
class LazyStringEncoder(json.JSONEncoder):
"""
Add support for lazy strings to the JSON encoder so that URLs and
translations can be resolved when rendering the widget only.
"""
# The string "Edit" here is arbitrary, chosen because it exists elsewhere in the
# translations dictionary and is likely to remain in the future.
lazy_string_types = [type(reverse_lazy("Edit")), type(gettext_lazy("Edit"))]
def default(self, obj):
if type(obj) in self.lazy_string_types:
return str(obj)
return json.JSONEncoder.default(self, obj)
class DraftailRichTextArea(widgets.HiddenInput):
template_name = "wagtailadmin/widgets/draftail_rich_text_area.html"
is_hidden = False
@ -90,7 +72,7 @@ class DraftailRichTextArea(widgets.HiddenInput):
context = super().get_context(name, value, attrs)
context["widget"]["attrs"]["data-w-init-detail-value"] = json.dumps(
self.options,
cls=LazyStringEncoder,
cls=DjangoJSONEncoder,
)
return context

Wyświetl plik

@ -5,7 +5,7 @@
<div class="title-wrapper">
{% if page.is_site_root %}
{% if perms.wagtailcore.add_site or perms.wagtailcore.change_site or perms.wagtailcore.delete_site %}
<a href="{% url 'wagtailsites:index' %}" title="{% trans 'Sites menu' %}">{% icon name="site" classname="initial" %}</a>
<a href="{% url 'wagtailsites:index' %}" title="{% trans 'Sites menu' %}" class="w-flex w-items-center">{% icon name="site" classname="initial" %}</a>
{% endif %}
{% endif %}
@ -17,7 +17,7 @@
without also reading out the buttons and indicators.
{% endcomment %}
{% fragment as page_title %}
<span id="page_{{ page.pk|unlocalize|admin_urlquote }}_title">
<span id="page_{{ page.pk|unlocalize|admin_urlquote }}_title" class="w-flex w-items-center w-gap-2">
{% if not page.is_site_root and not page.is_leaf %}{% icon name="folder" classname="initial" %}{% endif %}
{{ page.get_admin_display_title }}
</span>

Wyświetl plik

@ -133,13 +133,15 @@ class TestPageExplorer(WagtailTestUtils, TestCase):
def test_explore_root_shows_icon(self):
response = self.client.get(reverse("wagtailadmin_explore_root"))
self.assertEqual(response.status_code, 200)
soup = self.get_soup(response.content)
# Administrator (or user with add_site permission) should see the
# sites link with its icon
self.assertContains(
response,
'<a href="/admin/sites/" title="Sites menu"><svg',
)
url = reverse("wagtailsites:index")
link = soup.select_one(f'td a[href="{url}"]')
self.assertIsNotNone(link)
icon = link.select_one("svg use[href='#icon-site']")
self.assertIsNotNone(icon)
def test_ordering(self):
response = self.client.get(

Wyświetl plik

@ -1571,6 +1571,35 @@ class TestEditHandler(WagtailTestUtils, TestCase):
self.assertIsNotNone(rendered_heading)
self.assertEqual(rendered_heading.text.strip(), expected_heading)
def test_field_permissions(self):
self.user.is_superuser = False
self.user.save()
self.user.user_permissions.add(
Permission.objects.get(
content_type__app_label="wagtailadmin", codename="access_admin"
),
Permission.objects.get(
content_type__app_label=self.object._meta.app_label,
codename=get_permission_codename("change", self.object._meta),
),
)
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(list(response.context["form"].fields), ["name"])
self.user.user_permissions.add(
Permission.objects.get(
codename="can_set_release_date",
)
)
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(
list(response.context["form"].fields), ["name", "release_date"]
)
class TestDefaultMessages(WagtailTestUtils, TestCase):
def setUp(self):

Wyświetl plik

@ -518,7 +518,3 @@ class Table(Component):
@cached_property
def attrs(self):
return self.table.get_row_attrs(self.instance)
class InlineActionsTable(Table):
classname = "listing listing--inline-actions"

Wyświetl plik

@ -16,7 +16,7 @@ from wagtail.admin.filters import (
MultipleUserFilter,
WagtailFilterSet,
)
from wagtail.admin.ui.tables import Column, DateColumn, InlineActionsTable, UserColumn
from wagtail.admin.ui.tables import Column, DateColumn, UserColumn
from wagtail.admin.utils import get_latest_str
from wagtail.admin.views.generic.base import (
BaseListingView,
@ -197,7 +197,6 @@ class HistoryView(PermissionCheckedMixin, BaseObjectMixin, BaseListingView):
is_searchable = False
paginate_by = 20
filterset_class = HistoryFilterSet
table_class = InlineActionsTable
history_url_name = None
history_results_url_name = None
edit_url_name = None

Wyświetl plik

@ -28,6 +28,7 @@ from django.views.generic.edit import (
from wagtail.actions.unpublish import UnpublishAction
from wagtail.admin import messages
from wagtail.admin.filters import WagtailFilterSet
from wagtail.admin.forms.models import WagtailAdminModelForm
from wagtail.admin.forms.search import SearchForm
from wagtail.admin.panels import get_edit_handler
from wagtail.admin.ui.components import Component, MediaContainer
@ -628,6 +629,24 @@ class CreateView(
for locale in Locale.objects.all().exclude(id=self.locale.id)
]
def get_initial_form_instance(self):
if self.locale:
instance = self.model()
instance.locale = self.locale
return instance
def get_form_kwargs(self):
if instance := self.get_initial_form_instance():
# super().get_form_kwargs() will use self.object as the instance kwarg
self.object = instance
kwargs = super().get_form_kwargs()
form_class = self.get_form_class()
# Add for_user support for PermissionedForm
if issubclass(form_class, WagtailAdminModelForm):
kwargs["for_user"] = self.request.user
return kwargs
def save_instance(self):
"""
Called after the form is successfully validated - saves the object to the db
@ -716,9 +735,9 @@ class EditView(
return self.actions
def get_object(self, queryset=None):
if "pk" not in self.kwargs:
self.kwargs["pk"] = self.args[0]
self.kwargs["pk"] = unquote(str(self.kwargs["pk"]))
if self.pk_url_kwarg not in self.kwargs:
self.kwargs[self.pk_url_kwarg] = self.args[0]
self.kwargs[self.pk_url_kwarg] = unquote(str(self.kwargs[self.pk_url_kwarg]))
return super().get_object(queryset)
def get_page_subtitle(self):
@ -802,6 +821,13 @@ class EditView(
for translation in self.object.get_translations().select_related("locale")
]
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
form_class = self.get_form_class()
if issubclass(form_class, WagtailAdminModelForm):
kwargs["for_user"] = self.request.user
return kwargs
def save_instance(self):
"""
Called after the form is successfully validated - saves the object to the db.

Wyświetl plik

@ -75,398 +75,6 @@ class StreamBlockValidationError(ValidationError):
return result
class BaseStreamBlock(Block):
def __init__(self, local_blocks=None, search_index=True, **kwargs):
self._constructor_kwargs = kwargs
self.search_index = search_index
super().__init__(**kwargs)
# create a local (shallow) copy of base_blocks so that it can be supplemented by local_blocks
self.child_blocks = self.base_blocks.copy()
if local_blocks:
for name, block in local_blocks:
block.set_name(name)
self.child_blocks[name] = block
def empty_value(self, raw_text=None):
return StreamValue(self, [], raw_text=raw_text)
def sorted_child_blocks(self):
"""Child blocks, sorted in to their groups."""
return sorted(
self.child_blocks.values(), key=lambda child_block: child_block.meta.group
)
def grouped_child_blocks(self):
"""
The available child block types of this stream block, organised into groups according to
their meta.group attribute.
Returned as an iterable of (group_name, list_of_blocks) tuples
"""
return itertools.groupby(
self.sorted_child_blocks(), key=lambda child_block: child_block.meta.group
)
def value_from_datadict(self, data, files, prefix):
count = int(data["%s-count" % prefix])
values_with_indexes = []
for i in range(0, count):
if data["%s-%d-deleted" % (prefix, i)]:
continue
block_type_name = data["%s-%d-type" % (prefix, i)]
try:
child_block = self.child_blocks[block_type_name]
except KeyError:
continue
values_with_indexes.append(
(
int(data["%s-%d-order" % (prefix, i)]),
block_type_name,
child_block.value_from_datadict(
data, files, "%s-%d-value" % (prefix, i)
),
data.get("%s-%d-id" % (prefix, i)),
)
)
values_with_indexes.sort()
return StreamValue(
self,
[
(child_block_type_name, value, block_id)
for (
index,
child_block_type_name,
value,
block_id,
) in values_with_indexes
],
)
def value_omitted_from_data(self, data, files, prefix):
return ("%s-count" % prefix) not in data
@property
def required(self):
return self.meta.required
def clean(self, value):
cleaned_data = []
errors = {}
non_block_errors = ErrorList()
for i, child in enumerate(value): # child is a StreamChild instance
try:
cleaned_data.append(
(child.block.name, child.block.clean(child.value), child.id)
)
except ValidationError as e:
errors[i] = e
if self.meta.min_num is not None and self.meta.min_num > len(value):
non_block_errors.append(
ValidationError(
_("The minimum number of items is %(min_num)d")
% {"min_num": self.meta.min_num}
)
)
elif self.required and len(value) == 0:
non_block_errors.append(ValidationError(_("This field is required.")))
if self.meta.max_num is not None and self.meta.max_num < len(value):
non_block_errors.append(
ValidationError(
_("The maximum number of items is %(max_num)d")
% {"max_num": self.meta.max_num}
)
)
if self.meta.block_counts:
block_counts = defaultdict(int)
for item in value:
block_counts[item.block_type] += 1
for block_name, min_max in self.meta.block_counts.items():
block = self.child_blocks[block_name]
max_num = min_max.get("max_num", None)
min_num = min_max.get("min_num", None)
block_count = block_counts[block_name]
if min_num is not None and min_num > block_count:
non_block_errors.append(
ValidationError(
"{}: {}".format(
block.label,
_("The minimum number of items is %(min_num)d")
% {"min_num": min_num},
)
)
)
if max_num is not None and max_num < block_count:
non_block_errors.append(
ValidationError(
"{}: {}".format(
block.label,
_("The maximum number of items is %(max_num)d")
% {"max_num": max_num},
)
)
)
if errors or non_block_errors:
# The message here is arbitrary - outputting error messages is delegated to the child blocks,
# which only involves the 'params' list
raise StreamBlockValidationError(
block_errors=errors, non_block_errors=non_block_errors
)
return StreamValue(self, cleaned_data)
def to_python(self, value):
if isinstance(value, StreamValue):
return value
elif isinstance(value, str) and value:
try:
value = json.loads(value)
except ValueError:
# value is not valid JSON; most likely, this field was previously a
# rich text field before being migrated to StreamField, and the data
# was left intact in the migration. Return an empty stream instead
# (but keep the raw text available as an attribute, so that it can be
# used to migrate that data to StreamField)
return self.empty_value(raw_text=value)
if not value:
return self.empty_value()
# ensure value is a list and not some other kind of iterable
value = list(value)
if isinstance(value[0], dict):
# value is in JSONish representation - a dict with 'type' and 'value' keys.
# This is passed to StreamValue to be expanded lazily - but first we reject any unrecognised
# block types from the list
return StreamValue(
self,
[
child_data
for child_data in value
if child_data["type"] in self.child_blocks
],
is_lazy=True,
)
else:
# See if it looks like the standard non-smart representation of a
# StreamField value: a list of (block_name, value) tuples
try:
[None for (x, y) in value]
except (TypeError, ValueError) as exc:
# Give up trying to make sense of the value
raise TypeError(
f"Cannot handle {value!r} (type {type(value)!r}) as a value of a StreamBlock"
) from exc
# Test succeeded, so return as a StreamValue-ified version of that value
return StreamValue(
self,
[
(k, self.child_blocks[k].normalize(v))
for k, v in value
if k in self.child_blocks
],
)
def bulk_to_python(self, values):
# 'values' is a list of streams, each stream being a list of dicts with 'type', 'value' and
# optionally 'id'.
# We will iterate over these streams, constructing:
# 1) a set of per-child-block lists ('child_inputs'), to be sent to each child block's
# bulk_to_python method in turn (giving us 'child_outputs')
# 2) a 'block map' of each stream, telling us the type and id of each block and the index we
# need to look up in the corresponding child_outputs list to obtain its final value
child_inputs = defaultdict(list)
block_maps = []
for stream in values:
block_map = []
for block_dict in stream:
block_type = block_dict["type"]
if block_type not in self.child_blocks:
# skip any blocks with an unrecognised type
continue
child_input_list = child_inputs[block_type]
child_index = len(child_input_list)
child_input_list.append(block_dict["value"])
block_map.append((block_type, block_dict.get("id"), child_index))
block_maps.append(block_map)
# run each list in child_inputs through the relevant block's bulk_to_python
# to obtain child_outputs
child_outputs = {
block_type: self.child_blocks[block_type].bulk_to_python(child_input_list)
for block_type, child_input_list in child_inputs.items()
}
# for each stream, go through the block map, picking out the appropriately-indexed
# value from the relevant list in child_outputs
return [
StreamValue(
self,
[
(block_type, child_outputs[block_type][child_index], id)
for block_type, id, child_index in block_map
],
is_lazy=False,
)
for block_map in block_maps
]
def get_prep_value(self, value):
if not value:
# Falsy values (including None, empty string, empty list, and
# empty StreamValue) become an empty stream
return []
else:
# value is a StreamValue - delegate to its get_prep_value() method
# (which has special-case handling for lazy StreamValues to avoid useless
# round-trips to the full data representation and back)
return value.get_prep_value()
def normalize(self, value):
return self.to_python(value)
def get_form_state(self, value):
if not value:
return []
else:
return [
{
"type": child.block.name,
"value": child.block.get_form_state(child.value),
"id": child.id,
}
for child in value
]
def get_api_representation(self, value, context=None):
if value is None:
# treat None as identical to an empty stream
return []
return [
{
"type": child.block.name,
"value": child.block.get_api_representation(
child.value, context=context
),
"id": child.id,
}
for child in value # child is a StreamChild instance
]
def render_basic(self, value, context=None):
return format_html_join(
"\n",
'<div class="block-{1}">{0}</div>',
[(child.render(context=context), child.block_type) for child in value],
)
def get_searchable_content(self, value):
if not self.search_index:
return []
content = []
for child in value:
content.extend(child.block.get_searchable_content(child.value))
return content
def extract_references(self, value):
for child in value:
for (
model,
object_id,
model_path,
content_path,
) in child.block.extract_references(child.value):
model_path = (
f"{child.block_type}.{model_path}"
if model_path
else child.block_type
)
content_path = (
f"{child.id}.{content_path}" if content_path else child.id
)
yield model, object_id, model_path, content_path
def get_block_by_content_path(self, value, path_elements):
"""
Given a list of elements from a content path, retrieve the block at that path
as a BoundBlock object, or None if the path does not correspond to a valid block.
"""
if path_elements:
id, *remaining_elements = path_elements
for child in value:
if child.id == id:
return child.block.get_block_by_content_path(
child.value, remaining_elements
)
else:
# an empty path refers to the stream as a whole
return self.bind(value)
def deconstruct(self):
"""
Always deconstruct StreamBlock instances as if they were plain StreamBlocks with all of the
field definitions passed to the constructor - even if in reality this is a subclass of StreamBlock
with the fields defined declaratively, or some combination of the two.
This ensures that the field definitions get frozen into migrations, rather than leaving a reference
to a custom subclass in the user's models.py that may or may not stick around.
"""
path = "wagtail.blocks.StreamBlock"
args = [list(self.child_blocks.items())]
kwargs = self._constructor_kwargs
return (path, args, kwargs)
def check(self, **kwargs):
errors = super().check(**kwargs)
for name, child_block in self.child_blocks.items():
errors.extend(child_block.check(**kwargs))
errors.extend(child_block._check_name(**kwargs))
return errors
class Meta:
# No icon specified here, because that depends on the purpose that the
# block is being used for. Feel encouraged to specify an icon in your
# descendant block type
icon = "placeholder"
default = []
required = True
form_classname = None
min_num = None
max_num = None
block_counts = {}
collapsed = False
MUTABLE_META_ATTRIBUTES = [
"required",
"min_num",
"max_num",
"block_counts",
"collapsed",
]
class StreamBlock(BaseStreamBlock, metaclass=DeclarativeSubBlocksMetaclass):
pass
class StreamValue(MutableSequence):
"""
Custom type used to represent the value of a StreamBlock; behaves as a sequence of BoundBlocks
@ -801,6 +409,400 @@ class StreamValue(MutableSequence):
)
class BaseStreamBlock(Block):
def __init__(self, local_blocks=None, search_index=True, **kwargs):
self._constructor_kwargs = kwargs
self.search_index = search_index
super().__init__(**kwargs)
# create a local (shallow) copy of base_blocks so that it can be supplemented by local_blocks
self.child_blocks = self.base_blocks.copy()
if local_blocks:
for name, block in local_blocks:
block.set_name(name)
self.child_blocks[name] = block
def empty_value(self, raw_text=None):
return self.meta.value_class(self, [], raw_text=raw_text)
def sorted_child_blocks(self):
"""Child blocks, sorted in to their groups."""
return sorted(
self.child_blocks.values(), key=lambda child_block: child_block.meta.group
)
def grouped_child_blocks(self):
"""
The available child block types of this stream block, organised into groups according to
their meta.group attribute.
Returned as an iterable of (group_name, list_of_blocks) tuples
"""
return itertools.groupby(
self.sorted_child_blocks(), key=lambda child_block: child_block.meta.group
)
def value_from_datadict(self, data, files, prefix):
count = int(data["%s-count" % prefix])
values_with_indexes = []
for i in range(0, count):
if data["%s-%d-deleted" % (prefix, i)]:
continue
block_type_name = data["%s-%d-type" % (prefix, i)]
try:
child_block = self.child_blocks[block_type_name]
except KeyError:
continue
values_with_indexes.append(
(
int(data["%s-%d-order" % (prefix, i)]),
block_type_name,
child_block.value_from_datadict(
data, files, "%s-%d-value" % (prefix, i)
),
data.get("%s-%d-id" % (prefix, i)),
)
)
values_with_indexes.sort()
return self.meta.value_class(
self,
[
(child_block_type_name, value, block_id)
for (
index,
child_block_type_name,
value,
block_id,
) in values_with_indexes
],
)
def value_omitted_from_data(self, data, files, prefix):
return ("%s-count" % prefix) not in data
@property
def required(self):
return self.meta.required
def clean(self, value):
cleaned_data = []
errors = {}
non_block_errors = ErrorList()
for i, child in enumerate(value): # child is a StreamChild instance
try:
cleaned_data.append(
(child.block.name, child.block.clean(child.value), child.id)
)
except ValidationError as e:
errors[i] = e
if self.meta.min_num is not None and self.meta.min_num > len(value):
non_block_errors.append(
ValidationError(
_("The minimum number of items is %(min_num)d")
% {"min_num": self.meta.min_num}
)
)
elif self.required and len(value) == 0:
non_block_errors.append(ValidationError(_("This field is required.")))
if self.meta.max_num is not None and self.meta.max_num < len(value):
non_block_errors.append(
ValidationError(
_("The maximum number of items is %(max_num)d")
% {"max_num": self.meta.max_num}
)
)
if self.meta.block_counts:
block_counts = defaultdict(int)
for item in value:
block_counts[item.block_type] += 1
for block_name, min_max in self.meta.block_counts.items():
block = self.child_blocks[block_name]
max_num = min_max.get("max_num", None)
min_num = min_max.get("min_num", None)
block_count = block_counts[block_name]
if min_num is not None and min_num > block_count:
non_block_errors.append(
ValidationError(
"{}: {}".format(
block.label,
_("The minimum number of items is %(min_num)d")
% {"min_num": min_num},
)
)
)
if max_num is not None and max_num < block_count:
non_block_errors.append(
ValidationError(
"{}: {}".format(
block.label,
_("The maximum number of items is %(max_num)d")
% {"max_num": max_num},
)
)
)
if errors or non_block_errors:
# The message here is arbitrary - outputting error messages is delegated to the child blocks,
# which only involves the 'params' list
raise StreamBlockValidationError(
block_errors=errors, non_block_errors=non_block_errors
)
return self.meta.value_class(self, cleaned_data)
def to_python(self, value):
if isinstance(value, self.meta.value_class):
return value
elif isinstance(value, str) and value:
try:
value = json.loads(value)
except ValueError:
# value is not valid JSON; most likely, this field was previously a
# rich text field before being migrated to StreamField, and the data
# was left intact in the migration. Return an empty stream instead
# (but keep the raw text available as an attribute, so that it can be
# used to migrate that data to StreamField)
return self.empty_value(raw_text=value)
if not value:
return self.empty_value()
# ensure value is a list and not some other kind of iterable
value = list(value)
if isinstance(value[0], dict):
# value is in JSONish representation - a dict with 'type' and 'value' keys.
# This is passed to StreamValue to be expanded lazily - but first we reject any unrecognised
# block types from the list
return self.meta.value_class(
self,
[
child_data
for child_data in value
if child_data["type"] in self.child_blocks
],
is_lazy=True,
)
else:
# See if it looks like the standard non-smart representation of a
# StreamField value: a list of (block_name, value) tuples
try:
[None for (x, y) in value]
except (TypeError, ValueError) as exc:
# Give up trying to make sense of the value
raise TypeError(
f"Cannot handle {value!r} (type {type(value)!r}) as a value of a StreamBlock"
) from exc
# Test succeeded, so return as a StreamValue-ified version of that value
return self.meta.value_class(
self,
[
(k, self.child_blocks[k].normalize(v))
for k, v in value
if k in self.child_blocks
],
)
def bulk_to_python(self, values):
# 'values' is a list of streams, each stream being a list of dicts with 'type', 'value' and
# optionally 'id'.
# We will iterate over these streams, constructing:
# 1) a set of per-child-block lists ('child_inputs'), to be sent to each child block's
# bulk_to_python method in turn (giving us 'child_outputs')
# 2) a 'block map' of each stream, telling us the type and id of each block and the index we
# need to look up in the corresponding child_outputs list to obtain its final value
child_inputs = defaultdict(list)
block_maps = []
for stream in values:
block_map = []
for block_dict in stream:
block_type = block_dict["type"]
if block_type not in self.child_blocks:
# skip any blocks with an unrecognised type
continue
child_input_list = child_inputs[block_type]
child_index = len(child_input_list)
child_input_list.append(block_dict["value"])
block_map.append((block_type, block_dict.get("id"), child_index))
block_maps.append(block_map)
# run each list in child_inputs through the relevant block's bulk_to_python
# to obtain child_outputs
child_outputs = {
block_type: self.child_blocks[block_type].bulk_to_python(child_input_list)
for block_type, child_input_list in child_inputs.items()
}
# for each stream, go through the block map, picking out the appropriately-indexed
# value from the relevant list in child_outputs
return [
self.meta.value_class(
self,
[
(block_type, child_outputs[block_type][child_index], id)
for block_type, id, child_index in block_map
],
is_lazy=False,
)
for block_map in block_maps
]
def get_prep_value(self, value):
if not value:
# Falsy values (including None, empty string, empty list, and
# empty StreamValue) become an empty stream
return []
else:
# value is a StreamValue - delegate to its get_prep_value() method
# (which has special-case handling for lazy StreamValues to avoid useless
# round-trips to the full data representation and back)
return value.get_prep_value()
def normalize(self, value):
return self.to_python(value)
def get_form_state(self, value):
if not value:
return []
else:
return [
{
"type": child.block.name,
"value": child.block.get_form_state(child.value),
"id": child.id,
}
for child in value
]
def get_api_representation(self, value, context=None):
if value is None:
# treat None as identical to an empty stream
return []
return [
{
"type": child.block.name,
"value": child.block.get_api_representation(
child.value, context=context
),
"id": child.id,
}
for child in value # child is a StreamChild instance
]
def render_basic(self, value, context=None):
return format_html_join(
"\n",
'<div class="block-{1}">{0}</div>',
[(child.render(context=context), child.block_type) for child in value],
)
def get_searchable_content(self, value):
if not self.search_index:
return []
content = []
for child in value:
content.extend(child.block.get_searchable_content(child.value))
return content
def extract_references(self, value):
for child in value:
for (
model,
object_id,
model_path,
content_path,
) in child.block.extract_references(child.value):
model_path = (
f"{child.block_type}.{model_path}"
if model_path
else child.block_type
)
content_path = (
f"{child.id}.{content_path}" if content_path else child.id
)
yield model, object_id, model_path, content_path
def get_block_by_content_path(self, value, path_elements):
"""
Given a list of elements from a content path, retrieve the block at that path
as a BoundBlock object, or None if the path does not correspond to a valid block.
"""
if path_elements:
id, *remaining_elements = path_elements
for child in value:
if child.id == id:
return child.block.get_block_by_content_path(
child.value, remaining_elements
)
else:
# an empty path refers to the stream as a whole
return self.bind(value)
def deconstruct(self):
"""
Always deconstruct StreamBlock instances as if they were plain StreamBlocks with all of the
field definitions passed to the constructor - even if in reality this is a subclass of StreamBlock
with the fields defined declaratively, or some combination of the two.
This ensures that the field definitions get frozen into migrations, rather than leaving a reference
to a custom subclass in the user's models.py that may or may not stick around.
"""
path = "wagtail.blocks.StreamBlock"
args = [list(self.child_blocks.items())]
kwargs = self._constructor_kwargs
return (path, args, kwargs)
def check(self, **kwargs):
errors = super().check(**kwargs)
for name, child_block in self.child_blocks.items():
errors.extend(child_block.check(**kwargs))
errors.extend(child_block._check_name(**kwargs))
return errors
class Meta:
# No icon specified here, because that depends on the purpose that the
# block is being used for. Feel encouraged to specify an icon in your
# descendant block type
icon = "placeholder"
default = []
required = True
form_classname = None
min_num = None
max_num = None
block_counts = {}
collapsed = False
value_class = StreamValue
MUTABLE_META_ATTRIBUTES = [
"required",
"min_num",
"max_num",
"block_counts",
"collapsed",
"value_class",
]
class StreamBlock(BaseStreamBlock, metaclass=DeclarativeSubBlocksMetaclass):
pass
class StreamBlockAdapter(Adapter):
js_constructor = "wagtail.blocks.StreamBlock"

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -22,20 +22,19 @@ msgstr ""
msgid "Wagtail redirects"
msgstr ""
#: filters.py:12 templates/wagtailredirects/list.html:20 views.py:102
#: views.py:466
#: filters.py:26 views.py:102 views.py:119
msgid "Type"
msgstr ""
#: filters.py:15
#: filters.py:29
msgid "Permanent"
msgstr ""
#: filters.py:16
#: filters.py:30
msgid "Temporary"
msgstr ""
#: filters.py:18
#: filters.py:32
msgid "All"
msgstr ""
@ -68,59 +67,59 @@ msgstr ""
msgid "To field"
msgstr ""
#: models.py:12
#: models.py:13
msgid "redirect from"
msgstr ""
#: models.py:16
#: models.py:17
msgid "site"
msgstr ""
#: models.py:24 models.py:84
#: models.py:25 models.py:85
msgid "permanent"
msgstr ""
#: models.py:27
#: models.py:28
msgid ""
"Recommended. Permanent redirects ensure search engines forget the old page "
"(the 'Redirect from') and index the new page instead."
msgstr ""
#: models.py:33
#: models.py:34
msgid "redirect to a page"
msgstr ""
#: models.py:39
#: models.py:40
msgid "target page route"
msgstr ""
#: models.py:41
#: models.py:42
msgid ""
"Optionally specify a route on the target page to redirect to. Leave blank to "
"redirect to the default page route."
msgstr ""
#: models.py:48
#: models.py:49
msgid "redirect to any URL"
msgstr ""
#: models.py:51
#: models.py:52
msgid "automatically created"
msgstr ""
#: models.py:56
#: models.py:57
msgid "created at"
msgstr ""
#: models.py:86
#: models.py:87
msgid "temporary"
msgstr ""
#: models.py:203
#: models.py:204
msgid "redirect"
msgstr ""
#: models.py:204
#: models.py:205
msgid "redirects"
msgstr ""
@ -130,7 +129,6 @@ msgid "Add redirect"
msgstr ""
#: templates/wagtailredirects/add.html:19
#: templates/wagtailredirects/edit.html:19
msgid "Save"
msgstr ""
@ -141,7 +139,7 @@ msgstr ""
#: templates/wagtailredirects/choose_import_file.html:6
#: templates/wagtailredirects/confirm_import.html:6
#: templates/wagtailredirects/import_summary.html:5 views.py:116
#: templates/wagtailredirects/import_summary.html:5 views.py:130
msgid "Import redirects"
msgstr ""
@ -191,19 +189,6 @@ msgstr ""
msgid "Preview"
msgstr ""
#: templates/wagtailredirects/edit.html:3
#, python-format
msgid "Editing %(title)s"
msgstr ""
#: templates/wagtailredirects/edit.html:5
msgid "Editing"
msgstr ""
#: templates/wagtailredirects/edit.html:21
msgid "Delete redirect"
msgstr ""
#: templates/wagtailredirects/import_summary.html:3
#: templates/wagtailredirects/import_summary.html:6
msgid "Summary"
@ -215,14 +200,11 @@ msgid ""
"Found %(total)s redirects, created %(successes)s and found %(errors)s errors."
msgstr ""
#: templates/wagtailredirects/import_summary.html:17
#: templates/wagtailredirects/list.html:12
#: templates/wagtailredirects/list.html:15 views.py:84 views.py:463
#: templates/wagtailredirects/import_summary.html:17 views.py:84 views.py:116
msgid "From"
msgstr ""
#: templates/wagtailredirects/import_summary.html:18
#: templates/wagtailredirects/list.html:19 views.py:96 views.py:465
#: templates/wagtailredirects/import_summary.html:18 views.py:96 views.py:118
msgid "To"
msgstr ""
@ -246,76 +228,59 @@ msgid ""
"href=\"%(wagtailredirects_add_redirect_url)s\">add one</a>?"
msgstr ""
#: templates/wagtailredirects/list.html:18 views.py:90 views.py:464
#: views.py:90 views.py:117
msgid "Site"
msgstr ""
#: templates/wagtailredirects/list.html:27
msgid "Edit this redirect"
#: views.py:148
msgid "The redirect could not be saved due to errors."
msgstr ""
#: templates/wagtailredirects/reports/redirects_report.html:8
#: templates/wagtailredirects/reports/redirects_report.html:13
msgid "No redirects found."
msgstr ""
#: views.py:122
msgid "Export redirects"
msgstr ""
#: views.py:147
#: views.py:153
#, python-format
msgid "Redirect '%(redirect_title)s' updated."
msgstr ""
#: views.py:152 views.py:220
msgid "Edit"
msgstr ""
#: views.py:158
msgid "The redirect could not be saved due to errors."
msgstr ""
#: views.py:190
#: views.py:173
#, python-format
msgid "Redirect '%(redirect_title)s' deleted."
msgstr ""
#: views.py:215
#: views.py:198
#, python-format
msgid "Redirect '%(redirect_title)s' added."
msgstr ""
#: views.py:227
#: views.py:203
msgid "Edit"
msgstr ""
#: views.py:210
msgid "The redirect could not be created due to errors."
msgstr ""
#: views.py:266
#: views.py:249
msgid "Search redirects"
msgstr ""
#: views.py:280
#: views.py:263
#, python-format
msgid "File format of type \"%(extension)s\" is not supported"
msgstr ""
#: views.py:297
#: views.py:280
#, python-format
msgid "Imported file has a wrong encoding: %(error_message)s"
msgstr ""
#: views.py:304
#: views.py:287
#, python-format
msgid "%(error)s encountered while trying to read file: %(filename)s"
msgstr ""
#: views.py:395
#: views.py:378
#, python-format
msgid "Imported %(total)d redirect"
msgid_plural "Imported %(total)d redirects"
msgstr[0] ""
msgstr[1] ""
#: views.py:451
msgid "Export Redirects"
msgstr ""

Wyświetl plik

@ -1,38 +1 @@
{% extends "wagtailadmin/base.html" %}
{% load i18n wagtailadmin_tags %}
{% block titletag %}{% blocktrans trimmed with title=redirect.title %}Editing {{ title }}{% endblocktrans %}{% endblock %}
{% block content %}
{% trans "Editing" as editing_str %}
{% include "wagtailadmin/shared/header.html" with title=editing_str subtitle=redirect.title icon="redirect" %}
{% include "wagtailadmin/shared/non_field_errors.html" %}
<form action="{% url 'wagtailredirects:edit' redirect.id %}" method="POST" class="nice-padding" novalidate>
{% csrf_token %}
<ul class="fields">
{% for field in form.visible_fields %}
<li>{% formattedfield field %}</li>
{% endfor %}
<li>
<input type="submit" value="{% trans 'Save' %}" class="button" />
{% if user_can_delete %}
<a href="{% url 'wagtailredirects:delete' redirect.id %}" class="button no">{% trans "Delete redirect" %}</a>
{% endif %}
</li>
</ul>
</form>
{% endblock %}
{% block extra_js %}
{{ block.super }}
{% include "wagtailadmin/pages/_editor_js.html" %}
{{ form.media.js }}
{% endblock %}
{% block extra_css %}
{{ block.super }}
{{ form.media.css }}
{% endblock %}
{% extends "wagtailadmin/generic/edit.html" %}

Wyświetl plik

@ -1,6 +1,7 @@
from io import BytesIO
from django.conf import settings
from django.contrib.auth.models import Permission
from django.test import TestCase, override_settings
from django.urls import reverse
from openpyxl.reader.excel import load_workbook
@ -948,7 +949,7 @@ class TestRedirectsAddView(WagtailTestUtils, TestCase):
self.assertIsNone(redirects.first().site)
class TestRedirectsEditView(WagtailTestUtils, TestCase):
class TestRedirectsEditView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
def setUp(self):
# Create a redirect to edit
self.redirect = models.Redirect(
@ -975,6 +976,13 @@ class TestRedirectsEditView(WagtailTestUtils, TestCase):
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailredirects/edit.html")
self.assertBreadcrumbsItemsRendered(
[
{"url": reverse("wagtailredirects:index"), "label": "Redirects"},
{"url": "", "label": "/test"},
],
response.content,
)
url_finder = AdminURLFinder(self.user)
expected_url = "/admin/redirects/%d/" % self.redirect.id
@ -1056,6 +1064,39 @@ class TestRedirectsEditView(WagtailTestUtils, TestCase):
# Should not redirect to index
self.assertEqual(response.status_code, 200)
def test_get_with_no_permission(self, redirect_id=None):
self.user.is_superuser = False
self.user.save()
# Only basic access_admin permission is given
self.user.user_permissions.add(
Permission.objects.get(
content_type__app_label="wagtailadmin",
codename="access_admin",
)
)
response = self.get()
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, reverse("wagtailadmin_home"))
def test_get_with_edit_permission_only(self):
self.user.is_superuser = False
self.user.save()
self.user.user_permissions.add(
Permission.objects.get(
content_type__app_label="wagtailadmin",
codename="access_admin",
),
Permission.objects.get(
content_type__app_label="wagtailredirects",
codename="change_redirect",
),
)
response = self.get()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailredirects/edit.html")
class TestRedirectsDeleteView(WagtailTestUtils, TestCase):
def setUp(self):

Wyświetl plik

@ -7,7 +7,7 @@ urlpatterns = [
path("", views.IndexView.as_view(), name="index"),
path("results/", views.IndexView.as_view(results_only=True), name="index_results"),
path("add/", views.add, name="add"),
path("<int:redirect_id>/", views.edit, name="edit"),
path("<int:redirect_id>/", views.EditView.as_view(), name="edit"),
path("<int:redirect_id>/delete/", views.delete, name="delete"),
path("import/", views.start_import, name="start_import"),
path("import/process/", views.process_import, name="process_import"),

Wyświetl plik

@ -136,49 +136,23 @@ class IndexView(generic.IndexView):
return buttons
@permission_checker.require("change")
def edit(request, redirect_id):
theredirect = get_object_or_404(models.Redirect, id=redirect_id)
class EditView(generic.EditView):
model = Redirect
form_class = RedirectForm
permission_policy = permission_policy
template_name = "wagtailredirects/edit.html"
index_url_name = "wagtailredirects:index"
edit_url_name = "wagtailredirects:edit"
delete_url_name = "wagtailredirects:delete"
pk_url_kwarg = "redirect_id"
error_message = gettext_lazy("The redirect could not be saved due to errors.")
header_icon = "redirect"
_show_breadcrumbs = True
if not permission_policy.user_has_permission_for_instance(
request.user, "change", theredirect
):
raise PermissionDenied
if request.method == "POST":
form = RedirectForm(request.POST, request.FILES, instance=theredirect)
if form.is_valid():
with transaction.atomic():
form.save()
log(instance=theredirect, action="wagtail.edit")
messages.success(
request,
_("Redirect '%(redirect_title)s' updated.")
% {"redirect_title": theredirect.title},
buttons=[
messages.button(
reverse("wagtailredirects:edit", args=(theredirect.id,)),
_("Edit"),
)
],
)
return redirect("wagtailredirects:index")
else:
messages.error(request, _("The redirect could not be saved due to errors."))
else:
form = RedirectForm(instance=theredirect)
return TemplateResponse(
request,
"wagtailredirects/edit.html",
{
"redirect": theredirect,
"form": form,
"user_can_delete": permission_policy.user_has_permission(
request.user, "delete"
),
},
)
def get_success_message(self):
return _("Redirect '%(redirect_title)s' updated.") % {
"redirect_title": self.object.title
}
@permission_checker.require("delete")

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

Wyświetl plik

@ -21,6 +21,9 @@ msgstr ""
"Language: hi\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "default"
msgstr "मूलचर"
#, python-format
msgid "%(site_setting)s for %(site)s"
msgstr "%(site_setting)s for %(site)s"

Wyświetl plik

@ -14,6 +14,7 @@ from wagtail.test.testapp.models import (
PanelGenericSettings,
TabbedGenericSettings,
TestGenericSetting,
TestPermissionedGenericSetting,
)
from wagtail.test.utils import WagtailTestUtils
@ -76,6 +77,11 @@ class BaseTestGenericSettingView(WagtailTestUtils, TestCase):
class TestGenericSettingCreateView(BaseTestGenericSettingView):
def setUp(self):
self.user = self.login()
self.user.user_permissions.add(
Permission.objects.get(
content_type__app_label="wagtailadmin", codename="access_admin"
)
)
def test_get_edit(self):
response = self.get()
@ -107,6 +113,38 @@ class TestGenericSettingCreateView(BaseTestGenericSettingView):
# Ensure the form supports file uploads
self.assertContains(response, 'enctype="multipart/form-data"')
def test_create_restricted_field_without_permission(self):
self.user.is_superuser = False
self.user.save()
self.assertFalse(TestPermissionedGenericSetting.objects.exists())
response = self.post(
post_data={"sensitive_email": "test@example.com", "title": "test"},
setting=TestPermissionedGenericSetting,
)
self.assertEqual(response.status_code, 302)
settings = TestPermissionedGenericSetting.objects.get()
self.assertEqual(settings.title, "test")
self.assertEqual(settings.sensitive_email, "")
def test_create_restricted_field(self):
self.user.is_superuser = False
self.user.save()
self.user.user_permissions.add(
Permission.objects.get(codename="can_edit_sensitive_email_generic_setting")
)
self.assertFalse(TestPermissionedGenericSetting.objects.exists())
response = self.post(
post_data={"sensitive_email": "test@example.com", "title": "test"},
setting=TestPermissionedGenericSetting,
)
self.assertEqual(response.status_code, 302)
settings = TestPermissionedGenericSetting.objects.get()
self.assertEqual(settings.title, "test")
self.assertEqual(settings.sensitive_email, "test@example.com")
class TestGenericSettingEditView(BaseTestGenericSettingView):
def setUp(self):
@ -114,7 +152,12 @@ class TestGenericSettingEditView(BaseTestGenericSettingView):
self.test_setting.title = "Setting title"
self.test_setting.save()
self.login()
self.user = self.login()
self.user.user_permissions.add(
Permission.objects.get(
content_type__app_label="wagtailadmin", codename="access_admin"
)
)
def test_get_edit(self):
response = self.get()
@ -162,6 +205,50 @@ class TestGenericSettingEditView(BaseTestGenericSettingView):
expected_url=f"{url}{TestGenericSetting.objects.first().pk}/",
)
def test_edit_restricted_field(self):
test_setting = TestPermissionedGenericSetting()
test_setting.sensitive_email = "test@example.com"
test_setting.save()
self.user.is_superuser = False
self.user.save()
self.user.user_permissions.add(
Permission.objects.get(codename="can_edit_sensitive_email_generic_setting")
)
response = self.get(setting=TestPermissionedGenericSetting)
self.assertEqual(response.status_code, 200)
self.assertIn("sensitive_email", list(response.context["form"].fields))
response = self.post(
setting=TestPermissionedGenericSetting,
post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
)
self.assertEqual(response.status_code, 302)
test_setting.refresh_from_db()
self.assertEqual(test_setting.sensitive_email, "test-updated@example.com")
def test_edit_restricted_field_without_permission(self):
test_setting = TestPermissionedGenericSetting()
test_setting.sensitive_email = "test@example.com"
test_setting.save()
self.user.is_superuser = False
self.user.save()
response = self.get(setting=TestPermissionedGenericSetting)
self.assertEqual(response.status_code, 200)
self.assertNotIn("sensitive_email", list(response.context["form"].fields))
response = self.post(
setting=TestPermissionedGenericSetting,
post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
)
self.assertEqual(response.status_code, 302)
test_setting.refresh_from_db()
self.assertEqual(test_setting.sensitive_email, "test@example.com")
class TestAdminPermission(WagtailTestUtils, TestCase):
def test_registered_permission(self):

Wyświetl plik

@ -14,6 +14,7 @@ from wagtail.test.testapp.models import (
IconSiteSetting,
PanelSiteSettings,
TabbedSiteSettings,
TestPermissionedSiteSetting,
TestSiteSetting,
)
from wagtail.test.utils import WagtailTestUtils
@ -72,6 +73,11 @@ class BaseTestSiteSettingView(WagtailTestUtils, TestCase):
class TestSiteSettingCreateView(BaseTestSiteSettingView):
def setUp(self):
self.user = self.login()
self.user.user_permissions.add(
Permission.objects.get(
content_type__app_label="wagtailadmin", codename="access_admin"
)
)
def test_get_edit(self):
response = self.get()
@ -103,18 +109,55 @@ class TestSiteSettingCreateView(BaseTestSiteSettingView):
# Ensure the form supports file uploads
self.assertContains(response, 'enctype="multipart/form-data"')
def test_create_restricted_field_without_permission(self):
self.user.is_superuser = False
self.user.save()
self.assertFalse(TestPermissionedSiteSetting.objects.exists())
response = self.post(
post_data={"sensitive_email": "test@example.com", "title": "test"},
setting=TestPermissionedSiteSetting,
)
self.assertEqual(response.status_code, 302)
settings = TestPermissionedSiteSetting.objects.get()
self.assertEqual(settings.title, "test")
self.assertEqual(settings.sensitive_email, "")
def test_create_restricted_field(self):
self.user.is_superuser = False
self.user.save()
self.user.user_permissions.add(
Permission.objects.get(codename="can_edit_sensitive_email_site_setting")
)
self.assertFalse(TestPermissionedSiteSetting.objects.exists())
response = self.post(
post_data={"sensitive_email": "test@example.com", "title": "test"},
setting=TestPermissionedSiteSetting,
)
self.assertEqual(response.status_code, 302)
settings = TestPermissionedSiteSetting.objects.get()
self.assertEqual(settings.title, "test")
self.assertEqual(settings.sensitive_email, "test@example.com")
class TestSiteSettingEditView(BaseTestSiteSettingView):
def setUp(self):
default_site = Site.objects.get(is_default_site=True)
self.default_site = Site.objects.get(is_default_site=True)
self.test_setting = TestSiteSetting()
self.test_setting.title = "Site title"
self.test_setting.email = "initial@example.com"
self.test_setting.site = default_site
self.test_setting.site = self.default_site
self.test_setting.save()
self.login()
self.user = self.login()
self.user.user_permissions.add(
Permission.objects.get(
content_type__app_label="wagtailadmin", codename="access_admin"
)
)
def test_get_edit(self):
response = self.get()
@ -167,6 +210,52 @@ class TestSiteSettingEditView(BaseTestSiteSettingView):
response = self.client.get(url)
self.assertRedirects(response, status_code=302, expected_url="/admin/")
def test_edit_restricted_field(self):
test_setting = TestPermissionedSiteSetting()
test_setting.sensitive_email = "test@example.com"
test_setting.site = self.default_site
test_setting.save()
self.user.is_superuser = False
self.user.save()
self.user.user_permissions.add(
Permission.objects.get(codename="can_edit_sensitive_email_site_setting")
)
response = self.get(setting=TestPermissionedSiteSetting)
self.assertEqual(response.status_code, 200)
self.assertIn("sensitive_email", list(response.context["form"].fields))
response = self.post(
setting=TestPermissionedSiteSetting,
post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
)
self.assertEqual(response.status_code, 302)
test_setting.refresh_from_db()
self.assertEqual(test_setting.sensitive_email, "test-updated@example.com")
def test_edit_restricted_field_without_permission(self):
test_setting = TestPermissionedSiteSetting()
test_setting.sensitive_email = "test@example.com"
test_setting.site = self.default_site
test_setting.save()
self.user.is_superuser = False
self.user.save()
response = self.get(setting=TestPermissionedSiteSetting)
self.assertEqual(response.status_code, 200)
self.assertNotIn("sensitive_email", list(response.context["form"].fields))
response = self.post(
setting=TestPermissionedSiteSetting,
post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
)
self.assertEqual(response.status_code, 302)
test_setting.refresh_from_db()
self.assertEqual(test_setting.sensitive_email, "test@example.com")
@override_settings(
ALLOWED_HOSTS=["testserver", "example.com", "noneoftheabove.example.com"]

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

Wyświetl plik

@ -1,6 +1,7 @@
from django.test import TestCase
from django.urls import reverse
from wagtail.admin.staticfiles import versioned_static
from wagtail.test.utils import WagtailTestUtils
@ -13,3 +14,10 @@ class TestStyleGuide(WagtailTestUtils, TestCase):
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "wagtailstyleguide/base.html")
custom_css = versioned_static("wagtailstyleguide/css/animate-progress.css")
widget_css = versioned_static("wagtailadmin/css/panels/draftail.css")
widget_js = versioned_static("wagtailadmin/js/draftail.js")
self.assertContains(response, custom_css)
self.assertContains(response, widget_css)
self.assertContains(response, widget_js)

Wyświetl plik

@ -94,7 +94,7 @@ class ExampleForm(forms.Form):
@property
def media(self):
return forms.Media(
return super().media + forms.Media(
css={
"all": [versioned_static("wagtailstyleguide/css/animate-progress.css")]
}

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

Wyświetl plik

@ -6,7 +6,7 @@ from django.db import models
from django.db.models.fields.json import KeyTransform
from django.utils.encoding import force_str
from wagtail.blocks import Block, BlockField, StreamBlock, StreamValue
from wagtail.blocks import Block, BlockField, StreamBlock
from wagtail.rich_text import (
RichTextMaxLengthValidator,
extract_references_from_rich_text,
@ -88,7 +88,7 @@ class StreamField(models.Field):
# extract kwargs that are to be passed on to the block, not handled by super
block_opts = {}
for arg in ["min_num", "max_num", "block_counts", "collapsed"]:
for arg in ["min_num", "max_num", "block_counts", "collapsed", "value_class"]:
if arg in kwargs:
block_opts[arg] = kwargs.pop(arg)
@ -111,6 +111,10 @@ class StreamField(models.Field):
self.stream_block.set_meta_options(block_opts)
@property
def value_class(self):
return self.stream_block.meta.value_class
@property
def json_field(self):
return models.JSONField(encoder=DjangoJSONEncoder)
@ -142,7 +146,7 @@ class StreamField(models.Field):
def get_prep_value(self, value):
if (
isinstance(value, StreamValue)
isinstance(value, self.value_class)
and not (value)
and value.raw_text is not None
):
@ -151,7 +155,7 @@ class StreamField(models.Field):
# for reverse migrations that convert StreamField data back into plain text
# fields.)
return value.raw_text
elif isinstance(value, StreamValue):
elif isinstance(value, self.value_class):
# StreamValue instances must be prepared first.
return json.dumps(
self.stream_block.get_prep_value(value), cls=DjangoJSONEncoder
@ -163,7 +167,7 @@ class StreamField(models.Field):
return self.json_field.get_prep_value(value)
def get_db_prep_value(self, value, connection, prepared=False):
if not isinstance(value, StreamValue):
if not isinstance(value, self.value_class):
# When querying with JSONField features, the rhs might not be a StreamValue.
# As of Django 4.2, JSONField value serialisation is handled in
# get_db_prep_value instead of get_prep_value.

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -610,23 +610,23 @@ msgid_plural "%(num_parent_objects)d images have been deleted"
msgstr[0] ""
msgstr[1] ""
#: views/chooser.py:51 views/chooser.py:313
#: views/chooser.py:51 views/chooser.py:314
msgid "Upload"
msgstr ""
#: views/chooser.py:52 views/chooser.py:314
#: views/chooser.py:52 views/chooser.py:315
msgid "Uploading…"
msgstr ""
#: views/chooser.py:312 widgets.py:13
#: views/chooser.py:313 widgets.py:13
msgid "Choose an image"
msgstr ""
#: views/chooser.py:315
#: views/chooser.py:316
msgid "Choose another image"
msgstr ""
#: views/chooser.py:316 widgets.py:15
#: views/chooser.py:317 widgets.py:15
msgid "Edit this image"
msgstr ""

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -317,6 +317,23 @@ msgstr "Scegli un formato"
msgid "Insert image"
msgstr "Inserisci immagine"
#, python-format
msgid ""
"<span>%(total)s</span> Image <span class=\"w-sr-only\">created in "
"%(site_name)s</span>"
msgid_plural ""
"<span>%(total)s</span> Images <span class=\"w-sr-only\">created in "
"%(site_name)s</span>"
msgstr[0] ""
"<span>%(total)s</span> immagine <span class=\"w-sr-only\">creata in "
"%(site_name)s</span>"
msgstr[1] ""
"<span>%(total)s</span> immagini <span class=\"w-sr-only\">create in "
"%(site_name)s</span>"
msgstr[2] ""
"<span>%(total)s</span> immagini <span class=\"w-sr-only\">create in "
"%(site_name)s</span>"
msgid "Change image file:"
msgstr "Cambia immagine"

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -75,6 +75,9 @@ msgstr "Wagtail cache frontend"
msgid "Wagtail sitemaps"
msgstr "Sitemap Wagtail"
msgid "Unknown content type"
msgstr "Tipo contenuto sconosciuto"
msgid "Password"
msgstr "Password"
@ -87,6 +90,28 @@ msgstr "Commenta"
msgid "Locked"
msgstr "Bloccata"
#, python-format
msgid "No one can make changes while the %(model_name)s is locked"
msgstr "Nessuno può fare modifiche finché %(model_name)s è bloccato"
#, python-brace-format
msgid "<b>'{title}' was locked</b> by <b>you</b> on <b>{datetime}</b>."
msgstr "<b>'{title}' è stata bloccata</b> da <b>te</b> il <b>{datetime}</b>."
#, python-brace-format
msgid "<b>'{title}' is locked</b> by <b>you</b>."
msgstr "<b>'{title}' è bloccata</b> da <b>te</b>."
#, python-brace-format
msgid "<b>'{title}' is locked</b>."
msgstr "<b>'{title}' è bloccata</b>."
msgid "Locked by you"
msgstr "Bloccata da te"
msgid "Locked by another user"
msgstr "Bloccata da un altro utente"
msgid "Unlocked"
msgstr "Sbloccata"

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

Wyświetl plik

@ -0,0 +1,43 @@
from django.apps import apps
from django.core.management.base import BaseCommand
from django.db import connection, models
from wagtail.models import (
BaseLogEntry,
BootstrapTranslatableMixin,
ReferenceIndex,
TranslatableMixin,
)
class Command(BaseCommand):
help = "Converts UUID columns from char type to the native UUID type used in MariaDB 10.7+ and Django 5.0+."
def convert_field(self, model, field_name, null=False):
if model._meta.get_field(field_name).model != model:
# Field is inherited from a parent model
return
if not model._meta.managed:
# The migration framework skips unmanaged models, so we should too
return
old_field = models.CharField(null=null, max_length=36)
old_field.set_attributes_from_name(field_name)
new_field = models.UUIDField(null=null)
new_field.set_attributes_from_name(field_name)
with connection.schema_editor() as schema_editor:
schema_editor.alter_field(model, old_field, new_field)
def handle(self, **options):
self.convert_field(ReferenceIndex, "content_path_hash")
for model in apps.get_models():
if issubclass(model, BaseLogEntry):
self.convert_field(model, "uuid", null=True)
elif issubclass(model, BootstrapTranslatableMixin):
self.convert_field(model, "translation_key", null=True)
elif issubclass(model, TranslatableMixin):
self.convert_field(model, "translation_key")

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -182,7 +182,7 @@ msgstr ""
msgid "Choose"
msgstr ""
#: views/snippets.py:77 views/snippets.py:104 views/snippets.py:913
#: views/snippets.py:77 views/snippets.py:104 views/snippets.py:894
#: wagtail_hooks.py:44
msgid "Snippets"
msgstr ""
@ -200,17 +200,17 @@ msgstr ""
msgid "More options for '%(title)s'"
msgstr ""
#: views/snippets.py:389
#: views/snippets.py:370
#, python-format
msgid "Edit this %(model_name)s"
msgstr ""
#: views/snippets.py:395
#: views/snippets.py:376
#, python-format
msgid "%(model_name)s history"
msgstr ""
#: views/snippets.py:912
#: views/snippets.py:893
msgid "Home"
msgstr ""

Wyświetl plik

@ -233,22 +233,6 @@ class CreateView(generic.CreateEditViewOptionalFeaturesMixin, generic.CreateView
def _get_action_menu(self):
return SnippetActionMenu(self.request, view=self.view_name, model=self.model)
def _get_initial_form_instance(self):
instance = self.model()
# Set locale of the new instance
if self.locale:
instance.locale = self.locale
return instance
def get_form_kwargs(self):
return {
**super().get_form_kwargs(),
"instance": self._get_initial_form_instance(),
"for_user": self.request.user,
}
def get_side_panels(self):
side_panels = [
SnippetStatusSidePanel(
@ -308,9 +292,6 @@ class EditView(generic.CreateEditViewOptionalFeaturesMixin, generic.EditView):
locked_for_user=self.locked_for_user,
)
def get_form_kwargs(self):
return {**super().get_form_kwargs(), "for_user": self.request.user}
def get_side_panels(self):
side_panels = [
SnippetStatusSidePanel(

Wyświetl plik

@ -3,6 +3,7 @@ from django.shortcuts import resolve_url
from django.template.defaulttags import token_kwargs
from django.template.loader import render_to_string
from django.utils.encoding import force_str
from django.utils.functional import Promise
from django.utils.html import conditional_escape
from wagtail import VERSION, __version__
@ -120,6 +121,8 @@ def richtext(value):
elif value is None:
html = ""
else:
if isinstance(value, Promise):
value = str(value)
if isinstance(value, str):
html = expand_db_html(value)
else:

Wyświetl plik

@ -0,0 +1,24 @@
# Generated by Django 4.2.11 on 2024-04-22 08:19
from django.db import migrations, models
import wagtail.blocks
import wagtail.fields
import wagtail.images.blocks
class Migration(migrations.Migration):
dependencies = [
('tests', '0036_complexdefaultstreampage'),
]
operations = [
migrations.CreateModel(
name='JSONCustomValueStreamModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('primary_content', wagtail.fields.StreamField([('text', wagtail.blocks.CharBlock()), ('rich_text', wagtail.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock())])),
('secondary_content', wagtail.fields.StreamField([('text', wagtail.blocks.CharBlock()), ('rich_text', wagtail.blocks.RichTextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock())])),
],
),
]

Wyświetl plik

@ -0,0 +1,42 @@
# Generated by Django 4.2.11 on 2024-04-25 15:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0093_uploadedfile'),
('tests', '0036_complexdefaultstreampage'),
]
operations = [
migrations.CreateModel(
name='TestPermissionedGenericSetting',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100)),
('sensitive_email', models.EmailField(max_length=50)),
],
options={
'permissions': [('can_edit_sensitive_email_generic_setting', 'Can edit sensitive email generic setting.')],
},
),
migrations.AlterModelOptions(
name='featurecompletetoy',
options={'permissions': [('can_set_release_date', 'Can set release date')]},
),
migrations.CreateModel(
name='TestPermissionedSiteSetting',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100)),
('sensitive_email', models.EmailField(max_length=50)),
('site', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='wagtailcore.site')),
],
options={
'permissions': [('can_edit_sensitive_email_site_setting', 'Can edit sensitive email site setting.')],
},
),
]

Wyświetl plik

@ -42,6 +42,7 @@ from wagtail.blocks import (
RawHTMLBlock,
RichTextBlock,
StreamBlock,
StreamValue,
StructBlock,
)
from wagtail.contrib.forms.forms import FormBuilder
@ -1552,6 +1553,40 @@ class JSONBlockCountsStreamModel(models.Model):
)
class CustomStreamValue(StreamValue):
"""
Used by ``CustomStreamBlock`` and ``JSONCustomValueStreamModel.primary_content`` (below)
to demonstrate support for custom value classes with ``StreamField`` and ``StreamBlock``.
"""
def level_of_customness(self) -> bool:
return "medium"
class CustomValueStreamBlock(StreamBlock):
text = CharBlock()
rich_text = RichTextBlock()
image = ImageChooserBlock()
class Meta:
value_class = CustomStreamValue
class JSONCustomValueStreamModel(models.Model):
# `value_class` can be provided as an init kwarg to StreamField
primary_content = StreamField(
[
("text", CharBlock()),
("rich_text", RichTextBlock()),
("image", ImageChooserBlock()),
],
value_class=CustomStreamValue,
)
# `value_class` can be customised by overriding in StreamBlock.Meta
secondary_content = StreamField(CustomValueStreamBlock())
class ExtendedImageChooserBlock(ImageChooserBlock):
"""
Example of Block with custom get_api_representation method.
@ -1685,6 +1720,49 @@ class TestGenericSetting(BaseGenericSetting):
email = models.EmailField(max_length=50)
@register_setting
class TestPermissionedGenericSetting(BaseGenericSetting):
title = models.CharField(max_length=100)
sensitive_email = models.EmailField(max_length=50)
panels = [
FieldPanel("title"),
FieldPanel(
"sensitive_email",
permission="tests.can_edit_sensitive_email_generic_setting",
),
]
class Meta:
permissions = [
(
"can_edit_sensitive_email_generic_setting",
"Can edit sensitive email generic setting.",
),
]
@register_setting
class TestPermissionedSiteSetting(BaseSiteSetting):
title = models.CharField(max_length=100)
sensitive_email = models.EmailField(max_length=50)
panels = [
FieldPanel("title"),
FieldPanel(
"sensitive_email", permission="tests.can_edit_sensitive_email_site_setting"
),
]
class Meta:
permissions = [
(
"can_edit_sensitive_email_site_setting",
"Can edit sensitive email site setting.",
),
]
@register_setting
class ImportantPagesSiteSetting(BaseSiteSetting):
sign_up_page = models.ForeignKey(
@ -2274,6 +2352,9 @@ class FeatureCompleteToy(index.Indexed, models.Model):
def __str__(self):
return f"{self.name} ({self.release_date})"
class Meta:
permissions = [("can_set_release_date", "Can set release date")]
class PurgeRevisionsProtectedTestModel(models.Model):
revision = models.OneToOneField(

Wyświetl plik

@ -229,7 +229,7 @@ class FeatureCompleteToyViewSet(ModelViewSet):
panels = [
FieldPanel("name"),
FieldPanel("release_date"),
FieldPanel("release_date", permission="tests.can_set_release_date"),
]

Wyświetl plik

@ -20,7 +20,11 @@ from wagtail.blocks.base import get_error_json_data
from wagtail.blocks.field_block import FieldBlockAdapter
from wagtail.blocks.list_block import ListBlockAdapter, ListBlockValidationError
from wagtail.blocks.static_block import StaticBlockAdapter
from wagtail.blocks.stream_block import StreamBlockAdapter, StreamBlockValidationError
from wagtail.blocks.stream_block import (
StreamBlockAdapter,
StreamBlockValidationError,
StreamValue,
)
from wagtail.blocks.struct_block import StructBlockAdapter, StructBlockValidationError
from wagtail.models import Page
from wagtail.rich_text import RichText
@ -3174,6 +3178,7 @@ class TestStreamBlock(WagtailTestUtils, SimpleTestCase):
)
self.assertEqual(list(block.child_blocks.keys()), ["heading", "paragraph"])
self.assertIs(block.value_class, StreamValue)
def test_initialisation_with_binary_string_names(self):
# migrations will sometimes write out names as binary strings, just to keep us on our toes
@ -3186,6 +3191,20 @@ class TestStreamBlock(WagtailTestUtils, SimpleTestCase):
self.assertEqual(list(block.child_blocks.keys()), [b"heading", b"paragraph"])
def test_initialisation_with_custom_value_class(self):
class CustomStreamValue(StreamValue):
pass
block = blocks.StreamBlock(
[
("heading", blocks.CharBlock()),
("paragraph", blocks.CharBlock()),
],
value_class=CustomStreamValue,
)
self.assertIs(block.value_class, CustomStreamValue)
def test_initialisation_from_subclass(self):
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()

Wyświetl plik

@ -17,7 +17,9 @@ from wagtail.rich_text import RichText
from wagtail.signal_handlers import disable_reference_index_auto_update
from wagtail.test.testapp.models import (
ComplexDefaultStreamPage,
CustomStreamValue,
JSONBlockCountsStreamModel,
JSONCustomValueStreamModel,
JSONMinMaxCountStreamModel,
JSONStreamModel,
StreamPage,
@ -225,6 +227,38 @@ class TestStreamValueAccess(TestCase):
self.assertIsInstance(fetched_body[0].value, RichText)
self.assertEqual(fetched_body[0].value.source, "<h2>hello world</h2>")
def test_custom_value_class(self):
original_content = json.dumps([{"type": "text", "value": "foo"}])
obj = JSONCustomValueStreamModel.objects.create(
primary_content=original_content,
secondary_content=original_content,
)
# Both fields should return instances of CustomStreamValue
self.assertIsInstance(obj.primary_content, CustomStreamValue)
self.assertIsInstance(obj.secondary_content, CustomStreamValue)
# It should still be possible to update the fields using a raw dict value
new_content = [("rich_text", RichText("<h2>hello world</h2>"))]
obj.primary_content = new_content
obj.secondary_content = new_content
obj.save()
obj.refresh_from_db("primary_content", "secondary_content")
# CustomStreamValue is functionally equivalent to StreamValue, so the same value
# transformation should have taken place
for streamfield in ("primary_content", "secondary_content"):
with self.subTest(streamfield):
field_value = getattr(self, streamfield)
self.assertEqual(len(field_value), 1)
self.assertIsInstance(field_value[0].value, RichText)
self.assertEqual(field_value[0].value.source, "<h2>hello world</h2>")
# The value is still an instance of the custom value class
self.assertIsInstance(field_value, CustomStreamValue)
# So, we can do this...
self.assertEqual(field_value.level_of_customness(), "medium")
def test_can_append(self):
self.json_body.body.append(("text", "bar"))
self.json_body.save()

Wyświetl plik

@ -9,6 +9,7 @@ from django.test import TestCase
from django.test.utils import override_settings
from django.urls.exceptions import NoReverseMatch
from django.utils.safestring import SafeString
from django.utils.translation import gettext_lazy
from wagtail.coreutils import (
get_dummy_request,
@ -536,6 +537,10 @@ class TestRichtextTag(TestCase):
self.assertEqual(result, "Hello world!")
self.assertIsInstance(result, SafeString)
def test_call_with_lazy(self):
result = richtext(gettext_lazy("test"))
self.assertEqual(result, "test")
def test_call_with_none(self):
result = richtext(None)
self.assertEqual(result, "")

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"POT-Creation-Date: 2024-05-02 10:04+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -160,15 +160,16 @@ msgstr ""
msgid "Snug"
msgstr ""
#: models.py:92
#. Translators: "Density" is the term used to describe the amount of space between elements in the user interface
#: models.py:93
msgid "density"
msgstr ""
#: models.py:112
#: models.py:113
msgid "user profile"
msgstr ""
#: models.py:113
#: models.py:114
msgid "user profiles"
msgstr ""

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -6,7 +6,7 @@
# Giacomo Ghizzani <giacomo.ghz@gmail.com>, 2015-2018
# giammi <gian-maria.daffre@giammi.org>, 2018
# giammi <gian-maria.daffre@giammi.org>, 2018
# Marco Badan <marco.badan@gmail.com>, 2021-2023
# Marco Badan <marco.badan@gmail.com>, 2021-2024
# Sandro Badalamenti <sandro@mailinator.com>, 2019
msgid ""
msgstr ""
@ -14,7 +14,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-18 17:28+0100\n"
"PO-Revision-Date: 2014-02-19 11:54+0000\n"
"Last-Translator: Marco Badan <marco.badan@gmail.com>, 2021-2023\n"
"Last-Translator: Marco Badan <marco.badan@gmail.com>, 2021-2024\n"
"Language-Team: Italian (http://app.transifex.com/torchbox/wagtail/language/"
"it/)\n"
"MIME-Version: 1.0\n"
@ -132,6 +132,12 @@ msgstr "tema admin"
msgid "Default"
msgstr "Default"
msgid "Snug"
msgstr "Compatta"
msgid "density"
msgstr "densità"
msgid "user profile"
msgstr "profilo utente"
@ -265,6 +271,30 @@ msgstr "Sblocca"
msgid "Custom permissions"
msgstr "Permessi personalizzati"
msgid "Toggle all"
msgstr "Attiva/disattiva tutto"
msgid "Toggle all add permissions"
msgstr "Attiva/disattiva tutti i permessi di aggiunta"
msgid "Toggle all change permissions"
msgstr "Attiva/disattiva tutti i permessi di modifica"
msgid "Toggle all delete permissions"
msgstr "Attiva/disattiva tutti i permessi di eliminazione"
msgid "Toggle all publish permissions"
msgstr "Attiva/disattiva tutti i permessi di pubblicazione"
msgid "Toggle all lock permissions"
msgstr "Attiva/disattiva tutti i permessi di blocco"
msgid "Toggle all unlock permissions"
msgstr "Attiva/disattiva tutti i permessi di sblocco"
msgid "Toggle all custom permissions"
msgstr "Attiva/disattiva tutti i permessi personalizzati"
msgid "Other permissions"
msgstr "Altri permessi"
@ -307,6 +337,9 @@ msgstr "Elimina utente"
msgid "Select all users in listing"
msgstr "Seleziona tutti gli utenti nella lista"
msgid "Sorry, no users match your query"
msgstr "Nessun utente corrisponde alla tua ricerca"
#, python-format
msgid ""
"There are no users configured. Why not <a "
@ -395,6 +428,9 @@ msgstr "Visualizza utenti in questo gruppo"
msgid "Group '%(object)s' deleted."
msgstr "Gruppo '%(object)s' eliminato."
msgid "Last login"
msgstr "Ultimo accesso"
msgid "Group"
msgstr "Gruppo"

Wyświetl plik

@ -89,6 +89,7 @@ class UserProfile(models.Model):
SNUG = "snug", _("Snug")
density = models.CharField(
# Translators: "Density" is the term used to describe the amount of space between elements in the user interface
verbose_name=_("density"),
choices=AdminDensityThemes.choices,
default=AdminDensityThemes.DEFAULT,