2013-04-04 33 views
7

Tôi gặp rất nhiều khó khăn trong việc tìm ra cách tự động tạo một thể hiện của mô hình cho trường ForeignKey khi một biểu mẫu được gửi. Đây là một trang web đồ chơi đơn giản minh họa sự cố:Django: ValueError: không thể gán không có gì trên ForeignField khi cố gắng tạo ra trong sạch thất bại

Tôi có hai mô hình, Model1 và Model2. Model2 chứa ForeignKey đến Model1. Tôi muốn người dùng có thể tạo một thể hiện của Model2 bằng cách chọn cụ thể một thể hiện của Model1 để lưu trữ trong ForeignKey, hoặc bằng cách để trống giá trị đó và cho phép một cá thể của Model1 được tạo tự động.

Đây là những gì tôi cảm thấy giống như mã đó sẽ như thế nào. Mã models.py của tôi rất đơn giản:

# models.py 
from django.db import models 
from django.core.validators import MinValueValidator 


class Model1(models.Model): 

    # Note this field cannot be negative 
    my_field1 = models.IntegerField(validators=[MinValueValidator(0)]) 


class Model2(models.Model): 
    # blank = True will make key_to_model1 not required on the form, 
    # but since null = False, I will still require the ForeignKey 
    # to be set in the database. 
    related_model1 = models.ForeignKey(Model1, blank=True) 

    # Note this field cannot be negative 
    my_field2 = models.IntegerField(validators=[MinValueValidator(0)]) 

forms.py là một chút liên quan, nhưng những gì đang xảy ra khá đơn giản. Nếu Model2Form không nhận được một thể hiện của Model1, nó sẽ cố gắng tự động tạo một thể hiện trong phương thức sạch, xác nhận nó, và nếu nó hợp lệ, nó sẽ lưu nó. Nếu nó không hợp lệ, nó sẽ đưa ra một ngoại lệ.

#forms.py 
from django import forms 
from django.forms.models import model_to_dict 

from .models import Model1, Model2 


# A ModelForm used for validation purposes only. 
class Model1Form(forms.ModelForm): 
    class Meta: 
     model = Model1 


class Model2Form(forms.ModelForm): 
    class Meta: 
     model = Model2 

    def clean(self): 
     cleaned_data = super(Model2Form, self).clean() 

     if not cleaned_data.get('related_model1', None): 

      # Don't instantiate field2 if it doesn't exist. 
      val = cleaned_data.get('my_field2', None) 
      if not val: 
       raise forms.ValidationError("My field must exist") 

      # Generate a new instance of Model1 based on Model2's data 
      new_model1 = Model1(my_field1=val) 

      # validate the Model1 instance with a form form 
      validation_form_data = model_to_dict(new_model1) 
      validation_form = Model1Form(validation_form_data) 

      if not validation_form.is_valid(): 
       raise forms.ValidationError("Could not create a proper instance of Model1.") 

      # set the model1 instance to the related model and save it to the database. 
      new_model1.save() 
      cleaned_data['related_model1'] = new_model1 

     return cleaned_data 

Tuy nhiên, cách tiếp cận này không hoạt động. Nếu tôi nhập dữ liệu hợp lệ vào biểu mẫu của tôi, nó hoạt động tốt. Nhưng, nếu tôi không nhập bất cứ điều gì cho ForeignKey và đặt một giá trị âm cho số nguyên, tôi nhận được một ValueError.

Traceback: File "/Library/Python/2.7/site-packages/django/core/handlers/base.py" in get_response 111. response = callback(request, *callback_args, **callback_kwargs) File "/Library/Python/2.7/site-packages/django/views/generic/base.py" in view 48. return self.dispatch(request, *args, **kwargs) File "/Library/Python/2.7/site-packages/django/views/generic/base.py" in dispatch 69. return handler(request, *args, **kwargs) File "/Library/Python/2.7/site-packages/django/views/generic/edit.py" in post 172. return super(BaseCreateView, self).post(request, *args, **kwargs) File "/Library/Python/2.7/site-packages/django/views/generic/edit.py" in post 137. if form.is_valid(): File "/Library/Python/2.7/site-packages/django/forms/forms.py" in is_valid 124. return self.is_bound and not bool(self.errors) File "/Library/Python/2.7/site-packages/django/forms/forms.py" in _get_errors 115. self.full_clean() File "/Library/Python/2.7/site-packages/django/forms/forms.py" in full_clean 272. self._post_clean() File "/Library/Python/2.7/site-packages/django/forms/models.py" in _post_clean 309. self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude) File "/Library/Python/2.7/site-packages/django/forms/models.py" in construct_instance 51. f.save_form_data(instance, cleaned_data[f.name]) File "/Library/Python/2.7/site-packages/django/db/models/fields/init.py" in save_form_data 454. setattr(instance, self.name, data) File "/Library/Python/2.7/site-packages/django/db/models/fields/related.py" in set 362. (instance._meta.object_name, self.field.name))

