kopia lustrzana https://github.com/wagtail/wagtail
Porównaj commity
38 Commity
a27e8f075d
...
fa82c16a15
Autor | SHA1 | Data |
---|---|---|
Andy Babic | fa82c16a15 | |
Matt Westcott | 196cb02d10 | |
Matt Westcott | 1874350fbf | |
Storm Heg | 2031eb3d24 | |
Matt Westcott | 4ac42a723f | |
Matt Westcott | ef57f9b2c9 | |
Matt Westcott | 955703fba6 | |
Matt Westcott | 42b7c9bcde | |
Matt Westcott | 9d24ac4e39 | |
Jake Howard | ee57f6d4dc | |
Matt Westcott | 932402fd28 | |
Matt Westcott | 7de6872277 | |
Matt Westcott | 95d23fdf7d | |
Matt Westcott | 72edc09851 | |
Matt Westcott | fac768c076 | |
Matt Westcott | 81a11d63c6 | |
Matt Westcott | 617e5129c5 | |
Thibaud Colas | cae0002afe | |
Sage Abdullah | 08ee15a358 | |
Sage Abdullah | b8dd7f484f | |
Sage Abdullah | 56e69bc3ea | |
Jake Howard | afbafd657d | |
Benjamin Bach | b266e54ba9 | |
Matt Westcott | 763c990490 | |
Matt Westcott | 207d5dafd5 | |
Sage Abdullah | c3a52a6fdb | |
Sage Abdullah | 4302bed1b1 | |
Sage Abdullah | ae28020195 | |
rohitsrma | 6f28aa9d8b | |
rohitsrma | 3d63d0da4f | |
Andy Babic | 020c3a5e0d | |
Andy Babic | bb28daf65a | |
Andy Babic | c75b7f9404 | |
Andy Babic | b2c79f21b6 | |
Andy Babic | 0c9bb707cb | |
Andy Babic | 91930e1152 | |
Andy Babic | 4cf2b8fd64 | |
Andy Babic | bda27a5691 |
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/).
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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" %}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
@ -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"
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")]
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
@ -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"
|
||||
|
||||
|
|
|
@ -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.
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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())])),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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.')],
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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(
|
||||
|
|
|
@ -229,7 +229,7 @@ class FeatureCompleteToyViewSet(ModelViewSet):
|
|||
|
||||
panels = [
|
||||
FieldPanel("name"),
|
||||
FieldPanel("release_date"),
|
||||
FieldPanel("release_date", permission="tests.can_set_release_date"),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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, "")
|
||||
|
|
|
@ -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.
|
@ -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"
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Ładowanie…
Reference in New Issue