Django ManyToManyField not saving m2m relationships - python

I have a model defined like so:
class Vote(models.Model):
text = models.CharField(max_length=300)
voters = models.ManyToManyField(CustomUser, blank=True)
game = models.ForeignKey(Game, on_delete=models.CASCADE)
I want a vote to automatically add all players of its associated game to its list of voters, when it is created. The Game model has a method that returns its players, so I have overridden the save method of the Vote model:
def save(self, *args, **kwargs):
super().save(*args, **kwargs) #As the docs state it must be saved before m2m elements can be added
queryset = self.game.get_players
for player in queryset:
self.voters.add(player.id)
This does not work. It does not throw an error, and happily saves the model instance through the admin site. It does not, however, seem to add any of the players to the voters field, (and the vote_voters table remains empty in the db).
Obvious troubleshooting: the queryset is definitely not empty, the save method is definitely being called.

Your models.py
class Vote(models.Model):
text = models.CharField(max_length=300)
voters = models.ManyToManyField(CustomUser, blank=True)
game = models.ForeignKey(Game, on_delete=models.CASCADE)
in forms.py
from django import forms
from your_app.models import Vote
class VoteForm(forms.ModelForm):
class Meta:
model = Vote
fields = ('text', 'game')
And a class based create view
from django.views.generic.edit import CreateView
from your_app.models import Vote
from your_app.forms import VoteForm
class VoteCreate(CreateView):
model = Vote
form_class = VoteForm
def form_valid(self, form):
obj = form.save(commit=True)
obj.voters = obj.game.get_players
# or maybe this
# obj.voters.add([game.player for game in obj.game.get_players])
obj.save()
return super().form_valid(form)
Not tested but the idea in the create view is that you first create the object and then save the m2m. Check the form_valid method

It turns out that this was an issue with the admin section. Using the exact save method shown in the question worked perfectly when submitted through a form. #Selcuk's link to this answer this answer solved the problem

Related

Django form not populating with POST data

SOLUTION AT THE BOTTOM
Problem: Django form populating with list of objects rather than values
Summary: I have 2 models Entities and Breaks. Breaks has a FK relationship to the entity_id (not the PK) on the Entities model.
I want to generate an empty form for all the fields of Breaks. Generating a basic form populates all the empty fields, but for the FK it generates a dropdown list of all objects of the Entities table. This is not helpful so I have excluded this in the ModelForm below and tried to replace with a list of all the entity_ids of the Entities table. This form renders as expected.
class BreakForm(ModelForm):
class Meta:
model = Breaks
#fields = '__all__'
exclude = ('entity',)
def __init__(self, *args, **kwargs):
super(BreakForm, self).__init__(*args, **kwargs)
self.fields['entity_id'] = ModelChoiceField(queryset=Entities.objects.all().values_list('entity_id', flat=True))
The below FormView is the cbv called by the URL. As the below stands if I populate the form, and for the FK column entity_id choose one of the values, the form will not submit. By that field on the form template the following message appears Select a valid choice. That choice is not one of the available choices.
class ContactFormView(FormView):
template_name = "breaks/test/breaks_form.html"
form_class = BreakForm
My initial thoughts were either that the datatype of this field (string/integer) was wrong or that Django needed the PK of the row in the Entities table (for whatever reason).
So I added a post function to the FormView and could see that the request.body was populating correctly. However I can't work out how to populate this into the ModelForm and save to the database, or overcome the issue mentioned above.
Addendum:
Models added below:
class Entity(models.Model):
pk_securities = models.AutoField(primary_key=True)
entity_id = models.CharField(unique=True)
entity_description = models.CharField(blank=True, null=True)
class Meta:
managed = False
db_table = 'entities'
class Breaks(models.Model):
pk_break = models.AutoField(primary_key=True)
date = models.DateField(blank=True, null=True)
entity = models.ForeignKey(Entity, on_delete= models.CASCADE, to_field='entity_id')
commentary = models.CharField(blank=True, null=True)
active = models.BooleanField()
def get_absolute_url(self):
return reverse(
"item-update", args=[str(self.pk_break)]
)
def __str__(self):
return f"{self.pk_break}"
class Meta:
managed = False
db_table = 'breaks'
SOLUTION
Firstly I got this working by adding the following to the Entity Model class. However I didn't like this as it would have consequences elsewhere.
def __str__(self):
return f"{self.entity_id}"
I found this SO thread on the topic. The accepted answer is fantastic and the comments to it are helpful.
The solution is to subclass ModelChoiceField and override the label_from_instance
class EntityChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return obj.entity_id
I think your problem is two fold, first is not rendering the dropdown correctly and second is form is not saving. For first problem, you do not need to do any changes in ModelChoiceField queryset, instead, add to_field_name:
class BreakForm(ModelForm):
class Meta:
model = Breaks
#fields = '__all__'
def __init__(self, *args, **kwargs):
super(BreakForm, self).__init__(*args, **kwargs)
self.fields['entity_id'] = ModelChoiceField(queryset=Entities.objects.all(), to_field_name='entity_id')
Secondly, if you want to save the form, instead of FormView, use CreateView:
class ContactFormView(CreateView):
template_name = "breaks/test/breaks_form.html"
form_class = BreakForm
model = Breaks
In Django, the request object passed as parameter to your view has an attribute called "method" where the type of the request is set, and all data passed via POST can be accessed via the request. POST dictionary. The view will display the result of the login form posted through the loggedin. html.

