Как мне отфильтровать варианты ForeignKey в Django ModelForm?

StackOverflow https://stackoverflow.com/questions/291945

  •  08-07-2019
  •  | 
  •  

Вопрос

Допустим, у меня есть следующее в моем models.py:

class Company(models.Model):
   name = ...

class Rate(models.Model):
   company = models.ForeignKey(Company)
   name = ...

class Client(models.Model):
   name = ...
   company = models.ForeignKey(Company)
   base_rate = models.ForeignKey(Rate)

То есть.существует множество Companies, каждый из которых имеет ряд Rates и Clients.Каждый Client должна быть база Rate который выбирается из своего родительского Company's Rates, а не другой Company's Rates.

При создании формы для добавления Client, Я хотел бы удалить Company варианты (поскольку это уже было выбрано с помощью кнопки "Добавить клиента" на Company страницу) и ограничить Rate выбор в пользу этого Company также.

Как мне это сделать в Django 1.0?

Мой текущий forms.py файл на данный момент является просто шаблонным:

from models import *
from django.forms import ModelForm

class ClientForm(ModelForm):
    class Meta:
        model = Client

И тот views.py также является основным:

from django.shortcuts import render_to_response, get_object_or_404
from models import *
from forms import *

def addclient(request, company_id):
    the_company = get_object_or_404(Company, id=company_id)

    if request.POST:
        form = ClientForm(request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(the_company.get_clients_url())
    else:
        form = ClientForm()

    return render_to_response('addclient.html', {'form': form, 'the_company':the_company})

В Django 0.96 я смог взломать это, выполнив что-то вроде следующего перед рендерингом шаблона:

manipulator.fields[0].choices = [(r.id,r.name) for r in Rate.objects.filter(company_id=the_company.id)]

ForeignKey.limit_choices_to кажется многообещающим, но я не знаю, как пройти the_company.id и мне все равно не ясно, будет ли это работать вне интерфейса администратора.

Спасибо.(Это кажется довольно простым запросом, но если мне следует что-то переделать, я открыт для предложений.)

Это было полезно?

Решение

ForeignKey представлен django.forms.ModelChoiceField, который является полем выбора, выбор которого является набором запросов модели.Смотрите ссылку для Поле выбора модели.

Итак, предоставьте набор запросов к полю queryset атрибут.Зависит от того, как построена ваша форма.Если вы создадите явную форму, у вас будут поля с прямыми именами.

form.rate.queryset = Rate.objects.filter(company_id=the_company.id)

Если вы возьмете объект ModelForm по умолчанию, form.fields["rate"].queryset = ...

Это делается явно в представлении.Никакого взлома вокруг да около.

Другие советы

В дополнение к ответу С. Лотта и, как упоминалось в комментариях becomingGuru, можно добавить фильтры набора запросов, переопределив ModelForm.__init__ функция.(Это может легко применяться к обычным формам) это может помочь при повторном использовании и поддерживает функцию просмотра в чистоте.

class ClientForm(forms.ModelForm):
    def __init__(self,company,*args,**kwargs):
        super (ClientForm,self ).__init__(*args,**kwargs) # populates the post
        self.fields['rate'].queryset = Rate.objects.filter(company=company)
        self.fields['client'].queryset = Client.objects.filter(company=company)

    class Meta:
        model = Client

def addclient(request, company_id):
        the_company = get_object_or_404(Company, id=company_id)

        if request.POST:
            form = ClientForm(the_company,request.POST)  #<-- Note the extra arg
            if form.is_valid():
                form.save()
                return HttpResponseRedirect(the_company.get_clients_url())
        else:
            form = ClientForm(the_company)

        return render_to_response('addclient.html', 
                                  {'form': form, 'the_company':the_company})

Это может быть полезно для повторного использования, скажем, если у вас есть общие фильтры, необходимые для многих моделей (обычно я объявляю абстрактный класс Form).Например.

class UberClientForm(ClientForm):
    class Meta:
        model = UberClient

def view(request):
    ...
    form = UberClientForm(company)
    ...

#or even extend the existing custom init
class PITAClient(ClientForm):
    def __init__(company, *args, **args):
        super (PITAClient,self ).__init__(company,*args,**kwargs)
        self.fields['support_staff'].queryset = User.objects.exclude(user='michael')

В остальном я просто пересказываю материалы блога Django, из которых есть много хороших.

Это просто и работает с Django 1.4:

class ClientAdminForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(ClientAdminForm, self).__init__(*args, **kwargs)
        # access object through self.instance...
        self.fields['base_rate'].queryset = Rate.objects.filter(company=self.instance.company)

class ClientAdmin(admin.ModelAdmin):
    form = ClientAdminForm
    ....

Вам не нужно указывать это в классе form, но вы можете сделать это непосредственно в ModelAdmin, поскольку Django уже включает этот встроенный метод в ModelAdmin (из документации):

ModelAdmin.formfield_for_foreignkey(self, db_field, request, **kwargs)¶
'''The formfield_for_foreignkey method on a ModelAdmin allows you to 
   override the default formfield for a foreign keys field. For example, 
   to return a subset of objects for this foreign key field based on the
   user:'''

class MyModelAdmin(admin.ModelAdmin):
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "car":
            kwargs["queryset"] = Car.objects.filter(owner=request.user)
        return super(MyModelAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)

Еще более изящный способ сделать это (например, при создании интерфейсного интерфейса администратора, к которому пользователи могут получить доступ) - создать подкласс ModelAdmin, а затем изменить приведенные ниже методы.Конечным результатом является пользовательский интерфейс, который показывает им ТОЛЬКО контент, связанный с ними, позволяя вам (суперпользователю) видеть все.

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

Третье переопределение фильтрует любой запрос, содержащий ссылку на (в примере 'user' или 'porcupine' (просто в качестве иллюстрации).

Последнее переопределение фильтрует любое поле foreignkey в модели, чтобы фильтровать доступные варианты так же, как в базовом наборе запросов.

Таким образом, вы можете представить простой в управлении интерфейсный сайт администратора, который позволяет пользователям работать со своими собственными объектами, и вам не нужно забывать вводить конкретные фильтры ModelAdmin, о которых мы говорили выше.

class FrontEndAdmin(models.ModelAdmin):
    def __init__(self, model, admin_site):
        self.model = model
        self.opts = model._meta
        self.admin_site = admin_site
        super(FrontEndAdmin, self).__init__(model, admin_site)

удалить кнопки "удалить":

    def get_actions(self, request):
        actions = super(FrontEndAdmin, self).get_actions(request)
        if 'delete_selected' in actions:
            del actions['delete_selected']
        return actions

предотвращает разрешение на удаление

    def has_delete_permission(self, request, obj=None):
        return False

фильтрует объекты, которые можно просматривать на сайте администратора:

    def get_queryset(self, request):
        if request.user.is_superuser:
            try:
                qs = self.model.objects.all()
            except AttributeError:
                qs = self.model._default_manager.get_queryset()
            return qs

        else:
            try:
                qs = self.model.objects.all()
            except AttributeError:
                qs = self.model._default_manager.get_queryset()

            if hasattr(self.model, ‘user’):
                return qs.filter(user=request.user)
            if hasattr(self.model, ‘porcupine’):
                return qs.filter(porcupine=request.user.porcupine)
            else:
                return qs

фильтрует варианты для всех полей foreignkey на сайте администратора:

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if request.employee.is_superuser:
            return super(FrontEndAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)

        else:
            if hasattr(db_field.rel.to, 'user'):
                kwargs["queryset"] = db_field.rel.to.objects.filter(user=request.user)
            if hasattr(db_field.rel.to, 'porcupine'):
                kwargs["queryset"] = db_field.rel.to.objects.filter(porcupine=request.user.porcupine)
            return super(ModelAdminFront, self).formfield_for_foreignkey(db_field, request, **kwargs)

Чтобы сделать это с помощью общего представления, такого как CreateView...

class AddPhotoToProject(CreateView):
    """
    a view where a user can associate a photo with a project
    """
    model = Connection
    form_class = CreateConnectionForm


    def get_context_data(self, **kwargs):
        context = super(AddPhotoToProject, self).get_context_data(**kwargs)
        context['photo'] = self.kwargs['pk']
        context['form'].fields['project'].queryset = Project.objects.for_user(self.request.user)
        return context
    def form_valid(self, form):
        pobj = Photo.objects.get(pk=self.kwargs['pk'])
        obj = form.save(commit=False)
        obj.photo = pobj
        obj.save()

        return_json = {'success': True}

        if self.request.is_ajax():

            final_response = json.dumps(return_json)
            return HttpResponse(final_response)

        else:

            messages.success(self.request, 'photo was added to project!')
            return HttpResponseRedirect(reverse('MyPhotos'))

самая важная часть этого...

    context['form'].fields['project'].queryset = Project.objects.for_user(self.request.user)

, прочтите мой пост здесь

Если вы еще не создали форму и хотите изменить набор запросов, вы можете сделать:

formmodel.base_fields['myfield'].queryset = MyModel.objects.filter(...)

Это очень полезно, когда вы используете общие представления!

Итак, я действительно пытался понять это, но, похоже, Django все еще не делает это очень простым.Я не такой уж тупой, но я просто не вижу никакого (в какой-то степени) простого решения.

Я нахожу, что, как правило, довольно некрасиво переопределять представления администратора для такого рода вещей, и каждый пример, который я нахожу, никогда полностью не применим к представлениям администратора.

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

У меня есть эти занятия:

# models.py
class Company(models.Model):
    # ...
class Contract(models.Model):
    company = models.ForeignKey(Company)
    locations = models.ManyToManyField('Location')
class Location(models.Model):
    company = models.ForeignKey(Company)

Это создает проблему при настройке Администратора для Компании, поскольку в нем есть строки как для Контракта, так и для местоположения, а параметры m2m контракта для местоположения неправильно фильтруются в соответствии с Компанией, которую вы в данный момент редактируете.

Короче говоря, мне понадобился бы какой-нибудь параметр администратора, чтобы сделать что-то подобное:

# admin.py
class LocationInline(admin.TabularInline):
    model = Location
class ContractInline(admin.TabularInline):
    model = Contract
class CompanyAdmin(admin.ModelAdmin):
    inlines = (ContractInline, LocationInline)
    inline_filter = dict(Location__company='self')

В конечном счете, мне было бы все равно, был ли процесс фильтрации размещен на базовом CompanyAdmin или он был размещен на ContractInline .(Размещение его во встроенном имеет больше смысла, но это затрудняет ссылку на базовый контракт как на "self".)

Есть ли кто-нибудь, кто знает о чем-то столь же простом, как этот крайне необходимый короткий путь?Раньше, когда я создавал PHP-администраторов для подобных вещей, это считалось базовой функциональностью!На самом деле, это всегда было автоматически, и его приходилось отключать, если вы действительно этого не хотели!

Более общедоступный способ - вызвать get_form в классах администратора.Это также работает и для полей, не относящихся к базе данных.Например, здесь у меня есть поле с именем '_terminal_list' в форме, которое можно использовать в особых случаях для выбора нескольких элементов терминала из get_list (запрос), затем фильтрации на основе request.user:

class ChangeKeyValueForm(forms.ModelForm):  
    _terminal_list = forms.ModelMultipleChoiceField( 
queryset=Terminal.objects.all() )

    class Meta:
        model = ChangeKeyValue
        fields = ['_terminal_list', 'param_path', 'param_value', 'scheduled_time',  ] 

class ChangeKeyValueAdmin(admin.ModelAdmin):
    form = ChangeKeyValueForm
    list_display = ('terminal','task_list', 'plugin','last_update_time')
    list_per_page =16

    def get_form(self, request, obj = None, **kwargs):
        form = super(ChangeKeyValueAdmin, self).get_form(request, **kwargs)
        qs, filterargs = Terminal.get_list(request)
        form.base_fields['_terminal_list'].queryset = qs
        return form
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top