WTForms and Django OneToOneFields - python

I have a Django model which extends the auth User class, but I can't find a way to render data from both models in a single form.
For example, let's extend the User class with a country field using a One2OneField:
from django.contrib.auth.models import User
import pycountry
class Account(models.Model):
user = models.OneToOneField(User, primary_key=True)
COUNTRY_CHOICES = [(country.alpha2, country.name) for country in pycountry.countries]
country = models.CharField(max_length=2, choices=COUNTRY_CHOICES, default='US')
Now let's create a form which contains elements from both models:
class AccountSettingsForm(Form):
first_name = TextField(u'First name:', [validators.Length(min=2, max=35,message=u'First name should be between 2 and 35 characters.')])
last_name = TextField(u'Last name:', [validators.Length(min=2, max=35,message=u'Last name should be between 2 and 35 characters.')])
email = EmailField(u'E-mail address:', [validators.Email(message=u'Invalid e-mail address.')])
COUNTRY_CHOICES = [(country.alpha2, country.name) for country in pycountry.countries]
country = SelectField(u'Country', [valid_country,],choices=COUNTRY_CHOICES)
Now, on my "account settings" page, I have this:
#login_required
def update_settings(request):
form = AccountSettingsForm(request.POST, obj=request.user)
if request.method=='POST' and form.validate():
#processing goes here
context = {'form': form}
return render(request, 'account/settings.html', context)
When opening the page, only the info from "User" (like first name, last name, mail address,...) is displayed. However, the "country" part is not retrieved from the model.
What do I need to do to get both displayed on the same form? I don't see how I can explicitly bind the "country" field of the form to the user.account.country field on the model.

You can override the __init__() method like this to populate the data from obj.account (is that even the default name? I always use the related_name option).
class AccountSettingsForm(Form):
def __init__(self, formdata=None, obj=None, prefix='', data={}, meta=None, **kwargs):
data['country'] = obj.account.country
# etc
super(AccountSettingsForm, self).__init__(formdata, obj, prefix,
data, meta, **kwargs)

Seems it was easier than I thought.
Replace this:
form = AccountSettingsForm(request.POST, obj=request.user)
with:
form = AccountSettingsForm(request.POST, obj=request.user, country=request.user.account.country)

Related

How to redirect from FormView to a ListView after form validation

In my Django application, I would like to display a form where a user enters a name to search for Persons from an external data source (not model). I am using class-based generic views and have a working application (code attached below) with a minor inconvenience - I would like to see if there is a better way to do this.
First, here's how I have done it:
I have a Form with 3 fields (first, second and last name) and a clean() where I check if at least one field is populated
A FormView which renders this form, and form_valid() method which does nothing at the moment (reasons will become clear shortly)
An entry in urls.py to render this view and display the form in a template. The form is being submitted to a ListView with GET, and not the FormView itself (sad!)
A ListView where I define get_queryset(self) because data comes from an external source, not a Model; and I use self.request.GET.get('first_name', '') etc. to retrieve query string values, make a connection to the external data source and get a Pandas dataframe which I convert to a list of records and render, paginated, in the template.
An entry in urls.py for the List View and render the template of search results.
Everything works but, hopefully, the problem is apparent. The FormView is being used only to display the form, but the form submits to the ListView where data is retrieved to be displayed. This means that my form_valid() in the FormView and consequently clean() from the form aren't even used - I can work around it by using Javascript based validation but I would like to be able to use the FormView to its full potential.
So, how do I redirect to ListView with the form input, after form validation?
Here's my simple, and working, code:
urls.py
...
path('search/name', form_views.NameSearchView.as_view(), name='search-name'),
path('search/results', list_views.SearchResultsList.as_view(), name='search-results'),
...
forms.py
class NameSearchForm(forms.Form):
last_name = forms.CharField(label='Last Name', required=False)
first_name = forms.CharField(label='First Name', required=False)
second_name = forms.CharField(label='Second Name', required=False)
def clean(self):
cleaned_data = super().clean()
first_name = cleaned_data['first_name'].strip()
second_name = cleaned_data['second_name'].strip()
last_name = cleaned_data['last_name'].strip()
# At least one field should be filled to search
if not (first_name or second_name or last_name):
raise ValidationError(
_('Fill in a name to search!'),
code='invalid',
params={
'first_name': first_name,
'second_name': second_name,
'last_name': last_name,
})
form_views.py
class NameSearchView(FormView):
template_name = 'app/search_name.html'
form_class = NameSearchForm
# This is clearly wrong as it does nothing
success_url = '.'
def form_valid(self, form):
self.form = form
return HttpResponseRedirect(self.get_success_url())
list_views.py
class SearchResultsList(ListView):
template_name = 'app/search_results.html'
paginate_by = 10
def get_queryset(self):
first_name = self.request.GET.get('first_name', '').strip()
second_name = self.request.GET.get('second_name', '').strip()
last_name = self.request.GET.get('last_name', '').strip()
conn = create_connection() # This abstraction creates a database connection
query = '''
SELECT i.first_name first_name,
i.second_name second_name,
i.last_name last_name,
i.info
FROM db.person_tbl i
WHERE i.name_type = 'primary'
'''
params = []
terms = [first_name, second_name, last_name]
term_keys = ['first', 'sec', 'last']
for i, term in enumerate(terms):
if term:
query += f' AND i.srch_{term_keys[i]}_name = ?'
params.append(term)
# this abstraction gets the pandas dataframe
object_list = conn.get_data(query, params=params)\
.sort_values(by='last_name')\
.to_dict('records')
return object_list
I would like to be able to submit POST to the FormView itself, validate the form and then display the search results, paginated, through a ListView.
Any help would be greatly appreciated!

