Django request not available in form clean - python

I can't access request from the clean field of a ModelForm. I want to check that the request user is not trying to attend too many events.
I've tried this fix: Django: Accessing request in forms.py clean function
...among other things but am still struggling.
My form is this:
class EventEditForm(forms.ModelForm):
class Meta:
model = Event
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(EventEditForm, self).__init__(*args, **kwargs)
name = forms.CharField(max_length=1024,
initial="Give short, descriptive name",
widget=forms.TextInput(attrs={'onfocus':'if(this.value=="Give short, descriptive name") this.value="";'}))
location = forms.CharField(max_length=1024,
initial="Be specific, give online map link",
widget=forms.TextInput(attrs={'onfocus':'if(this.value=="Be specific, give online map link") this.value="";'}))
dateTimeOptions = {
'format': 'dd/mm/yyyy HH:ii P',
'autoclose': 'true',
'showMeridian': 'true',
}
date = forms.DateTimeField(label="Date and time",widget=DateTimeWidget(options=dateTimeOptions,
attrs={'id':"date-time"}))
host_act = forms.CharField(max_length=1024,
initial="What act will you bring, if any?", required=False,
widget=forms.TextInput(attrs={'onfocus':'if(this.value=="What act will you bring, if any?") this.value="";'}))
description = forms.CharField(required=False, max_length=10240,
initial="Give some details. Any activities prepared?",
widget=forms.Textarea(attrs={'onfocus':'if(this.value=="Give some details. Any activities prepared?") this.value="";'}))
def clean_location(self):
cd = self.cleaned_data
location = cd.get('location')
if location == "Be specific, give online map link" or '':
raise forms.ValidationError("Please enter a location")
return location
def clean_name(self):
cd = self.cleaned_data
name = cd.get('name')
other_names = Event.objects.proposed(datetime.now)
if name == "Give short, descriptive name" or '':
raise forms.ValidationError("Please enter a name")
return name
def clean(self):
"""
Check that there is not another event on at
the same time and place. Then check that user has not committed
to another event on the same date, even somewhere else.
"""
cleaned_data = super(EventEditForm, self).clean()
event_start_estimate = cleaned_data.get("date") - timedelta(hours=3)
event_end_estimate = event_start_estimate + timedelta(hours=7)
location = cleaned_data.get("location")
events_on_date = Event.objects.\
filter(date__range=[event_start_estimate,event_end_estimate])
events_at_location_on_date = events_on_date.filter(location=location)
print events_at_location_on_date
# Check event clash is not this event clashing with itself
try:
events_with_same_date_and_location_id = events_at_location_on_date[0].id
except:
pass
this_event_id = self.instance.id
try:
if events_with_same_date_and_location_id == this_event_id:
events_at_location_on_date = False
except:
pass
if events_at_location_on_date:
raise forms.ValidationError("There is already an event on \
this date. Please join this event or pick another \
date and time. Note: You may have accidentally failed to \
change the date with the picker.")
print self.request
user = self.request
print user
events_on_date_user_has_commit = events_on_date.filter(host__exact=user)
if events_on_date_user_has_commit:
raise forms.ValidationError("You are already committed to an event\
on this date.")
return cleaned_data
My view is:
class EventEditView(UpdateView):
template_name = "edit_event.html"
model=Event
pk_url_kwarg='event_id'
form_class = EventEditForm

You cannot access the request in a form. You need to pass it in from the view in some way. If you're using a FormView generic view, the answer you have linked to is a great way to do it. Looks like you are already expecting the request to be sent in:
self.request = kwargs.pop('request', None)
If you are using a function based view, or a non FormView classed based view, just pass in the request as a keyword argument when you initialize the form. For example:
form = EventEditForm(request.POST, request=request) # or self.request for a class based view

Related

Modifying FlaskForms class - object has no attribute '_fields'

I am trying to create a FlaskForm that changes depending on the customer passed to it and having issues when the HTML is getting rendered. I haven't done much work with classes unfortunately so I'm not sure what the issue is. I get an error in Python that says:
AttributeError: 'GlobalsForm' object has no attribute '_fields'
I modified the __init__ so that you can set who the customer is plus a created a class function that checks to see who the customer is and customizes the form fields depending on who the customer is. I have a feeling it doesn't like me modifying the class I am inheriting and am probably missing something to make it work. Need to do something with "super"?
Here is what I have so far:
routes.py
#app.route('/global_vars', methods=['GET', 'POST'])
def global_vars():
data = session.get('dev_data', None)
form = GlobalsForm(customer=data['customer'])
form.set_fields()
return render_template('global_vars.html', title='Config Provisioning - Globals', form=form)
forms.py
class GlobalsForm(FlaskForm):
def __init__(self, customer):
self.customer = customer
def set_fields(self):
if self.customer == 'Cust1':
site_choices = [('Site1', 'Site1'), ('Site2', 'Site2'), ('Site3', 'Site3')]
site = SelectField('Site: ', choices = site_choices)
floor_room = StringField("Floor/Room (Ex. FL1RM56 or 1J46): ", validators=[InputRequired()])
rack = StringField("Rack (Data Centers only): ")
l2_domain_choices = [('USR', 'USR - User'), ('DC', 'DC - Data Center'), ('OOBM', 'OOBM - Out of Band Management')]
l2_domain = SelectField('Layer 2 Domain: ', choices=l2_domain_choices)
function_choices = [('ACSW', 'ACSW - Access Switch)'), ('AGSW', 'AGSW - Aggregation Switch'), ('CRSW', 'CRSW - Core Switch')]
function = SelectField('Device function: ', choices=function_choices)
else:
hostname = Stringfield("Hostname: ", validators=[InputRequired()])
I figured it out.. Had to expand on the super() line...
class GlobalsForm(FlaskForm):
def __init__(self, customer=None, *args, **kwargs):
self.customer = customer
super(GlobalsForm, self).__init__(*args, **kwargs)

Test form with dynamic choice fields

I created a form with a TypedChoiceField:
class EditProjectForm(ModelForm):
def __init__(self, action, *args, **kwargs):
super(EditProjectForm, self).__init__(*args, **kwargs)
now = datetime.datetime.now()
if action == 'edit':
project_year = kwargs['project_year']
self.fields['year'].choices = [(project_year, project_year)]
else:
self.fields['year'].choices = [(now.year, now.year), (now.year + 1, now.year + 1)]
year = forms.TypedChoiceField(coerce=int)
...
This works perfectly fine when using it inside a view. Now I want to write tests for this form:
form_params = {
'project_year': datetime.datetime.now().year,
}
form = EditProjectForm('new', form_params)
self.assertTrue(form.is_valid())
The test fails, because is_valid() returns False. This is because when calling super.__init__() in the EditProjectForm, the field year doesn't have its choices yet. So the validation for this field fails and an error is added to the error list inside the form.
Moving the super call after self.fields['year'].choices doesn't work either, because self.fields is only available after the super.__init__() call.
How can I add the choices dynamically and still be able to test this?
Okay, I found the problem.
The field year is a class variable, and is instantiated even before the tests setUp method and the forms __init__ method was called. Since I haven't passed the required choices parameter for this field, the error was issued way before the form object was created.
I changed the behaviour so I change the type of the fields in the __init__ method rather than using a class variable for that.
class EditProjectForm(ModelForm):
def __init__(self, action, *args, **kwargs):
super(EditProjectForm, self).__init__(*args, **kwargs)
now = datetime.datetime.now()
if action == 'edit':
project_year = kwargs['project_year']
choices = [(project_year, project_year)]
else:
choices = [(now.year, now.year), (now.year + 1, now.year + 1)]
self.fields['year'] = forms.TypedChoiceField(coerce=int, choices=choices)

django form custom clean gives keyerror

I am using a custom clean on a form I made. But then some weird things happen
If my input data is valid. it works fine. However when it is not correct, I get a KeyError at key_ID in the clean method.
Also, if I add print statements in the clean_key_ID and clean_verification_code, these don't show up in my console.
My form:
class ApiForm(forms.Form):
key_ID = forms.CharField(min_length=7, max_length=7, required=True)
verification_code = forms.CharField(
min_length=64,
max_length=64,
required=True,
)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
super(ApiForm, self).__init__(*args, **kwargs)
def clean_key_ID(self):
data = self.cleaned_data['key_ID']
try:
int(data)
except ValueError:
raise forms.ValidationError("Should contain 7 numbers")
return data
def clean_verification_code(self):
data = self.cleaned_data['verification_code']
if not re.match("^[A-Za-z0-9]*$", data):
raise forms.ValidationError(
"Should only contain 64 letters and numbers"
)
return data
def clean(self):
key = self.cleaned_data['key_ID']
vcode = self.cleaned_data['verification_code']
if Api.objects.filter(key=key, vcode=vcode, user=self.user).exists():
raise forms.ValidationError(
"This key has already been entered, try to update it"
)
#connect with api and validate key
api = api_connect()
auth = api.auth(keyID=key, vCode=vcode)
try:
keyinfo = auth.account.APIKeyInfo()
except RuntimeError:
raise forms.ValidationError("Invallid data, cannot connect to api")
in my view I get my form in the normal way:
view:
api_form = ApiForm(request.POST or None, user=request.user)
if request.POST and api_form.is_valid():
#do things with form
I am lost and have no clue where this error is coming from. My form is rendered in the normal way {{api_form}}.
I have already tried removing the underscores in my field names. But the error remains
If key_ID is invalid, then it will not be in self.cleaned_data in your clean method. You have to change your clean method to handle this, for example:
def clean(self):
key = self.cleaned_data.get('key_ID')
if not key:
# early return
return
# rest of method as before
...

