How to test input to a Django MultiValueField? - python

I'm currently working in a Django project which defines a custom DateTimeField as follows (in dashboard/forms/fields):
import pytz
from datetime import date, datetime
from django import forms
from django.core.exceptions import ValidationError
from dashboard.forms.widgets import DateTimeWidget
class DateTimeField(forms.MultiValueField):
widget = DateTimeWidget
DATE_FORMAT = '%B %d, %Y'
TIME_FORMAT = '%I:%M %p'
DATETIME_FORMAT = f'{DATE_FORMAT} {TIME_FORMAT}'
def __init__(self, timezone_choices=None, timezone=None, **kwargs):
fields = (forms.CharField(), forms.CharField(), forms.CharField())
super().__init__(fields=fields, **kwargs)
self.timezone_choices = timezone_choices
self.timezone = timezone
#property
def timezone_choices(self):
return self._timezone_choices
#timezone_choices.setter
def timezone_choices(self, value):
self._timezone_choices = self.widget.timezone_choices = value
#property
def timezone(self):
return self._timezone
#timezone.setter
def timezone(self, value):
self._timezone = self.widget.timezone = value
def compress(self, data_list):
try:
date, time, zone = data_list
tz = pytz.timezone(zone)
dt = datetime.strptime(f'{date} {time}', self.DATETIME_FORMAT)
return tz.localize(dt)
except ValueError:
return None
This field is used in a form called SessionForm like so:
class SessionForm(forms.ModelForm):
class Meta:
model = Session
fields = [
'scheduled_for',
]
scheduled_for = DateTimeField(
required=False,
timezone_choices=Family.TIMEZONE_CHOICES
)
This form includes the following clean() method, which I'd like to test:
def clean(self):
cleaned_data = super().clean()
status = cleaned_data.get('status')
location = cleaned_data.get('location')
if status in [Session.SCHEDULED, Session.SCHEDULED_CALENDARED] and not cleaned_data.get('scheduled_for'):
self.add_error(
'scheduled_for',
f"This field is required if the status is '{Session.SCHEDULED}' or '{Session.SCHEDULED_CALENDARED}'.")
return cleaned_data
To this end, I've tried writing the following test:
class SessionCreateTest(TestCase):
def test_scheduled_session_with_scheduled_time_and_expert_and_location_is_valid(self):
scheduled_time = dateutil.parser.parse("5 January 2019 at 1:30 PM")
date = scheduled_time.strftime(DateTimeField.DATE_FORMAT)
time = scheduled_time.strftime(DateTimeField.TIME_FORMAT)
zone = pytz.country_timezones('US')[20] # 'America/Los_Angeles'
scheduled_for = (date, time, zone)
self.data.update(
status=Session.SCHEDULED,
scheduled_for=scheduled_for,
expert=ExpertFactory().id,
location=Session.AT_HOME)
form = SessionForm(data=self.data)
import ipdb; ipdb.set_trace()
Unfortunately, when I drop into the debugger, I see that the form still has errors:
> /Users/kurtpeek/Documents/Dev/lucy2/lucy-web/dashboard/tests/test_sessions.py(666)test_scheduled_session_with_scheduled_time_and_expert_and_location_is_valid()
665 import ipdb; ipdb.set_trace()
--> 666 self.assertTrue(form.is_valid())
667
ipdb> form.errors
{'scheduled_for': ["This field is required if the status is 'Scheduled' or 'Scheduled & Calendared'."]}
However, if I pass my input for this field to the field's compress() method, it seems to be 'parsed' correctly:
ipdb> field = DateTimeField()
ipdb> field.compress(scheduled_for)
datetime.datetime(2019, 1, 5, 13, 30, tzinfo=<DstTzInfo 'America/Los_Angeles' PST-1 day, 16:00:00 STD>)
I don't understand why this test is not passing? How could I 'break this down' further?

I suggest you put the break point inside the clean method of the form. Check the structure of scheduled_for inside cleaned_data dict. It should give you the answer you need to test it.

