I try to implement validators for Django project
I've defined a file validators.py in myapp
I add validators=[validate_test] for my ran_num field
but got an error
I have another question: is it possible to control field BEFORE form submission?
validators.py
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
def validate_test(self, value):
if value == 'PPP': # just test
raise ValidationError(
_("Test"),
code='Test',
params={},
)
forms.py
from .validators import validate_date
class EditForm(forms.ModelForm):
def __init__(self, request, *args, **kwargs):
super(EditForm, self).__init__(*args, **kwargs)
self.request = request
self.fields["asp_ent_loc"] = forms.ChoiceField(label = _("Site concerned by the operation"), widget=forms.Select, choices=SITE_CONCERNE)
self.fields["med_num"] = forms.CharField(label = _("Trial bacth number"), required=True,validators=[validate_test] )
self.fields["asp_ent_dat"] = forms.DateField(
label = _("Entry date"),
required = True,
initial = datetime.datetime.now(),
)
self.fields["asp_ent_pro_pay"] = forms.ChoiceField(label = _("Country of treatment origin in case of entry"), widget=forms.Select, choices=PAYS)
self.fields["asp_ent_pro_sit"] = forms.ChoiceField(label = _("Processing source site in case of entry"), widget=forms.Select, choices=SITE_PROVENANCE)
def clean(self):
cleaned_data = super(EditForm, self).clean()
class Meta:
model = Entree
fields = ('asp_ent_loc','med_num','asp_ent_dat','asp_ent_pro_pay','asp_ent_pro_sit',)
def clean_med_num(self):
data = self.cleaned_data['med_num']
if len(data) != 3:
raise forms.ValidationError(_("Error on Batch number format (3 letters)"))
if not Medicament.objects.filter(med_num = data).exists():
raise ValidationError(_('Batch number does not exist'))
return data
def clean_asp_ent_dat(self):
data = self.cleaned_data['asp_ent_dat']
entrydate = datetime.datetime.strptime(str(data), "%Y-%m-%d")
currentdate = datetime.datetime.now()
if entrydate > currentdate:
raise forms.ValidationError(_("Please control entry date"))
return data
Your Validation function is not a model's method which means it doesn't accept the self. Your Validation func only accept value argument. In your scenario-
def validate_test(value): # remove self from here
if value == 'PPP':
raise ValidationError(
_("Test"),
code='Test',
params={},
)
Related
I am testing a modelform and getting a ValidationError. My model, view and test are as follows:
model
class Course(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
course_name = models.CharField(max_length=30)
grade_level = models.CharField(max_length=4, default="SEC")
view
# method_decorator([login_required, teacher_required], name='dispatch')
class CourseUpdateView(PermissionRequiredMixin, UpdateView):
raise_exceptions = True
permission_required = 'gradebook.change_course'
permission_denied_message = "You don't have access to this."
model = Course
fields = ['course_name', 'grade_level', ]
template_name_suffix = '_update'
def get_success_url(self, *args, **kwargs):
return reverse('gradebook:coursedetail', kwargs={'course_pk': self.object.pk})
form
class CourseForm(ModelForm):
class Meta:
model = Course
fields = ('course_name', 'grade_level',)
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.qc = Course.objects.filter(user=user)
def clean(self):
super(CourseForm, self).clean()
course_name = self.cleaned_data.get('course_name')
if course_name and self.qc.filter(course_name__iexact=course_name).exists():
raise ValidationError("A course with that name already exists.")
if len(course_name) > 20:
if len(course_name) > 10:
raise ValidationError(
"Your course name cannot be longer than 20 characters")
return self.cleaned_data
Test
class CourseUpdateTests(TestCase):
#classmethod
def setUpTestData(cls):
cls.user = CustomUser.objects.create_user(
username='tester',
email='tester#email.com',
password='tester123',
is_teacher=True,
is_active=True,
)
cls.user.save()
def test_CourseUpdate_valid(self):
request = HttpRequest()
request.POST = {
'user': self.user,
'id': '4d192045-07fa-477f-bac2-5a99fe2e7d46',
'course_name': "Science",
'grade_level': "SEC"
}
form = CourseForm(request.POST)
self.assertTrue(form.is_valid())
The error I get:
Raise exceptions.ValidationError(
django.core.exceptions.ValidationError: ["“{'user': <CustomUser: tester>, 'id': '4d192045-07fa-477f-bac2-5a99fe2e7d46', 'course_name': 'Science', 'grade_level': 'SEC'}” is not a valid UUID."]
I have tried not putting the id in the request.POST but get the same error.
I originally tried to test for a valid form by using:
def test_CourseUpdate_valid(self):
form = CourseForm(data={
'user': self.user,
'id': '4d192045-07fa-477f-bac2-5a99fe2e7c04',
'course_name': "Science",
'grade_level': "SEC"
},)
self.assertTrue(form.is_valid())
This did not work though, giving me the error TypeError: __init__() missing 1 required positional argument: 'user'
Your original solution was not good because you were missing the user positional argument in the form init function.
Secondly, your CourseForm class should specify the rest of the fields (id, and user) if you want to pass them to the form.
You could probably just not pass id and user to the CourseForm data in the test as they aren't relevant.
This should work:
def test_CourseUpdate_valid(self):
form = CourseForm(self.user, data={
'course_name': "Science",
'grade_level': "SEC"
},)
self.assertTrue(form.is_valid())
Can you try that and let me know if the problem persists?
Just quickly to summarize I have an app, which allows clubs to sign up and carry out different tasks. One of the features is a scheduling / rosters. Currently I have a form to add times to the roster. The club pages work off a session key based on the initially selected club.
My form consists of:
club_id - which I have hidden and initialized based on the logged in user.
pitch_id - which is currently displaying all pitches associated to all clubs but I need this to only show pitches based on the foreign key for the club_id
Would appreciate any help.
form.py
from django import forms
from clubkit.roster.models import RosterId
import datetime
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
class RosterForm(forms.ModelForm):
class Meta():
model = RosterId
fields = ('club_id', 'pitch_id', 'team_id', 'date',
'start_time', 'finish_time', 'reoccuring_event',)
widgets = {
'date': forms.DateInput(attrs={'id': 'datepicker'})
}
def clean_date(self):
date = self.clean_date['date']
if date < datetime.date.today():
raise ValidationError(_('Date cannot be in the past.'))
return date
def __init__(self, *args, **kwargs):
super(RosterForm, self).__init__(*args, **kwargs)
self.fields['club_id'].widget = forms.HiddenInput()
models.py for pitch
class Pitch(models.Model):
club_id = models.ForeignKey(ClubInfo, on_delete=models.CASCADE, related_name="pitches")
pitch_name = models.CharField(max_length=30)
PITCH_SIZES = (
('S', 'Small'),
('M', 'Medium'),
('L', 'Large'),
)
PITCH_TYPE = (
('1', 'Outdoor'),
('2', 'Indoor'),
)
pitch_size = models.CharField(max_length=1, choices=PITCH_SIZES)
pitch_type = models.CharField(max_length=1, choices=PITCH_TYPE)
open_time = models.TimeField(default='09:00')
close_time = models.TimeField(default='22:00')
RENT_TYPE = (
('0', 'Not Available To Rent'),
('1', 'Available To Rent'),
)
rental = models.CharField(max_length=1, choices=RENT_TYPE)
rental_price = models.DecimalField(default=0.00, max_digits=6, decimal_places=2)
max_people = models.IntegerField(null=True)
def __str__(self):
return self.pitch_name
models.py for roster
from django.db import models
from clubkit.clubs.models import ClubInfo, Pitch, Team
# Model to store roster information
class RosterId(models.Model):
club_id = models.ForeignKey(ClubInfo, on_delete=models.CASCADE)
pitch_id = models.ForeignKey(Pitch, on_delete=models.CASCADE)
team_id = models.ForeignKey(Team, on_delete=models.CASCADE)
date = models.DateField(max_length=8)
start_time = models.TimeField(default='')
finish_time = models.TimeField(default='')
reoccuring_event = models.BooleanField(default=False)
views.py
class ClubRoster(APIView):
renderer_classes = [TemplateHTMLRenderer]
template_name = 'roster.html'
# Get method to retrieve current roster information and form
def get(self, request):
if request.user.is_authenticated:
club_pk = request.session.get('pk')
club_info = ClubInfo.objects.filter(user=request.user).first()
reoccuring_event = RosterId.objects.filter(reoccuring_event=True, club_id=club_pk)
inital_data = {
'club_id': club_info,
}
form = RosterForm(initial=inital_data)
roster = RosterId.objects.filter(club_id=club_pk)
return Response({'form': form,
'roster': roster,
'club_pk': club_pk,
'reoccuring_event': reoccuring_event
})
Sounds like you're looking for some manipulation of the form field query. Maybe this answer will provide further help.
The result could look like this.
from django import forms
from clubkit.roster.models import RosterId, Pitch
import datetime
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
class RosterForm(forms.ModelForm):
class Meta():
model = RosterId
fields = ('club_id', 'pitch_id', 'team_id', 'date',
'start_time', 'finish_time', 'reoccuring_event',)
widgets = {
'date': forms.DateInput(attrs={'id': 'datepicker'})
}
def clean_date(self):
date = self.clean_date['date']
if date < datetime.date.today():
raise ValidationError(_('Date cannot be in the past.'))
return date
def __init__(self, *args, **kwargs):
super(RosterForm, self).__init__(*args, **kwargs)
self.fields['club_id'].widget = forms.HiddenInput()
instance = kwargs.get('instance', None)
if instance:
self.fields['pitch_id'].queryset = Roster.objects.filter(club_id=instance.club_id)
Ah well or adjust instance to your 'initial'.
I have encountered a weird behaviour where one of the admin views which is quite simple and straight forward takes a while to load up until an nginx timeout in prod.
Through Django debug toolbar and new relic I have found that the guilty code is the field_sets block in Django templates, which makes it way harder to debug.
On production, when the view does load, it is empty.
This is the first time I encounter something like that, and thus I decided to reach out to the community for help.
The model:
from datetime import timedelta
from decimal import Decimal
from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.translation import ugettext as _
from core.common.models import (SoftDeletableModel, TimeStampedModel,
UniqueFieldMixin, FlagableMixin)
from core.models import Pair
from payments.utils import money_format
from ticker.models import Price
from django.core.exceptions import ValidationError
class Order(TimeStampedModel, SoftDeletableModel,
UniqueFieldMixin, FlagableMixin):
USD = "USD"
RUB = "RUB"
EUR = "EUR"
BTC = "BTC"
EXCHANGE = 2
BUY = 1
SELL = 0
TYPES = (
(SELL, 'SELL'),
(BUY, 'BUY'),
(EXCHANGE, 'EXCHANGE'),
)
_order_type_help = '{} - {}<br/>{} - {}<br/>{} - {}<br/>'.format(
'BUY', 'Customer is giving fiat, and getting crypto money.',
'SELL', 'Customer is giving crypto and getting fiat money',
'EXCHANGE', 'Customer is exchanging different kinds of crypto '
'currencies'
)
PAID_UNCONFIRMED = -1
CANCELED = 0
INITIAL = 1
PAID = 2
RELEASED = 3
COMPLETED = 4
STATUS_TYPES = (
(PAID_UNCONFIRMED, 'UNCONFIRMED PAYMENT'),
(CANCELED, 'CANCELED'),
(INITIAL, 'INITIAL'),
(PAID, 'PAID'),
(RELEASED, 'RELEASED'),
(COMPLETED, 'COMPLETED'),
)
IN_PAID = [PAID, RELEASED, COMPLETED]
IN_RELEASED = [RELEASED, COMPLETED]
_could_be_paid_msg = 'Could be paid by crypto transaction or fiat ' \
'payment, depending on order_type.'
_order_status_help =\
'{} - {}<br/>{} - {}<br/>{} - {}<br/>' \
'{} - {}<br/>{} - {}<br/>{} - {}<br/>'\
.format(
'INITIAL', 'Initial status of the order.',
'PAID', 'Order is Paid by customer. ' + _could_be_paid_msg,
'PAID_UNCONFIRMED', 'Order is possibly paid (unconfirmed crypto '
'transaction or fiat payment is to small to '
'cover the order.)',
'RELEASED', 'Order is paid by service provider. ' + _could_be_paid_msg,
'COMPLETED', 'All statuses of the order is completed',
'CANCELED', 'Order is canceled.'
)
# Todo: inherit from BTC base?, move lengths to settings?
order_type = models.IntegerField(
choices=TYPES, default=BUY,
# help_text=_order_type_help
)
status = models.IntegerField(choices=STATUS_TYPES, default=INITIAL,
# help_text=_order_status_help
)
amount_base = models.DecimalField(max_digits=18, decimal_places=8)
amount_quote = models.DecimalField(max_digits=18, decimal_places=8)
payment_window = models.IntegerField(default=settings.PAYMENT_WINDOW)
user = models.ForeignKey(User, related_name='orders')
unique_reference = models.CharField(
max_length=settings.UNIQUE_REFERENCE_MAX_LENGTH)
admin_comment = models.CharField(max_length=200)
payment_preference = models.ForeignKey('payments.PaymentPreference',
default=None,
null=True)
withdraw_address = models.ForeignKey('core.Address',
null=True,
related_name='order_set',
default=None)
is_default_rule = models.BooleanField(default=False)
from_default_rule = models.BooleanField(default=False)
pair = models.ForeignKey(Pair)
price = models.ForeignKey(Price, null=True, blank=True)
user_marked_as_paid = models.BooleanField(default=False)
system_marked_as_paid = models.BooleanField(default=False)
class Meta:
ordering = ['-created_on']
# unique_together = ['deleted', 'unique_reference']
def validate_unique(self, exclude=None):
# TODO: exclude expired?
if not self.deleted and \
Order.objects.exclude(pk=self.pk).filter(
unique_reference=self.unique_reference,
deleted=False).exists():
raise ValidationError(
'Un-deleted order with same reference exists')
super(Order, self).validate_unique(exclude=exclude)
def _types_range_constraint(self, field, types):
""" This is used for validating IntegerField's with choices.
Assures that value is in range of choices.
"""
if field > max([i[0] for i in types]):
raise ValidationError(_('Invalid order type choice'))
elif field < min([i[0] for i in types]):
raise ValidationError(_('Invalid order type choice'))
def _validate_fields(self):
self._types_range_constraint(self.order_type, self.TYPES)
self._types_range_constraint(self.status, self.STATUS_TYPES)
def clean(self, *args, **kwargs):
self._validate_fields()
super(Order, self).clean(*args, **kwargs)
def save(self, *args, **kwargs):
self._validate_fields()
if not self.unique_reference:
self.unique_reference = \
self.gen_unique_value(
lambda x: get_random_string(x),
lambda x: Order.objects.filter(unique_reference=x).count(),
settings.UNIQUE_REFERENCE_LENGTH
)
if self.status == self.INITIAL:
self.convert_coin_to_cash()
super(Order, self).save(*args, **kwargs)
def _not_supported_exchange_msg(self):
msg = _('Sorry, we cannot convert {} to {}'.format(
self.currency_from.code, self.currency_to.code
))
return msg
def convert_coin_to_cash(self):
self.amount_base = Decimal(self.amount_base)
price = Price.objects.filter(pair=self.pair).last()
self.price = price
# For annotations
amount_quote = None
if self.order_type == Order.BUY:
amount_quote = self.amount_base * price.ticker.ask
elif self.order_type == Order.SELL:
amount_quote = self.amount_base * price.ticker.bid
self.amount_quote = money_format(amount_quote)
#property
def is_buy(self):
return self.order_type == Order.BUY
#property
def payment_deadline(self):
"""returns datetime of payment_deadline (creation + payment_window)"""
# TODO: Use this for pay until message on 'order success' screen
return self.created_on + timedelta(minutes=self.payment_window)
#property
def expired(self):
"""Is expired if payment_deadline is exceeded and it's not paid yet"""
# TODO: validate this business rule
# TODO: Refactor, it is unreasonable to have different standards of
# time in the DB
return (timezone.now() > self.payment_deadline) and (
self.status not in Order.IN_PAID)
#property
def payment_status_frozen(self):
"""return a boolean indicating if order can be updated
Order is frozen if it is expired or has been paid
"""
# TODO: validate this business rule
return self.expired or self.status in Order.IN_RELEASED
#property
def withdrawal_address_frozen(self):
"""return bool whether the withdraw address can
be changed"""
return self.status in Order.IN_RELEASED
def __str__(self):
return "{} {} pair:{} base:{} quote:{} status:{}".format(
self.user.username or self.user.profile.phone,
self.get_order_type_display(),
self.pair.name,
self.amount_base,
self.amount_quote,
self.get_status_display()
)
If your model has a ForeignKey or ManyToManyField to a model with many items, then it can it can be very slow to render select box for that field.
You can improve performance by adding these field(s) to raw_id_fields.
In your case, since the price list is very long, you could do:
class OrderAdmin(admin.ModelAdmin):
raw_id_fields = ['price']
...
I'm trying to run a validation where a user can't enter the same name_field twice but other users entering the same name will not interfere.
I tried using "unique_together = (("username","name_field"))" but when a user enters the same value twice the server generates an integrity error as opposed to rendering a warning message next to the form field.
then I tried overriding the clean() method in my model, Which runs fine if I only check "field_name" like so:
def clean(self):
existing = self.__class__.objects.filter(
name_field=self.name_field).count()
if existing > 0:
raise ValidationError({'name_field':self.username })
But I am running into trouble when checking the username value, for instance:
def clean(self):
existing = self.__class__.objects.filter(
username=self.username, ###This part crashes!!! (username not found)
name_field=self.name_field).count()
if existing > 0:
raise ValidationError({'name_field':self.username })
I'm guessing due to it not being an actual field in the form its not present during the call to clean(). So my question is am I doing the validation correctly for this kind of problem? And how can I pass or where can I find the value for the current user from within a models clean method (in a safe way hopefully without adding fields to my form)?
views.py
def add_stuff(request):
if request.user.is_authenticated():
form = addStuff(request.POST or None)
if request.method == 'POST':
if form.is_valid():
sub_form = form.save(commit=False)
sub_form.username = request.user
sub_form.save()
return redirect('../somewhere_else/')
forms.py
class addStuff(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(addStuff, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
'name_field',
'type_field',
ButtonHolder(
Submit('Save', 'Save')
),
)
class Meta:
model = UserStuff
fields = ('name_field',
'type_field',
)
models.py
class UserStuff(models.Model):
username = models.ForeignKey(User)
name_field = models.CharField(max_length=24, blank=False,null=False)
type_field = models.CharField(max_length=24, blank=True,null=True)
def clean(self):
existing = self.__class__.objects.filter(
username=self.username, ###This part crashes!!! (username not found)
name_field=self.name_field).count()
if existing > 0:
raise ValidationError({'name_field':self.username })
def __unicode__(self):
return "%s For User: \"%s\" " % (self.name_field, self.username)
class Meta:
managed = True
db_table = 'my_db_table'
unique_together = (("username","name_field"))
Thanks for any insight!
I now am running the clean override from the form instead of the model (as recommended by Daniel). This has solved a bunch of issues and I now have a working concept:
models.py
class UserStuff(models.Model):
username = models.ForeignKey(User)
name_field = models.CharField(max_length=24, blank=False,null=False)
type_field = models.CharField(max_length=24, blank=True,null=True)
def clean(self):
existing = self.__class__.objects.filter(
username=self.username, ###This part crashes!!! (username not found)
name_field=self.name_field).count()
if existing > 0:
raise ValidationError({'name_field':self.username })
def __unicode__(self):
return "%s For User: \"%s\" " % (self.name_field, self.username)
class Meta:
managed = True
db_table = 'my_db_table'
forms.py
class addStuff(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(addStuff, self).__init__(*args, **kwargs)
initial = kwargs.pop('initial')
self.username = initial['user']
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
'name_field',
'type_field',
ButtonHolder(
Submit('Save', 'Save')
),
)
def clean(self):
cleaned_data = super(addStuff, self).clean()
name_field = self.cleaned_data['name_field']
obj = UserStuff.objects.filter(username_id=self.username.id,
name_field=name_field,
)
if len(obj) > 0:
raise ValidationError({'name_field':
"This name already exists!" } )
return cleaned_data
class Meta:
model = UserStuff
fields = ('name_field',
'type_field',
)
views.py
def add_stuff(request):
if request.user.is_authenticated():
form = addStuff(request.POST or None,
initial={'user':request.user})
if request.method == 'POST':
if form.is_valid():
sub_form = form.save(commit=False)
sub_form.username = request.user
sub_form.save()
return redirect('../somewhere_else/')
best of luck!
Do you have a hint why captcha is not showing?
from captcha.fields import CaptchaField
class ResponseForm(models.ModelForm):
captcha = CaptchaField() # added this according to documentation
class Meta:
model = Response
fields = ['comments']
def __init__(self, *args, **kwargs):
[...]
def save(self, commit=True):
[...]
It works here:
from captcha.fields import CaptchaField
class UserCreationForm(UserCreationFormBase):
email = forms.EmailField(
label='Email',
error_messages={'invalid': 'Check email address!'})
captcha = CaptchaField() # works fine here
class Meta:
model = User
fields = ('username', 'email', 'password1', 'password2')
def get_credentials(self):
[...]
Is it because of functions def __init__ and/or def save?
The behavior is that no captcha field is shown and that the form does nothing on hitting submit. Without captcha in the code it's working fine.
Update
No error message. It seems page loads again.
Console output: POST /umfragen/testumfrage-2/ HTTP/1.1 200 2661
Full def __init__:
# expects a survey object passed in initially
survey = kwargs.pop('survey')
self.survey = survey
super(ResponseForm, self).__init__(*args, **kwargs)
self.uuid = random_uuid = uuid.uuid4().hex
# adds a field for each survey question,
# corresponding to the question type
data = kwargs.get('data')
for q in survey.questions():
if q.question_type == Question.TEXT:
self.fields['question_%d' % q.pk] = forms.CharField(
label=q.question_text,
widget=forms.Textarea)
elif q.question_type == Question.RADIO:
# calls function get_choices from models.py
question_choices = q.get_choices()
self.fields['question_%d' % q.pk] = forms.ChoiceField(
label=q.question_text,
widget=forms.RadioSelect,
choices=question_choices)
elif q.question_type == Question.SELECT:
question_choices = q.get_choices()
question_choices = tuple([('', '- - - - -')]) + question_choices
self.fields['question_%d' % q.pk] = forms.ChoiceField(
label=q.question_text,
widget=forms.Select,
choices=question_choices)
elif q.question_type == Question.SELECT_MULTIPLE:
question_choices = q.get_choices()
self.fields['question_%d' % q.pk] = forms.MultipleChoiceField(
label=q.question_text,
widget=forms.CheckboxSelectMultiple,
choices=question_choices)
# adds category as a data attribute and allows sorting the questions by category
if q.category:
classes = self.fields['question_%d' % q.pk].widget.attrs.get('class')
if classes:
self.fields['question_%d' % q.pk].widget.attrs['class'] = classes + ('cat_%s' % q.category.name)
else:
self.fields['question_%d' % q.pk].widget.attrs['class'] = ('cat_%s' % q.category.name)
self.fields['question_%d' % q.pk].widget.attrs['category'] = q.category.name
# initializes the form field with values from a POST request
if data:
self.fields['question_%d' % q.pk].initial = data.get('question_%d' % q.pk)