Django custom Form and GenericView - django

The following code is working but I wonder if there is a more elegant way of doing. I have to pass the specie_id so I can filter the breeds to the corresponding specie. I can pass the specie_id to the view but I also have the information in the Resident model ("specie").
both get() and post() have nearly the same code, passing the specie_id.
The View
class ResidentUpdate(UpdateView):
model = Resident
template_name = "administration/resident_form.html"
form_class = ResidentCreateForm
def get(self, request, pk):
initial = self.model.objects.get(id=pk)
form = self.form_class(instance=initial, specie_id=initial.specie.id)
return render(request, self.template_name, {"form": form})
def post(self, request, pk):
initial = self.model.objects.get(id=pk)
form = self.form_class(request.POST, specie_id=initial.specie.id, instance=initial)
if form.is_valid():
form.save()
return redirect("resident_detail", pk)
return render(request, self.template_name, {"form", form})
The Form
class ResidentCreateForm(ModelForm):
class Meta:
model = Resident
fields = [
"name",
"specie",
"breed",
"gender",
"gender_status",
"birth_date",
"organization",
"social_behaviors",
"notes",
]
widgets = {
"birth_date": DateInput(attrs={"class": "flatpickr"}),
"specie": HiddenInput(),
}
def __init__(self, *args, **kwargs):
self.specie_id = kwargs.pop("specie_id", None)
super(ResidentCreateForm, self).__init__(*args, **kwargs)
self.fields["specie"].initial = self.specie_id
self.fields["breed"].queryset = Breed.objects.for_specie(self.specie_id)
EDIT :
#Alasdair's answer is good and I think I perfected it a little more. My form is used for the create view too. So I added a check to see if I have the specie_id in kwargs (create) or if I have to use the specie_id from the instance (update)
def __init__(self, *args, **kwargs):
self.specie_id = kwargs.pop("specie_id", None)
super(ResidentForm, self).__init__(*args, **kwargs)
if not self.specie_id:
self.specie_id = self.instance.specie.id
self.fields["specie"].initial = self.specie_id
self.fields["breed"].queryset = Breed.objects.for_specie(self.specie_id)

It looks like you can do self.fields["breed"].queryset = Breed.objects.for_specie(self.initial.specie_id), then you don't need to pass in specie_id to the form.
class ResidentCreateForm(ModelForm):
class Meta:
model = Resident
fields = [
"name",
"specie",
"breed",
"gender",
"gender_status",
"birth_date",
"organization",
"social_behaviors",
"notes",
]
widgets = {
"birth_date": DateInput(attrs={"class": "flatpickr"}),
}
def __init__(self, *args, **kwargs):
super(ResidentCreateForm, self).__init__(*args, **kwargs)
self.fields["breed"].queryset = Breed.objects.for_specie(self.instance.specie_id)
Note I've removed the specie hidden input above, I don't think it's necessary.
The UpdateView takes care of passing instance to the form, so you can simplify the view.
from django.urls import reverse
class ResidentUpdate(UpdateView):
model = Resident
template_name = "administration/resident_form.html"
form_class = ResidentCreateForm
def get_success_url(self):
"""Redirect to resident_detail view after a successful update"""
return reverse('resident_detail', args=[self.kwargs['pk']]

I think a better approach would be overriding the get_form_kwargs method in your views.
def get_form_kwargs(self):
form_kwargs = super().get_form_kwargs()
form_kwargs.update({'specie_id': self.kwargs.get('specie_id')})
return form_kwargs

Related

Django - redefine Form Select using pk URL

I need to set input (select) value using pk sended by URL.
Using __init__ in form, almost get the answer, but __init__ method is executed twice and clean my value.
Form:
class CrearDelitoForm(forms.ModelForm):
class Meta:
model = Delito
exclude = ()
def __init__(self, numero_pk = None, *args, **kwargs):
super(CrearDelitoForm, self).__init__(*args, **kwargs)
self.fields["imputado"].queryset = Imputado.objects.filter(numero_id = numero_pk)
DelitoFormset = inlineformset_factory(
Expediente,
Delito,
form=CrearDelitoForm,
extra=1,
can_delete=True,
fields=('imputado', 'delito', 'categoria'),
}
)
Views:
class CrearDelito(CreateView):
model = Delito
form_class = CrearDelitoForm
template_name = 'crear_delito.html'
def get_context_data(self,**kwargs):
context = super().get_context_data(**kwargs)
context['formset'] = DelitoFormset()
context['expedientes'] = Expediente.objects.filter(id = self.kwargs['pk'])
return context
def get_form_kwargs(self, **kwargs):
kwargs['numero_pk'] = self.kwargs['pk']
return kwargs
**
System check identified no issues (0 silenced).
June 08, 2020 - 11:33:57
Django version 2.2.12, using settings 'red.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
, ]>
**
I think problem is on context = super().get_context_data(**kwargs) but don't know why.
A CreateView makes a form, the form you specified in the form_class. It will furthermore pass the request.POST and request.FILES to the form, you thus do not have to construct a formset yourself, but let the CreateView do that.
You need to pass the dictionary to the form_kwargs to the form_kwargs of the formset, so the get_form_kwargs needs to be altered to:
class CrearDelito(CreateView):
model = Delito
form_class = DelitoFormset
template_name = 'crear_delito.html'
def get_context_data(self,**kwargs):
context = super().get_context_data(**kwargs)
context['expedientes'] = Expediente.objects.filter(id=self.kwargs['pk'])
context['formset'] = context['form']
return context
def get_form_kwargs(self, **kwargs):
form_kwargs = super().get_form_kwargs(**kwargs)
form_kwargs['form_kwargs'] = {'numero_pk': self.kwargs['pk']}
form_kwargs['instance'] = Expediente.objects.get(id=self.kwargs['pk'])
return form_kwargs
def get_success_url(self):
return reverse('repositorio:crear_victima', args=[request.POST.get('numero_id')])