Related

How to return a list of available time slots via a forms ValidationError

models.py
from django.db import models
from django.utils import timezone
from django.urls import reverse
from django.contrib.auth.models import User
class Customer(models.Model):
username = models.ForeignKey(User,on_delete=models.CASCADE)
name = models.CharField(max_length=20,null=True)
def __str__(self):
return self.name
# Create your models here.
class Booking(models.Model):
customer_name = models.ForeignKey(Customer,on_delete=models.CASCADE,null=True)
username = models.ForeignKey(User,on_delete=models.CASCADE)
qty_plts = models.PositiveSmallIntegerField(default=1)
cbm = models.PositiveSmallIntegerField(default=1)
created_date = models.DateTimeField(default=timezone.now())
delivery_date = models.DateField(null=True)
delivery_time = models.TimeField(null=True)
booking_number = models.CharField(max_length=50,unique=True)
def __str__(self):
return self.booking_number
def save(self, **kwargs):
if not self.booking_number:
self.booking_number = f"{self.delivery_date:%Y%m%d}{self.delivery_time:%H%M}"
super().save(**kwargs)
def get_absolute_url(self):
return reverse('bookmyslot:detail',kwargs={'pk':self.pk})
forms.py
from django import forms
from bookmyslot.models import Booking,Customer
from bootstrap_datepicker_plus import DatePickerInput
import datetime as dt
from django.utils import timezone
HOUR_CHOICES = [(dt.time(hour=x), '{:02d}:00'.format(x)) for x in range(7, 13)]
class BookingForm(forms.ModelForm):
def __init__(self,*args,**kwargs):
user = kwargs.pop('username',None)
super(BookingForm,self).__init__(*args,**kwargs)
self.fields['qty_plts'].label = "Quantity Of Pallets"
self.fields['cbm'].label = "Shipment CBM"
self.fields['delivery_date'].label = "Delivery Date"
self.fields['delivery_time'].label = "Delivery Time"
self.fields['customer_name'].label = "Customer Name"
self.fields['customer_name'].queryset = Customer.objects.filter(username=user)
def clean(self):
cleaned_data = super(BookingForm,self).clean()
booking_number = f"{cleaned_data.get('delivery_date'):%Y%m%d}{cleaned_data.get('delivery_time'):%H%M}"
if Booking.objects.filter(booking_number=booking_number).exists():
raise forms.ValidationError("Requested slot is already booked, please choose another time")
class Meta:
model = Booking
fields = ('customer_name','qty_plts','cbm','delivery_date','delivery_time')
widgets = {'delivery_date':DatePickerInput(options={"daysOfWeekDisabled":[0,6],"minDate":timezone.now().date().strftime('%Y-%m-%d')}),
'delivery_time':forms.Select(choices=HOUR_CHOICES)}
views.py
from django.shortcuts import render
# Create your views here.
from .models import Booking,Customer
from .forms import BookingForm
from django.urls import reverse,reverse_lazy
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import (ListView,DetailView,CreateView,UpdateView,DeleteView,TemplateView)
class BookingCreate(LoginRequiredMixin,CreateView):
login_url = '/login'
redirect_field_name = 'bookmyslot/booking_detail.html'
model = Booking
form_class = BookingForm
def get_form_kwargs(self, **kwargs):
form_kwargs = super(BookingCreate,self).get_form_kwargs(**kwargs)
form_kwargs['username'] = self.request.user
return form_kwargs
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.username = self.request.user
self.object.save()
return super().form_valid(form)
I am trying to figure out how to return a list of available time slots, using a ValidationError.
There are 6 time slots [delivery_time] to choose from on any given date -> [7,8,9,10,11,12]
The booking_number field is a unique id that is a concatenation of the delivery_date and delivery_time, which is generated each time a user successfully creates a booking.
So, let's assume there are 3 existing bookings for 2021-10-21 at 7:00,08:00 & 10:00, which are saved in the Booking model with the following booking numbers:
202110210700
202110210800
202110211000
Assuming a user tries to book over a slot that already exists e.g. 202110210700,
the validation error should return "Requested slot is already booked, please choose another from one of these available slots:
09:00
11:00
12:00
How can I achieve this?
You can use an utility function like this to return the remained time slot in the form validation like this :
app_name/utilities.py
def list_diff(l1: list, l2: list):
""" Return a list of elements that are present in l1
or in l2 but not in both l1 & l2.
IE: list_diff([1, 2, 3, 4], [2,4]) => [1, 3]
"""
return [i for i in l1 + l2 if i not in l1 or i not in l2]
def check_free_time(time_slot: list, exist_list: list):
""" Return the list of available time slot if exist,
according to a given exist slot list.
Return the remained time slot, or empty list if all are used
IE: ([7, 12], [7, 8, 9, 10, 11, 12]) => [8, 9, 10, 11]
"""
remain_slot = list_diff(time_slot, exist_list)
return remain_slot
Now import the check_free_time in your forms.py file and use it if the booking_number exist.
from datetime import datetime
from .utilities import check_free_time
if Booking.objects.filter(booking_number=booking_number).exists():
today = datetime.today()
d = today.day
m = today.month
y = today.year
# Retrieve today's bookings
today_bookings = Booking.objects.filter(delivery_date__year=y,delivery_date__month=m delivery_date__day=d)
# A list of today's bookings time slot (take only hours)
# Return something like <QuerySet [{'delivery_date__hour': 11}, ...]>
today_time_slot = today_bookings.values('delivery_date__hour')
# Convert it to list of hours values since the utility function accept list.
today_time_slot_list = [h['delivery_date__hour'] for h in list(today_time_slot)]
# The line above return something like [9, 11, ...]
all_time_slot = [7, 8, 9, 10, 11, 12]
# Now we can call the utility function `check_free_time`
available_slot = check_free_time(all_time_slot, today_time_slot_list)
if available_slot: # The are some available slot (list not empty)
# I use python3.6 f-string to format the message
# Note that the list is in a raw format ([8,11,12]), you can do better like ['8h:00', '11h:00', '12h:00']
message = f"Requested slot is already booked, please choose another time in {available slot}."
raise forms.ValidationError(message)
else: # The list is empty, all slot are taken
message = "The are not available slot for this booking today."
raise forms.ValidationError(message)
NB : I tested only in a python an Django shell, if some errors try to add in comments.

