kopia lustrzana https://github.com/wagtail/wagtail
Porównaj commity
8 Commity
19dd1968c4
...
f04c9ff8a7
Autor | SHA1 | Data |
---|---|---|
Rohit Sharma | f04c9ff8a7 | |
Matt Westcott | a09bba67cd | |
Matt Westcott | 6fa3985674 | |
Jake Howard | 84d9bd6fb6 | |
Jake Howard | 37f9ae2ec6 | |
rohitsrma | ce967a404a | |
rohitsrma | 28e68f3882 | |
rohitsrma | 186a9fc128 |
|
@ -13,7 +13,9 @@ Changelog
|
|||
* Fix: Preserve whitespace in comment replies (Elhussein Almasri)
|
||||
* Docs: Remove duplicate section on frontend caching proxies from performance page (Jake Howard)
|
||||
* Docs: Document `restriction_type` field on PageViewRestriction (Shlomo Markowitz)
|
||||
* Docs: Document Wagtail's bug bounty policy (Jake Howard)
|
||||
* Maintenance: Use `DjangoJSONEncoder` instead of custom `LazyStringEncoder` to serialize Draftail config (Sage Abdullah)
|
||||
* Maintenance: Refactor image chooser pagination to check `WAGTAILIMAGES_CHOOSER_PAGE_SIZE` at runtime (Matt Westcott)
|
||||
|
||||
|
||||
6.1 (01.05.2024)
|
||||
|
|
|
@ -34,6 +34,12 @@ At any given time, the Wagtail team provides official security support for sever
|
|||
When new releases are issued for security reasons, the accompanying notice will include a list of affected versions.
|
||||
This list is comprised solely of supported versions of Wagtail: older versions may also be affected, but we do not investigate to determine that, and will not issue patches or new releases for those versions.
|
||||
|
||||
## Bug Bounties
|
||||
|
||||
Wagtail does not have a "Bug Bounty" program. Whilst we appreciate and accept reports from anyone, and will gladly give credit to you and/or your organisation, we aren't able to "reward" you for reporting the vulnerability.
|
||||
|
||||
["Beg Bounties"](https://www.troyhunt.com/beg-bounties/) are ever increasing among security researchers, and it's not something we condone or support.
|
||||
|
||||
## How Wagtail discloses security issues
|
||||
|
||||
Our process for taking a security issue from private discussion to public disclosure involves multiple steps.
|
||||
|
@ -46,8 +52,8 @@ On the day of disclosure, we will take the following steps:
|
|||
1. Apply the relevant patch(es) to Wagtail's codebase.
|
||||
The commit messages for these patches will indicate that they are for security issues, but will not describe the issue in any detail; instead, they will warn of upcoming disclosure.
|
||||
2. Issue the relevant release(s), by placing new packages on [the Python Package Index](https://pypi.org/project/wagtail/), tagging the new release(s) in Wagtail's GitHub repository and updating Wagtail's [release notes](../releases/index).
|
||||
3. Post a public entry on [Wagtail's blog](https://wagtail.org/blog/), describing the issue and its resolution in detail, pointing to the relevant patches and new releases, and crediting the reporter of the issue (if the reporter wishes to be publicly identified).
|
||||
4. Post a notice to the [Wagtail discussion board](https://github.com/wagtail/wagtail/discussions), [Slack workspace](https://wagtail.org/slack/) and Twitter feed ([\@WagtailCMS](https://twitter.com/wagtailcms)) that links to the blog post.
|
||||
3. Publish a [security advisory](https://github.com/wagtail/wagtail/security/advisories?state=published) on Wagtail's GitHub repository. This describes the issue and its resolution in detail, pointing to the relevant patches and new releases, and crediting the reporter of the issue (if the reporter wishes to be publicly identified)
|
||||
4. Post a notice to the [Wagtail discussion board](https://github.com/wagtail/wagtail/discussions), [Slack workspace](https://wagtail.org/slack/) and Twitter feed ([\@WagtailCMS](https://twitter.com/wagtailcms)) that links to the security advisory.
|
||||
|
||||
If a reported issue is believed to be particularly time-sensitive -- due to a known exploit in the wild, for example -- the time between advance notification and public disclosure may be shortened considerably.
|
||||
|
||||
|
|
|
@ -30,11 +30,13 @@ depth: 1
|
|||
|
||||
* Remove duplicate section on frontend caching proxies from performance page (Jake Howard)
|
||||
* Document `restriction_type` field on PageViewRestriction (Shlomo Markowitz)
|
||||
* Document Wagtail's bug bounty policy (Jake Howard)
|
||||
|
||||
|
||||
### Maintenance
|
||||
|
||||
* Use `DjangoJSONEncoder` instead of custom `LazyStringEncoder` to serialize Draftail config (Sage Abdullah)
|
||||
* Refactor image chooser pagination to check `WAGTAILIMAGES_CHOOSER_PAGE_SIZE` at runtime (Matt Westcott)
|
||||
|
||||
|
||||
## Upgrade considerations - changes affecting all projects
|
||||
|
|
|
@ -17,6 +17,10 @@ class ViewSet(WagtailMenuRegisterable):
|
|||
For more information on how to use this class, see :ref:`using_base_viewset`.
|
||||
"""
|
||||
|
||||
#: A special value that, when passed in a kwargs dict to construct a view, indicates that
|
||||
#: the attribute should not be written and should instead be left as the view's initial value
|
||||
UNDEFINED = object()
|
||||
|
||||
#: A name for this viewset, used as the default URL prefix and namespace.
|
||||
name = None
|
||||
|
||||
|
@ -42,12 +46,13 @@ class ViewSet(WagtailMenuRegisterable):
|
|||
in addition to any kwargs passed to this method. Items from get_common_view_kwargs will be
|
||||
filtered to only include those that are valid for the given view_class.
|
||||
"""
|
||||
merged_kwargs = self.get_common_view_kwargs()
|
||||
merged_kwargs.update(kwargs)
|
||||
filtered_kwargs = {
|
||||
key: value
|
||||
for key, value in self.get_common_view_kwargs().items()
|
||||
if hasattr(view_class, key)
|
||||
for key, value in merged_kwargs.items()
|
||||
if hasattr(view_class, key) and value is not self.UNDEFINED
|
||||
}
|
||||
filtered_kwargs.update(kwargs)
|
||||
return view_class.as_view(**filtered_kwargs)
|
||||
|
||||
def inject_view_methods(self, view_class, method_names):
|
||||
|
|
|
@ -29,7 +29,7 @@ class ChooserViewSet(ViewSet):
|
|||
) #: Label for the 'choose' button in the chooser widget, when an item has already been chosen
|
||||
edit_item_text = _("Edit") #: Label for the 'edit' button in the chooser widget
|
||||
|
||||
per_page = 10 #: Number of results to show per page
|
||||
per_page = ViewSet.UNDEFINED #: Number of results to show per page
|
||||
|
||||
#: A list of URL query parameters that should be passed on unmodified as part of any links or
|
||||
#: form submissions within the chooser modal workflow.
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from wagtail.contrib.forms.bulk_actions.form_bulk_action import FormSubmissionBulkAction
|
||||
from wagtail.contrib.forms.utils import get_forms_for_user
|
||||
|
||||
|
||||
class DeleteBulkAction(FormSubmissionBulkAction):
|
||||
display_name = _("Delete")
|
||||
aria_label = _("Delete selected objects")
|
||||
action_type = "delete"
|
||||
template_name = "bulk_actions/confirm_bulk_delete.html"
|
||||
|
||||
def check_perm(self, obj):
|
||||
return get_forms_for_user(self.request.user).exists()
|
||||
|
||||
@classmethod
|
||||
def execute_action(cls, objects, **kwargs):
|
||||
num_forms = 0
|
||||
for obj in objects:
|
||||
num_forms = num_forms + 1
|
||||
obj.delete()
|
||||
return num_forms, 0
|
||||
|
||||
def get_success_message(self, count, num_child_objects):
|
||||
|
||||
return ngettext(
|
||||
"One submission has been deleted.",
|
||||
"%(count)d submissions have been deleted.",
|
||||
count,
|
||||
) % {"count": count}
|
|
@ -0,0 +1,10 @@
|
|||
from wagtail.admin.views.bulk_action import BulkAction
|
||||
from wagtail.contrib.forms.models import FormSubmission
|
||||
|
||||
|
||||
class FormSubmissionBulkAction(BulkAction):
|
||||
models = [FormSubmission]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
return context
|
|
@ -0,0 +1,41 @@
|
|||
{% extends 'wagtailadmin/bulk_actions/confirmation/base.html' %}
|
||||
{% load i18n wagtailadmin_tags %}
|
||||
|
||||
{% block titletag %}{% blocktrans count counter=items|length %}Delete 1 item{% plural %}Delete {{ counter }} items{% endblocktrans %}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
{% trans "Delete" as del_str %}
|
||||
{% include "wagtailadmin/shared/header.html" with title=del_str icon="doc-empty-inverse" %}
|
||||
{% endblock header %}
|
||||
|
||||
|
||||
{% block items_with_access %}
|
||||
{% if items %}
|
||||
<p>
|
||||
{% blocktrans trimmed count counter=items|length %}
|
||||
Are you sure you want to delete this form submission?
|
||||
{% plural %}
|
||||
Are you sure you want to delete these form submissions?
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
{% include 'bulk_actions/list_form_submissions.html' %}
|
||||
{% endif %}
|
||||
{% endblock items_with_access %}
|
||||
|
||||
{% block items_with_no_access %}
|
||||
|
||||
{% blocktranslate trimmed asvar no_access_msg count counter=items_with_no_access|length %}You don't have permission to delete this item{% plural %}You don't have permission to delete these items{% endblocktranslate %}
|
||||
{% include 'bulk_actions/list_items_with_no_access.html' with items=items_with_no_access %}
|
||||
|
||||
{% endblock items_with_no_access %}
|
||||
|
||||
{% block form_section %}
|
||||
{% if items %}
|
||||
{% trans 'Yes, delete' as action_button_text %}
|
||||
{% trans "No, don't delete" as no_action_button_text %}
|
||||
{% include 'wagtailadmin/bulk_actions/confirmation/form.html' with action_button_class="serious" %}
|
||||
{% else %}
|
||||
{% include 'wagtailadmin/bulk_actions/confirmation/go_back.html' %}
|
||||
{% endif %}
|
||||
{% endblock form_section %}
|
|
@ -0,0 +1,29 @@
|
|||
{% load i18n %}
|
||||
<table class="listing">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="submit_time" class="">
|
||||
Submission date
|
||||
</th>
|
||||
{% for heading in data_headings %}
|
||||
<th>
|
||||
{{ heading.name }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% for item in items %}
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
{{item.item.submit_time}}
|
||||
</td>
|
||||
{% for label,data in item.item.form_data.items %}
|
||||
<td>
|
||||
{{ data }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
|
@ -0,0 +1,6 @@
|
|||
{% extends 'wagtailadmin/bulk_actions/confirmation/list_items_with_no_access.html' %}
|
||||
{% load i18n wagtailadmin_tags %}
|
||||
|
||||
{% block per_item %}
|
||||
{{ item }}
|
||||
{% endblock per_item %}
|
|
@ -1,23 +0,0 @@
|
|||
{% extends "wagtailadmin/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block titletag %}{% blocktrans trimmed with title=page.title %}Delete form data {{ title }}{% endblocktrans %}{% endblock %}
|
||||
{% block bodyclass %}menu-explorer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% trans "Delete form data" as del_str %}
|
||||
{% include "wagtailadmin/shared/header.html" with title=del_str subtitle=page.title icon="doc-empty-inverse" %}
|
||||
|
||||
<div class="nice-padding">
|
||||
<p>
|
||||
{% blocktrans trimmed count counter=submissions.count %}
|
||||
Are you sure you want to delete this form submission?
|
||||
{% plural %}
|
||||
Are you sure you want to delete these form submissions?
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<form action="{% url 'wagtailforms:delete_submissions' page.id %}?{{ request.GET.urlencode }}" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="{% trans 'Delete' %}" class="button serious">
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,14 +1,14 @@
|
|||
{% extends "wagtailadmin/generic/index_results.html" %}
|
||||
{% load i18n %}
|
||||
{% load i18n l10n wagtailadmin_tags %}
|
||||
{% block results %}
|
||||
<form class="w-overflow-auto" data-controller="w-bulk" data-w-bulk-action-inactive-class="w-invisible" action="{% url 'wagtailforms:delete_submissions' form_page.id %}" method="get">
|
||||
<form class="w-overflow-auto" data-controller="w-bulk" data-w-bulk-action-inactive-class="w-invisible">
|
||||
<table class="listing">
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" data-action="w-bulk#toggleAll" data-w-bulk-target="all" /></th>
|
||||
{% include 'wagtailadmin/bulk_actions/select_all_checkbox_cell.html' %}
|
||||
{% for heading in data_headings %}
|
||||
<th id="{{ heading.name }}" class="{% if heading.order %}ordered icon {% if heading.order == 'ascending' %}icon-arrow-up-after{% else %}icon-arrow-down-after{% endif %}{% endif %}">
|
||||
{% if heading.order %}<a href="?order_by={% if heading.order == 'ascending' %}-{% endif %}{{ heading.name }}">{{ heading.label }}</a>{% else %}{{ heading.label }}{% endif %}
|
||||
|
@ -17,10 +17,16 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% trans "Select response" as checkbox_aria_label %}
|
||||
{% for row in data_rows %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="selected-submissions" class="select-submission" value="{{ row.model_id }}" data-action="w-bulk#toggle" data-w-bulk-target="item" />
|
||||
<td class="bulk-action-checkbox-cell">
|
||||
<input type="checkbox"
|
||||
{% if obj_type == 'data_rows' %}data-page-status="{% if instance.live %}live{% else %}draft{% endif %}"{% endif %}
|
||||
data-object-id="{{ row.id|unlocalize|admin_urlquote }}" data-bulk-action-checkbox class="bulk-action-checkbox"
|
||||
aria-label="{% trans "Select" %}"
|
||||
{% if aria_describedby %}aria-describedby="{{ aria_describedby }}"{% endif %}
|
||||
/>
|
||||
</td>
|
||||
{% for cell in row.fields %}
|
||||
<td>
|
||||
|
@ -31,11 +37,6 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="nice-padding">
|
||||
<button class="button no w-invisible" data-w-bulk-target="action">
|
||||
{% trans "Delete selected submissions" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,3 +1,15 @@
|
|||
{% extends "wagtailadmin/generic/listing.html" %}
|
||||
{% load i18n %}
|
||||
{% load i18n wagtailadmin_tags %}
|
||||
{% block titletag %}{% blocktrans trimmed with form_title=form_page.title|capfirst %}Submissions of {{ form_title }}{% endblocktrans %}{% endblock %}
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
window.wagtailConfig.BULK_ACTION_ITEM_TYPE = 'SNIPPET';
|
||||
</script>
|
||||
<script defer src="{% versioned_static 'wagtailadmin/js/bulk-actions.js' %}"></script>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block bulk_actions %}
|
||||
{% trans "Select all documents in listing" as select_all_text %}
|
||||
{% include 'wagtailadmin/bulk_actions/footer.html' with select_all_obj_text=select_all_text app_label=app_label model_name=model_name objects=data_rows %}
|
||||
{% endblock %}
|
|
@ -1201,63 +1201,84 @@ class TestDeleteFormSubmission(WagtailTestUtils, TestCase):
|
|||
fixtures = ["test.json"]
|
||||
|
||||
def setUp(self):
|
||||
self.form_model = FormSubmission
|
||||
self.login(username="siteeditor", password="password")
|
||||
self.form_page = Page.objects.get(url_path="/home/contact-us/")
|
||||
|
||||
def test_delete_submission_show_confirmation(self):
|
||||
response = self.client.get(
|
||||
reverse("wagtailforms:delete_submissions", args=(self.form_page.id,))
|
||||
+ f"?selected-submissions={FormSubmission.objects.first().id}"
|
||||
reverse(
|
||||
"wagtail_bulk_action",
|
||||
args=(
|
||||
self.form_model._meta.app_label,
|
||||
self.form_model._meta.model_name,
|
||||
"delete",
|
||||
),
|
||||
)
|
||||
+ f"?&id={FormSubmission.objects.first().id}"
|
||||
)
|
||||
# Check show confirm page when HTTP method is GET
|
||||
self.assertTemplateUsed(response, "wagtailforms/confirm_delete.html")
|
||||
self.assertTemplateUsed(response, "bulk_actions/confirm_bulk_delete.html")
|
||||
|
||||
# Check that the deletion has not happened with GET request
|
||||
self.assertEqual(FormSubmission.objects.count(), 2)
|
||||
|
||||
def test_delete_submission_with_permissions(self):
|
||||
response = self.client.post(
|
||||
reverse("wagtailforms:delete_submissions", args=(self.form_page.id,))
|
||||
+ f"?selected-submissions={FormSubmission.objects.first().id}"
|
||||
reverse(
|
||||
"wagtail_bulk_action",
|
||||
args=(
|
||||
self.form_model._meta.app_label,
|
||||
self.form_model._meta.model_name,
|
||||
"delete",
|
||||
),
|
||||
)
|
||||
+ f"?&id={FormSubmission.objects.first().id}"
|
||||
)
|
||||
|
||||
# Check that the submission is gone
|
||||
self.assertEqual(FormSubmission.objects.count(), 1)
|
||||
# Should be redirected to list of submissions
|
||||
self.assertRedirects(
|
||||
response,
|
||||
reverse("wagtailforms:list_submissions", args=(self.form_page.id,)),
|
||||
)
|
||||
|
||||
def test_delete_multiple_submissions_with_permissions(self):
|
||||
response = self.client.post(
|
||||
reverse("wagtailforms:delete_submissions", args=(self.form_page.id,))
|
||||
+ "?selected-submissions={}&selected-submissions={}".format(
|
||||
reverse(
|
||||
"wagtail_bulk_action",
|
||||
args=(
|
||||
self.form_model._meta.app_label,
|
||||
self.form_model._meta.model_name,
|
||||
"delete",
|
||||
),
|
||||
)
|
||||
+ "?&id={}&id={}".format(
|
||||
FormSubmission.objects.first().id, FormSubmission.objects.last().id
|
||||
)
|
||||
)
|
||||
|
||||
# Check that both submissions are gone
|
||||
self.assertEqual(FormSubmission.objects.count(), 0)
|
||||
# Should be redirected to list of submissions
|
||||
self.assertRedirects(
|
||||
response,
|
||||
reverse("wagtailforms:list_submissions", args=(self.form_page.id,)),
|
||||
)
|
||||
|
||||
def test_delete_submission_bad_permissions(self):
|
||||
self.login(username="eventeditor", password="password")
|
||||
|
||||
response = self.client.post(
|
||||
reverse("wagtailforms:delete_submissions", args=(self.form_page.id,))
|
||||
+ f"?selected-submissions={FormSubmission.objects.first().id}"
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"wagtail_bulk_action",
|
||||
args=(
|
||||
self.form_model._meta.app_label,
|
||||
self.form_model._meta.model_name,
|
||||
"delete",
|
||||
),
|
||||
)
|
||||
+ f"?&id={FormSubmission.objects.first().id}"
|
||||
)
|
||||
|
||||
# Check that the user received a permission denied response
|
||||
self.assertRedirects(response, "/admin/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check that the deletion has not happened
|
||||
self.assertEqual(FormSubmission.objects.count(), 2)
|
||||
html = response.content.decode()
|
||||
self.assertInHTML(
|
||||
f"<p>You don't have permission to delete this item</p>",
|
||||
html,
|
||||
)
|
||||
|
||||
def test_delete_submission_after_filter_form_submissions_for_user_hook(self):
|
||||
# Hook forbids to delete form submissions for everyone
|
||||
|
@ -1267,26 +1288,24 @@ class TestDeleteFormSubmission(WagtailTestUtils, TestCase):
|
|||
with self.register_hook(
|
||||
"filter_form_submissions_for_user", construct_forms_for_user
|
||||
):
|
||||
response = self.client.post(
|
||||
reverse("wagtailforms:delete_submissions", args=(self.form_page.id,))
|
||||
+ f"?selected-submissions={FormSubmission.objects.first().id}"
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"wagtail_bulk_action",
|
||||
args=(
|
||||
self.form_model._meta.app_label,
|
||||
self.form_model._meta.model_name,
|
||||
"delete",
|
||||
),
|
||||
)
|
||||
+ f"?&id={FormSubmission.objects.first().id}"
|
||||
)
|
||||
|
||||
# An user can't delete a from submission with the hook
|
||||
self.assertRedirects(response, "/admin/")
|
||||
self.assertEqual(FormSubmission.objects.count(), 2)
|
||||
|
||||
# An user can delete a form submission without the hook
|
||||
response = self.client.post(
|
||||
reverse("wagtailforms:delete_submissions", args=(self.form_page.id,))
|
||||
+ "?selected-submissions={}".format(
|
||||
CustomFormPageSubmission.objects.first().id
|
||||
)
|
||||
)
|
||||
self.assertEqual(FormSubmission.objects.count(), 1)
|
||||
self.assertRedirects(
|
||||
response,
|
||||
reverse("wagtailforms:list_submissions", args=(self.form_page.id,)),
|
||||
self.assertEqual(response.status_code, 200)
|
||||
html = response.content.decode()
|
||||
self.assertInHTML(
|
||||
f"<p>You don't have permission to delete this item</p>",
|
||||
html,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1294,43 +1313,56 @@ class TestDeleteCustomFormSubmission(WagtailTestUtils, TestCase):
|
|||
fixtures = ["test.json"]
|
||||
|
||||
def setUp(self):
|
||||
self.form_model = CustomFormPageSubmission
|
||||
self.login(username="siteeditor", password="password")
|
||||
self.form_page = Page.objects.get(url_path="/home/contact-us-one-more-time/")
|
||||
|
||||
def test_delete_submission_show_confirmation(self):
|
||||
response = self.client.get(
|
||||
reverse("wagtailforms:delete_submissions", args=(self.form_page.id,))
|
||||
+ "?selected-submissions={}".format(
|
||||
CustomFormPageSubmission.objects.first().id
|
||||
reverse(
|
||||
"wagtail_bulk_action",
|
||||
args=(
|
||||
self.form_model._meta.app_label,
|
||||
self.form_model._meta.model_name,
|
||||
"delete",
|
||||
),
|
||||
)
|
||||
+ "?&id={}".format(CustomFormPageSubmission.objects.first().id)
|
||||
)
|
||||
|
||||
# Check show confirm page when HTTP method is GET
|
||||
self.assertTemplateUsed(response, "wagtailforms/confirm_delete.html")
|
||||
self.assertTemplateUsed(response, "bulk_actions/confirm_bulk_delete.html")
|
||||
|
||||
# Check that the deletion has not happened with GET request
|
||||
self.assertEqual(CustomFormPageSubmission.objects.count(), 2)
|
||||
|
||||
def test_delete_submission_with_permissions(self):
|
||||
response = self.client.post(
|
||||
reverse("wagtailforms:delete_submissions", args=(self.form_page.id,))
|
||||
+ "?selected-submissions={}".format(
|
||||
CustomFormPageSubmission.objects.first().id
|
||||
reverse(
|
||||
"wagtail_bulk_action",
|
||||
args=(
|
||||
self.form_model._meta.app_label,
|
||||
self.form_model._meta.model_name,
|
||||
"delete",
|
||||
),
|
||||
)
|
||||
+ "?&id={}".format(CustomFormPageSubmission.objects.first().id)
|
||||
)
|
||||
|
||||
# Check that the submission is gone
|
||||
self.assertEqual(CustomFormPageSubmission.objects.count(), 1)
|
||||
# Should be redirected to list of submissions
|
||||
self.assertRedirects(
|
||||
response,
|
||||
reverse("wagtailforms:list_submissions", args=(self.form_page.id,)),
|
||||
)
|
||||
|
||||
def test_delete_multiple_submissions_with_permissions(self):
|
||||
response = self.client.post(
|
||||
reverse("wagtailforms:delete_submissions", args=(self.form_page.id,))
|
||||
+ "?selected-submissions={}&selected-submissions={}".format(
|
||||
reverse(
|
||||
"wagtail_bulk_action",
|
||||
args=(
|
||||
self.form_model._meta.app_label,
|
||||
self.form_model._meta.model_name,
|
||||
"delete",
|
||||
),
|
||||
)
|
||||
+ "?&id={}&id={}".format(
|
||||
CustomFormPageSubmission.objects.first().id,
|
||||
CustomFormPageSubmission.objects.last().id,
|
||||
)
|
||||
|
@ -1338,27 +1370,28 @@ class TestDeleteCustomFormSubmission(WagtailTestUtils, TestCase):
|
|||
|
||||
# Check that both submissions are gone
|
||||
self.assertEqual(CustomFormPageSubmission.objects.count(), 0)
|
||||
# Should be redirected to list of submissions
|
||||
self.assertRedirects(
|
||||
response,
|
||||
reverse("wagtailforms:list_submissions", args=(self.form_page.id,)),
|
||||
)
|
||||
|
||||
def test_delete_submission_bad_permissions(self):
|
||||
self.login(username="eventeditor", password="password")
|
||||
|
||||
response = self.client.post(
|
||||
reverse("wagtailforms:delete_submissions", args=(self.form_page.id,))
|
||||
+ "?selected-submissions={}".format(
|
||||
CustomFormPageSubmission.objects.first().id
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"wagtail_bulk_action",
|
||||
args=(
|
||||
self.form_model._meta.app_label,
|
||||
self.form_model._meta.model_name,
|
||||
"delete",
|
||||
),
|
||||
)
|
||||
+ "?&id={}".format(CustomFormPageSubmission.objects.first().id)
|
||||
)
|
||||
|
||||
# Check that the user received a permission denied response
|
||||
self.assertRedirects(response, "/admin/")
|
||||
|
||||
# Check that the deletion has not happened
|
||||
self.assertEqual(CustomFormPageSubmission.objects.count(), 2)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
html = response.content.decode()
|
||||
self.assertInHTML(
|
||||
f"<p>You don't have permission to delete this item</p>",
|
||||
html,
|
||||
)
|
||||
|
||||
|
||||
class TestFormsWithCustomSubmissionsList(WagtailTestUtils, TestCase):
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from django.urls import path
|
||||
|
||||
from wagtail.contrib.forms.views import (
|
||||
DeleteSubmissionsView,
|
||||
FormPagesListView,
|
||||
get_submissions_list_view,
|
||||
)
|
||||
|
@ -21,9 +20,4 @@ urlpatterns = [
|
|||
{"results_only": True},
|
||||
name="list_submissions_results",
|
||||
),
|
||||
path(
|
||||
"submissions/<int:page_id>/delete/",
|
||||
DeleteSubmissionsView.as_view(),
|
||||
name="delete_submissions",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -45,3 +45,44 @@ def get_forms_for_user(user):
|
|||
editable_forms = fn(user, editable_forms)
|
||||
|
||||
return editable_forms
|
||||
|
||||
def get_form_submissions_as_data(
|
||||
data_fields={}, submissions=[], orderable_fields=[], ordering_by_field={}
|
||||
):
|
||||
"""
|
||||
Build data_rows as list of dicts containing id and fields and
|
||||
build data_headings as list of dicts containing id and fields
|
||||
"""
|
||||
|
||||
data_rows = []
|
||||
for submission in submissions:
|
||||
form_data = submission.get_data()
|
||||
data_row = []
|
||||
for name, label in data_fields:
|
||||
val = form_data.get(name)
|
||||
if isinstance(val, list):
|
||||
val = ", ".join(val)
|
||||
data_row.append(val)
|
||||
data_rows.append({"id": submission.id, "fields": data_row})
|
||||
|
||||
data_headings = []
|
||||
for name, label in data_fields:
|
||||
order_label = None
|
||||
if name in orderable_fields:
|
||||
order = ordering_by_field.get(name)
|
||||
if order:
|
||||
order_label = order[1] # 'ascending' or 'descending'
|
||||
else:
|
||||
order_label = "orderable" # not ordered yet but can be
|
||||
data_headings.append(
|
||||
{
|
||||
"name": name,
|
||||
"label": label,
|
||||
"order": order_label,
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
data_headings,
|
||||
data_rows,
|
||||
)
|
|
@ -3,19 +3,17 @@ from collections import OrderedDict
|
|||
|
||||
from django.contrib.admin.utils import quote
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext, gettext_lazy, ngettext
|
||||
from django.views.generic import TemplateView
|
||||
from django.utils.translation import gettext, gettext_lazy
|
||||
from django_filters import DateFromToRangeFilter
|
||||
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.filters import DateRangePickerWidget, WagtailFilterSet
|
||||
from wagtail.admin.ui.tables import Column, TitleColumn
|
||||
from wagtail.admin.views import generic
|
||||
from wagtail.admin.views.generic.base import BaseListingView
|
||||
from wagtail.admin.views.mixins import SpreadsheetExportMixin
|
||||
from wagtail.contrib.forms.utils import get_forms_for_user
|
||||
from wagtail.contrib.forms.utils import get_form_submissions_as_data, get_forms_for_user
|
||||
from wagtail.models import Page
|
||||
|
||||
|
||||
|
@ -81,69 +79,6 @@ class FormPagesListView(generic.IndexView):
|
|||
return get_forms_for_user(self.request.user).select_related("content_type")
|
||||
|
||||
|
||||
class DeleteSubmissionsView(TemplateView):
|
||||
"""Delete the selected submissions"""
|
||||
|
||||
template_name = "wagtailforms/confirm_delete.html"
|
||||
page = None
|
||||
submissions = None
|
||||
success_url = "wagtailforms:list_submissions"
|
||||
|
||||
def get_queryset(self):
|
||||
"""Returns a queryset for the selected submissions"""
|
||||
submission_ids = self.request.GET.getlist("selected-submissions")
|
||||
submission_class = self.page.get_submission_class()
|
||||
return submission_class._default_manager.filter(id__in=submission_ids)
|
||||
|
||||
def handle_delete(self, submissions):
|
||||
"""Deletes the given queryset"""
|
||||
count = submissions.count()
|
||||
submissions.delete()
|
||||
messages.success(
|
||||
self.request,
|
||||
ngettext(
|
||||
"One submission has been deleted.",
|
||||
"%(count)d submissions have been deleted.",
|
||||
count,
|
||||
)
|
||||
% {"count": count},
|
||||
)
|
||||
|
||||
def get_success_url(self):
|
||||
"""Returns the success URL to redirect to after a successful deletion"""
|
||||
return self.success_url
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""Check permissions, set the page and submissions, handle delete"""
|
||||
page_id = kwargs.get("page_id")
|
||||
|
||||
if not get_forms_for_user(self.request.user).filter(id=page_id).exists():
|
||||
raise PermissionDenied
|
||||
|
||||
self.page = get_object_or_404(Page, id=page_id).specific
|
||||
|
||||
self.submissions = self.get_queryset()
|
||||
|
||||
if self.request.method == "POST":
|
||||
self.handle_delete(self.submissions)
|
||||
return redirect(self.get_success_url(), page_id)
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Get the context for this view"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context.update(
|
||||
{
|
||||
"page": self.page,
|
||||
"submissions": self.submissions,
|
||||
}
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class SubmissionsListFilterSet(WagtailFilterSet):
|
||||
date = DateFromToRangeFilter(
|
||||
label=gettext_lazy("Submission date"),
|
||||
|
@ -280,43 +215,22 @@ class SubmissionsListView(SpreadsheetExportMixin, BaseListingView):
|
|||
data_fields = self.form_page.get_data_fields()
|
||||
data_rows = []
|
||||
context["submissions"] = submissions
|
||||
context["page_title"] = self.page_title
|
||||
if not self.is_export:
|
||||
# Build data_rows as list of dicts containing model_id and fields
|
||||
for submission in submissions:
|
||||
form_data = submission.get_data()
|
||||
data_row = []
|
||||
for name, label in data_fields:
|
||||
val = form_data.get(name)
|
||||
if isinstance(val, list):
|
||||
val = ", ".join(val)
|
||||
data_row.append(val)
|
||||
data_rows.append({"model_id": submission.id, "fields": data_row})
|
||||
# Build data_headings as list of dicts containing model_id and fields
|
||||
ordering_by_field = self.get_validated_ordering()
|
||||
orderable_fields = self.orderable_fields
|
||||
data_headings = []
|
||||
for name, label in data_fields:
|
||||
order_label = None
|
||||
if name in orderable_fields:
|
||||
order = ordering_by_field.get(name)
|
||||
if order:
|
||||
order_label = order[1] # 'ascending' or 'descending'
|
||||
else:
|
||||
order_label = "orderable" # not ordered yet but can be
|
||||
data_headings.append(
|
||||
{
|
||||
"name": name,
|
||||
"label": label,
|
||||
"order": order_label,
|
||||
}
|
||||
)
|
||||
(data_headings, data_rows) = get_form_submissions_as_data(
|
||||
data_fields=data_fields,
|
||||
submissions=submissions,
|
||||
orderable_fields=self.orderable_fields,
|
||||
ordering_by_field=self.get_validated_ordering(),
|
||||
)
|
||||
|
||||
context.update(
|
||||
{
|
||||
"app_label": "wagtailforms",
|
||||
"model_name": "formsubmission",
|
||||
"form_page": self.form_page,
|
||||
"data_headings": data_headings,
|
||||
"data_rows": data_rows,
|
||||
}
|
||||
)
|
||||
|
||||
return context
|
||||
return context
|
|
@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
from wagtail import hooks
|
||||
from wagtail.admin.menu import MenuItem
|
||||
from wagtail.contrib.forms import urls
|
||||
from wagtail.contrib.forms.bulk_actions.delete import DeleteBulkAction
|
||||
from wagtail.contrib.forms.utils import get_forms_for_user
|
||||
|
||||
|
||||
|
@ -29,3 +30,7 @@ def register_forms_menu_item():
|
|||
icon_name="form",
|
||||
order=700,
|
||||
)
|
||||
|
||||
|
||||
for action_class in [DeleteBulkAction]:
|
||||
hooks.register("register_bulk_action", action_class)
|
||||
|
|
|
@ -1681,6 +1681,22 @@ class TestImageChooserView(WagtailTestUtils, TestCase):
|
|||
response = self.get({"p": 9999})
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@override_settings(WAGTAILIMAGES_CHOOSER_PAGE_SIZE=4)
|
||||
def test_chooser_page_size(self):
|
||||
images = [
|
||||
Image(
|
||||
title="Test image %i" % i,
|
||||
file=get_test_image_file(size=(1, 1)),
|
||||
)
|
||||
for i in range(1, 12)
|
||||
]
|
||||
Image.objects.bulk_create(images)
|
||||
|
||||
response = self.get()
|
||||
|
||||
self.assertContains(response, "Page 1 of 3")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_filter_by_tag(self):
|
||||
for i in range(0, 10):
|
||||
image = Image.objects.create(
|
||||
|
|
|
@ -72,10 +72,15 @@ class ImageCreationFormMixin(CreationFormMixin):
|
|||
class BaseImageChooseView(BaseChooseView):
|
||||
template_name = "wagtailimages/chooser/chooser.html"
|
||||
results_template_name = "wagtailimages/chooser/results.html"
|
||||
per_page = 12
|
||||
ordering = "-created_at"
|
||||
construct_queryset_hook_name = "construct_image_chooser_queryset"
|
||||
|
||||
@property
|
||||
def per_page(self):
|
||||
# Make per_page into a property so that we can read back WAGTAILIMAGES_CHOOSER_PAGE_SIZE
|
||||
# at runtime.
|
||||
return getattr(settings, "WAGTAILIMAGES_CHOOSER_PAGE_SIZE", 20)
|
||||
|
||||
def get_object_list(self):
|
||||
return (
|
||||
permission_policy.instances_user_has_any_permission_for(
|
||||
|
@ -309,7 +314,6 @@ class ImageChooserViewSet(ChooserViewSet):
|
|||
preserve_url_parameters = ChooserViewSet.preserve_url_parameters + ["select_format"]
|
||||
|
||||
icon = "image"
|
||||
per_page = getattr(settings, "WAGTAILIMAGES_CHOOSER_PAGE_SIZE", 10)
|
||||
choose_one_text = _("Choose an image")
|
||||
create_action_label = _("Upload")
|
||||
create_action_clicked_label = _("Uploading…")
|
||||
|
|
Ładowanie…
Reference in New Issue