Deform Inter-Field Validation not highlighting field

I followed this example, but I modified it a bit to suit my project
This is what I have:
class AgentFormValidation(object):
def __init__(self, context, request):
self.context = context
self.request = request
def __call__(self, form, value):
number = value['identity_information']['number']
print validateID(number)
type = value['identity_information']['type']
q = sqlahelper.get_session().query(Agents.id_number).filter(Agents.id_number == number).first()
if type == "IDNumber":
if not validateID(number):
if q:
exc = colander.Invalid(form["identity_information"], "ID Number %s already exists in Database" % number)
exc.number = "ID Number already exists "
raise exc
else:
exc = colander.Invalid(form["identity_information"], "ID Number %s is not a valid SA ID Number" % number)
exc.number = "Invalid ID number"
raise exc
elif type == "Passport":
if q:
exc = colander.Invalid(form["identity_information"], "Passport number %s already exists in Database" % number)
exc.number = "Passport number already exists"
raise exc
def gen_agent_schema_form(self):
_but = ('create agent',)
_title = "Create Agent"
if not self.context.__is_new__:
_but = ('update agent',)
_title = "Agent Details"
deals = []
if self.context.ou:
deals = [(deal.id, str(deal)) for deal in self.context.ou[0].org_deals]
schema = Agent(validator=AgentFormValidation(self.context, self.request), title=_title).bind(deals=deals)
form = Form(schema, buttons=_but)
return schema, form
The validation works just fine. It just doesn't want to highlight the element.
When I replace:
exc.number = "ID Number already exists"
with
exc['number'] = "ID Number already exists"
It does highlight, but it Highlights the very first element on the form, which is first_name, which is also wrong.
I feel like I'm missing something small.
UPDATE
So I played around a little, when I do:
exc = colander.Invalid(form, "ID Number %s already exists in Database" % number)
exc["identity_information"] = "ID Number already exists "
raise exc
I get an alert message box(not js alert) above the correct field:
Instead of this, I need the field to highlight as in the example above.
In your custom validator you always pass the form as the first parameter to colander.Invalid(). This way you add validation messages to the top of the form, but you do not trigger highlighting of schema nodes / elements. Start using simple validators working on a single schema node/element.
exc = colander.Invalid(form["identity_information"], "ID Number %s already exists in Database" % number)
Anyway, I don't see a clear requirement for inter-field validation. Instead of applying a complex class-based validator on the Agent Schema you could use available colander validators or create custom validators on each Agent Schema node, thus keeping validators as simple as possible. Your current validator implementation cares about too much for too many use cases.
I assumed you have two different use cases - registration and some another agent-related task. By inventing different schemas I can keep validation specific to schema nodes and use case. Uniqueness of ID may only be important while creating stuff with an add/create form, in an update form users may not able to change all these values but a limited set of personal information.
Main idea is usally apply schema node validators to get validation messages at schema nodes. In special cases apply form level validators (agent registration).
Your use case illustrates the domain of form validation. A class that validates form inputs against schemas and handles persistence-related topics (primary key uniqueness) is doing too much. Both should be encapsulated to evolve separately. Your business rules will change in time, the database table will always require uniqueness for primary keys.
Schemas
import colander
class AgentRegistration(colander.Schema):
"""schema for agent registration with email validation
Note the form validator is invoked only if none of the individual field validators raise an error.
"""
first_name = colander.SchemaNode(colander.String())
number = colander.SchemaNode(colander.Integer(), validator=can_register_agent)
email = colander.SchemaNode(colander.String(), validator=colander.Email())
verify_email = colander.SchemaNode(colander.String(), validator=colander.Email())
validator = verify_email_validator
class AgentDeals(colander.Schema):
"schema for managing agent deals"
first_name = colander.SchemaNode(colander.String())
number = colander.SchemaNode(colander.Integer(), validator=validateID)
email = colander.SchemaNode(colander.String(), validator=colander.Email())
Schema Validators
def agent_unique(node, value):
"validate uniqueness of value in database table Agents"
if sqlahelper.get_session().query(Agents.id_number).filter(Agents.id_number == value).first()
raise Invalid(node,
'ID Number %r is already given to another agent. Please change it' % value)
def valid_SA_ID(node, value):
"validates SA ID Number - just a copy of your requirement calling your custom function"
if not validateID(value):
raise Invalid(node,
'SA ID Number %r is not valid.' % value)
def can_register_agent(node, value):
"ensure Agent ID Number is a valid and not already existing in database"
valid_SA_ID(node, value)
agent_unique(node, value)
def verify_email_validator(form, values):
"""schema level validator with access to all values
validates emails are same
validation messages are displayed on top of form"""
if values['email'] != values['verify_email']:
raise colander.Invalid(form, 'Email values do not match.')
Form
class AgentRegistrationView(object)
def __init__(self, request):
"""Set some common variables needed for each view.
"""
self.request = request
self.context = request.context
#view_config(route_name='add_agent', permission='admin', renderer='add_agent.mako')
def add_agent(self):
"""return form to create new agents
may be we do not need to bind any deals data here"""
schema = AgentRegistration()
form = deform.Form(schema, action=self.request.route_url('add_agent'), buttons=('Add Agent','Cancel'))
class AgentDealsView(object)
def __init__(self, request):
"""Set some common variables needed for each view.
"""
self.request = request
self.context = request.context
def get_deals(self)
"""return deals to be bound to Agent Schema"""
if self.context.ou:
return [(deal.id, str(deal)) for deal in self.context.ou[0].org_deals]
#view_config(route_name='edit_agent' permission='edit', renderer='edit_agent.mako')
def edit_agent(self):
# we bind deals data to form
schema = AgentDeals().bind(deals=self.get_deals())
form = deform.Form(schema, action=self.request.route_url('edit_agent'), buttons=('Save','Cancel'))
References
Extending colander - Defining a new validator
Colander Schema binding - An example
Deform/colander with access to all nodes
Colander's Schema validation or Deform's form validation?
Besides that you may be interested in ColanderAlchemy, but it adds another level of abstraction. Actually, I do not recommend it to you.

