Django: edit existing object, but new on save - django

In a blog-ish part of my django app, editing an existing entry results in creating a new entry. It should modify the existing entry.
I have a simple model:
class BlogEntry(models.Model):
...
slug = models.SlugField(unique=True)
and a simple form for editing it:
class BlogEntryForm(forms.ModelForm):
...
slug = forms.CharField(required=False, widget=forms.HiddenInput())
class Meta:
model = BlogEntry
fields = (..., 'slug')
and the view, somewhat simplified, is also straight-forward:
class BlogEditView(View):
#method_decorator(login_required)
def get(self, request, slug=None):
context = {
'user': request.user,
}
if slug is None:
print('Creating new entry.')
context['entry'] = BlogEntryForm()
context['entry'].publication_date = datetime.datetime.now()
return render(request, 'blog/edit.html', context)
print('Using existing entry.')
entry = get_object_or_404(BlogEntry, slug=slug)
context['entry'] = BlogEntryForm(instance=entry)
return render(request, 'blog/edit.html', context)
#method_decorator(login_required)
def post(self, request):
blog_entry_form = BlogEntryForm(request.POST)
if blog_entry_form.is_valid():
blog_entry = blog_entry_form.save(commit=False)
if blog_entry.slug is None or blog_entry.slug == '':
print('New entry, slug is empty.')
blog_entry.creation_date = datetime.datetime.now()
blog_entry.slug = slugify(blog_entry.title) + '_' + hex(random.randint(0, 1e10))
blog_entry.author = request.user
blog_entry.save()
return redirect(reverse('blog/article', args=[blog_entry.slug]))
...
In get(), I confirm with the print's that I am taking the correct branch. If the entry exists, I set entry with the instance. In post(), however, I always take the new entry branch.
The layout has the typical hidden element loop.
{% for hidden in form.hidden_fields %}
{{ hidden }} hidden
{% endfor %}
though, suspiciously, when I look at the served html, I don't see the slug entry, so it's no surprise that it's not getting passed through.
Anyone see what I'm missing?

You should add the slug argument to the post() method as well. This will allow you to get the blog entry to edit from the database and pass this entry to the form as an instance argument:
#method_decorator(login_required)
def post(self, request, slug=None):
blog_entry = BlogEntry.objects.filter(slug=slug).first()
blog_entry_form = BlogEntryForm(request.POST, instance=blog_entry)
...
UPDATE: To pass the slug argument to the post() method you should use the empty action attribute of the <form> tag:
<form action="" method="POST">
In this case the form will be submitted to the url from which it was loaded. So the slug argument for the POST request will be the same as for the GET.

Related

using CKEditor with Django and an inlineformset_factory with empty_form

When I render the empty form it is not attaching any media to it. i.e. the CKEditor is not displayed. The element looks like it is missing the css/js - it's like it doesn't get set up properly.
Note : the other sections are displayed correctly.
Where to start? Problem with Django's Empty Form method? Problem with CKEditor? Me :)
<div class="container">
<button type ="button" class="btn-info btn-lg" id="add_section">Add Section</button>
{{form.media }}
{{form|crispy }}
{{sections.media}}
<div>
{{sections.empty_form}}
</div>
<div id = 'section_management'> {{ sections.management_form }} </div>
{% for section in sections %}
{{ section|crispy }}
{% endfor %}
<button class="btn btn-info ml-2" type="submit">Update</button>
Cancel
</div>
Here's my Forms
class SectionForm(forms.ModelForm):
content = RichTextFormField()
class Meta:
model = Section
fields = ('content',)
empty_permitted=True
def __init__(self, *args, **kwargs):
print('section form called')
super().__init__(*args, **kwargs)
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ('title','category','span')
def __init__(self, *args, **kwargs):
self.is_superuser = kwargs.pop('is_superuser', None)
super().__init__(*args, **kwargs)
if self.is_superuser == False:
self.fields.pop("span")
view code
class ArticleUpdateView(LoginRequiredMixin,UserPassesTestMixin,UpdateView):
template_name = 'articles/ArticleUpdate.html'
form_class = ArticleForm
model = Article
SectionFormSet = inlineformset_factory(Article, Section, form=SectionForm, extra=0, can_delete=False, fields=('content',))
#if i always pass back at least 1 extra section form, I can grab the html for it in Jquery */
#if i do not pass back extra=0 how would i get the html in jquery for the extra form?
def test_func(self):
article = self.get_object()
if self.request.user == article.author or self.request.user.is_superuser :
return True
else:
return False
def get_context_data(self, **kwargs):
print('get context data called update view')
'''
section_form
'''
context = super().get_context_data(**kwargs)
if self.request.POST:
context['sections'] = self.SectionFormSet(self.request.POST,instance=self.object)
else:
context['sections'] = self.SectionFormSet(instance=self.object)
return context
def get_section_form(self): #we know we can access this in the template
return SectionForm()
def save_sections(self):
print('save sections called update view')
try:
context = self.get_context_data()
section_form = context['sections']
if section_form.is_valid():
# section_form.instance = self.object #if im passing instance in the factory, do I need it here to?
section_form.save()
except Exception as e:
print('failed to save section: ' + str(e))
def form_valid(self, form):
print('form valid called update view')
form.instance.author = self.request.user
response = super().form_valid(form) #save article form
self.save_sections()
return response
def get_success_url(self):
return reverse_lazy('index')
Basically, what I've done so far to overcome this problem is by accessing the form directly from the template, bypassing the inlineFormSet to get an empty form....(hope that makes sense).
I go directly to the view :
{{view.get_section_form}}
with this method in the view
def get_section_form(self): #we know we can access this in the template
return SectionForm()
I have subsequently found out I can do this in the template as well :
{{sections.media}}
{{sections.form}}
The above also passes an empty form - with the media filled in- as long as you pass the model form into the factory to start of with.
These are work-arounds for me currently, but would appreciate a proper answer as to why empty_form doesn't work properly.
My further investigation into this was basically comparing what is returned via accessing the formset to return an empty form, or using the modelForm directly.
Django docs :
empty_formĀ¶
BaseFormSet provides an additional attribute empty_form which returns a form instance with a prefix of __prefix__ for easier use in dynamic forms with JavaScript.
If you replace prefix on the generated html -- everything works. No idea why. You can replace it with anything, i.e. prefix1
at which point CKEditor starts to display the formset correctly.

Django Python Forms - submitting complex view with multiple forms and formsets

Imagine this concept,
I have a Taxi that can be ordered by a group for a full day multiple visits, and I should assign a group leader for each booking.
now I have a Booking (PNR) that holds Clients traveling Routes, and a Group Leader (Operator) assigned for that booking.
my view holds these:
form for selecting the operator
formset to add clients
in this view I'm trying to make it easier for the user by giving the ability to save each form separately by ajax or save all data of forms by a button at the bottom of the view.
I've been searching for a few days and I got the nearest approach on these two linkes 1 & 2 but still can't make my code run correctly and do what it's supposed to do. :( ANY SUPPORT WILL BE HIGHLY APPRECIATED!
My models.py:
class Operator (models.Model):
name = models.CharField(max_length=50)
# Other Fields
def __str__(self):
return self.code
class PNR (models.Model):
date_created = models.DateTimeField(auto_now_add=True)
# Other Fields
def __str__(self):
return self.pk
class Client (models.Model):
related_pnr = models.ForeignKey(PNR, on_delete=models.CASCADE)
name = models.CharField(max_length=200)
# Other Fields
def __str__(self):
return self.related_pnr+" "+self.name
My forms.py:
class ChooseOperatorCode(forms.Form):
operator = forms.ModelChoiceField(queryset=Operator.objects.all())
def clean(self, *args, **kwargs):
operator = self.cleaned_data.get('operator')
return super(ChooseOperatorCode, self).clean(*args, **kwargs)
class NewClientForm(forms.Form):
name = forms.CharField(widget=forms.TextInput(attrs={}), label=False, max_length=200)
# Other Fields
def clean(self, *args, **kwargs):
name = self.cleaned_data.get('name')
# Other Fields
return super(NewClientForm, self).clean(*args, **kwargs)
My views.py:
#login_required
def create_pnr(request):
pnr = PNR.objects.create(created_by_user=request.user)
choose_operator_form = ChooseOperatorCode(request.POST or None)
if choose_operator_form.is_valid():
pnr.created_by_operator = choose_operator_form.cleaned_data.get('operator')
pnr.save()
choose_operator_form.save()
clients_form = NewClientForm(request.POST or None)
if clients_form.is_valid():
client = Client()
client.related_pnr = pnr.pk
client.name = clients_form.cleaned_data.get('name')
client.save()
clients_form.save()
context = {
'pnr': pnr.pk,
'choose_operator_form': choose_operator_form,
'clients_form': clients_form,
}
return render(request, 'reservation_new.html', context)
#login_required
def edit_pnr(request, pnr_id):
pnr = PNR.objects.get(id=pnr_id)
choose_operator_form = ChooseOperatorCode(request.POST or None)
if choose_operator_form.is_valid():
pnr.created_by_operator = choose_operator_form.cleaned_data.get('operator')
pnr.save()
clients_form = NewClientForm(request.POST or None)
if clients_form.is_valid():
client = Client()
client.related_pnr = pnr.pk
client.name = clients_form.cleaned_data.get('name')
client.save()
clients_form.save()
context = {
'pnr': pnr.pk,
'choose_operator_form': choose_operator_form,
'clients_form': clients_form,
}
return render(request, 'reservation_edit.html', context)
My url.py:
path('operation/reservation/new/', views.create_pnr, name='new_pnr'),
path('operation/reservation/existing/<int:pnr_id>/', views.edit_pnr,
name='existing_pnr'),
And Finally my template.html: (for both new and edit)
<form method="POST" action="{% url 'existing_pnr' pnr_id=pnr %}" id="choose_operator_form">
{% csrf_token %}
{{choose_operator_form}}
</form>
<form method="POST" action="{% url 'existing_pnr' pnr_id=pnr %}" id="clients_form">
{% csrf_token %}
{{clients_form}}
</form>
<script type="javascript">
$(document).('submit', '#choose_operator_form', function(e){
e.preventDefault();
$.ajax({
type:'POST',
url:"{% url 'existing_pnr' %}",
data: $('#choose_operator_form').serialize(),
success: function (result) {
// show success msg
},
error: function (data) {
// show error msg
}
});
});
//same code for clients form
</script>
The pnr doesn't get added to the request object, so request.pnr makes no sense. You need to pass the id of the PNR through the URL, that way it can be accessed in the view:
In urls.py, make sure that the url for your edit_pnr view gets the pnr_id: something like path('pnr/<int:pnr_id>/edit/', ..., name="existing_pnr").
In your template, construct the url for editing a pnr like this: {% url 'existing_pnr' pnr_id=pnr %}
In your view, receive the pnr_id: def edit_pnr(request, pnr_id): ...
Now you can fetch the PNR that's being edit like this: pnr = get_object_or_404(PNR, pk=pnr_id) which will correctly return a 404 Not Found if someone tries to access a non-existing PNR.

Django: how to validate inlineformset against another related form

I have looked around quite a bit but can't quite figure out how to make this work. I basically have a Document form and an Item inlineformset in a view, and I need to perform some validation dependent on the field values in each form. For example, if the Item's copyright_needed field is YES then the Document's account field is required.
How can I pass a reference to the Document form, so that inside ItemForm's clean method, I can look at the Document form's cleaned_data? I'm trying to use curry, as I've seen recommended in other SO answers, but it's not working quite right.
Models.py
class Document(models.Model):
account = models.CharField(max_length=22, blank=True, null=True)
class Item(models.Model):
copyright_needed = models.CharField(max_length=1)
# Document foreign key
document = models.ForeignKey(Document)
It's the ItemForm clean method that shows what I'd like to accomplish, and the error I'm getting.
Forms.py -- EDIT - added init to ItemForm
class ItemForm(forms.ModelForm):
class Meta:
model = Item
fields=[..., 'copyright_needed' ]
def __init__(self, *args, **kwargs):
self.doc_form = kwargs.pop('doc_form')
super(ItemForm, self).__init__(*args, **kwargs)
def clean(self):
cleaned_data = super(ItemForm, self).clean()
msg_required = "This field is required."
cr = cleaned_data.get("copyright_needed")
# This line generates this error: DocumentForm object has no attribute cleaned_data
acct_num = self.doc_form.cleaned_data.get("account")
if cr and cr == Item.YES:
if not acct_num:
self.doc_form.add_error("account", msg_required)
return cleaned_data
class DocumentForm(forms.ModelForm):
...
account = forms.CharField(widget=forms.TextInput(attrs={'size':'25'}), required=False)
class Meta:
model = Document
fields = [ ..., 'account' ]
views.py
def create_item(request):
# create empty forms
form=DocumentForm()
ItemFormSet = inlineformset_factory(Document, Item,
form=ItemForm,
can_delete=False,
extra=1 )
# This is my attempt to pass the DocumentForm to each ItemForm, but its not working
ItemFormSet.form = staticmethod(curry(ItemForm, doc_form=form))
item_formset=ItemFormSet(instance=Document())
if request.POST:
d = Document()
form=DocumentForm(request.POST, instance=d)
if form.is_valid():
new_document=form.save(commit=False)
item_formset=ItemFormSet(request.POST, instance=new_document)
if item_formset.is_valid():
new_document.save()
new_item=item_formset.save()
return HttpResponseRedirect(...)
item_formset=ItemFormSet(request.POST)
return render(request,...)
I'm not even sure what the view is doing - it looks like you're confused on the role of the inlineformset and curry. Firstly, you're currying the init method of ItemForm with the doc_form, but you haven't written an init.
Secondly, it looks like you want to be able to edit the Items inside the Document form. So you need the modelformset_factory, and pass in a custom Formset, on which you write a clean method, that has access to everything you need.
from django.forms.models import modelformset_factory
ItemFormSet = modelformset_factory(Item, form=ItemForm, formset=MyCustomFormset)
then in your customformset -
class MyCustomFormset(BaseInlineFormset):
def clean():
super(MyCustomFormset, self).clean()
for form in self.forms:
#do stuff
Note the clean method on each ItemForm has already been called - this is similar to writing your own clean() on a normal modelform.
EDIT:
OK, so ignore the formset clean, I misunderstood. Just make your document form in the view, pass it along with the formset, then put them all in the same form tag.
<form method="post" action=".">
{%for field in doc_form %}
{{field}}
{%endfor%}
{%for form in formset%}
{{form.as_p}}
{%endfor%}
</form>
Then you have access to all the fields in your request.POST, and you can do whatever you want
doc_form = DocumentForm(request.POST)
formset = ItemFormSet(request.POST)
if all([doc_form.is_valid(), formset.is_valid()]):
#do some stuff

'str' object has no attribute 'visible_fields'; django class based views

I've been struggling with class views all day after starting on them yesterday. My issue is constantly getting 'str' object has no attribute 'visible_fields', so the 'form' item below is not really a form:
template-
<form action="" method="post">
{% csrf_token %}
{{form|bootstrap}}
<input type="submit" name="submit" value="Add new article"/>
</form>
view-
class ArticleCreateView(CreateView):
model = Article
template_name = 'index/add_article.html'
form_class = ArticleForm
def post(self, request, *args, **kwargs):
article_form = self.get_form()
if article_form.is_valid():
article = article_form.save(commit=False)
title = article_form.cleaned_data['title']
url = article_form.cleaned_data['url']
title = process_title(url)
article.title = title
article.save()
return redirect("index:article_list")
else:
form = ArticleForm()
print type(form)
print dir(self)
return render(request, 'index/add_article.html')
The worst part is printing type(form) shows it is <class 'index.forms.ArticleForm'>. I'm trying to just have it redirect to the list view if the form saved, and replay the form with the error (You already have an article with that URL) if the form is bad. I heard class views are easier to work with and huge projects I've read through use them, but they really seem worse than the old views. I assume that's because I'm not using them well
Every example I've seen has a template getting a "form" somehow, like
class RangeCreateView(CreateView):
model = Range
template_name = 'dashboard/ranges/range_form.html'
form_class = RangeForm
def get_success_url(self):
if 'action' in self.request.POST:
return reverse('dashboard:range-products',
kwargs={'pk': self.object.id})
else:
msg = render_to_string(
'dashboard/ranges/messages/range_saved.html',
{'range': self.object})
messages.success(self.request, msg, extra_tags='safe noicon')
return reverse('dashboard:range-list')
def get_context_data(self, **kwargs):
ctx = super(RangeCreateView, self).get_context_data(**kwargs)
ctx['title'] = _("Create range")
return ctx
then like magic in range_form.html:
{% include "dashboard/partials/form_fields.html" with form=form %}
My issue here is I need to process the title of the form, with
def process_title(url):
def _search_for_title(url):
try:
r = requests.get(url)
content = r.text
t = html.document_fromstring(content)
return t.find(".//title").text
except IOError:
return None
title = _search_for_title(url)
return title or 'None'
This kind of ruins the purpose of a class based view. It seems I should be processing the title by overriding 'clean' in the form itself?
Otherwise, how can I make this view pass a form object, render it in the template, and just re-render the template if the form didn't pass?
And how can I access the form in the template?
Thank you
You do it by following the example view, rather than overriding post instead as you are doing.
In your case, simply displaying and processing a form is the default behaviour of a CreateView. So there is no need to override any methods at all. Your view should just be:
class ArticleCreateView(CreateView):
model = Article
template_name = 'index/add_article.html'
form_class = ArticleForm

Django: Search form in Class Based ListView

I am trying to realize a Class Based ListView which displays a selection of a table set. If the site is requested the first time, the dataset should be displayed. I would prefer a POST submission, but GET is also fine.
That is a problem, which was easy to handle with function based views, however with class based views I have a hard time to get my head around.
My problem is that I get a various number of error, which are caused by my limited understanding of the classed based views. I have read various documentations and I understand views for direct query requests, but as soon as I would like to add a form to the query statement, I run into different error. For the code below, I receive an ValueError: Cannot use None as a query value.
What would be the best practise work flow for a class based ListView depending on form entries (otherwise selecting the whole database)?
This is my sample code:
models.py
class Profile(models.Model):
name = models.CharField(_('Name'), max_length=255)
def __unicode__(self):
return '%name' % {'name': self.name}
#staticmethod
def get_queryset(params):
date_created = params.get('date_created')
keyword = params.get('keyword')
qset = Q(pk__gt = 0)
if keyword:
qset &= Q(title__icontains = keyword)
if date_created:
qset &= Q(date_created__gte = date_created)
return qset
forms.py
class ProfileSearchForm(forms.Form):
name = forms.CharField(required=False)
views.py
class ProfileList(ListView):
model = Profile
form_class = ProfileSearchForm
context_object_name = 'profiles'
template_name = 'pages/profile/list_profiles.html'
profiles = []
def post(self, request, *args, **kwargs):
self.show_results = False
self.object_list = self.get_queryset()
form = form_class(self.request.POST or None)
if form.is_valid():
self.show_results = True
self.profiles = Profile.objects.filter(name__icontains=form.cleaned_data['name'])
else:
self.profiles = Profile.objects.all()
return self.render_to_response(self.get_context_data(object_list=self.object_list, form=form))
def get_context_data(self, **kwargs):
context = super(ProfileList, self).get_context_data(**kwargs)
if not self.profiles:
self.profiles = Profile.objects.all()
context.update({
'profiles': self.profiles
})
return context
Below I added the FBV which does the job. How can I translate this functionality into a CBV?
It seems to be so simple in function based views, but not in class based views.
def list_profiles(request):
form_class = ProfileSearchForm
model = Profile
template_name = 'pages/profile/list_profiles.html'
paginate_by = 10
form = form_class(request.POST or None)
if form.is_valid():
profile_list = model.objects.filter(name__icontains=form.cleaned_data['name'])
else:
profile_list = model.objects.all()
paginator = Paginator(profile_list, 10) # Show 10 contacts per page
page = request.GET.get('page')
try:
profiles = paginator.page(page)
except PageNotAnInteger:
profiles = paginator.page(1)
except EmptyPage:
profiles = paginator.page(paginator.num_pages)
return render_to_response(template_name,
{'form': form, 'profiles': suppliers,},
context_instance=RequestContext(request))
I think your goal is trying to filter queryset based on form submission, if so, by using GET :
class ProfileSearchView(ListView)
template_name = '/your/template.html'
model = Person
def get_queryset(self):
name = self.kwargs.get('name', '')
object_list = self.model.objects.all()
if name:
object_list = object_list.filter(name__icontains=name)
return object_list
Then all you need to do is write a get method to render template and context.
Maybe not the best approach. By using the code above, you no need define a Django form.
Here's how it works : Class based views separates its way to render template, to process form and so on. Like, get handles GET response, post handles POST response, get_queryset and get_object is self explanatory, and so on. The easy way to know what's method available, fire up a shell and type :
from django.views.generic import ListView if you want to know about ListView
and then type dir(ListView). There you can see all the method defined and go visit the source code to understand it. The get_queryset method used to get a queryset. Why not just define it like this, it works too :
class FooView(ListView):
template_name = 'foo.html'
queryset = Photo.objects.all() # or anything
We can do it like above, but we can't do dynamic filtering by using that approach. By using get_queryset we can do dynamic filtering, using any data/value/information we have, it means we also can use name parameter that is sent by GET, and it's available on kwargs, or in this case, on self.kwargs["some_key"] where some_key is any parameter you specified
Well, I think that leaving validation to form is nice idea. Maybe not worth it in this particular case, because it is very simple form - but for sure with more complicated one (and maybe yours will grow also), so I would do something like:
class ProfileList(ListView):
model = Profile
form_class = ProfileSearchForm
context_object_name = 'profiles'
template_name = 'pages/profile/list_profiles.html'
profiles = []
def get_queryset(self):
form = self.form_class(self.request.GET)
if form.is_valid():
return Profile.objects.filter(name__icontains=form.cleaned_data['name'])
return Profile.objects.all()
This is similar to #jasisz 's approach, but simpler.
class ProfileList(ListView):
template_name = 'your_template.html'
model = Profile
def get_queryset(self):
query = self.request.GET.get('q')
if query:
object_list = self.model.objects.filter(name__icontains=query)
else:
object_list = self.model.objects.none()
return object_list
Then all you have to do on the html template is:
<form method='GET'>
<input type='text' name='q' value='{{ request.GET.q }}'>
<input class="button" type='submit' value="Search Profile">
</form>
This has been explained nicely on the generic views topic here Dynamic filtering.
You can do filtering through GET, I don't think you can use POST method for this as ListView is not inherited from edit mixings.
What you can do is:
urls.py
urlpatterns = patterns('',
(r'^search/(\w+)/$', ProfileSearchListView.as_view()),
)
views.py
class ProfileSearchListView(ListView):
model = Profile
context_object_name = 'profiles'
template_name = 'pages/profile/list_profiles.html'
profiles = []
def get_queryset(self):
if len(self.args) > 0:
return Profile.objects.filter(name__icontains=self.args[0])
else:
return Profile.objects.filter()
I think that the error you are getting is because your form doesn't require the name field. So, although the form is valid, the cleaned_data for your name field is empty.
These could be the problematic lines:
if form.is_valid():
self.show_results = True
self.profiles = Profile.objects.filter(name__icontains=form.cleaned_data['name'])
If I were you, I would try changing the line:
self.profiles = Profile.objects.filter(name__icontains=form.cleaned_data['name'])
to this:
self.profiles = Profile.objects.none()
If you stop receiving errors (and your template receives an empty object_list), the problem you have is what I said before: name field not required.
Let us know if this doesn't work!
Search on all fields in model
class SearchListView(ItemsListView):
# Display a Model List page filtered by the search query.
def get_queryset(self):
fields = [m.name for m in super(SearchListView, self).model._meta.fields]
result = super(SearchListView, self).get_queryset()
query = self.request.GET.get('q')
if query:
result = result.filter(
reduce(lambda x, y: x | Q(**{"{}__icontains".format(y): query}), fields, Q())
)
return result
def get_queryset(self):
query_name = self.request.GET.get('query', '')
object_list = Product.objects.filter(
Q(title__icontains=query_name)
)
return object_list
<form action="" method="GET">
{% csrf_token %}
<input type="text" name="query" placeholder="Search keyword">
<i class="ti-search"></i>
</form>