I am using Django REST Framework, specifically a ModelSerializer instance, to receive some date/time information, among other fields. The Django form which POSTs or PUTs to my view is using a single field for date, and separate fields for hour, minute, and am/pm.
I wrote a function to deal with recombining the values into a Python datetime object, but for some reason, when my function returns a correct datetime, the time portion is getting zero'ed out when the datetime is assigned back to the serializer object for saving.
I am new to DRF so maybe I just need to approach this another way altogether....
def roomeventrequest(request, roomevent_id):
"""
THIS IS THE VIEW
"""
...
elif request.method == 'PUT':
data = JSONParser().parse(request)
roomevent = RoomEvent.objects.get(pk=roomevent_id)
serializer = RoomEventSerializer(roomevent, data=data)
if serializer.is_valid():
serializer.data['start_datetime'] = _process_datetime(serializer.validated_data['start_datetime'],
data['start_hour'],
data['start_min'],
data['start_ampm'])
serializer.data['end_datetime'] = _process_datetime(serializer.validated_data['start_datetime'],
data['end_hour'],
data['end_min'],
data['start_ampm'])
print (serializer.data['start_datetime'])
print (serializer.data['end_datetime'])
serializer.save()
return JSONResponse(serializer.data, status=201)
return JSONResponse(serializer.errors, status=400)
def _process_datetime(date_obj, hour, minute, ampm):
print (date_obj)
if ampm == 'am' and hour == 12:
hour = 0
elif ampm == 'pm':
hour += 12
return_date = date_obj.replace(minute=int(minute), hour=int(hour))
print(return_date)
return return_date
And the above outputs the following from the print statements:
2015-05-21 00:00:00
2015-05-21 08:00:00
2015-05-21 00:00:00
2015-05-21 09:00:00
2015-05-21T00:00:00
2015-05-21T00:00:00
Why is the resulting time portion blank? Where have I gotten off track here?
The problem you are seeing is that you are modifying the serializer data from the outside, which doesn't actually propagate to the data used internally. So even though you are changing the start_datetime and end_datetime fields, internally DRF still sees the datetime objects that only contain the date.
You have a few options
Validate the date fields in a separate serializer (or just manually) and construct the correct date input on your own.
Combine all of the date fields before passing them into the serializer, such that they match one of the Django datetime input formats.
Directly modify serializer.validated_data (instead of serializer.data) in your code. This is what is passed on to create and update.
I would recommend avoiding #3 for now, as the validated_data dictionary is designed to be read-only and that may be enforced in the future. So that leaves you with #1 and #2, both of which work require modifications to different parts of your code and work better for different situations.
The first option works best if your validation needs to return errors to the frontend that need to match the specific field, instead of just commenting on the incorrect date format. But it also requires the creation of a custom MultipartDatetimeSerializer that is used for validating across all of the fields.
from datetime import date, datetime, time
class MultipartDatetimeSerializer(serializers.Serializer):
date = serializers.DateField()
hour = serializers.IntegerField(
min_value=1,
max_value=12
)
minute = serializers.IntegerField(
min_value=0,
max_value=59,
)
period = serializers.ChoiceField(
choices=(
('am', 'A.M.', ),
('pm', 'P.M.', ),
)
)
def to_internal_value(self, data):
parsed_data = super(MultipartDatetimeSerializer, self).to_internal_value(data)
hour = parsed_data['hour']
if parsed_data['period'] == 'pm':
hour += 12
elif hour == 12:
hour = 0
time_data = time(
hour=hour,
minute=parsed_data['minute']
)
return datetime.combine(
date=parsed_data['date'],
time=time_data
)
def to_representation(self, instance):
"""
Convert a datetime to a dictionary containing the
four different fields.
The period must be manually determined (and set), so there
is some pre-processing that has to happen here.
"""
obj = {
"date": instance.date,
"hour": instance.hour,
"minute": instance.minute,
}
if obj["hour"] > 12:
obj["hour"] -= 12
obj["period"] = 'pm'
else:
if obj["hour"] == 0:
obj["hour"] = 12
obj["period"] = 'am'
return super(MultipartDatetimeSerializer, self).to_representation(obj)
This serializer can now be used to split a datetime into the date, hour, minute, and period components.
obj = datetime.now()
serializer = MultipartDatetimeSerializer(obj)
print(serializer.data)
As well as combine them back together
data = {
"date": "2015-01-01",
"hour": "11",
"minute": "59",
"period": "pm",
}
serializer = MultipartDatetimeSerializer(data=data)
if serializer.is_valid():
print(serializer.to_internal_value(serializers.validated_data))
else:
print(serializer.errors)
The second option works best if you just need to return an error saying that the data given is not an actual date. You can find a date format that closely matches what is being entered and then concatenate the incoming data to match that.
In your case, the closest date format appears to be %Y-%m-%d %I:%M %p which will match a date like 2015-01-01 11:59 PM.
So, all that is left is to set the date format of the date field on your serializer to accept the above format (as well as ISO 8601, the default), which is as simple as setting input_formats on the field to
['iso-8601', '%Y-%m-%d %I:%M %p']
And changing the data passed to the serializer to concatenate the incoming values to match the field
data = JSONParser().parse(request)
data['start_datetime'] = "%s %s:%s %s" % (data['start_datetime'], data['start_hour'], data['start_min'], data['start_ampm'], )
data['end_datetime'] = "%s %s:%s %s" % (data['end_datetime'], data['end_hour'], data['end_min'], data['end_ampm'], )
Note that I'm always using the %s modifier instead of the %d modifier as DRF can handle incorrect numbers being passed into the fields, and it prevents an unhandled exception from occurring when a string is passed in.
Related
I have a model in Django with a DateTimeField attribute. I want to forbid insertion of new data in the database if the duration between the datetime field of the new data and the latest datetime field in the database is less than some duration threshold.
class MyModel(models.Model):
time_stamp = models.DateTimeField(default=timezone.now, null=True)
When I want to insert a datapoint say today, and the latest time stamp in my database is yesterday, and the duration threshold is one month (this operation should not be possible).
You can define this logic in your views like so:
from django.shortcuts import get_object_or_404
import datetime
def CreateNew(request, id):
obj = get_object_or_404(MyModel, id = id) #Get the object from your database
form = YourForm(request.POST or None, instance = obj) #create form instance to be rendered inside template
diff = (timezone.now() - obj.time_stamp).total_seconds()
threshold = datetime.timedelta(days=30).total_seconds()
if diff < threshold: # Compare dates to check condition
return HttpResponse('<h1>Not Allowed</h1>')
elif form.is_valid(): # If condition is passed save form as you normally would
form.instance.time_stamp = timezone.now() # Update time_stamp to current time
form.save()
return HttpResponseRedirect("/")
context = {
'form': form
}
return render(request, "Your_template", context)
If you are determined that this is prevented in a more hard manner than putting protection logic in view(s), then instead of checking in the view you can check in the model's save method.
def save( self, *args, **kwargs):
diff = (timezone.now() - self.time_stamp).total_seconds()
threshold = datetime.timedelta(days=30).total_seconds()
if diff < threshold: # Compare dates to check condition
# not certain ValueError is the best choice of exception
raise ValueError(
f"Can't save because {diff} seconds since the previous save, the minimum is {theshold}"
)
super().save( *args, **kwargs)
This check can still be bypassed, by Django bulk_update for example, and by raw SQL. Some databases may let you put the check into the database itself.
The downside is that fixing mistakes using (for example) the Django Admin may become difficult. In this case you can programmatically bypass the check by resetting the timestamp first.
I wrote the following code:
date = self.request.query_params.get('date')
queryset = Fixture.objects.all().order_by('-date')
if(date):
date = pytz.utc.localize(datetime.strptime(date, "%Y-%m-%d")).astimezone(pytz.UTC)
queryset = Fixture.objects.filter(date__date=date).order_by('date')
Upon excuting this with date = "2020-09-02" the queryset returns values containing the date "2020-09-03". How come this happens and how can this be solved?
If you want to work with a date, why make it a datetime first? Your parsing could be simplified.
date = self.request.query_params.get('date')
queryset = Fixture.objects.all().order_by('-date')
if date:
date = datetime.strptime(date, "%Y-%m-%d").date()
queryset = Fixture.objects.filter(date__date=date).order_by('date')
But this parsing is also is sensitive for wrongly inserted data, and you'll get an error. Best practice imo would be creating a simple form with a DateField.
class ParseDateForm(forms.Form):
date = forms.DateField()
# This somewhere in a method
form = ParseDateForm(data=self.request.query_params)
queryset = Fixture.objects.all().order_by('-date')
if form.is_valid():
date = form.cleaned_data['date']
queryset = Fixture.objects.filter(date__date=date).order_by('date')
I have a form that creates a lesson with a date and time. I currently have validators to ensure that past dates can't be used, which are working perfectly. However, I am having trouble visualising how a validator that makes sure the time entered isn't past 11:59 pm would work. I inlucded a snippet of what I am trying to achieve (I know it doesn't work the way it is layed out, it is just there to provide context). I would appreaciate any help with this.
forms.py
def validate_date1(value):
if value < timezone.now():
raise ValidationError('Date cannot be in the past')
def validate_date2(value):
if value < timezone.now():
raise ValidationError('Date cannot be in the past')
def present_date1(value):
if value > '11:59 pm':
raise ValidationError('Time cannot be past 11:59 pm')
def present_date2(value):
if value > '11:59 pm':
raise ValidationError('Time cannot be past 11:59 pm')
class LessonForm(forms.ModelForm):
lesson_instrument = forms.ChoiceField(choices=instrument_list, widget=forms.Select(attrs={'class' : 'form-control', 'required' : 'True'}))
lesson_datetime_start = forms.DateTimeField(input_formats=['%Y-%m-%d %I:%M %p'], widget=forms.DateTimeInput(attrs={'class': 'form-control', 'placeholder':'YYYY-MM-DD Hour:Minute am/pm'}), validators=[validate_date1, present_date1])
lesson_datetime_end = forms.DateTimeField(input_formats=['%Y-%m-%d %I:%M %p'], required=False, widget=forms.DateTimeInput(attrs={'class': 'form-control', 'placeholder':'YYYY-MM-DD Hour:Minute am/pm'}), validators=[validate_date2, present_date2])
lesson_weekly = forms.BooleanField(required=False)
The validators for a DateTimeField will get a datetime.datetime object, not a string.
Here we extract the time component out of the datetime and compare it to our constant last possible time.
import datetime
LAST_POSSIBLE_TIME = datetime.time(23, 59)
def validate_time(value):
if value.time() > LAST_POSSIBLE_TIME:
raise ValidationError('Time cannot be past 11:59 pm')
So, the input date cannot be in the past and it cannot be after 23:59, so basically it needs to be within the rest of the present day.
How about:
import pytz
def date_is_not_past(dt):
if dt < datetime.now(pytz.UTC):
raise ValidationError('Date cannot be in the past')
def date_is_today(dt):
if dt.date() != datetime.now(pytz.UTC).date():
raise ValidationError('Date needs to be today')
You want to validate lesson_datetime_start and lesson_datetime_end together, not separately. Just checking that the time isn't greater than 11:59pm doesn't cut it, since that would make 2019-05-04 11:00pm - 2019-05-05 12:00am invalid even though it's a correct one hour interval starting at 11pm.
To do that, add a clean() method to your form:
def clean(self):
cleaned_data = super().clean()
if self.cleaned_data.get('lesson_datetime_start') \
and self.cleaned_data.get('lesson_datetime_end') \
and self.cleaned_data['lesson_datetime_start'] >= self.cleaned_data['lesson_datetime_end']:
raise ValidationError({'lesson_datetime_end': "End time must be later than start time."})
return cleaned_data
In the same way, you could add a validator that the duration of the lesson isn't greater than a certain expected time interval (e.g. cannot be longer than 4 hours), by subtracting the two datetime fields and comparing them to datetime.timedelta(hours=x).
You can also do it in your model, so assuming you have a Lesson model with fields lesson_start and lesson_end:
def clean(self):
if self.lesson_start and self.lesson_end and self.lesson_start >= self.lesson_end:
raise ValidationError({'lesson_end': "End time must be later than start time."})
I have a form that maps correctly to my model. What I want to do is have users input their date of birth using three SelectField (day, month, year) and write the combination of those three values to my model.dateOfBirth. I know of the existing DateField and DateTimeField options, but neither of them are suitable.
I've attempted this using a FormField but it fails when I call populate_obj on the whole Form 'str' object has no attribute 'day', assuming that it is trying to set model.dateOfBirth.day which of course, doesn't exist.
I can pull all the relevant data from form.data and write to model outside of populate_obj, but I can't seem to remove the dateOfBirth data to stop populate_obj from failing. There are of course other questions about how I could prefill the form data when passing obj on form creation, but I'm willing to sacrifice that for now.
I've looked at using a FieldList, but the docs say that it gives all its data back in a list, instead of a dict, which will still give me the same issues
Forms
class DOBForm(Form):
day = SelectField(u'Please enter your date of birth',
choices=days,
validators=[InputRequired(message=u' ')]
)
month = SelectField(u' ',
choices=months,
validators=[InputRequired(message=u' ')]
)
year = SelectField(u' ',
choices=years,
validators=[InputRequired(message=u' ')]
)
and
class MainForm(Form):
dateOfBirth = FormField(DOBForm)
Model
class Model
dateOfBirth = db.Column('dateOfBirth', Date)
You can write a function for this.
from datetime import datetime
def date_of_birth(**kwargs):
day = kwargs['day']
month = kwargs['month']
year = kwargs['year']
strip="-"
seq = (day,month,year)
date = datetime.strptime(strip.join(seq) , '%d-%m-%Y')
birthday = stringDate.strftime('%d-%m-%Y')
dob = Model()
dob.dateOfBirth = birthday
session = Session()
session.add(dob)
session.commit()
retval = row2dict(dob)
session.close()
return retval
Now you can call this function.
I don't test this function yet. If you get any error or have any query , let me know..
In my Django app, the program gets the time of an event from the user, say user inputs 10:30 for the time of an event. The program uses modelformset_factory to create a whole bunch of forms:
#forms.py
class EventForm(ModelForm):
class Meta:
model = Event
fields = ['entry', 'time', 'work']
localized_fields = ('time', )
widgets = {
'work': Textarea(attrs = {'cols': 40, 'rows': 8}),
}
#models.py
class Event(models.Model):
'''
Each day there are many events, e.g. at 10 am, the framer orders material, etc.
'''
entry = models.ForeignKey(Entry)
time = models.TimeField()
work = models.TextField()
#views.py:
EventFormSet = modelformset_factory( Event, form = EventForm, exclude = ('entry',),extra = 5)
eventset = self.EventFormSet(request.POST)
all_errors = eventset.errors # All errors returns [{}, {}, {}, {}, {}], so the data is free of error.
try:
eventset.is_valid()
except ValidationError:
return render(request, self.template_name, {'form': self.day_log(initial = self.initial_values), 'eventforms': self.event_formset})
events_instances = eventset.save(commit = False)
for instance in events_instances:
if instance.time:
event_date = datetime.date(the_year, the_month, the_day) # Naive date
event_time = instance.time # Naive time
naive_event_time = timezone.is_naive(event_time) # Checking naivaty
event_datetime = datetime.datetime.combine(event_date, event_time) # Naive datetime object
is_aware_event_datetime = timezone.is_aware(event_datetime) # Checking to see if the datetime object is aware or naive
event_aware_datetime = datetime.datetime(the_year, the_month, the_day, instance.time.hour, instance.time.minute, instance.time.second, tzinfo = pytz.utc) # Making an aware time
is_aware_event_aware_datetime = timezone.is_aware(event_aware_datetime) # Making sure the event_aware_datetime is indeed aware
instance.time = event_aware_datetime
awareness_test = timezone.is_aware(instance.time) # Making sure instance.time is aware
eventset.save() # Here is where the exception is thrown.
The Django app fails to save time data to Postgres. The exception type is DataError with the exception value:
invalid input syntax for type timestamp with time zone:
LINE 1: ...y_event" ("entry_id", "time", "work") VALUES (14, '18:43:04....
with ^ pointing at ' of '18:
Edit:
Here is my database:
CREATE TABLE site_activity_event
(
id serial NOT NULL,
entry_id integer NOT NULL,
"time" time without time zone NOT NULL,
work text NOT NULL,
CONSTRAINT site_activity_event_pkey PRIMARY KEY (id),
CONSTRAINT site_activity_event_entry_id_fkey FOREIGN KEY (entry_id)
REFERENCES site_activity_entry (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED
)
I did sqlall my app to ensure that the data type of model field correspond to that of the app and all is well there. This seems to be a postgres issue, but Django is supposed to take care of all those pesky interface issues with Postgres (https://docs.djangoproject.com/en/1.8/topics/i18n/timezones/#migration-guide), so that makes me think I am missing something. Any help is much appreciated.
Well, is "18:43:04" a valid timestamp with a timezone? Do you think it specifies a unique point in time? If you do then you have uncovered a major bug in PostgreSQL's date/time handling. If not, then you should either supply a valid timestamp or use the correct type in your database - I can't say which without knowing more about these events.
Your Event has a TimeField but it looks like the database was created with a DateTimeField.