ModelForm object has no attribute 'fields'

I am using a django (3.0) ModelMultipleChoice field for a form. I am trying to modify the queryset to make some restrictions on it.
here is the views :
def nouvelle_tache(request,id_livrable):
livrable=Livrable.objects.get(pk=id_livrable)
projet = livrable.projet
if request.method == "POST":
form = NouvelleTache(request.POST,projet=projet)
tache = form.save(commit=False)
tache.livrable = livrable
tache.id_tache = livrable.id_derniere_tache() + Decimal(0.01)
tache.save()
form.save_m2m()
etat = Temps_etat_tache(etat=form.cleaned_data['etat_initial'],tache=tache)
etat.save()
return redirect('tache',tache.pk)
else:
form = NouvelleTache(projet=projet)
return render(request, 'application_gestion_projets_AMVALOR/nouvelle_tache.html', locals())
And the forms :
class NouvelleTache(forms.ModelForm):
def __init__(self, *args, **kwargs):
projet = kwargs.pop('projet', None)
queryset = Utilisateur.objects.all()
for utilisateur in projet.utilisateurs:
queryset = queryset.exclude(pk=utilisateur.pk)
self.fields['ressources'].queryset = queryset
super(NouvelleTache, self).__init__(*args, **kwargs)
ressources= forms.ModelMultipleChoiceField(queryset=Utilisateur.objects.all() ,widget =forms.CheckboxSelectMultiple )
etat_initial = forms.ModelChoiceField(queryset=Etat_tache.objects.none())
class Meta:
model = Tache
fields = ['libelle']
I have the followig error : 'NouvelleTache' object has no attribute 'fields'
I don't understand why because many other users seems to have similar code and it works.
Any help would be appreciate.
super(NouvelleTache, self).__init__(*args, **kwargs)
needs to be executed first, as the fields are set in the super class:
def __init__(self, *args, **kwargs):
projet = kwargs.pop('projet', None)
queryset = Utilisateur.objects.all()
for utilisateur in projet.utilisateurs:
queryset = queryset.exclude(pk=utilisateur.pk)
super(NouvelleTache, self).__init__(*args, **kwargs)
self.fields['ressources'].queryset = queryset

Passing and accessing value in Django form

