I having a weird issue with Django 1.3. When I try to add a new Activity using the ActivityForm, the Activity is often updated even if I explicitly create a new Activity object with not Id.
In addition, when the init of my ActivityForm class has an explicit trip_id parameter, the UI displays "Select a valid choice. That choice is not one of the available choices." for the field location (even though the selection shown is valid). However, if I make this an optional parameter and I pop it from kwargs, I don't see this issue.
Can someone please take a look at the code below and see what I am doing wrong?
forms.py
class DestinationMultipleChoiceField(ModelMultipleChoiceField):
def label_from_instance(self, obj):
return obj.city_name
class ActivityForm(forms.Form):
description = forms.CharField(max_length=100, required=True)
location = DestinationChoiceField(queryset = Visit.objects.none(), empty_label=None, required=True)
def __init__(self, trip_id = None, *args, **kwargs):
super(ActivityForm, self).__init__(*args, **kwargs)
if trip_id is not None:
self.fields['location'].queryset = Visit.objects.filter(trip=trip_id).all().select_related('destination')
# A new Activity() is created if nothing is provided
def save(self, activity = Activity()):
if not self.is_valid():
raise forms.ValidationError('ActivityForm was not validated first before trying to call save().')
activity.visit = self.cleaned_data['location']
activity.description = self.cleaned_data['description']
activity.added_by = User.objects.get(pk=1)
activity.save()
views.py
def add_activity(request, trip_id = None, template_name = 'edit-form.html'):
if request.POST:
form = ActivityForm(trip_id, request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('views.list_trip'))
else:
form = ActivityForm(trip_id)
return render_to_response(template_name, {
'page_title': 'Add',
'form': form,
}, context_instance=RequestContext(request))
Look at this line:
def save(self, activity = Activity()):
In Python, default parameters are evaluated once; thus, when the save method is called the first time, a new Activity will be created, but subsequent calls will use that Activity.
Change save to something like this:
def save(self, activity=None):
if activity is None:
activity = Activity()
# Rest of the method
Then, a new Activity will be created on every call (if one is not supplied as an argument).
Model instances are mutable. As such they should never be used as default arguments to a method, as you do in save. There's a reason why you've never seen that in any documentation or examples.
As noted in very many other questions on this site, default arguments are evaluated on definition, not execution. So each call to save with no activity argument will use the same Activity that was originally defined.
However, I don't understand why you're not using a ModelForm, which does most of this for you in any case.
Related
Recently I've been trying to do something with this.
Think of the family as a facebook group.
class Family(models.Model):
name = models.CharField(max_length=50)
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='owned_families')
users = models.ManyToManyField(User, related_name='families', blank=True)
let's assume we have this family object called fm, for illustration purpose.
My problem is, The owner is one of the users right? I mean, When someone creates a family, He's now the owner right? he owns it but he's still a user listed in it's users list.
Now, when I create a new family fm , I want to add the fm.owner to fm.users.
Let's talk about what I've tried.
post_save signal doesn't work with m2m. X
m2m_changed happens when the field is changed, not created. X
Overriding save method, lemme illustrate what I tried to acheive. ?
def save(self, *args, **kwargs):
old = self.pk
super(Family, self).save(*args, **kwargs)
if old is None:
print('This actually shows up')
self.users.add(self.owner)
Basically, this saves the pk each time, First time a family is created, Before calling super..... it has no .pk so I'm counting on this to check if it had no pk (On creation).
The problem is self.users.add(self.owner) doesn't work.
I've tried to clone the object as whole and keep track of it like
def save(self, *args, **kwargs):
old = self
super(Family, self).save(*args, **kwargs)
if old is None:
print("This actually doesn't show up")
self.users.add(self.owner)
This actually is terrible, It takes a refernce to self and when calling super...., The selfand it's reference old gets mutated, I just wanted to show this as this question itself might solve someone's problem.
So I solved this by.
import copy
def save(self, *args, **kwargs):
old = copy.deepcopy(self)
super(Family, self).save(*args, **kwargs)
if old is None:
print('This actually shows up')
self.users.add(self.owner)
but self.users.add(self.owner) still doesn't work.
What am I missing?
The problem is probably that in the django admin, the instance is saved first, and only after that the inline formsets and m2m-fields are saved. If the owner is not in there, it will be removed.
You can override some functionality in the admin to remedy this:
class FamilyAdmin(ModelAdmin):
def save_related(self, request, form, formsets, change):
super(FamilyAdmin, self).save_related(request, form, formsets, change)
form.instance.users.add(form.instance.owner)
Furthermore, you can try (note that there are other ways to remove the owner that are not picked up by any signal or other hook) to prevent code from removing the owner:
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
#receiver(m2m_changed, sender=Family.users.through)
def famliy_users_changed(sender, **kwargs):
family = kwargs['instance']
pk_set = kwargs['pk_set']
action = kwargs['action']
if action == "pre_add":
pk_set.add(family.owner_id)
if action == "pre_remove":
pk_set.remove(family.owner_id)
if action == "post_clear":
family.users.add(family.owner)
But generally speaking, you are jumping through those hoops because you are denormalizing your data (putting the owner in users makes that information redundant, forcing you to keep your data correct). Since you always know the owner is one of the users, why not wrap that in a method
class Family(...):
# ...
def members(self):
return User.objects.filter(Q(pk__in=self.users.all()|Q(pk=self.owner_id)))
and access family members through that method?
I have a form which has a variable that calls a function to get a list of names. I need to pass the current logged in user as a dynamic parameter variable into this function.
I have spent about 2 days on this trying to work any and every solution I can. Cannot find anything that works. I have tried to initialize a request object but cannot get that to work.
class ManagerForm(forms.Form):
names = get_employee_names(<<dynamic username goes here>>)
manager = forms.ChoiceField(choices=names, widget=forms.RadioSelect)
The expected result is to pass the username as a string into the function as a parameter.
Forms by itself doesn't have access to request object and therefore can't identify which user is currently logged. Your view should pass current user username instead:
views.py:
def index(request):
# ...
form = ManagerForm(request.POST or None, current_user_username=request.user.username)
# ...
forms.py:
def get_employee_names(username):
# assuming it constructs correct choices tuples, like:
# choices = ((username, username), ('noname', 'noname'))
return choices
class ManagerForm(forms.Form):
manager = forms.ChoiceField(choices=[], widget=forms.RadioSelect)
def __init__(self, *args, **kwargs):
username = kwargs.pop('current_user_username')
super().__init__(*args, **kwargs)
self.fields['manager'].choices = get_employee_names(username)
This is description of what django expect choices to be.
I have this current form:
class PersonForm(forms.Form):
article = forms.CharField(required=False)
workshop = forms.ModelChoiceField(queryset=Program.objects.none(),
empty_label="----",
required=False,
label='Atelier')
def __init__(self, *args, **kwargs):
super(PersonForm, self).__init__(*args, **kwargs)
article = self.initial.get('article', None)
if article:
a = Article.objects.get(pk=article)
if a.workshop:
self.fields['workshop'].queryset = Program.objects.filter(event=a.event, workshop=True)
self.fields['workshop'].required = True
self.helper.layout.insert(4, Row1Field('workshop',))
The queryset used to retrieve all the workshops is dynamic, so the queryset attribute inside the ModelChoiceField is set to Program.objects.none(), and the dynamic queryset is done inside the form's __init__ method
Everything is working fine: all the values displayed for workshop inside the select are ok. But when I post the form, there is a validation error: 'Select a valid choice. This choice is not among available choices.'
Is there something I'm missing?
Depending on the code that is instantiating the form upon post, initial may not be passed. You could try:
article = self.initial.get('article', None) or self.data.get('article', None)
This will get the pk from your form POST data.
I have this form:
forms.py
class OptionForm(forms.Form):
name = forms.CharField(widget=forms.TextInput(attrs={'class' : 'form-control'}), max_length=100, label='Option Name')
default = forms.BooleanField(label='Default', required=False)
So I can add a new option or edit one that already exist with this two views
views.py
def new_opt(request):
if request.method=='POST':
fnewopt = OptionForm(request.POST)
if fnewopt.is_valid():
model = OptionModel()
model.name = fnewopt.cleaned_data.get('name')
model.default = fnewopt.cleaned_data.get('default')
model.save()
def edit_opt(request, id_opt):
opt = OptionModel.objects.get(id=id_opt)
if request.method == 'POST':
feditopt = OptionForm(request.POST)
if feditopt.is_valid():
model = opt
model.name = feditopt.cleaned_data.get('name')
model.default = feditopt.cleaned_data.get('default')
modelo.save()
But I need that when a user choose one option as default no matter if is new or one that already exist all others options set default to False
You can do that by customizing your model's save() method:
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.default:
# At this point self.pk will exist whether
# this was a new or updated instance.
# Using update() allows this to run in a single query
# (plus the query for super().save(), of course).
OptionModel.objects.exclude(pk=self.pk).update(default=False)
A few other notes:
You should wrap your save() method in an atomic transaction.
This will do the extra update() every time the option is saved, not just the first time. To avoid that you'd have to detect when the value was changed to True. There are third-party packages to allow you to do that, but it's probably not worth the bother.
This question and answer doesn't address the validation issue—what if another OptionModel already has default=True?
If your new option has default=True, set all existing options' default=false then set your new option default = true. I'm not sure where your options go when they get saved. You might have a better time finding out which option is true and just setting that false.
I have django model form MyModelFormA for the model ModelA (I am using these in FormView).
I want to pass initial values to the form using existing object of ModelA and create new object of it if changes occur.
I have been passing initial values like below:
def get_form_kwargs(self):
kwargs = super(MyFormView, self).get_form_kwargs()
kwargs.update({'instance': ModelAObject})
I'm not sure why but when the form is validated like below
def form_valid(self, form):
instance = form.save()
Its just updating existing instance object instead of creating new.
HTTP requests being stateless, how does the request knows an instance that is being passed processed in previous request
How to solve this?
I thought of doing something like
def get_initial(self):
initial = model_to_dict(MyModelAObject)
return initial
Actually There are only a subset of MyModelA fields in MyModelFormA. Passing all fields as dict initially, wouldn't create any trouble?
is there any much elegant way to handle it?
When you pass ModelForm an instance, it sets id field of that instance as initial of the form as well. So, if it receives an ID in the POST, its treats it as a existing object and updates it
You will need to pass individual field's initial value(i.e except id). The best way is to only pass the fields you need, in ModelForm, as initial.
def get_initial(self):
return {
'a': MyModelAObject.a,
....
}
Probably you can try this:
def form_valid(self, form):
if form.has_changed()
instance = form.save(commit=False)
instance.pk = None
#if you have id
instance.id = None
instance.save() #will give you new instance.
Check In Django 1.4, do Form.has_changed() and Form.changed_data, which are undocumented, work as expected? to see how form.has_changed() will work.