kopia lustrzana https://github.com/wagtail/wagtail
Merge branch 'main' into fix/added-False-in-save
commit
6e5450df6e
|
@ -9,12 +9,15 @@ Changelog
|
|||
* 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 and collections (documents) feature (Salvo Polizzi, Jake Howard)
|
||||
* Add system checks to ensure that `WAGTAIL_DATE_FORMAT`, `WAGTAIL_DATETIME_FORMAT`, `WAGTAIL_TIME_FORMAT` are correctly configured (Rohit Sharma, Coen van der Kamp)
|
||||
* 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)
|
||||
* Fix: Ensure re-ordering buttons work correctly when using a nested InlinePanel (Adrien Hamraoui)
|
||||
* Fix: Resolve inconsistent use of model `verbose_name` in group edit view when listing custom permissions (Neeraj Yetheendran, Omkar Jadhav)
|
||||
* 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 multiple pages in the reference sections and multiple page names to their US spelling instead of UK spelling (Victoria Poromon)
|
||||
* Docs: Update content and page names to their US spelling instead of UK spelling (Victoria Poromon)
|
||||
* Maintenance: Move RichText HTML whitelist parser to use the faster, built in `html.parser` (Jake Howard)
|
||||
* Maintenance: Remove duplicate 'path' in default_exclude_fields_in_copy (Ramchandra Shahi Thakuri)
|
||||
* Maintenance: Update unit tests to always use the faster, built in `html.parser` & remove `html5lib` dependency (Jake Howard)
|
||||
|
@ -22,6 +25,7 @@ Changelog
|
|||
* Maintenance: Rename the React `Button` that only renders links (a element) to `Link` and remove unused prop & behavior that was non-compliant for aria role usage (Advik Kabra)
|
||||
* Maintenance: Set up an `wagtail.models.AbstractWorkflow` model to support future customisations around workflows (Hossein)
|
||||
* Maintenance: Improve `classnames` template tag to handle nested lists of strings, use template tag for admin `body` element (LB (Ben) Johnston)
|
||||
* Maintenance: Merge `UploadedDocument` and `UploadedImage` into new `UploadedFile` model for easier shared code usage (Advik Kabra, Karl Hobley)
|
||||
|
||||
|
||||
6.0 (07.02.2024)
|
||||
|
|
|
@ -798,6 +798,7 @@
|
|||
* Hossein
|
||||
* Andre Delorme
|
||||
* arshyia3000
|
||||
* Adrien Hamraoui
|
||||
|
||||
## Translators
|
||||
|
||||
|
|
|
@ -52,8 +52,10 @@ export class InlinePanel extends ExpandingFormset {
|
|||
const childId = 'inline_child_' + prefix;
|
||||
const deleteInputId = 'id_' + prefix + '-DELETE';
|
||||
const currentChild = $('#' + childId);
|
||||
const $up = currentChild.find('[data-inline-panel-child-move-up]');
|
||||
const $down = currentChild.find('[data-inline-panel-child-move-down]');
|
||||
const $up = currentChild.find('[data-inline-panel-child-move-up]:first ');
|
||||
const $down = currentChild.find(
|
||||
'[data-inline-panel-child-move-down]:first ',
|
||||
);
|
||||
|
||||
$('#' + deleteInputId + '-button').on('click', () => {
|
||||
/* set 'deleted' form field to true */
|
||||
|
@ -148,8 +150,14 @@ export class InlinePanel extends ExpandingFormset {
|
|||
forms.each(function updateButtonStates(i) {
|
||||
const isFirst = i === 0;
|
||||
const isLast = i === forms.length - 1;
|
||||
$('[data-inline-panel-child-move-up]', this).prop('disabled', isFirst);
|
||||
$('[data-inline-panel-child-move-down]', this).prop('disabled', isLast);
|
||||
$('[data-inline-panel-child-move-up]:first', this).prop(
|
||||
'disabled',
|
||||
isFirst,
|
||||
);
|
||||
$('[data-inline-panel-child-move-down]:first', this).prop(
|
||||
'disabled',
|
||||
isLast,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,8 +74,8 @@ To easily identify the most relevant changes to users, items are grouped togethe
|
|||
|
||||
- Major features (no prefix) - things that will inspire users to upgrade to a new release
|
||||
- Minor enhancements (no prefix) - other improvements to the developer or end user experience
|
||||
- Bug fixes (prefixed with "Fix:") - things that address broken behaviour from previous releases
|
||||
- Documentation (prefixed with "Docs:") - changes to documentation that do not accompany a specific code change; reorganisations, tutorials, recipes and so on
|
||||
- Bug fixes (prefixed with "Fix:") - things that address broken behavior from previous releases
|
||||
- Documentation (prefixed with "Docs:") - changes to documentation that do not accompany a specific code change; reorganizations, tutorials, recipes and so on
|
||||
- Maintenance (prefixed with "Maintenance:") - cleanup, refactoring and other changes to code or tooling that are not intended to have a visible effect to developers or end users
|
||||
|
||||
The name of the contributor should be added at the end of the summary, in brackets.
|
||||
|
@ -131,7 +131,7 @@ git branch -d pr/xxxx
|
|||
|
||||
## When you have made a mistake
|
||||
|
||||
It's ok! Everyone makes mistakes. If you realise that recently merged changes
|
||||
It's ok! Everyone makes mistakes. If you realize that recently merged changes
|
||||
have a negative impact, create a new pull request with a revert of the changes
|
||||
and merge it without waiting for a review. The PR will serve as additional
|
||||
documentation for the changes and will run through the CI tests.
|
||||
|
|
|
@ -237,7 +237,7 @@ We want to make Wagtail accessible for users of a wide variety of assistive tech
|
|||
- Mobile [VoiceOver](https://support.apple.com/en-gb/guide/voiceover-guide/welcome/web) on iOS, or [TalkBack](https://support.google.com/accessibility/android/answer/6283677?hl=en-GB) on Android
|
||||
- Windows [High-contrast mode](https://support.microsoft.com/en-us/windows/use-high-contrast-mode-in-windows-10-fedc744c-90ac-69df-aed5-c8a90125e696)
|
||||
|
||||
We aim for Wagtail to work in those environments. Our development standards ensure that the site is usable with other assistive technologies. In practice, testing with assistive technology can be a daunting task that requires specialised training – here are tools we rely on to help identify accessibility issues, to use during development and code reviews:
|
||||
We aim for Wagtail to work in those environments. Our development standards ensure that the site is usable with other assistive technologies. In practice, testing with assistive technology can be a daunting task that requires specialized training – here are tools we rely on to help identify accessibility issues, to use during development and code reviews:
|
||||
|
||||
- [@wordpress/jest-puppeteer-axe](https://github.com/WordPress/gutenberg/tree/trunk/packages/jest-puppeteer-axe) running Axe checks as part of integration tests.
|
||||
- [Axe](https://chrome.google.com/webstore/detail/axe/lhdoppojpmngadmnindnejefpokejbdd) Chrome extension for more comprehensive automated tests of a given page.
|
||||
|
|
|
@ -422,11 +422,11 @@ Avoid documentation source comments in committed documentation.
|
|||
|
||||
### Figure
|
||||
|
||||
reStructuredText figures (`.. figure::`) only offer very marginal improvements over vanilla images. If your figure has a caption, add it as an italicised paragraph underneath the image.
|
||||
reStructuredText figures (`.. figure::`) only offer very marginal improvements over vanilla images. If your figure has a caption, add it as an italicized paragraph underneath the image.
|
||||
|
||||
### Other reStructuredText syntax and Sphinx directives
|
||||
|
||||
We generally want to favour Markdown over reStructuredText, to make it as simple as possible for newcomers to make documentation contributions to Wagtail. Always prefer Markdown, unless the document’s formatting highly depends on reStructuredText syntax.
|
||||
We generally want to favor Markdown over reStructuredText, to make it as simple as possible for newcomers to make documentation contributions to Wagtail. Always prefer Markdown, unless the document’s formatting highly depends on reStructuredText syntax.
|
||||
|
||||
If you want to use a specific Sphinx directive, consult with core contributors to see whether its usage is justified, and document its expected usage on this page.
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ You may also want to join StackOverflow and [follow the Wagtail tag](https://sta
|
|||
|
||||
### 3. Before contributing code
|
||||
|
||||
Firstly, it is important to be able to understand how to **build with** Wagtail before you can understand how to contribute **to** Wagtail. Take the time to do the full [Wagtail getting started tutorial](../getting_started/index) without focusing yet on how to contribute code but instead how to use Wagtail to build your own basic demo website. This will require you to have Python and other dependencies installed on your machine and may not be easy the first time, but keep at it and ask questions if you get stuck.
|
||||
Firstly, it is important to be able to understand how to **build with** Wagtail before you can understand how to contribute **to** Wagtail. Take the time to do the full [Wagtail getting started tutorial](../getting_started/index) without focusing yet on how to contribute code but instead on how to use Wagtail to build your own basic demo website. This will require you to have Python and other dependencies installed on your machine and may not be easy the first time, but keep at it and ask questions if you get stuck.
|
||||
|
||||
Remember that there are many other ways to contribute, such as answering questions in StackOverflow or `#support`, contributing to one of the [other packages](https://github.com/wagtail/) or even the [Wagtail user guide](https://guide.wagtail.org/en-latest/contributing/). Sometimes, it's best to get started with a non-code contribution to get a feel for Wagtail's code or the CMS interface.
|
||||
|
||||
|
@ -80,7 +80,7 @@ Take the time to **read** the issue and links before adding new comments or ques
|
|||
|
||||
```
|
||||
- [ ] Do the Wagtail tutorial
|
||||
- [ ] Look at the Wagtail organisation on GitHub, take note of any interesting projects
|
||||
- [ ] Look at the Wagtail organization on GitHub, take note of any interesting projects
|
||||
- [ ] Read through the Issue Tracking section in the docs
|
||||
- [ ] Give a go at a non-code contribution
|
||||
```
|
||||
|
@ -268,7 +268,7 @@ You can add a comment if you want to the pull request that you have updated, but
|
|||
|
||||
### 8. Next steps
|
||||
|
||||
When you take time to contribute out of your own personal time, or even that from your paid employer, it can be very frustrating when a pull request does not get reviewed. It is best to temper your expectations with this process and remember that many people on the other side of this are also volunteers or have limited time to prioritise.
|
||||
When you take time to contribute out of your own personal time, or even that from your paid employer, it can be very frustrating when a pull request does not get reviewed. It is best to temper your expectations with this process and remember that many people on the other side of this are also volunteers or have limited time to prioritize.
|
||||
|
||||
It is best to celebrate your accomplishment at this point even if your pull request never gets merged. It's good to balance that with an eagerness about getting your amazing fix in place to help people who use the project. Balancing this tension is hard, but the unhelpful thing to do is give up and never contribute or decide that you won’t respond to feedback because it came too late.
|
||||
|
||||
|
@ -321,7 +321,7 @@ Unless you are updating the documentation or only making visual style changes, y
|
|||
|
||||
If you are new to writing tests in Django, start by reading the [Django documentation on testing](inv:django#topics/testing/overview). Re-read the [Wagtail documentation notes on testing](testing) and have a look at [existing tests](https://cs.github.com/?scopeName=All+repos&scope=&q=repo%3Awagtail%2Fwagtail+path%3A**%2Ftests%2F**).
|
||||
|
||||
Note that the JavaScript testing is not as robust as the Python testing, if possible at least attempt to add some basic JS tests to new behaviour.
|
||||
Note that the JavaScript testing is not as robust as the Python testing, if possible at least attempt to add some basic JS tests to new behavior.
|
||||
|
||||
### Where can I get help with my pull request (PR)?
|
||||
|
||||
|
@ -344,7 +344,7 @@ Here are some links for using Gitpod with the Wagtail packages:
|
|||
|
||||
### How can I be assigned an issue to contribute to
|
||||
|
||||
We only use GitHub's issue assignment feature to members of the Wagtail core team for when tasks are being planned as part of core roadmap features or when being used for a specific internship program. If an issue is not assigned to anyone, feel free to work on it, there is no need to ask to be assigned the issue.
|
||||
We only use GitHub's issue assignment feature for members of the Wagtail core team when tasks are being planned as part of core roadmap features or when being used for a specific internship program. If an issue is not assigned to anyone, feel free to work on it, there is no need to ask to be assigned the issue.
|
||||
|
||||
Instead, review the issue, understand it and if you feel you can contribute you can just raise a Pull Request, or add a comment that you are taking a look at this. There are no strict claiming or reserving rules in place, anyone is free to work on any issue, but try to avoid double effort if someone has already got a Pull Request underway.
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ Thank you for your interest in improving Wagtail!
|
|||
|
||||
## Issues
|
||||
|
||||
The easiest way to contribute to Wagtail is to tell us how to improve it! First, check to see if your bug or feature request has already been submitted at [github.com/wagtail/wagtail/issues](https://github.com/wagtail/wagtail/issues). If it has, and you have some supporting information which may help us deal with it, comment on the existing issue. If not, please [create a new one](https://github.com/wagtail/wagtail/issues/new), providing as much relevant context as possible. For example, if you're experiencing problems with installation, detail your environment and the steps you've already taken. If something isn't displaying correctly, tell us what browser you're using, and include a screenshot if possible.
|
||||
The easiest way to contribute to Wagtail is to tell us how to improve it! First, check to see if your bug or feature request has already been submitted at [github.com/wagtail/wagtail/issues](https://github.com/wagtail/wagtail/issues). If it has, and you have some supporting information that may help us deal with it, comment on the existing issue. If not, please [create a new one](https://github.com/wagtail/wagtail/issues/new), providing as much relevant context as possible. For example, if you're experiencing problems with installation, detail your environment and the steps you've already taken. If something isn't displaying correctly, tell us what browser you're using, and include a screenshot if possible.
|
||||
|
||||
If your bug report is a security issue, **do not** report it with an issue. Please read our guide to [reporting security issues](security).
|
||||
|
||||
|
@ -24,7 +24,7 @@ issue_tracking
|
|||
|
||||
If you are just getting started with development and have never contributed to an open-source project, we recommend you read the [Your first contribution guide](first_contribution_guide). If you're a confident Python or Django developer, [fork it](https://github.com/wagtail/wagtail/) and read the [developing docs](developing_for_wagtail) to get stuck in!
|
||||
|
||||
We welcome all contributions, whether they solve problems which are specific to you or they address existing issues. If you're stuck for ideas, pick something from the [issue list](https://github.com/wagtail/wagtail/issues?q=is%3Aopen), or email us directly on [hello@wagtail.org](mailto:hello@wagtail.org) if you'd like us to suggest something!
|
||||
We welcome all contributions, whether they solve problems that are specific to you or they address existing issues. If you're stuck for ideas, pick something from the [issue list](https://github.com/wagtail/wagtail/issues?q=is%3Aopen), or email us directly at [hello@wagtail.org](mailto:hello@wagtail.org) if you'd like us to suggest something!
|
||||
|
||||
For large-scale changes, we'd generally recommend breaking them down into smaller pull requests that achieve a single well-defined task and can be reviewed individually. If this isn't possible, we recommend opening a pull request on the [Wagtail RFCs](https://github.com/wagtail/rfcs/) repository, so that there's a chance for the community to discuss the change before it gets implemented.
|
||||
|
||||
|
@ -37,7 +37,7 @@ developing
|
|||
|
||||
## Translations
|
||||
|
||||
Wagtail has internationalisation support so if you are fluent in a non-English language you can contribute by localising the interface.
|
||||
Wagtail has internationalization support so if you are fluent in a non-English language you can contribute by localizing the interface.
|
||||
|
||||
Translation work should be submitted through [Transifex](https://explore.transifex.com/torchbox/wagtail/), for information on how to get started see [](contributing_translations).
|
||||
|
||||
|
@ -48,8 +48,8 @@ Translation work should be submitted through [Transifex](https://explore.transif
|
|||
We welcome contributions to all aspects of Wagtail. If you would like to improve the design of the user interface, or extend the documentation, please submit a pull request as above. Here are some other ways to contribute if you are getting started or have been using Wagtail for a long time but are unable to contribute code.
|
||||
|
||||
- Contribute to one of the other [core Wagtail projects](https://github.com/orgs/wagtail/repositories) in GitHub.
|
||||
- Contribute to one of the community maintained packages on [Wagtail Nest](https://github.com/wagtail-nest/).
|
||||
- Contribute user facing documentation (including translations) on the [Wagtail guide](https://guide.wagtail.org/en-latest/contributing/).
|
||||
- Contribute to one of the community-maintained packages on [Wagtail Nest](https://github.com/wagtail-nest/).
|
||||
- Contribute user-facing documentation (including translations) on the [Wagtail guide](https://guide.wagtail.org/en-latest/contributing/).
|
||||
|
||||
### Non-code contributions
|
||||
|
||||
|
|
|
@ -10,12 +10,12 @@ Do not use issues for support queries or other questions ("How do I do X?" - alt
|
|||
|
||||
As soon as a ticket is opened - ideally within one day - a member of the core team will give it an initial classification, by either closing it due to it being invalid or updating it with the relevant labels. When a bug is opened, it will automatically be assigned the [`type:Bug`](https://github.com/wagtail/wagtail/labels/type%3ABug) and [`status:Unconfirmed`](https://github.com/wagtail/wagtail/labels/status%3AUnconfirmed) labels, once confirmed the bug can have the unconfirmed status removed. A member of the team will potentially also add a release milestone to help guide the priority of this issue. Anyone is invited to help Wagtail with reproducing `status:Unconfirmed` bugs and commenting if it is a valid bug or not with additional steps to reproduce if needed.
|
||||
|
||||
Don't be discouraged if you feel that your ticket has been given a lower priority than it deserves - this decision isn't permanent. We will consider all feedback, and reassign or reopen tickets where appropriate. (From the other side, this means that the core team member doing the classification should feel free to make bold unilateral decisions - there's no need to seek consensus first. If they make the wrong judgement call, that can always be reversed later.)
|
||||
Don't be discouraged if you feel that your ticket has been given a lower priority than it deserves - this decision isn't permanent. We will consider all feedback, and reassign or reopen tickets where appropriate. (From the other side, this means that the core team member doing the classification should feel free to make bold unilateral decisions - there's no need to seek consensus first. If they make the wrong judgment call, that can always be reversed later.)
|
||||
|
||||
The possible milestones that it might be assigned to are as follows:
|
||||
|
||||
- **invalid** (closed): this issue doesn't identify a specific action to be taken, or the action is not one that we want to take. For example - a bug report for something that's working as designed, or a feature request for something that's actively harmful.
|
||||
- **real-soon-now**: no-one on the core team has resources allocated to work on this right now, but we know it's a pain point, and it will be prioritised whenever we next get a chance to choose something new to work on. In practice, that kind of free choice doesn't happen very often - there are lots of pressures determining what we work on from day to day - so if this is a feature or fix you need, we encourage you to work on it and contribute a pull request, rather than waiting for the core team to get round to it!
|
||||
- **real-soon-now**: no-one on the core team has resources allocated to work on this right now, but we know it's a pain point, and it will be prioritized whenever we next get a chance to choose something new to work on. In practice, that kind of free choice doesn't happen very often - there are lots of pressures determining what we work on from day to day - so if this is a feature or fix you need, we encourage you to work on it and contribute a pull request, rather than waiting for the core team to get round to it!
|
||||
- A specific version number (for example **1.6**): the issue is important enough that it needs to be fixed in this version. There are resources allocated and/or plans to work on the issue in the given version.
|
||||
- No milestone: the issue is accepted as valid once the `status:Unconfirmed` label is removed (when it's confirmed as a report for a legitimate bug, or a useful feature request) but is not deemed a priority to work on (in the opinion of the core team). For example - a bug that's only cosmetic, or a feature that would be kind of neat but not really essential. There are no resources allocated to it - feel free to take it on!
|
||||
|
||||
|
@ -24,7 +24,7 @@ On some occasions it may take longer for the core team to classify an issue into
|
|||
- It may require a non-trivial amount of work to confirm the presence of a bug. In this case, feedback and further details from other contributors, whether or not they can replicate the bug, would be particularly welcomed.
|
||||
- It may require further discussion to decide whether the proposal is a good idea or not - if so, it will be tagged ["design decision needed"](https://github.com/wagtail/wagtail/labels/status%3ANeeds%20Design%20Decision).
|
||||
|
||||
We will endeavour to make sure that issues don't remain in this state for prolonged periods. Issues and PRs tagged "design decision needed" will be revisited regularly and discussed with at least two core contributors - we aim to review each ticket at least once per release cycle (= 6 weeks) as part of weekly core team meetings.
|
||||
We will endeavor to make sure that issues don't remain in this state for prolonged periods. Issues and PRs tagged "design decision needed" will be revisited regularly and discussed with at least two core contributors - we aim to review each ticket at least once per release cycle (= 6 weeks) as part of weekly core team meetings.
|
||||
|
||||
## Pull requests
|
||||
|
||||
|
@ -34,7 +34,7 @@ As with issues, the core team will classify pull requests as soon as they are op
|
|||
- Core team members are invited to assign themselves to the pull request for review.
|
||||
- More specific details on how to triage Pull Requests can be found on the [PR triage wiki page](https://github.com/wagtail/wagtail/wiki/PR-triage).
|
||||
|
||||
Subsequently (ideally within a week or two, but possibly longer for larger submissions) a core team member will merge it if it is ready to be merged, or tag it as requiring further work ('needs work' / 'needs tests' / 'needs docs'). Pull requests that require further work are handled and prioritised in the same way as issues - anyone is welcome to pick one up from the backlog, whether or not they were the original committer.
|
||||
Subsequently (ideally within a week or two, but possibly longer for larger submissions) a core team member will merge it if it is ready to be merged, or tag it as requiring further work ('needs work' / 'needs tests' / 'needs docs'). Pull requests that require further work are handled and prioritized in the same way as issues - anyone is welcome to pick one up from the backlog, whether or not they were the original committer.
|
||||
|
||||
Rebasing / squashing of pull requests is welcome, but not essential. When doing so, do not squash commits that need reviewing into previous ones and make sure to preserve the sequence of changes. To fix mistakes in earlier commits, use `git commit --fixup` so that the final merge can be done with `git rebase -i --autosquash`.
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ else:
|
|||
|
||||
Always compare against the version using greater-or-equals (`>=`), so that code for newer versions of Django is first.
|
||||
|
||||
Do not use a `try ... except` when seeing if an object has an attribute or method introduced in a newer versions of Django, as it does not clearly express why the `try ... except` is used. An explicit check against the Django version makes the intention of the code very clear.
|
||||
Do not use a `try ... except` when seeing if an object has an attribute or method introduced in newer versions of Django, as it does not clearly express why the `try ... except` is used. An explicit check against the Django version makes the intention of the code very clear.
|
||||
|
||||
```python
|
||||
# Do not do this
|
||||
|
|
|
@ -13,7 +13,7 @@ Most normal bugs in Wagtail are reported as [GitHub issues](https://github.com/w
|
|||
Instead, if you believe you've found something in Wagtail which has security implications, please send a description of the issue via email to <security@wagtail.org>.
|
||||
Mail sent to that address reaches a subset of the core team, who can forward security issues to other core team members for broader discussion if needed.
|
||||
|
||||
Once you've submitted an issue via email, you should receive an acknowledgement from a member of the security team within 48 hours, and depending on the action to be taken, you may receive further followup emails.
|
||||
Once you've submitted an issue via email, you should receive an acknowledgment from a member of the security team within 48 hours, and depending on the action to be taken, you may receive further followup emails.
|
||||
|
||||
If you want to send an encrypted email (optional), the public key ID for <security@wagtail.org> is `0xbed227b4daf93ff9`, and this public key is available from most commonly-used keyservers.
|
||||
|
||||
|
@ -57,4 +57,4 @@ In various places Wagtail provides the option to export data in CSV format, and
|
|||
|
||||
Since the CSV format has no concept of formulae or macros, there is also no agreed-upon convention for escaping data to prevent it from being interpreted in that way; commonly-suggested approaches such as prefixing the field with a quote character would corrupt legitimate data (such as phone numbers beginning with '+') when interpreted by software correctly following the CSV specification.
|
||||
|
||||
Wagtail's data exports default to XLSX, which can be loaded into spreadsheet software without any such issues. This minimises the risk of a user handling CSV files insecurely, as they would have to explicitly choose CSV over the more familiar XLSX format.
|
||||
Wagtail's data exports default to XLSX, which can be loaded into spreadsheet software without any such issues. This minimizes the risk of a user handling CSV files insecurely, as they would have to explicitly choose CSV over the more familiar XLSX format.
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
Wagtail uses [Transifex](https://www.transifex.com/) to translate the content for the admin interface. Our goal is to ensure that Wagtail can be used by those who speak many different languages. Translation of admin content is a great way to contribute without needing to know how to write code.
|
||||
|
||||
```{note}
|
||||
For translations and internationalisation of content made with Wagtail see [](internationalisation).
|
||||
For translations and internationalization of content made with Wagtail see [](internationalisation).
|
||||
```
|
||||
|
||||
## Translation workflow
|
||||
|
||||
Wagtail is localised (translated) using Django's [translation system](inv:django#topics/i18n/translation) and the translations are provided to and managed by [Transifex](https://www.transifex.com/), a web platform that helps organisations coordinate translation projects.
|
||||
Wagtail is localized (translated) using Django's [translation system](inv:django#topics/i18n/translation) and the translations are provided to and managed by [Transifex](https://www.transifex.com/), a web platform that helps organizations coordinate translation projects.
|
||||
|
||||
Translations from Transifex are only integrated into the repository at the time of a new release. When a release is close to being ready there will be a RC (Release Candidate) for the upcoming version and the translations will be exported to Transifex.
|
||||
|
||||
|
@ -26,7 +26,7 @@ These new translations are imported into Wagtail for any subsequent RC and the f
|
|||
- Click on start for free
|
||||
- Fill in your Username, Email and Password
|
||||
- Agree to the terms and conditions
|
||||
- Click on free trial or join an existing organisation
|
||||
- Click on free trial or join an existing organization
|
||||
- Join [Wagtail](https://app.transifex.com/torchbox/wagtail/dashboard/) and see the list of languages on the dashboard
|
||||
- Request access to become a member of the language team you want to work with on Slack (mention your Transifex username)
|
||||
- A view resources button appears when you hover over the ready to use part on the right side of the page
|
||||
|
@ -40,7 +40,7 @@ These new translations are imported into Wagtail for any subsequent RC and the f
|
|||
|
||||
In code, strings can be marked for translation with using Django's [translation system](inv:django#topics/i18n/translation), using `gettext` or `gettext_lazy` in Python and `blocktranslate`, `translate`, and `_(" ")` in templates.
|
||||
|
||||
In both Python and templates, make sure to always use named placeholder. In addition, in Python, only use the printf style formatting. This is to ensure compatibility with Transifex and help translators in their work.
|
||||
In both Python and templates, make sure to always use a named placeholder. In addition, in Python, only use the printf style formatting. This is to ensure compatibility with Transifex and help translators in their work.
|
||||
|
||||
### Translations within Python
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ We use [djhtml](https://github.com/rtts/djhtml) for formatting and [Curlylint](h
|
|||
|
||||
- Write [valid](https://validator.w3.org/nu/), [semantic](https://html5doctor.com/element-index/) HTML.
|
||||
- Follow [ARIA authoring practices](https://w3c.github.io/aria-practices/), in particular, [No ARIA is better than Bad ARIA](https://w3c.github.io/aria-practices/#no_aria_better_bad_aria).
|
||||
- Use IDs for semantics only, classes for styling, `data-` attributes for JavaScript behaviour.
|
||||
- Use IDs for semantics only, classes for styling, `data-` attributes for JavaScript behavior.
|
||||
- Order attributes with `id` first, then `class`, then any `data-` or other attributes with Stimulus `data-controller` first.
|
||||
- For comments, use [Django template syntax](https://docs.djangoproject.com/en/stable/ref/templates/language/#comments) instead of HTML.
|
||||
|
||||
|
@ -33,7 +33,7 @@ We use [Prettier](https://prettier.io/) for formatting and [Stylelint](https://s
|
|||
- We follow [BEM](https://getbem.com/) and [ITCSS](https://www.xfive.co/blog/itcss-scalable-maintainable-css-architecture/), with a large amount of utilities created with [Tailwind](https://tailwindcss.com/).
|
||||
- Familiarise yourself with our [stylelint-config-wagtail](https://github.com/wagtail/stylelint-config-wagtail) configuration, which details our preferred code style.
|
||||
- Use `rems` for `font-size`, because they offer absolute control over text. Additionally, unit-less `line-height` is preferred because it does not inherit a percentage value of its parent element, but instead is based on a multiplier of the `font-size`.
|
||||
- Always use variables for design tokens such as colours or font sizes, rather than hard-coding specific values.
|
||||
- Always use variables for design tokens such as colors or font sizes, rather than hard-coding specific values.
|
||||
- We use the `w-` prefix for all styles intended to be reusable by Wagtail site implementers.
|
||||
|
||||
### Stylesheets
|
||||
|
@ -100,7 +100,7 @@ We use [Prettier](https://prettier.io/) for formatting and [ESLint](https://esli
|
|||
|
||||
## Stimulus
|
||||
|
||||
Wagtail uses [Stimulus](https://stimulus.hotwired.dev/) as a lightweight framework to attach interactive behaviour to DOM elements via `data-` attributes.
|
||||
Wagtail uses [Stimulus](https://stimulus.hotwired.dev/) as a lightweight framework to attach interactive behavior to DOM elements via `data-` attributes.
|
||||
|
||||
### Why Stimulus
|
||||
|
||||
|
@ -122,17 +122,17 @@ First think of how to name the controller. Keep it concise, one or two words ide
|
|||
|
||||
1. Start with the HTML templates, build as much of the UI as you can in HTML alone. Ensure it is accessible and follows the CSS guidelines.
|
||||
2. Create the controller file in our `client/src/controllers` folder, along with its tests (see [](testing)) and Storybook stories.
|
||||
3. For initialisation, consider which [controller lifecycle methods](https://stimulus.hotwired.dev/reference/lifecycle-callbacks#methods) to use, if any (`connect`, `initialize`).
|
||||
3. For initialization, consider which [controller lifecycle methods](https://stimulus.hotwired.dev/reference/lifecycle-callbacks#methods) to use, if any (`connect`, `initialize`).
|
||||
4. If relevant, also consider how to handle the controlled element being removed from the DOM [`disconnect` lifecycle method](https://stimulus.hotwired.dev/reference/lifecycle-callbacks#disconnection).
|
||||
5. Document controller classes and methods with [JSDoc annotations](https://jsdoc.app/index.html).
|
||||
6. Use [values](https://stimulus.hotwired.dev/reference/values) to provide options and also reactive state, avoiding instance properties if possible. Prefer falsey or empty defaults and avoid too much usage of the Object type when using values.
|
||||
7. Build the behaviour around small, discrete, methods and use [Stimulus actions](https://stimulus.hotwired.dev/reference/actions) declared in HTML to drive when they are called.
|
||||
7. Build the behavior around small, discrete, methods and use [Stimulus actions](https://stimulus.hotwired.dev/reference/actions) declared in HTML to drive when they are called.
|
||||
|
||||
### Helpful tips
|
||||
|
||||
- Prefer controllers that do a small amount of 'work' that is collected together, instead of lots of large or specific controllers.
|
||||
- Lean towards dispatching events for key behaviour in the UI interaction as this provides a great way for custom code to hook into this without an explicit API, but be sure to document these.
|
||||
- Multiple controllers can be attached to one DOM element for composing behaviour, where practical split out behaviour to separate controllers.
|
||||
- Lean towards dispatching events for key behavior in the UI interaction as this provides a great way for custom code to hook into this without an explicit API, but be sure to document these.
|
||||
- Multiple controllers can be attached to one DOM element for composing behavior, where practical split out behavior to separate controllers.
|
||||
- Consider when to document controller usage for non-contributors.
|
||||
- When writing unit tests, note that DOM updates triggered by data attribute changes are completed async (next `microtick`) so will require a await Promise or similar to check for the changes in JSDom.
|
||||
- Avoid hard-coding a controller's identifier, instead reference it with `this.identifier` if adjusting attributes. This way the controller can be used easily with a changed identifier or extended by other classes in the future.
|
||||
|
|
|
@ -205,6 +205,8 @@ WAGTAILADMIN_EXTERNAL_LINK_CONVERSION = 'exact'
|
|||
|
||||
Customize Wagtail's behavior when an internal page url is entered in the external link chooser. Possible values for this setting are `'all'`, `'exact'`, `'confirm`, or `''`. The default, `'all'`, means that Wagtail will automatically convert submitted urls that exactly match page urls to the corresponding internal links. If the url is an inexact match - for example, the submitted url has query parameters - then Wagtail will confirm the conversion with the user. `'exact'` means that any inexact matches will be left as external urls, and the confirmation step will be skipped. `'confirm'` means that every link conversion will be confirmed with the user, even if the match is exact. `''` means that Wagtail will not attempt to convert any urls entered to internal page links.
|
||||
|
||||
(wagtail_date_time_formats)=
|
||||
|
||||
### `WAGTAIL_DATE_FORMAT`, `WAGTAIL_DATETIME_FORMAT`, `WAGTAIL_TIME_FORMAT`
|
||||
|
||||
```python
|
||||
|
|
|
@ -18,6 +18,7 @@ depth: 1
|
|||
* 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)
|
||||
|
||||
|
||||
### Bug fixes
|
||||
|
@ -25,13 +26,15 @@ depth: 1
|
|||
* Fix typo in `__str__` for MySQL search index (Jake Howard)
|
||||
* Ensure that unit tests correctly check for migrations in all core Wagtail apps (Matt Westcott)
|
||||
* Correctly handle `date` objects on `human_readable_date` template tag (Jhonatan Lopes)
|
||||
* Ensure re-ordering buttons work correctly when using a nested InlinePanel (Adrien Hamraoui)
|
||||
* Resolve inconsistent use of model `verbose_name` in group edit view when listing custom permissions (Neeraj Yetheendran, Omkar Jadhav)
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* Add contributing development documentation on how to work with a fork of Wagtail (Nix Asteri, Dan Braghis)
|
||||
* Make sure the settings panel is listed in tabbed interface examples (Tibor Leupold)
|
||||
* Update multiple pages in the reference sections and multiple page names to their US spelling instead of UK spelling (Victoria Poromon)
|
||||
* Update content and page names to their US spelling instead of UK spelling (Victoria Poromon)
|
||||
|
||||
|
||||
### Maintenance
|
||||
|
@ -43,6 +46,7 @@ depth: 1
|
|||
* Rename the React `Button` that only renders links (a element) to `Link` and remove unused prop & behavior that was non-compliant for aria role usage (Advik Kabra)
|
||||
* Set up an `wagtail.models.AbstractWorkflow` model to support future customisations around workflows (Hossein)
|
||||
* Improve `classnames` template tag to handle nested lists of strings, use template tag for admin `body` element (LB (Ben) Johnston)
|
||||
* Merge `UploadedDocument` and `UploadedImage` into new `UploadedFile` model for easier shared code usage (Advik Kabra, Karl Hobley)
|
||||
|
||||
|
||||
## Upgrade considerations
|
||||
|
|
|
@ -235,3 +235,54 @@ def file_overwrite_check(app_configs, **kwargs):
|
|||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@register("datetime_format")
|
||||
def datetime_format_check(app_configs, **kwargs):
|
||||
"""
|
||||
If L10N is enabled, check if WAGTAIL_* formats are compatible with Django input formats.
|
||||
See https://docs.djangoproject.com/en/stable/topics/i18n/formatting/#creating-custom-format-files
|
||||
See https://docs.wagtail.org/en/stable/reference/settings.html#wagtail-date-format-wagtail-datetime-format-wagtail-time-format
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import formats, translation
|
||||
|
||||
errors = []
|
||||
|
||||
if not getattr(settings, "USE_L10N", False):
|
||||
return errors
|
||||
|
||||
formats.FORMAT_SETTINGS = formats.FORMAT_SETTINGS.union(
|
||||
[
|
||||
"WAGTAIL_DATE_FORMAT",
|
||||
"WAGTAIL_DATETIME_FORMAT",
|
||||
"WAGTAIL_TIME_FORMAT",
|
||||
]
|
||||
)
|
||||
|
||||
for code, label in settings.LANGUAGES:
|
||||
with translation.override(code):
|
||||
for wagtail_format, django_formats in [
|
||||
("WAGTAIL_DATE_FORMAT", "DATE_INPUT_FORMATS"),
|
||||
("WAGTAIL_DATETIME_FORMAT", "DATETIME_INPUT_FORMATS"),
|
||||
("WAGTAIL_TIME_FORMAT", "TIME_INPUT_FORMATS"),
|
||||
]:
|
||||
wagtail_format_value = getattr(settings, wagtail_format, None)
|
||||
django_formats_value = getattr(settings, django_formats, None)
|
||||
|
||||
if wagtail_format_value is None:
|
||||
# Skip the iteration if wagtail_format is not present
|
||||
continue
|
||||
|
||||
input_format = formats.get_format_lazy(wagtail_format_value)
|
||||
input_formats = formats.get_format_lazy(django_formats_value)
|
||||
if str(input_format) not in str(input_formats):
|
||||
errors.append(
|
||||
Error(
|
||||
"Configuration error",
|
||||
hint=f"{wagtail_format} {input_format} must be in {django_formats} for language {label} ({code}).",
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
from django.core.checks import Error
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from wagtail.admin.checks import datetime_format_check
|
||||
from wagtail.test.utils import WagtailTestUtils
|
||||
|
||||
|
||||
class TestDateTimeChecks(WagtailTestUtils, TestCase):
|
||||
fixtures = ["test.json"]
|
||||
|
||||
def test_datetime_format(self):
|
||||
with override_settings(
|
||||
WAGTAIL_CONTENT_LANGUAGES=[
|
||||
("en", "English"),
|
||||
],
|
||||
LANGUAGES=[
|
||||
("en", "English"),
|
||||
],
|
||||
WAGTAIL_DATE_FORMAT="%m/%d/%Y",
|
||||
WAGTAIL_TIME_FORMAT="%H:%M",
|
||||
USE_L10N=True,
|
||||
):
|
||||
errors = datetime_format_check(None)
|
||||
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_datetime_format_with_unsupported_date(self):
|
||||
with override_settings(
|
||||
WAGTAIL_CONTENT_LANGUAGES=[
|
||||
("en", "English"),
|
||||
],
|
||||
LANGUAGES=[
|
||||
("en", "English"),
|
||||
],
|
||||
WAGTAIL_DATE_FORMAT="%d.%m.%Y.",
|
||||
WAGTAIL_TIME_FORMAT="%H:%M",
|
||||
USE_L10N=True,
|
||||
):
|
||||
errors = datetime_format_check(None)
|
||||
|
||||
expected_errors = [
|
||||
Error(
|
||||
"Configuration error",
|
||||
hint="WAGTAIL_DATE_FORMAT %d.%m.%Y. must be in DATE_INPUT_FORMATS for language English (en).",
|
||||
)
|
||||
]
|
||||
self.assertEqual(errors, expected_errors)
|
||||
|
||||
def test_datetime_format_with_unsupported_date_not_using_l10n(self):
|
||||
"""
|
||||
Test that the check doesn't raise an error when USE_L10N is False.
|
||||
"""
|
||||
|
||||
with override_settings(
|
||||
WAGTAIL_CONTENT_LANGUAGES=[
|
||||
("en", "English"),
|
||||
],
|
||||
LANGUAGES=[
|
||||
("en", "English"),
|
||||
],
|
||||
WAGTAIL_DATE_FORMAT="%d.%m.%Y.",
|
||||
WAGTAIL_TIME_FORMAT="%H:%M",
|
||||
USE_L10N=False,
|
||||
):
|
||||
errors = datetime_format_check(None)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_datetime_format_with_unsupported_datetime_and_time(self):
|
||||
with override_settings(
|
||||
WAGTAIL_CONTENT_LANGUAGES=[
|
||||
("en", "English"),
|
||||
],
|
||||
LANGUAGES=[
|
||||
("en", "English"),
|
||||
],
|
||||
WAGTAIL_DATETIME_FORMAT="%d.%m.%Y. %H:%M",
|
||||
WAGTAIL_TIME_FORMAT="%I:%M %p",
|
||||
USE_L10N=True,
|
||||
):
|
||||
errors = datetime_format_check(None)
|
||||
|
||||
expected_errors = [
|
||||
Error(
|
||||
"Configuration error",
|
||||
hint="WAGTAIL_DATETIME_FORMAT %d.%m.%Y. %H:%M must be in DATETIME_INPUT_FORMATS for language English (en).",
|
||||
),
|
||||
Error(
|
||||
"Configuration error",
|
||||
hint="WAGTAIL_TIME_FORMAT %I:%M %p must be in TIME_INPUT_FORMATS for language English (en).",
|
||||
),
|
||||
]
|
||||
self.assertEqual(errors, expected_errors)
|
|
@ -1,5 +1,6 @@
|
|||
import os.path
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponseBadRequest, JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
@ -10,13 +11,13 @@ from django.views.decorators.vary import vary_on_headers
|
|||
from django.views.generic.base import TemplateView, View
|
||||
|
||||
from wagtail.admin.views.generic import PermissionCheckedMixin
|
||||
from wagtail.models import UploadedFile
|
||||
|
||||
|
||||
class AddView(PermissionCheckedMixin, TemplateView):
|
||||
# subclasses need to provide:
|
||||
# - permission_policy
|
||||
# - template_name
|
||||
# - upload_model
|
||||
|
||||
# - edit_object_url_name
|
||||
# - delete_object_url_name
|
||||
|
@ -152,10 +153,12 @@ class AddView(PermissionCheckedMixin, TemplateView):
|
|||
return JsonResponse(self.get_invalid_response_data(form))
|
||||
else:
|
||||
# Some other field of the form has failed validation, e.g. a required metadata field
|
||||
# on a custom image model. Store the object as an upload_model instance instead and
|
||||
# on a custom image model. Store the object as an UploadedFile instance instead and
|
||||
# present the edit form so that it will become a proper object when successfully filled in
|
||||
self.upload_object = self.upload_model.objects.create(
|
||||
file=self.request.FILES["files[]"], uploaded_by_user=self.request.user
|
||||
self.upload_object = UploadedFile.objects.create(
|
||||
for_content_type=ContentType.objects.get_for_model(self.get_model()),
|
||||
file=self.request.FILES["files[]"],
|
||||
uploaded_by_user=self.request.user,
|
||||
)
|
||||
self.object = self.model(
|
||||
title=self.request.FILES["files[]"].name,
|
||||
|
@ -297,7 +300,6 @@ class CreateFromUploadView(View):
|
|||
# subclasses need to provide:
|
||||
# - edit_upload_url_name
|
||||
# - delete_upload_url_name
|
||||
# - upload_model
|
||||
# - upload_pk_url_kwarg
|
||||
# - edit_upload_form_prefix
|
||||
# - context_object_id_name
|
||||
|
@ -320,7 +322,11 @@ class CreateFromUploadView(View):
|
|||
self.model = self.get_model()
|
||||
self.form_class = self.get_edit_form_class()
|
||||
|
||||
self.upload = get_object_or_404(self.upload_model, id=upload_id)
|
||||
self.upload = get_object_or_404(
|
||||
UploadedFile,
|
||||
id=upload_id,
|
||||
for_content_type=ContentType.objects.get_for_model(self.model),
|
||||
)
|
||||
|
||||
if self.upload.uploaded_by_user != request.user:
|
||||
raise PermissionDenied
|
||||
|
@ -369,14 +375,17 @@ class CreateFromUploadView(View):
|
|||
|
||||
class DeleteUploadView(View):
|
||||
# subclasses need to provide:
|
||||
# - upload_model
|
||||
# - upload_pk_url_kwarg
|
||||
|
||||
http_method_names = ["post"]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
upload_id = kwargs[self.upload_pk_url_kwarg]
|
||||
upload = get_object_or_404(self.upload_model, id=upload_id)
|
||||
upload = get_object_or_404(
|
||||
UploadedFile,
|
||||
id=upload_id,
|
||||
for_content_type=ContentType.objects.get_for_model(self.get_model()),
|
||||
)
|
||||
|
||||
if upload.uploaded_by_user != request.user:
|
||||
raise PermissionDenied
|
||||
|
|
|
@ -16,7 +16,7 @@ urlpatterns = [
|
|||
path("multiple/add/", multiple.AddView.as_view(), name="add_multiple"),
|
||||
path("multiple/<int:doc_id>/", multiple.EditView.as_view(), name="edit_multiple"),
|
||||
path(
|
||||
"multiple/create_from_uploaded_document/<int:uploaded_document_id>/",
|
||||
"multiple/create_from_uploaded_document/<int:uploaded_file_id>/",
|
||||
multiple.CreateFromUploadedDocumentView.as_view(),
|
||||
name="create_multiple_from_uploaded_document",
|
||||
),
|
||||
|
@ -26,7 +26,7 @@ urlpatterns = [
|
|||
name="delete_multiple",
|
||||
),
|
||||
path(
|
||||
"multiple/delete_upload/<int:uploaded_document_id>/",
|
||||
"multiple/delete_upload/<int:uploaded_file_id>/",
|
||||
multiple.DeleteUploadView.as_view(),
|
||||
name="delete_upload_multiple",
|
||||
),
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# Generated by Django 5.0.1 on 2024-01-30 18:19
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtaildocs', '0012_uploadeddocument'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='UploadedDocument',
|
||||
),
|
||||
]
|
|
@ -215,23 +215,3 @@ class Document(AbstractDocument):
|
|||
|
||||
# provides args: request
|
||||
document_served = Signal()
|
||||
|
||||
|
||||
class UploadedDocument(models.Model):
|
||||
"""
|
||||
Temporary storage for documents uploaded through the multiple doc uploader, when validation
|
||||
rules (e.g. required metadata fields) prevent creating a Document object from the document file
|
||||
alone. In this case, the document file is stored against this model, to be turned into a
|
||||
Document object once the full form has been filled in.
|
||||
"""
|
||||
|
||||
file = models.FileField(upload_to="uploaded_documents", max_length=200)
|
||||
uploaded_by_user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name=_("uploaded by user"),
|
||||
null=True,
|
||||
blank=True,
|
||||
editable=False,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
uploaded_by_user.wagtail_reference_index_ignore = True
|
||||
|
|
|
@ -2,6 +2,7 @@ import json
|
|||
from unittest import mock
|
||||
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
from django.test.utils import override_settings
|
||||
|
@ -12,7 +13,13 @@ from django.utils.http import urlencode
|
|||
from wagtail.admin.admin_url_finder import AdminURLFinder
|
||||
from wagtail.documents import get_document_model, models
|
||||
from wagtail.documents.tests.utils import get_test_document_file
|
||||
from wagtail.models import Collection, GroupCollectionPermission, Page, ReferenceIndex
|
||||
from wagtail.models import (
|
||||
Collection,
|
||||
GroupCollectionPermission,
|
||||
Page,
|
||||
ReferenceIndex,
|
||||
UploadedFile,
|
||||
)
|
||||
from wagtail.test.testapp.models import (
|
||||
CustomDocument,
|
||||
CustomDocumentWithAuthor,
|
||||
|
@ -1277,15 +1284,16 @@ class TestMultipleCustomDocumentUploaderWithRequiredField(TestMultipleDocumentUp
|
|||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Create an UploadedDocument for running tests on
|
||||
self.uploaded_document = models.UploadedDocument.objects.create(
|
||||
# Create an UploadedFile for running tests on
|
||||
self.uploaded_document = UploadedFile.objects.create(
|
||||
for_content_type=ContentType.objects.get_for_model(get_document_model()),
|
||||
file=get_test_document_file(),
|
||||
uploaded_by_user=self.user,
|
||||
)
|
||||
|
||||
def test_add_post(self):
|
||||
"""
|
||||
This tests that a POST request to the add view saves the document as an UploadedDocument
|
||||
This tests that a POST request to the add view saves the document as an UploadedFile
|
||||
and returns an edit form
|
||||
"""
|
||||
response = self.client.post(
|
||||
|
@ -1326,11 +1334,11 @@ class TestMultipleCustomDocumentUploaderWithRequiredField(TestMultipleDocumentUp
|
|||
|
||||
# Check JSON
|
||||
response_json = json.loads(response.content.decode())
|
||||
self.assertIn("uploaded_document_id", response_json)
|
||||
self.assertIn("uploaded_file_id", response_json)
|
||||
self.assertIn("form", response_json)
|
||||
self.assertIn("success", response_json)
|
||||
self.assertEqual(
|
||||
response_json["uploaded_document_id"],
|
||||
response_json["uploaded_file_id"],
|
||||
response.context["uploaded_document"].id,
|
||||
)
|
||||
self.assertTrue(response_json["success"])
|
||||
|
@ -1363,10 +1371,10 @@ class TestMultipleCustomDocumentUploaderWithRequiredField(TestMultipleDocumentUp
|
|||
|
||||
# Check JSON
|
||||
response_json = json.loads(response.content.decode())
|
||||
self.assertIn("uploaded_document_id", response_json)
|
||||
self.assertIn("uploaded_file_id", response_json)
|
||||
self.assertIn("form", response_json)
|
||||
self.assertEqual(
|
||||
response_json["uploaded_document_id"],
|
||||
response_json["uploaded_file_id"],
|
||||
response.context["uploaded_document"].id,
|
||||
)
|
||||
self.assertTrue(response_json["success"])
|
||||
|
@ -1419,11 +1427,11 @@ class TestMultipleCustomDocumentUploaderWithRequiredField(TestMultipleDocumentUp
|
|||
|
||||
# Check JSON
|
||||
response_json = json.loads(response.content.decode())
|
||||
self.assertIn("uploaded_document_id", response_json)
|
||||
self.assertIn("uploaded_file_id", response_json)
|
||||
self.assertIn("form", response_json)
|
||||
self.assertIn("success", response_json)
|
||||
self.assertEqual(
|
||||
response_json["uploaded_document_id"],
|
||||
response_json["uploaded_file_id"],
|
||||
response.context["uploaded_document"].id,
|
||||
)
|
||||
self.assertTrue(response_json["success"])
|
||||
|
@ -1438,10 +1446,10 @@ class TestMultipleCustomDocumentUploaderWithRequiredField(TestMultipleDocumentUp
|
|||
def test_create_from_upload_invalid_post(self):
|
||||
"""
|
||||
Posting an invalid form to the create_from_uploaded_document view throws a validation error
|
||||
and leaves the UploadedDocument intact
|
||||
and leaves the UploadedFile intact
|
||||
"""
|
||||
doc_count_before = CustomDocumentWithAuthor.objects.count()
|
||||
uploaded_doc_count_before = models.UploadedDocument.objects.count()
|
||||
uploaded_doc_count_before = UploadedFile.objects.count()
|
||||
|
||||
# Send request
|
||||
response = self.client.post(
|
||||
|
@ -1459,9 +1467,9 @@ class TestMultipleCustomDocumentUploaderWithRequiredField(TestMultipleDocumentUp
|
|||
)
|
||||
|
||||
doc_count_after = CustomDocumentWithAuthor.objects.count()
|
||||
uploaded_doc_count_after = models.UploadedDocument.objects.count()
|
||||
uploaded_doc_count_after = UploadedFile.objects.count()
|
||||
|
||||
# no changes to document / UploadedDocument count
|
||||
# no changes to document / UploadedFile count
|
||||
self.assertEqual(doc_count_after, doc_count_before)
|
||||
self.assertEqual(uploaded_doc_count_after, uploaded_doc_count_before)
|
||||
|
||||
|
@ -1497,7 +1505,7 @@ class TestMultipleCustomDocumentUploaderWithRequiredField(TestMultipleDocumentUp
|
|||
Posting a valid form to the create_from_uploaded_document view will create the document
|
||||
"""
|
||||
doc_count_before = CustomDocumentWithAuthor.objects.count()
|
||||
uploaded_doc_count_before = models.UploadedDocument.objects.count()
|
||||
uploaded_doc_count_before = UploadedFile.objects.count()
|
||||
|
||||
# Send request
|
||||
response = self.client.post(
|
||||
|
@ -1519,7 +1527,7 @@ class TestMultipleCustomDocumentUploaderWithRequiredField(TestMultipleDocumentUp
|
|||
)
|
||||
|
||||
doc_count_after = CustomDocumentWithAuthor.objects.count()
|
||||
uploaded_doc_count_after = models.UploadedDocument.objects.count()
|
||||
uploaded_doc_count_after = UploadedFile.objects.count()
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -1530,7 +1538,7 @@ class TestMultipleCustomDocumentUploaderWithRequiredField(TestMultipleDocumentUp
|
|||
self.assertIn("doc_id", response_json)
|
||||
self.assertTrue(response_json["success"])
|
||||
|
||||
# Document should have been created, UploadedDocument deleted
|
||||
# Document should have been created, UploadedFile deleted
|
||||
self.assertEqual(doc_count_after, doc_count_before + 1)
|
||||
self.assertEqual(uploaded_doc_count_after, uploaded_doc_count_before - 1)
|
||||
|
||||
|
@ -1544,7 +1552,7 @@ class TestMultipleCustomDocumentUploaderWithRequiredField(TestMultipleDocumentUp
|
|||
|
||||
def test_delete_uploaded_document(self):
|
||||
"""
|
||||
This tests that a POST request to the delete view deletes the UploadedDocument
|
||||
This tests that a POST request to the delete view deletes the UploadedFile
|
||||
"""
|
||||
# Send request
|
||||
response = self.client.post(
|
||||
|
@ -1559,9 +1567,7 @@ class TestMultipleCustomDocumentUploaderWithRequiredField(TestMultipleDocumentUp
|
|||
|
||||
# Make sure the document is deleted
|
||||
self.assertFalse(
|
||||
models.UploadedDocument.objects.filter(
|
||||
id=self.uploaded_document.id
|
||||
).exists()
|
||||
UploadedFile.objects.filter(id=self.uploaded_document.id).exists()
|
||||
)
|
||||
|
||||
# Check JSON
|
||||
|
|
|
@ -12,14 +12,12 @@ from wagtail.admin.views.generic.multiple_upload import EditView as BaseEditView
|
|||
|
||||
from .. import get_document_model
|
||||
from ..forms import get_document_form, get_document_multi_form
|
||||
from ..models import UploadedDocument
|
||||
from ..permissions import permission_policy
|
||||
|
||||
|
||||
class AddView(BaseAddView):
|
||||
permission_policy = permission_policy
|
||||
template_name = "wagtaildocs/multiple/add.html"
|
||||
upload_model = UploadedDocument
|
||||
|
||||
edit_object_url_name = "wagtaildocs:edit_multiple"
|
||||
delete_object_url_name = "wagtaildocs:delete_multiple"
|
||||
|
@ -31,7 +29,7 @@ class AddView(BaseAddView):
|
|||
delete_upload_url_name = "wagtaildocs:delete_upload_multiple"
|
||||
edit_upload_form_prefix = "uploaded-document"
|
||||
context_upload_name = "uploaded_document"
|
||||
context_upload_id_name = "uploaded_document_id"
|
||||
context_upload_id_name = "uploaded_file_id"
|
||||
|
||||
def get_model(self):
|
||||
return get_document_model()
|
||||
|
@ -90,8 +88,7 @@ class DeleteView(BaseDeleteView):
|
|||
class CreateFromUploadedDocumentView(BaseCreateFromUploadView):
|
||||
edit_upload_url_name = "wagtaildocs:create_multiple_from_uploaded_document"
|
||||
delete_upload_url_name = "wagtaildocs:delete_upload_multiple"
|
||||
upload_model = UploadedDocument
|
||||
upload_pk_url_kwarg = "uploaded_document_id"
|
||||
upload_pk_url_kwarg = "uploaded_file_id"
|
||||
edit_upload_form_prefix = "uploaded-document"
|
||||
context_object_id_name = "doc_id"
|
||||
context_upload_name = "uploaded_document"
|
||||
|
@ -118,5 +115,7 @@ class CreateFromUploadedDocumentView(BaseCreateFromUploadView):
|
|||
|
||||
|
||||
class DeleteUploadView(BaseDeleteUploadView):
|
||||
upload_model = UploadedDocument
|
||||
upload_pk_url_kwarg = "uploaded_document_id"
|
||||
upload_pk_url_kwarg = "uploaded_file_id"
|
||||
|
||||
def get_model(self):
|
||||
return get_document_model()
|
||||
|
|
|
@ -24,7 +24,7 @@ urlpatterns = [
|
|||
path("multiple/add/", multiple.AddView.as_view(), name="add_multiple"),
|
||||
path("multiple/<int:image_id>/", multiple.EditView.as_view(), name="edit_multiple"),
|
||||
path(
|
||||
"multiple/create_from_uploaded_image/<int:uploaded_image_id>/",
|
||||
"multiple/create_from_uploaded_image/<int:uploaded_file_id>/",
|
||||
multiple.CreateFromUploadedImageView.as_view(),
|
||||
name="create_multiple_from_uploaded_image",
|
||||
),
|
||||
|
@ -34,7 +34,7 @@ urlpatterns = [
|
|||
name="delete_multiple",
|
||||
),
|
||||
path(
|
||||
"multiple/delete_upload/<int:uploaded_image_id>/",
|
||||
"multiple/delete_upload/<int:uploaded_file_id>/",
|
||||
multiple.DeleteUploadView.as_view(),
|
||||
name="delete_upload_multiple",
|
||||
),
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# Generated by Django 5.0.1 on 2024-01-30 18:19
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtailimages', '0025_alter_image_file_alter_rendition_file'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='UploadedImage',
|
||||
),
|
||||
]
|
|
@ -1331,23 +1331,3 @@ class Rendition(AbstractRendition):
|
|||
|
||||
class Meta:
|
||||
unique_together = (("image", "filter_spec", "focal_point_key"),)
|
||||
|
||||
|
||||
class UploadedImage(models.Model):
|
||||
"""
|
||||
Temporary storage for images uploaded through the multiple image uploader, when validation rules (e.g.
|
||||
required metadata fields) prevent creating an Image object from the image file alone. In this case,
|
||||
the image file is stored against this model, to be turned into an Image object once the full form
|
||||
has been filled in.
|
||||
"""
|
||||
|
||||
file = models.ImageField(upload_to="uploaded_images", max_length=200)
|
||||
uploaded_by_user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name=_("uploaded by user"),
|
||||
null=True,
|
||||
blank=True,
|
||||
editable=False,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
uploaded_by_user.wagtail_reference_index_ignore = True
|
||||
|
|
|
@ -4,6 +4,7 @@ import urllib
|
|||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile, TemporaryUploadedFile
|
||||
from django.template.defaultfilters import filesizeformat
|
||||
from django.template.loader import render_to_string
|
||||
|
@ -16,12 +17,12 @@ from django.utils.safestring import mark_safe
|
|||
|
||||
from wagtail.admin.admin_url_finder import AdminURLFinder
|
||||
from wagtail.images import get_image_model
|
||||
from wagtail.images.models import UploadedImage
|
||||
from wagtail.images.utils import generate_signature
|
||||
from wagtail.models import (
|
||||
Collection,
|
||||
GroupCollectionPermission,
|
||||
Page,
|
||||
UploadedFile,
|
||||
get_root_collection_id,
|
||||
)
|
||||
from wagtail.test.testapp.models import (
|
||||
|
@ -2886,7 +2887,7 @@ class TestMultipleImageUploaderWithCustomImageModel(WagtailTestUtils, TestCase):
|
|||
|
||||
def test_unique_together_validation_error(self):
|
||||
"""
|
||||
If unique_together validation fails, create an UploadedImage and return a form so the
|
||||
If unique_together validation fails, create an UploadedFile and return a form so the
|
||||
user can fix it
|
||||
"""
|
||||
root_collection = Collection.get_first_root_node()
|
||||
|
@ -2895,7 +2896,7 @@ class TestMultipleImageUploaderWithCustomImageModel(WagtailTestUtils, TestCase):
|
|||
self.image.save()
|
||||
|
||||
image_count_before = CustomImage.objects.count()
|
||||
uploaded_image_count_before = UploadedImage.objects.count()
|
||||
uploaded_image_count_before = UploadedFile.objects.count()
|
||||
|
||||
response = self.client.post(
|
||||
reverse("wagtailimages:add_multiple"),
|
||||
|
@ -2908,9 +2909,9 @@ class TestMultipleImageUploaderWithCustomImageModel(WagtailTestUtils, TestCase):
|
|||
)
|
||||
|
||||
image_count_after = CustomImage.objects.count()
|
||||
uploaded_image_count_after = UploadedImage.objects.count()
|
||||
uploaded_image_count_after = UploadedFile.objects.count()
|
||||
|
||||
# an UploadedImage should have been created now, but not a CustomImage
|
||||
# an UploadedFile should have been created now, but not a CustomImage
|
||||
self.assertEqual(image_count_after, image_count_before)
|
||||
self.assertEqual(uploaded_image_count_after, uploaded_image_count_before + 1)
|
||||
|
||||
|
@ -3034,8 +3035,9 @@ class TestMultipleImageUploaderWithCustomRequiredFields(WagtailTestUtils, TestCa
|
|||
def setUp(self):
|
||||
self.user = self.login()
|
||||
|
||||
# Create an UploadedImage for running tests on
|
||||
self.uploaded_image = UploadedImage.objects.create(
|
||||
# Create an UploadedFile for running tests on
|
||||
self.uploaded_image = UploadedFile.objects.create(
|
||||
for_content_type=ContentType.objects.get_for_model(get_image_model()),
|
||||
file=get_test_image_file(),
|
||||
uploaded_by_user=self.user,
|
||||
)
|
||||
|
@ -3053,11 +3055,11 @@ class TestMultipleImageUploaderWithCustomRequiredFields(WagtailTestUtils, TestCa
|
|||
|
||||
def test_add_post(self):
|
||||
"""
|
||||
A POST request to the add view should create an UploadedImage rather than an image,
|
||||
A POST request to the add view should create an UploadedFile rather than an image,
|
||||
as we do not have enough data to pass CustomImageWithAuthor's validation yet
|
||||
"""
|
||||
image_count_before = CustomImageWithAuthor.objects.count()
|
||||
uploaded_image_count_before = UploadedImage.objects.count()
|
||||
uploaded_image_count_before = UploadedFile.objects.count()
|
||||
|
||||
response = self.client.post(
|
||||
reverse("wagtailimages:add_multiple"),
|
||||
|
@ -3069,9 +3071,9 @@ class TestMultipleImageUploaderWithCustomRequiredFields(WagtailTestUtils, TestCa
|
|||
)
|
||||
|
||||
image_count_after = CustomImageWithAuthor.objects.count()
|
||||
uploaded_image_count_after = UploadedImage.objects.count()
|
||||
uploaded_image_count_after = UploadedFile.objects.count()
|
||||
|
||||
# an UploadedImage should have been created now, but not a CustomImageWithAuthor
|
||||
# an UploadedFile should have been created now, but not a CustomImageWithAuthor
|
||||
self.assertEqual(image_count_after, image_count_before)
|
||||
self.assertEqual(uploaded_image_count_after, uploaded_image_count_before + 1)
|
||||
|
||||
|
@ -3137,10 +3139,10 @@ class TestMultipleImageUploaderWithCustomRequiredFields(WagtailTestUtils, TestCa
|
|||
def test_create_from_upload_invalid_post(self):
|
||||
"""
|
||||
Posting an invalid form to the create_from_uploaded_image view throws a validation error and leaves the
|
||||
UploadedImage intact
|
||||
UploadedFile intact
|
||||
"""
|
||||
image_count_before = CustomImageWithAuthor.objects.count()
|
||||
uploaded_image_count_before = UploadedImage.objects.count()
|
||||
uploaded_image_count_before = UploadedFile.objects.count()
|
||||
|
||||
# Send request
|
||||
response = self.client.post(
|
||||
|
@ -3156,9 +3158,9 @@ class TestMultipleImageUploaderWithCustomRequiredFields(WagtailTestUtils, TestCa
|
|||
)
|
||||
|
||||
image_count_after = CustomImageWithAuthor.objects.count()
|
||||
uploaded_image_count_after = UploadedImage.objects.count()
|
||||
uploaded_image_count_after = UploadedFile.objects.count()
|
||||
|
||||
# no changes to image / UploadedImage count
|
||||
# no changes to image / UploadedFile count
|
||||
self.assertEqual(image_count_after, image_count_before)
|
||||
self.assertEqual(uploaded_image_count_after, uploaded_image_count_before)
|
||||
|
||||
|
@ -3194,7 +3196,7 @@ class TestMultipleImageUploaderWithCustomRequiredFields(WagtailTestUtils, TestCa
|
|||
Posting a valid form to the create_from_uploaded_image view will create the image
|
||||
"""
|
||||
image_count_before = CustomImageWithAuthor.objects.count()
|
||||
uploaded_image_count_before = UploadedImage.objects.count()
|
||||
uploaded_image_count_before = UploadedFile.objects.count()
|
||||
|
||||
# Send request
|
||||
response = self.client.post(
|
||||
|
@ -3212,7 +3214,7 @@ class TestMultipleImageUploaderWithCustomRequiredFields(WagtailTestUtils, TestCa
|
|||
)
|
||||
|
||||
image_count_after = CustomImageWithAuthor.objects.count()
|
||||
uploaded_image_count_after = UploadedImage.objects.count()
|
||||
uploaded_image_count_after = UploadedFile.objects.count()
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -3223,7 +3225,7 @@ class TestMultipleImageUploaderWithCustomRequiredFields(WagtailTestUtils, TestCa
|
|||
self.assertIn("image_id", response_json)
|
||||
self.assertTrue(response_json["success"])
|
||||
|
||||
# Image should have been created, UploadedImage deleted
|
||||
# Image should have been created, UploadedFile deleted
|
||||
self.assertEqual(image_count_after, image_count_before + 1)
|
||||
self.assertEqual(uploaded_image_count_after, uploaded_image_count_before - 1)
|
||||
|
||||
|
@ -3239,7 +3241,7 @@ class TestMultipleImageUploaderWithCustomRequiredFields(WagtailTestUtils, TestCa
|
|||
|
||||
def test_delete_uploaded_image(self):
|
||||
"""
|
||||
This tests that a POST request to the delete view deletes the UploadedImage
|
||||
This tests that a POST request to the delete view deletes the UploadedFile
|
||||
"""
|
||||
# Send request
|
||||
response = self.client.post(
|
||||
|
@ -3254,7 +3256,7 @@ class TestMultipleImageUploaderWithCustomRequiredFields(WagtailTestUtils, TestCa
|
|||
|
||||
# Make sure the image is deleted
|
||||
self.assertFalse(
|
||||
UploadedImage.objects.filter(id=self.uploaded_image.id).exists()
|
||||
UploadedFile.objects.filter(id=self.uploaded_image.id).exists()
|
||||
)
|
||||
|
||||
# Check JSON
|
||||
|
|
|
@ -15,7 +15,6 @@ from wagtail.admin.views.generic.multiple_upload import EditView as BaseEditView
|
|||
from wagtail.images import get_image_model
|
||||
from wagtail.images.fields import get_allowed_image_extensions
|
||||
from wagtail.images.forms import get_image_form, get_image_multi_form
|
||||
from wagtail.images.models import UploadedImage
|
||||
from wagtail.images.permissions import ImagesPermissionPolicyGetter, permission_policy
|
||||
from wagtail.images.utils import find_image_duplicates
|
||||
|
||||
|
@ -23,7 +22,6 @@ from wagtail.images.utils import find_image_duplicates
|
|||
class AddView(BaseAddView):
|
||||
permission_policy = ImagesPermissionPolicyGetter()
|
||||
template_name = "wagtailimages/multiple/add.html"
|
||||
upload_model = UploadedImage
|
||||
|
||||
edit_object_url_name = "wagtailimages:edit_multiple"
|
||||
delete_object_url_name = "wagtailimages:delete_multiple"
|
||||
|
@ -35,7 +33,7 @@ class AddView(BaseAddView):
|
|||
delete_upload_url_name = "wagtailimages:delete_upload_multiple"
|
||||
edit_upload_form_prefix = "uploaded-image"
|
||||
context_upload_name = "uploaded_image"
|
||||
context_upload_id_name = "uploaded_image_id"
|
||||
context_upload_id_name = "uploaded_file_id"
|
||||
|
||||
def get_model(self):
|
||||
return get_image_model()
|
||||
|
@ -131,8 +129,7 @@ class DeleteView(BaseDeleteView):
|
|||
class CreateFromUploadedImageView(BaseCreateFromUploadView):
|
||||
edit_upload_url_name = "wagtailimages:create_multiple_from_uploaded_image"
|
||||
delete_upload_url_name = "wagtailimages:delete_upload_multiple"
|
||||
upload_model = UploadedImage
|
||||
upload_pk_url_kwarg = "uploaded_image_id"
|
||||
upload_pk_url_kwarg = "uploaded_file_id"
|
||||
edit_upload_form_prefix = "uploaded-image"
|
||||
context_object_id_name = "image_id"
|
||||
context_upload_name = "uploaded_image"
|
||||
|
@ -160,5 +157,7 @@ class CreateFromUploadedImageView(BaseCreateFromUploadView):
|
|||
|
||||
|
||||
class DeleteUploadView(BaseDeleteUploadView):
|
||||
upload_model = UploadedImage
|
||||
upload_pk_url_kwarg = "uploaded_image_id"
|
||||
upload_pk_url_kwarg = "uploaded_file_id"
|
||||
|
||||
def get_model(self):
|
||||
return get_image_model()
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
# Generated by Django 4.2.7 on 2024-02-12 21:04
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
("wagtailcore", "0092_alter_collectionviewrestriction_password_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="UploadedFile",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("file", models.FileField(max_length=200, upload_to="wagtail_uploads")),
|
||||
(
|
||||
"for_content_type",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="uploads",
|
||||
to="contenttypes.contenttype",
|
||||
verbose_name="for content type",
|
||||
),
|
||||
),
|
||||
(
|
||||
"uploaded_by_user",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
editable=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="uploaded by user",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -100,16 +100,6 @@ from .audit_log import ( # noqa: F401
|
|||
LogEntryQuerySet,
|
||||
ModelLogEntry,
|
||||
)
|
||||
from .collections import ( # noqa: F401
|
||||
BaseCollectionManager,
|
||||
Collection,
|
||||
CollectionManager,
|
||||
CollectionMember,
|
||||
CollectionViewRestriction,
|
||||
GroupCollectionPermission,
|
||||
GroupCollectionPermissionManager,
|
||||
get_root_collection_id,
|
||||
)
|
||||
from .copying import _copy, _copy_m2m_relations, _extract_field_data # noqa: F401
|
||||
from .i18n import ( # noqa: F401
|
||||
BootstrapTranslatableMixin,
|
||||
|
@ -120,6 +110,17 @@ from .i18n import ( # noqa: F401
|
|||
bootstrap_translatable_model,
|
||||
get_translatable_models,
|
||||
)
|
||||
from .media import ( # noqa: F401
|
||||
BaseCollectionManager,
|
||||
Collection,
|
||||
CollectionManager,
|
||||
CollectionMember,
|
||||
CollectionViewRestriction,
|
||||
GroupCollectionPermission,
|
||||
GroupCollectionPermissionManager,
|
||||
UploadedFile,
|
||||
get_root_collection_id,
|
||||
)
|
||||
from .reference_index import ReferenceIndex # noqa: F401
|
||||
from .sites import Site, SiteManager, SiteRootPath # noqa: F401
|
||||
from .specific import SpecificMixin
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
|
@ -188,3 +190,29 @@ class GroupCollectionPermission(models.Model):
|
|||
unique_together = ("group", "collection", "permission")
|
||||
verbose_name = _("group collection permission")
|
||||
verbose_name_plural = _("group collection permissions")
|
||||
|
||||
|
||||
class UploadedFile(models.Model):
|
||||
"""
|
||||
Temporary storage for media fields uploaded through the multiple image/document uploader.
|
||||
When validation rules (e.g. required metadata fields) prevent creating an Image/Document object from the file alone.
|
||||
In this case, the file is stored against this model, to be turned into an Image/Document object once the full form
|
||||
has been filled in.
|
||||
"""
|
||||
|
||||
for_content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
verbose_name=_("for content type"),
|
||||
related_name="uploads",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
)
|
||||
file = models.FileField(upload_to="wagtail_uploads", max_length=200)
|
||||
uploaded_by_user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name=_("uploaded by user"),
|
||||
null=True,
|
||||
blank=True,
|
||||
editable=False,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
|
@ -15,7 +15,7 @@ import wagtail.contrib.table_block.blocks
|
|||
import wagtail.fields
|
||||
import wagtail.images.blocks
|
||||
import wagtail.images.models
|
||||
import wagtail.models.collections
|
||||
import wagtail.models.media
|
||||
import wagtail.search.index
|
||||
import wagtail.test.testapp.models
|
||||
|
||||
|
@ -1043,7 +1043,7 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"collection",
|
||||
models.ForeignKey(
|
||||
default=wagtail.models.collections.get_root_collection_id,
|
||||
default=wagtail.models.media.get_root_collection_id,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to="wagtailcore.collection",
|
||||
|
@ -2244,7 +2244,7 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"collection",
|
||||
models.ForeignKey(
|
||||
default=wagtail.models.collections.get_root_collection_id,
|
||||
default=wagtail.models.media.get_root_collection_id,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to="wagtailcore.collection",
|
||||
|
@ -2341,7 +2341,7 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"collection",
|
||||
models.ForeignKey(
|
||||
default=wagtail.models.collections.get_root_collection_id,
|
||||
default=wagtail.models.media.get_root_collection_id,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to="wagtailcore.collection",
|
||||
|
@ -2622,7 +2622,7 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"collection",
|
||||
models.ForeignKey(
|
||||
default=wagtail.models.collections.get_root_collection_id,
|
||||
default=wagtail.models.media.get_root_collection_id,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to="wagtailcore.collection",
|
||||
|
@ -3891,7 +3891,7 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"collection",
|
||||
models.ForeignKey(
|
||||
default=wagtail.models.collections.get_root_collection_id,
|
||||
default=wagtail.models.media.get_root_collection_id,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to="wagtailcore.collection",
|
||||
|
@ -3936,7 +3936,7 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"collection",
|
||||
models.ForeignKey(
|
||||
default=wagtail.models.collections.get_root_collection_id,
|
||||
default=wagtail.models.media.get_root_collection_id,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to="wagtailcore.collection",
|
||||
|
@ -4055,7 +4055,7 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
"collection",
|
||||
models.ForeignKey(
|
||||
default=wagtail.models.collections.get_root_collection_id,
|
||||
default=wagtail.models.media.get_root_collection_id,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to="wagtailcore.collection",
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# Generated by Django 4.2.7 on 2024-02-12 22:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("tests", "0033_customcopyformpage"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ModelWithVerboseName",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Custom verbose name",
|
||||
"verbose_name_plural": "Custom verbose names",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1921,6 +1921,15 @@ class TableBlockStreamPage(Page):
|
|||
content_panels = [FieldPanel("table")]
|
||||
|
||||
|
||||
class ModelWithVerboseName(ClusterableModel):
|
||||
class Meta:
|
||||
verbose_name = "Custom verbose name"
|
||||
verbose_name_plural = "Custom verbose names"
|
||||
|
||||
|
||||
register_snippet(ModelWithVerboseName)
|
||||
|
||||
|
||||
class UserProfile(models.Model):
|
||||
# Wagtail's schema must be able to coexist alongside a custom UserProfile model
|
||||
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import itertools
|
||||
import re
|
||||
|
||||
from django import template
|
||||
|
||||
|
@ -85,7 +84,10 @@ def format_permissions(permission_bound_field):
|
|||
checkbox = checkboxes_by_id[perm.id]
|
||||
# identify the main categories of permission, and assign to
|
||||
# the relevant dict key, else bung in the 'custom_perms' list
|
||||
permission_action = perm.codename.split("_")[0]
|
||||
permission_action = perm.codename.split("_", maxsplit=1)
|
||||
permission_action = permission_action[permission_action[0].lower() == "can"]
|
||||
permission_action = permission_action.rsplit(maxsplit=1)[0]
|
||||
|
||||
if permission_action in main_permission_names:
|
||||
if permission_action in extra_perms_exist:
|
||||
extra_perms_exist[permission_action] = True
|
||||
|
@ -98,9 +100,7 @@ def format_permissions(permission_bound_field):
|
|||
custom_perms.append(
|
||||
{
|
||||
"perm": perm,
|
||||
"name": re.sub(
|
||||
f"{perm.content_type.name}$", "", perm.name, flags=re.I
|
||||
).strip(),
|
||||
"name": f"Can {permission_action}",
|
||||
"selected": checkbox.data["selected"],
|
||||
}
|
||||
)
|
||||
|
|
|
@ -1567,6 +1567,35 @@ class TestGroupCreateView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
|
|||
# Should not show inputs for publish permissions on models without DraftStateMixin
|
||||
self.assertNotInHTML("Can publish advert", html)
|
||||
|
||||
def test_view_permission_if_model_has_verbose(self):
|
||||
"""
|
||||
Tests for bug #10982
|
||||
https://github.com/wagtail/wagtail/issues/10982
|
||||
|
||||
Ensures Model Name or Verbose Name is stripped from Custom Permissions before being displayed
|
||||
A Model "ModelWithVerboseName" is added to wagtail.test.testapp.models with verbose_name = "Custom Verbose Name"
|
||||
|
||||
"""
|
||||
response = self.get()
|
||||
|
||||
self.assertContains(response, "Can view", msg_prefix="No Can view permission")
|
||||
self.assertNotContains(
|
||||
response,
|
||||
"Can view model with verbose name",
|
||||
msg_prefix=" Model Name not stripped from Can view permission",
|
||||
)
|
||||
self.assertNotContains(
|
||||
response,
|
||||
"Can view custom verbose name",
|
||||
msg_prefix="Verbose Name of ModelWithVerboseName not stripped from Can view permission",
|
||||
)
|
||||
pattern = r'Can\sview\s\[.*?"'
|
||||
self.assertNotRegex(
|
||||
response.content.decode(),
|
||||
pattern,
|
||||
msg="Model Name not stripped from custom permissions",
|
||||
)
|
||||
|
||||
|
||||
class TestGroupEditView(AdminTemplateTestUtils, WagtailTestUtils, TestCase):
|
||||
def setUp(self):
|
||||
|
|
Ładowanie…
Reference in New Issue