Post additional model values to database in views.py not modelform

model
class Person(models.Model):
first_name = models.CharField(blank=False,max_length=256,default='')
last_name = models.CharField(blank=False,max_length=256,default='')
plan = models.CharField(blank=False,max_length=256,default='')
plan_price = models.CharField(blank=False,max_length=256,default='')
Views.py
if request.method == 'POST':
if form.is_valid():
form.save(commit=True)
return index(request)
In my modelForm I accept 3 values from the user: first_name, last_name, and plan. I dont have any problem with posting to the database from the form, what i am trying to find out is how I can say something like this
if plan = 'plan1':
#set plan_price to '$399'
else
#set plan_price to '$699'
#then post first_name, last_name, plan, plan_price to database
You can try the following:
if form.is_valid():
person = form.save(commit=False)
plans = {
'plan1': 399,
'plan2': 699,
# ...
}
person.plan_price = plans.get(person.plan, some_default)
person.save()
return index(request)
# you might consider a redirect instead so as not to have the same content on various urls
If the plan price always should match the plan you can also override the model's save method and leave the view as you had it:
class Person(models.Model):
# ...
def save(self, *args, **kwargs):
self.plan_price = some_logic(self.plan)
super(Person, self).save(*args, **kwargs)
But then, you could replace that field by a property alltogether as it seems redundant. If plans, for instance, change prices, I would consider a Plan model with a name and price field.
in your createview you can use this function and write your code there
def form_valid(self, form):
if self.object.plan = 'plan1':
form.instance.price = 399
else:
[...]
return super(your_class_name, self).form_valid(form)
you can access the created object fields by self.object.filed

auth select chooses in forms.py

I have read this code in this question and look nice.
but if I have user auth and I want user select only your odjects how to change that code ?for ex chooses your personal upload images.
from django.forms.widgets import Select
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=( (x.id, x.name) for x in CHOICES )),
}
my model :
class MyModel(models.Model):
user = models.ForeignKey(User, unique=True)
upload = models.ImageField(upload_to='images')
Whenever you instantiate your form inside your view, you should pass the user object, like this my_form = MyModelForm(user=request.user).
Then build your MyModelForm:
# forms.py
from django.forms import ModelForm
from django.forms.widgets import Select
class MyModelForm(ModelForm):
def __init__(self, *args, **kwargs):
# extract "user" value from kwrags (passed through form init). If there's no "user" keyword, just set self.user to an empty string.
self.user = kwargs.pop('user', '')
super(MyModelForm, self).__init__(*args, **kwargs)
if self.user:
# generate the choices as (value, display). Display is the one that'll be shown to user, value is the one that'll be sent upon submitting (the "value" attribute of <option>)
choices = MyModel.objects.filter(user=self.user).values_list('id', 'upload')
self.fields['upload'].widget = Select(choices=choices)
class Meta:
model = MyModel
fields = ('upload',)
Now, whenever you instantiate the form with a user keyword argument (my_form = MyModelForm(user=request.user)), this form will be rendered like this (in your template write it like {{ my_form }}):
<select>
<option value="the_id_of_the_MyModel_model">upload_name</option>
<option value="the_id_of_the_MyModel_model">upload_name</option>
...
</select>
Finally, in order to display images in the dropdown menu (remember, "value" is the one that will be sent back to server upon submiting the form, while the display one just for the UX), take a look here.
[UPDATE]: How to do it in your views.py
# views.py
def my_view(request):
my_form = MyModelForm(user=request.user)
if request.method == 'POST':
my_form = MyModelForm(request.POST, user=request.user)
if my_form.is_valid():
# ['upload'] should be the name of the <select> here, i.e if <select name="whatever"> then this should be "whatever"
pk = my_form.cleaned_data['upload']
# image, now, is the value of the option selected (that is, the id of the object)
obj = MyModel.objects.get(id=pk)
print(obj.upload.url) # this should print the image's path
return render(request, 'path/to/template.html', {'my_form': my_form})

