Tango with Django - Chapter 8 - Exercise - python

I need some help getting the add_page function to work properly. I am very new to HTML and even newer to Django. The chapter I am working on can be found here: http://www.tangowithdjango.com/book17/chapters/forms.html. Currently my relevent files look like this:
Forms.py
from django import forms
from rango.models import Page, Category
class CategoryForm(forms.ModelForm):
name = forms.CharField(max_length=128, help_text="Please enter the category name.")
views = forms.IntegerField(widget=forms.HiddenInput(), initial=0)
likes = forms.IntegerField(widget=forms.HiddenInput(), initial=0)
slug = forms.CharField(widget=forms.HiddenInput(), required=False)
# An inline class to provide additional information on the form.
class Meta:
# Provide an association between the ModelForm and a model
model = Category
fields = ('name',)
class PageForm(forms.ModelForm):
title = forms.CharField(max_length=128, help_text="Please enter the title of the page.")
url = forms.URLField(max_length=200, help_text="Please enter the URL of the page.")
views = forms.IntegerField(widget=forms.HiddenInput(), initial=0)
class Meta:
# Provide an association between the ModelForm and a model
model = Page
# What fields do we want to include in our form?
# This way we don't need every field in the model present.
# Some fields may allow NULL values, so we may not want to include them...
# Here, we are hiding the foreign key.
# we can either exclude the category field from the form,
exclude = ('category',)
#or specify the fields to include (i.e. not include the category field)
#fields = ('title', 'url', 'views')
def clean(self):
cleaned_data = self.cleaned_data
url = cleaned_data.get('url')
# If url is not empty and doesn't start with 'http://', prepend 'http://'.
if url and not url.startswith('http://'):
url = 'http://' + url
cleaned_data['url'] = url
return cleaned_data
Views.py:
from django.shortcuts import render
from django.http import HttpResponse
from rango.models import Category, Page
from rango.forms import CategoryForm, PageForm
def index(request):
# Query the database for a list of ALL categories currently stored.
# Order the categories by no. likes in descending order.
# Retrieve the top 5 only - or all if less than 5.
# Place the list in our context_dict dictionary which will be passed to the template engine.
category_list = Category.objects.order_by('-likes')[:5]
page_list = Page.objects.order_by('-view')[:5]
context_dict = {'categories': category_list,
'pages': page_list}
# Render the response and send it back!
return render(request, 'rango/index.html', context_dict)
def category(request, category_name_slug):
# Create a context dictionary which we can pass to the template rendering engine.
context_dict = {}
try:
# Can we find a category name slug with the given name?
# If we can't, the .get() method raises a DoesNotExist exception.
# So the .get() method returns one model instance or raises an exception.
category = Category.objects.get(slug=category_name_slug)
context_dict['category_name'] = category.name
context_dict['category_name_slug'] = category_name_slug
# Retrieve all of the associated pages.
# Note that filter returns >= 1 model instance.
pages = Page.objects.filter(category=category)
# Adds our results list to the template context under name pages.
context_dict['pages'] = pages
# We also add the category object from the database to the context dictionary.
# We'll use this in the template to verify that the category exists.
context_dict['category'] = category
except Category.DoesNotExist:
# We get here if we didn't find the specified category.
# Don't do anything - the template displays the "no category" message for us.
pass
# Go render the response and return it to the client.
print context_dict
return render(request, 'rango/category.html', context_dict)
def add_category(request):
# A HTTP POST?
if request.method == 'POST':
form = CategoryForm(request.POST)
# Have we been provided with a valid form?
if form.is_valid():
# Save the new category to the database.
form.save(commit=True)
# Now call the index() view.
# The user will be shown the homepage.
return index(request)
else:
# The supplied form contained errors - just print them to the terminal.
print form.errors
else:
# If the request was not a POST, display the form to enter details.
form = CategoryForm()
# Bad form (or form details), no form supplied...
# Render the form with error messages (if any).
return render(request, 'rango/add_category.html', {'form': form})
def add_page(request, category_name_slug):
try:
cat = Category.objects.get(slug=category_name_slug)
except Category.DoesNotExist:
cat = None
if request.method == 'POST':
form = PageForm(request.POST)
if form.is_valid():
if cat:
page = form.save(commit=False)
page.category = cat
page.views = 0
page.save()
# probably better to use a redirect here.
return category(request, category_name_slug)
else:
print form.errors
else:
form = PageForm()
context_dict = {'form':form, 'category': cat}
return render(request, 'rango/add_page.html', context_dict)
urls.py
from django.conf.urls import patterns, url
from rango import views
urlpatterns = patterns('',
url(r'^$', views.index, name='index'),
# url(r'^about/$', views.about, name='about'),
url(r'^add_category/$', views.add_category, name='add_category'),
url(r'^category/(?P<category_name_slug>[\w\-]+)/add_page/$', views.add_page, name='add_page'),
url(r'^category/(?P<category_name_slug>[\w\-]+)/$', views.category, name='category'),)
I think this ^ is where I am encountering the issue. I manage to get to the "add a page" screen, but when I try to submit something, I receive an error that states I am only supplying 1 argument and add_page() requires 2. I think I may need an additional url that is similar to the "add_category" URL, but that must mean by other URL is pointing to the wrong place?
category.html
<!DOCTYPE html>
<html>
<head>
<title>Rango</title>
</head>
<body>
<h1>{{ category_name }}</h1>
{% if category %}
{% if pages %}
<ul>
{% for page in pages %}
<li>{{ page.title }}</li>
{% endfor %}
</ul>
{% else %}
<strong>No pages currently in category.</strong>
{% endif %}
<li>Add a New Page</li>
{% else %}
The specified category {{ category_name }} does not exist!
{% endif %}
</body>
</html>
add_page.html:
<!DOCTYPE html>
<html>
<head>
<title>Rango</title>
</head>
<body>
<h1>Add a Page</h1>
<form id="page_form" method="post" action="/rango/add_page/">
{% csrf_token %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% for field in form.visible_fields %}
{{ field.errors }}
{{ field.help_text }}
{{ field }}
{% endfor %}
<input type="submit" name="submit" value="Create Page" />
</form>
</body>
</html>

I edited the add_page function to include category_name_slug:
def add_page(request, category_name_slug):
try:
cat = Category.objects.get(slug=category_name_slug)
except Category.DoesNotExist:
cat = None
if request.method == 'POST':
form = PageForm(request.POST)
if form.is_valid():
if cat:
page = form.save(commit=False)
page.category = cat
page.views = 0
page.save()
# probably better to use a redirect here.
return category(request, category_name_slug)
else:
print form.errors
else:
form = PageForm()
# made the change here
context_dict = {'form':form, 'category': cat, 'category_name_slug': category_name_slug}
return render(request, 'rango/add_page.html', context_dict)
Then I edited the add_page.html to look like this:
<!DOCTYPE html>
<html>
<head>
<title>Rango</title>
</head>
<body>
<h1>Add a Page</h1>
<form id="page_form" method="post" action="/rango/category/{{ category_name_slug }}/add_page/">
{% csrf_token %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% for field in form.visible_fields %}
{{ field.errors }}
{{ field.help_text }}
{{ field }}
{% endfor %}
<input type="submit" name="submit" value="Create Page" />
</form>
</body>
</html>

if you dont wanna edit your views.py
just doit
<!DOCTYPE html>
<html>
<head>
<title>Rango</title>
</head>
<body>
<h1>Add a Page</h1>
<form id="page_form" method="post" action="/rango/category/{{ category }}/add_page/">
{% csrf_token %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% for field in form.visible_fields %}
{{ field.errors }}
{{ field.help_text }}
{{ field }}
{% endfor %}
<input type="submit" name="submit" value="Create Page" />
</form>
</body>
</html>
but i have problem to it still cannot be save on database.

Related

How can I implement update and delete in django view?

I am creating a movie review website. In it, I want to be able to allow a User to make one comment on one movie and then Update or Delete that comment. But I am only able to implement POST right now. How do I change the view, html or model?
Question to ask
How can I keep the comments posted by a user at the top of the comment list so that they can be updated and deleted?
An example of what we would like to implement is Rotten Tomatoes.
Models.py:
class Comment_movie(models.Model):
comment = models.TextField(max_length=1000)
stars = models.FloatField(
blank=False,
null=False,
default=0,
validators=[MinValueValidator(0.0),
MaxValueValidator(10.0)]
)
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
created_at = models.DateTimeField(default=datetime.now)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ('user', 'movie')
indexes = [
models.Index(fields=['user', 'movie']),
]
views.py:
def view_movie_detail(request, movie_id):
if not(Movie.objects.filter(id=movie_id)):
Movie(id = movie_id).save()
movie = Movie.objects.get(id=movie_id)
if request.method == "POST":
form = Comment_movie_CreateForm(request.POST)
if form.is_valid():
Comment_movie(
comment = form.cleaned_data['comment'],
user = request.user,
stars = form.cleaned_data['stars'],
movie = movie
).save()
return redirect('view_movie_detail', movie_id=movie_id)
else:
form = Comment_movie_CreateForm()
data = requests.get(f"https://api.themoviedb.org/3/movie/{movie_id}?api_key={TMDB_API_KEY}&language=en-US")
recommendations = requests.get(f"https://api.themoviedb.org/3/movie/{movie_id}/recommendations?api_key={TMDB_API_KEY}&language=en-US")
comments = reversed(Comment_movie.objects.filter(movie_id=movie_id))
average = movie.average_stars()
context = {
"data": data.json(),
"recommendations": recommendations.json(),
"type": "movie",
"comments": comments,
"average" : average,
"form": form,
}
return render(request, "Movie/movie_detail.html", context)
movie.html:
<h2>Comments</h2>
{% if form.errors %}
<div class = "error_list">
{% for errors in form.errors.values %}
{% for error in errors %}
{{ error }}<br>
{% endfor %}
{% endfor %}
</div>
{% endif %}
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<button type="submit">Post Comment</button>
</form>
{% endif %}
<hr>
Looks like you want to do multiple actions in one view. One form for each action and a template field to differentiate actions would be a solution. In this specific case, 'create' action and 'update' action can be automatically determined if we take advantage of unique_together.
from django.shortcuts import get_object_or_404
def view_movie_detail(request, movie_id):
# It makes little sense you create a movie with just an id attr
# So I use get_object_or_404 instead
movie = get_object_or_404(Movie, id=movie_id)
try:
comment_movie = Comment_movie.objects.get(user=request.user, movie=movie)
except Comment_movie.DoesNotExist:
comment_movie = None
if request.method == 'POST':
if request.POST.get('action') == 'delete':
comment_movie.delete()
return redirect('view_movie_detail', movie_id=movie_id)
else:
form = Comment_movie_CreateForm(request.POST, instance=comment_movie)
if form.is_valid():
form.save()
return redirect('view_movie_detail', movie_id=movie_id)
else:
form = Comment_movie_CreateForm(instance=comment_movie)
# Put your view logic outside of the conditional expression.
# Otherwise your code breaks when the form validates to False
data = requests.get(f"https://api.themoviedb.org/3/movie/{movie_id}?api_key={TMDB_API_KEY}&language=en-US")
recommendations = requests.get(f"https://api.themoviedb.org/3/movie/{movie_id}/recommendations?api_key={TMDB_API_KEY}&language=en-US")
comments = reversed(Comment_movie.objects.filter(movie_id=movie_id).exclude(user=request.user))
average = movie.average_stars()
context = {
"data": data.json(),
"recommendations": recommendations.json(),
"type": "movie",
"comments": comments,
"average" : average,
"form": form,
"comment_movie": comment_movie, # NOTE add the comment to context
}
return render(request, "Movie/movie_detail.html", context)
Note instance=coment_movie will make form use instance attribute when rendering in template.
And in your templates, render all three forms, and add ‘action’ to each form. One good place would be the submit button.
<h2>Comments</h2>
{% if form.errors %}
<div class = "error_list">
{% for errors in form.errors.values %}
{% for error in errors %}
{{ error }}<br>
{% endfor %}
{% endfor %}
</div>
{% endif %}
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
{% if comment_movie %}
<button type="submit">Edit Comment</button>
<button type="submit" name="action" value="delete">Delete Comment</button>
{% else %}
<button type="submit">Post Comment</button>
{% endif %}
</form>
{% endif %}
<hr>
{% for comment in comments %}
<div>{{ comment.comment }}</div>
{% endfor %}
Check out django-multi-form-view. This module does not fit your question perfectly, but shares some basic ideas.
Note two addtional submit buttons in template. They are rendered only if comment is not None, which means user has made comment before. The second button coresponds to action='delete'
To your question: Render the form first, and render the rest comments after the form such that user comment is always at top.

django redirect to form view and autofill with previously entered values

I have following scenario.
User fills out a form
If the user clicks the "continue" button and the form is valid the user will be redirected to a summary view
In the summary view the user checks the input again. He can either continue or go back.
If he continues the data will be saved in the database, if he goes back he can edit the form.
Since in step 4 the user is at the view summary I have to redirect him to the home view. I don´t want the user to fill out the entire form again, the previously entered data should be autofilled if he goes back.
Something special: I am using django-tagify2 for one input in the form to get tags rather then text. If the user goes back the tags should be rendered correctly in the tagify specific form.
So here are my files:
home.html
{% extends "messenger/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="message-container">
<form method="POST" autocomplete="off">
{% csrf_token %}
{{ form|crispy }}
<button name="sendmessage" class="btn btn-outline-info" type="submit">Continue</button>
</form>
</div>
{% endblock content %}
summary.html
{% extends "messenger/base.html" %}
{% block content %}
<h4>Zusammenfassung</h4>
<p><b>Empfänger: </b>{{ receiver }}</p>
<br>
<p><b>Betreff: </b>{{ subject }}</p>
<br>
<p><b>Nachricht: </b>{{ message }}</p>
<button>Edit</button>
<button>Continue</button>
{% endblock content %}
home view
#login_required(login_url='login')
def home(request):
if request.method == 'POST' and 'sendmessage' in request.POST:
message_form = MessageForm(request.POST)
if message_form.is_valid():
receiver_list = message_form['receiver'].value().split(';')
subject = message_form['subject'].value()
message = message_form['textmessage'].value()
#create sessions and send data to next view
session_receiver = receiver_list
request.session['session_receiver'] = session_receiver
session_subject = subject
request.session['session_subject'] = session_subject
session_message = message
request.session['session_message'] = session_message
return redirect('summary')
else:
message_form = MessageForm()
return render(request, 'messenger/home.html', {'form': message_form})
summary view
def summary(request):
receiver = request.session.get('session_receiver')
subject = request.session.get('session_subject')
message = request.session.get('session_message')
return render(request, 'messenger/summary.html', {'receiver':receiver,
'subject':subject,
'message':message})
So what is the best way to do this?
Can I use the session variables to set the fields in the form?
I don´t want to change the logic in the app. I want a home/summary/success view/template where I can loop as long is I want between home and summary until the user is happy with his entered form data
How about checking request.session when there is get request to home view? Then you can bind message_form = MessageForm() to session data.
You can check out htmx and django-htmx. You can do what you want easily without session by swapping HTML with context.
I played around with the session values and views and finally got a way to redirect to other views with prefilled form fields based on session values.
#login_required(login_url='login')
def home(request):
if request.method == 'POST' and 'sendmessage' in request.POST:
message_form = MessageForm(request.POST)
if message_form.is_valid():
ad_group = message_form['ad_group'].value().split(';')
ad_user = message_form['ad_user'].value().split(';')
subject = message_form['subject'].value()
message = message_form['textmessage'].value()
#create sessions and send data to next view
session_ad_group = ad_group
request.session['session_ad_group'] = session_ad_group
session_ad_user = ad_user
request.session['session_ad_user'] = session_ad_user
session_subject = subject
request.session['session_subject'] = session_subject
session_message = message
request.session['session_message'] = session_message
return redirect('summary')
else:
if request.session.get('session_subject'):
message_form = MessageForm(initial={'ad_group': request.session.get('session_ad_group'),
'ad_user': request.session.get('session_ad_user'),
'subject': request.session.get('session_subject'),
'textmessage': request.session.get('session_message')})
return render(request, 'messenger/home.html', {'form': message_form})
else:
message_form = MessageForm()
return render(request, 'messenger/home.html', {'form': message_form})
def summary(request):
ad_group = request.session.get('session_ad_group')
ad_user = request.session.get('session_ad_user')
subject = request.session.get('session_subject')
message = request.session.get('session_message')
if request.method == 'POST' and 'edit' in request.POST:
message_form = MessageForm(initial={'ad_group':ad_group, 'ad_user': ad_user,
'subject':subject, 'textmessage':message})
return render(request, 'messenger/home.html', {'form': message_form})
return render(request, 'messenger/summary.html', {'ad_group':ad_group,
'ad_user': ad_user,
'subject':subject,
'message':message})
Template
{% extends "messenger/base.html" %}
{% block content %}
<h2>Zusammenfassung</h2>
<div class="border-top pt-3">
<p><b>AD-Gruppe: </b>{{ ad_group }}</p>
<p><b>AD-User: </b>{{ ad_user }}</p>
<br>
<p><b>Betreff: </b>{{ subject }}</p>
<br>
<p><b>Nachricht: </b>{{ message }}</p>
<div class="buttoncontainer">
<form name="edit" action="" method="post">
{% csrf_token %}
<button class="btn edit_btn" formaction="{% url 'messenger-home' %}">Zurück</button>
</form>
<form name="senden" action="" method="post">
{% csrf_token %}
<button class="btn continue_btn" formaction="{% url 'send_messages' %}">Nachricht senden</button>
</form>
</div>
</div>
{% endblock content %}

Django httpresponse is using get instead of post

Thank you for taking the time to help! I've been stuck for hours. I'm learning django by going through this fantastic youtube video: https://www.youtube.com/watch?v=sm1mokevMWk&t=4252s. I believe I copied the code from the video exactly, and I double and triple checked it. Yet, despite declaring method = "post" in "create".html django consistently uses a get response. WHY?!
#urls.py
from django.urls import path
from . import views
urlpatterns = [
path('<int:id>', views.index, name='index'),
path("",views.home, name = 'home'),
path("create/", views.create, name="create"),
]
#views.py
from django.shortcuts import render
from django.http import HttpResponse, HttpResponseRedirect
from .models import ToDoList, Item
from .forms import CreateNewList
def index(response, id):
ls = ToDoList.objects.get(id=id)
return render(response, 'main/list.html', {"ls":ls})
def home(response):
return render(response, "main/home.html", {})
def create(response):
print(response.method)
if response.method == "POST":
form = CreateNewList(response.POST)
if form.is_valid():
n = form.cleaned_data['name']
t = ToDoList(name=n)
t.save()
return HttpResponseRedirect("/%i" %t.id)
else:
form = CreateNewList()
return render(response, "main/create.html", {"form":form})
#create.html
{% extends 'main/base.html' %}
{% block title %} Create New List {% endblock %}
{% block content %}
Create Pages
<form method="post" action="/create/">
{{form.as_p}}
<button type="submit", name ="save" >Create New</button>
</form>
{% endblock %}
#base.html
<html>
<head>
<title>{% block title %}Jeff's website{% endblock %}</title>
</head>
<body>
<div id="content", name="content">
{% block content %}
{% endblock %}
</div>
</body>
</html>
#home.html
{% extends 'main/base.html' %}
{% block title %}
Home
{% endblock %}
{% block content %}
<h1>Home Page</h1>
{% endblock %}
#models.py
from django.db import models
class ToDoList(models.Model):
name = models.CharField(max_length=200)
def __str__(self):
return self.name
class Item(models.Model):
todolist = models.ForeignKey(ToDoList, on_delete=models.CASCADE)
text = models.CharField(max_length=300)
complete = models.BooleanField()
def __str__(self):
return self.text
You need to perform a GET request first to display your form and then , you make a post request for the submit, that why you check the request method .
if response.method == "POST" => The form is already displayed and we need to submit it
else we need to display our form

Django - saving form to the built in database SQLite

This is my first time using Django and I am very simply trying to save text to the database. I have created the table inputs in the database.
I am getting the following error;
Error - Page not found (404)
My code is as follows;
Models.py
from django.db import models
class Input(models.Model):
waist = models.IntegerField(default=0)
height = models.IntegerField(default=0)
def __unicode__(self):
return "{0} {1} {2}".format(
self, self.waist, self.height)
forms.py
class InputForm(forms.ModelForm):
class Meta:
model = Input
fields ={
'waist',
'height'
}
views.py
def InputView(request):
if request.POST:
form = InputForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect('account/input')
else:
form = InputForm()
args = {'form' : form}
return render(request,'accounts/input.html', args)
urls.py
url(r'^input/$',views.InputView, name='view_input')
input.html
{% extends 'base.html' %}
{% block head %}
<title> Edit Profile </title>
{% endblock %}
{% block body %}
<div class="container">
<h1> Enter Body Details </h1>
<br>
<br>
<form action="account/input" method="post">
{% csrf_token %}
<ul>
{{form.as_ul}}
</ul>
<input type="Submit" name="submit" value="submit"/>
</form>
</div>
{% endblock %}
If any one can help it would be greatly appreciated.
HttpResponseRedirect('account/input')
you need to add one more '/' to the beginning like
HttpResponseRedirect('/account/input')
Another way to do it is to use reverse() so if you change the URL you don't have to change your code and you avoid mistakes entering the URL.
Instead of
HttpResponseRedirect('/account/input')
use
HttpResponseRedirect(reverse('view_input'))
remember to add the import
from django.urls import reverse

Displaying form content instead of form object name

I am new to Django and I am trying to create a form in a template. I think I have accomplished this however I a missing a piece. This is what I currently see:
I think I am missing something from my form class that should display the choices.
forms.py
from django import forms
class TestForm(forms.Form):
one = forms.ChoiceField(choices=('HDFS', 'HDFS'), widget=forms.RadioSelect())
two = forms.ChoiceField(choices=('HIVE', 'HIVE'), widget=forms.RadioSelect())
three = forms.ChoiceField(choices=('BOTH', 'Both of HDFS and HIVE'), widget=forms.RadioSelect())
beatle = [one, two, three]
event_textarea = forms.Textarea(attrs={'rows': '8', 'class': 'form-control', 'placeholder': 'Events...', 'id': 'event_textarea'})
views.py
def home(request):
if request == 'POST':
# create a form instane and populate it with data from the request
form = TestForm(request.POST)
if form.is_valid():
# process the data in form.cleaned_data as required
form.cleaned_data()
# redirect to a new URL:
return HttpResponseRedirect('/test/')
# if a GET (or any other method) we'll create a blank form
else:
form = TestForm()
return render(request, 'home/home_page.html', {'form': form})
template:
{% extends 'index/index.html' %}
{% load staticfiles %}
{% block head %}
<script type="text/javascript" src="{{ STATIC_URL }}home/js/home.js" async></script>
<link href="{{ STATIC_URL }}home/css/home.css" rel="stylesheet">
{% endblock head %}
{% block content %}
<div>Welcome to Trinity E2E testing</div>
<form id="test-form" action="/test/" method="post"> {# pass data to /test/ URL #}
{% csrf_token %}
{% for radio in form.beatle %}
<div class="btn btn-default btn-lg">
{{ radio }}
</div>
{% endfor %}
{{ form.event_textarea }}
<input id="submit-test" type="submit" class="btn btn-default btn-lg" value="Submit">
</form>
{% endblock content %}
beatle list contains references to class attributes, not instance attributes.
How about make it a instance method to return instance attributes (form fields):
def beatle(self):
return [self.one, self.two, self.three]
UPDATE
To correctly return bound fields:
def beatle(self):
return [self['one'], self['two'], self['three']]
or
def beatle(self):
return [self[name] for name in ['one', 'two', 'three']]

Categories

Resources