I have a Workshop Django app. Each Workshop can have multiple attendees via a related WorkshopAttendee model (simplified below). I am using Django's ModelForm in a Class Based View, and in forms.py I am using crispy-forms:
models.py (relevant parts)
class Workshop(models.Model):
title = models.CharField(max_length=100)
information = models.TextField()
location = models.TextField()
class WorkshopAttendee(models.Model):
workshop = models.ForeignKey(Workshop)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
views.py (relevant parts)
from django.views.generic.edit import FormView
from workshop.forms import WorkshopAttendeeForm
class WorkshopAttendeeFormView(FormView):
def form_valid(self, form):
# Clean the data
form_data = form.cleaned_data
form.save(commit=False)
return super(WorkshopAttendeeFormView, self).form_valid(form)
forms.py
from django.forms import ModelForm
from workshop.models import WorkshopAttendee
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, HTML, Field, \
Fieldset, Button, Hidden, Submit, Reset
from crispy_forms.bootstrap import FormActions
# Create the form class
class WorkshopAttendeeForm(ModelForm):
def __init__(self, *args, **kwargs):
# Crispy form Layouts, Fieldsets, etc
super(WorkshopAttendeeForm, self).__init__(*args, **kwargs)
class Meta:
model = WorkshopAttendee
urls.py (relevant parts)
urlpatterns = patterns('',
url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[\w\-.]+)/(?P<action>[\w\-.]+)/$',
WorkshopAttendeeFormView.as_view(
form_class=WorkshopAttendeeForm,
success_url="success/",
template_name='workshop/workshop_registration.html',
), name='workshop-attendee-register'
),
url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[\w\-.]+)/$',
WorkshopDetailView.as_view(
context_object_name='workshop_detail',
queryset=Workshop.objects.select_related(),
template_name='workshop/workshop_detail.html',
), name='workshop-detail'
),
)
My question is, how can I seed the form with the workshop_id (i.e. the FK relation) in a hidden form field? Obviously because the form hasn't been submitted yet, there is no FK relation yet. But in the URL I have the kwarg of Workshop slug. I can hard-code the workshop_id as a Hidden() crispy-forms field on a per workshop basis, but this is totally unDRY. Any ideas? I don't think I can use the select_related() or prefetch_related() methods on the model in urls.py, so maybe I have to somehow get both models into the form view somehow?
I don't feel like this is an edge-case scenario, and I am sure someone else has had a similar app workflow.
Thanks in advance.
UPDATE
After further research it appears that I can do this, using Django Formsets. Not figured out exactly how yet..... hints welcome.
You don't need to pass the PK - you got it already in your URL as the slug.
So let's say this is your URL: http://example.com/workshops/awesome-workshop-slug/sign_in/
Your urls.py should look like this:
url(r'^workshop/(?P<workshop_slug>\w+)/sign_in/$',
# ../workshops/awesome-workshop-slug/sign_in/
view = 'workshop_signin',
name = 'workshop-signin',
),
Ok, in your views.py you're able to do this:
#login_required
def workshop_signin(request, workshop_slug, template='workshop/sign_in.html'):
"""Register user to workshop."""
form = WorkshopForm()
workshop = Workshop.objects.filter(slug=workshop_slug)[0]
if request.method == 'POST':
form = WorkshopForm(request.POST, instance=workshop)
if form.is_valid():
messages.info(request, 'Yay!')
kwargs = {
'workshop_form': form,
}
return render_to_response(template, kwargs, context_instance=RequestContext(request))
*untested quick and dirty code
Turns out I was overly complicating this.
All I needed to do was modify the form_valid method for Django's GCBV FormView to this:
workshop/views.py
from django.views.generic.edit import FormView
from workshop.models import Workshop, WorkshopAttendee
from workshop.forms import WorkshopAttendeeForm
from django.http import HttpResponseRedirect
class WorkshopAttendeeFormView(FormView):
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.workshop = Workshop.objects.filter(slug=self.kwargs['slug'])[0]
self.object.save()
return HttpResponseRedirect(self.get_success_url())
Which basically does not save the form on submit, but instead overrides it and first updates the object it is about to save (the WorkshopAttendee object) with the relevant Workshop (based on the Workshop slug field, which is unique), then saves the updated object (self.object.save) and kicks me to the success url.
Big thanks to #init3 for his helpful pointers. Much appreciated.
Related
I want to add / remove members from a Team model. Members are specified as a ManyToManyField. I use django-rules to specify permissions, so team owners should be able to add/remove members.
# models.py
from django.db import models
from rules.contrib.models import RulesModel
from django.conf import settings
class Team(RulesModel):
name = models.CharField(max_length=80)
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
help_text="Owner can view, change or delete this team.",
related_name="team_owner",
)
members = models.ManyToManyField(
settings.AUTH_USER_MODEL, blank=True, related_name="team_members"
)
The permissions are specified as following:
import rules
#rules.predicate
def is_team_owner(user, obj):
return obj.owner == user
rules.add_perm("teamapp.change_team", is_team_owner)
I've specified some generic views (CreateView, DetailView, UpdateView and DeleteView) to manage the Team. Now I want two separate views to add and remove members on the same.
# views.py
from django.views.generic import (
CreateView,
DetailView,
UpdateView,
ListView,
DeleteView,
)
from rules.contrib.views import PermissionRequiredMixin
from django.contrib.auth import get_user_model
from .models import Team
class TeamMemberAddView(PermissionRequiredMixin, UpdateView):
model = Team
permission_required = "teamapp.change_team"
raise_exception = True
fields = ["members"]
def form_valid(self, form):
user = get_user_model()
new_member = user.objects.get(pk=1)
self.object.members.add(new_member)
return super(TeamMemberAddView, self).form_valid(form)
Which generic view can I use to add / remove members? Which approach is recommended here? I wanted 1 dedicated view to select an existing User to be added, and some links on the list view to delete members. My approach fails, because it does not add members, it only updates to the last User selected. So the ManyToMany table only contains one record.
TL;DR: replace the last line of form_valid by return HttpResponseRedirect(self.get_success_url())
It's important to understand how form_valid of UpdateView works. I recommend to visualize the methods on ccbv.co.uk.
From ModelFormMixin:
If the form is valid, save the associated model.
def form_valid(self, form):
"""If the form is valid, save the associated model."""
self.object = form.save()
return super().form_valid(form)
It means that the object will be saved with the data submitted by the form. UpdateView will restrict the changes to the fields variable:
fields = ["members"]
From FormMixin:
If the form is valid, redirect to the supplied URL.
def form_valid(self, form):
"""If the form is valid, redirect to the supplied URL."""
return HttpResponseRedirect(self.get_success_url())
For your concrete case (add a many-to-many relationship), you need to bypass the model saving from ModelFormMixin by simply returning the supplied URL after adding the relationship (last line changed):
def form_valid(self, form):
user = get_user_model()
new_member = user.objects.get(pk=1)
self.object.members.add(new_member)
return HttpResponseRedirect(self.get_success_url())
Side note: your form seems to provide the member object you want to add, so you could use this instead of including it in the url. Try:
def form_valid(self, form):
for member in form.cleaned_data['members'].all():
self.object.members.add(member.id)
return HttpResponseRedirect(self.get_success_url())
I am new to Django and using this project to learn it. I am able to save the Journal record but the many to many relationship does not work.
This 'create' view displays the correct form including the multi-select box with all of the cryptos listed (from Crypto model). When submitting the form the many-to-many records do not save but the Journal saves fine.
I have found a bunch of different answers to this, some are for python 2.7, but this is the simplest method based on the [Django documentation][1]. Any help is greatly appreciated.
Also, the relationship works fine in the Admin section, so I am thinking it has something to do with the Forms and/or the View & saving.
models.py
from django.db import models
from crypto.models import Crypto as CryptoModel
class Journal(models.Model):
title = models.CharField(max_length=200, help_text='Journal Title', blank=False, null=False)
content = models.TextField(max_length=2000, help_text='Journal Content (HTML OK)', blank=False, null=False)
crypto_id = models.ManyToManyField(CryptoModel, blank=True)
created = models.DateTimeField(help_text='Created', auto_now_add=True, null=True)
def __str__(self):
return self.title ## String for representing the Model object, usually name field or title
forms.py
from django.forms import ModelForm, ModelMultipleChoiceField, widgets
from journal.models import Journal as JournalModel
from crypto.models import Crypto as CryptoModel
class JournalForm(ModelForm):
# select multiple items box
cryptos = ModelMultipleChoiceField(widget=widgets.SelectMultiple(attrs={'size': 30}), queryset=CryptoModel.objects.all())
class Meta:
model = JournalModel
fields = [
"title",
"content",
]
labels = {
'title': 'Journal Title',
}
required = [
"title", # same as model
"content", # same as model
]
views.py
from journal.forms import JournalForm
from django.utils import timezone
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render, redirect, get_object_or_404
from journal.models import Journal as JournalModel
def Create(request):
if request.method == "POST":
form = JournalForm(request.POST) # form instance
context = {'form': form} # if errors, keep the form data on next page load
journal = form.save(commit=False) # False needed for many-to-many
journal.title = form.cleaned_data["title"]
journal.content = form.cleaned_data["content"]
journal.created = timezone.now()
journal.save() # save the form journal data, now we have a PK
form.save_m2m() # save the 'form' using ManytoMany method
return HttpResponseRedirect('/journal/')
form = JournalForm()
context = {'form': form}
return render(request, 'journal/create.html', context)
models.py 2
from django.db import models
from crypto.models import Crypto
class Journal(models.Model):
title = models.CharField(max_length=200, help_text='Journal Title', blank=False, null=False)
content = models.TextField(max_length=2000, help_text='Journal Content (HTML OK)', blank=False, null=False)
crypto_id = models.ManyToManyField(Crypto, blank=True)
created = models.DateTimeField(help_text='Created', auto_now_add=True, null=True)
def __str__(self):
return self.title ## String for representing the Model object, usually name field or title
forms.py 2
from django.forms import ModelForm, ModelMultipleChoiceField, widgets
from journal.models import Journal
from crypto.models import Crypto
class JournalForm(ModelForm):
# select multiple items box
cryptos = ModelMultipleChoiceField(widget=widgets.SelectMultiple(attrs={'size': 30}), queryset=Crypto.objects.all())
class Meta:
model = JournalModel
fields = [
"title",
"content",
"cryptos",
]
views.py 2
from journal.forms import JournalForm
from django.utils import timezone
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render, redirect, get_object_or_404
from journal.models import Journal
def Create(request):
if request.method == "POST":
form = JournalForm(request.POST) # form instance
context = {'form': form} # if errors, keep the form data on next page load
journal = form.save(commit=False) # False needed for many-to-many
journal.created = timezone.now()
journal.save() # save the form journal data, now we have a PK
journal.crypto_id.set(form.cleaned_data.get("cryptos")) # must be after "save"
form.save_m2m() # save the 'form' using ManytoMany method
return HttpResponseRedirect('/journal/')
form = JournalForm()
context = {'form': form}
return render(request, 'journal/create.html', context)
hope this solve your problem just but this line after save your journal instance
journal.crypto_id.set(form.cleaned_data.get("cryptos"))
You've called your model and form fields different things; Django can't know they relate to the same field. The form name - crypos - is the correct one, you should rename your model field to that.
Also, you haven't specified the field in the fields list, so Django won't even try to set it on the model.
Note that in your view you don't need to set title or content, that's what form.save does for you already.
Thank you abdullah, adding "journal.crypto_id.set(form.cleaned_data.get("cryptos"))" to the VIEW fixed the issue.
An additional note is that this must be places after the 'journal' form is saved but before the many to many is saved.
I updated the "models.py 2", "forms.py 2" and "views.py 2" section above. This is the working code.
you are always welcome.
yes but it after journal.save().
and set don't need to call save() from form.
The dropdown list appears correctly in the html, However I am unable to figure out why I run into the same error time after time when I try to submit / .
"Select a valid choice. That choice is not one of the available choices."
the problem context
I have two models defined in Django. One CourseModel database to hold all the offered courses and one registration database to link a course to a user.
models.py
from django.db import models
# Create your models here.
class CourseModel(models.Model):
course = models.CharField(max_length=100)
date = models.DateField(max_length=100)
time = models.TimeField()
location = models.CharField(max_length=100)
datetime = models.DateTimeField()
class RegistrationModel(models.Model):
name = models.CharField(max_length=100)
adress = models.CharField(max_length=100)
city = models.CharField(max_length=100)
email = models.EmailField(max_length=100)
course = models.ForeignKey('self', on_delete=models.CASCADE)
def __str__(self):
return self.name
I use modelForm to create a registration form, where the user can subscribe for a course from a dropdown list.
forms.py
from django.forms import ModelForm, RegexField
from home.models import RegistrationModel, CourseModel
from django import forms
import datetime
class RegistrationForm(ModelForm):
def __init__(self, *args, **kwargs):
super(RegistrationForm, self).__init__(*args, **kwargs)
self.fields['course'].queryset = CourseModel.objects.exclude(date__lt=datetime.datetime.today()).values_list('datetime', flat=True)
self.fields['course'].empty_label = None
class Meta:
model = RegistrationModel
fields = '__all__'
views.py
from django.shortcuts import render, redirect
from home.forms import RegistrationForm
from .models import CourseModel
import datetime
def home(request):
return render(request, 'home/home.html')
def registration(request):
if request.method == 'POST':
form = RegistrationForm(request.POST)
crs = request.POST.get('course')
print(crs)
if form.is_valid():
cleanform = form.save(commit=False)
cleanform.course = crs
cleanform.save()
return redirect('home')
else:
form = RegistrationForm()
return render(request, 'home/registration.html', {'form': form})
In the RegistrationForm's __init__() method, your self.fields['course'].queryset = ...values_list('datetime', flat=True) returns datetime instances. See values_list() docs.
I believe this may cause the issue. I guess the queryset should return CourseModel instances, based on the Django docs:
ForeignKey is represented by django.forms.ModelChoiceField, which is a ChoiceField whose choices are a model QuerySet.
Also, your RegistrationModel.course field has a foreign key to 'self' instead of the CourseModel. Not sure if that is what you want.
Other examples of setting the field queryset can be found here.
I would like to seek assistance and guidance to my problem.
I have the following models:
class myinfo(models.Model):
name = models.CharField(max_length=30, null=True)
class mynumbers(models.Model):
fkey = models.ForeignKey("myinfo")
Job_Position = models.CharField(max_length=30, null=True)
The mynumbers model is dynamically generated via django-dynamic-formset.
My form
class info(ModelForm):
name= forms.CharField( max_length=20)
class Meta:
model = APPLICANT_DATA
fields = ('name',)
class numbers(ModelForm):
number = forms.CharField( max_length=20)
class Meta:
model = APPLICANT_DATA
fields = ('number',)
If you want to save your dynamic form fields you have to do this in views
for field in formset:
field.save()
My views:
def index(request):
aformset = formset_factory(numbers)
formset = aformset(request.POST)
form = info(request.POST)
if request.method == 'POST':
if form.is_valid():
if formset.is_valid():
for field in formset:
formset.save()
form.save()
But the problem starts when my dynamically generated field has a foreign key(mynumbers) which raises an error must be a myinfo instance. How would I save the 2 forms where mynumbers has a foriegn key to myinfo? Is there a better way to what I did? Thank you in advance,
This is where inlineformset_factory would be used. This allows you to have a parent model and a number of child models (related w/ parent via a foreignkey) and save them together. There are many arguments that can be passed to the inlineformset_factory in order to customize behavior (such as the minimum and maximum number of inline forms allowed, whether the user can delete inline forms, etc.) but the crux is shown below.
# views.py
from django.forms.models import inlineformset_factory
from my_app.forms import numbers as NumberForm
from my_app.forms import info as InfoForm
from my_app import models
myFormset = inlineformset_factory(models.myinfo,
models.mynumbers,
form=NumberForm
)
def index(request):
if request.POST:
form = InfoForm(request.POST)
if form.is_valid():
info = form.save(commit=False)
formset = myFormset(request.POST, instance=info)
if formset.is_valid():
info.save()
formset.save()
return HttpResponse('saved successfully')
else:
form = InfoForm()
formset = myFormset(instance=models.myinfo())
return render_to_response("recipes/submit.html", {
"form": form,
"formset":formset,
},
context_instance=RequestContext(request))
Please note: In your question, you typed for field in formset: formset.save(). A formset is a collection of forms, not a collection of fields.
Formsets can be tricky and require the template rendered correctly with additional template components that are not part of regular forms (such as the management_form variable that allows for Django to properly process what has been added/deleted/moved/changed). It's definitely worth doing some tutorials in order to get an idea of best practices so that you don't go down a troubleshooting rabbithole w/ your custom implementation.
I suggest this post from Charles Leifer as a good entry to get acquainted with the basics.
I have a model:
class Article(models.Model):
text = models.CharField()
author = models.ForeignKey(User)
How do I write class-based view that creates a new model instance and sets author foreign key to request.user?
Update:
Solution moved to separate answer below.
I solved this by overriding form_valid method. Here is verbose style to clarify things:
class CreateArticle(CreateView):
model = Article
def form_valid(self, form):
article = form.save(commit=False)
article.author = self.request.user
#article.save() # This is redundant, see comments.
return super(CreateArticle, self).form_valid(form)
Yet we can make it short (thanks dowjones123), this case is mentioned in docs.:
class CreateArticle(CreateView):
model = Article
def form_valid(self, form):
form.instance.author = self.request.user
return super(CreateArticle, self).form_valid(form)
I just stumbled into this problem and this thread led me in the right direction (thank you!). Based on this Django documentation page, we can avoid calling the form's save() method at all:
class CreateArticle(LoginRequiredMixin, CreateView):
model = Article
def form_valid(self, form):
form.instance.author = self.request.user
return super(CreateArticle, self).form_valid(form)
Berislav's code in views.py doesn't work for me. The form is rendered as expected, with the user value in a hidden input, but the form is not saved (I don't know why). I have tried a slightly different approach, that works for me:
views.py
from django.views.generic import *
from myapp.forms import ArticleForm
from myapp.models import Article
class NewArticleView(CreateView):
model = Article
form_class = ArticleForm
def get_initial(self):
return {
"user": self.request.user
}
You should set up a CreateView using a ModelForm for that model. In the form definition, you set the ForeignKey to have the HiddenInput widget, and then use the get_form method on the view to set the value of your user:
forms.py:
from django import forms
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
widgets = {"user": forms.HiddenInput()}
views.py:
from django.views.generic import *
from myapp.forms import ArticleForm
from myapp.models import Article
class NewArticleView(CreateView):
model = Article
form_class = ArticleForm
def get_form(self, form_class):
initials = {
"user": self.request.user
}
form = form_class(initial=initials)
return form
There are answers that are mainly related to the User model foreign key. However, let's suppose a simple scenario in which there is a model Comment containing a foreign key of the Article model, and you need to have a CreateView for Comment where each comment will have a foreign key of the Article model. In that case, the Article id would probably be in the URL, for example, /article/<article-id>/comment/create/. Here is how you can deal with such a scenario
class CommentCreateView(CreateView):
model = Comment
# template_name, etc
def dispatch(self, request, *args, **kwargs):
self.article = get_object_or_404(Article, pk=self.kwargs['article_id'])
return super(CommentCreateView, self).dispatch(request, *args, **kwargs)
def form_valid(self, form):
form.instance.article= self.article # if the article is not a required field, otherwise you can use the commit=False way
return super(CommentCreateView, self).form_valid(form)