Метод Django to_python пользовательского models.DateTimeField не получает правильное значение
У меня есть ModelForm, основанная на модели, которая включает в себя настроенное поле DateTimeField. Единственной настройкой является переопределение метода to_python, чтобы я мог преобразовать строку в формате 'AM/PM' в формат 24-часового времени, который Django будет проверять. Я указал виджет DateTimeInput на форме, чтобы я мог форматировать в нотацию AM/PM (format=('%m/%d/%Y %H:%M %p')), которую я инициализирую текущим временем. Форма отображается правильно, например, '10/10/2021 04:33 PM', но когда вызывается функция to_python пользовательского поля, переданное ей значение не включает время; только дату. Также я не понимаю, почему метод to_python этого поля вызывается (дважды), когда слушатель другого поля создает вызов AJAX, а не когда нажимается кнопка Submit. Я просмотрел данные запроса, отправленные при нажатии кнопки Submit, и в них действительно есть полное '10/10/2021 04:33 PM'. (Я понимаю, что фактическое преобразование в to_python для обработки AM/PM не выполняется вообще; я еще не дошел до этого). Я пробовал использовать несколько популярных методов подбора времени даты, но ни один из них не решил эту проблему.
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 %}
вывод терминала:
- when form is opened:
[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
- when user makes a selection in a choice field dropdown causing AJAX call:
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
- when user clicks Submit button:
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
и форма выдает сообщение об ошибке: "Введите действительную дату/время."
Проблема оказалась в том, что виджет DateTimeInput вызывал value_from_datadict
до того, как был вызван метод to_python поля. Поэтому docs немного вводят в заблуждение, когда говорят: "Метод 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))