2010-07-06 21 views
8

Tôi đã làm việc thông qua một widget ManyToManyField ra lệnh, và có khía cạnh front-end của nó làm việc độc đáo:Ordered ManyToManyField có thể được sử dụng trong fieldsets

alt text http://i45.tinypic.com/33e79c8.png

Thật không may, tôi đang gặp một rất nhiều rắc rối khi làm việc phụ trợ. Cách rõ ràng để treo lên phụ trợ là sử dụng một bảng through đã khóa một mô hình với ForeignKey s cho cả hai bên của mối quan hệ và ghi đè phương thức lưu. Điều này sẽ làm việc tuyệt vời, ngoại trừ do idiosyncrasies của nội dung, nó là một yêu cầu tuyệt đối mà widget này được đặt trong một fieldset (sử dụng ModelAdmin fieldsets tài sản), đó là apparently not possible.

Tôi hết ý tưởng. Bất kỳ đề xuất?

Cảm ơn!

+0

+1 để sử dụng từ 'idiosyncrasies' trong câu hỏi của bạn. Ý tưởng tuyệt vời. Tôi đang suy nghĩ một chút. –

+1

Bạn có thể cho chúng tôi biết lý do tại sao bạn sử dụng mô hình 'thông qua' thay vì một cổ phiếu có nhiều mối quan hệ? bạn đang lưu trữ siêu dữ liệu bổ sung nào trong mối quan hệ này? Vấn đề tôi thấy khi có tính năng này được triển khai trong quản trị viên mặc định là hiện tại không có cách nào để thêm siêu dữ liệu bổ sung trong quan hệ thông qua chỉ bằng cách chọn yếu tố tạo danh sách như tiện ích con của bạn ngụ ý. – Thomas

+0

Thomas: dữ liệu đặt hàng sẽ là trường bổ sung, cho mô hình "thông qua" ba trường: một ForeignKey cho mỗi bên của M2M và một thứ tự đại diện PositiveIntegerField, với cả ba trường là unique_together. –

Trả lời

8

Liên quan đến cách thiết lập mô hình, bạn ở ngay trong bảng qua bảng có cột "đơn đặt hàng" là cách lý tưởng để thể hiện. Bạn cũng đúng trong đó Django sẽ không cho phép bạn đề cập đến mối quan hệ đó trong một fieldset. Bí quyết để giải quyết vấn đề này là hãy nhớ rằng các tên trường bạn chỉ định trong "fieldets" hoặc "fields" của ModelAdmin không thực sự tham chiếu đến các trường của Model, nhưng đến các trường của ModelForm, chúng tôi miễn phí để ghi đè lên niềm vui của trái tim chúng tôi. Với nhiều lĩnh vực2many, điều này trở nên phức tạp, nhưng hãy theo tôi:

Giả sử bạn đang cố gắng đại diện cho các cuộc thi và đối thủ cạnh tranh trong đó, với số lượng nhiều người đã đặt hàng giữa các cuộc thi và đối thủ cạnh tranh. cuộc thi đó. Sau đó, models.py của bạn sẽ trông giống như sau:

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() 

Hy vọng rằng điều này tương tự như những gì bạn đang xử lý. Bây giờ, dành cho quản trị viên. Tôi đã viết một ví dụ admin.py với nhiều nhận xét để giải thích những gì đang xảy ra, nhưng dưới đây là tóm tắt để giúp bạn:

Vì tôi không có mã cho tiện ích m2m đã đặt hàng mà bạn đã viết, tôi đã đã sử dụng tiện ích con giả mạo chỗ dành sẵn mà chỉ đơn giản là kế thừa từ TextInput. Dữ liệu nhập giữ một danh sách được phân cách bằng dấu phẩy (không có dấu cách) của ID người dự thi và thứ tự xuất hiện của chúng trong chuỗi xác định giá trị của cột "xếp hạng" trong mô hình ContestResults.

Điều xảy ra là chúng tôi ghi đè mặc định ModelForm cho Cuộc thi với riêng của chúng tôi và sau đó xác định trường "kết quả" bên trong nó (chúng tôi không thể gọi trường là "thí sinh", vì sẽ có xung đột với tên m2m trường trong mô hình). Sau đó, chúng tôi ghi đè __init__(), được gọi khi biểu mẫu được hiển thị trong quản trị viên, vì vậy, chúng tôi có thể tìm bất kỳ Cuộc thi nào có thể đã được xác định cho Cuộc thi và sử dụng chúng để điền tiện ích con. Chúng tôi cũng ghi đè save() để chúng tôi có thể nhận dữ liệu từ tiện ích con và tạo các Cuộc thi cần thiết. Lưu ý rằng vì mục đích đơn giản, ví dụ này bỏ qua những thứ như xác thực dữ liệu từ tiện ích con, vì vậy mọi thứ sẽ bị hỏng nếu bạn cố nhập bất kỳ điều gì bất ngờ vào kiểu nhập văn bản. Ngoài ra, mã để tạo ContestResults khá đơn giản và có thể được cải thiện đáng kể.

Tôi cũng nên thêm rằng tôi đã thực sự chạy mã này và xác minh rằng nó hoạt động.

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) 

Trong trường hợp bạn đang tự hỏi, tôi đã chạy vào vấn đề này bản thân mình trên một dự án tôi đã làm việc trên, vì vậy hầu hết các mã này xuất phát từ dự án đó. Tôi hy vọng bạn thấy nó hữu dụng.

+0

Tôi đánh giá cao sự thông suốt và chu đáo của phản hồi, cảm ơn bạn! Tôi sẽ đào sâu vào điều này ngày hôm nay và cho bạn biết làm thế nào nó quay ra. –

+0

Tôi rất gần với việc này để làm việc, cảm ơn bạn! Chìa khóa lớn là con trỏ mà các trường thực sự được gọi là các trường trên biểu mẫu chứ không phải trên một mô hình. Một vấn đề mà tôi đang gặp phải là thiết lập ModelForm.base_fields [field] .initial dường như không có; dữ liệu trong trường vẫn được điền bởi giá trị trong cơ sở dữ liệu (không giống như trường hợp của bạn, tôi thực sự sẽ ghi đè giá trị ban đầu của một trường m2m thực; đó dường như là một sự phân biệt quan trọng. Người dùng django post, http://bit.ly/a7R3ao –

+0

Ah, tôi nghĩ rằng tôi đã chạy vào điều này trước đây.Trong một số hình thức, thay vì 'ModelForm.base_fields [field] .initial', bạn cần phải đặt' ModelForm Bạn có thể thử điều đó và cho tôi biết nó đi như thế nào? –

Các vấn đề liên quan