I'm trying to pass info to my form and I have a bit of a struggle with that. My code looks as follows:
views.py
class ObjectUpdateView(UpdateView):
template_name = 'manage/object_form.html'
form_class = ObjectEditForm
def get_success_url(self):
#...
def form_valid(self, form):
return super(ObjectUpdateView, self).form_valid(form)
def get_object(self):
return get_object_or_404(Room, pk=self.kwargs['object_id'])
def get_form_kwargs(self, **kwargs):
objectid = self.kwargs['object_id']
object = Object.objects.get(id = objectid)
container = object.container
kwargs['container_id'] = container.id
return kwargs
forms.py
class ObjectEditForm(forms.ModelForm):
class Meta:
model = Object
fields = ['TestField']
def __init__(self, *args, **kwargs):
super(ObjectEditForm, self).__init__(*args, **kwargs)
self.Container_id = kwargs.pop('container_id')
form_page.html
{{fomr.kwarg.Container_id}}
As you can see I'd like to access Container_id value in my form_page.html. Unfortunately, nothing is there. What I also noticed, that with __init__ I had to add, now values are empty in my form. Before I added __init__ all values were properly passed (well, except Container_id).
Could you recommend how I can pass such value to be accessed in the form template?
You can render this with:
{{ form.Container_id }}
In your form you should first pop the container_id from the kwargs, like:
class ObjectEditForm(forms.ModelForm):
class Meta:
model = Object
fields = ['TestField']
def __init__(self, *args, **kwargs):
# first pop from the kwargs
self.Container_id = kwargs.pop('container_id', None)
super(ObjectEditForm, self).__init__(*args, **kwargs)
Use the context over the form
That being said, it is a bit strange that you pass this to the form, and not add this to the context data. You can simplify your view a lot to:
class ObjectUpdateView(UpdateView):
template_name = 'manage/object_form.html'
pk_url_kwarg = 'object_id'
form_class = ObjectEditForm
def get_success_url(self):
#...
def get_context_data(self, **kwargs):
objectid = self.kwargs['object_id']
object = Object.objects.get(id = objectid)
context = super().get_context_data()
context.update(container_id=object.container_id)
return context
Django automatically fetches a single element based on the pk_url_kwarg [Django-doc]. You only need to set it correctly, so here that is the object_id.
In that case, we can simply render this with:
{{ container_id }}
and you do not need to store this in the form.

How to inject same context in many different Django views?

I want to put info about one object in many views without repeating it in get_context_data in each view. As u understand i need a class with get_context_data inside, that i can mix with other views.
Here in my example i want to see 'some_object' in context of UpdateAnotherObjectView:
class BaseObjectInfoView(View):
def get_context_data(self, **kwargs):
context_data = super(BaseObjectInfoView, self).get_context_data(**kwargs)
context_data['some_object'] = SomeObjects.objects.get(pk=1)
return context_data
class UpdateAnotherObjectView(BaseObjectInfo, UpdateView):
template_name = 'create_object.html'
form_class = AnotherObjectForm
model = AnotherObjects
def get_context_data(self, **kwargs):
context_data = super(UpdateAnotherObjectView, self).get_context_data(**kwargs)
context_data['all_another_objects'] = AnotherObjects.objects.all()
return context_data
it works, but get_context_data is not a part of parent 'View' class. May be i need more special class to inherit from in BaseObjectInfoView?
or maybe better to construct context with another method ?
Mixins don't need to be views, but it helps IDE's if they have the methods they're overriding.
Contexts are handled by django.views.generic.base.ContextMixin (details on this very handy site), So the class-based views way of things would be this:
from django.views import generic
class WebsiteCommonMixin(generic.base.ContextMixin):
page_title = ''
active_menu = None
def get_context_data(self, **kwargs):
context = super(WebsiteCommonMixin, self).get_context_data(**kwargs)
context.update(dict(page_title=self.page_title, active_menu=self.active_menu))
return context
class NewsListView(WebsiteCommonMixin, ListView):
page_title = 'News list'
active_menu = 'News'
model = News
paginate_by = 12
I do this for many projects and the simple views you have to create anyway, are fully declarative. And by simple, I mean that they can consist of mulitple mixins, all doing the hard stuff in either get_queryset, get_context_data or form_valid. More elaborate example, straight from a project:
class FeedbackMixin(object):
message = 'Well Done!'
def __init__(self):
self._message_kwargs = {}
super().__init__()
def add_message_kwarg(self, name, value) -> None:
self._message_kwargs[name] = value
def format_message(self, kwargs) -> str:
return self.message.format(**kwargs)
def generate_message(self) -> None:
msg = self.format_message(self._message_kwargs)
messages.success(getattr(self, 'request'), msg)
class ModelFeedbackMixin(FeedbackMixin, generic.edit.ModelFormMixin):
success_view_name = None
success_url_kwargs = None
def get_success_url_kwargs(self):
return self.success_url_kwargs
def get_success_url(self) -> str:
success_url_kwargs = self.get_success_url_kwargs()
if not self.success_view_name:
url = super().get_success_url()
elif success_url_kwargs is not None:
url = reverse(self.success_view_name, kwargs=success_url_kwargs)
else:
if hasattr(self.object, 'slug'):
url_kwargs = {'slug': self.object.slug}
else:
url_kwargs = {'pk': self.object.pk}
url = reverse(self.success_view_name, kwargs=url_kwargs)
return url
def form_valid(self, form):
response = super().form_valid(form)
self.generate_message()
return response
Maybe this way could be easier to read...
def add_context(func):
# this is a wrapper function
def wrapper(*args, **kwargs):
context_data = func(*args, **kwargs)
context_data['some_object'] = SomeObjects.objects.get(pk=1)
return context_data
return wrapper
class UpdateAnotherObjectView(BaseObjectInfo, UpdateView):
template_name = 'create_object.html'
form_class = AnotherObjectForm
model = AnotherObjects
#add_context
def get_context_data(self, **kwargs):
kwargs['all_another_objects'] = AnotherObjects.objects.all()
return kwargs