Django 'User' object has no attribute 'Goals'

I have created a function that can save multiple goals per one user and display them in an html file. The issue is once I logout, I cannot log back in with the same user as I get the error User object has no attribute Goals, even though it is saved in the database. My question is what is causing this error, the references to goals in my view maybe, and what is a potential solution? Thank you!
models.py
class Goals(models.Model):
user = models.ForeignKey(User, null=True, default=None, on_delete=models.CASCADE)
goal = models.CharField(max_length=2000)
instrument = models.CharField(max_length=255, choices=instrument_list, blank=True)
goal_date = models.DateField(auto_now=False, auto_now_add=False)
def __str__(self):
return self.Goals
#receiver(post_save, sender=User)
def create_user_goals(sender, instance, created, **kwargs):
if created:
Goals.objects.create(user=instance)
#receiver(post_save, sender=User)
def save_user_goals(sender, instance, **kwargs):
instance.Goals.save()
class GoalsForm(ModelForm):
class Meta:
model = Goals
exclude = ('user',)
views.py
def goal_creation(request):
form = GoalsForm()
cur_goals = Goals.objects.filter(user=request.user)
if request.method == 'POST':
form = GoalsForm(request.POST)
if form.is_valid():
goals = form.save(commit=False)
goals.user = request.user
goals.save()
cur_goals = Goals.objects.filter(user=request.user)
return redirect('/student/goal-progress')
else:
form = GoalsForm()
context = {'form' : form, 'goals': cur_goals}
return render(request, 'student/goal_creation.html', context)
You have two issues:
You can't access child instances using instance.Goals; you should use instance.goals_set.
You can't save a queryset. You should save Goals instances one by one, i.e.
for goal in instance.goals_set.all():
goal.save()
That being said, I recommend you to rename your Goals class to Goal as it will create confusion with Django's naming conventions. It also makes sense because each row represents a single goal.
Try adding related_name='goals' to user field definition of Goals class:
user = models.ForeignKey(User, related_name='goals', null=True, default=None, on_delete=models.CASCADE)
Then, you should be able to access this property on the user's object:
user_instance.goals.all().
Migration might be required.
Although this is not directly related to the issue, I think that it's better to name the model class in singular form "Goal", it will be consistent with other model's names (model represents one object=one row) and avoid ambiguity in automatic pluralization.

Django: CreateView don't show field and set it to request.user (ForeignKey)