time data '0:02:00' does not match format '%H %M %S'

I want to save a string as time format to store it in the database for a django project,
Here's my utils.py file:
import datetime
import re
import math
from django.utils.html import strip_tags
def count_words(text_string):
word_string = strip_tags(text_string)
matching_words = re.findall(r'\w+', word_string)
count = len(matching_words)
return count
def get_read_time(text_string):
count = count_words(text_string)
read_time_min = math.ceil(count/200.0) #Assuming 200 words per min Reading
read_time = str(datetime.timedelta(minutes=read_time_min))
return read_time
Here's the required portion of the models.py file:
class Post(models.Model):
read_time = models.TimeField(null=True, blank=True)
Here's the required portion of the views.py file:
class PostDetailView(DetailView):
model = Post
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(**kwargs)
texts = self.object.content
# print(texts)
read_time=get_read_time(texts)
# print(read_time)
Post.objects.create(read_time=datetime.strptime(read_time, "%H:%M:%S"))
return context
The Output format of the string is 0:02:00 this I want to save it in the database as datetime field.
But I am encountering this error:-
Exception Type: ValueError at /post/this-blog-contains-an-image-with-it/
Exception Value: 's' is a bad directive in format '%H:%MM:%ss'
A TimeField [Django-doc] expects a time object, not a datetime object. You can make use of the .time() method [Django-doc] to retrieve the time part:
class PostDetailView(DetailView):
model = Post
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(**kwargs)
texts = self.object.content
read_time=get_read_time(texts)
Post.objects.create(read_time=datetime.strptime(read_time, '%H:%M:%S').time())
return context
It is however rather odd to construct a string instead of returning the timedelta part itself:
def get_read_time(text_string):
count = count_words(text_string)
read_time_min = math.ceil(count/200.0) #Assuming 200 words per min Reading
return datetime.timedelta(minutes=read_time_min)
Then you can use this timedelta to obtain the time:
class PostDetailView(DetailView):
model = Post
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(**kwargs)
texts = self.object.content
read_time=get_read_time(texts)
Post.objects.create(read_time=(datetime.datetime.min + read_time).time())
return context
It furthermore does not make sense to construct objects in the get_context_data. A GET request is supposed to have no side-effects, so you should only make database changes for POST, PUT, PATCH, DELETE, etc. requests. Not in the get_context_data.

