小部件在两个字段中填充值

发布于 2024-09-13 00:54:00 字数 110 浏览 3 评论 0原文

我知道如果我需要 django-admin 中的字段的自定义“选择器”,我需要创建一个自定义小部件。 但是,如果小部件必须生成两个值(例如 X 和 Y 坐标)怎么办?如何将它们填充到模型中的两个不同字段中?

I know that if I need a custom "selector" for a field in django-admin I need to create a custom widget.
But what if the widget have to produce two values, for example X and Y coordinates, how can I fill them in two different fields from the model?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

倦话 2024-09-20 00:54:00

您可以查看日期时间字段的实现,该字段在管理中呈现为 2 个字段。

自上而下,

管理员使用

class AdminSplitDateTime(forms.SplitDateTimeWidget):
    """
    A SplitDateTime Widget that has some admin-specific styling.
    """
    def __init__(self, attrs=None):
        widgets = [AdminDateWidget, AdminTimeWidget]
        # Note that we're calling MultiWidget, not SplitDateTimeWidget, because
        # we want to define widgets.
        forms.MultiWidget.__init__(self, widgets, attrs)

    def format_output(self, rendered_widgets):
        return mark_safe(u'<p class="datetime">%s %s<br />%s %s</p>' % \
            (_('Date:'), rendered_widgets[0], _('Time:'), rendered_widgets[1]))

它又使用 SplitDateTimeWidget

class SplitDateTimeWidget(MultiWidget):
    """
    A Widget that splits datetime input into two <input type="text"> boxes.
    """
    date_format = DateInput.format
    time_format = TimeInput.format

    def __init__(self, attrs=None, date_format=None, time_format=None):
        if date_format:
            self.date_format = date_format
        if time_format:
            self.time_format = time_format
        widgets = (DateInput(attrs=attrs, format=self.date_format),
                   TimeInput(attrs=attrs, format=self.time_format))
        super(SplitDateTimeWidget, self).__init__(widgets, attrs)

    def decompress(self, value):
        if value:
            return [value.date(), value.time().replace(microsecond=0)]
        return [None, None]

它又扩展了您在 django.forms.widgets 中定义的 MultiWidget还应该延长。它有许多有用的方法,您可以覆盖它们。

class MultiWidget(Widget):
"""
A widget that is composed of multiple widgets.

Its render() method is different than other widgets', because it has to
figure out how to split a single value for display in multiple widgets.
The ``value`` argument can be one of two things:

    * A list.
    * A normal value (e.g., a string) that has been "compressed" from
      a list of values.

In the second case -- i.e., if the value is NOT a list -- render() will
first "decompress" the value into a list before rendering it. It does so by
calling the decompress() method, which MultiWidget subclasses must
implement. This method takes a single "compressed" value and returns a
list.

When render() does its HTML rendering, each value in the list is rendered
with the corresponding widget -- the first value is rendered in the first
widget, the second value is rendered in the second widget, etc.

Subclasses may implement format_output(), which takes the list of rendered
widgets and returns a string of HTML that formats them any way you'd like.

You'll probably want to use this class with MultiValueField.
"""
def __init__(self, widgets, attrs=None):
    self.widgets = [isinstance(w, type) and w() or w for w in widgets]
    super(MultiWidget, self).__init__(attrs)

def render(self, name, value, attrs=None):
    # value is a list of values, each corresponding to a widget
    # in self.widgets.
    if not isinstance(value, list):
        value = self.decompress(value)
    output = []
    final_attrs = self.build_attrs(attrs)
    id_ = final_attrs.get('id', None)
    for i, widget in enumerate(self.widgets):
        try:
            widget_value = value[i]
        except IndexError:
            widget_value = None
        if id_:
            final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
        output.append(widget.render(name + '_%s' % i, widget_value, final_attrs))
    return mark_safe(self.format_output(output))

def id_for_label(self, id_):
    # See the comment for RadioSelect.id_for_label()
    if id_:
        id_ += '_0'
    return id_
id_for_label = classmethod(id_for_label)

def value_from_datadict(self, data, files, name):
    return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]

