自定义 models.DateTimeField 的 Django to_python 方法未收到正确的值

Django to_python method of custom models.DateTimeField not receiving the correct value

我有一个基于包含自定义 DateTimeField 的模型的 ModelForm。唯一的自定义是覆盖 to_python 方法,这样我就可以将 'AM/PM' 格式的字符串转换为 Django 将验证的 24 小时时间格式。我在表单上指定了一个 DateTimeInput 小部件,因此我可以将其格式化为 AM/PM 符号(格式=('%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 的实际转换根本没有完成;我还没有做到这一点)。我尝试使用几个流行的日期时间选择器,但 none 解决了这个问题。

models.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})

forms.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'}),
        }

views.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

并且表单显示错误消息:“输入有效的 date/time。”

问题原来是 DateTimeInput 小部件调用 value_from_datadict 字段的 to_python 方法被调用之前。所以 docs 说的有点误导:“字段上的 to_python() 方法是每次验证的第一步。” 即使DateTimeInput 小部件接受 AM/PM 格式字符串,并根据该字符串显示日期时间,它只删除时间部分和 returns 日期。所以解决方案是将输入小部件子类化并为其编写一个新的 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))