Porównaj commity

...

8 Commity

Autor SHA1 Wiadomość Data
Rohit Sharma f04c9ff8a7
Merge ce967a404a into a09bba67cd 2024-05-09 09:39:56 +01:00
Matt Westcott a09bba67cd
Refactor image chooser pagination to check WAGTAILIMAGES_CHOOSER_PAGE_SIZE at runtime 2024-05-09 09:38:54 +01:00
Matt Westcott 6fa3985674 Release note for #11926 2024-05-08 12:34:39 +01:00
Jake Howard 84d9bd6fb6 Mention use of GitHub's security advisories 2024-05-08 12:34:39 +01:00
Jake Howard 37f9ae2ec6 Add note about bug bounties 2024-05-08 12:34:39 +01:00
rohitsrma ce967a404a Update tests 2024-04-16 01:03:59 +05:30
rohitsrma 28e68f3882 Add permission check 2024-04-14 20:04:13 +05:30
rohitsrma 186a9fc128 Implement bulk actions for form submissions 2024-04-14 03:01:25 +05:30
20 zmienionych plików z 345 dodań i 216 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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.

Wyświetl plik

@ -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

Wyświetl plik

@ -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):

Wyświetl plik

@ -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.

Wyświetl plik

@ -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}

Wyświetl plik

@ -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

Wyświetl plik

@ -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 %}

Wyświetl plik

@ -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>

Wyświetl plik

@ -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 %}

Wyświetl plik

@ -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 %}

Wyświetl plik

@ -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 %}

Wyświetl plik

@ -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 %}

Wyświetl plik

@ -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):

Wyświetl plik

@ -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",
),
]

Wyświetl plik

@ -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,
)

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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(

Wyświetl plik

@ -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…")