from django import forms
from django.core.exceptions import ValidationError
from django.http import HttpResponse
import base64
import io
from PIL import ImageFile
from postgresqleu.util.storage import InlineEncodedStorage
from postgresqleu.util.forms import IntegerBooleanField
from postgresqleu.util.widgets import StaticTextWidget
from postgresqleu.util.validators import color_validator
from postgresqleu.confsponsor.backendforms import BackendSponsorshipLevelBenefitForm
from .base import BaseBenefit, BaseBenefitForm
class ImageUploadForm(BaseBenefitForm):
decline = forms.BooleanField(label='Decline this benefit', required=False)
image = forms.FileField(label='Image file', required=False)
uploadedimage = forms.CharField(label='Uploaed', widget=forms.HiddenInput, required=False)
preview = forms.CharField(label='Preview', required=False, widget=StaticTextWidget)
confirm = forms.BooleanField(label='Confirm preview', required=False)
def __init__(self, *args, **kwargs):
super(ImageUploadForm, self).__init__(*args, **kwargs)
self.fields['image'].help_text = "Upload a file in %s format, fitting in a box of %sx%s pixels." % (self.params['format'].upper(), self.params['xres'], self.params['yres'])
self.fields['image'].widget.attrs['accept'] = 'image/png'
if not self.is_bound:
self._delete_stage2_fields()
def _delete_stage2_fields(self):
del self.fields['uploadedimage']
del self.fields['preview']
del self.fields['confirm']
def clean(self):
declined = self.cleaned_data.get('decline', False)
if declined and self.cleaned_data.get('image', None):
self._delete_stage2_fields()
raise ValidationError('You cannot both decline and upload an image at the same time.')
if declined:
self._delete_stage2_fields()
else:
if self.cleaned_data.get('image', None) or self.cleaned_data.get('uploadedimage', None):
# We have an image. Prepare a preview field if it's not already confirmed
self.data = self.data.copy()
if self.cleaned_data.get('image', None):
self.cleaned_data['image'].seek(0)
imgdata = base64.b64encode(self.cleaned_data['image'].read()).decode('ascii')
imgtag = "data:image/png;base64,{}".format(imgdata)
self.data['uploadedimage'] = imgdata
else:
imgtag = "data:image/png;base64,{}".format(self.cleaned_data['uploadedimage'])
if self.params.get('previewbackground', None):
self.data['preview'] = '
'.format(imgtag, imgtag, self.params.get('previewbackground'))
else:
self.data['preview'] = ''.format(imgtag)
# Remove the image field now that we have transferred things to imagedata. Also remove the ability to decline.
del self.fields['image']
del self.fields['decline']
# And finally, check if we've already confirmed
if not self.cleaned_data.get('confirm', None):
self.add_error('confirm', "You must confirm the image looks OK in the preview before you can proceed.{}".format(
' In particular, verify the effect of transparency on the given background color.' if self.params.get('previewbackground') else '',
))
else:
# If we don't have an image it either wasn't specified, or the image validator removed it
if 'image' not in self._errors:
# Unless there is an error already flagged in the clean_image method
self._errors['image'] = self.error_class(['This field is required'])
self._delete_stage2_fields()
return self.cleaned_data
def clean_image(self):
if not self.cleaned_data.get('image', None):
# This check is done in the global clean as well, so we accept it here since
# we might have decliend it.
return None
imagedata = self.cleaned_data['image']
try:
p = ImageFile.Parser()
p.feed(imagedata.read())
p.close()
image = p.image
except Exception as e:
raise ValidationError("Could not parse image: %s" % e)
if image.format != self.params['format'].upper():
raise ValidationError("Only %s format images are accepted, not '%s'" % (self.params['format'].upper(), image.format))
xres = int(self.params['xres'])
yres = int(self.params['yres'])
resstr = "%sx%s" % (xres, yres)
upresstr = "%sx%s" % image.size
# Check maximum resolution
if image.size[0] > xres or image.size[1] > yres:
raise ValidationError("Maximum size of image is %s. Uploaded image is %s." % (resstr, upresstr))
# One of the sizes has to be exactly what the spec says, otherwise we might have an image that's
# too small.
if image.size[0] != xres and image.size[1] != yres:
raise ValidationError("Image must be %s pixels wide or %s pixels high. Uploaded image is %s." % (xres, yres, upresstr))
if int(self.params.get('transparent', 0)) == 1:
# Require transparency, only supported for PNG
if self.params['format'].upper() != 'PNG':
raise ValidationError("Transparency validation requires PNG images")
if image.mode != 'RGBA':
raise ValidationError("Image must have transparent background")
return self.cleaned_data['image']
class ImageUploadBackendForm(BackendSponsorshipLevelBenefitForm):
format = forms.ChoiceField(label="Image format", choices=(('PNG', 'PNG'), ))
xres = forms.IntegerField(label="X resolution")
yres = forms.IntegerField(label="Y resolution")
transparent = IntegerBooleanField(label="Require transparent", required=False)
previewbackground = forms.CharField(max_length=20, required=False,
label='Preview background',
validators=[color_validator, ],
help_text="Background color used in preview",
)
class_param_fields = ['format', 'xres', 'yres', 'transparent', 'previewbackground']
class ImageUpload(BaseBenefit):
@classmethod
def get_backend_form(self):
return ImageUploadBackendForm
def generate_form(self):
return ImageUploadForm
def save_form(self, form, claim, request):
if form.cleaned_data['decline']:
return False
storage = InlineEncodedStorage('benefit_image')
storage.save(str(claim.id), io.BytesIO(base64.b64decode(form.cleaned_data['uploadedimage'])))
return True
def render_claimdata(self, claimedbenefit, isadmin):
if claimedbenefit.declined:
return 'Benefit declined.'
if self.params.get('previewbackground', None):
return ''.format(claimedbenefit.id, claimedbenefit.id, self.params.get('previewbackground'))
return 'Uploaded image:
' % claimedbenefit.id
def get_claimdata(self, claimedbenefit):
return {
'image': {
'suburl': '/{}'.format(claimedbenefit.id),
'tag': InlineEncodedStorage('benefit_image').get_tag(claimedbenefit.id),
},
}
def get_claimfile(self, claimedbenefit):
hashval, data = InlineEncodedStorage('benefit_image').read(claimedbenefit.id)
if hashval is None and data is None:
raise Http404()
resp = HttpResponse(content_type='image/png')
resp['ETag'] = '"{}"'.format(hashval)
resp.write(data)
return resp