forked from CGM_Public/pretix_original
Add Question.valid_file_portrait as well as crop editor for images
This commit is contained in:
@@ -62,6 +62,7 @@ valid_date_min date Minimum value f
|
||||
valid_date_max date Maximum value for date questions (optional)
|
||||
valid_datetime_min datetime Minimum value for date and time questions (optional)
|
||||
valid_datetime_max datetime Maximum value for date and time questions (optional)
|
||||
valid_file_portrait boolean Turn on file validation for portrait photos
|
||||
dependency_question integer Internal ID of a different question. The current
|
||||
question will only be shown if the question given in
|
||||
this attribute is set to the value given in
|
||||
@@ -83,6 +84,10 @@ dependency_value string An old version
|
||||
|
||||
The attributes ``valid_*`` have been added.
|
||||
|
||||
.. versionchanged:: 3.18
|
||||
|
||||
The attribute ``valid_file_portrait`` have been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -134,6 +139,7 @@ Endpoints
|
||||
"valid_date_max": null,
|
||||
"valid_datetime_min": null,
|
||||
"valid_datetime_max": null,
|
||||
"valid_file_portrait": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"dependency_values": [],
|
||||
@@ -211,6 +217,7 @@ Endpoints
|
||||
"valid_date_max": null,
|
||||
"valid_datetime_min": null,
|
||||
"valid_datetime_max": null,
|
||||
"valid_file_portrait": false,
|
||||
"dependency_question": null,
|
||||
"dependency_value": null,
|
||||
"dependency_values": [],
|
||||
@@ -311,6 +318,7 @@ Endpoints
|
||||
"valid_date_max": null,
|
||||
"valid_datetime_min": null,
|
||||
"valid_datetime_max": null,
|
||||
"valid_file_portrait": false,
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -392,6 +400,7 @@ Endpoints
|
||||
"valid_date_max": null,
|
||||
"valid_datetime_min": null,
|
||||
"valid_datetime_max": null,
|
||||
"valid_file_portrait": false,
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -316,8 +316,8 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
||||
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
||||
'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min',
|
||||
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max'
|
||||
)
|
||||
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
|
||||
'valid_file_portrait')
|
||||
|
||||
def validate_identifier(self, value):
|
||||
Question._clean_identifier(self.context['event'], value, self.instance)
|
||||
|
||||
@@ -36,6 +36,7 @@ import copy
|
||||
import json
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
from urllib.error import HTTPError
|
||||
|
||||
import dateutil.parser
|
||||
@@ -47,6 +48,7 @@ from babel import Locale
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db.models import QuerySet
|
||||
from django.forms import Select
|
||||
@@ -63,6 +65,7 @@ from phonenumber_field.phonenumber import PhoneNumber
|
||||
from phonenumber_field.widgets import PhoneNumberPrefixWidget
|
||||
from phonenumbers import NumberParseException, national_significant_number
|
||||
from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE
|
||||
from PIL import ImageOps
|
||||
|
||||
from pretix.base.forms.widgets import (
|
||||
BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget,
|
||||
@@ -80,7 +83,9 @@ from pretix.base.settings import (
|
||||
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
|
||||
)
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
from pretix.control.forms import ExtFileField, SplitDateTimeField
|
||||
from pretix.control.forms import (
|
||||
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
|
||||
)
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
from pretix.helpers.escapejson import escapejson_attr
|
||||
from pretix.helpers.i18n import get_format_without_seconds
|
||||
@@ -386,6 +391,126 @@ class MaxDateTimeValidator(MaxValueValidator):
|
||||
raise e
|
||||
|
||||
|
||||
class PortraitImageWidget(UploadedFileWidget):
|
||||
template_name = 'pretixbase/forms/widgets/portrait_image.html'
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
d = super().value_from_datadict(data, files, name)
|
||||
if d is not None:
|
||||
d._cropdata = json.loads(data.get(name + '_cropdata', '{}') or '{}')
|
||||
return d
|
||||
|
||||
|
||||
class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileField):
|
||||
widget = PortraitImageWidget
|
||||
default_error_messages = {
|
||||
'aspect_ratio_landscape': _(
|
||||
"You uploaded an image in landscape orientation. Please upload an image in portrait orientation."
|
||||
),
|
||||
'aspect_ratio_not_3_by_4': _(
|
||||
"Please upload an image where the width is 3/4 of the height."
|
||||
),
|
||||
'max_dimension': _(
|
||||
"The file you uploaded has a very large number of pixels, please upload an image no larger than 10000 x 10000 pixels."
|
||||
),
|
||||
'invalid_image': _(
|
||||
"Upload a valid image. The file you uploaded was either not an "
|
||||
"image or a corrupted image."
|
||||
),
|
||||
}
|
||||
|
||||
def to_python(self, data):
|
||||
"""
|
||||
Based on Django's ImageField
|
||||
"""
|
||||
f = super().to_python(data)
|
||||
if f is None:
|
||||
return None
|
||||
|
||||
from PIL import Image
|
||||
|
||||
# We need to get a file object for Pillow. We might have a path or we might
|
||||
# have to read the data into memory.
|
||||
if hasattr(data, 'temporary_file_path'):
|
||||
file = data.temporary_file_path()
|
||||
else:
|
||||
if hasattr(data, 'read'):
|
||||
file = BytesIO(data.read())
|
||||
else:
|
||||
file = BytesIO(data['content'])
|
||||
|
||||
try:
|
||||
image = Image.open(file)
|
||||
# verify() must be called immediately after the constructor.
|
||||
image.verify()
|
||||
|
||||
# We want to do more than just verify(), so we need to re-open the file
|
||||
if hasattr(file, 'seek'):
|
||||
file.seek(0)
|
||||
image = Image.open(file)
|
||||
|
||||
# load() is a potential DoS vector (see Django bug #18520), so we verify the size first
|
||||
if image.width > 10_000 or image.height > 10_000:
|
||||
raise ValidationError(
|
||||
self.error_messages['max_dimension'],
|
||||
code='max_dimension',
|
||||
)
|
||||
|
||||
image.load()
|
||||
|
||||
# Annotating so subclasses can reuse it for their own validation
|
||||
f.image = image
|
||||
# Pillow doesn't detect the MIME type of all formats. In those
|
||||
# cases, content_type will be None.
|
||||
f.content_type = Image.MIME.get(image.format)
|
||||
|
||||
# before we calc aspect ratio, we need to check and apply EXIF-orientation
|
||||
image = ImageOps.exif_transpose(image)
|
||||
|
||||
if f._cropdata:
|
||||
image = image.crop((
|
||||
f._cropdata.get('x', 0),
|
||||
f._cropdata.get('y', 0),
|
||||
f._cropdata.get('x', 0) + f._cropdata.get('width', image.width),
|
||||
f._cropdata.get('y', 0) + f._cropdata.get('height', image.height),
|
||||
))
|
||||
with BytesIO() as output:
|
||||
# This might use a lot of memory, but temporary files are not a good option since
|
||||
# we don't control the cleanup
|
||||
image.save(output, format=f.image.format)
|
||||
f = SimpleUploadedFile(f.name, output.getvalue(), f.content_type)
|
||||
f.image = image
|
||||
|
||||
if image.width > image.height:
|
||||
raise ValidationError(
|
||||
self.error_messages['aspect_ratio_landscape'],
|
||||
code='aspect_ratio_landscape',
|
||||
)
|
||||
|
||||
if not 3 / 4 * .95 < image.width / image.height < 3 / 4 * 1.05: # give it some tolerance
|
||||
raise ValidationError(
|
||||
self.error_messages['aspect_ratio_not_3_by_4'],
|
||||
code='aspect_ratio_not_3_by_4',
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception('foo')
|
||||
# Pillow doesn't recognize it as an image.
|
||||
if isinstance(exc, ValidationError):
|
||||
raise
|
||||
raise ValidationError(
|
||||
self.error_messages['invalid_image'],
|
||||
code='invalid_image',
|
||||
) from exc
|
||||
if hasattr(f, 'seek') and callable(f.seek):
|
||||
f.seek(0)
|
||||
return f
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('ext_whitelist', (".png", ".jpg", ".jpeg", ".jfif", ".tif", ".tiff", ".bmp"))
|
||||
kwargs.setdefault('max_size', 10 * 1024 * 1024)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class BaseQuestionsForm(forms.Form):
|
||||
"""
|
||||
This form class is responsible for asking order-related questions. This includes
|
||||
@@ -596,6 +721,14 @@ class BaseQuestionsForm(forms.Form):
|
||||
initial=initial.options.all() if initial else None,
|
||||
)
|
||||
elif q.type == Question.TYPE_FILE:
|
||||
if q.valid_file_portrait:
|
||||
field = PortraitImageField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
initial=initial.file if initial else None,
|
||||
widget=PortraitImageWidget(position=pos, event=event, answer=initial, attrs={'data-portrait-photo': 'true'}),
|
||||
)
|
||||
else:
|
||||
field = ExtFileField(
|
||||
label=label, required=required,
|
||||
help_text=help_text,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.13 on 2021-04-19 09:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0181_team_can_checkin_orders'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='valid_file_portrait',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -1140,6 +1140,12 @@ class Question(LoggedModel):
|
||||
valid_datetime_max = models.DateTimeField(null=True, blank=True,
|
||||
verbose_name=_('Maximum value'),
|
||||
help_text=_('Currently not supported in our apps and during check-in'))
|
||||
valid_file_portrait = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Validate file to be a portrait'),
|
||||
help_text=_('If checked, files must be images with an aspect ratio of 3:4. This is commonly used for photos '
|
||||
'printed on badges.')
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{% load i18n %}
|
||||
{% if widget.is_initial %}<div class="photo-initial">{{ widget.initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
|
||||
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}">
|
||||
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}<br></div>
|
||||
<span class="photo-input">{{ widget.input_text }}:{% else %}<span class="photo-input">{% endif %}
|
||||
<input type="{{ widget.type }}" name="{{ widget.name }}" {% include "django/forms/widgets/attrs.html" %}>
|
||||
</span>
|
||||
<div class="photo-buttons hidden">
|
||||
<button class="btn btn-primary" type="button" data-action="upload">
|
||||
<span class="fa fa-upload"></span> {% trans "Upload photo" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="photo-camera hidden">
|
||||
<video></video>
|
||||
</div>
|
||||
<div class="photo-preview hidden">
|
||||
<img>
|
||||
</div>
|
||||
<input type="hidden" name="{{ widget.name }}_cropdata" value="">
|
||||
@@ -178,8 +178,7 @@ class CachedFileInput(forms.ClearableFileInput):
|
||||
return ctx
|
||||
|
||||
|
||||
class SizeFileField(forms.FileField):
|
||||
|
||||
class SizeValidationMixin:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.max_size = kwargs.pop("max_size", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -196,13 +195,12 @@ class SizeFileField(forms.FileField):
|
||||
data = super().clean(*args, **kwargs)
|
||||
if isinstance(data, UploadedFile) and self.max_size and data.size > self.max_size:
|
||||
raise forms.ValidationError(_("Please do not upload files larger than {size}!").format(
|
||||
size=SizeFileField._sizeof_fmt(self.max_size)
|
||||
size=SizeValidationMixin._sizeof_fmt(self.max_size)
|
||||
))
|
||||
return data
|
||||
|
||||
|
||||
class ExtFileField(SizeFileField):
|
||||
widget = ClearableBasenameFileInput
|
||||
class ExtValidationMixin:
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
ext_whitelist = kwargs.pop("ext_whitelist")
|
||||
@@ -220,6 +218,14 @@ class ExtFileField(SizeFileField):
|
||||
return data
|
||||
|
||||
|
||||
class SizeFileField(SizeValidationMixin, forms.FileField):
|
||||
pass
|
||||
|
||||
|
||||
class ExtFileField(ExtValidationMixin, SizeFileField):
|
||||
widget = ClearableBasenameFileInput
|
||||
|
||||
|
||||
class CachedFileField(ExtFileField):
|
||||
widget = CachedFileInput
|
||||
|
||||
|
||||
@@ -153,6 +153,7 @@ class QuestionForm(I18nModelForm):
|
||||
'valid_datetime_max',
|
||||
'valid_date_min',
|
||||
'valid_date_max',
|
||||
'valid_file_portrait',
|
||||
]
|
||||
widgets = {
|
||||
'valid_datetime_min': SplitDateTimePickerWidget(),
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<script type="text/javascript" src="{% static "charts/raphael-min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "charts/morris.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "clipboard/clipboard.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "cropper/cropper.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "rrule/rrule.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/ui/questions.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/jquery.qrcode.min.js" %}"></script>
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
{% bootstrap_field form.valid_datetime_min layout="control" %}
|
||||
{% bootstrap_field form.valid_datetime_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-file">
|
||||
{% bootstrap_field form.valid_file_portrait layout="control" %}
|
||||
</div>
|
||||
<div id="answer-options">
|
||||
<h3>{% trans "Answer options" %}</h3>
|
||||
<noscript>
|
||||
|
||||
@@ -102,6 +102,13 @@
|
||||
<a href="{{ q.answer.frontend_file_url }}?token={% answer_token request q.answer %}">
|
||||
{{ q.answer.file_name }}
|
||||
</a>
|
||||
{% if q.answer.is_image %}
|
||||
<br>
|
||||
<a href="{{ q.answer.frontend_file_url }}?token={% answer_token request q.answer %}" data-lightbox="order"
|
||||
class="answer-thumb">
|
||||
<img src="{{ q.answer.frontend_file_url }}?token={% answer_token request q.answer %}">
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elif q.type == "M" %}
|
||||
{{ q.answer|rich_text_snippet }}
|
||||
{% else %}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<script type="text/javascript" src="{% static "bootstrap/js/bootstrap.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "datetimepicker/bootstrap-datetimepicker.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "slider/bootstrap-slider.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "cropper/cropper.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/jquery.qrcode.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/widget/floatformat.js" %}"></script>
|
||||
|
||||
3625
src/pretix/static/cropper/cropper.js
Normal file
3625
src/pretix/static/cropper/cropper.js
Normal file
File diff suppressed because it is too large
Load Diff
304
src/pretix/static/cropper/cropper.scss
Normal file
304
src/pretix/static/cropper/cropper.scss
Normal file
@@ -0,0 +1,304 @@
|
||||
/*!
|
||||
* Cropper.js v1.5.11
|
||||
* https://fengyuanchen.github.io/cropperjs
|
||||
*
|
||||
* Copyright 2015-present Chen Fengyuan
|
||||
* Released under the MIT license
|
||||
*
|
||||
* Date: 2021-02-17T11:53:21.992Z
|
||||
*/
|
||||
|
||||
.cropper-container {
|
||||
direction: ltr;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.cropper-container img {
|
||||
display: block;
|
||||
height: 100%;
|
||||
image-orientation: 0deg;
|
||||
max-height: none !important;
|
||||
max-width: none !important;
|
||||
min-height: 0 !important;
|
||||
min-width: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cropper-wrap-box,
|
||||
.cropper-canvas,
|
||||
.cropper-drag-box,
|
||||
.cropper-crop-box,
|
||||
.cropper-modal {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.cropper-wrap-box,
|
||||
.cropper-canvas {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cropper-drag-box {
|
||||
background-color: #fff;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cropper-modal {
|
||||
background-color: #000;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.cropper-view-box {
|
||||
display: block;
|
||||
height: 100%;
|
||||
outline: 1px solid #39f;
|
||||
outline-color: rgba(51, 153, 255, 0.75);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cropper-dashed {
|
||||
border: 0 dashed #eee;
|
||||
display: block;
|
||||
opacity: 0.5;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.cropper-dashed.dashed-h {
|
||||
border-bottom-width: 1px;
|
||||
border-top-width: 1px;
|
||||
height: calc(100% / 3);
|
||||
left: 0;
|
||||
top: calc(100% / 3);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cropper-dashed.dashed-v {
|
||||
border-left-width: 1px;
|
||||
border-right-width: 1px;
|
||||
height: 100%;
|
||||
left: calc(100% / 3);
|
||||
top: 0;
|
||||
width: calc(100% / 3);
|
||||
}
|
||||
|
||||
.cropper-center {
|
||||
display: block;
|
||||
height: 0;
|
||||
left: 50%;
|
||||
opacity: 0.75;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.cropper-center::before,
|
||||
.cropper-center::after {
|
||||
background-color: #eee;
|
||||
content: ' ';
|
||||
display: block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.cropper-center::before {
|
||||
height: 1px;
|
||||
left: -3px;
|
||||
top: 0;
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
.cropper-center::after {
|
||||
height: 7px;
|
||||
left: 0;
|
||||
top: -3px;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.cropper-face,
|
||||
.cropper-line,
|
||||
.cropper-point {
|
||||
display: block;
|
||||
height: 100%;
|
||||
opacity: 0.1;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cropper-face {
|
||||
background-color: #fff;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.cropper-line {
|
||||
background-color: #39f;
|
||||
}
|
||||
|
||||
.cropper-line.line-e {
|
||||
cursor: ew-resize;
|
||||
right: -3px;
|
||||
top: 0;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.cropper-line.line-n {
|
||||
cursor: ns-resize;
|
||||
height: 5px;
|
||||
left: 0;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.cropper-line.line-w {
|
||||
cursor: ew-resize;
|
||||
left: -3px;
|
||||
top: 0;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.cropper-line.line-s {
|
||||
bottom: -3px;
|
||||
cursor: ns-resize;
|
||||
height: 5px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.cropper-point {
|
||||
background-color: #39f;
|
||||
height: 5px;
|
||||
opacity: 0.75;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.cropper-point.point-e {
|
||||
cursor: ew-resize;
|
||||
margin-top: -3px;
|
||||
right: -3px;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.cropper-point.point-n {
|
||||
cursor: ns-resize;
|
||||
left: 50%;
|
||||
margin-left: -3px;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.cropper-point.point-w {
|
||||
cursor: ew-resize;
|
||||
left: -3px;
|
||||
margin-top: -3px;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.cropper-point.point-s {
|
||||
bottom: -3px;
|
||||
cursor: s-resize;
|
||||
left: 50%;
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
.cropper-point.point-ne {
|
||||
cursor: nesw-resize;
|
||||
right: -3px;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.cropper-point.point-nw {
|
||||
cursor: nwse-resize;
|
||||
left: -3px;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.cropper-point.point-sw {
|
||||
bottom: -3px;
|
||||
cursor: nesw-resize;
|
||||
left: -3px;
|
||||
}
|
||||
|
||||
.cropper-point.point-se {
|
||||
bottom: -3px;
|
||||
cursor: nwse-resize;
|
||||
height: 20px;
|
||||
opacity: 1;
|
||||
right: -3px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.cropper-point.point-se {
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.cropper-point.point-se {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.cropper-point.point-se {
|
||||
height: 5px;
|
||||
opacity: 0.75;
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.cropper-point.point-se::before {
|
||||
background-color: #39f;
|
||||
bottom: -50%;
|
||||
content: ' ';
|
||||
display: block;
|
||||
height: 200%;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: -50%;
|
||||
width: 200%;
|
||||
}
|
||||
|
||||
.cropper-invisible {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cropper-bg {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');
|
||||
}
|
||||
|
||||
.cropper-hide {
|
||||
display: block;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.cropper-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.cropper-move {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.cropper-crop {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.cropper-disabled .cropper-drag-box,
|
||||
.cropper-disabled .cropper-face,
|
||||
.cropper-disabled .cropper-line,
|
||||
.cropper-disabled .cropper-point {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -567,6 +567,7 @@ var form_handlers = function (el) {
|
||||
|
||||
el.find("input[name*=question], select[name*=question]").change(questions_toggle_dependent);
|
||||
questions_toggle_dependent();
|
||||
questions_init_photos(el);
|
||||
};
|
||||
|
||||
$(function () {
|
||||
|
||||
@@ -105,6 +105,7 @@ $(function () {
|
||||
$("#valid-date").toggle($("#id_type").val() == "D");
|
||||
$("#valid-datetime").toggle($("#id_type").val() == "W");
|
||||
$("#valid-number").toggle($("#id_type").val() == "N");
|
||||
$("#valid-file").toggle($("#id_type").val() == "F");
|
||||
|
||||
show = $("#id_type").val() == "B" && $("#id_required").prop("checked");
|
||||
$(".alert-required-boolean").toggle(show);
|
||||
|
||||
@@ -318,6 +318,21 @@ input[type=number].short {
|
||||
}
|
||||
}
|
||||
|
||||
.photo-initial {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.photo-preview {
|
||||
max-width: 550px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-top: 10px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.propagated-settings-box {
|
||||
position: relative;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@import "../../pretixbase/scss/_theme.scss";
|
||||
@import "../../typeahead/typeahead.scss";
|
||||
@import "../../charts/morris.scss";
|
||||
@import "../../cropper/cropper.scss";
|
||||
@import "../../datetimepicker/_bootstrap-datetimepicker.scss";
|
||||
@import "_sb-admin-2.scss";
|
||||
@import "_forms.scss";
|
||||
|
||||
@@ -133,6 +133,7 @@ var form_handlers = function (el) {
|
||||
|
||||
el.find("input[name*=question], select[name*=question]").change(questions_toggle_dependent);
|
||||
questions_toggle_dependent();
|
||||
questions_init_photos(el);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -70,6 +70,46 @@ function questions_toggle_dependent(ev) {
|
||||
$(this).prop("required", false).addClass("required-hidden");
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function questions_init_photos(el) {
|
||||
if (!FileReader) {
|
||||
// No browser support
|
||||
return
|
||||
}
|
||||
|
||||
el.find("input[data-portrait-photo]").each(function () {
|
||||
var $inp = $(this)
|
||||
var $container = $inp.parent().parent()
|
||||
|
||||
$container.find(".photo-input").addClass("hidden")
|
||||
$container.find(".photo-buttons").removeClass("hidden")
|
||||
|
||||
$container.find("button[data-action=upload]").click(function () {
|
||||
$inp.click();
|
||||
})
|
||||
|
||||
var cropper = new Cropper($container.find(".photo-preview img").get(0), {
|
||||
aspectRatio: 3 / 4,
|
||||
viewMode: 1,
|
||||
zoomable: false,
|
||||
crop(event) {
|
||||
$container.find("input[type=hidden]").val(JSON.stringify(cropper.getData(true)));
|
||||
},
|
||||
});
|
||||
/* This rule is very important, please don't ignore this */
|
||||
|
||||
$inp.on("change", function () {
|
||||
if (!$inp.get(0).files[0]) return
|
||||
$container.find("button[data-action=upload]").append("<span class='fa fa-spin fa-cog'></span>")
|
||||
var fr = new FileReader()
|
||||
fr.onload = function () {
|
||||
cropper.replace(fr.result)
|
||||
$container.find(".photo-preview").removeClass("hidden")
|
||||
$container.find("button[data-action=upload] .fa-spin").remove()
|
||||
}
|
||||
fr.readAsDataURL($inp.get(0).files[0])
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,3 +142,11 @@
|
||||
.btn-invisible {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.answer-thumb img {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-height: 100px;
|
||||
max-width: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
@@ -102,6 +102,22 @@ a.btn, button.btn {
|
||||
}
|
||||
}
|
||||
|
||||
.photo-initial {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.photo-preview {
|
||||
max-width: 550px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-top: 10px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media(max-width: $screen-xs-max) {
|
||||
.nameparts-form-group {
|
||||
display: block;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@import "../../bootstrap/scss/_bootstrap_reduced.scss";
|
||||
@import "../../pretixbase/scss/_theme.scss";
|
||||
@import "../../lightbox/css/lightbox.scss";
|
||||
@import "../../cropper/cropper.scss";
|
||||
@import "../../datetimepicker/_bootstrap-datetimepicker.scss";
|
||||
@import "../../slider/_bootstrap-slider.scss";
|
||||
@import "../../fontawesome/scss/font-awesome.scss";
|
||||
|
||||
@@ -1848,6 +1848,7 @@ TEST_QUESTION_RES = {
|
||||
"valid_date_max": None,
|
||||
"valid_datetime_min": None,
|
||||
"valid_datetime_max": None,
|
||||
"valid_file_portrait": False,
|
||||
"help_text": {"en": "This is an example question"},
|
||||
"options": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user