wtforms field created through setattr() loses properties - python

I am trying to create a form in python / Flask that will add some dynamic slider inputs to a set of standard fields. I am struggling to get it to work properly, though.
Most of the web forms in my app are static, created through wtforms as in:
class CritiqueForm(Form):
rating = IntegerField('Rating')
comment = TextAreaField('Comments')
submit = SubmitField('Save Critique')
When I am explicit like that, I can get the expected results by using the CritiqueForm() in the view and passing the form object to render in the template.
However, I have a critique form that needs to dynamically include some sliders for rating criteria specific to a particular record. The number of sliders can vary form one record to the next, as will the text and IDs that come from the record's associated criteria.
When I looked for some ways to handle this, I found a possible solution from dezza (Dynamic forms from variable length elements: wtforms) by creating a class method in the form, which I could then call before instantiating the form I want to render. As in:
class CritiqueForm(Form):
rating = IntegerField('Rating')
comment = TextAreaField('Comments')
submit = SubmitField('Save Critique')
#classmethod
def append_slider(cls, name, label):
setattr(cls, name, IntegerField(label))
return cls
where 'append_slider' is always an IntegerField with a label I provide. This works enough to allow me to populate the criteria sliders in the view, as in:
#app.route('/critique/<url_id>/edit', methods=['GET', 'POST'])
def edit_critique(url_id):
from app.models import RecordModel
from app.models.forms import CritiqueForm
record = RecordModel.get_object_by_url_id(url_id)
if not record: abort(404)
# build editing form
ratings = list()
for i, criterium in enumerate(record.criteria):
CritiqueForm.append_slider('rating_' + str(i+1),criterium.name)
ratings.append('form.rating_' + str(i+1))
form = CritiqueForm(request.form)
# Process valid POST
if request.method=='POST' and form.validate():
# Process the submitted form and show updated read-only record
return render_template('critique.html')
# Display edit form
return render_template('edit_critique.html',
form=form,
ratings=ratings,
)
The ratings list is built to give the template an easy way to reference the dynamic fields:
{% for rating_field in ratings %}
{{ render_slider_field(rating_field, label_visible=True, default_value=0) }}
{% endfor %}
where render_slider_field is a macro to turn the IntegerField into a slider.
With form.rating—an integer field explicitly defined in CritiqueForm—there is no problem and the slider is generated with a label, as expected. With the dynamic integer fields, however, I cannot reference the label value in the integer field. The last part of the stack trace looks like:
File "/home/vagrant/msp/app/templates/edit_critique.html", line 41, in block "content"
{{ render_slider_field(rating_field, label_visible=True, default_value=0) }}
File "/home/vagrant/msp/app/templates/common/form_macros.html", line 49, in template
{% set label = kwargs.pop('label', field.label.text) %}
File "/home/vagrant/.virtualenvs/msp/lib/python2.7/site-packages/jinja2/environment.py", line 397, in getattr
return getattr(obj, attribute)
UndefinedError: 'str object' has no attribute 'label'
Through some debugging, I have confirmed that none of the expected field properties (e.g., name, short_name, id ...) are showing up. When the dust settles, I just want this:
CritiqueForm.append_slider('rating', 'Rating')
to be equivalent to this:
rating = IntegerField('Rating')
Is the setattr() technique inherently limiting in what information can be included in the form, or am I just initializing or referencing the field properties incorrectly?
EDIT:
Two changes allowed my immediate blockers to be removed.
1) I was improperly referencing the form field in the template. The field parameters (e.g., label) appeared where expected with this change:
{% for rating_field in ratings %}
{{ render_slider_field(form[rating_field], label_visible=True, default_value=0) }}
{% endfor %}
where I replace the string rating_field with form[rating_field].
2) To address the problem of dynamically changing a base class from the view, a new form class ThisForm() is created to extend my base CritiqueForm, and then the dynamic appending is done there:
class ThisForm(CritiqueForm):
pass
# build criteria form fields
ratings = list()
for i, criterium in enumerate(record.criteria):
setattr(ThisForm, 'rating_' + str(i+1), IntegerField(criterium.name))
ratings.append('rating_' + str(i+1))
form = ThisForm(request.form)
I don't know if this addresses the anticipated performance and data integrity problems noted in the comments, but it at least seems a step in the right direction.

setattr(obj, name, value) is the very exact equivalent of obj.name = value - both being syntactic sugar for obj.__setattr__(name, value) -, so your problem is not with "some limitation" of setattr() but first with how wtform.Form works. If you look at the source code, you can see there's much more to make fields and form work together than just having the fields declared as class attributes (metaclass magic involved...). IOW, you'll have to go thru the source code to find out how to dynamically add fields to a form.
Also, your code tries to set the new fields on the class itself. This is a big NO NO in a multiprocess / multithreaded / long-running process environnement with concurrent access - each request will modify the (shared at process level) form class, adding or overriding fields aphazardly. It might seem to work on a single-process single-threaded dev server with a single concurrent user but will break in production with the most unpredictable errors or (worse) wrong results.
So what you want to find out is really how to dynamically add fields to a form instance - or, as an alternative, how to dynamically build a new temporary form class (which is far from difficult really - remember that Python classes are objects too).

Related

Optimise django manytomany relation

I was wondering if there is a way to make this more optimised.
class AppTopic(models.Model):
topic = models.CharField(max_length=20) # lowercase (cookies)
class UserTopic(models.Model):
topic = models.CharField(max_length=20) # Any case (cOoKiEs)
app_topic = models.ForeignKey(AppTopic) # related to (cookies -> lowercase version of cOoKiEs)
class User(models.Model):
topics = models.ManyToManyField(UserTopic) # AppTopic stored with any case
The goal is to have all AppTopics be lowercase on the lowest level, but I want to allow users to chose what capitalisation they want. In doing so I still want to keep the relation with it's lowercase version. The topic on the lowest level could be cookies, the user might pick cOoKiEs which should still be related to the orignal cookies.
My current solution works, but requires an entirely new table with almost no use. I will continue using this if there isn't really a smarter way to do it.
There's nothing in the Django model API that would allow you to manipulate values from the models themselves. If you don't want to change the value in the database or in the model instance, you can change how it displays on the template level using the
lower filter in Django template language.
<body>
<div>
{% if condition %}
<h1>{{AppTopic|lower}}</h1>
{% else %}
<h1>{{AppTopic|??}}</h1> #AppTopic but camelcase. There's no camelcase filter in Django
{% endif %}
</div>
</body
Views.py
def View(request, condition)
...
return render(request, 'template.html', {condition:'condition'})

Changing Id of django form in for loop

I'm stuck in my code. Need help.
This is my front end. I am rendering forms stored in "form_list".
The problem is that the forms stored are of same type and thus produce input fields with same "id" and same "name".
This is my view:-
#login_required
def VideoLinkView(request):
"""view to save the video links """
current_form_list = []
current_form = None
if request.method == 'GET':
vl = VideoLink.objects.filter(company=CompanyModel.objects.get(owner=request.user))
for link in vl:
current_form = VideoLinkForm(link.__dict__)
current_form_list.append(current_form)
return render(request, "premium/video_link.html", context={'form_list':current_form_list})
This is my html template :-
{% for form in form_list %}
<div class="form-group">
<label for="id_video_link">Video Link:</label>
{{ form.video_link }}
</div>
{% endfor %}
How can I create different "id" and different "name" in each iteration of for loop's input tag, automatically without having knowledge of no form stored in form_list.
I tried {{ forloop.counter}} it didn't worked, perhaps I made some mistake. Also, raw python don't work in template.
Thanks in Advance.
The way you are creating your forms is wrong in two ways. Firstly, the first positional argument is for the values submitted by the user; passing this arg triggers validation, among other things. If you are passing values for display to prepopulate the form, you must use the initial kwarg:
current_form = VideoLinkForm(initial={...dict_of_values...})
However, even that is not correct for your use case here. link is a model instance; you should use the instance kwarg:
current_form = VideoLinkForm(instance=link)
Now, to solve the problem you asked, you could just pass a prefix as well as I originally recommended:
for i, link in enumerate(vl):
current_form = VideoLinkForm(instance=link, prefix="link{}".format(i))
However, now that you have shown all the details, we can see that this is not the best approach. You have a queryset; so you should simply use a model formset.
from django.forms import modelformset_factory
VideoLinkFormSet = modelformset_factory(VideoLink, form=VideoLinkForm, queryset=vl)
current_form_list = VideoLinkFormSet()

Django ModelChoiceField allow objects creation