Exception Type: ValueError at /add/ Exception Value: Cannot assign None: "Model2.related_model1" does not allow null values.

Vì vậy, những gì đang xảy ra là Django đang bắt ValidationError của tôi và vẫn tạo ra một thể hiện của Model2 mặc dù xác thực không thành công.

Tôi có thể sửa lỗi này bằng cách ghi đè phương thức _post_clean để không tạo phiên bản Model2 nếu có lỗi. Nhưng, giải pháp đó là xấu. Đặc biệt, hành vi của _post_clean rất hữu ích nói chung - Trong các dự án phức tạp hơn, tôi cần _post_clean để chạy vì các lý do khác.

Tôi cũng có thể cho phép ForeignKey rỗng nhưng không bao giờ được đặt thành null trong thực tế. Nhưng, một lần nữa, điều đó có vẻ như là một ý tưởng tồi.

Tôi thậm chí có thể thiết lập một mô hình giả1 mà tôi sử dụng bất cứ khi nào xác nhận trên mẫu mới đã thất bại Model1, nhưng điều đó cũng có vẻ bị hack.

Nói chung, tôi có thể nghĩ ra rất nhiều hacks để sửa lỗi này, nhưng tôi không biết cách sửa lỗi này theo cách hợp lý, gọn gàng.

+0

Tắt đầu của tôi Tôi nghĩ bạn có thể cần ít nhất trao đổi lưu trữ và xóa sạch_data xung quanh (như trong model1.save sau đó related_model1 = model1) –

+0

Xin chào Jeff, Cảm ơn bạn đã trả lời nhanh! Tôi nghĩ rằng thứ tự đó không quan trọng, vì các biến này là các tham chiếu và mô hình đã được xác nhận, do đó, lưu sẽ không bao giờ thất bại. Đơn đặt hàng của bạn dường như tự nhiên hơn, mặc dù, vì vậy tôi sẽ chuyển đổi nó. Tuy nhiên, mã này không bao giờ được tiếp cận khi lỗi đang xảy ra, do đó, đó không phải là nguyên nhân của lỗi. –

+0

