Learning django and python to build a blog app.
Note: A single project can contain multiple apps, which is good for separating out different part of project. You can take a single app and add it to multiple project.
create virtual environment
- pip list
- cd Desktop
- mkdir Djangoenv
- cd Djangoenv
- virtualenv project1_env
- project1_env\Scripts\activate (linux: source project1_env/bin/activate)
- where python (linux: which python)
- where pip
install django
- pip install django
- python -m django --version
- django-admin startproject django_project
- cd django_project
- py manage.py runserver
basic routing in django
- create blog app within the project
- py manage.py startapp blog
|-- blog
| |-- __init__.py
| |-- admin.py
| |-- apps.py
| |-- migration
| | |-- __init__.py
| |-- models.py
| |-- tests.py
| |-- views.py
|-- db.sqlite3
|-- django_project
| |-- __init__.py
| |-- settings.py
| |-- urls.py
| |-- wsgi.py
|-- manage.py
- add home & about page
use templates to return home and about page
- do not forget to add the blog application to the list of installed apps in project's settings.py
- blog.apps.BlogConfig
- by adding apps to this list, django will correctly search the template
- from django.shortcuts import render (render function has the third optional arg, which will pass the data to the template
- this third arg is a dict, the keys of the dict will be accessible in the template
template inheritance
- base.html contains repeated section of home and about templates.
- add bootstrap to the website
- add navigation bar, beautify content, custom styles in main.css
- Starter template
- in Django, static files (eg. css and javascripte) need to be located in a static directory within the blog app
- use Django url tag with the name of the route in urlpattern instead of hard-coding it for better maintainess
create database & migrations & query data
- Django ORM, Object-Relational Mapping, is a technique that lets you query and manipulate data from a database using an object-oriented paradigm
- Django ORM represent database structure as classes(models)
- each class is its own table in the database, each attribute of the class is a field in the database
- author is user, which is a separate table
- from django.contrib.auth.models import User
- author = models.ForeignKey(User, on_delete=models.CASCADE) # if user is deleted, post also
- we need to run migrations to update the database with any change
- py manage.py makemigrations
- py manage.py sqlmigrate blog 0001
- py manage.py migrate
- Django python shell allow us to work with the models interactively line by line:
- py manage.py shell
(PROJEC~1) C:\Users\Jiayi Su\Desktop\Djangoenv\django_project>py manage.py makemigrations
Migrations for 'blog':
blog\migrations\0001_initial.py
- Create model Post
(PROJEC~1) C:\Users\Jiayi Su\Desktop\Djangoenv\django_project>py manage.py sqlmigrate blog 0001
BEGIN;
--
-- Create model Post
--
CREATE TABLE "blog_post" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "title" varchar(100) NOT N
ULL, "content" text NOT NULL, "date_posted" datetime NOT NULL, "author_id" integer NOT NULL REFERENCE
S "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "blog_post_author_id_dd7a8485" ON "blog_post" ("author_id");
COMMIT;
(PROJEC~1) C:\Users\Jiayi Su\Desktop\Djangoenv\django_project>py manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
Applying blog.0001_initial... OK
(PROJEC~1) C:\Users\Jiayi Su\Desktop\Djangoenv\django_project>py manage.py shell
Python 3.7.2 (tags/v3.7.2:9a3ffc0492, Dec 23 2018, 23:09:28) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from blog.models import Post
>>> from django.contrib.auth.models import User
>>> User.objects.all()
<QuerySet [<User: jiayisu>, <User: TestUser>]>
>>> User.objects.first()
<User: jiayisu>
>>> User.objects.filter(username='jiayisu')
<QuerySet [<User: jiayisu>]>
>>> User.objects.filter(username='jiayisu').first()
<User: jiayisu>
>>> user = User.objects.filter(username='jiayisu').first()
>>> user
<User: jiayisu>
>>> user.id
1
>>> user.pk
1
>>> user = User.objects.get(id=1)
>>> user
<User: jiayisu>
>>> Post.objects.all()
<QuerySet []>
>>> post_1=Post(title='Blog 1', content='First Post Content!', author=user)
>>> Post.objects.all()
<QuerySet []>
>>> post_1.save()
>>> Post.objects.all()
<QuerySet [<Post: Post object (1)>]>
>>> exit()
def __str__(self):
return self.title
(PROJEC~1) C:\Users\Jiayi Su\Desktop\Djangoenv\django_project>py manage.py shell
Python 3.7.2 (tags/v3.7.2:9a3ffc0492, Dec 23 2018, 23:09:28) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from blog.models import Post
>>> from django.contrib.auth.models import User
>>> Post.objects.all()
<QuerySet [<Post: Blog 1>]>
>>> User.objects.filter(username='jiayisu').first()
<User: jiayisu>
>>> user = User.objects.filter(username='jiayisu').first()
>>> user
<User: jiayisu>
>>> post_2 = Post(title='Blog 2', content='Second Post Content!', author_id=user.id)
>>> post_2.save()
>>> Post.objects.all()
<QuerySet [<Post: Blog 1>, <Post: Blog 2>]>
>>> post = Post.objects.first()
>>> post.content
'First Post Content!'
>>> post.date_posted
datetime.datetime(2019, 3, 7, 22, 23, 49, 609491, tzinfo=<UTC>)
>>> post.author
<User: jiayisu>
>>> post.author.email
'[email protected]'
>>> #--------------------------get all the post by this user-------------------------------------------
>>> user.post_set
<django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager.<locals>.RelatedManag
er object at 0x000001A0F67EAB00>
>>> user.post_set.all()
<QuerySet [<Post: Blog 1>, <Post: Blog 2>]>
>>> #--------------------------create a post directly (no need to specify user & save)------------------
>>> user.post_set.create(title='Blog 3', content='Third Post Content!')
<Post: Blog 3>
>>> Post.objects.all()
<QuerySet [<Post: Blog 1>, <Post: Blog 2>, <Post: Blog 3>]>
>>>
query the data and pass it to view.py
- from .models import Post # use the post from database
- 'post':Post.objects.all() # query the posts
- py manage.py runserver # now, the posts are updated
- change the formatting of the date_posted: in home.html, post.date_posted(date:"F d, Y")
- to see Post model in admin page, we have to register Post in admin.py
- from .models import Post
- admin.site.register(Post)
- now we can update, delete, change author of any post
user registration
- py manage.py startapp users
- in settings.py, add "'users.apps.UsersConfig'," to INSTALLED_APPS list
- in users/views.py
def register(request):
form = UserCreationForm()
return render(request, 'users/register.html', {'form': form})
- create users/templates/users/register.html, which extends blog/base.html
- in the form, we need to add "{% csrf_token %}", which is Cross Site Request Forgery protection
- in the project's url.py, create an url pattern that uses the register/views.py, so we can navigate to this page in the browser
- from users import views as user_views
- path('register/', user_views.register, name='register'),
- if we get a POST request, it instantiates the UserCreationForm with that POST data, else eg. it's a GET request, we just desplay a blank form.
- if the form is valid, create the user, save the data, grab the username, and redirect the user to the home page.
- from django.contrib import messages, which has debug, info, success, warning and error tags.
def register(request):
if request.method == 'POST':
form = UserCreationForm(request.POST)
if form.is_valid():
form.save()
username = form.cleaned_data.get('username')
messages.success(request, f'Account created for {username}!')
return redirect('blog-home')
else:
form = UserCreationForm()
return render(request, 'users/register.html', {'form': form})
- put flush messages in base template so any massages pop up on any page.
<div class="col-md-8">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor%}
{% endif %}
{% block content %}{% endblock %}
</div>
add email field to the form and use django-crispy-forms
- users/forms.py
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
class UserRegisterForm(UserCreationForm): # inheritsUserCreationForm
email = forms.EmailField() # default: required=True
class Meta: # keep the configuration in one place
model = User # the model that'll be affected is User model
fields = ['username', 'email', 'password1', 'password2'] # fields will be shown in the model in this order
- crispy forms allow us to put some simple tags in the template that'll style the form in a bootstrap fashion
- pip install django-crispy-forms
- in project/settings.py INSTALLED_APPS, add 'crispy_forms', CRISPY_TEMPLATE_PACK = 'bootstrap4'
- in register.html
{% load crispy_forms_tags %}
{% block content %}
...
{{ form|crispy }}
login/logout systems & users are forced to login before they can see the /profile page
- in project/urls.py from django.contrib.auth import views as auth_views
- create path: path('login/', auth_views.LoginViews.as_view(template_name='users/login.html'), name='login'),
- LOGIN_REDIRECT_URL = 'blog-home'
- modify register route to redirect users to login page:
- in users/views.py f'Your account has been created! You are now able to log in'
- return redirect('login')
- django provides a user variable that contains the current user and has an attribute called .is_authenticated to check if the user is currently logged in
create user's profile page that users can access after logged in
- create the view: in users/views.py
def profile(request):
return render(request, 'users/profile.html')
- create the template: users/templates/users/profile.html
- create the routes in url_patterns that will use this view: in urls.py urlpatterns list, add path('profile/', user_views.profile, name='profile'),
- add profile button in navigation bar in base.html, display a link to the profile page when logged in
- put a check to see if the user is logged in before accessing the profile page:
- in views.py, from django.contrib.auth.decorators import login_required
@login_required
def profile(request):
return render(request, 'users/profile.html')
- .../accounts/login/?next=/profile/ is the default location that django looks for login routes, but we have simply put our login page at /login, so we need to tell django where it can find the login route:
- in settings.py LOGIN_URL = 'login'
- ?next=/profile/ it's keeping track of the page that we were trying to access and it will direct us to that page after we log in
- note: the default redirect url is to the 'home-page'
- so if we log in here, we will be redirected to /profile page now
user's profile and picture
- extend the existing User model that django provides
- in users/models.py
from django.db import models
from django.contrib.auth.models import User
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
image = models.ImageField(default='default.jpg', upload_to='profile_pics')
def __str__(self):
return f'{self.user.username} Profile'
- Anytime after changing the models, be sure to makemigrations to update the database
- pip install Pillow
- py manages.py makemigrations
- py manages.py migrate
- in order to be able to view the profile on /admin page, we need to register this model within admin.py
- in users/admin.py:
from django.contrib import admin
from .models import Profile
admin.site.register(Profile)
- add profile manually through admin page
(PROJEC~1) C:\Users\Jiayi Su\Desktop\Djangoenv\django_project>py manage.py shell
Python 3.7.2 (tags/v3.7.2:9a3ffc0492, Dec 23 2018, 23:09:28) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.models import User
>>> user = User.objects.filter(username='jiayisu').first()
>>> user
<User: jiayisu>
>>> user.profile
<Profile: jiayisu Profile>
>>> user.profile.image
<ImageFieldFile: profile_pics/girl.jpg>
>>> user.profile.image.width
564
>>> user.profile.image.url
'profile_pics/girl.jpg'
>>> user = User.objects.filter(username='TestUser').first()
>>> user
<User: TestUser>
>>> user.profile.image
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "C:\Users\JIAYIS~1\Desktop\DJANGO~1\PROJEC~1\lib\site-packages\django\db\models\fields\related
_descriptors.py", line 414, in __get__
self.related.get_accessor_name()
django.contrib.auth.models.User.profile.RelatedObjectDoesNotExist: User has no profile.
>>>
- change the dir where the profile pictures will be saved
- in settings.py: MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_URL = '/media/'
- profile.html
{% extends "blog/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="content-section">
<div class="media">
<img class="rounded-circle account-img" src="{{ user.profile.image.url }}">
<div class="media-body">
<h2 class="account-heading">{{ user.username }}</h2>
<p class="text-secondary">{{ user.email }}</p>
</div>
</div>
<!-- FORM HERE -->
</div>
{% endblock content %}
- Serving files uploaded by a user during development
- During development, you can serve user-uploaded media files from MEDIA_ROOT using the django.views.static.serve() view.This is not suitable for production use!
- in project/urls.py
from django.conf import settings
from django.conf.urls.static import static
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
- auto-set default pic for newly-created users
- in users/signals.py
from django.db.models.signals import post_save # signal
from django.contrib.auth.models import User # sender
from django.dispatch import receiver
from .models import Profile
@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
instance.profile.save()
- import the signal inside of the ready function of users/apps.py
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'
def ready(self):
import users.signals
update user's profile (information and picture) and auto-resize uploaded picture
- users/forms.py create a model form, which allows us to create a form that'll work with a specific database model.
- here, we want our form to update the user model
class UserUpdateForm(forms.ModelForm):
email = forms.EmailField()
class Meta:
model = User
fields = ['username', 'email']
class ProfileUpdateForm(forms.ModelForm):
class Meta:
model = Profile
fields = ['image']
- add these forms to profile view
@login_required
def profile(request):
u_form = UserUpdateForm()
p_form = ProfileUpdateForm()
context = {
'u_form': u_form,
'p_form': p_form,
}
return render(request, 'users/profile.html', context)
- put two fields for these forms into a single form, so the user see it as one
- in users/profile.html
<!-- FORM HERE IS USERUPDATE & PROFILEUPDATE Form-->
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<fieldset class="form-group">
<legend class="border-bottom mb-4">Profile Info</legend>
{{ u_form|crispy }}
{{ p_form|crispy }}
</fieldset>
<div class="form-group">
<button class="btn btn-outline-info" type="submit">Update</button>
</div>
</form>
- populate the fields with current user info: in views.py
@login_required
def profile(request):
if request.method == 'POST':
u_form = UserUpdateForm(request.POST, instance=request.user)
p_form = ProfileUpdateForm(request.POST, request.FILES,
instance=request.user.profile)
if u_form.is_valid() and p_form.is_valid():
u_form.save()
p_form.save()
messages.success(request,
f'Your account has been updated!')
return redirect('profile')
else:
u_form = UserUpdateForm(instance=request.user)
p_form = ProfileUpdateForm(instance=request.user.profile)
context = {
'u_form': u_form,
'p_form': p_form,
}
return render(request, 'users/profile.html', context)
- auto-resize image when uploading picture: in models.py, override the save method
def save(self):
super().save()
img = Image.open(self.image.path)
if img.height > 300 or img.width > 300:
output_size = (300, 300)
img.thumbnail(output_size)
img.save(self.image.path)
- display the image of the author beside each post: blog/home.html
<img class="rounded-circle article-img" src="{{ post.author.profile.image.url }}">
function view VS class-based view
- ListView, DetailView, CreateView, UpdateView, DeleteView
- in blog/views.py, create a ListView for model Post. We can change the template django was looking for with template_name
- in blog/urls.py:
from .views import PostListView
urlpatterns = [
# path('', views.home, name='blog-home'),
path('', PostListView.as_view(), name='blog-home'),
path('about/', views.about, name='blog-about'),
]
- by default, ListView will call the variable object_list instead of posts, so we can either go into the template and change it so it's looping over object_list
- or we can set one more variable in our ListView and let the class know that we want that variable to be called "posts" instead
- to see the latest post at the top, we need to change the order our query is making to the database:
- in our ListView, add an ordering attribute with the field we want to order on, the minus sign will reverse the order
from django.views.generic import ListView
# ------------function view
def home(request):
context = {
'posts': Post.objects.all()
}
# return HttpResponse('<h1>Blog Home</h1>')
return render(request, 'blog/home.html', context)
# ------------class-based view
class PostListView(ListView):
model = Post
template_name = 'blog/home.html' # <app>/<model>_<viewtype>.html
context_object_name = 'posts'
ordering = ['-date_posted']
a detail view: a view for individual post
- views.py
class PostDetailView(DetailView):
model = Post
- urls.py: pk is the primary key of the post we want to view, pk will be passed to the class-based view
path('post/<int:pk>/', PostDetailView.as_view(), name='post-detail'),
- create a template that'll desplay our post details
- in views, by default, our generic class-based view will be looking for a template with the naming convention: /_.html
- so we need to create a template: blog/templates/blog/post_detail.html
- within this html, change "post" to "object"
- now we can add links to the routes for the individual post on the home page:
- home.html
<h2><a class="article-title" href="{% url 'post-detail' post.id %}">{{ post.title }}</a></h2>
CreateView UpdateView DeleteView
- for CreateView, it'll share a template with the UpdateView, named _form.html
- we want the author to be the current logged-in user, so we can override the form_valid method for our CreateView
- this will allow us to add the author before the form is submitted
class PostCreateView(CreateView):
model = Post
fields = ['title', 'content']
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
- in Post model, we need to create the get_absolute_url method so that django knows how to find the location to a specific post
- the reverse function will simply return the full url to a route as a string
- whereas the redirect function will redirect you to a specific route
- in blog/models.py reverse() need two args: the full path of the route we want to get, and a specific post with a primary key
from django.urls import reverse
class Post(models.Model):
# ...
def get_absolute_url(self):
return reverse('post-detail', kwargs={'pk':self.pk})
- if you just want to go to the home page, you can set an attribute in CreateView called success_url and set that to the home page
- We shouldn't be able to create a post unless we're logged in, if not, we'll be directed to the login page
- for function-based views (user profile page we created) login_required decorator was used
- for class-based views, we use login mixin, which is a class we inherit from that'll add that login functionality to the view
from django.contrib.auth.mixins import LoginRequiredMixin
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
fields = ['title', 'content']
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
- UpdateView in urls.py, we provide the primary key in the url to the post that we wanna update
- the template for UpdateView just uses the same post_form template that we created for the CreateView
path('post/<int:pk>/update/', PostUpdateView.as_view(), name='post-update'),
- we only want the people who wrote the post to be able to edit it, so add another mixin
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Post
fields = ['title', 'content']
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
def test_func(self):
post = self.get_object()
if self.request.user == post.author:
return True
return False
- DeleteView, we want users to be logged in and is the author of the post in order to see the DeleteView
- template is a form that just ask us if we're sure to wanna delete a post, if we submit the form, the post will be deleted
- blog/post_confirm_delete.html
{% extends "blog/base.html" %}
{% block content %}
<div class="content-section">
<form method="POST">
{% csrf_token %}
<fieldset class="form-group">
<legend class="border-bottom mb-4">Delete Post</legend>
<h2>Are you sure you want to delete the post "{{ object.title }}"</h2>
</fieldset>
<div class="form-group">
<button class="btn btn-outline-danger" type="submit">Yes, Delete</button>
<a class="btn btn-outline-secondary" href="{% url 'post-detail' object.id %}">Cancel</a>
</div>
</form>
</div>
{% endblock content %}
- add links to these routes
- put a link to navigation bar to create a new post
- blog/base.html
<a class="nav-item nav-link" href="{% url 'post-create' %}">New Post</a>
- links to update and delete the post
- blog/post_detail.html
{% if object.author == user %}
<div>
<a class="btn btn-secondary btn-sm mt-1 mb-1" href="{% url 'post-update' object.id %}">Update</a>
<a class="btn btn-danger btn-sm mt-1 mb-1" href="{% url 'post-delete' object.id %}">Delete</a>
</div>
{% endif %}
Pagination
- use json file data to create posts and work with paginator object
(PROJEC~1) C:\Users\Jiayi Su\Desktop\Djangoenv\django_project>py manage.py shell
Python 3.7.2 (tags/v3.7.2:9a3ffc0492, Dec 23 2018, 23:09:28) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> import json
>>> from blog.models import Post
>>> with open('posts.json') as f:
... post_json = json.load(f)
...
>>> for post in post_json:
... post = Post(title=post['title'], content=post['content'], author_id=post['user_id'])
... post.save()
...
>>> from django.core.paginator import Paginator
>>> posts = ['1', '2', '3', '4', '5']
>>> p = Paginator(posts, 2)
>>> p.num_pages
3
>>> for page in p.page_range:
... print(page)
...
1
2
3
>>> p1 = p.page(1) # to look at a specific page (page 1)
>>>
>>> p1
<Page 1 of 3>
>>> p1.number # the number of that page
1
>>> p1.object_list
['1', '2']
>>> p1.has_previous()
False
>>> p1.has_next()
True
>>> p1.next_page_number()
2
>>>
- pagination of home page
{% if is_paginated %}
{% if page_obj.has_previous %}
<a class="btn btn-outline-info mb-4" href="?page=1">First</a>
<a class="btn btn-outline-info mb-4" href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<a class="btn btn-info mb-4" href="?page={{ num }}">{{ num }}</a>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<a class="btn btn-outline-info mb-4" href="?page={{ num }}">{{ num }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<a class="btn btn-outline-info mb-4" href="?page={{ page_obj.next_page_number }}">Next</a>
<a class="btn btn-outline-info mb-4" href="?page={{ page_obj.paginator.num_pages }}">Last</a>
{% endif %}
{% endif %}
- posts of a specific author
class PostListView(ListView):
model = Post
template_name = 'blog/home.html' # <app>/<model>_<viewtype>.html
context_object_name = 'posts'
ordering = ['-date_posted']
paginate_by = 5
class UserPostListView(ListView):
model = Post
template_name = 'blog/user_posts.html' # <app>/<model>_<viewtype>.html
context_object_name = 'posts'
paginate_by = 5
def get_queryset(self):
user = get_object_or_404(User, username=self.kwargs.get('username'))
return Post.objects.filter(author=user).order_by('-date_posted')
use email to let users reset password
- email info
[email protected] to me
You're receiving this email because you requested a password reset for your user account at 127.0.0.1:8000.
Please go to the following page and choose a new password:
http://127.0.0.1:8000/password-reset-confirm/MQ/558-5f8a6d2c1592b17b0f87/
Your username, in case you've forgotten: jiayisu
Thanks for using our site!
The 127.0.0.1:8000 team
- django_project/urls.py
path('password-reset/', auth_views.PasswordResetView.as_view(template_name='users/password_reset.html'),
name='password_reset'),
path('password-reset/done/', auth_views.PasswordResetDoneView.as_view(template_name='users/password_reset_done.html'),
name='password_reset_done'),
path('password-reset-confirm/<uidb64>/<token>/',
auth_views.PasswordResetConfirmView.as_view(template_name='users/password_reset_confirm.html'),
name='password_reset_confirm'),
path('password-reset-complete/', auth_views.PasswordResetCompleteView.as_view(template_name='users/password_reset_complete.html'),
name='password_reset_complete'),
- django_project/settings.py
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.environ.get('EMAIL_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_PASS')
- password_reset_comfirm.html
{% extends "blog/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="content-section">
<form method="POST">
{% csrf_token %}
<fieldset class="form-group">
<legend class="border-bottom mb-4">Reset Password</legend>
{{ form|crispy }}
</fieldset>
<div class="form-group">
<button class="btn btn-outline-info" type="submit">Reset Password</button>
</div>
</form>
</div>
{% endblock content %}
- create a link for users to reset password on login page
<div class="form-group">
<button class="btn btn-outline-info" type="submit">Log In</button>
<small class="text-muted ml-2">
<a href="{% url 'password_reset' %}">Forgot Password?</a>
</small>
</div>
- room for improvements:
- unitest
- deploy on different platforms
- send longer running request up to a message queue and make it a synchronous
- add a commenting system, a search feature