Series of Questions without database with Django/Twilio

I want to create a series of questions for anyone to answer. I could create a User for everyone who texts in and have a field for "completed_questions" but I was wondering if this could be done without a database. For example:
def HelloQuiz(request):
body = request.REQUEST.get('Body', None)
if body == 'Quiz me up':
message = "2 + 3?"
if body == '6':
message = "Congrats! Next question: what color is green?"
if body == 'Green':
message == "You beat the quiz!"
resp = twilio.twiml.Response()
resp.sms(message)
return HttpResponse(str(resp))
But here, if they text green as their first response, they automatically beat the quiz. I can't really think of any way to achieve this because it requires a look into a past completed action.
Thanks!
Using sqlite3 is the way to go. It's way better than writing to a text file.
What I have in mind is you can use the standard User model to create user, maybe use the phone number as username, something like:
from_number = request.REQUEST.get('From', None)
user = User.objects.get_or_create(username=from_number, ...)
# ... your code ...
Then you only need to define one model for the quiz and probably a relationship model:
class Quiz(models.Model):
message = models.CharField(max_length=100)
order = models.IntegerField()
response = models.CharField(max_length=100)
# ... fields you need ...
def __unicode__():
return self.message
class QuizUser(models.Model):
quiz = models.ForeignKey('Quiz')
user = models.ForeignKey('User')
So whenever there is a message coming in you can use some simple queryset to lookup for the right user and quiz like:
from_number = request.REQUEST.get('From', None)
body = request.REQUEST.get('Body', None)
user = User.objects.get(username=from_number)
quiz = Quiz.objects.get(message=body)
user_quiz = QuizUser.objects.get(user=user, quiz=quiz)
# ... your code ...
This is just my basic opinion.
You can of course go for you original plan, use custom user model and add completed_quiz field and have the list of quiz as a static list.
Have fun coding!

Categories