thêm dòng sau 'nhập pdb; pdb.set_trace() 'để chỉ trước khi tạo new_model1 và sau đó bạn có thể chạy một dòng tại một thời điểm (bằng cách sử dụng n) và các giá trị của new_model1 đến thiết bị đầu cuối của bạn đang chạy django. Tôi nghi ngờ một cái gì đó là đi một chút sai lầm về việc tạo validation_form và kiểm tra nếu nó là hợp lệ. [link to docs on pdb] (http://docs.python.org/2/library/pdb.html) –

Trả lời

1

Tôi đã tìm thấy giải pháp mà tôi cho rằng có thể chấp nhận được, dựa phần nào vào thảo luận của karthikr trong các nhận xét. Tôi chắc chắn vẫn còn mở để lựa chọn thay thế.

Ý tưởng là sử dụng logic trong chế độ xem để chọn giữa hai biểu mẫu để xác thực: Một biểu mẫu là mẫu mô hình chuẩn và một là mẫu mô hình không có trường ForeignKey.

Vì vậy, models.py của tôi giống hệt nhau.

forms.py của tôi có hai biểu mẫu Model2 ... một biểu mẫu cực kỳ đơn giản và không có trường ForeignKey và với logic mới để tự động tạo phiên bản Model1 mới cho ForeignKey. Logic sạch các hình thức mới của chỉ là logic sạch mà tôi sử dụng để đưa vào Model2Form tôi:

#forms.py 
from django import forms 
from django.forms.models import model_to_dict 

from .models import Model1, Model2 


# A ModelForm used for validation purposes only. 
class Model1Form(forms.ModelForm): 
    class Meta: 
     model = Model1 


class Model2Form(forms.ModelForm): 
    class Meta: 
     model = Model2 

# This inherits from Model2Form so that any additional logic that I put in Model2Form 
# will apply to it. 
class Model2FormPrime(Model2Form): 
    class Meta: 
     model = Model2 
     exclude = ('related_model1',) 

    def clean(self): 
     cleaned_data = super(Model2Form, self).clean() 

     if cleaned_data.get('related_model1', None): 
      raise Exception('Huh? This should not happen...') 

     # Don't instantiate field2 if it doesn't exist. 
     val = cleaned_data.get('my_field2', None) 
     if not val: 
      raise forms.ValidationError("My field must exist") 

     # Generate a new instance of Model1 based on Model2's data 
     new_model1 = Model1(my_field1=val) 

     # validate the Model1 instance with a form form 
     validation_form_data = model_to_dict(new_model1) 
     validation_form = Model1Form(validation_form_data) 

     if not validation_form.is_valid(): 
      raise forms.ValidationError("Could not create a proper instance of Model1.") 

     # set the Model1 instance to the related model and save it to the database. 
     cleaned_data['related_model1'] = new_model1 

     return cleaned_data 

    def save(self, commit=True): 
     # Best to wait til save is called to save the instance of Model1 
     # so that instances aren't created when the Model2Form is invalid 
     self.cleaned_data['related_model1'].save() 

     # Need to handle saving this way because otherwise related_model1 is excluded 
     # from the save due to Meta.excludes 
     instance = super(Model2FormPrime, self).save(False) 
     instance.related_model1 = self.cleaned_data['related_model1'] 
     instance.save() 

     return instance 

Và sau đó nhìn logic của tôi sử dụng một trong hai hình thức để xác nhận, tùy thuộc vào bài dữ liệu.Nếu nó sử dụng Model2FormPrime và xác nhận thất bại, nó sẽ di chuyển dữ liệu và các lỗi để một Model2Form thường xuyên để hiển thị cho người sử dụng:

# Create your views here. 
from django.views.generic.edit import CreateView 
from django.http import HttpResponseRedirect 

from .forms import Model2Form, Model2FormPrime 


class Model2CreateView(CreateView): 
    form_class = Model2Form 
    template_name = 'form_template.html' 
    success_url = '/add/' 

    def post(self, request, *args, **kwargs): 
     if request.POST.get('related_model', None): 
      # Complete data can just be sent to the standard CreateView form 
      return super(Model2CreateView, self).post(request, *args, **kwargs) 
     else: 
      # super does this, and I won't be calling super. 
      self.object = None 

      # use Model2FormPrime to validate the post data without the related model. 
      validation_form = Model2FormPrime(request.POST) 
      if validation_form.is_valid(): 
       return self.form_valid(validation_form) 
      else: 
       # Create a normal instance of Model2Form to be displayed to the user 
       # Insantiate it with post data and validation_form's errors 
       form = Model2Form(request.POST) 
       form._errors = validation_form._errors 
       return self.form_invalid(form) 

Giải pháp này hoạt động, và nó khá linh hoạt. Tôi có thể thêm logic vào các mô hình của mình và đến Model2Form cơ bản mà không phải lo lắng quá nhiều về việc phá vỡ nó hoặc vi phạm DRY.

Nó hơi xấu xí, tuy nhiên, vì nó yêu cầu tôi sử dụng hai biểu mẫu để thực chất là thực hiện công việc của một, vượt qua lỗi giữa các biểu mẫu. Vì vậy, tôi chắc chắn mở cửa cho các giải pháp thay thế nếu có ai có thể đề xuất bất cứ điều gì.

+1

Cảm ơn bạn đã đặt câu hỏi và câu trả lời! Chúng tôi đang đối mặt chính xác cùng một vấn đề. Chúng tôi cũng nghĩ về một loạt các giải pháp tấn công. Của bạn có vẻ khá thanh lịch, chúng tôi sẽ xem nếu nó hoạt động trong trường hợp của chúng tôi. Tôi ước rằng các mô hình có tên có ý nghĩa hơn trong ví dụ của bạn (ví dụ: Tác giả/Sách thay vì Model1/Model2), bởi vì nó không dễ dàng theo logic. Dù sao, cảm ơn rất nhiều lần nữa! –

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