I have a list of client records in my database. Every year, we generate a single work order for each client. Then, for each work order record, the user should be able to create a note that is specific to the work order. However, not all work orders need a note, just some.
Now, I can't simply add a note field to the work order because some times, we need to create the note before the work order is even generated. Sometimes this note is specific to a work order that won't happen for 2-3 years. Thus, the notes and the work order must be independent, although they will "find" each other when they both exist.
OK, so here's the situation. I want the user to be able to fill out a very simple note form, where they have two fields: noteYear and note. Thus, all they do is pick a year, and then write the note. The kicker is that the user should not be able to create two notes for the same year for the same client.
What I'm trying to get as is validating the note by ensuring that there isn't already a note for that year for that client. I'm assuming this would be achieved by a custom is_valid method within the form, but I can't figure out how to go about doing that.
This is what I tried so far (note that I know it's wrong, it doesn't work, but it's my attempt so far):
Note that systemID is my client record
My model:
class su_note(models.Model):
YEAR_CHOICES = (
('2013', 2013),
('2014', 2014),
('2015', 2015),
('2016', 2016),
('2017', 2017),
('2018', 2018),
('2019', 2019),
('2020', 2020),
('2021', 2021),
('2022', 2022),
('2023', 2023),
)
noteYear = models.CharField(choices = YEAR_CHOICES, max_length = 4, verbose_name = 'Relevant Year')
systemID = models.ForeignKey(System, verbose_name = 'System ID')
note = models.TextField(verbose_name = "Note")
def __unicode__(self):
return u'%s | %s | %s' % (self.systemID.systemID, self.noteYear, self.noteType)
And my form:
class SU_Note_Form(ModelForm):
class Meta:
model = su_note
fields = ('noteYear', 'noteType', 'note')
def is_valid(self):
valid = super (SU_Note_Form, self).is_valid()
#If it is not valid, we're done -- send it back to the user to correct errors
if not valid:
return valid
# now to check that there is only one record of SU for the system
sysID = self.cleaned_data['systemID']
sysID = sysID.systemID
snotes = su_note.objects.filter(noteYear = self.cleaned_data['noteYear'])
for s in snotes:
if s.systemID == self.systemID:
self._errors['Validation_Error'] = 'There is already a startup note for this year'
return False
return True
EDIT -- Here's my solution (thanks to janos for sending me in the right direction)
My final form looks like this:
class SU_Note_Form(ModelForm):
class Meta:
model = su_note
fields = ('systemID', 'noteYear', 'noteType', 'note')
def clean(self):
cleaned_data = super(SU_Note_Form, self).clean()
sysID = cleaned_data['systemID']
sysID = sysID.systemID
try:
s = su_note.objects.get(noteYear = cleaned_data['noteYear'], systemID__systemID = sysID)
print(s)
self.errors['noteYear'] = "There is already a note for this year."
except:
pass
return cleaned_data
For anyone else looking at this code, the only confusing part is the line that has: sysID = sysID.systemID. The systemID is actually a field of another model - even though systemID is also a field of this model -- poor design, probably, but it works.
See this page in the Django docs:
https://docs.djangoproject.com/en/dev/ref/forms/validation/
Since your validation logic depends on two fields (the year and the systemID), you need to implement this using a custom cleaning method on the form, for example:
def clean(self):
cleaned_data = super(SU_Note_Form, self).clean()
sysID = cleaned_data['systemID']
sysID = sysID.systemID
try:
su_note.objects.get(noteYear=cleaned_data['noteYear'], systemID=systemID)
raise forms.ValidationError('There is already a startup note for this year')
except su_note.DoesNotExist:
pass
# Always return the full collection of cleaned data.
return cleaned_data
Related
Here I have a simple function for sending leave request and accepting by the admin.This code works for now but I want to add some feature here.For example if the user enter day = 2 which is IntegerField then it get stores into databse then after the leave has been accepted by the function below def accept_leave(request,pk): I want to display the remaining days of leave(Example:1 day 12 hours and 30 sec. remaining to complete leave ).After 2 days completed it should display some message like you leave has been completed.
I got no idea for starting this .How can I do it ?Any help would be great.
Is there any mistake in my approach ?
EDIT: Now I removed the day(Integer Field) and added start_day and end_day as DateTimeField. Now how can I display the remaining days and time of leave after the leave is accepted ?
models.py
class Leave(models.Model):
staff = models.ForeignKey(get_user_model(),on_delete=models.CASCADE,related_name='staff_leave')
organization = models.ForeignKey(Organization,on_delete=models.CASCADE,related_name='staff_leave')
sub = models.CharField(max_length=300)
msg = models.TextField()
start_day = models.DateTimeField()
end_day = models.DateTimeField()
#day = models.IntegerField(default=0)
is_accepted = models.BooleanField(default=False)
is_rejected = models.BooleanField(default=False)
sent_on = models.DateTimeField(auto_now_add=True)
views.py
def send_leave_request(request):
form = MakeLeaveForm()
if request.method == 'POST':
form = MakeLeaveForm(request.POST)
if form.is_valid():
leave_days = form.cleaned_data['day']
org = form.cleaned_data['organization']
start_day = form.cleaned_data['start_day']
end_day = form.cleaned_data['end_day']
diff = end_day - start_day
leave_days = diff.days
print('org',org)
if leave_days > request.user.staff.organization.max_leave_days:
messages.error(request,'Sorry can not be sent.Your leave days is greater than {}.'.format(request.user.staff.organization.max_leave_days))
return redirect('organization:view_leaves')
else:
leave = form.save(commit=False)
leave.staff = request.user
leave.organization = org
leave.save()
return redirect('organization:view_leaves')
return render(request,'organization/send_leaves.html',{'form':form})
def accept_leave(request,pk):
leave = get_object_or_404(Leave, pk=pk)
leave.is_accepted = True
leave.is_rejected = False
leave.day = ?
leave.save()
return redirect('organization:leave_detail',leave.pk)
For your leave request, why don't you store something like :
start_date and end_date
The idea is (ideally) to store only things you can't compute.
Then you can make a python property that computes the number of days between start_date and end_date (a "fget" would be enough for the property). A python property won't be stored in your database but it's not a big deal because you can compute it ! So you don't have to store it.
days = property(fget=_get_days, doc="type: Integer")
That means whenever the "days" attribute of an object "Leave" is accessed, the function "_get_days" is called to retrieve what you want.
If self represents a Leave object and you do print(self.days) it will print the result of _get_days finally.
The "doc" part is just here to indicate your property returns an Integer. It is not mandatory but a good practice in order not to forget it.
Then you must write that method "_get_days" (it must be above your property definition or Python won't know what is "_get_days"
def _get_days(self):
return self.end_date - self.start_date
(something like that, that you convert into an int somehow)
Moreover, for your additional functionality, you must know how much leaves your user can have. Just store that on the user, on your user team or whatever you want.
Then to check if the user has remaining leaves he can take, just browse a queryset with all his accepted leaves and use the property mentioned above.
Then you substract the result to the total number of leaves the user can take.
I'm using Django 1.8.4 in my dev machine using Sqlite and I have these models:
class ModelA(Model):
field_a = CharField(verbose_name='a', max_length=20)
field_b = CharField(verbose_name='b', max_length=20)
class Meta:
unique_together = ('field_a', 'field_b',)
class ModelB(Model):
field_c = CharField(verbose_name='c', max_length=20)
field_d = ForeignKey(ModelA, verbose_name='d', null=True, blank=True)
class Meta:
unique_together = ('field_c', 'field_d',)
I've run proper migration and registered them in the Django Admin. So, using the Admin I've done this tests:
I'm able to create ModelA records and Django prohibits me from creating duplicate records - as expected!
I'm not able to create identical ModelB records when field_b is not empty
But, I'm able to create identical ModelB records, when using field_d as empty
My question is: How do I apply unique_together for nullable ForeignKey?
The most recent answer I found for this problem has 5 year... I do think Django have evolved and the issue may not be the same.
Django 2.2 added a new constraints API which makes addressing this case much easier within the database.
You will need two constraints:
The existing tuple constraint; and
The remaining keys minus the nullable key, with a condition
If you have multiple nullable fields, I guess you will need to handle the permutations.
Here's an example with a thruple of fields that must be all unique, where only one NULL is permitted:
from django.db import models
from django.db.models import Q
from django.db.models.constraints import UniqueConstraint
class Badger(models.Model):
required = models.ForeignKey(Required, ...)
optional = models.ForeignKey(Optional, null=True, ...)
key = models.CharField(db_index=True, ...)
class Meta:
constraints = [
UniqueConstraint(fields=['required', 'optional', 'key'],
name='unique_with_optional'),
UniqueConstraint(fields=['required', 'key'],
condition=Q(optional=None),
name='unique_without_optional'),
]
UPDATE: previous version of my answer was functional but had bad design, this one takes in account some of the comments and other answers.
In SQL NULL does not equal NULL. This means if you have two objects where field_d == None and field_c == "somestring" they are not equal, so you can create both.
You can override Model.clean to add your check:
class ModelB(Model):
#...
def validate_unique(self, exclude=None):
if ModelB.objects.exclude(id=self.id).filter(field_c=self.field_c, \
field_d__isnull=True).exists():
raise ValidationError("Duplicate ModelB")
super(ModelB, self).validate_unique(exclude)
If used outside of forms you have to call full_clean or validate_unique.
Take care to handle the race condition though.
#ivan, I don't think that there's a simple way for django to manage this situation. You need to think of all creation and update operations that don't always come from a form. Also, you should think of race conditions...
And because you don't force this logic on DB level, it's possible that there actually will be doubled records and you should check it while querying results.
And about your solution, it can be good for form, but I don't expect that save method can raise ValidationError.
If it's possible then it's better to delegate this logic to DB. In this particular case, you can use two partial indexes. There's a similar question on StackOverflow - Create unique constraint with null columns
So you can create Django migration, that adds two partial indexes to your DB
Example:
# Assume that app name is just `example`
CREATE_TWO_PARTIAL_INDEX = """
CREATE UNIQUE INDEX model_b_2col_uni_idx ON example_model_b (field_c, field_d)
WHERE field_d IS NOT NULL;
CREATE UNIQUE INDEX model_b_1col_uni_idx ON example_model_b (field_c)
WHERE field_d IS NULL;
"""
DROP_TWO_PARTIAL_INDEX = """
DROP INDEX model_b_2col_uni_idx;
DROP INDEX model_b_1col_uni_idx;
"""
class Migration(migrations.Migration):
dependencies = [
('example', 'PREVIOUS MIGRATION NAME'),
]
operations = [
migrations.RunSQL(CREATE_TWO_PARTIAL_INDEX, DROP_TWO_PARTIAL_INDEX)
]
Add a clean method to your model - see below:
def clean(self):
if Variants.objects.filter("""Your filter """).exclude(pk=self.pk).exists():
raise ValidationError("This variation is duplicated.")
I think this is more clear way to do that for Django 1.2+
In forms it will be raised as non_field_error with no 500 error, in other cases, like DRF you have to check this case manual, because it will be 500 error.
But it will always check for unique_together!
class BaseModelExt(models.Model):
is_cleaned = False
def clean(self):
for field_tuple in self._meta.unique_together[:]:
unique_filter = {}
unique_fields = []
null_found = False
for field_name in field_tuple:
field_value = getattr(self, field_name)
if getattr(self, field_name) is None:
unique_filter['%s__isnull' % field_name] = True
null_found = True
else:
unique_filter['%s' % field_name] = field_value
unique_fields.append(field_name)
if null_found:
unique_queryset = self.__class__.objects.filter(**unique_filter)
if self.pk:
unique_queryset = unique_queryset.exclude(pk=self.pk)
if unique_queryset.exists():
msg = self.unique_error_message(self.__class__, tuple(unique_fields))
raise ValidationError(msg)
self.is_cleaned = True
def save(self, *args, **kwargs):
if not self.is_cleaned:
self.clean()
super().save(*args, **kwargs)
One possible workaround not mentioned yet is to create a dummy ModelA object to serve as your NULL value. Then you can rely on the database to enforce the uniqueness constraint.
I'm having strange behavior with a form ModelChoiceField. A little background. I need a form that has a variable queryset for a certain field. Looking at this question and this, I have created an init method for my form to handle this based on the request passed in to the init method.
class QueryTimeEntryForm(forms.Form):
query_start_date = forms.DateField(label='Start Date:', required=True, widget=forms.TextInput(), input_formats=['%m/%d/%Y', '%Y-%m-%d'])
query_end_date = forms.DateField(label='End Date:', required=True, widget=forms.TextInput(), input_formats=['%m/%d/%Y', '%Y-%m-%d'])
time_query_unit = forms.ModelChoiceField(queryset=Unit.objects.all().order_by('unit'), label='', required=False, empty_label='Choose a unit', widget=forms.Select())
time_query_employee = forms.ModelChoiceField(queryset=Employee.objects.none(), label='', required=False, empty_label='Choose an employee', widget=forms.Select())
time_query_radio = forms.ChoiceField(label='', widget=forms.RadioSelect(attrs={'class':'queryRadio'}), choices=QUERY_CHOICES, initial='1')
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super (QueryTimeEntryForm, self).__init__(*args, **kwargs)
#depending on the user, set the queryset of the employee drop down
#get the employee category for the user
today = datetime.today()
emp = Employee.objects.filter(user__exact=self.request.user)
ec = EmployeeCategory.objects.filter(employee__exact=emp[0]).filter(effectiveDate__lte=today).filter(Q(enddate__gte=today) | Q(enddate__isnull=True))[0]
if ec.category.category == 1:
self.fields['time_query_employee'].queryset = Employee.objects.filter(user__exact=self.request.user)
elif ec.category.category == 2:
#get the unit for this category 2 employee
unit = EmployeeUnit.objects.filter(employee__exact=emp).filter(effective_date__lte=today).filter(Q(end_date__gte=today) | Q(end_date__isnull=True))[0].unit
#get the employees who currently share the unit with the current category 2 employee, excluding the current category 2 employee
self.fields['time_query_employee'].queryset = Employee.objects.filter(employee_for_employeeunit__unit__exact=unit).filter(Q(employee_for_employeeunit__end_date__gte=today) | Q(employee_for_employeeunit__end_date__isnull=True)).exclude(employee_for_employeeunit__exact=emp).order_by('user__first_name')
else:
#get category 1
cat = Category.objects.filter(category__exact=1)[0]
self.fields['time_query_employee'].queryset = Employee.objects.filter(employee_for_employeecategory__category__exact=cat).filter(Q(employee_for_employeecategory__enddate__gte=today) | Q(employee_for_employeecategory__enddate__isnull=True)).order_by('user__first_name')
When the form isn't bound, everything works just fine. I get just the Employees I expect in the drop down in the html. The problem I'm having is that when posting the form, the ModelChoiceField fails validation. As I step through, I notice that similar to this question, I'm getting the "Select a valid choice" error, probably because the queryset is Employees.objects.none() when super is called and the validation occurs. Should I clear all the errors and redo a full_clean after setting the queryset, or should I take a different approach? Basically I'm stuck, not understanding exactly what's going on, nor where to go from here. Everything was working fine before I added the init method and had a standard Employee queryset, so it must be something I'm doing with that.
Please help. Thanks!
Have you been able to test all three of your category branches? Personally I would probably insert a pdb.set_trace() call at the beginning of the __init__ call, run it using the Django development server, and see what happens when I post the form.
As a readability tip - you can omit __exact, and you can replace your filter(Q() | Q()) calls here with a .exclude making the inverse comparison, since null values will never be true. That is, instead of your original:
unit = EmployeeUnit.objects.filter(employee__exact=emp).filter(effective_date__lte=today).filter(Q(end_date__gte=today) | Q(end_date__isnull=True))[0].unit
you can write:
unit = EmployeeUnit.objects.filter(employee=emp, effective_date__lte=today).exclude(end_date__lt=today)[0].unit
#Shawn, I encountered this same issue today. I noticed when debugging (in Eclipse, with the Variables pane shown/active) the form's __init__() method and line-stepping through the code that I'd get the "Select a valid choice" error. However, if I clear my breakpoints and just let it run, or if I line-step debug with the Variables pane not shown/not active, then I don't get the error. Something with the rendering of the variables in Eclipse results in the error.
Update: Reading directly the django source code i got one undocumented missing piece to solve my problem. Thanks to Brandon that solved half of the problem by giving me one of the missing pieces. See my own answer to see my solution (i dont want to mix things here).
I have the following (simplified) models:
Order(models.Model):
status = models.CharField( max_length=25, choices=STATUS_CHOICES, default='PENDING')
total = models.DecimalField( max_digits=22, decimal_places=2)
def clean(self):
if self.estatus == 'PAID' or self.estatus == 'SENT':
if len(self.payment.all()) > 0:
raise ValidationError("The status cannot be SENT or PAID if there is no payment for the order")
Payment(models.Model):
amount = models.DecimalField( max_digits=22, decimal_places=2 )
order = models.ForeignKey(Order, related_name="payment")
def clean(self):
if self.amount < self.order.total or self.amount <= 0:
ValidationError("The payment cannot be less than the order total")
In my admin.py i have:
class paymentInline(admin.StackedInline):
model = Payment
max_num = 1
class OrderAdmin(admin.ModelAdmin):
model = Order
inlines = [ paymentInline, ]
The validation in the clean method of the Order does not work because there is no payment saved when the validation occurs (obviously it has not been saved to the database).
The validation inside the payment works fine (if editing or adding a new payment).
I want to validate if the order has a payment if the status is 'PAID' or 'SENT', but as i cannot doit the way is in the clean method.
My question is, how can i access the 'payment.amount' value entered by the user in the inline (payment) of the Order form, to accomplish my validation? (considering im in the clean method of the Order model)
After reading the django source code i found one property of the BaseInlineFormSet that contains the Parent Instance of the Inline, in my case, the Order instance being edited.
Brandon gave me another important piece, iterating over the self.forms of the BaseInlineFormSet to get each of the instances (even not saved or not cleaned or empty), in my case, each Payment Instance being edited.
These are the two pieces of information needed to check if the Order with status 'PAID' or 'SENT' has a payment or not. Iterate over the cleaned_data of the formset would not give Order data (i.e. when not changing the Order, just changing the Payment, or when not adding a Payment -and an empty Payment- but changing the Order) which is needed to decide to save the model if the order status is different than 'PAID' or 'SENT', so this method was discarded before.
The models are keep the same, I only modified the admin.py to add the next:
class PaymentInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
order = None
payment = None
if any(self.errors):
return
# django/forms/models.py Line # 672,674 .. class BaseInlineFormSet(BaseModelFormSet) . Using Django 1.3
order = self.instance
#There should be only one form in the paymentInline (by design), so in the iteration below we end with a valid payment data.
if len(self.forms) > 1:
raise forms.ValidationError(u'Only one payment per order allowed.')
for f in self.forms:
payment = f.save(commit=False)
if payment.amount == None:
payment.amount = 0
if order != None:
if order.status in ['PAID', 'SENT'] and payment.amount <= 0:
raise forms.ValidationError(u'The order with %s status must have an associated payment.'%order.status)
class pymentInline(admin.StackedInline):
model = Payment
max_num = 1
formset = PaymentInlineFormset
class OrderAdmin(admin.ModelAdmin):
inlines = [ paymentInline, ]
admin.site.register(Order, OrderAdmin)
admin.site.register(Payment)
It sounds like you just need to validate that there is at least one valid formset in the inlines...you might give this code a try: http://wadofstuff.blogspot.com/2009/08/requiring-at-least-one-inline-formset.html
Hope that gets you going.
[Edit]
I took a look at some code I had written for another app that had custom validation on inlines based on a property on the related model. Of course, you may need to make some adjustments, but give this a try. You'll need to specify an inline to use in your admin model too.
#I would put this in your app's admin.py
class PaymentInline(admin.TabularInline):
model = Payment
formset = PaymentInlineFormset
#I would put this in the app's forms.py
class PaymentInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
order = None
valid_forms = 0
for error in self.errors:
if error:
return
for cleaned_data in self.cleaned_data:
amount = cleaned_data.get('amount', 0)
if order == None:
order = cleaned_data.get('order')
if amount > 0:
valid_forms += 1
if order.status in ['PAID', 'SENT'] and len(valid_forms) > 0:
raise forms.ValidationError(u'Your error message')
I am trying to access data.get_age_display in my email template. I can't seem to get the display of this. I am not sure what I am doing wrong, I've using get_FIELD_display numerous times before but passed as context to a normal template. Is there something different with forms?
class RequestForm(forms.Form):
ADULT = 1
SENIOR = 2
STUDENT = 3
AGE_GROUP = (
(ADULT, 'Adult'),
(SENIOR, 'Senior'),
(STUDENT, 'Student'),
)
name = forms.CharField(max_length=255)
phone = forms.CharField(max_length=15)
age = forms.ChoiceField(choices=AGE_GROUP)
details = forms.CharField(widget=forms.Textarea())
def save(self):
order = Order(
name = self.cleaned_data['name'],
phone = self.cleaned_data['phone'],
age = self.cleaned_data['age'],
details = self.cleaned_data['details'],
)
order.save()
template = loader.get_template('request_email.txt')
# send over the order object in an email extracted so they can handle the ticket order
context = Context({
'data': order,
})
#import pdb; pdb.set_trace()
email_subject = 'Request Tickets'
mail_managers(email_subject, template.render(context))
in my request_email.txt all I am doing is {{ data.get_age_display }} any ideas?
Jeff
You haven't shown the code for the Order model that you're creating. Are you sure that the age field on the model has choices set?
Any reason you're not using a ModelForm? You're creating an Order object within the form's save() method, but not returning it. A modelform would do that for you, as well as removing the need to redeclare the fields for the form.
I know this is coming WAAAAAY later than the question being posted but here's my answer for completeness and anyone else who might benefit from it :-)
I'm going to assume that in AGE_GROUP, ADULT, SENIOR and STUDENT are integers. Your form cleaning will NOT automatically clean the string contained in the POST and return an integer. So in this code:
context = Context({
'data': order,
})
you would think order.age is referring to an integer but that is, in fact, incorrect. It's burned me a few times before because this will correctly save the integer to the physical table, but the order instance still has the string representation of the age field.
You could do one of two things:
1. Clean this in the field:
clean_age(self):
return int(self.cleaned_data['age'])
or create a new field type:
def MyChoiceField(forms.ChoiceField):
def clean(self, value):
if not value:
if self.required:
raise forms.ValidationError(self.error_messages['required'])
return None
else:
return None
return int(value)
link that to the form field:
age = MyChoiceField(choices=AGE_GROUP)
and then you'll be able to apply this logic to any other such choice field in future. Personally, I find the latter approach the best one and I stick all my custom field types into a form_utils file so that I can use them everywhere. Another gotcha is that forms.charField doesn't automatically strip the entered text and you can use this approach to fix that too.