Django ModelForm add foreign key after post submit ignored

I am currently working on our user profile. A user can add multiple E-Mail addresses to his/her account.
views.py
#login_required(login_url='/login')
def profile_update_emails(request):
context = {}
...
try:
email = CustomerEmails.objects.get(customer=request.user)
update_emails_form = UpdateEmailsForm(request.POST or None, instance=email)
except CustomerEmails.DoesNotExist:
update_emails_form = UpdateEmailsForm(request.POST or None)
context.update({'update_emails_form': update_emails_form})
if request.POST:
if update_emails_form.is_valid():
update_emails_form.save(commit=False)
update_emails_form.customer = request.user
update_emails_form.save()
messages.success(request, "All good")
return render(request, 'usercp/profile.html', context)
forms.py
class UpdateEmailsForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(forms.ModelForm, self).__init__(*args, **kwargs)
for f in self.fields:
self.fields[f].widget.attrs['class'] = 'form-control'
class Meta:
model = CustomerEmails
fields = ('email',)
models.py
class CustomerEmails(models.Model):
customer = models.ForeignKey(Customer)
email = models.CharField(max_length=255, unique=True)
error/trace:
IntegrityError at /usercp/profile/profile_update_emails
(1048, "Column 'customer_id' cannot be null")
Please note I have a custom user model. Which is not the problem here.
I am not quite sure, why the customer field is not getting populated before the second save() though. There are currently no rows in that table for the CustomerEmails model. (I know that this will clash with the .get() in the future, since a user can have multiple e-mails, but first things first)
You need to set the relationship on the model instance, which is returned from the form save, not on the form itself.
obj = update_emails_form.save(commit=False)
obj.customer = request.user
obj.save()

django dynamic forms - Dynamic ChoiceField

In django, how can I make a selectible formField to access the db for every time it is being calld?
Right now the line :
status = forms.ChoiceField(choices=FormsTools.StatusesToTuples(Status.objects.all()))
is executed once django is loaded and not every time the form is being showed.
How can I make the field dynamic ? so every time the form is being showed the selectible field will have values from db?
UPDATE:
POST data:
.
status: u'4'
.
.
in the Model, the field looks like this: status = models.IntegerField()
The View:
def edit_call(request, call_id):
c = Call.objects.get(id=call_id)
if request.POST:
form = CallForm(request.POST, instance=c)
print form.errors
if form.is_valid():
form.save()
return HttpResponseRedirect('/ViewCalls/')
else:
form = CallForm(instance=c)
args = {}
args.update(csrf(request))
args["form"] = form
args["id"] = call_id
t = get_template('edit_call.html')
cont = RequestContext(request, args)
html = t.render(cont)
return HttpResponse(html)
The form:
simple as:
class CallForm (forms.ModelForm):
employee_id = forms.ModelChoiceField(queryset=Employee.objects.all())
status = forms.ModelChoiceField(queryset=Status.objects.all())
class Meta():
model = Call
You need to call the contructor each time you load the form to update the choices. So the form should be:
class CallForm(forms.ModelForm):
...
status = forms.ChoiceField()
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=None,
empty_permitted=False):
super(CallForm, self).__init__(data, files, auto_id, prefix, initial, error_class,
label_suffix, empty_permitted)
self.fields['status'].choices = FormsTools.StatusesToTuples(Status.objects.all())
Have you looked at forms.ModelChoiceField?
UPDATED ANSWER FOLLOWING UPDATED QUESTION:
You now need to get your models and your forms to match:
Your model has an IntegerField, your form has a ModelChoiceField. The latter returns a pk string, not an integer ID.
Given that you're using a modelform, why not just let it do the work of creating the fields for you?
class CallForm(forms.ModelForm):
class Meta:
model = Call
fields = ('employee', 'status') # assuming these are what the field names are

Categories