首页 > 解决方案 > 自定义模型的 Django to_python 方法。DateTimeField 没有收到正确的值

问题描述

我有一个基于包含自定义 DateTimeField 的模型的 ModelForm。唯一的自定义是覆盖 to_python 方法,以便我可以将“AM/PM”格式的字符串转换为 Django 将验证的 24 小时时间格式。我在表单上指定了一个 DateTimeInput 小部件,因此我可以格式化为 AM/PM 表示法(format=('%m/%d/%Y %H:%M %p')),并将其初始化为当前日期时间。表单显示正确,例如 '10/10/2021 04:33 PM' 但是当调用自定义字段的 to_python 函数时,传递给它的值不包括时间;只有日期。另外,我不明白为什么当另一个字段上的侦听器创建 AJAX 调用而不是单击提交按钮时,该字段的 to_python 方法被调用(两次)。我查看了单击提交时发送的请求数据,它确实有完整的“10/10/2021 04:33 PM”。(我意识到在 to_python 中处理 AM/PM 的实际转换根本没有完成;我还没有做到这一点)。我尝试使用几个流行的日期时间选择器,但没有一个能解决这个问题。

模型.py:

class ampmDateTimeField (models.DateTimeField):
    def to_python(self, value):
        print ('initial_date_time = ',value)
        # Here is where the code will go to do the actual conversion
        # for now, just see what the super's conversion is doing
        converted_date_time = super().to_python(value)
        print ('converted_date_time = ',converted_date_time)
        return converted_date_time


class Encounter(SafeDeleteModel):
    _safedelete_policy = SOFT_DELETE
    encounter_date = ampmDateTimeField()
    animal = models.ForeignKey(Animal, null=True, on_delete=models.SET_NULL)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL)
    handling_time = models.BigIntegerField(blank=True, null=True)
    crate_time = models.BigIntegerField(blank=True, null=True)
    holding_time = models.BigIntegerField(blank=True, null=True)
    comments = models.TextField(blank=True, null=True)

    def __str__(self) -> str:
        return (self.user.username + '/' + self.animal.Name)
    
    def get_absolute_url(self):
        return reverse("encounter-detail", kwargs={"pk": self.pk})

表格.py:

class Open_Encounter_Form(ModelForm):
    numPerDayField = CharField(label='Today\'s uses')
    #aNumField = CharField(name='Today',max_length=4)

    class Meta:
        model = Encounter        
        fields = ['encounter_date','animal','numPerDayField','user','handling_time','crate_time','holding_time','comments']
        widgets = {
            'comments': Textarea(attrs={'rows': 4, 'cols': 40}),
            'encounter_date': DateTimeInput(format=('%m/%d/%Y  %H:%M %p'), attrs={'size':'24'}),
        }

视图.py:

def open_encounter(request):
    if request.method == 'POST':
        print('request: ', request.POST)
        form = Open_Encounter_Form(request.POST)
        if form.is_valid():
            #save the data
            aRecord=form.save()
            return HttpResponseRedirect(reverse('index'))
        
    else:
        current_user = request.user
        form=Open_Encounter_Form(initial={'encounter_date': datetime.datetime.today(),'user': current_user})
    return render(request, 'openencounter.html', {'form': form})

openencounter.html:

{% extends "base_generic.html" %}

{% block content %}
<h1>New Encounter</h1>
<form action="/encounters/openencounter/" method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Submit">
</form>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
$("#id_animal").change(function () {
    var url = "{% url 'animal-data-API' %}"
    console.log('url:',url)
    var animalId = $("#id_animal").val();
    console.log('animalID: ', animalId);
    $.ajax({
        url: url,
        data: {
            'animal': animalId
        },
        success: function (data) {
            console.log('returned data:',data);
            uses = data['uses'];
            theMax = data['theMax'];
            if (uses > theMax) {
                $("#id_numPerDayField").css("color", "red")
            } else if (uses == theMax) {
                $("#id_numPerDayField").css("color", "blue")
            } else {
                $("#id_numPerDayField").css("color", "black")
            }
            theStr = uses.toString() + ' (out of ' + theMax.toString() + ')'
            $("#id_numPerDayField").val(theStr)
        }
    });
})
</script>

{% endblock %}

终端输出:

  1. 打开表单时:
[10/Oct/2021 13:16:25] "GET /encounters/openencounter/ HTTP/1.1" 200 4508
[10/Oct/2021 13:16:25] "GET /static/styles.css HTTP/1.1" 200 77
  1. 当用户在导致 AJAX 调用的选择字段下拉列表中进行选择时:
initial_date_time =  2021-10-10
converted_date_time =  2021-10-10 00:00:00
initial_date_time =  2021-10-10 00:00:00
converted_date_time =  2021-10-10 00:00:00
[10/Oct/2021 13:18:00] "GET /encounters/api/load_animal_uses/?animal=5 HTTP/1.1" 200 24
  1. 当用户单击提交按钮时:
request:  <QueryDict: {'csrfmiddlewaretoken': ['LOH28mXa5QTdomcPdJBPNbuJisY6TgykeI6yYzKseujglK7PX1HtoOm4QWmjlVCN'], 'encounter_date': ['10/10/2021  13:18 PM'], 'animal': ['5'], 'numPerDayField': ['0 (out of 4)'], 'user': ['1'], 'handling_time': [''], 'crate_time': [''], 'holding_time': [''], 'comments': ['']}>
[10/Oct/2021 13:19:29] "POST /encounters/openencounter/ HTTP/1.1" 200 4590
[10/Oct/2021 13:19:29] "GET /static/styles.css HTTP/1.1" 200 77

并且表单显示错误消息:“输入有效的日期/时间。”

标签: djangodjango-modelsdjango-forms

解决方案


问题原来是在调用字段的 to_python 方法value_from_datadict 之前调用了 DateTimeInput 小部件。因此,当文档说: “字段上的 to_python() 方法是每次验证的第一步”时,文档有点误导。 即使 DateTimeInput 小部件接受 AM/PM 格式字符串并根据该字符串显示日期时间,它也会删除时间部分并仅返回日期。所以解决方案是将输入小部件子类化并为其编写一个新方法。这是我的初稿,它有效:value_from_datadict

class ampmDateTimeInput(DateTimeInput):
    def value_from_datadict(self, data, files, name):        
        theRawDate = data['encounter_date']
        theConvertedDate = theRawDate
        # check if there is an AM or PM on the end
        if (len(theRawDate) > 2): 
            theStr = theRawDate[-2:]
            print ('theStr: ', theStr)
            if (theStr == 'PM'):
                #convert to 24 hr format
                #get the hours
                theHrsStr = theRawDate[12:14]
                theHrs = int(theHrsStr)
                theHrs = theHrs + 12
                #pad with leading zero if needed
                theHrsStr = str(theHrs)
                if (len(theHrsStr) == 1):
                    theHrsStr = '0' + theHrsStr
                theConvertedDate = theConvertedDate[0:11] + theHrsStr + theConvertedDate[14:17]
            elif (theStr == 'AM'):
                #just strip the AM
                theConvertedDate = theConvertedDate[0:17] 
            #else just pass the existing string; the user has removed the AM or PM manually    

        # create a mutable instance of the data     
        aNewData = data.copy()
        aNewData['encounter_date'] = theConvertedDate
        return(super().value_from_datadict(aNewData, files, name))

推荐阅读