summaryrefslogtreecommitdiff
path: root/postgresqleu
diff options
context:
space:
mode:
Diffstat (limited to 'postgresqleu')
-rw-r--r--postgresqleu/confreg/admin.py4
-rw-r--r--postgresqleu/confreg/backendforms.py7
-rw-r--r--postgresqleu/confreg/forms.py5
-rw-r--r--postgresqleu/confreg/migrations/0065_speaker_photo_bytea.py2
-rw-r--r--postgresqleu/confreg/migrations/0087_speaker_photo512.py19
-rw-r--r--postgresqleu/confreg/models.py33
-rw-r--r--postgresqleu/confreg/views.py21
-rw-r--r--postgresqleu/urls.py2
-rw-r--r--postgresqleu/util/backendforms.py1
-rw-r--r--postgresqleu/util/backendviews.py2
-rw-r--r--postgresqleu/util/fields.py41
-rw-r--r--postgresqleu/util/image.py28
-rw-r--r--postgresqleu/util/widgets.py1
13 files changed, 139 insertions, 27 deletions
diff --git a/postgresqleu/confreg/admin.py b/postgresqleu/confreg/admin.py
index 1e4657a2..f922d55e 100644
--- a/postgresqleu/confreg/admin.py
+++ b/postgresqleu/confreg/admin.py
@@ -286,7 +286,7 @@ class ConferenceAdditionalOptionAdmin(admin.ModelAdmin):
class SpeakerAdminForm(ConcurrentProtectedModelForm):
- exclude_fields_from_validation = ['photo', ]
+ exclude_fields_from_validation = ['photo', 'photo512', ]
class Meta:
model = Speaker
@@ -294,7 +294,7 @@ class SpeakerAdminForm(ConcurrentProtectedModelForm):
class SpeakerAdmin(admin.ModelAdmin):
- list_display = ['user', 'email', 'fullname', 'has_abstract', 'has_photo']
+ list_display = ['user', 'email', 'fullname', 'has_abstract', 'has_photo', 'has_photo512']
search_fields = ['fullname', 'user__email']
autocomplete_fields = ('user', )
ordering = ['fullname']
diff --git a/postgresqleu/confreg/backendforms.py b/postgresqleu/confreg/backendforms.py
index d38c1904..20f9aea7 100644
--- a/postgresqleu/confreg/backendforms.py
+++ b/postgresqleu/confreg/backendforms.py
@@ -798,11 +798,14 @@ class BackendSpeakerForm(BackendForm):
list_fields = ['fullname', 'user', 'company', ]
markdown_fields = ['abstract', ]
readonly_fields = ['user', ]
- exclude_fields_from_validation = ['user', 'photo', ]
+ exclude_fields_from_validation = ['user', 'photo512', ]
+ # We must save the photo field as well, since it's being updaed in the pre_save signal,
+ # and we want to include that updating.
+ extra_update_fields = ['photo', ]
class Meta:
model = Speaker
- fields = ['fullname', 'user', 'twittername', 'company', 'abstract', 'photo', ]
+ fields = ['fullname', 'user', 'twittername', 'company', 'abstract', 'photo512', ]
widgets = {
'user': StaticTextWidget,
}
diff --git a/postgresqleu/confreg/forms.py b/postgresqleu/confreg/forms.py
index 372ec836..953fef4f 100644
--- a/postgresqleu/confreg/forms.py
+++ b/postgresqleu/confreg/forms.py
@@ -462,10 +462,11 @@ class ConferenceFeedbackForm(forms.Form):
class SpeakerProfileForm(forms.ModelForm):
class Meta:
model = Speaker
- exclude = ('user', 'speakertoken')
+ exclude = ('user', 'speakertoken', 'photo')
def __init__(self, *args, **kwargs):
super(SpeakerProfileForm, self).__init__(*args, **kwargs)
+ self.fields['photo512'].help_text = 'Photo will be rescaled to 512x512 pixels. We reserve the right to make minor edits to speaker photos if necessary'
def clean_twittername(self):
if not self.cleaned_data['twittername']:
@@ -503,7 +504,7 @@ class CallForPapersForm(forms.ModelForm):
if 'data' in kwargs and 'speaker' in kwargs['data']:
vals.extend([int(x) for x in kwargs['data'].getlist('speaker')])
- self.fields['speaker'].queryset = Speaker.objects.defer('photo').filter(pk__in=vals)
+ self.fields['speaker'].queryset = Speaker.objects.defer('photo', 'photo512').filter(pk__in=vals)
self.fields['speaker'].label_from_instance = lambda x: "{0} <{1}>".format(x.fullname, x.email)
self.fields['speaker'].required = True
self.fields['speaker'].help_text = "Type the beginning of a speakers email address to add more speakers"
diff --git a/postgresqleu/confreg/migrations/0065_speaker_photo_bytea.py b/postgresqleu/confreg/migrations/0065_speaker_photo_bytea.py
index aad3b470..9b5979b0 100644
--- a/postgresqleu/confreg/migrations/0065_speaker_photo_bytea.py
+++ b/postgresqleu/confreg/migrations/0065_speaker_photo_bytea.py
@@ -21,7 +21,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='speaker',
name='photo',
- field=postgresqleu.util.fields.ImageBinaryField(blank=True, null=True, verbose_name='Photo', max_length=1000000),
+ field=postgresqleu.util.fields.ImageBinaryField(blank=True, null=True, verbose_name='Photo (low res)', max_length=1000000),
),
migrations.RunSQL("UPDATE confreg_speaker SET photo=decode(confreg_speaker_photo.photo, 'base64') FROM confreg_speaker_photo WHERE confreg_speaker_photo.id=confreg_speaker.id"),
migrations.DeleteModel(
diff --git a/postgresqleu/confreg/migrations/0087_speaker_photo512.py b/postgresqleu/confreg/migrations/0087_speaker_photo512.py
new file mode 100644
index 00000000..1bdbf385
--- /dev/null
+++ b/postgresqleu/confreg/migrations/0087_speaker_photo512.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.2.11 on 2022-06-26 17:09
+
+from django.db import migrations
+import postgresqleu.util.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('confreg', '0086_conferenceregistrationlog_changedata'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='speaker',
+ name='photo512',
+ field=postgresqleu.util.fields.ImageBinaryField(blank=True, max_length=1000000, null=True, verbose_name='Photo'),
+ ),
+ ]
diff --git a/postgresqleu/confreg/models.py b/postgresqleu/confreg/models.py
index 0bdea427..baa2a163 100644
--- a/postgresqleu/confreg/models.py
+++ b/postgresqleu/confreg/models.py
@@ -3,6 +3,7 @@
from django.db import models
from django.db.models import Q
from django.db.models.expressions import F
+from django.db.models.signals import pre_save
from django.contrib.auth.models import User
from django.conf import settings
from django.core.exceptions import ValidationError
@@ -14,6 +15,7 @@ from django.template.defaultfilters import slugify
from django.contrib.postgres.fields import DateTimeRangeField
from django.contrib.postgres.indexes import GinIndex
from django.core.serializers.json import DjangoJSONEncoder
+from django.dispatch import receiver
from postgresqleu.util.validators import validate_lowercase, validate_urlname
from postgresqleu.util.validators import TwitterValidator
@@ -22,6 +24,7 @@ from postgresqleu.util.forms import ChoiceArrayField
from postgresqleu.util.fields import LowercaseEmailField, ImageBinaryField, PdfBinaryField
from postgresqleu.util.time import today_conference
from postgresqleu.util.db import exec_no_result
+from postgresqleu.util.image import rescale_image_bytes
import base64
import pytz
@@ -917,11 +920,12 @@ class Speaker(models.Model):
verbose_name='Twitter name')
company = models.CharField(max_length=100, null=False, blank=True)
abstract = models.TextField(null=False, blank=True, verbose_name="Bio")
- photo = ImageBinaryField(blank=True, null=True, verbose_name="Photo", max_length=1000000, max_resolution=(128, 128))
+ photo = ImageBinaryField(blank=True, null=True, verbose_name="Photo (low res)", max_length=1000000, resolution=(128, 128))
+ photo512 = ImageBinaryField(blank=True, null=True, verbose_name="Photo", max_length=1000000, resolution=(512, 512), auto_scale=True)
lastmodified = models.DateTimeField(auto_now=True, null=False, blank=False)
speakertoken = models.TextField(null=False, blank=False, unique=True)
- _safe_attributes = ('id', 'name', 'fullname', 'twittername', 'company', 'abstract', 'photo', 'has_photo', 'photo_data', 'lastmodified', )
+ _safe_attributes = ('id', 'name', 'fullname', 'twittername', 'company', 'abstract', 'photo', 'photo512', 'has_photo', 'has_photo512', 'photo_data', 'photo_data512', 'lastmodified', )
json_included_attributes = ['fullname', 'twittername', 'company', 'abstract', 'lastmodified']
@property
@@ -946,17 +950,21 @@ class Speaker(models.Model):
return len(self.abstract) > 0
has_abstract.boolean = True
+ @property
def has_photo(self):
return (self.photo is not None and self.photo != "")
- has_photo.boolean = True
+
+ @property
+ def has_photo512(self):
+ return (self.photo512 is not None and self.photo512 != "")
@cached_property
def photo_data(self):
return base64.b64encode(self.photo).decode('ascii')
- @property
- def photofile(self):
- return self.photo
+ @cached_property
+ def photo512_data(self):
+ return base64.b64encode(self.photo512).decode('ascii')
def __str__(self):
return self.name
@@ -969,12 +977,25 @@ class Speaker(models.Model):
# our fix. So make the fix conditional until we have 3.2.10 everywhere so we can throw it away
if 'photo' in r[2] and isinstance(r[2]['photo'], memoryview):
r[2]['photo'] = bytes(r[2]['photo'])
+ if 'photo512' in r[2] and isinstance(r[2]['photo512'], memoryview):
+ r[2]['photo512'] = bytes(r[2]['photo512'])
return r
class Meta:
ordering = ['fullname', ]
+@receiver(pre_save, sender=Speaker)
+def _speaker_photo_resizer(sender, instance, *args, **kwargs):
+ if instance.photo512:
+ instance.photo = rescale_image_bytes(instance.photo512, (128, 128))
+ else:
+ # If there is a photo, remove it but only if the photo512 entry has actually
+ # been deleted (which means it's not NULL anymore)
+ if instance.photo and instance.photo512 is not None:
+ instance.photo = None
+
+
class DeletedItems(models.Model):
itemid = models.IntegerField(null=False, blank=False)
type = models.CharField(max_length=16, blank=False, null=False)
diff --git a/postgresqleu/confreg/views.py b/postgresqleu/confreg/views.py
index 0174fd9f..23e0b28d 100644
--- a/postgresqleu/confreg/views.py
+++ b/postgresqleu/confreg/views.py
@@ -1226,7 +1226,8 @@ INNER JOIN confreg_room r ON r.id=t.room_id GROUP BY day
'name', spk.fullname,
'company', spk.company,
'twittername', spk.twittername,
- 'hasphoto', spk.photo IS NOT NULL AND spk.photo != ''::bytea
+ 'hasphoto', spk.photo IS NOT NULL AND spk.photo != ''::bytea,
+ 'hasphoto512', spk.photo512 IS NOT NULL AND spk.photo512 != ''::bytea
) ORDER BY spk.fullname) FILTER (WHERE spk.id IS NOT NULL), '[]') AS speakers
FROM confreg_conferencesession s
LEFT JOIN confreg_track t ON t.id=s.track_id
@@ -1486,9 +1487,20 @@ def speaker_card(request, confname, speakerid, cardformat):
})
-def speakerphoto(request, speakerid):
+def speakerphoto(request, speakerid, phototype='1/'):
speaker = get_object_or_404(Speaker, pk=speakerid)
- return HttpResponse(bytes(speaker.photo), content_type='image/jpg')
+ if phototype is None or phototype == '1/':
+ if not speaker.photo:
+ raise Http404()
+ photo = bytes(speaker.photo)
+ elif phototype == '5/':
+ if not speaker.photo512:
+ raise Http404()
+ photo = bytes(speaker.photo512)
+ else:
+ raise Http404()
+ content_type = 'image/png' if photo[:8] == b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A' else 'image/jpg'
+ return HttpResponse(photo, content_type=content_type)
@login_required
@@ -1511,7 +1523,8 @@ def speakerprofile(request, confurlname=None):
conferences = []
callforpapers = None
except Exception:
- pass
+ # We used to accept errors here. Why?
+ raise
if request.method == 'POST':
# Attempt to save
diff --git a/postgresqleu/urls.py b/postgresqleu/urls.py
index f474e85d..8433b076 100644
--- a/postgresqleu/urls.py
+++ b/postgresqleu/urls.py
@@ -124,7 +124,7 @@ urlpatterns.extend([
url(r'^events/([^/]+)/checkin/([a-z0-9]{64})/$', postgresqleu.confreg.checkin.checkin),
url(r'^events/([^/]+)/checkin/([a-z0-9]{64})/api/(\w+)/$', postgresqleu.confreg.checkin.api),
url(r'^events/([^/]+)/sessions/$', postgresqleu.confreg.views.sessionlist),
- url(r'^events/speaker/(\d+)/photo/$', postgresqleu.confreg.views.speakerphoto),
+ url(r'^events/speaker/(\d+)/photo/(\d+/)?$', postgresqleu.confreg.views.speakerphoto),
url(r'^events/speakerprofile/$', postgresqleu.confreg.views.speakerprofile),
url(r'^events/([^/]+)/speakerprofile/$', postgresqleu.confreg.views.speakerprofile),
url(r'^events/([^/]+)/callforpapers/$', postgresqleu.confreg.views.callforpapers),
diff --git a/postgresqleu/util/backendforms.py b/postgresqleu/util/backendforms.py
index 6c22192c..755bcace 100644
--- a/postgresqleu/util/backendforms.py
+++ b/postgresqleu/util/backendforms.py
@@ -28,6 +28,7 @@ class BackendForm(ConcurrentProtectedModelForm):
vat_fields = {}
verbose_field_names = {}
exclude_date_validators = []
+ extra_update_fields = []
form_before_new = None
newformdata = None
_newformdata = _NewFormDataField()
diff --git a/postgresqleu/util/backendviews.py b/postgresqleu/util/backendviews.py
index abe6dedb..fac9c5cf 100644
--- a/postgresqleu/util/backendviews.py
+++ b/postgresqleu/util/backendviews.py
@@ -170,7 +170,7 @@ def backend_process_form(request, urlname, formclass, id, cancel_url='../', save
for fn, ffields in form.json_form_fields.items():
all_excludes.extend(ffields)
- form.instance.save(update_fields=[f for f in form.fields.keys() if f not in all_excludes and not isinstance(form[f].field, forms.ModelMultipleChoiceField)])
+ form.instance.save(update_fields=[f for f in form.fields.keys() if f not in all_excludes and not isinstance(form[f].field, forms.ModelMultipleChoiceField)] + form.extra_update_fields)
# Merge fields stored in json
if form.json_form_fields:
diff --git a/postgresqleu/util/fields.py b/postgresqleu/util/fields.py
index ae41061a..88b381b0 100644
--- a/postgresqleu/util/fields.py
+++ b/postgresqleu/util/fields.py
@@ -2,7 +2,9 @@ from django.db import models
from django.core.exceptions import ValidationError
from .forms import ImageBinaryFormField, PdfBinaryFormField
-from PIL import ImageFile
+import io
+
+from PIL import Image, ImageFile
from postgresqleu.util.magic import magicdb
@@ -19,7 +21,8 @@ class ImageBinaryField(models.Field):
empty_values = [None, b'']
def __init__(self, max_length, *args, **kwargs):
- self.max_resolution = kwargs.pop('max_resolution', None)
+ self.resolution = kwargs.pop('resolution', None)
+ self.auto_scale = kwargs.pop('auto_scale', False)
super(ImageBinaryField, self).__init__(*args, **kwargs)
self.max_length = max_length
@@ -65,12 +68,34 @@ class ImageBinaryField(models.Field):
except Exception as e:
raise ValidationError("Could not parse image: %s" % e)
- if img.format.upper() != 'JPEG':
- raise ValidationError("Only JPEG files are allowed")
-
- if self.max_resolution:
- if img.size[0] > self.max_resolution[0] or img.size[1] > self.max_resolution[1]:
- raise ValidationError("Maximum image size is {}x{}".format(*self.max_resolution))
+ if img.format.upper() not in ('JPEG', 'PNG'):
+ raise ValidationError("Only JPEG or PNG files are allowed")
+
+ if self.resolution:
+ if img.size[0] != self.resolution[0] or img.size[1] != self.resolution[1]:
+ if self.auto_scale:
+ scale = min(
+ float(self.resolution[0]) / float(img.size[0]),
+ float(self.resolution[1]) / float(img.size[1]),
+ )
+ newimg = img.resize(
+ (int(img.size[0] * scale), int(img.size[1] * scale)),
+ Image.BICUBIC,
+ )
+ saver = io.BytesIO()
+ if newimg.size[0] != newimg.size[1]:
+ # This is not a square, so we have to roll it again
+ centeredimg = Image.new('RGBA', self.resolution)
+ centeredimg.paste(newimg, (
+ (self.resolution[0] - newimg.size[0]) // 2,
+ (self.resolution[1] - newimg.size[1]) // 2,
+ ))
+ centeredimg.save(saver, format='PNG')
+ else:
+ newimg.save(saver, format="PNG")
+ value = saver.getvalue()
+ else:
+ raise ValidationError("Image size must be {}x{}".format(*self.resolution))
return value
diff --git a/postgresqleu/util/image.py b/postgresqleu/util/image.py
new file mode 100644
index 00000000..507f9968
--- /dev/null
+++ b/postgresqleu/util/image.py
@@ -0,0 +1,28 @@
+import io
+
+from PIL import Image, ImageFile
+
+
+# Rescale an image in the form of bytes to a new set of bytes
+# in the same format. Assumes the aspect is correct and that
+# the incoming data is valid (it's expected to be for example
+# the output of previous image operations)
+def rescale_image_bytes(origbytes, resolution):
+ p = ImageFile.Parser()
+ p.feed(origbytes)
+ p.close()
+ img = p.image
+
+ scale = min(
+ float(resolution[0]) / float(img.size[0]),
+ float(resolution[1]) / float(img.size[1]),
+ )
+
+ newimg = img.resize(
+ (int(img.size[0] * scale), int(img.size[1] * scale)),
+ Image.BICUBIC,
+ )
+ saver = io.BytesIO()
+ newimg.save(saver, format=img.format)
+
+ return saver.getvalue()
diff --git a/postgresqleu/util/widgets.py b/postgresqleu/util/widgets.py
index 65aad0a1..d7b46062 100644
--- a/postgresqleu/util/widgets.py
+++ b/postgresqleu/util/widgets.py
@@ -94,6 +94,7 @@ class InlineImageUploadWidget(forms.ClearableFileInput):
context = self.get_context(name, value, attrs)
if value and not isinstance(value, UploadedFile):
context['widget']['value'] = base64.b64encode(value).decode('ascii')
+ context['widget']['imagetype'] = 'image/png' if bytes(value[:8]) == b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A' else 'image/jpg'
return mark_safe(loader.render_to_string('confreg/widgets/inline_photo_upload_widget.html', context))