Django generic create view default value for model choice field

I have a view that inherits from the generic CreateView and overrides the get_initial method like so:
class PosterVisualCreateView (ModelFormMixin, generic.edit.CreateView, ObjectClassContextMixin):
model = Poster
valid_message = "Successfully created object."
template_name = "poser/create_poster_visual.html"
def get_form_class (self):
return super(PosterVisualCreateView, self).get_form_class(extra="CreateVisual")
def get_form_kwargs (self):
kwargs = super(PosterVisualCreateView, self).get_form_kwargs()
kwargs.update({
"company": self.request.company
})
return kwargs
def get_context_data (self, **kwargs):
context = super(PosterVisualCreateView, self).get_context_data(**kwargs)
context.update({
"company": self.request.company,
})
return context
def get_initial (self):
initial = super(PosterVisualCreateView, self).get_initial()
initial.update({
"company": self.request.company,
"template": self.request.company.template_set.all().first()
})
return initial
def form_valid(self, form):
success_url = super(PosterVisualCreateView, self).form_valid(form)
attributes = form.instance.create_attributes()
for attribute in attributes:
attribute.poster = form.instance
attribute.save()
form.instance.save()
form.instance.render_html(commit=True)
form.instance.save()
return success_url
#method_decorator(login_required)
def dispatch (self, *args, **kwargs):
return super(PosterVisualCreateView, self).dispatch(*args, **kwargs)
The page renders this form:
class PosterFormCreateVisual (CompanyHiddenForm):
"""Create form for Posters."""
template = fields.ModelChoiceField(widget=forms.RadioSelect, queryset=Template.objects.all())
product = fields.ModelChoiceField(widget=forms.Select, queryset=Product.objects.all(),
required=False)
class Meta:
model = Poster
fields = ("template", "product", "company")
def __init__ (self, *args, **kwargs):
company = kwargs.pop("company", None)
assert company is not None, "Company is required to create attribute form."
super(PosterFormCreateVisual, self).__init__(*args, **kwargs)
self.fields["template"].queryset = company.template_set.all()
self.fields["product"].queryset = company.product_set.all()
The initial value should be the first item in the radio selection for template but this isn't the case, can anyone help me out here?
Try this:
"template": self.request.company.template_set.all().first()
But as far as I understand you original code should work too.
BTW, how you tested the form? By hitting the "Refresh" button or Ctrl-R/F5 key? Some browsers reload page but save the previously selected/entered form values. To check initial values you should reload the form page by clicking on the address bar (or pressing Ctrl-L) and then pressing the Enter key.