def _has_changed(self, initial, data):
    if initial is None:
        initial = [u'' for x in range(0, len(data))]
    else:
        if not isinstance(initial, list):
            initial = self.decompress(initial)
    for widget, initial, data in zip(self.widgets, initial, data):
        if widget._has_changed(initial, data):
            return True
    return False

def format_output(self, rendered_widgets):
    """
    Given a list of rendered widgets (as strings), returns a Unicode string
    representing the HTML for the whole lot.

    This hook allows you to format the HTML design of the widgets, if
    needed.
    """
    return u''.join(rendered_widgets)

def decompress(self, value):
    """
    Returns a list of decompressed values for the given compressed value.
    The given value can be assumed to be valid, but not necessarily
    non-empty.
    """
    raise NotImplementedError('Subclasses must implement this method.')

def _get_media(self):
    "Media for a multiwidget is the combination of all media of the subwidgets"
    media = Media()
    for w in self.widgets:
        media = media + w.media
    return media
media = property(_get_media)

def __deepcopy__(self, memo):
    obj = super(MultiWidget, self).__deepcopy__(memo)
    obj.widgets = copy.deepcopy(self.widgets)
    return obj

You can look at the implementation of the date-time field, that renders as 2 fields in the admin.

Going top-down,

the admin uses

class AdminSplitDateTime(forms.SplitDateTimeWidget):
    """
    A SplitDateTime Widget that has some admin-specific styling.
    """
    def __init__(self, attrs=None):
        widgets = [AdminDateWidget, AdminTimeWidget]
        # Note that we're calling MultiWidget, not SplitDateTimeWidget, because
        # we want to define widgets.
        forms.MultiWidget.__init__(self, widgets, attrs)

    def format_output(self, rendered_widgets):
        return mark_safe(u'<p class="datetime">%s %s<br />%s %s</p>' % \
            (_('Date:'), rendered_widgets[0], _('Time:'), rendered_widgets[1]))

which in turn uses SplitDateTimeWidget:

class SplitDateTimeWidget(MultiWidget):
    """
    A Widget that splits datetime input into two <input type="text"> boxes.
    """
    date_format = DateInput.format
    time_format = TimeInput.format

    def __init__(self, attrs=None, date_format=None, time_format=None):
        if date_format:
            self.date_format = date_format
        if time_format:
            self.time_format = time_format
        widgets = (DateInput(attrs=attrs, format=self.date_format),
                   TimeInput(attrs=attrs, format=self.time_format))
        super(SplitDateTimeWidget, self).__init__(widgets, attrs)

    def decompress(self, value):
        if value:
            return [value.date(), value.time().replace(microsecond=0)]
        return [None, None]

Which in turn extends the MultiWidget defined in django.forms.widgets which you should also extend. It has many useful methods which you can override.

class MultiWidget(Widget):
"""
A widget that is composed of multiple widgets.

Its render() method is different than other widgets', because it has to
figure out how to split a single value for display in multiple widgets.
The ``value`` argument can be one of two things:

    * A list.
    * A normal value (e.g., a string) that has been "compressed" from
      a list of values.

In the second case -- i.e., if the value is NOT a list -- render() will
first "decompress" the value into a list before rendering it. It does so by
calling the decompress() method, which MultiWidget subclasses must
implement. This method takes a single "compressed" value and returns a
list.

When render() does its HTML rendering, each value in the list is rendered
with the corresponding widget -- the first value is rendered in the first
widget, the second value is rendered in the second widget, etc.

Subclasses may implement format_output(), which takes the list of rendered
widgets and returns a string of HTML that formats them any way you'd like.

You'll probably want to use this class with MultiValueField.
"""
def __init__(self, widgets, attrs=None):
    self.widgets = [isinstance(w, type) and w() or w for w in widgets]
    super(MultiWidget, self).__init__(attrs)

def render(self, name, value, attrs=None):
    # value is a list of values, each corresponding to a widget
    # in self.widgets.
    if not isinstance(value, list):
        value = self.decompress(value)
    output = []
    final_attrs = self.build_attrs(attrs)
    id_ = final_attrs.get('id', None)
    for i, widget in enumerate(self.widgets):
        try:
            widget_value = value[i]
        except IndexError:
            widget_value = None
        if id_:
            final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
        output.append(widget.render(name + '_%s' % i, widget_value, final_attrs))
    return mark_safe(self.format_output(output))

def id_for_label(self, id_):
    # See the comment for RadioSelect.id_for_label()
    if id_:
        id_ += '_0'
    return id_
id_for_label = classmethod(id_for_label)

def value_from_datadict(self, data, files, name):
    return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]

def _has_changed(self, initial, data):
    if initial is None:
        initial = [u'' for x in range(0, len(data))]
    else:
        if not isinstance(initial, list):
            initial = self.decompress(initial)
    for widget, initial, data in zip(self.widgets, initial, data):
        if widget._has_changed(initial, data):
            return True
    return False

def format_output(self, rendered_widgets):
    """
    Given a list of rendered widgets (as strings), returns a Unicode string
    representing the HTML for the whole lot.

    This hook allows you to format the HTML design of the widgets, if
    needed.
    """
    return u''.join(rendered_widgets)

def decompress(self, value):
    """
    Returns a list of decompressed values for the given compressed value.
    The given value can be assumed to be valid, but not necessarily
    non-empty.
    """
    raise NotImplementedError('Subclasses must implement this method.')

def _get_media(self):
    "Media for a multiwidget is the combination of all media of the subwidgets"
    media = Media()
    for w in self.widgets:
        media = media + w.media
    return media
media = property(_get_media)

def __deepcopy__(self, memo):
    obj = super(MultiWidget, self).__deepcopy__(memo)
    obj.widgets = copy.deepcopy(self.widgets)
    return obj
Smile简单爱 2024-09-20 00:54:00

Jannis Leidel 很久以前就发布了一个小部件。 django 坐标字段
据我记得,它从地图中获取坐标并将其传递给一个字段,然后一些 JavaScript 将其切成 2 个字段的 2 个坐标。

与自定义 表单 结合使用,它应该可以很好地工作出色地

Jannis Leidel released a widget quite a long time ago. django-coordinatesfield
As far as I remember, it took the coordinates from a map and passed it a single field and some javascript cut it into 2 coordinates for 2 fields.

Combined with a custom form it should work quite well

春花秋月 2024-09-20 00:54:00

以下是 ModelForm 的示例:
http://www.adamalton.co .uk/blog/displaying-django-genericforeignkey-as-single-form-field/

在表单中添加一个额外的表单字段(对于您的单个小部件)并排除两个“真实”字段,然后覆盖 init并保存方法来执行使其工作的额外逻辑。

另外,同样的问题:
如何获取要设置的单个小部件Django 中有 2 个字段?

Here's an example for a ModelForm:
http://www.adamalton.co.uk/blog/displaying-django-genericforeignkey-as-single-form-field/

Add an extra form field to the form (for your single widget) and exclude the two 'real' fields, then override the init and save methods to do the extra logic that makes it work.

Also, same question:
How to get a single widget to set 2 fields in Django?

乱了心跳 2024-09-20 00:54:00

您可以使小部件呈现两个(隐藏)html 输入,其名称与需要填充的模型字段相关,并通过 javascript 为它们分配必要的值!

You can make the widget render two (hidden) html inputs, whose names relate to the model's fields that need to be filled and assign the necessary values via javascript to them!

開玄 2024-09-20 00:54:00

我花了一些时间试图让 django 屈服于我的意愿,并意识到我对这个问题想得太多了。问题实际上不是用一个小部件设置两个字段,而是能够在同一页面上带有地图的表单上设置两个字段。那么问题就很简单了。假设您有一个从模型派生的表单,该模型具有“纬度”和“经度”两个字段。现在您需要做的就是在模板页面上放置一个地图来设置这些字段,并且可能会显示一个标记。例如,当您单击地图时。有几种方法可以做到这一点,但其中一种是:

{% block head %}
<link href="{% static 'css/map.css' %}" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
    integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"
    integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
{% endblock %}

{% block content %}
{{ form.media }}
<div id='map' style='height: 400px; width: 600px;'></div>
<div class="row">
    <div class="col">
        <div id='map' style='height: 400px; width: 600px;'></div>
        <form method="post">
            {% csrf_token %}
            {% crispy form %}
        </form>
    </div>
</div>

<script>

    //Get lat lon elements
    var lat_element = document.getElementsByName("latitude")[0]
    var lon_element = document.getElementsByName("longitude")[0]

    // Create a marker that will be moved to the current location
    var marker = L.marker([0.0, 0.0])

    // Function that will set the location
    function onMapClick(e) {
        marker.setLatLng(e.latlng).addTo(map);
        
        // Set the form element values with the new pos
        lat_element.value = e.latlng['lat'].toFixed(6).toString();
        lon_element.value = e.latlng['lng'].toFixed(6).toString();

    }

    //Get existing lat lon
    var lat = parseFloat(lat_element.value);
    var lon = parseFloat(lon_element.value);

    // initialize the map
    var map = L.map('map').setView([0.0, 0.0], 1).on('click', onMapClick);

    // load a tile layer
    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        maxZoom: 19,
        attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
    }).addTo(map);

    if (!isNaN(lat)) {
        marker.setLatLng([lat, lon]).addTo(map);
        map.setView([lat, lon], 12)
    }

</script>
{% endblock content %}

当然这是一个非常简单的布局和实现,但我认为一旦你看到这根本不需要是一个 django 问题,而是一个更普遍的前端设计问题,很明显有一百万种方法可以做到这一点。由于这个原因,我没有在答案中放入任何 django 代码。就我个人而言,我使用的是脆脆的表单,它带有一个方便的 Layout 类,我将把地图 div 元素放在该 Layout 中,而不是放在模板中。可能性是无限的。

在我看来,django 小部件和表单的构建理念是“一个小部件 == 一个字段”,并且尝试将其偏离这一点可能比上述解决方案更加复杂。

I spent some time trying to bend django to my will on this and realised I was overthinking the problem. The issue really isn't setting two fields with one widget, it's just being able to set two fields on a form with a map on the same page. Then the problem is quite simple. Say you have a form derived from a model that has two fields 'latitude' and 'longitude'. Now all you need to do is put a map on the template page that sets those fields, and maybe displays a a marker. For example when you click the map. There are a few ways to do this but one would be:

{% block head %}
<link href="{% static 'css/map.css' %}" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
    integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"
    integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
{% endblock %}

{% block content %}
{{ form.media }}
<div id='map' style='height: 400px; width: 600px;'></div>
<div class="row">
    <div class="col">
        <div id='map' style='height: 400px; width: 600px;'></div>
        <form method="post">
            {% csrf_token %}
            {% crispy form %}
        </form>
    </div>
</div>

<script>

    //Get lat lon elements
    var lat_element = document.getElementsByName("latitude")[0]
    var lon_element = document.getElementsByName("longitude")[0]

    // Create a marker that will be moved to the current location
    var marker = L.marker([0.0, 0.0])

    // Function that will set the location
    function onMapClick(e) {
        marker.setLatLng(e.latlng).addTo(map);
        
        // Set the form element values with the new pos
        lat_element.value = e.latlng['lat'].toFixed(6).toString();
        lon_element.value = e.latlng['lng'].toFixed(6).toString();

    }

    //Get existing lat lon
    var lat = parseFloat(lat_element.value);
    var lon = parseFloat(lon_element.value);

    // initialize the map
    var map = L.map('map').setView([0.0, 0.0], 1).on('click', onMapClick);

    // load a tile layer
    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        maxZoom: 19,
        attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
    }).addTo(map);

    if (!isNaN(lat)) {
        marker.setLatLng([lat, lon]).addTo(map);
        map.setView([lat, lon], 12)
    }

</script>
{% endblock content %}

Of course that's a very simple layout and implementation but I think that once you see this doesn't need to be a django problem at all but a more general front-end design problem, it becomes clear there are a million ways to do this. I haven't put any django code in the answer for that reason. Personally I'm using crispy forms, which comes with a handy Layout class and I'll be putting the map div element in that Layout rather than in the template. The possibilities are endless.

It seems to me django widgets and forms are built with the idea that one widget == one field and trying to bend it away from that could be endlessly more complicated than the above solution.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文