Allow custom image model to have fields defined as required

If validation rules prevent the multiple image upload view from
creating Image objects from just the image file, an UploadedImage object is
created instead, and turned into an image once the form is filled in.

* Fixes #847
* Add UploadedImage model and related views
* Update custom image model docs
pull/5972/head
Matt Westcott 2019-04-26 17:19:04 +01:00 zatwierdzone przez LB
rodzic ef0c8f3446
commit 4023a90d6e
11 zmienionych plików z 457 dodań i 20 usunięć

Wyświetl plik

@ -27,6 +27,7 @@ Changelog
* Added `select_related` attribute to site settings to enable more efficient fetching of foreign key values (Andy Babic)
* Add caching of image renditions (Tom Dyson, Tim Kamanin)
* Add documentation for reporting security issues (Matt Westcott)
* Fields on a custom image model can now be defined as required `blank=False` (Matt Westcott)
* Fix: Added ARIA alert role to live search forms in the admin (Casper Timmers)
* Fix: Reorder login form elements to match expected tab order (Kjartan Sverrisson)
* Fix: Re-add 'Close Explorer' button on mobile viewports (Sævar Öfjörð Magnússon)

Wyświetl plik

@ -47,12 +47,9 @@ Here's an example:
)
.. note::
.. versionchanged:: 2.9
Fields defined on a custom image model must either be set as non-required
(``blank=True``), or specify a default value - this is because uploading
the image and entering custom data happen as two separate actions, and
Wagtail needs to be able to create an image record immediately on upload.
Fields on a custom image model can now be defined as required (``blank=False``).
Then set the ``WAGTAILIMAGES_IMAGE_MODEL`` setting to point to it:

Wyświetl plik

@ -40,6 +40,7 @@ Other features
* Added ``select_related`` attribute to site settings to enable more efficient fetching of foreign key values (Andy Babic)
* Add caching of image renditions (Tom Dyson, Tim Kamanin)
* Add documentation for reporting security issues (Matt Westcott)
* Fields on a custom image model can now be defined as required ``blank=False`` (Matt Westcott)
Bug fixes

Wyświetl plik

@ -15,7 +15,9 @@ urlpatterns = [
url(r'^multiple/add/$', multiple.add, name='add_multiple'),
url(r'^multiple/(\d+)/$', multiple.edit, name='edit_multiple'),
url(r'^multiple/create_from_uploaded_image/(\d+)/$', multiple.create_from_uploaded_image, name='create_multiple_from_uploaded_image'),
url(r'^multiple/(\d+)/delete/$', multiple.delete, name='delete_multiple'),
url(r'^multiple/delete_upload/(\d+)/$', multiple.delete_upload, name='delete_upload_multiple'),
url(r'^chooser/$', chooser.chooser, name='chooser'),
url(r'^chooser/(\d+)/$', chooser.image_chosen, name='image_chosen'),

Wyświetl plik

@ -0,0 +1,24 @@
# Generated by Django 2.2 on 2019-04-30 10:09
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),
('wagtailimages', '0001_squashed_0021'),
]
operations = [
migrations.CreateModel(
name='UploadedImage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.ImageField(max_length=200, upload_to='uploaded_images')),
('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')),
],
),
]

Wyświetl plik

@ -572,3 +572,17 @@ class Rendition(AbstractRendition):
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
)

Wyświetl plik

@ -1,6 +1,6 @@
{% load i18n %}
<form action="{% url 'wagtailimages:edit_multiple' image.id %}" method="POST" enctype="multipart/form-data" novalidate>
<form action="{{ edit_action }}" method="POST" enctype="multipart/form-data" novalidate>
<ul class="fields">
{% csrf_token %}
{% for field in form %}
@ -12,7 +12,7 @@
{% endfor %}
<li>
<input type="submit" value="{% trans 'Update' %}" class="button" />
<a href="{% url 'wagtailimages:delete_multiple' image.id %}" class="delete button button-secondary no">{% trans "Delete" %}</a>
<a href="{{ delete_action }}" class="delete button button-secondary no">{% trans "Delete" %}</a>
</li>
</ul>
</form>

