How to limit field choices related to another table in database? - django

I'm working on simple expenses manager. Every user is able to add/remove/edit an Operation, which represents expense or earning. However I have noticed a bug - while adding new Operation it is possible to choose other users Accounts and Categories.
Here is my Operation model:
class Operation(models.Model):
def get_category_color(self):
return Category.objects.get(name=self.category).color
types_tuples = ((-1, 'expense'), (1, 'earning'))
user = models.ForeignKey(User)
account = models.ForeignKey(Account)
category = models.ForeignKey(Category)
date = models.DateField()
amount = models.DecimalField(max_digits=10, decimal_places=2)
type = models.IntegerField(choices=types_tuples)
currency = models.CharField(max_length=3)
color = property(get_category_color)
OperationCreate view:
class OperationCreate(CreateView, OperationMixIn):
model = Operation
form_class = OperationForm
success_url = reverse_lazy('manage_operations')
def form_valid(self, form):
operation = form.save(commit=False)
operation.currency = Account.objects.get(pk=form.instance.account_id).currency
self.update_account_balance(form)
form.instance.user = self.request.user
return super(OperationCreate, self).form_valid(form)
and OperationForm:
class OperationForm(ModelForm):
class Meta:
model = Operation
fields = ['account', 'type', 'category', 'date', 'amount']
The question is how can I limit choices for Account and Category that are available during posting new Operation? I would like user to see only those which are related to his/her account.
I tried limit_choices_to as a parameter for models.ForeignKey(Account) and models.ForeignKey(Category) in Operation model but couldn't make it work that way.
I assume I need to use a query which will return only Accounts and Categories related to the current user. However I have no clue how and where should I apply it.
Could you please point me in the right direction?
EDIT:
OperationForm edited due to #efkin suggestion:
class OperationForm(ModelForm):
class Meta:
model = Operation
fields = ['account', 'type', 'category', 'date', 'amount']
def __ini__(self, user, *args, **kwargs):
super(OperationForm, self).__init__(*args, **kwargs)
self.fields['account'] = ModelChoiceField(queryset=Account.objects.filter(user=user))
self.fields['category'] = ModelChoiceField(queryset=Category.objects.filter(user=user))

You could use a simple Form with ModelChoiceField fields for foreign keys.