add time when we save a Timefield in django

I have a dateTime field in a model. The dateTime field named breakfast_start_time takes an input.
I have to save another variable or timefield(whichever is better) named breakfast_attendence_start_time whose value should be automatically saved 15 minutes less than the breakfast_start_time.
For this we use
def save(self, *args, **kwargs):
#do something
super().save(*args, *kwargs)
I am trying to do
breakfast_attendence_start_time = breakfast_start_time - time(15,0)
but it is giving error that
class TimeField does not define '_sub_', so the '-' operator cannot be used on its instances
Edited:
Here is the full code
class Hostel(models.Model):
name = models.CharField(max_length=15)
breakfast_start_time = models.TimeField()
lunch_start_time = models.TimeField()
snacks_start_time = models.TimeField()
dinner_start_time = models.TimeField()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
I am taking start time from admin panel and I want to add 4 more variable/field like breakfast_attendence_start_time whose value should be saved automatically 15 minutes earlier than breakfast_start_time how can I achive that.
You should use timedelta to sub specific time with DateTime field. Such as
import datetime
time_before_15_minute = datetime.datetime.now() - datetime.timedelta(minutes=15)
Use DateTimeField instead of TimeField and use timedelta to make substractions
from datetime import datetime, timedelta
n = datetime(2019, 10, 4, 12, 30)
m = n - timedelta(minutes = 15) # m is now datetime(2019, 10, 4, 12, 15)
You can play with the DateTimeField but this will return time of when this function was called or used. Hope it helps
from django.utils import timezone
class AKA(models.Model):
create_time = models.DateTimeField(default=timezone.now)
def __str__(self):
return self.create_time

Can't save datetime.now() to DateTimeField

I've defined a model as follows (Shortened it for the question)
from datetime import datetime, date, timedelta
class Case(models.Model):
received_email_sent = models.DateTimeField(null=True, blank=True, default=None)
def send_received_email(self):
message = settings.EMAIL_HEADER + self.case_received_email() + settings.EMAIL_FOOTER
send_mail('Subject here', message, settings.EMAIL_HOST_USER, ['xxx#xxx.com'], fail_silently=False)
self.received_email_sent = datetime.now()
and in the view I call send_received_email on an existing object. I know that the send_received_email block is being entered because I'm receiving the emails every time I test this out, but the self.received_email_sent = datetime.now() part is leaving that field as its default value (None) every time. Here's the relevant part of the view:
from logbook.models import Case
def job_email(request, case_id):
case = get_object_or_404(Case,pk=case_id)
case.send_received_email()
return HttpResponseRedirect('/jobs/'+str(case.case_id))
I have also tried an alternative method, where saving the field is done in the view instead of the model, like so:
models:
class Case(models.Model):
received_email_sent = models.DateTimeField(null=True, blank=True, default=None)
def send_received_email(self):
message = settings.EMAIL_HEADER + self.case_received_email() + settings.EMAIL_FOOTER
send_mail('Subject here', message, settings.EMAIL_HOST_USER, ['xxx#xxx.com'], fail_silently=False)
#self.received_email_sent = datetime.now()
views:
from datetime import datetime
from logbook.models import Case
def job_email(request, case_id):
case = get_object_or_404(Case,pk=case_id)
case.send_received_email()
case.received_email_sent = datetime.now()
return HttpResponseRedirect('/jobs/'+str(case.case_id))
I have also tried both of the above routes with various tweaks like removing the brackets on now() and changing from datetime import datetime to just import datetime. No joy. Thanks for having a look at this.
You need to call self.save() at the end of send_received_email().

How to know if a period overlaps another

I'd like to make an application that allows me to reserve an item during a specified period.
I need a function to check if the specified item is already booked during the period i want to use it (so the booking should fail). Can you help me?
models.py
from django.db import models
from datetime import *
from django.db.models import Q
import datetime
from django.core.exceptions import ValidationError
class Reservation(models.Model):
date_debut = models.DateTimeField('debut de la reservation')
date_fin = models.DateTimeField('fin de la reservation')
obj_res = models.ForeignKey('Materiel')
notice = models.CharField(max_length=200)
personne = models.ForeignKey('Personne')
def __int__(self):
return self.id
def save(self, *args, **kwargs):
new_start_date = datetime.datetime(2013, 11, 16, 10, 00)
new_end_date = datetime.datetime(2013, 11, 16, 11, 00)
material = Materiel.objects.get(nom="Bimaire 1")
clashing_reservations = Reservation.objects.filter(obj_res=material).filter(
Q(date_debut__lte=new_start_date, date_fin__gte=new_start_date) |
Q(date_debut__lt=new_end_date, date_fin__gte=new_end_date)
)
if clashing_reservations.exists():
raise ValidationError('Those dates clash with another reservation.')
return super(Reservation, self).save(*args, **kwargs)
class Materiel(models.Model):
nom = models.CharField(max_length=200)
description = models.CharField(max_length=200)
responsable = models.CharField(max_length=200)
modalites = models.CharField(max_length=200)
def __unicode__(self):
return self.nom
class Personne(models.Model):
nom = models.CharField(max_length=200)
prenom = models.CharField(max_length=200)
def __unicode__(self):
return self.nom
views.py
def reservation(request):
if request.POST:
form = ReservationForm(request.POST, request.FILES)
if form.is_valid():
form.save()
else:
form = ReservationForm()
args = {}
args.update(csrf(request))
args["form"] = form
return render_to_response("reservation.html", args)
EDIT
Thanks so far it's seems to work.
But now i want define that new_start_date and new_end_date are the actual values of the form.
This is untested code, but I believe that this logic should test whether any other reservations overlap the one submitted in the form. This should probably be put in a clean method of the form, or some other validation. Perhaps even on the save method of the Reservation model:
from django.db.models import Q
new_start_date = datetime.datetime(2013, 11, 16, 10, 00)
new_end_date = datetime.datetime(2013, 11, 16, 11, 00)
material = Materiel.objects.get(nom='Whatever')
clashing_reservations = Reservation.objects.filter(objet=material).filter(
Q(date_debut__lte=new_start_date, date_fin__gte=new_start_date) |
Q(date_debut__lt=new_end_date, date_fin_gte=new_end_date)
)
if clashing_reservations.exists():
raise ValidationError('Those dates clash with another reservation.')
I don't know the format your dates are in but regardless you can use the module datetime to compare (subtract, add, higher/lower etc.) dates and times with one another.
So I've made a simple example to illustrate its use (I presume your format is months/days/years):
from datetime import *
debut_date = datetime.strptime(date_debut_db, "%m/%d/%y")
fin_date = datetime.strptime(date_fin_db, "%m/%d/%y")
debut_date2 = datetime.strptime(date_debut_form, "%m/%d/%y")
fin_date2 = datetime.strptime(date_fin_form, "%m/%d/%y")
if (debut_date2 > debut_date and debut_date2 < fin_date) or (fin_date2 > debut_date and fin_date2 < fin_date):
print "Impossible!"
else:
print "Possible!"
date_debut_db and date_fin_db are the dates you get out of your database whereas date_debut_form and date_fin_form are the ones that the user fills in.

Categories

Resources