Wyświetl plik

@ -9,8 +9,9 @@ from django.urls import reverse
from django.utils.http import RFC3986_SUBDELIMS, urlquote
from wagtail.core.models import Collection, GroupCollectionPermission
from wagtail.images.models import UploadedImage
from wagtail.images.views.serve import generate_signature
from wagtail.tests.testapp.models import CustomImage
from wagtail.tests.testapp.models import CustomImage, CustomImageWithAuthor
from wagtail.tests.utils import WagtailTestUtils
from .utils import Image, get_test_image_file
@ -1082,13 +1083,13 @@ class TestMultipleImageUploader(TestCase, WagtailTestUtils):
# Check form
self.assertIn('form', response.context)
self.assertEqual(response.context['form'].initial['title'], 'test.png')
self.assertEqual(response.context['edit_action'], '/admin/images/multiple/%d/' % response.context['image'].id)
self.assertEqual(response.context['delete_action'], '/admin/images/multiple/%d/delete/' % response.context['image'].id)
# Check JSON
response_json = json.loads(response.content.decode())
self.assertIn('image_id', response_json)
self.assertIn('form', response_json)
self.assertIn('success', response_json)
self.assertEqual(response_json['image_id'], response.context['image'].id)
self.assertTrue(response_json['success'])
def test_add_post_noajax(self):
@ -1111,7 +1112,7 @@ class TestMultipleImageUploader(TestCase, WagtailTestUtils):
def test_add_post_badfile(self):
"""
This tests that the add view checks for a file when a user POSTs to it
The add view must check that the uploaded file is a valid image
"""
response = self.client.post(reverse('wagtailimages:add_multiple'), {
'files[]': SimpleUploadedFile('test.png', b"This is not an image!"),
@ -1149,7 +1150,7 @@ class TestMultipleImageUploader(TestCase, WagtailTestUtils):
# Send request
response = self.client.post(reverse('wagtailimages:edit_multiple', args=(self.image.id, )), {
('image-%d-title' % self.image.id): "New title!",
('image-%d-tags' % self.image.id): "",
('image-%d-tags' % self.image.id): "cromarty, finisterre",
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Check response
@ -1164,6 +1165,11 @@ class TestMultipleImageUploader(TestCase, WagtailTestUtils):
self.assertEqual(response_json['image_id'], self.image.id)
self.assertTrue(response_json['success'])
# test that changes have been applied to the image
image = Image.objects.get(id=self.image.id)
self.assertEqual(image.title, "New title!")
self.assertIn('cromarty', image.tags.names())
def test_edit_post_noajax(self):
"""
This tests that a POST request to the edit view without AJAX returns a 400 response
@ -1301,15 +1307,38 @@ class TestMultipleImageUploaderWithCustomImageModel(TestCase, WagtailTestUtils):
self.assertEqual(response.context['form'].initial['title'], 'test.png')
self.assertIn('caption', response.context['form'].fields)
self.assertNotIn('not_editable_field', response.context['form'].fields)
self.assertEqual(response.context['edit_action'], '/admin/images/multiple/%d/' % response.context['image'].id)
self.assertEqual(response.context['delete_action'], '/admin/images/multiple/%d/delete/' % response.context['image'].id)
# Check JSON
response_json = json.loads(response.content.decode())
self.assertIn('image_id', response_json)
self.assertIn('form', response_json)
self.assertIn('success', response_json)
self.assertEqual(response_json['image_id'], response.context['image'].id)
self.assertTrue(response_json['success'])
def test_add_post_badfile(self):
"""
The add view must check that the uploaded file is a valid image
"""
response = self.client.post(reverse('wagtailimages:add_multiple'), {
'files[]': SimpleUploadedFile('test.png', b"This is not an image!"),
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Check response
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
# Check JSON
response_json = json.loads(response.content.decode())
self.assertNotIn('image_id', response_json)
self.assertNotIn('form', response_json)
self.assertIn('success', response_json)
self.assertIn('error_message', response_json)
self.assertFalse(response_json['success'])
self.assertEqual(
response_json['error_message'], "Not a supported image format. Supported formats: GIF, JPEG, PNG, WEBP."
)
def test_edit_post(self):
"""
This tests that a POST request to the edit view edits the image
@ -1317,7 +1346,7 @@ class TestMultipleImageUploaderWithCustomImageModel(TestCase, WagtailTestUtils):
# Send request
response = self.client.post(reverse('wagtailimages:edit_multiple', args=(self.image.id, )), {
('image-%d-title' % self.image.id): "New title!",
('image-%d-tags' % self.image.id): "",
('image-%d-tags' % self.image.id): "footwear, dystopia",
('image-%d-caption' % self.image.id): "a boot stamping on a human face, forever",
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
@ -1337,6 +1366,7 @@ class TestMultipleImageUploaderWithCustomImageModel(TestCase, WagtailTestUtils):
new_image = CustomImage.objects.get(id=self.image.id)
self.assertEqual(new_image.title, "New title!")
self.assertEqual(new_image.caption, "a boot stamping on a human face, forever")
self.assertIn('footwear', new_image.tags.names())
def test_delete_post(self):
"""
@ -1365,6 +1395,196 @@ class TestMultipleImageUploaderWithCustomImageModel(TestCase, WagtailTestUtils):
self.assertEqual(CustomImage.objects.filter(id=self.image.id).count(), 0)
@override_settings(WAGTAILIMAGES_IMAGE_MODEL='tests.CustomImageWithAuthor')
class TestMultipleImageUploaderWithCustomRequiredFields(TestCase, WagtailTestUtils):
"""
This tests the multiple image upload views located in wagtailimages/views/multiple.py
with a custom image model
"""
def setUp(self):
self.user = self.login()
# Create an UploadedImage for running tests on
self.uploaded_image = UploadedImage.objects.create(
file=get_test_image_file(),
uploaded_by_user=self.user,
)
def test_add(self):
"""
This tests that the add view responds correctly on a GET request
"""
# Send request
response = self.client.get(reverse('wagtailimages:add_multiple'))
# Check response
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailimages/multiple/add.html')
def test_add_post(self):
"""
A POST request to the add view should create an UploadedImage 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()
response = self.client.post(reverse('wagtailimages:add_multiple'), {
'files[]': SimpleUploadedFile('test.png', get_test_image_file().file.getvalue()),
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
image_count_after = CustomImageWithAuthor.objects.count()
uploaded_image_count_after = UploadedImage.objects.count()
# an UploadedImage 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)
# Check response
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
self.assertTemplateUsed(response, 'wagtailimages/multiple/edit_form.html')
# Check image
self.assertIn('uploaded_image', response.context)
self.assertTrue(response.context['uploaded_image'].file.name)
# Check form
self.assertIn('form', response.context)
self.assertEqual(response.context['form'].initial['title'], 'test.png')
self.assertIn('author', response.context['form'].fields)
self.assertEqual(response.context['edit_action'], '/admin/images/multiple/create_from_uploaded_image/%d/' % response.context['uploaded_image'].id)
self.assertEqual(response.context['delete_action'], '/admin/images/multiple/delete_upload/%d/' % response.context['uploaded_image'].id)
# Check JSON
response_json = json.loads(response.content.decode())
self.assertIn('form', response_json)
self.assertIn('success', response_json)
self.assertTrue(response_json['success'])
def test_add_post_badfile(self):
"""
The add view must check that the uploaded file is a valid image
"""
response = self.client.post(reverse('wagtailimages:add_multiple'), {
'files[]': SimpleUploadedFile('test.png', b"This is not an image!"),
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Check response
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
# Check JSON
response_json = json.loads(response.content.decode())
self.assertNotIn('image_id', response_json)
self.assertNotIn('form', response_json)
self.assertIn('success', response_json)
self.assertIn('error_message', response_json)
self.assertFalse(response_json['success'])
self.assertEqual(
response_json['error_message'], "Not a supported image format. Supported formats: GIF, JPEG, PNG, WEBP."
)
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
"""
image_count_before = CustomImageWithAuthor.objects.count()
uploaded_image_count_before = UploadedImage.objects.count()
# Send request
response = self.client.post(reverse('wagtailimages:create_multiple_from_uploaded_image', args=(self.uploaded_image.id, )), {
('uploaded-image-%d-title' % self.uploaded_image.id): "New title!",
('uploaded-image-%d-tags' % self.uploaded_image.id): "",
('uploaded-image-%d-author' % self.uploaded_image.id): "",
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
image_count_after = CustomImageWithAuthor.objects.count()
uploaded_image_count_after = UploadedImage.objects.count()
# no changes to image / UploadedImage count
self.assertEqual(image_count_after, image_count_before)
self.assertEqual(uploaded_image_count_after, uploaded_image_count_before)
# Check response
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
# Check form
self.assertIn('form', response.context)
self.assertIn('author', response.context['form'].fields)
self.assertEqual(response.context['edit_action'], '/admin/images/multiple/create_from_uploaded_image/%d/' % response.context['uploaded_image'].id)
self.assertEqual(response.context['delete_action'], '/admin/images/multiple/delete_upload/%d/' % response.context['uploaded_image'].id)
self.assertFormError(response, 'form', 'author', "This field is required.")
# Check JSON
response_json = json.loads(response.content.decode())
self.assertIn('form', response_json)
self.assertIn('New title!', response_json['form'])
self.assertFalse(response_json['success'])
def test_create_from_upload(self):
"""
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()
# Send request
response = self.client.post(reverse('wagtailimages:create_multiple_from_uploaded_image', args=(self.uploaded_image.id, )), {
('uploaded-image-%d-title' % self.uploaded_image.id): "New title!",
('uploaded-image-%d-tags' % self.uploaded_image.id): "abstract, squares",
('uploaded-image-%d-author' % self.uploaded_image.id): "Piet Mondrian",
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
image_count_after = CustomImageWithAuthor.objects.count()
uploaded_image_count_after = UploadedImage.objects.count()
# Check response
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
# Check JSON
response_json = json.loads(response.content.decode())
self.assertIn('image_id', response_json)
self.assertTrue(response_json['success'])
# Image should have been created, UploadedImage deleted
self.assertEqual(image_count_after, image_count_before + 1)
self.assertEqual(uploaded_image_count_after, uploaded_image_count_before - 1)
image = CustomImageWithAuthor.objects.get(id=response_json['image_id'])
self.assertEqual(image.title, 'New title!')
self.assertEqual(image.author, 'Piet Mondrian')
self.assertTrue(image.file.name)
self.assertTrue(image.file_hash)
self.assertTrue(image.file_size)
self.assertEqual(image.width, 640)
self.assertEqual(image.height, 480)
self.assertIn('abstract', image.tags.names())
def test_delete_uploaded_image(self):
"""
This tests that a POST request to the delete view deletes the UploadedImage
"""
# Send request
response = self.client.post(reverse(
'wagtailimages:delete_upload_multiple', args=(self.uploaded_image.id, )
), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Check response
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
# Make sure the image is deleted
self.assertFalse(CustomImageWithAuthor.objects.filter(id=self.uploaded_image.id).exists())
# Check JSON
response_json = json.loads(response.content.decode())
self.assertTrue(response_json['success'])
class TestURLGeneratorView(TestCase, WagtailTestUtils):
def setUp(self):
# Create an image for running tests on

Wyświetl plik

@ -1,8 +1,10 @@
import os.path
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseBadRequest, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.template.loader import render_to_string
from django.utils.encoding import force_str
from django.urls import reverse
from django.views.decorators.http import require_POST
from django.views.decorators.vary import vary_on_headers
@ -11,6 +13,7 @@ from wagtail.core.models import Collection
from wagtail.images import get_image_model
from wagtail.images.fields import ALLOWED_EXTENSIONS
from wagtail.images.forms import get_image_form
from wagtail.images.models import UploadedImage
from wagtail.images.permissions import permission_policy
from wagtail.search.backends import get_search_backends
@ -79,18 +82,39 @@ def add(request):
'image_id': int(image.id),
'form': render_to_string('wagtailimages/multiple/edit_form.html', {
'image': image,
'edit_action': reverse('wagtailimages:edit_multiple', args=(image.id,)),
'delete_action': reverse('wagtailimages:delete_multiple', args=(image.id,)),
'form': get_image_edit_form(Image)(
instance=image, prefix='image-%d' % image.id, user=request.user
),
}, request=request),
})
else:
# Validation error
elif 'file' in form.errors:
# The uploaded file is invalid; reject it now
return JsonResponse({
'success': False,
'error_message': '\n'.join(form.errors['file']),
})
else:
# Some other field of the image form has failed validation, e.g. a required metadata field
# on a custom image model. Store the image as an UploadedImage instead and present the
# edit form so that it will become a proper Image when successfully filled in
uploaded_image = UploadedImage.objects.create(
file=request.FILES['files[]'], uploaded_by_user=request.user
)
image = Image(title=request.FILES['files[]'].name, collection_id=request.POST.get('collection'))
# https://github.com/django/django/blob/stable/1.6.x/django/forms/util.py#L45
'error_message': '\n'.join(['\n'.join([force_str(i) for i in v]) for k, v in form.errors.items()]),
return JsonResponse({
'success': True,
'uploaded_image_id': uploaded_image.id,
'form': render_to_string('wagtailimages/multiple/edit_form.html', {
'uploaded_image': uploaded_image,
'edit_action': reverse('wagtailimages:create_multiple_from_uploaded_image', args=(uploaded_image.id,)),
'delete_action': reverse('wagtailimages:delete_upload_multiple', args=(uploaded_image.id,)),
'form': get_image_edit_form(Image)(
instance=image, prefix='uploaded-image-%d' % uploaded_image.id, user=request.user
),
}, request=request),
})
else:
# Instantiate a dummy copy of the form that we can retrieve validation messages and media from;
@ -142,6 +166,8 @@ def edit(request, image_id, callback=None):
'image_id': int(image_id),
'form': render_to_string('wagtailimages/multiple/edit_form.html', {
'image': image,
'edit_action': reverse('wagtailimages:edit_multiple', args=(image_id,)),
'delete_action': reverse('wagtailimages:delete_multiple', args=(image_id,)),
'form': form,
}, request=request),
})
@ -163,3 +189,75 @@ def delete(request, image_id):
'success': True,
'image_id': int(image_id),
})
@require_POST
def create_from_uploaded_image(request, uploaded_image_id):
Image = get_image_model()
ImageForm = get_image_edit_form(Image)
uploaded_image = get_object_or_404(UploadedImage, id=uploaded_image_id)
if not request.is_ajax():
return HttpResponseBadRequest("Cannot POST to this view without AJAX")
if uploaded_image.uploaded_by_user != request.user:
raise PermissionDenied
image = Image()
form = ImageForm(
request.POST, request.FILES, instance=image, prefix='uploaded-image-' + uploaded_image_id, user=request.user
)
if form.is_valid():
# assign the file content from uploaded_image to the image object, to ensure it gets saved to
# Image's storage
image.file.save(os.path.basename(uploaded_image.file.name), uploaded_image.file.file, save=False)
image.uploaded_by_user = request.user
image.file_size = image.file.size
image.file.open()
image.file.seek(0)
image._set_file_hash(image.file.read())
image.file.seek(0)
form.save()
uploaded_image.file.delete()
uploaded_image.delete()
# Reindex the image to make sure all tags are indexed
for backend in get_search_backends():
backend.add(image)
return JsonResponse({
'success': True,
'image_id': image.id,
})
else:
return JsonResponse({
'success': False,
'form': render_to_string('wagtailimages/multiple/edit_form.html', {
'uploaded_image': uploaded_image,
'edit_action': reverse('wagtailimages:create_multiple_from_uploaded_image', args=(uploaded_image.id,)),
'delete_action': reverse('wagtailimages:delete_upload_multiple', args=(uploaded_image.id,)),
'form': form,
}, request=request),
})
@require_POST
def delete_upload(request, uploaded_image_id):
uploaded_image = get_object_or_404(UploadedImage, id=uploaded_image_id)
if not request.is_ajax():
return HttpResponseBadRequest("Cannot POST to this view without AJAX")
if uploaded_image.uploaded_by_user != request.user:
raise PermissionDenied
uploaded_image.file.delete()
uploaded_image.delete()
return JsonResponse({
'success': True,
})

Wyświetl plik

@ -0,0 +1,62 @@
# Generated by Django 2.2 on 2019-04-26 15:48
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
import wagtail.core.models
import wagtail.images.models
import wagtail.search.index
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('wagtailcore', '0041_group_collection_permissions_verbose_name_plural'),
('taggit', '0002_auto_20150616_2121'),
('tests', '0049_rawhtmlblock'),
]
operations = [
migrations.CreateModel(
name='CustomImageWithAuthor',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='title')),
('file', models.ImageField(height_field='height', upload_to=wagtail.images.models.get_upload_to, verbose_name='file', width_field='width')),
('width', models.IntegerField(editable=False, verbose_name='width')),
('height', models.IntegerField(editable=False, verbose_name='height')),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='created at')),
('focal_point_x', models.PositiveIntegerField(blank=True, null=True)),
('focal_point_y', models.PositiveIntegerField(blank=True, null=True)),
('focal_point_width', models.PositiveIntegerField(blank=True, null=True)),
('focal_point_height', models.PositiveIntegerField(blank=True, null=True)),
('file_size', models.PositiveIntegerField(editable=False, null=True)),
('file_hash', models.CharField(blank=True, editable=False, max_length=40)),
('author', models.CharField(max_length=255)),
('collection', models.ForeignKey(default=wagtail.core.models.get_root_collection_id, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.Collection', verbose_name='collection')),
('tags', taggit.managers.TaggableManager(blank=True, help_text=None, through='taggit.TaggedItem', to='taggit.Tag', verbose_name='tags')),
('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')),
],
options={
'abstract': False,
},
bases=(wagtail.search.index.Indexed, models.Model),
),
migrations.CreateModel(
name='CustomRenditionWithAuthor',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('filter_spec', models.CharField(db_index=True, max_length=255)),
('file', models.ImageField(height_field='height', upload_to=wagtail.images.models.get_rendition_upload_to, width_field='width')),
('width', models.IntegerField(editable=False)),
('height', models.IntegerField(editable=False)),
('focal_point_key', models.CharField(blank=True, default='', editable=False, max_length=16)),
('image', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='renditions', to='tests.CustomImageWithAuthor')),
],
options={
'unique_together': {('image', 'filter_spec', 'focal_point_key')},
},
),
]

Wyświetl plik

@ -932,6 +932,24 @@ class CustomRendition(AbstractRendition):
)
# Custom image model with a required field
class CustomImageWithAuthor(AbstractImage):
author = models.CharField(max_length=255)
admin_form_fields = Image.admin_form_fields + (
'author',
)
class CustomRenditionWithAuthor(AbstractRendition):
image = models.ForeignKey(CustomImageWithAuthor, related_name='renditions', on_delete=models.CASCADE)
class Meta:
unique_together = (
('image', 'filter_spec', 'focal_point_key'),
)
class CustomDocument(AbstractDocument):
description = models.TextField(blank=True)
fancy_description = RichTextField(blank=True)