To learn more about Django and understand how to program as DRY as possible, I'm building a small website with a couple of features:
Post Blog
Post Image
Post Bookmark
On every object in these APPs, users can post a comment. So I got
a Comment APP with a model that takes a GenericForeignKey. (Properly working)
I want to display a comment form in the Detail view of the Blog, Image, and Bookmark. To keep this dry, I created an inclusion tag
#register.inclusion_tag('comments/add_comment.html')
def show_comment_form(obj):
pk = obj.pk
content_type = ContentType.objects.get_for_model(obj)
form = AddCommentForm()
return {'pk': pk, 'content_type': content_type, 'form': form}
Which uses the following template for rendering the form:
<form action="{% url 'comments:add' pk content_type %}" method="post">
<div class="create-blog-container">
<div class="box-top">{% trans "Leave your comment behind" %}...</div>
<div class="box-content">
{% csrf_token %}
{{ form.as_div }}
</div>
<div class="box-footer">
<input type="submit" class="btn btn-green" value="{% trans 'Add comment' %}" />
</div>
</form>
When adding
{% show_comment_form blog %}
In the detail template of the blog, it renders the form with the right parameters. When I hit the SUBMIT button, the form is handled in the following Add view (currently just takes the form, validates, and adds it to the database via a service).
class Add(LoginRequiredMixin, View):
model = Comment
def post(self, *args, **kwargs):
content_type = self.kwargs.get('content_type')
pk = self.kwargs.get('pk')
form = AddCommentForm(self.request.POST)
if form.is_valid():
content = form.cleaned_data.get('content')
comments.services.comment.add(content_type, pk, self.request.user, content)
messages.add_message(self.request, messages.SUCCESS, _("Your comment has been posted"))
else:
print(form.errors)
return HttpResponseRedirect(reverse('blogs:detail', kwargs={'pk': pk}))
But this solution does not render back the form with validation errors. I went back to the basics to create a 'hard-coded' form in the blog's detail view, but it's not flexible nor DRY.
Can somebody give me a push in the right direction how to convert this form (via an inclusion tag, or other suggested method) into a DRY and flexible form to post a comment on a generic content_type?
In order to complete DRY I suggest (as you mentioned) implementing commenting for Django contenttypes and mixing it with AJAX.
Another one for your case it's possible to pass validated form to you inclusion tag (show_comment_form)
#register.inclusion_tag('comments/add_comment.html')
def show_comment_form(obj, form=None):
pk = obj.pk
content_type = ContentType.objects.get_for_model(obj)
form = form or AddCommentForm()
return {'pk': pk, 'content_type': content_type, 'form': form}
Change your inclusion tag, so it will post comment for on different URL, depending on object type. Also change it, so it won't make new form if there is already comment_form in current context. Example:
#register.inclusion_tag('comments/add_comment.html', takes_context=True)
def show_comment_form(context, obj):
pk = obj.pk
content_type = ContentType.objects.get_for_model(obj)
form = context.get('comment_form', AddCommentForm())
url_id = "%s:%s:comment:add" % (content_type.app_label, content_type.model)
return {'pk': pk, 'content_type': content_type, 'form': form, url_id}
template:
{# content_type may be obsolete now #}
<form action="{% url url_id pk content_type %}" method="post">
<div class="create-blog-container">
<div class="box-top">{% trans "Leave your comment behind" %}...</div>
<div class="box-content">
{% csrf_token %}
{{ form.as_div }}
</div>
<div class="box-footer">
<input type="submit" class="btn btn-green" value="{% trans 'Add comment' %}" />
</div>
</form>
Create view mixin, that will add to your view handling comment creation (or form validation), create new views for each of your content types from normal view for that content and your new mixin and register that view on separate URL (name that url in way that you're creating url_id in your template tag).
It's dry, flexible and doesn't require lot of changes.
Related
views.py
def registerPage(request):
form = UserCreateForm()
if request.method=='POST':
form=UserCreateForm(request.POST)
if form.is_valid():
user=form.save(commit=False)
user.save()
return redirect('home')
return render(request,'base/signup.html',{'form':form})
model.py
class User(AbstractUser):
name = models.CharField(max_length=200,null=True)
email = models.EmailField(unique=True,null=True)
bio=models.TextField(null=True)
avatar = models.ImageField(upload_to='images/',null=True)
USERNAME_FIELD='email'
REQUIRED_FIELDS=['username']
forms.py
class UserCreateForm(UserCreationForm):
class Meta:
model = User
fields = ['name','email','password1','password2','bio','avatar']
htmltemplate
{% include 'main.html' %}
{% block content %}
<div>
<form method="POST">
{% csrf_token %}
{% for field in form %}
{{field.label}}
{{field}}
{% endfor %}
<input type="submit" value="Register" >
</form>
</div>
{% endblock content %}
when ever i try to sign up on html template it doesnt work but if i do it in admin panel it works how can i solve it ?
First of all, it is generally not recommended to mess with the default User model from django. Its better to create a Profile model with a OneToOneField relationship with the user.
Other than that, your issue lies with your form. Since you have avatar which is an ImageField you need to change your form in a way that it can accept FILES.
So what you need to do is change your form like this:
<form method="post" action="" enctype="multipart/form-data" >
When you are writing client-side code:
use multipart/form-data when your form includes any <input type="file"> elements.
In order to make your POST request valid, you need to also receive your FILES on your view. That can be done by changing your code to:
if request.method=='POST':
form=UserCreateForm(request.POST, request.FILES)
if form.is_valid():
....
Hello I am making django to-do list and I have a question. Am i able to delete and create elements in one view? Personally I would not use CreateView for that and make it like in 'def post' and ignore 'get_success_url' but is it good and djangonic practice?
views.py
class Home(CreateView):
model = Item
template_name = 'home/todo.html'
form_class = itemCreationForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['items'] = Item.objects.all()
return context
def get_success_url(self):
return reverse('home-page')
def post(self, request):
form = itemCreationForm()
if 'delete' in self.request.POST:
print('deleted')
return redirect('home-page')
if 'add' in self.request.POST and form.is_valid():
form.save()
return HttpResponse('home-page')
HTML
<div class="card__items-list">
{% for item in items %}
<div class="card__item-row">
<p class="card__item">{{ item.name }}</p>
<form action="{% url 'home-page' %}" class="card__delete" method="POST"> {% csrf_token %}
<button class="card__delete__button" name="delete" type="submit">✘</button>
</form>
</div>
{% endfor %}
</div>
<form name="add" method="POST"> {% csrf_token %}
{{ form }}
<button name="add" class="submit-button" type="submit">Submit</button>
</form>
</div>
It is possible to handle both the deletion and creation of objects in a single Django view function. However, it is arguably a better practice to handle these operations in separate HTTP action verbs (POST vs DELETE).
For example, your Home view could handle both of these operations, but separate them into different if blocks based on the HTTP action verb of the request. Then, from your web page, the create operation would send an HTTP POST request, and the delete operation would send an HTTP DELETE request.
This could be done in a single Django view function using the following sample code:
def my_sample_view(request):
if request.method == "POST":
# Handle create logic here.
elif request.method == "DELETE":
# Handle delete logic here.
Yes, you can do this.
Personally, I would use FormView not CreateView, and do the POST processing in the form_valid method. It's contrary to one's expectations for a CreateView to delete something, whereas processing a form can basically do anything.
What I really want to do is , if a user click on "ADD more" button then a same form repeat itself and the values should store in database, if he/she doesn't click of that button then only the values from first form should be stored.
I am not able to get this, I just created a form , and a table in database for those details but can't loop though the form neither in data.
please help.
This is the form and the button:
This is the model.py code:
from django.db import models
class experience(models.Model):
company_name = models.CharField(max_length=100)
address = models.CharField(max_length=100)
startdate = models.Datefield(default = 01-01-2020)
lastdate = models.DateField(default = 01-01-2020)
profile = models.CharField(max_length=100)
description = models.TextField(max_length = 250)
This is the views.py code:
from django.shortcuts import render, redirect
import requests
from django.contrib.auth.models import User, auth
# Create your views here.
def profile(request):
return render(request, 'profile.html')
Unfortunately, there's no built-in way (as far as I know) in Django to do that without Javascript, but here's an approach:
HTML:
<div class="container" id="experiencesContainer">
<form method='POST' name='experienceForm'>
{{form.as_p}}
</form>
<form method='POST' name='experienceForm'>
{{form.as_p}}
</form>
<button type="button" id="addMoreButton">Add more</button>
<button type="submit">Save Changes</button>
</div>
Django POST method:
# Get a list of submitted forms
experiences = request.POST.getlist('experienceForm')
for experience in experiences:
# this is how you loop throuh every form
experience.get('company_name)
Your javascript something like:
// clonning his childs as well
let cloneForm = document.querySelector('form[name=experienceForm]').cloneNode(true);
document.querySelector('div#experiencesContainer').appendChild(cloneForm);
// see this https://www.w3schools.com/jsref/met_node_clonenode.asp
Of course this code is not tested but I've done this in several projects before, hope it works!
A simple way would be to request the same view from the "Add", just make sure your form view saves the data when request method is POST.
<form action="{% url 'your-form-url' %}" method="GET">
{% csrf_token %}
<input type="submit" value="Add">
</form>
one other way to repeat forms would be using formsets. Formsets allow you to repeat the same form 'extra' times. Check out the documentation for more about this.
def repeat_form(request):
ExpFormSet = formset_factory(ExperienceForm, extra=3)
#extra defines the no. of forms you want to display
if request.method == 'POST':
formset = ExpFormSet(request.POST, request.FILES)
if formset.is_valid():
# do something with the formset.cleaned_data
#loop through each form in the formser
for form in formset.cleaned_data:
obj = form.save()
else:
formset = ExpFormSet()
return render(request, 'exp_form.html', {'formset': formset})
The corresponding template should be:-
<form method="post">
{{ formset.management_form }}
{% for form in formset %}
{{ form.as_p }}
{% endfor %}
</form>
Make sure you add form.management_form. Using the combination of the above might solve your problem of taking and saving several inputs.
I am writing a very basic web page in Python which has a text box where a user can type in a username, then hit the Ok button which submits a form using a GET request. The GET passes the username as an argument and searches the auth_user table in the database.
My problem is I am not able to pass the username argument, please help if you can Django 2.0 url patterns
urls.py
app_name = 'just_gains'
urlpatterns = [
path('lifecoaching', views.LifeCoach, name='life_coaching'),
path('lifecoaching/resultslifecoaching/<str:user_name>', views.LifeCoachSearchResults, name='results_life_coaching'),
]
forms.py
class LifeCoachSearch(forms.Form):
user_name = forms.CharField(label='Username', max_length=100, required = False)
views.py
def LifeCoach(request):
if request == 'GET':
form = LifeCoachSearch(request.GET)
if form.is_valid:
user_name = form.cleaned_data['user_name']
LifeCoachSearchResults(request,user_name)
else:
form = LifeCoachSearch()
return render(request, 'just_gains/life_coaching.html', {'form': form})
def LifeCoachSearchResults(request, user_name):
testUser = User.objects.filter(username__startswith=user_name)
context = {'TestUser': testUser}
return render(request, 'just_gains/results_life_coaching.html', context)
HTML (lifecoaching)
<form action="{% url 'just_gains:results_life_coaching' %}" method="GET" >
{% csrf_token %}
{{ form }}
<input type="submit" value="OK">
</form>
HTML (resultslifecoaching)
<ul>
<li><a>print usernames that match the argument</a></li>
</ul>
Forgive me for the short response as I am on mobile. Try passing your username as a string in the path using <str:user_name>
Usually I think the form should submit via POST rather than GET, and the value of the submitted username would then be available in the dictionary request.POST['username']. GET should be used to get forms from the server; POST posts information back to the server. POST ensures that the browser bundles everything in the form and sends it complete, but GET tries to encode it in the URL and makes no guarantees.
Using forms, its helpful to have the View divide so that GET requests pull up blank or prepopulated forms (the empty search box) and POST requests are processed and redirected to the parameterized results screen you have.
You would then create a httpRedirect to re-assign the request to your URL with a parameter. I think this link, example 2 is the right way to go.
https://docs.djangoproject.com/en/2.0/topics/http/shortcuts/#redirect
So your function would look like:
def LifeCoach(request):
if request.method = 'GET':
return render(request, 'just_gains/life_coaching.html', context)
elif request.method = 'POST':
# I have skipped form validation here for brevity
return redirect('results_life_coaching',request.POST['username'])
It's possible that having a field called username may clash with or confuse you later when using request.USER['username']. Don't forget to change your form html! All the best!
[Edit 1] My code was wrong; GET should call the lifecoaching form, POST should redirect to the results_life_coaching page.
[Edit 2] My suggestions for your templates:
HTML (lifecoaching.html)
<form action="{% url 'just_gains:life_coaching' %}" method="POST" >
{% csrf_token %}
{{ form }}
<input type="submit" value="OK">
</form>
HTML (resultslifecoaching.html)
<ul>
{% for item in username_list %}
<li>{{item.user_name}} - {{item.achievement}} </li>
{% endfor %}
</ul>
I am using Django and Postgresql as database. I have a HTML page with two fields name and item. I can save the data in the database by clicking on submit. But, I want to show the saved data from database in the HTML page. It means, whenever we load the page, it should show the existing saved data and after submitting new data, the list should be updated. Below is my python code.
models.py
from django.contrib.auth.models import User
from django.db import models
class AllocationPlan(models.Model):
name = models.CharField(max_length=50)
item = models.CharField(max_length=4096)
views.py
class HomePageView(TemplateView):
template_name = "index.html"
def post(self, request, **kwargs):
if request.method == 'POST':
form = AllocationPlanForm(request.POST)
if form.is_valid():
form.save()
return render(request, 'index.html', { 'form': AllocationPlanForm() })
forms.py
from django import forms
from django.forms import ModelForm
from homeapp.models import AllocationPlan
class AllocationPlanForm(ModelForm):
class Meta:
model = AllocationPlan
fields = "__all__"
index.html
<html>
<form method="post">{% csrf_token %}
Name:<br>
<input type="text" name="name" >
<br>
Item:<br>
<input type="text" name="item" >
<br><br>
<input type="submit" value="Submit"/>
</form>
{% for i in form %}
{{ i.name }}
{{ i.item }}
{% endfor %}
</html>
It is returning NONE
Forms in Django are not used to display lists of data. It's merely used to render / validate the form (<form> tag in html). See also the forms doc.
Furthermore, it seems like you're using the TemplateView incorrectly. The post method in your view is only called on a POST request. When you're just viewing the page normally, the template is rendered normally, but since you only add the data to the template in a POST request, the template does not receive a form parameter when loading the view normally (therefore defaulting to None).
According to the TemplateView documentation, you can add the context like so:
class HomePageView(TemplateView):
template_name = 'index.html'
def get_context_data(self, **kwargs):
context = super(HomePageView, self).get_context_data(**kwargs)
# Get the allocation plans from database. Limit to last 10. Adjust to your own needs
context['plans'] = AllocationPlan.objects.all()[:10]
context['form'] = AllocationPlanForm()
return context
def post(self, request, **kwargs):
form = AllocationPlanForm(request.POST)
if form.is_valid():
form.save()
# Handle rest of request here (for example, return the updated page).
As you can see, there is no need to check if request.method == 'POST' in your post method, because Django only calls this method on POST requests. See also dispatch in the docs
To render the data from your database you can now access them in your template as plans:
{% for plan in plans %}
{{ plan.name }}
{{ plan.item }}
{% endfor %}
In your HTML, there is also no need to manually create the form content:
<form method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit" />
</form>
This will automatically create the HTML necessary for the form.