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."})
Related
So I have this Post model. I want to be able to retrieve all posts that were created in a month, year under a certain time zone.
My goal is to implement a feature where a user anywhere in the world let's say in PST can get all posts by another person from a certain month in their time zone. So let's say user A is in EST and user B is in PST (3 hours behind EST). User B wants to see all posts that user A created in October of 2021. Since the app will display posts in the time zone the user is currently in (we send date time in UTC then the front-end converts to local time) then the app should only send to user B all posts by user A that were created in October 2021 PST. So for example if user A (the user in EST) made a post at 11pm Oct 31 2021 EST(8pm Oct 31 2021 PST) and a post at 1am Nov 1st 2021 EST (10pm Oct 31st 2021 PST) then user B should on get both posts back, because the 2nd one was made in November in EST, but October in PST.
model.py
class Post(models.Model):
uuid = models.UUIDField(primary_key=True)
created = models.DateTimeField('Created at', auto_now_add=True)
updated_at = models.DateTimeField('Last updated at', auto_now=True, blank=True, null=True)
creator = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="post_creator")
body = models.CharField(max_length=POST_MAX_LEN)
So for example if a user creates 10 posts in November, 2 in December of 2021 in PST. Then I have a view that takes month, year and time_zone and let's say the url looks something like /post/<int:month>/<int:year>/<str:time_zone> and the user pings /post/11/2021/PST then it should return the 10 posts from November. How do I return all posts from a month and year in a time zone given time zone, month and year?
Note: The tricky edge case to take into consideration is if they post on the very last day of a month very late. Depending on the time zone something like 12/31/2021 in UTC could be 01/01/2022. Because Django stores datetime fields in UTC what would need to be done is converted created to the given time_zone then get posts from the specified month and year.
Setup:
Django 3.2.9
Postgresql
Attempted Solutions
The most obvious solution to me is to convert created to the specified time_zone then to do Post.objects.filter(created__in_range=<some range>)
Note
Main issue seems to be Pyzt, which takes in time in a very specific format "Amercian/Los_Angeles" w.e format this is. Rather than the abbreviated time zone format like "PST".
Take the month's first moment (midnight on the 1st) in UTC and the next month's first moment in UTC, adjust them with the timezone you want, do a posted__range=(a, b) query?
This might work (but date math is fiddly...).
This requires python-dateutil to make computing the end time robust.
import datetime
import pytz
from dateutil.relativedelta import relativedelta
from django.utils import timezone
year = 2021
month = 6
tz = pytz.timezone("America/Los_Angeles")
start = datetime.datetime(year, month, 1)
end = start + relativedelta(months=1)
start_as_tz_in_utc = tz.localize(start).astimezone(pytz.utc)
end_as_tz_in_utc = tz.localize(end).astimezone(pytz.utc)
print(start_as_tz_in_utc, end_as_tz_in_utc, sep="\n")
That prints out
2021-06-01 07:00:00+00:00
2021-07-01 07:00:00+00:00
which seems about right.
Then you might make a query such as
posts = Post.objects.filter(created__range=(
start_as_tz_in_utc,
end_as_tz_in_utc,
))
I would try something like this:
Assuming you are passing a given month through as given_month
and given year as given_year
and given timezone as given_timezone
Model_Name.objects.filter(created.split('/',4)[2]=given_month,created.split('/',4)[3]=given_year, created.split('/',4)[4]=given_timezone)
This should get the value from the month and year and timzone section of your post. You might need to play around with what I gave you. Also it might be better to add a relationship from user to post so you can filter the user for posts with my given answer. In most cases this will be a lot more efficient, assuming there are more posts than users. This should get you on the correct track.
Consider saving the record in UTC date time, do not attempt to save any time zone offset. just record the time the data arrived to the server in UTC, and let the browser or your app translate it to their configured timezone. It doesnt matter if your User is in China created a post and then moved to NY the next day and read the previously created post, the created_at datetime will be same for both locations because it is universal. So if the user B wants to recover the data the user A recorded since october it will as easy to querying created_at::date>="2021-10-01"
Sorry, if it is not very on point to what you're expecting, but Django creates a PostgreSQL timestamp with a time zone field for the DateTimeField.
# \d posts_post
Table "public.posts_post"
Column | Type | Collation | Nullable | Default
------------+--------------------------+-----------+----------+---------
uuid | uuid | | not null |
created | timestamp with time zone | | not null |
updated_at | timestamp with time zone | | |
body | character varying(500) | | not null |
Indexes:
"posts_post_pkey" PRIMARY KEY, btree (uuid)
It means you can query them as timestamps with a custom field class from Timestamp fields in django.
from django.db import models
from datetime import datetime
from time import strftime
class UnixTimestampField(models.DateTimeField):
"""UnixTimestampField: creates a DateTimeField that is represented on the
database as a TIMESTAMP field rather than the usual DATETIME field.
"""
def __init__(self, null=False, blank=False, **kwargs):
super(UnixTimestampField, self).__init__(**kwargs)
# default for TIMESTAMP is NOT NULL unlike most fields, so we have to
# cheat a little:
self.blank, self.isnull = blank, null
self.null = True # To prevent the framework from shoving in "not null".
def db_type(self, connection):
typ=['TIMESTAMP']
# See above!
if self.isnull:
typ += ['NULL']
if self.auto_created:
typ += ['default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP']
return ' '.join(typ)
def to_python(self, value):
if isinstance(value, int):
return datetime.fromtimestamp(value)
else:
return models.DateTimeField.to_python(self, value)
def get_db_prep_value(self, value, connection, prepared=False):
if value==None:
return None
# Use '%Y%m%d%H%M%S' for MySQL < 4.1
return strftime('%Y-%m-%d %H:%M:%S',value.timetuple())
In the query, you will pass the timestamp created from the required timezone DateTime. This approach is also should be faster since you don't need to perform 1 additional DateTime conversion.
class Post(models.Model):
uuid = models.UUIDField(primary_key=True)
created = UnixTimestampField(verbose_name='Created at', auto_now_add=True)
updated_at = UnixTimestampField(verbose_name='Last updated at', auto_now=True, blank=True, null=True)
from datetime import datetime, timezone
Post.objects.filter(created__gte=datetime(year=2021, month=11, day=1, tzinfo=timezone.utc).timestamp(), created__lt=datetime(year=2021, month=12, day=1, tzinfo=timezone.utc).timestamp())
In django, I'm validating a date:
from datetime import date
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
if start_date < date.today():
raise ValidationError(
_('%(start_date)s must be on or after today.'),
params={'start_date': start_date},
)
where start_date is a datetime instance.
What do I need to write in place of s in %(start_date)s in order to format the date nicely; preferably as dd-mmm-yyyy?
You can use strftime(...) method as,
def validate_date(start_date):
if start_date < date.today():
raise ValidationError(
_('%(start_date)s must be on or after today.'),
params={'start_date': start_date.strftime("%d-%b-%Y")},
)
return start_date
You will get the exception as,
django.core.exceptions.ValidationError: ['01-Jan-2020 must be on or after today.']
You can see the full set for format code that supported by strftime(...) method here, strftime() and strptime() Format Codes
Update-1
You can use the str.format() method as,
def validate_date(start_date):
if start_date < date.today():
raise ValidationError(
_("{:%d-%b-%Y} must be on or after today.".format(start_date))
)
return start_date
Update-2
as #Justin Ezequiel mention in his comment, you can use the f-string as
def validate_date(start_date):
if start_date < date.today():
raise ValidationError(
_(f"{start_date:%d-%b-%Y} must be on or after today.")
)
return start_date
I'm making a website using django.
class Member(models.Model):
...
end_date = models.DateField(blank=True, default=(datetime.now() + timedelta(days=30)))
Membership_status = models.IntegerField(blank=True, null=True, default=1) # 1 = active, 0=deactivate, 2=refund
What I want to do is comparing the end_date field to today.date every 1.a.m. and if today's day < end_date, Membership_status field is changed to 0 automatically.
I heard I should use django-kronos(https://github.com/jgorset/django-kronos).
But I can't understand the using method.
Is there anyone can tell me details how I implement what I want?
Any help will be very helpful to me, thanks!
First of all, this is not an answer to your original query, but merely a suggestion for your future,
Never pass a function call into your field defaults. If you did, the function would be evaluated at the time of your migrations. If you look into the migration files you can see for sure. Instead wrap it in a function and pass that as a callable.
Eg:
from django.utils import timezone
def TODAY():
return timezone.now().date()
def NEXT_MONTH_DAY():
return TODAY() + timedelta(days=30)
Now, in your models,
class Member(models.Model):
...
end_date = models.DateField(blank=True, default=NEXT_MONTH_DAY)
This way the function NEXT_MONTH_DAY is called whenever an instance of Member is created.
EDIT:
For your original query, I haven't tested the code, but I suppose you are looking for maybe something like this,
import kronos
#kronos.register('0 1 * * *')
def the_task():
for member in Member.objects.all():
if TODAY() == member.end_date:
member.Membership_status = 0
member.save()
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.
I am following this to add a custom validation on my modelform and it is working...mostly.
My code:
from django import forms
from django.utils.translation import ugettext_lazy as _
from datetime import datetime, timedelta
from datetimewidget.widgets import DateWidget
from .models import User
class UserForm(forms.ModelForm):
class Meta:
# Set this form to use the User model.
model = User
# Constrain the UserForm to just these fields.
fields = ("birthdate")
widgets = {
'birthdate': DateWidget(attrs={'id':"id_birthdate"}, bootstrap_version=3)
}
def clean_birthdate(self):
birthdate = self.cleaned_data["birthdate"]
min_time = datetime.strptime('1920-01-01', '%Y-%m-%d').date()
delta = birthdate - min_time
if delta <= timedelta(days=0):
raise forms.ValidationError(_("We don't accept people born before 1920"))
return birthdate
It raises the error like intended until 1900-01-01, but once i enter to 1899 it doesn't.
I am not sure what may be causing it. I am using DateTimeWidget.
The error i am getting is:
year=1899 is before 1900; the datetime strftime() methods require year >= 1900
I checked the result of the comparison and it is working as intended (False for years below 1920).
In short model is being updated and error is not being raised when it should.
This is a limitation of python's built-in strftime function. It does not support dates prior to 1900. Try this instead
if birthdate.year < 1920:
raise forms.ValidationError(_("We don't accept people born before 1920"))