I use a generic CreateView to let logged in users (Creator) add objects of the model Piece. Since creating a Piece is done by the Creator (logged in user) there is no need for the CreateView to either show or manipulate the 'creator' field. Hence I wish to not show it and set it to the logged in user. However, approaches such as overwriting form_valid or using get_form_kwargs seem not to get it done. Using the form_valid method gives a ValueError:
Cannot assign "<SimpleLazyObject: <User: patrick1>>": "Piece.creator" must be a "Creator" instance.
The solution seems to be just around the corner, I hope.
Tried but did not work:
form_valid method, form_valid method, get_form_kwargs method
My code:
models.py
class Piece(models.Model):
creator = models.ForeignKey('Creator', on_delete=models.SET_NULL, null=True)
title = models.CharField(max_length=200)
summary = models.TextField(max_length=1000, help_text='Enter a brief description of the Piece')
created = models.DateField(null=True, blank=True)
...
from django.contrib.auth.models import User
class Creator(models.Model):
...
user = models.OneToOneField(User, on_delete=models.CASCADE)
...
views.py
class PieceCreate(LoginRequiredMixin, CreateView):
model = Piece
fields = ['title', 'summary', 'created']
initial = {'created': datetime.date.today()}
def form_valid(self, form):
obj = form.save(commit=False)
obj.creator = self.request.user
return super(PieceCreate, self).form_valid(form)
success_url = reverse_lazy('pieces')
Any suggestions are highly appreciated!
obj.creator = Creator.objects.get(user=self.request.user)
or any other solution that will give you Creator instance for current user instead of User. Just as the error message says.
Cannot assign "User: patrick1": "Piece.creator" must be a "Creator" instance.

Django How can I save two different class which are connected each other by OneToOne relationship in a form at once?