Django's ModelChoiceField is the default form field used for foreign keys when deriving a form from a model using ModelForm. Upon validation, the field will check that selected value does exist in the corresponding related table, and raise a ValidationError if it is not the case.
I'm creating a form for a Document model that has a type field, a foreign key to a Type model which does only contain a name attribute. Here is the code of models.py for clarity
class Type(models.Model):
name = models.CharField(max_length=32)
class Document(models.Model):
title = models.CharField(max_length=256)
type = models.ForeignKey(Type, related_name='related_documents')
Instead of a standard select control, I'm using selectize.js to provide auto-completion to the users. Moreover, selectize provides a "create" option, that allows to enter a value that does not exist yet into the select.
I would like to extend the ModelChoiceField in order to create a new Type object when the selected value does not exist (the new value will be assigned to name field, this should be an option of the field for reusability). If possible, I would like the object to not be inserted into DB until save() is called on the validated form (to prevent that multiple failed validation create multiple rows in db). What would be a good way to do so in Django? I tried to look into the documentation and the source code, tried to override ModelChoiceField and tried to build this behavior basted on a TextField but I'm not sure if there isn't a simpler way to do it.
I looked into the following questions but couldn't find the answer.
Django ModelChoiceField has no plus button
Django: override RelatedFieldWidgetWrapper
How to set initial value in a dynamic Django ModelChoiceField
I would like to keep the process of adding new types as simple as possible - i.e.: do not use a pop-up attached to a '+' button. User should be able to type the value, and the value gets created if it doesn't exist.
Thanks
This seems like it'd be easier without using a ModelForm. You could create a list of all of your Type objects then pass that to the template as a context variable. Then, you could use that list to construct a <select> element. Then use jquery and selectize to add the necessary attributes to the form.
#views.py
...
types = Type.objects.all()
...
#template.html
...
<form action="" method="POST">{% csrf_token %}
<input type='text' name='title'>
<select name='type' id='id_type'>
{% for type in types %}
<option value="{{type.id}}">{{type.name}}</option>
{% endfor %}
</select>
<input type='submit'>
</form>
<script type='text/javascript'>
$('#id_type').selectize({
create: true
});
</script>
...
Then when you get a form submission, you can process it in a simple view function:
if request.method == POST:
title_post = request.POST.get('title','')
type_post = request.POST.get('type',None)
if type_post is not None:
try:
#If an existing Type was selected, we'll have an ID to lookup
type = Type.objects.get(id=type_post)
except:
#If that lookup failed, we've got a user-inputted name to use
type = Type.objects.create(name=type_post)
new_doc = Document.objects.create(title=title_post, type=type)

Get values of related models from form in template (not so much hidden filed)

i have UpdateView opening form,
url(r'^calendar/(?P<pk>[0-9]*)/update/$', UpdateView.as_view(model=Calendar,success_url='..',template_name_suffix='_update_form'),name='calendar_update'),
where one of fileds point to really long list via ForeignKey :
class Calendar(models.Model):
task = models.ForeignKey(Task,help_text=u"Task")
...
class Task(models.Model):
long_name = models.TextField(blank=True,help_text=u"")
...
which is not going to change in the form, so i want it have in the template as a hidden field (no problem so far) but also I would like to have shown its value there (but not have to get the full table, as Select list do).
I would like have it something like this:
...
<tr><td>{{ form.task.label_tag}}</td>
<td>{{ form.task.as_hidden }} {{ form.task.long_name }}</td></tr>
where long_name is field on the Task related record (TextField), but it does not work
Is there a way to get the related name without having to get the data manually and manage everything with that form in views.py?

Filtering QuerySet based on captured URL parameters in Django?

I'm making a generic template that will be displaying a list of objects from a queryset:
{% for o in objects %}
{{ o.name }}
{% endfor %}
I want to be able to use the template in multiple situations, where different sorts of filtering and ordering need to be done. I have created a view function for this:
def display_objects(request, filters, orders, template_name):
objects = Object.objects.all()
for filter in filters:
objects = objects.filter(('%s__%s' % (filter['field'], filter['relationship']), filter['value']))
for order in orders:
objects = objects.order_by('-' if 'descending' in order else '' + order['field'])
# render objects to template with context
pass
I'm not sure if what I have done so far will even work, but I've run into an issue. I don't know if it's feasible to filter the query set by a parameter captured in the URL with my current function.
For example, if I wanted to display objects pertaining to a certain user I would do something like:
(r'^user/(?P<account_username>[^/]+)/$', display_objects, dict(filters=[{'field':'account__username','relationship':'iexact','value':account_username}], orders=[{'field':'foobar'}], template_name='user.html'))
Obviously, account_username isn't a defined field until the URL is parsed and dispatched to the display_objects function. It would be easy enough to make a view function that takes an account_username parameter, but I want to be able to use the function for displaying other object query sets that will be filtered with different captured parameters.
Is there some way I can pass captured URL parameters to a view function to dynamically filter or order a query set to be displayed?
Here's one way you can do this:
in urls.py:
(r'^user/(?P<account_username>[^/]+)/$', display_objects, dict(filters=[{'field':'account__username','relationship':'iexact'}], orders=[{'field':'foobar'}], template_name='user.html'))
And then in views.py:
def display_objects(request, filters, orders, template_name, **kwargs):
objects = Object.objects.all()
for filter in filters:
objects = objects.filter(('%s__%s' % (filter['field'], filter['relationship']), kwargs.get(filter['field'])))
for order in orders:
objects = objects.order_by('-' if 'descending' in order else '' + order['field'])
# render objects to template with context
pass
Although honestly I'm not sure if this is a good way of doing things...
You can't pass a string directly to the filter method. You need to translate it into kwargs.
query_string = '%s__%s' % (filter['field'], filter['relationship'])
objects = objects.filter(**{query_string: filter['value']}))

Categories