I've been trying to set up a basic private messaging system in Django using the generic CreateView.
I am currently having trouble with the "Receiver"/"To" field in my form. I tried to make it so it was a drop down field with the options being followers of the logged-in user.
Currently, the field is populating with the correct usernames (in this case, "testuser1") but it is throwing an error saying this field needs to be populated with an instance of the User object.
ValueError: Cannot assign "'testuser1'": "Message.reciever" must be a "User" instance.
Is there a way to have the form pass in the object of the username that is selected?
Model:
class Message(models.Model):
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sender")
reciever = models.ForeignKey(User, on_delete=models.CASCADE, related_name="reciever")
subject = models.CharField(max_length=128, default="-")
content = models.TextField()
send_date = models.DateTimeField(default=timezone.now)
User Relationships Model:
class UserRelationships(models.Model):
user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name="following")
following_user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name="followers")
created = models.DateTimeField(auto_now_add=True)
UPDATED Form:
class MessageCreateForm(forms.ModelForm):
class Meta:
model = Message
fields = ['sender', 'reciever', 'subject', 'content']
widgets = {'sender': forms.HiddenInput()}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
follower_objects = kwargs.pop('follower_objects')
super(MessageCreateForm, self).__init__(*args, **kwargs)
self.fields['reciever'] = RecieverModelChoiceField(queryset=User.objects.filter(username__in=follower_objects))
View:
class MessageCreateView(LoginRequiredMixin, CreateView):
model = Message
template_name = 'message/compose.html'
form_class = MessageCreateForm
def get_initial(self):
initial = super().get_initial()
initial['sender'] = self.request.user
return initial
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
user = self.request.user
followers = user.followers.values_list('user_id', flat=True)
follower_objects = []
kwargs['user'] = self.request.user
kwargs['follower_objects'] = follower_objects
for id in followers:
follower = User.objects.get(id=id)
follower_objects.append(follower)
return kwargs
def form_valid(self, form):
form.instance.user = self.request.user
return super().form_valid(form)
You have to use forms.ModelChoiceField instead of forms.ChoiceField
ForeignKey (model) > ModelChoiceField (form) - Default widget: Select
ModelChoiceField has attribute queryset.
You can filter field reciever.queryset directly in MessageCreateForm.__init__ method.
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
super(MessageCreateForm, self).__init__(*args, **kwargs)
self.fields['reciever'].queryset = user.followers
UPDATE:
You can set a custom ModelChoiceField that will return any label you want (more info).
from django.forms import ModelChoiceField
class RecieverModelChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return obj.username
or
def __init__(self, *args, **kwargs):
....
self.fields['reciever'].label_from_instance = lambda obj: "%s" % obj.username
Related
I have a Campaign Model, and a CampaignCreateForm which is a ModelForm. The Campaign model has a contact_list field of type JSONField. When a user is creating a campaign using the CampaignCreateForm they upload a CSV file which is processed to create the JSON data for the contact_list field.
What is the best way to approach this so that I can test the form separately from the view?
I've built this using a CampaignCreateView which inherits from CreateView, and included the logic to parse the CSV file and create the JSON data in the views form_valid method, but this makes unit-testing the form (and any form field validation) impossible. I want to test the functions included in the forms clean method. With my current approach the view and form must be tested together, and that feels wrong.
How can I create the form such that all the logic for the form (processing the CSV file to create the JSON data and discarding the uploaded file) is handled in the form only?
My current CreateView and ModelForm are below:
View:
class CampaignCreateView(LoginRequiredMixin, CreateView):
model = Campaign
form_class = CampaignCreateForm # required if you want to use a custom model form, requires `model` also
template_name = "writing/campaign_create.html"
def get_success_url(self):
""" If model has get_absolute_url() defined, then success_url or get_success_url isnt neccessary
"""
user = User.objects.get(username=self.kwargs.get("username"))
return reverse("writing:campaigns", kwargs={"username": user.username})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update({"user": self.request.user})
return kwargs
def form_valid(self, form):
""" by default, form_valid redirects to success_url
"""
form.instance.user = self.request.user
form.instance.image_url = f"https://picsum.photos/seed/{randrange(10000)}/500/300"
file = form.cleaned_data["contact_list_file"]
file_content = file.open("r")
json_contact_list = csv_to_json(file_content)
form.instance.contact_list = json_contact_list
contact_list = json.loads(json_contact_list) # as python dict
form.instance.items = len(contact_list)
response = super().form_valid(form)
log_campaign_progress(pk=form.instance.pk, status="t2h-created", stage="campaign")
enqueue_handwriting_generation(campaign_pk=form.instance.pk)
return response
Form:
class CampaignCreateForm(forms.ModelForm):
contact_list_file = forms.FileField(required=True)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop(
"user"
) # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole
super().__init__(*args, **kwargs)
class Meta:
model = Campaign
fields = ("name", "message", "contact_list_file")
def clean(self):
# Cammpaign name is unique for the user
try:
Campaign.objects.get(name=self.cleaned_data["name"], user=self.user)
except Campaign.DoesNotExist:
pass
else:
self.add_error(
"name",
ValidationError(
_("You've already created a campaign with this name"), code="BadCampaignName"
),
)
# if an error is attached to a field then the field is removed from cleaned_data
self = check_message_length(self)
self = check_message_text_is_valid(self)
self = check_file_is_valid(self)
self = check_message_tags_exist_in_contact_list(self)
return self.cleaned_data
Model:
# TimeStampedModel inherits form models.Model
class Campaign(TimeStampedModel):
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
)
order = models.ForeignKey(
Order,
on_delete=models.SET_NULL,
null=True,
)
name = models.CharField(max_length=80)
notes = models.CharField(max_length=2000, null=False, blank=True)
message = models.TextField(
max_length=1800,
null=False,
blank=False,
)
contact_list = models.JSONField(null=True)
items = models.PositiveIntegerField(null=False, blank=False)
purchased = models.BooleanField(default=False, null=False)
def __str__(self):
return self.name
class Meta:
ordering = ["-modified"]
unique_together = ("user", "name")
... more fields
In my ModelForm, I am filtering the project_users to a certain set of all Users. How can I customize the checkboxes to show a users first_name and last_name?
Currently showing their email address as the checkbox label.
models.py
class Project(models.Model):
project_business_profile = models.ForeignKey(BusinessProfile, on_delete=models.CASCADE)
project_users = models.ManyToManyField(User, related_name='project_users')
...
def __str__(self):
return str(self.project_name)
views.py
class EditProject(LoginRequiredMixin, View):
login_url = '/signin'
redirect_field_name = 'signin'
def get(self, request, project_id):
...
form = EditProjectForm(instance=project)
...
forms.py
class EditProjectForm(ModelForm):
project_users = forms.ModelMultipleChoiceField(
widget = forms.CheckboxSelectMultiple,
queryset = User.objects.none()
)
class Meta:
model = Project
fields = ['project_users']
def __init__(self, *args, **kwargs):
super(EditProjectForm, self).__init__(*args, **kwargs)
current_project = self.instance
current_business = current_project.project_business_profile
users = current_business.business_users.all()
self.fields['project_users'].queryset = current_business.business_users.all()
// Spits out the correct users however I need to access other user fields of User in template. Name etc
template
{{form.as_p}}
I'm not entirely sure I understand, do you just want to change the label which shows your user? If so something like this may work:
forms.py
class EditProjectForm(ModelForm):
project_users = forms.ModelMultipleChoiceField(
widget = forms.CheckboxSelectMultiple,
queryset = User.objects.none()
)
class Meta:
model = Project
fields = ['project_users']
def __init__(self, *args, **kwargs):
super(EditProjectForm, self).__init__(*args, **kwargs)
current_project = self.instance
current_business = current_project.project_business_profile
users = current_business.business_users.all()
self.fields['project_users'] = UserChoiceField(queryset=users)
// Spits out the correct users however I need to access other user fields of User in template. Name etc
class UserChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
user = User.objects.get(id=obj.id)
return user.get_full_name()
I have a model StaffProfile.while creating a form for Visiti want to get staff_user data(Based on current user) to that ChoiceField (to_meet).
models.py
class StaffProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE,
related_name="user_profile")
staff_user = models.ManyToManyField(User, null=True, blank=True,
related_name="staff_user")
class Visit(models.Model):
name = models.CharField(max_length=200, name="name")
gender = models.CharField(choices=GENDER_CHOICE, max_length=1, name="gender")
mobile = models.CharField(max_length=18, default="", name="mobile")
to_meet = models.ForeignKey(User, on_delete=models.CASCADE)
forms.py
class VisitForm(forms.ModelForm):
to_meet = forms.ChoiceField(choices=[], required=False, label="Select Staff")
class Meta:
model = Visit
fields = ("__all__")
def __init__(self, *args, **kwargs):
super(VisitForm, self).__init__(*args, **kwargs)
self.fields['to_meet'].choices = StaffProfile.objects.filter(user=request.user).values_list("staff_user")
Initially override the __init__() method of your view
class VisitForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(VisitForm, self).__init__(*args, **kwargs)
self.fields['to_meet'].choices = [self.request.user]
to_meet = forms.ChoiceField(choices=[], required=False, label="Select Staff")
class Meta:
model = Visit
fields = "__all__" # small typo here
Then, in your view,
def foo_view(request):
# if this is a POST request we need to process the form data
if request.method == 'POST':
# create a form instance and populate it with data from the request:
form = VisitForm(request.POST,request=request)
# check whether it's valid:
if form.is_valid():
# do somrthing
.....
You need to pass the request from view to form. For example:
def some_view(request):
form = VisitForm(request=request)
# rest of the code
and use it in the form:
def __init__(self, *args, **kwargs):
request = kwargs.pop('request')
super(VisitForm, self).__init__(*args, **kwargs)
self.fields['to_meet'].choices = StaffProfile.objects.filter(user=request.user).values_list("staff_user")
I recently was able to figure out how to do this in the CreateView, but the same is not working for the UpdateView (Here's the original post on how to do it in the CreateView: (Django) Limited ForeignKey choices by Current User)
Essentially, I need it to display only the Universes created by the currently logged in user, but by default, it displays all universes.
When I try to set a form_class and have it mimic the solution for CreatView, it spits out an improperly configured error.
models.py:
class Universe(models.Model):
user = models.ForeignKey(User,related_name='universe',on_delete=models.CASCADE)
name = models.CharField(max_length=100, unique=True)
description = models.TextField(max_length=2000,blank=True,default="")
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('universe:singleuniverse',kwargs={'pk': self.pk})
class Meta:
ordering = ['name']
unique_together = ['user','name']
class Character(models.Model):
user = models.ForeignKey(User,related_name='characters',on_delete=models.CASCADE)
universe = models.ForeignKey("story_universe.Universe", on_delete=models.CASCADE)
name = models.CharField(max_length=255,unique=True)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('character_developer:singlecharacter',kwargs={'pk': self.pk})
class Meta():
ordering = ['name']
unique_together=['user','name']
views.py:
class UpdateCharacter(LoginRequiredMixin,generic.UpdateView):
model = Character
fields = ('universe','name')
template_name = 'character_developer/character_update_form.html'
UPDATE
The error was:
Error: ImproperlyConfigured at /characters/update/3/
UpdateCharacter is missing a QuerySet. Define UpdateCharacter.model, UpdateCharacter.queryset, or override UpdateCharacter.get_queryset().
and here's what the code looked like to get the error:
views.py:
class UpdateCharacter(LoginRequiredMixin,generic.UpdateView):
template_name = 'character_developer/character_update_form.html'
form_class = UpdateForm
def get_form_kwargs(self):
kwargs = super(UpdateCharacter,self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
forms.py
class UpdateForm(forms.ModelForm):
def __init__(self,*args,**kwargs):
user = kwargs.pop('user')
super(UpdateForm,self).__init__(*args,**kwargs)
self.fields['universe'].queryset = Universe.objects.filter(user=user)
class Meta:
model = Character
fields = ('universe','name')
I believe you need the following in your views.py (almost an exact extension of your CreateCharacter):
class UpdateCharacter(LoginRequiredMixin, generic.UpdateView):
model = Character
template_name ='character_developer/character_create.html'
form_class = UpdateForm
def get_form_kwargs(self):
kwargs = super(UpdateCharacter,self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self,form):
self.object = form.save(commit=False)
self.object.user = self.request.user
self.object.save
return super().form_valid(form)
I would caveat the above - make sure that the user instance in the request object is that of the currently logged in user, and has sufficient permission to update their own user - and can't somehow make a request on their behalf.
I want to upload multiple images.
class IssuePanel(models.Model):
issue = models.ForeignKey(ComicIssue, on_delete=models.CASCADE)
panel = models.FileField(upload_to='comic_issues_files/panels/')
date_uploaded = models.DateTimeField(auto_now_add=True)
After following the examples on django-multiupload's repository on github, I have this on forms.py
class PanelsForm(forms.ModelForm):
class Meta:
model = ComicIssue
fields = ('issue', 'issue_title', 'issue_cover', 'issue_description', 'issue_cover', 'issue_file')
panels = MultiFileField(min_num=1, max_num=20, max_file_size=2048*2048*5)
def save(self, commit=False):
instance = super(PanelsForm, self).save()
for each in self.cleaned_data['panels']:
IssuePanel.objects.create(panel=each, issue=instance)
return instance
views.py
class ComicIssueCreate(LoginRequiredMixin, CreateView):
model = ComicIssue
slug_field = 'comicseries_id'
form_class = PanelsForm
def form_valid(self, form):
obj = form.save(commit=False)
obj.title = ComicSeries.objects.get(id=self.kwargs['pk'])
obj.user = self.request.user
obj.save()
return redirect('comics:series_detail', pk=obj.title.id, slug=obj.title.slug)
urls.py
url(r'^comic/issue/(?P<pk>[0-9]+)/add/$', views.ComicIssueCreate.as_view(), name='comic-issue-add'),
However, I get this error
IntegrityError at /comic/issue/21/add/
NOT NULL constraint failed: comics_comicissue.title_id
class ComicIssue(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE,
null=True, blank=True, verbose_name='Uploaded by: '
)
title = models.ForeignKey(ComicSeries, on_delete=models.CASCADE, verbose_name='Series Title')
issue = models.CharField(verbose_name='Issue Number', max_length=500)
issue_title = models.CharField(verbose_name='Issue Title', max_length=1000)
issue_cover = models.ImageField(verbose_name='Issue cover', upload_to='comic_issues', height_field=None, width_field=None, max_length=None)
issue_description = models.TextField(verbose_name='Description')
issue_file = models.FileField(verbose_name='Issue file', upload_to='comic_issues_files', max_length=100,
help_text='File in pdf or as single image', null=True, blank=True
)
date_added = models.DateTimeField(auto_now_add=True, null=True)
is_favorite = models.BooleanField(default=False)
issue_slug = models.SlugField(default='')
class Meta:
verbose_name = 'Comic Issue'
verbose_name_plural = 'Comic Issues'
def __str__(self):
return '{}: {} issue number - {}'.format(self.title.title, self.issue_title, self.issue)
def save(self, *args, **kwargs):
self.issue_slug = slugify(self.issue_title)
super(ComicIssue, self).save(*args, **kwargs)
def get_absolute_url(self):
return reverse('comics:issue_detail', kwargs={'issue_slug':self.issue_slug,'pk': self.pk})
Could this function in the ComicIssue model be a problem since it is also highlighted on the error page:
def save(self, commit=False, *args, **kwargs):
self.issue_slug = slugify(self.issue_title)
super(ComicIssue, self).save(*args, **kwargs)
I am passing the title_id from the url. It is working on other models just not this one. How do I save the foreign key?
You're saving your form in the form's save method regardless of the commit value. The first line is instance = super(PanelsForm, self).save() which will try to save a ComicIssue instance even though in your view, you wrote obj = form.save(commit=False).
You can do two things: pass the title and user to your form at init so the form can handle assigning those during save. Or change your form's save method to:
def save(self, commit=False):
instance = super(PanelsForm, self).save(commit=commit)
if commit:
for each in self.cleaned_data['panels']:
IssuePanel.objects.create(panel=each, issue=instance)
return instance
And then your view needs to call the form's save() method twice (note that the form's instance is passed by reference, so changing it in the view also changes the form's instance):
def form_valid(self, form):
obj = form.save(commit=False)
obj.title = ComicSeries.objects.get(id=self.kwargs['pk'])
obj.user = self.request.user
form.save()
return redirect('comics:series_detail', pk=obj.title.id, slug=obj.title.slug)