Let me explain what my problem is in more detail.
First I have a class 'UserInfo' which connected to User class of django.contrib.auth.models like below
models.py
class UserInfo(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
phone = models.CharField(max_length=15,blank=True,unique=True)
position = models.CharField(max_length=15,blank=True,unique=True)
class Meta:
default_permissions = ()
def __str__(self):
return self.position
then I wanted to use ModelForm because I can write less codes. The reason why I made CreateNewUser class is that I wanted to let user can see only [username, email, groups, user_permissions] and control those. (to prevent them to select superuser or staff or inactive options)
forms.py
class CreateNewUserInfo(forms.ModelForm):
class Meta:
model = UserInfo
fields = '__all__'
class CreateNewUser(forms.ModelForm):
class Meta:
model = User
fields = ['username', 'email', 'groups', 'user_permissions']
problem happened in here. I wanted to use FormView to use generic view with class typed view so that I can write less codes(concise code). there is attribute named 'form_class' and I couldn't put two different class with it. I wanted to put different two class to one form with generic view.
views.py
class TestView(FormView):
form_class = CustomForm
template_name = 'manager/alltoall.html'
def form_valid(self, form):
At the end, I made new class in forms.py and wrote every field which I need like below.
forms.py
class CustomForm(forms.Form):
username = forms.CharField(initial='testname',max_length=150)
email = forms.EmailField()
phone_number = forms.CharField(max_length=15)
position = forms.CharField(max_length=15)
views.py
class TestView(FormView):
form_class = CustomForm
template_name = 'manager/alltoall.html'
def form_valid(self, form):
username = form.cleaned_data['username']
email = form.cleaned_data['email']
phone_number = form.cleaned_data['phone_number']
position = form.cleaned_data['position']
with transaction.atomic():
user = User.objects.create(username=username,email=email)
userinfo = UserInfo.objects.create(user=user,phone=phone_number,position=position)
userinfo.save()
user.save()
return super(TestView, self).form_valid(form)
Is there anyway to use ModelForm and FormView to show two different class in a form at the same time? Additionally, I used transaction like above because I wanted to save data in database with two different class. Is it right approach or Is there any other way to do that more conveniently(or efficiently) with built in functions in django?
Thank you for taking your time to read all. I wonder if it is too long, but I wanted to deliver what I wanted to know exactly. Thank you!

Pass object to modelForm before CreateView form_valid method

I have a model with good validation, I'm using the clean method inside the model. the problem is when I am validating I am using an object that has not been set in the form which raise an exception that the object is not there yet.
I want a solution to pass the object from url primary key to the form before any validation, so my clean method works fine.
Here is a similar example.
The main model
class Student(models.Model):
first_name = models.CharField(max_length=30)
lets sat that each student might have one semester at a time. However, if there are any semesters before then the start date must be after the last semester end date.
class Semester(models.Model):
student = models.OneToOneField(Student)
start_date = models.DateField()
def clean(self):
# do not allow the start date to be before last semester end date
if self.student.semesterhistory_set.all().count() > 0:
last_semester_end_date = self.student.semesterhistory_set.last().end_date
if last_semester_end_date >= self.start_date:
message = _("Start Date for this semester must be after %s" % last_date)
raise ValidationError(message)
class SemesterHistory(models.Model):
student = models.ForeignKey(Student)
start_date = models.DateField()
end_date = models.DateField()
In the view, I am passing the student object which will be used in validation after validating the form. (problem)
# URL for this is like this student/(pk)/semesters/create/
class SemesterCreate(CreateView):
model = Semester
fields = ['start_date']
def form_valid(self, form):
form.instance.student = get_object_or_404(Student, id=int(self.kwargs['pk']))
return super(SemesterCreate, self).form_valid(form)
Error:
RelatedObjectDoesNotExist Semester has no student
Obviously you need call form.save(commit=False) which returns instance ... Also semantically wrong approach raise 404 in form_valid...
class SemesterCreate(CreateView):
model = Semester
fields = ['start_date']
student = object = None
def dispatch(self, request, *args, **kwargs):
self.student = get_object_or_404(Student, id=kwargs['pk'])
return super(SemesterCreate, self).dispatch(request, *args, **kwargs)
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.student = self.student
self.object.save()
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
return reverse('...')
https://docs.djangoproject.com/en/1.8/topics/forms/modelforms/#the-save-method
Actually to have a clean set I would add a custom ModelForm. I also use CreateView and this is how i use it .
First add a custom ModelForm (I personnaly add a forms.py file in my apps):
from django.contrib.auth.forms import UserCreationForm
from .model import Semester
class CreateSemesterForm(UserCreationForm):
error_messages = {
'last_date': _("some message"),
}
class Meta:
model = Semester
fields = ('some', 'fields') #or __all__
def clean_your_date_field(self):
#clean_name_field will be called when the form_valid will be called with all clean_fields functions
#here you define your clean method and raise Validation error
return field
def save(self, commit=True):
semester = super(CreateSemesterForm, self).save(commit=False)
#here you can set some other values
if commit:
semester.save()
return semester
And in your custom CreateView you have to add :
class SemesterCreate(CreateView):
form_class = CreateArtistForm
As you set model and fields in the ModelForm you can remove fields and model args from CreateView.
You also can override form_valid in your Custom ModelForm.
Now CreateView will call form_valid which call all clean functions, and if it's all passes, it returns and save your semester.
I came across this yesterday after facing the exact same issue with my project.
It's been a couple of years since you've posted this, but figure I'd post my solution to help anyone else out who might stumble across this.
The solution I came across is to use a custom modelform:
from django import forms
from .models import Blade
class SemesterForm(forms.ModelForm):
class Meta:
model = Semester
fields = '__all__'
widgets = {'student': forms.HiddenInput()}
And the in your view:
class SemesterCreate(CreateView):
model = Semester
def get_initial(self, **kwargs):
# get current student from pk in url
current_student = get_object_or_404(Student,
pk=self.kwargs.get('pk'))
return { 'site': current_student }
The trick here is that you must set the student field to hidden in the form. That way, it will keep the initialised value you give it in the view, but won't be available to the user.
So, it will be there when the form is submitted and the full_clean() method is called (which will then call the clean() method on the model), and your nice and tidy validations performed in the model clean() will work.

Categories

Resources