mirror of https://github.com/django/django.git
Fixed #7975 -- Callable defaults in inline model formsets now work correctly. Based on patch from msaelices. Thanks for your hard work msaelices.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@8816 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
ca7db155aa
commit
7c7ad041b3
|
@ -231,7 +231,7 @@ class Field(object):
|
||||||
|
|
||||||
def get_default(self):
|
def get_default(self):
|
||||||
"Returns the default value for this field."
|
"Returns the default value for this field."
|
||||||
if self.default is not NOT_PROVIDED:
|
if self.has_default():
|
||||||
if callable(self.default):
|
if callable(self.default):
|
||||||
return self.default()
|
return self.default()
|
||||||
return force_unicode(self.default, strings_only=True)
|
return force_unicode(self.default, strings_only=True)
|
||||||
|
@ -306,7 +306,8 @@ class Field(object):
|
||||||
defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
|
defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
|
||||||
if self.has_default():
|
if self.has_default():
|
||||||
defaults['initial'] = self.get_default()
|
defaults['initial'] = self.get_default()
|
||||||
|
if callable(self.default):
|
||||||
|
defaults['show_hidden_initial'] = True
|
||||||
if self.choices:
|
if self.choices:
|
||||||
# Fields with choices get special treatment.
|
# Fields with choices get special treatment.
|
||||||
include_blank = self.blank or not (self.has_default() or 'initial' in kwargs)
|
include_blank = self.blank or not (self.has_default() or 'initial' in kwargs)
|
||||||
|
@ -314,9 +315,7 @@ class Field(object):
|
||||||
defaults['coerce'] = self.to_python
|
defaults['coerce'] = self.to_python
|
||||||
if self.null:
|
if self.null:
|
||||||
defaults['empty_value'] = None
|
defaults['empty_value'] = None
|
||||||
|
|
||||||
form_class = forms.TypedChoiceField
|
form_class = forms.TypedChoiceField
|
||||||
|
|
||||||
# Many of the subclass-specific formfield arguments (min_value,
|
# Many of the subclass-specific formfield arguments (min_value,
|
||||||
# max_value) don't apply for choice fields, so be sure to only pass
|
# max_value) don't apply for choice fields, so be sure to only pass
|
||||||
# the values that TypedChoiceField will understand.
|
# the values that TypedChoiceField will understand.
|
||||||
|
@ -325,7 +324,6 @@ class Field(object):
|
||||||
'widget', 'label', 'initial', 'help_text',
|
'widget', 'label', 'initial', 'help_text',
|
||||||
'error_messages'):
|
'error_messages'):
|
||||||
del kwargs[k]
|
del kwargs[k]
|
||||||
|
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
return form_class(**defaults)
|
return form_class(**defaults)
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.encoding import smart_unicode, smart_str
|
from django.utils.encoding import smart_unicode, smart_str
|
||||||
|
|
||||||
from util import ErrorList, ValidationError
|
from util import ErrorList, ValidationError
|
||||||
from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput, TimeInput
|
from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput, TimeInput, SplitHiddenDateTimeWidget
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile as UploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile as UploadedFile
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
@ -59,7 +59,7 @@ class Field(object):
|
||||||
creation_counter = 0
|
creation_counter = 0
|
||||||
|
|
||||||
def __init__(self, required=True, widget=None, label=None, initial=None,
|
def __init__(self, required=True, widget=None, label=None, initial=None,
|
||||||
help_text=None, error_messages=None):
|
help_text=None, error_messages=None, show_hidden_initial=False):
|
||||||
# required -- Boolean that specifies whether the field is required.
|
# required -- Boolean that specifies whether the field is required.
|
||||||
# True by default.
|
# True by default.
|
||||||
# widget -- A Widget class, or instance of a Widget class, that should
|
# widget -- A Widget class, or instance of a Widget class, that should
|
||||||
|
@ -73,9 +73,12 @@ class Field(object):
|
||||||
# initial -- A value to use in this Field's initial display. This value
|
# initial -- A value to use in this Field's initial display. This value
|
||||||
# is *not* used as a fallback if data isn't given.
|
# is *not* used as a fallback if data isn't given.
|
||||||
# help_text -- An optional string to use as "help text" for this Field.
|
# help_text -- An optional string to use as "help text" for this Field.
|
||||||
|
# show_hidden_initial -- Boolean that specifies if it is needed to render a
|
||||||
|
# hidden widget with initial value after widget.
|
||||||
if label is not None:
|
if label is not None:
|
||||||
label = smart_unicode(label)
|
label = smart_unicode(label)
|
||||||
self.required, self.label, self.initial = required, label, initial
|
self.required, self.label, self.initial = required, label, initial
|
||||||
|
self.show_hidden_initial = show_hidden_initial
|
||||||
if help_text is None:
|
if help_text is None:
|
||||||
self.help_text = u''
|
self.help_text = u''
|
||||||
else:
|
else:
|
||||||
|
@ -840,6 +843,7 @@ class FilePathField(ChoiceField):
|
||||||
self.widget.choices = self.choices
|
self.widget.choices = self.choices
|
||||||
|
|
||||||
class SplitDateTimeField(MultiValueField):
|
class SplitDateTimeField(MultiValueField):
|
||||||
|
hidden_widget = SplitHiddenDateTimeWidget
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
'invalid_date': _(u'Enter a valid date.'),
|
'invalid_date': _(u'Enter a valid date.'),
|
||||||
'invalid_time': _(u'Enter a valid time.'),
|
'invalid_time': _(u'Enter a valid time.'),
|
||||||
|
|
|
@ -128,6 +128,12 @@ class BaseForm(StrAndUnicode):
|
||||||
"""
|
"""
|
||||||
return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name
|
return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name
|
||||||
|
|
||||||
|
def add_initial_prefix(self, field_name):
|
||||||
|
"""
|
||||||
|
Add a 'initial' prefix for checking dynamic initial values
|
||||||
|
"""
|
||||||
|
return u'initial-%s' % self.add_prefix(field_name)
|
||||||
|
|
||||||
def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
|
def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
|
||||||
"Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()."
|
"Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()."
|
||||||
top_errors = self.non_field_errors() # Errors that should be displayed above all fields.
|
top_errors = self.non_field_errors() # Errors that should be displayed above all fields.
|
||||||
|
@ -245,7 +251,7 @@ class BaseForm(StrAndUnicode):
|
||||||
Returns True if data differs from initial.
|
Returns True if data differs from initial.
|
||||||
"""
|
"""
|
||||||
return bool(self.changed_data)
|
return bool(self.changed_data)
|
||||||
|
|
||||||
def _get_changed_data(self):
|
def _get_changed_data(self):
|
||||||
if self._changed_data is None:
|
if self._changed_data is None:
|
||||||
self._changed_data = []
|
self._changed_data = []
|
||||||
|
@ -258,7 +264,13 @@ class BaseForm(StrAndUnicode):
|
||||||
for name, field in self.fields.items():
|
for name, field in self.fields.items():
|
||||||
prefixed_name = self.add_prefix(name)
|
prefixed_name = self.add_prefix(name)
|
||||||
data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
|
data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
|
||||||
initial_value = self.initial.get(name, field.initial)
|
if not field.show_hidden_initial:
|
||||||
|
initial_value = self.initial.get(name, field.initial)
|
||||||
|
else:
|
||||||
|
initial_prefixed_name = self.add_initial_prefix(name)
|
||||||
|
hidden_widget = field.hidden_widget()
|
||||||
|
initial_value = hidden_widget.value_from_datadict(
|
||||||
|
self.data, self.files, initial_prefixed_name)
|
||||||
if field.widget._has_changed(initial_value, data_value):
|
if field.widget._has_changed(initial_value, data_value):
|
||||||
self._changed_data.append(name)
|
self._changed_data.append(name)
|
||||||
return self._changed_data
|
return self._changed_data
|
||||||
|
@ -300,6 +312,7 @@ class BoundField(StrAndUnicode):
|
||||||
self.field = field
|
self.field = field
|
||||||
self.name = name
|
self.name = name
|
||||||
self.html_name = form.add_prefix(name)
|
self.html_name = form.add_prefix(name)
|
||||||
|
self.html_initial_name = form.add_initial_prefix(name)
|
||||||
if self.field.label is None:
|
if self.field.label is None:
|
||||||
self.label = pretty_name(name)
|
self.label = pretty_name(name)
|
||||||
else:
|
else:
|
||||||
|
@ -308,6 +321,8 @@ class BoundField(StrAndUnicode):
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
"""Renders this field as an HTML widget."""
|
"""Renders this field as an HTML widget."""
|
||||||
|
if self.field.show_hidden_initial:
|
||||||
|
return self.as_widget() + self.as_hidden(only_initial=True)
|
||||||
return self.as_widget()
|
return self.as_widget()
|
||||||
|
|
||||||
def _errors(self):
|
def _errors(self):
|
||||||
|
@ -318,7 +333,7 @@ class BoundField(StrAndUnicode):
|
||||||
return self.form.errors.get(self.name, self.form.error_class())
|
return self.form.errors.get(self.name, self.form.error_class())
|
||||||
errors = property(_errors)
|
errors = property(_errors)
|
||||||
|
|
||||||
def as_widget(self, widget=None, attrs=None):
|
def as_widget(self, widget=None, attrs=None, only_initial=False):
|
||||||
"""
|
"""
|
||||||
Renders the field by rendering the passed widget, adding any HTML
|
Renders the field by rendering the passed widget, adding any HTML
|
||||||
attributes passed as attrs. If no widget is specified, then the
|
attributes passed as attrs. If no widget is specified, then the
|
||||||
|
@ -330,29 +345,33 @@ class BoundField(StrAndUnicode):
|
||||||
auto_id = self.auto_id
|
auto_id = self.auto_id
|
||||||
if auto_id and 'id' not in attrs and 'id' not in widget.attrs:
|
if auto_id and 'id' not in attrs and 'id' not in widget.attrs:
|
||||||
attrs['id'] = auto_id
|
attrs['id'] = auto_id
|
||||||
if not self.form.is_bound:
|
if not self.form.is_bound or only_initial:
|
||||||
data = self.form.initial.get(self.name, self.field.initial)
|
data = self.form.initial.get(self.name, self.field.initial)
|
||||||
if callable(data):
|
if callable(data):
|
||||||
data = data()
|
data = data()
|
||||||
else:
|
else:
|
||||||
data = self.data
|
data = self.data
|
||||||
return widget.render(self.html_name, data, attrs=attrs)
|
if not only_initial:
|
||||||
|
name = self.html_name
|
||||||
def as_text(self, attrs=None):
|
else:
|
||||||
|
name = self.html_initial_name
|
||||||
|
return widget.render(name, data, attrs=attrs)
|
||||||
|
|
||||||
|
def as_text(self, attrs=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Returns a string of HTML for representing this as an <input type="text">.
|
Returns a string of HTML for representing this as an <input type="text">.
|
||||||
"""
|
"""
|
||||||
return self.as_widget(TextInput(), attrs)
|
return self.as_widget(TextInput(), attrs, **kwargs)
|
||||||
|
|
||||||
def as_textarea(self, attrs=None):
|
def as_textarea(self, attrs=None, **kwargs):
|
||||||
"Returns a string of HTML for representing this as a <textarea>."
|
"Returns a string of HTML for representing this as a <textarea>."
|
||||||
return self.as_widget(Textarea(), attrs)
|
return self.as_widget(Textarea(), attrs, **kwargs)
|
||||||
|
|
||||||
def as_hidden(self, attrs=None):
|
def as_hidden(self, attrs=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Returns a string of HTML for representing this as an <input type="hidden">.
|
Returns a string of HTML for representing this as an <input type="hidden">.
|
||||||
"""
|
"""
|
||||||
return self.as_widget(self.field.hidden_widget(), attrs)
|
return self.as_widget(self.field.hidden_widget(), attrs, **kwargs)
|
||||||
|
|
||||||
def _data(self):
|
def _data(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -25,7 +25,8 @@ __all__ = (
|
||||||
'HiddenInput', 'MultipleHiddenInput',
|
'HiddenInput', 'MultipleHiddenInput',
|
||||||
'FileInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput',
|
'FileInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput',
|
||||||
'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
|
'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
|
||||||
'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget',
|
'CheckboxSelectMultiple', 'MultiWidget',
|
||||||
|
'SplitDateTimeWidget',
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_TYPES = ('css','js')
|
MEDIA_TYPES = ('css','js')
|
||||||
|
@ -617,7 +618,8 @@ class MultiWidget(Widget):
|
||||||
if initial is None:
|
if initial is None:
|
||||||
initial = [u'' for x in range(0, len(data))]
|
initial = [u'' for x in range(0, len(data))]
|
||||||
else:
|
else:
|
||||||
initial = self.decompress(initial)
|
if not isinstance(initial, list):
|
||||||
|
initial = self.decompress(initial)
|
||||||
for widget, initial, data in zip(self.widgets, initial, data):
|
for widget, initial, data in zip(self.widgets, initial, data):
|
||||||
if widget._has_changed(initial, data):
|
if widget._has_changed(initial, data):
|
||||||
return True
|
return True
|
||||||
|
@ -662,3 +664,11 @@ class SplitDateTimeWidget(MultiWidget):
|
||||||
return [value.date(), value.time().replace(microsecond=0)]
|
return [value.date(), value.time().replace(microsecond=0)]
|
||||||
return [None, None]
|
return [None, None]
|
||||||
|
|
||||||
|
class SplitHiddenDateTimeWidget(SplitDateTimeWidget):
|
||||||
|
"""
|
||||||
|
A Widget that splits datetime input into two <input type="hidden"> inputs.
|
||||||
|
"""
|
||||||
|
def __init__(self, attrs=None):
|
||||||
|
widgets = (HiddenInput(attrs=attrs), HiddenInput(attrs=attrs))
|
||||||
|
super(SplitDateTimeWidget, self).__init__(widgets, attrs)
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django import forms
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -92,6 +96,16 @@ class Price(models.Model):
|
||||||
class MexicanRestaurant(Restaurant):
|
class MexicanRestaurant(Restaurant):
|
||||||
serves_tacos = models.BooleanField()
|
serves_tacos = models.BooleanField()
|
||||||
|
|
||||||
|
# models for testing callable defaults (see bug #7975). If you define a model
|
||||||
|
# with a callable default value, you cannot rely on the initial value in a
|
||||||
|
# form.
|
||||||
|
class Person(models.Model):
|
||||||
|
name = models.CharField(max_length=128)
|
||||||
|
|
||||||
|
class Membership(models.Model):
|
||||||
|
person = models.ForeignKey(Person)
|
||||||
|
date_joined = models.DateTimeField(default=datetime.datetime.now)
|
||||||
|
karma = models.IntegerField()
|
||||||
|
|
||||||
__test__ = {'API_TESTS': """
|
__test__ = {'API_TESTS': """
|
||||||
|
|
||||||
|
@ -621,4 +635,71 @@ False
|
||||||
>>> formset.errors
|
>>> formset.errors
|
||||||
[{'__all__': [u'Price with this Price and Quantity already exists.']}]
|
[{'__all__': [u'Price with this Price and Quantity already exists.']}]
|
||||||
|
|
||||||
|
# Use of callable defaults (see bug #7975).
|
||||||
|
|
||||||
|
>>> person = Person.objects.create(name='Ringo')
|
||||||
|
>>> FormSet = inlineformset_factory(Person, Membership, can_delete=False, extra=1)
|
||||||
|
>>> formset = FormSet(instance=person)
|
||||||
|
|
||||||
|
# Django will render a hidden field for model fields that have a callable
|
||||||
|
# default. This is required to ensure the value is tested for change correctly
|
||||||
|
# when determine what extra forms have changed to save.
|
||||||
|
|
||||||
|
>>> form = formset.forms[0] # this formset only has one form
|
||||||
|
>>> now = form.fields['date_joined'].initial
|
||||||
|
>>> print form.as_p()
|
||||||
|
<p><label for="id_membership_set-0-date_joined">Date joined:</label> <input type="text" name="membership_set-0-date_joined" value="..." id="id_membership_set-0-date_joined" /><input type="hidden" name="initial-membership_set-0-date_joined" value="..." id="id_membership_set-0-date_joined" /></p>
|
||||||
|
<p><label for="id_membership_set-0-karma">Karma:</label> <input type="text" name="membership_set-0-karma" id="id_membership_set-0-karma" /><input type="hidden" name="membership_set-0-id" id="id_membership_set-0-id" /></p>
|
||||||
|
|
||||||
|
# test for validation with callable defaults. Validations rely on hidden fields
|
||||||
|
|
||||||
|
>>> data = {
|
||||||
|
... 'membership_set-TOTAL_FORMS': '1',
|
||||||
|
... 'membership_set-INITIAL_FORMS': '0',
|
||||||
|
... 'membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')),
|
||||||
|
... 'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')),
|
||||||
|
... 'membership_set-0-karma': '',
|
||||||
|
... }
|
||||||
|
>>> formset = FormSet(data, instance=person)
|
||||||
|
>>> formset.is_valid()
|
||||||
|
True
|
||||||
|
|
||||||
|
# now test for when the data changes
|
||||||
|
|
||||||
|
>>> one_day_later = now + datetime.timedelta(days=1)
|
||||||
|
>>> filled_data = {
|
||||||
|
... 'membership_set-TOTAL_FORMS': '1',
|
||||||
|
... 'membership_set-INITIAL_FORMS': '0',
|
||||||
|
... 'membership_set-0-date_joined': unicode(one_day_later.strftime('%Y-%m-%d %H:%M:%S')),
|
||||||
|
... 'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')),
|
||||||
|
... 'membership_set-0-karma': '',
|
||||||
|
... }
|
||||||
|
>>> formset = FormSet(filled_data, instance=person)
|
||||||
|
>>> formset.is_valid()
|
||||||
|
False
|
||||||
|
|
||||||
|
# now test with split datetime fields
|
||||||
|
|
||||||
|
>>> class MembershipForm(forms.ModelForm):
|
||||||
|
... date_joined = forms.SplitDateTimeField(initial=now)
|
||||||
|
... class Meta:
|
||||||
|
... model = Membership
|
||||||
|
... def __init__(self, **kwargs):
|
||||||
|
... super(MembershipForm, self).__init__(**kwargs)
|
||||||
|
... self.fields['date_joined'].widget = forms.SplitDateTimeWidget()
|
||||||
|
|
||||||
|
>>> FormSet = inlineformset_factory(Person, Membership, form=MembershipForm, can_delete=False, extra=1)
|
||||||
|
>>> data = {
|
||||||
|
... 'membership_set-TOTAL_FORMS': '1',
|
||||||
|
... 'membership_set-INITIAL_FORMS': '0',
|
||||||
|
... 'membership_set-0-date_joined_0': unicode(now.strftime('%Y-%m-%d')),
|
||||||
|
... 'membership_set-0-date_joined_1': unicode(now.strftime('%H:%M:%S')),
|
||||||
|
... 'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')),
|
||||||
|
... 'membership_set-0-karma': '',
|
||||||
|
... }
|
||||||
|
>>> formset = FormSet(data, instance=person)
|
||||||
|
>>> formset.is_valid()
|
||||||
|
True
|
||||||
|
|
||||||
|
|
||||||
"""}
|
"""}
|
||||||
|
|
|
@ -1093,7 +1093,7 @@ u'<input type="text" name="date" value="2007-09-17 12:51:34" />'
|
||||||
>>> w.render('date', datetime.datetime(2007, 9, 17, 12, 51))
|
>>> w.render('date', datetime.datetime(2007, 9, 17, 12, 51))
|
||||||
u'<input type="text" name="date" value="2007-09-17 12:51:00" />'
|
u'<input type="text" name="date" value="2007-09-17 12:51:00" />'
|
||||||
|
|
||||||
# TimeInput ###############################################################
|
# TimeInput ###################################################################
|
||||||
|
|
||||||
>>> w = TimeInput()
|
>>> w = TimeInput()
|
||||||
>>> w.render('time', None)
|
>>> w.render('time', None)
|
||||||
|
@ -1113,5 +1113,20 @@ u'<input type="text" name="time" value="12:51:00" />'
|
||||||
We should be able to initialize from a unicode value.
|
We should be able to initialize from a unicode value.
|
||||||
>>> w.render('time', u'13:12:11')
|
>>> w.render('time', u'13:12:11')
|
||||||
u'<input type="text" name="time" value="13:12:11" />'
|
u'<input type="text" name="time" value="13:12:11" />'
|
||||||
|
|
||||||
|
# SplitHiddenDateTimeWidget ###################################################
|
||||||
|
|
||||||
|
>>> from django.forms.widgets import SplitHiddenDateTimeWidget
|
||||||
|
|
||||||
|
>>> w = SplitHiddenDateTimeWidget()
|
||||||
|
>>> w.render('date', '')
|
||||||
|
u'<input type="hidden" name="date_0" /><input type="hidden" name="date_1" />'
|
||||||
|
>>> w.render('date', d)
|
||||||
|
u'<input type="hidden" name="date_0" value="2007-09-17" /><input type="hidden" name="date_1" value="12:51:34" />'
|
||||||
|
>>> w.render('date', datetime.datetime(2007, 9, 17, 12, 51, 34))
|
||||||
|
u'<input type="hidden" name="date_0" value="2007-09-17" /><input type="hidden" name="date_1" value="12:51:34" />'
|
||||||
|
>>> w.render('date', datetime.datetime(2007, 9, 17, 12, 51))
|
||||||
|
u'<input type="hidden" name="date_0" value="2007-09-17" /><input type="hidden" name="date_1" value="12:51:00" />'
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue