Приказанный ManyToManyField, который может использоваться в fieldsets

Я работал через заказанный виджет ManyToManyField и имею аспект фронтенда его работающий приятно:

alt text

К сожалению, я испытываю много затруднений, получая работу бэкенда. Очевидный способ поднять трубку бэкенд состоит в том, чтобы использовать a through таблица выключила модель с ForeignKeys обеим сторонам отношений и перезаписи метод сохранения. Это работало бы отлично, за исключением того, что из-за особенностей содержания, это - абсолютное требование что этот виджет быть помещенным в fieldset (использующий ModelAdmin fieldsets свойство), который, по-видимому, не возможен.

Я вне идей. Какие-либо предложения?

Спасибо!

9
задан Glorfindel 1 August 2019 в 05:09
поделиться

1 ответ

Что касается настройки моделей , вы правы в том, что сквозная таблица со столбцом «порядок» - идеальный способ ее представления. Вы также правы в том, что Django не позволит вам ссылаться на эти отношения в наборе полей. Уловка для решения этой проблемы заключается в том, чтобы помнить, что имена полей, которые вы указываете в «fieldsets» или «fields» ModelAdmin , на самом деле не относятся к полям Model , но к полям ModelForm , которые мы можем игнорировать к удовольствию нашего сердца. С полями many2many это становится сложно, но потерпите меня:

Допустим, вы пытаетесь представить соревнования и конкурентов, которые соревнуются в них, с упорядоченным many2many между соревнованиями и конкурентами, где порядок представляет рейтинг конкурентов в этом конкурс. Ваш models.py будет выглядеть так:

from django.db import models

class Contest(models.Model):
    name = models.CharField(max_length=50)
    # More fields here, if you like.
    contestants = models.ManyToManyField('Contestant', through='ContestResults')

class Contestant(models.Model):
    name = models.CharField(max_length=50)

class ContestResults(models.Model):
    contest = models.ForeignKey(Contest)
    contestant = models.ForeignKey(Contestant)
    rank = models.IntegerField()

Надеюсь, это похоже на то, с чем вы имеете дело. Теперь для админа. Я написал пример admin.py с множеством комментариев, чтобы объяснить, что происходит, но вот краткое изложение, которое поможет вам:

Поскольку у меня нет кода для заказанного виджета m2m, вы Я уже писал, я использовал фиктивный виджет-заполнитель, который просто наследуется от TextInput .Входные данные содержат список разделенных запятыми (без пробелов) идентификаторов участников, а порядок их появления в строке определяет значение их столбца «ранг» в модели ContestResults .

Что происходит, так это то, что мы заменяем стандартную ModelForm для Contest нашей собственной, а затем определяем внутри него поле «результаты» (мы не можем называть поле «участники», так как конфликт имени с полем m2m в модели). Затем мы переопределяем __ init __ () , который вызывается, когда форма отображается в админке, поэтому мы можем получить любые результаты ContestResults, которые, возможно, уже были определены для конкурса, и использовать их для заполнения виджета. Мы также переопределяем save () , чтобы мы могли, в свою очередь, получить данные из виджета и создать необходимые ContestResults.

Обратите внимание, что для простоты в этом примере опущены такие вещи, как проверка данных из виджета, поэтому что-то сломается, если вы попытаетесь ввести что-нибудь неожиданное в текстовый ввод. Кроме того, код для создания ContestResults довольно упрощен, и его можно значительно улучшить.

Я также должен добавить, что я действительно запустил этот код и убедился, что он работает.

from django import forms
from django.contrib import admin
from models import Contest, Contestant, ContestResults

# Generates a function that sequentially calls the two functions that were
# passed to it
def func_concat(old_func, new_func):
    def function():
        old_func()
        new_func()
    return function

# A dummy widget to be replaced with your own.
class OrderedManyToManyWidget(forms.widgets.TextInput):
    pass

# A simple CharField that shows a comma-separated list of contestant IDs.
class ResultsField(forms.CharField):
    widget = OrderedManyToManyWidget()

class ContestAdminForm(forms.models.ModelForm):
    # Any fields declared here can be referred to in the "fieldsets" or
    # "fields" of the ModelAdmin. It is crucial that our custom field does not
    # use the same name as the m2m field field in the model ("contestants" in
    # our example).
    results = ResultsField()

    # Be sure to specify your model here.
    class Meta:
        model = Contest

    # Override init so we can populate the form field with the existing data.
    def __init__(self, *args, **kwargs):
        instance = kwargs.get('instance', None)
        # See if we are editing an existing Contest. If not, there is nothing
        # to be done.
        if instance and instance.pk:
            # Get a list of all the IDs of the contestants already specified
            # for this contest.
            contestants = ContestResults.objects.filter(contest=instance).order_by('rank').values_list('contestant_id', flat=True)
            # Make them into a comma-separated string, and put them in our
            # custom field.
            self.base_fields['results'].initial = ','.join(map(str, contestants))
            # Depending on how you've written your widget, you can pass things
            # like a list of available contestants to it here, if necessary.
        super(ContestAdminForm, self).__init__(*args, **kwargs)

    def save(self, *args, **kwargs):
        # This "commit" business complicates things somewhat. When true, it 
        # means that the model instance will actually be saved and all is
        # good. When false, save() returns an unsaved instance of the model.
        # When save() calls are made by the Django admin, commit is pretty
        # much invariably false, though I'm not sure why. This is a problem
        # because when creating a new Contest instance, it needs to have been
        # saved in the DB and have a PK, before we can create ContestResults.
        # Fortunately, all models have a built-in method called save_m2m()
        # which will always be executed after save(), and we can append our
        # ContestResults-creating code to the existing same_m2m() method.
        commit = kwargs.get('commit', True)
        # Save the Contest and get an instance of the saved model
        instance = super(ContestAdminForm, self).save(*args, **kwargs)
        # This is known as a lexical closure, which means that if we store
        # this function and execute it later on, it will execute in the same
        # context (i.e. it will have access to the current instance and self).
        def save_m2m():
            # This is really naive code and should be improved upon,
            # especially in terms of validation, but the basic gist is to make
            # the needed ContestResults. For now, we'll just delete any
            # existing ContestResults for this Contest and create them anew.
            ContestResults.objects.filter(contest=instance).delete()
            # Make a list of (rank, contestant ID) tuples from the comma-
            # -separated list of contestant IDs we get from the results field.
            formdata = enumerate(map(int, self.cleaned_data['results'].split(',')), 1)
            for rank, contestant in formdata:
                ContestResults.objects.create(contest=instance, contestant_id=contestant, rank=rank)
        if commit:
            # If we're committing (fat chance), simply run the closure.
            save_m2m()
        else:
            # Using a function concatenator, ensure our save_m2m closure is
            # called after the existing save_m2m function (which will be
            # called later on if commit is False).
            self.save_m2m = func_concat(self.save_m2m, save_m2m)
        # Return the instance like a good save() method.
        return instance

class ContestAdmin(admin.ModelAdmin):
    # The precious fieldsets.
    fieldsets = (
        ('Basic Info', {
            'fields': ('name', 'results',)
        }),)
    # Here's where we override our form
    form = ContestAdminForm

admin.site.register(Contest, ContestAdmin)

Если вам интересно, я сам столкнулся с этой проблемой в проекте, над которым работаю, поэтому большая часть этого кода взята из этого проекта. Надеюсь, вы сочтете это полезным.

9
ответ дан 4 December 2019 в 21:47
поделиться
Другие вопросы по тегам:

Похожие вопросы: