Deleting object from a view in Django - django

I have a page that shows a list of objects and I want to add a button beside each one to enable the user to delete it. I've found a few questions about similar scenarios online but for some reason I keep getting errors when I try to replicate the solutions.
Here is the delete function in views.py:
def delete_dish(request, pk):
query = Dish.objects.get_object_or_404(pk)
supermenu = query['supermenu'].pk
query.delete()
return redirect('foodmenu:menu', supermenu)
Here is the form in the HTML template:
{% if not supermenu.dish_set.count == 0 %}
<ul>
{% for dish in supermenu.dish_set.all %}
<li>
{{ dish.name }} - {{ dish.description }}
<form action="{% url 'foodmenu:delete_dish' dish.id %}" method="POST">
{% csrf_token %}
<button type="submit">X</button>
</form>
</li>
{% if not dish.price_set.count == 0 %}
<ul>
{% for price in dish.price_set.all %}
{% if price.label %}
<li>{{ price.label }}: {{ price.cost }}</li>
{% else %}
<li>{{ price.cost }}</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
{% endfor %}
</ul>
{% else %}
<p>No dishes on this menu!</p>
{% endif %}
And here is the urls.py:
app_name = 'foodmenu'
urlpatterns = [
...
path('dish/delete/<int:dish.id>', views.delete_dish, name="delete_dish")
]
When I click the button, the browser goes to ./dish/delete/1 (1 being the pk of the object), but Django returns a 404 error.

Instead of query = Dish.objects.get_object_or_404(pk) you should use get_object_or_404 shortcut:
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_POST
#require_POST
def delete_dish(request, pk):
if request.method
query = get_object_or_404(Dish, pk=pk)
supermenu = query.supermenu.pk
query.delete()
return redirect('foodmenu:menu', supermenu)
Also change your url pattern to this:
path('dish/delete/<int:pk>/', views.delete_dish, name="delete_dish")
UPD
As #daniherrera mentioned in his comment, you probably want to check request's method, to prevent accidental deletions. For this you can use require_POST decorator.

Related

Django simple search with Class based views and forms.py

I have been trying to do a variation of what Williams Vincent did on this page: https://learndjango.com/tutorials/django-search-tutorial .
I am using Django 3.2 so if there are modifications, I need to make I have not identified them. I am having some troubles.
This what I made which worked just fine.
my_search.html:
{% extends "base.html" %}
{% block body %}
{% for city in object_list %}
<li>
{{city.name}}   {{city.city_no}}
</li>
{% endfor %}
{% endblock %}
views.py:
from django.views.generic import ListView
from .models import City
class SearchResutlsView(ListView): # test version
model = City
template_name = "search_results.html"
def get_queryset(self):
return City.objects.filter(name__icontains='Boston')
Now it is time to add forms.py, but when I made the below changes to the code it does not work. What am I missing? There are no errors displayed. I get a blank html.
{% extends "base.html" %}
{% block body %}
<form class="d-flex" method='get' action="{% url 'city:search_results' %}">
{{ form }}
<button class="btn btn-outline-success" type="submit" value="qu">Search Name</button>
</form>
{% for city in city_list %}
<li>
{{city.name}}   {{city.city_no}}
</li>
{% endfor %}
{% endblock %}
forms.py
from django import forms
class SearchForm(forms.Form):
q = forms.CharField(label='Search label', max_length=50, strip=True)
views.py
from django.views.generic import FormView, ListView
from .models import City
class SearchResutlsView(FormView):
model = City
form_class = SearchForm
template_name = "city/search_results.html"
def get_queryset(self):
query = self.request.Get.get("q")
if query:
city_list = City.objects.filter(name__icontains=query)
else:
city_list = City.objects.none()
return city_list
First, Your method should be POST not get.
Second, you need to add CSRF token.
something like that:
{% extends "base.html" %}
{% block body %}
<form class="d-flex" method='post' action="{% url 'city:search_results' %}">
{% csrf_token %}
{{ form }}
<button class="btn btn-outline-success" type="submit" value="qu">Search Name</button>
</form>
{% for city in city_list %}
<li>
{{city.name}}   {{city.city_no}}
</li>
{% endfor %}
{% endblock %}
and in views.py
query = self.request.POST.get("q")

Reverse for 'about_me' not found. 'about_me' is not a valid view function or pattern name

What's happening. I still have trouble displaying the title list. Where did I make a mistake?
views.py
def dashboard(request):
bookmarks = Text_field.objects.order_by('created')
return render(request, 'Site/dashboard.html', {'bookmarks': bookmarks})
def about_me(request, pk):
about = get_object_or_404(Text_field, pk=pk)
return render(request, 'Site/about_me.html', {'about': about})
dashboard.html
{% extends "Site/base.html" %}
{% load static %}
{% block content %}
{% for bookmark in bookmarks %}
<div>
<p>Stworzono dn. {{ bookmark.created }}</p>
<h1>{{ bookmark.title }}</h1>
<p>{{ bookmark.text|linebreaksbr }}</p>
</div>
{% endfor %}
{% endblock %}
urls.py
path('', views.dashboard, name='dashboard'),
path('about/<int:pk>/', views.about_me, name='about_me'),
In your comment you say you defined an app_name in your urls.py, you need to include that app_name in your {% url ... %} name:
{% url 'Site:about_me' pk=bookmark.pk %}

No Post matches the given query

I am using Django version 2.
I am working on a blog web app using PostgreSQL database.
I am trying to add a search feature to the web app but when I open the url (http://localhost:8000/search/) to make a search, I get the error below.
Page not found (404)
Request Method: GET
Request URL: http://localhost:8000/search/
Raised by: blog.views.post_detail
No Post matches the given query.
here is the blog/urls.py
from django.contrib.postgres.search import SearchVector
from .forms import CommentForm, SearchForm
from .models import Post, Comment
from django.shortcuts import render, get_object_or_404
from django.urls import path
from . import views
urlpatterns = [
path('', views.PostListView.as_view(), name='home'),
path('<slug:post>/', views.post_detail, name='post_detail'),
path('<int:post_id>/share', views.post_share, name='share'),
path('search/', views.post_search, name='post_search'),
]
here is the views.py
def post_detail(request, post):
post = get_object_or_404(Post, slug=post)
comments = post.comments.filter(active=True)
new_comment = None
if request.method == 'POST':
comment_form = CommentForm(data=request.POST)
if comment_form.is_valid():
new_comment = comment_form.save(commit=False)
new_comment.post = post
new_comment.save()
else:
comment_form = CommentForm()
return render(request, 'detail.html', {'post': post,
'comments': comments, 'new_comment':
new_comment,'comment_form': comment_form})
def post_search(request):
form = SearchForm()
query = None
results = []
if 'query' in request.GET:
form = SearchForm(request.GET)
if form.is_valid():
query = form.cleaned_data['query']
results = Post.objects.annotate(
search=SearchVector('title','body'),
).filter(search=query)
return render(request, 'search.html',
{'form':form,'query':query,'results':results})
Here are the templates files.
For search.html -(page to make query)
{% extends 'base.html' %}
{% block title %}Search{% endblock title %}
{% block page_title %}Search Posts{% endblock page_title %}
{% block content %}
{% if query %}
<h1>Posts containing "{{ query }}"</h1>
<h3>
{% with results.count as total_results %}
Found {{ total_results }} result{{ total_results|pluralize }}
{% endwith %}
</h3>
{% for post in results %}
<h4>{{ post.title }}</h4>
{{ post.body|truncatewords:5 }}
{% empty %}
<p>There is no results for your query.</p>
{% endfor %}
<p>Search again</p>
{% else %}
<h1>Search for posts</h1>
<form action="." method="GET">
{{ form.as_p }}
<input type="submit" value="Search">
</form>
{% endif %}
{% endblock content %}
For the html for post_detail
{% extends 'base.html' %}
{% load blog_tags %}
{% load crispy_forms_tags %}
{% load profanity %}
{% block title %}{{post.title}}{% endblock title %}
{% block navlink %}
<nav>
<ul>
<li class="current">Home</li>
<li>About</li>
<li >Services</li>
</ul>
</nav>
{% endblock navlink %}
{% block page_title %}{{post.title}}{% endblock page_title %}
{% block content %}
{{post.body|markdown}}
<p>Share this post via email</p>
{% with comments.count as total_comments %}
<h2>{{ total_comments }} comment{{ total_comments|pluralize }} </h2>
{% endwith %}
<div class="container">
{% for comment in comments %}
<div class="comment">
<p>Comment {{ forloop.counter }} by <em>{{ comment.name }}</em> - {{comment.created}} </p>
{{ comment.body|censor|linebreaks }}
</div>
{% empty %}
<p>There are no comments yet.</p>
{% endfor %}
{% if new_comment %}
<h2>Your comment has been added</h2>
{% else %}
<h2>Add a new comment</h2>
<form method="post">{% csrf_token %}
{{comment_form|crispy}}
<input class="button_1" type="submit" value="Add comment">
</form>
{% endif %}
</div>
{% endblock content %}
This is probably for one of the strange reason search is here actually matches with slug:post. One of easiest solution for your problem is to re-order urls and make sure /search place before. Like following
urlpatterns = [
path('', views.PostListView.as_view(), name='home'),
path('search/', views.post_search, name='post_search'),
path('<slug:post>/', views.post_detail, name='post_detail'),
path('<int:post_id>/share', views.post_share, name='share')
]
My suggestion for further developement whenever its need to add slug in url we need to make sure there is some prefix before it.

How to redirect back to same page when errors in Django comments

How do you get Django comments to redirect back to the same page where you're filling out a comment if there are errors in the comment submission form?
So basically I have a template like this:
{% block content %}
{% render_comment_form for show %}
{% get_comment_count for show as comment_count %}
<div id="comments-count">
{% if comment_count == 0 %}
No comments yet. Be the first!
{% else %}
Number Of Comments: {{ comment_count }}
{% endif %}
</div>
{% if comment_count > 0 %}
{% render_comment_list for show %}
{% endif %}
{% endblock %}
I created my own list.html and form.html and everything looks fine. In the form.html template there is some code like this:
<ul class="form-errors">
{% for field in form %}
{% for error in field.errors %}
<li>{{ field.label }}: {{ error|escape }}</li>
{% endfor %}
{% endfor %}
</ul>
So obviously, if there is an error in the comment submission form, I would like the user to see the same page as before only with some errors displayed in the comments form. Or alternatively if this is not possible, just ignore the error and instead of transitioning to the preview.html template, it would just not save the comment and again go back to the page.
Any help? Note ideally I dont want to have to create a custom comments app. This functionality should be there already. I know there's a next variable you can pass (and I am doing this), but it only works if the comment form is successful.
you have to use HttpResponseRedirect
from django.http import HttpResponseRedirect
def comment_form(request):
error = request.GET.get('error', None)
requestDict = {'error': error}
return render_to_response('comments.html', requestDict, context_instance=RequestContext(request))
def post_comment(request):
....
your code
....
if something_goes_wrong:
HttpResponseRedirect('project/comment_form/?error=ThereisProblem')
And in template you can do this:
{If error %}
<h1>{{error}}<h1>
{%else%}
render comments...
{%endif%}
Hope this will help you :)

Display number of instances for each model in Django's admin index

I need to display number of objects at main django site admin page.
For example, in list of models I need to display
Elephants (6)
instead of
Elephants
I added this code to my model:
class Elephant(models.Model):
....
class Meta:
verbose_name_plural = 'Elephants ' + '(' + unicode(count_elephants()) + ')'
where count_elephants() calculates number of objects. The problem is that verbose_name_plural is calculated at server start and is not called when I delete/insert objects, so this calculated value becomes irrelevant.
Is it possible to do it in correct way?
Thanks!
Since verbose_name_plural is used in many other ways, a better way to do this will be to change the admin index view and admin template.
However, since the admin app can change, this is probably tied to a specific version of
django. I am attaching for example the modified admin taken from django 1.2.5.
(Note: I will use an in place replacement for the index method, but it will be probably better to subclass it instead of replacing the method)
As a start, copy from django/contrib/admin/sites.py the AdminSite.index method and it's required imports, and modify it to include counts (one line changed, look for 'THIS LINE WAS ADDED"). Add it to any of your admin.py files or somewhere else appropriate:
from django.utils.text import capfirst
from django import template
from django.shortcuts import render_to_response
from django.views.decorators.cache import never_cache
from django.utils.translation import ugettext as _
def index_with_count(self, request, extra_context=None):
"""
Displays the main admin index page, which lists all of the installed
apps that have been registered in this site.
"""
app_dict = {}
user = request.user
for model, model_admin in self._registry.items():
app_label = model._meta.app_label
has_module_perms = user.has_module_perms(app_label)
if has_module_perms:
perms = model_admin.get_model_perms(request)
# Check whether user has any perm for this module.
# If so, add the module to the model_list.
if True in perms.values():
model_dict = {
'name': capfirst(model._meta.verbose_name_plural),
'admin_url': mark_safe('%s/%s/' % (app_label, model.__name__.lower())),
'perms': perms,
'count': model.objects.count(), # THIS LINE WAS ADDED
}
if app_label in app_dict:
app_dict[app_label]['models'].append(model_dict)
else:
app_dict[app_label] = {
'name': app_label.title(),
'app_url': app_label + '/',
'has_module_perms': has_module_perms,
'models': [model_dict],
}
# Sort the apps alphabetically.
app_list = app_dict.values()
app_list.sort(lambda x, y: cmp(x['name'], y['name']))
# Sort the models alphabetically within each app.
for app in app_list:
app['models'].sort(lambda x, y: cmp(x['name'], y['name']))
context = {
'title': _('Site administration'),
'app_list': app_list,
'root_path': self.root_path,
}
context.update(extra_context or {})
context_instance = template.RequestContext(request, current_app=self.name)
return render_to_response(self.index_template or 'admin/index.html', context,
context_instance=context_instance
)
site.index = never_cache(type(site.index)(index_with_count, site, AdminSite))
Now copy the django/contrib/admin/templates/admin/index.html file into admin/index.html in any of your templates folders to override the original template and modify it to show the counts:
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% load adminmedia %}{% admin_media_prefix %}css/dashboard.css" />{% endblock %}
{% block coltype %}colMS{% endblock %}
{% block bodyclass %}dashboard{% endblock %}
{% block breadcrumbs %}{% endblock %}
{% block content %}
<div id="content-main">
{% if app_list %}
{% for app in app_list %}
<div class="module">
<table summary="{% blocktrans with app.name as name %}Models available in the {{ name }} application.{% endblocktrans %}">
<caption>{% blocktrans with app.name as name %}{{ name }}{% endblocktrans %}</caption>
{% for model in app.models %}
<tr>
<th scope="row">
{% if model.perms.change %}
{{ model.name }}
{% else %}
{{ model.name }}
{% endif %}
({{ model.count }})
</th>
{% if model.perms.add %}
<td>{% trans 'Add' %}</td>
{% else %}
<td> </td>
{% endif %}
{% if model.perms.change %}
<td>{% trans 'Change' %}</td>
{% else %}
<td> </td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% endfor %}
{% else %}
<p>{% trans "You don't have permission to edit anything." %}</p>
{% endif %}
</div>
{% endblock %}
{% block sidebar %}
<div id="content-related">
<div class="module" id="recent-actions-module">
<h2>{% trans 'Recent Actions' %}</h2>
<h3>{% trans 'My Actions' %}</h3>
{% load log %}
{% get_admin_log 10 as admin_log for_user user %}
{% if not admin_log %}
<p>{% trans 'None available' %}</p>
{% else %}
<ul class="actionlist">
{% for entry in admin_log %}
<li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">
{% if entry.is_deletion %}
{{ entry.object_repr }}
{% else %}
{{ entry.object_repr }}
{% endif %}
<br/>
{% if entry.content_type %}
<span class="mini quiet">{% filter capfirst %}{% trans entry.content_type.name %}{% endfilter %}</span>
{% else %}
<span class="mini quiet">{% trans 'Unknown content' %}</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% endblock %}
This will do it.
(You will still need to modify the app_index view to see the counts correctly in the app index pages, I leave this as an exercise to you :-)
A 2022 update using Django 4.0
Subclass the default admin site, see the official Django doc on subclassing the AdminSite.
in the subclass, overwrite the _build_app_dict() method to add count in its model_dict as in:
model_dict = {
"name": capfirst(model._meta.verbose_name_plural),
"object_name": model._meta.object_name,
"perms": perms,
"admin_url": None,
"add_url": None,
"count": model.objects.count(), # THIS IS ALL YOU NEED TO ADD
}
Override the default admin site for your project with the subclass that we have created and optimized. see the official Django doc overriding the default admin site.
Override the app_list.html template if you haven't already. Inside your app_list.html template, you can now use the model.count variable like so {{ model.count }}. see the official Django docs on overriding templates.