Okay, so I did some more digging and came up with the solution. OperationForm must be modified using ModelChoiceFiled (as #efkin said) and OperationCreate should contains override for get_form_kwargs method from ModelFormMixIn:
class OperationCreate(CreateView, OperationMixIn):
model = Operation
form_class = OperationForm
success_url = reverse_lazy('manage_operations')
def get_form_kwargs(self):
kwargs = super(OperationCreate, self).get_form_kwargs()
kwargs.update({'user': self.request.user})
return kwargs
def form_valid(self, form):
operation = form.save(commit=False)
operation.currency = Account.objects.get(pk=form.instance.account_id).currency
self.update_account_balance(form)
form.instance.user = self.request.user
return super(OperationCreate, self).form_valid(form)

Related

Django: how to filter form field based off foreignkey AND many to many relationship?

Currently, when a user creates a task, they can assign it to all users. I only want them to be able to assign a task based on the members of the project. I feel like the concept I have right now works but I need to replace the ????. Task's assignee has a foreignkey relationship with the user_model. The user_model is also connected with members on a many to many relationship.
projects/models.py
class Project(models.Model):
name = models.CharField(max_length=200)
description = models.TextField()
members = models.ManyToManyField(USER_MODEL, related_name="projects")
tasks/models.py
class Task(models.Model):
name = models.CharField(max_length=200)
start_date = models.DateTimeField()
due_date = models.DateTimeField()
is_completed = models.BooleanField(default=False)
project = models.ForeignKey(
"projects.Project", related_name="tasks", on_delete=models.CASCADE
)
assignee = models.ForeignKey(
USER_MODEL, null=True, related_name="tasks", on_delete=models.SET_NULL
)
tasks/views.py
class TaskCreateView(LoginRequiredMixin, CreateView):
model = Task
template_name = "tasks/create.html"
# fields = ["name", "start_date", "due_date", "project", "assignee"]
form_class = TaskForm
def get_form_kwargs(self):
kwargs = super(TaskCreateView, self).get_form_kwargs()
kwargs["user"] = self.request.user
kwargs["project_members"] = ??????????
return kwargs
tasks/forms.py
class TaskForm(ModelForm):
class Meta:
model = Task
fields = ["name", "start_date", "due_date", "project", "assignee"]
def __init__(self, *args, **kwargs):
user = kwargs.pop("user")
project_members = kwargs.pop("project_members")
super(TaskForm, self).__init__(*args, **kwargs)
self.fields["project"].queryset = Project.objects.filter(members=user)
self.fields["assignee"].queryset = Project.objects.filter(
members=?????????
)
Update:
I followed SamSparx's suggestions and changed the URL paths so now TaskCreateView knows which project id. I updated my tasks/views to the following but I get a TypeError: "super(type, obj): obj must be an instance or subtype of type" and it points to the line: form = super(TaskForm, self).get_form(*args, **kwargs) Maybe it has something to do with having a get_form_kwargs and get_form function? I kept my existing features for the custom form such as when a user creates a task, they can only select projects they are associated with.
Views.py updated
class TaskCreateView(LoginRequiredMixin, CreateView):
model = Task
template_name = "tasks/create.html"
form_class = TaskForm
def get_form_kwargs(self):
kwargs = super(TaskCreateView, self).get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs
def get_form(self, *args, **kwargs):
form = super(TaskForm, self).get_form(*args, **kwargs)
form.fields["assignee"].queryset = Project.members.filter(
project_id=self.kwargs["project_id"]
)
def form_valid(self, form):
form.instance.project_id = Project.self.kwargs["project_id"]
return super(TaskCreateView, self).form_valid(form)
def get_success_url(self):
return reverse_lazy("list_projects")
I have also tried to update the forms.py with the following but get an error that .filter cannot be used on Many to Many relationships.
Updated forms.py
class TaskForm(ModelForm):
class Meta:
model = Task
fields = ["name", "start_date", "due_date", "project", "assignee"]
def __init__(self, *args, **kwargs):
user = kwargs.pop("user")
super(TaskForm, self).__init__(*args, **kwargs)
self.fields["project"].queryset = Project.objects.filter(members=user)
self.fields["assignee"].queryset = Project.members.filter(
project_id=self.kwargs["project_id"]
)
Another thing I have tried is to go back to my first approach now that I have the url paths: tasks/create/(project_id)
Views.py
class TaskCreateView(LoginRequiredMixin, CreateView):
model = Task
template_name = "tasks/create.html"
form_class = TaskForm
def get_form_kwargs(self):
kwargs = super(TaskCreateView, self).get_form_kwargs()
kwargs["user"] = self.request.user
kwargs["project_id"] = Project.objects.all()[0].members.name
# prints to auth.User.none
return kwargs
I feel like if the kwargs["project_id"] line can be changed to getting list of members of whatever project with the ID in the URL, then this should solve it
Forms.py
class TaskForm(ModelForm):
class Meta:
model = Task
fields = ["name", "start_date", "due_date", "project", "assignee"]
def __init__(self, *args, **kwargs):
user = kwargs.pop("user")
project_id = kwargs.pop("project_id")
super(TaskForm, self).__init__(*args, **kwargs)
self.fields["project"].queryset = Project.objects.filter(members=user)
self.fields["assignee"].queryset = Project.objects.filter(
members=project_id
)
The problem here is that your task doesn't know what members are relevant to include as assignees until you have chosen the project the task belongs to, and both project and assignee are chosen in the same form, so Django doeesn't know who is relevant yet.
The easiest way to handle this is to ensure the call to create a task is associated with the project it is going to be for - eg,
Update your URLs to handle the project ID
Path('create-task/<int:project_id>', TaskCreateView.as_view(), name='create_task')
Update your view
class TaskCreateView(LoginRequiredMixin, CreateView):
model = Task
template_name = "tasks/create.html"
# fields = ["name", "start_date", "due_date", "assignee"]
#NB: I have remove project from the field list, you may need to do the same in your form as it is handled elsewhere
form_class = TaskForm
def get_form(self, *args, **kwargs):
form = super(TaskCreateView, self).get_form(*args, **kwargs)
form.fields['assignee'].queryset = Project.members.filter(project_id = self.kwargs['project_id'])
Return form
def form_valid(self, form):
form.instance.project_id = project.self.kwargs['project_id']
return super(TaskCreateView, self).form_valid(form)
Add links
Create Task for this project
This will create a link on the project details page, or underneath the project in a listview to 'create task for this project', carrying the project informaton for the view via the URL. Otherwise you will have to get into some rather more complex ajax calls that populate the potential assignees list based on the selection within the project dropdown in a dynamic fashion

TypeError: super(type, obj): obj must be an instance or subtype of type?

Why am I getting this error?
TypeError: super(type, obj): obj must be an instance or subtype of type
This is my models.py file
class UserNotification(models.Model):
Name = models.CharField(max_length=250)
Mobile_No = models.CharField(max_length=10, validators=[RegexValidator(r'^\d{1,10}$')])
Proof = models.TextField()
viewed = models.BooleanField(default=False)
user = models.ForeignKey(User)
date = models.DateTimeField(default=timezone.now)
def __str__(self):
return self.Name
class Meta:
ordering = ["-date"]
This is my views.py file
class RequestItem(generic.CreateView):
model = UserNotification
fields = ['Name', 'Mobile_No', 'Proof']
def get_form(self, form_class=None):
if form_class is None:
form_class = self.get_form_class()
form = super(UserNotification, self).get_form(form_class)
form.fields['Name'].widget = TextInput(attrs={'placeholder': '*Enter your name'})
form.fields['Mobile_No'].widget = TextInput(
attrs={'placeholder': "*Enter your's mobile number to get a call back from angel"})
form.fields['Proof'].widget = TextInput(attrs={'placeholder': '*enter proof you have for your lost item'})
return form
def form_valid(self, form):
print(self.kwargs)
self.object = form.save(commit=False)
qs = Report_item.objects.filter(id=self.kwargs.get("pk"))
self.object.user = qs[0].owner
self.object.save()
return HttpResponse("<h1>Your request has been processed</h1>")
I am using django 1.11. There was no error and code working properly until I add the placeholder function. After adding the placeholder I am getting this error. Please help me to resolve it.
The problem is where you call super() inside get_form. You need to use the current class; for some reason you have put the model class there. It needs to be:
form = super(RequestItem, self).get_form(form_class)
Or better, since you are using Python 3, use the short version:
form = super().get_form(form_class)
Note however this isn't really a good way to do what you're trying to do here. Rather, declare an actual form class which sets the widget attributes for the fields you want to change, and refer to it in the view class by setting the form_class attribute at class level.

Saving the form while there are two ForeignKeys,

I have two models as shown below
class college(models.Model):
name = models.CharField(max_length=256)
class education(models.Model):
author = models.ForeignKey('auth.User')
school = ForeignKey(college)
field = models.CharField(max_length=200)
startyear = models.IntegerField(blank =True,null = True)
endyear = models.IntegerField(blank =True,null = True)
Views as shown below
class EducationListView(ListView):
template_name = 'education.html'
def get_queryset(self):
return education.objects.filter(author__username=self.request.user.username).order_by('-startyear')
class EducationCreate(CreateView):
model = dupeducation
fields = ('school','field','startyear','endyear')
template_name = 'education_form.html'
def form_valid(self, form):
form.instance.author = self.request.user
obj,created = college.objects.get_or_create(name=form['school'])
obj.save()
form.instance.school = obj
return super(EducationCreate, self).form_valid(form)
class EducationUpdate(UpdateView):
model = education
fields = ('school','field','startyear','endyear')
template_name = 'education_form.html'
def form_valid(self, form):
form.instance.author = self.request.user
return super(EducationUpdate, self).form_valid(form)
class EducationDelete(DeleteView):
model = education
success_url = reverse_lazy('education')
I am unable to save the form. It's throwing an error to the school field like this "Select a valid choice. That choice is not one of the available choices.".
My goal is to take input for the school field and check that field with get_object_or_create . If that object does not exist, create it and attach it to the school field.
If you debug, you'll see that save() is not being reached. Your problem is in the Field validation.
What you need to do is to override the clean_<field>() method called before any object is saved.
You can read more about it here: Django how to override clean() method in a subclass of custom form?
While overriding clean_school(), you will be able to add the value to the database, and later, in save(), simply make the attribution.

Need help getting correct instance for form_valid in a generic view

I can't work out how to get the correct instance for the form_valid part of my generic view.
I am trying to allow a user to post on their project wall(bit like Facebook). I need the post to be related to an individual project(a user can have more than one project). Should the instance be a pk or the project title? Any example code or help would be very appreciated! I struggle understanding how when you create a new post, it knows which project to associate itself with.
views
class NewPost(CreateView):
model = ProjectPost
form_class = ProjectPostForm
template_name = 'howdidu/new_post.html'
def form_valid(self, form):
newpost = form.save(commit=False)
form.instance.user = self.request.user
newpost.save()
self.object = newpost
return super(NewPost, self).form_valid(form)
def get_success_url(self):
project_username = self.request.user.username
project_slug = self.object.slug
return reverse('user_project', kwargs={'username':project_username, 'slug': project_slug})
models
class UserProject(models.Model):
user = models.ForeignKey(User)
title = models.CharField(max_length=100)
project_overview = models.CharField(max_length=1000)
project_picture = models.ImageField(upload_to='project_images', blank=True)
date_created = models.DateTimeField(auto_now_add=True)
project_views = models.IntegerField(default=0)
project_likes = models.IntegerField(default=0)
project_followers = models.IntegerField(default=0)
slug = models.SlugField(max_length=100, unique=True) #should this be unique or not?
def save(self, *args, **kwargs):
self.slug = slugify(self.title)
super(UserProject, self).save(*args, **kwargs)
def __unicode__(self):
return self.title
class ProjectPost(models.Model):
project = models.ForeignKey(UserProject)
title = models.CharField(max_length=100)
post_overview = models.CharField(max_length=1000)
date_created = models.DateTimeField(auto_now_add=True)
post_views = models.IntegerField(default=0)
post_likes = models.IntegerField(default=0)
forms
#form to add project details
class UserProjectForm(forms.ModelForm):
class Meta:
model = UserProject
fields = ('title', 'project_picture', 'project_overview')
#form to create a post
class ProjectPostForm(forms.ModelForm):
class Meta:
model = ProjectPost
fields = ('title', 'post_overview')
Ok, in that case, I would recommend a URL something like
url(r'^(?P<pk>\d+)/post/add/$', views.NewPostCreateView.as_view(), name='...'),
and then a view like
class NewPost(CreateView):
model = ProjectPost
form_class = ProjectPostForm
template_name = 'howdidu/new_post.html'
def form_valid(self, form):
self.object = form.save(commit=False)
# Find project by using the 'pk' in the URL
project = get_object_or_404(UserProject, pk=self.kwargs['pk'])
# Then just set the project on the newPost and save()
self.object.project = project
self.object.save()
return super(NewPost, self).form_valid(form)
def get_success_url(self):
# Unchanged ...
I see in your code that you were trying to do something with the user but I don't understand why your Post does not have a user field (you may want to add a created_by) and the UserProject should already have a user set.
I am also assuming the user got to the his/her project first, so you know by definition that the project he is adding a post to is his. If that is not the case, then just change the logic to get the UserProject through a regular query. e.g. maybe with `UserProject.objects.get(user = self.request.user) if there is one project per user (again, just as an example).
Anyway, I am making some assumptions here, but hopefully the main question was how to set the project on the newPost and that is answered in my example.

Save a django object getting another model instance as foreign key from a form

I'm stuck trying to save an instance of a model that gets from a form an instance of another model as foreign key.
Models
class Customer(models.Model):
owner = models.ForeignKey(User)
custname = models.CharField()
class Appointment(models.Model):
user = models.ForeignKey(User)
start = models.DateTimeField()
end = models.DateTimeField()
customer = models.ForeignKey(Customer)
Form
class AppointmentForm(forms.Form):
basedate = forms.DateField()
start = forms.TimeField(widget=forms.Select())
end = forms.IntegerField(widget=forms.Select())
customer = forms.ModelMultipleChoiceField(queryset=Customer.objects.all())
The method that I'm not able to get working in a generic FormView:
def form_valid(self, form):
if form.is_valid():
appointment = Appointment()
appointment.user = self.request.user
basedate = form.cleaned_data['basedate']
start = form.cleaned_data['start']
duration = form.cleaned_data['end']
appointment.start = datetime.datetime.combine(basedate, start)
appointment.end = appointment.start + datetime.timedelta(minutes=duration)
appointment.save()
return super(AppointmentCreate, self).form_valid(form)
What should I add in the last method to read the foreign key customer from the form, and therefore pass it to the appointment? And is there any way of filtering so that in the form only appear customers belonging to the request.user?
Many thanks in advance for your help.
Something like this should work. A couple of things:
1) I changed the form field to a ModelChoiceField instead of multiple choice. You'll want to use a ModelChoiceField to show the relationship. I changed this from MultipleChoice since, according to your model, you only want to save one choice. You can read more on ModelChoiceFields here: https://docs.djangoproject.com/en/dev/ref/forms/fields/
2) In your forms, I changed the choice query to customer = forms.ModelChoiceField(queryset=Customer.objects.filter(owner=request.user). This will filter for Customers of the specific user only.
forms.py
class AppointmentForm(forms.Form):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request")
super(AppointmentForm, self).__init__(*args, **kwargs)
basedate = forms.DateField()
start = forms.TimeField(widget=forms.Select())
end = forms.IntegerField(widget=forms.Select())
customer = forms.ModelChoiceField(queryset=Customer.objects.filter(owner=request.user))
views.py
def form_valid(self, form):
if request.method=='POST':
form = AppointmentForm(request.POST, request=request)
if form.is_valid():
appointment = Appointment()
appointment.user = self.request.user
basedate = form.cleaned_data['basedate']
start = form.cleaned_data['start']
duration = form.cleaned_data['end']
appointment.customer = form.cleaned_data['customer']
appointment.start = datetime.datetime.combine(basedate, start)
appointment.end = appointment.start + datetime.timedelta(minutes=duration)
appointment.save()
return super(AppointmentCreate, self).form_valid(form)
else:
form = AppointmentForm()
Finally I did it. The key was to override the get method of FormView class in views.py, rather than modifying the init in forms.py:
forms.py:
class AppointmentForm(forms.Form):
basedate = forms.DateField()
start = forms.TimeField(widget=forms.Select())
end = forms.IntegerField(widget=forms.Select())
customer = forms.ModelChoiceField(queryset=Customer.objects.all())
...
views.py:
def get(self, request, *args, **kwargs):
"""
Handles GET requests and instantiates a blank version of the form.
"""
choices_start, choices_duration = self._get_choices()
form_class = self.get_form_class()
form = self.get_form(form_class)
form.fields['start'].widget=forms.Select(choices=choices_start)
form.fields['end'].widget=forms.Select(choices=choices_duration)
form.fields['customer'].queryset=Customer.objects.filter(owner=request.user)
return self.render_to_response(self.get_context_data(form=form))
#Dan: Many thanks for your effort in helping me out.