')
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/B-Application-And-Routes/django_project/db.sqlite3 b/Django_Blog/00-Practices/B-Application-And-Routes/django_project/db.sqlite3
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/B-Application-And-Routes/django_project/django_project/__init__.py b/Django_Blog/00-Practices/B-Application-And-Routes/django_project/django_project/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/B-Application-And-Routes/django_project/django_project/asgi.py b/Django_Blog/00-Practices/B-Application-And-Routes/django_project/django_project/asgi.py
new file mode 100644
index 000000000..c6cc048ed
--- /dev/null
+++ b/Django_Blog/00-Practices/B-Application-And-Routes/django_project/django_project/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for django_project project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')
+
+application = get_asgi_application()
diff --git a/Django_Blog/00-Practices/B-Application-And-Routes/django_project/django_project/settings.py b/Django_Blog/00-Practices/B-Application-And-Routes/django_project/django_project/settings.py
new file mode 100644
index 000000000..555eab2f8
--- /dev/null
+++ b/Django_Blog/00-Practices/B-Application-And-Routes/django_project/django_project/settings.py
@@ -0,0 +1,120 @@
+"""
+Django settings for django_project project.
+
+Generated by 'django-admin startproject' using Django 3.1.7.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.1/ref/settings/
+"""
+
+from pathlib import Path
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = '5*=-(6^ld0#*l0o)2&b$8m%aitohgehj%o$l%z22@k71ie6*m5'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'django_project.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'django_project.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': BASE_DIR / 'db.sqlite3',
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.1/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.1/howto/static-files/
+
+STATIC_URL = '/static/'
diff --git a/Django_Blog/00-Practices/B-Application-And-Routes/django_project/django_project/urls.py b/Django_Blog/00-Practices/B-Application-And-Routes/django_project/django_project/urls.py
new file mode 100644
index 000000000..5a2984ce2
--- /dev/null
+++ b/Django_Blog/00-Practices/B-Application-And-Routes/django_project/django_project/urls.py
@@ -0,0 +1,44 @@
+"""django_project URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/3.1/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+
+# This is project urls
+
+from django.contrib import admin
+from django.urls import path, include
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+
+ ######################################################
+ #In the case of multiple apps in a project,
+ # the app 'blog' as example.
+ #
+ #blog/ and blog/* are mapped to 'blog.urls'
+ #ie. http://127.0.0.1:8000/blog
+ # http://127.0.0.1:8000/blog/about
+ ######################################################
+ #path('blog/', include('blog.urls')),
+
+ ######################################################
+ #In the case of only one app ('blog') in a project,
+ #
+ #/ and /* are mapped to 'blog.urls'
+ #ie. http://127.0.0.1:8000/
+ # http://127.0.0.1:8000/about
+ ######################################################
+ path('', include('blog.urls')),
+
+]
diff --git a/Django_Blog/00-Practices/B-Application-And-Routes/django_project/django_project/wsgi.py b/Django_Blog/00-Practices/B-Application-And-Routes/django_project/django_project/wsgi.py
new file mode 100644
index 000000000..54bf5eb73
--- /dev/null
+++ b/Django_Blog/00-Practices/B-Application-And-Routes/django_project/django_project/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for django_project project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')
+
+application = get_wsgi_application()
diff --git a/Django_Blog/00-Practices/B-Application-And-Routes/django_project/manage.py b/Django_Blog/00-Practices/B-Application-And-Routes/django_project/manage.py
new file mode 100644
index 000000000..5ffd8de1e
--- /dev/null
+++ b/Django_Blog/00-Practices/B-Application-And-Routes/django_project/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/Django_Blog/00-Practices/B-Application-And-Routes/zreadme.txt b/Django_Blog/00-Practices/B-Application-And-Routes/zreadme.txt
new file mode 100644
index 000000000..b692a4b53
--- /dev/null
+++ b/Django_Blog/00-Practices/B-Application-And-Routes/zreadme.txt
@@ -0,0 +1,42 @@
+
+# Note: single project directory can holds multiple apps.
+# Also, you can take single app add to multiple projects
+
+##############################################################
+# Project directory: django_project/django_project
+# App directory: django_project/blog
+##############################################################
+
+# create 'blog' app
+1. django_project> python manage.py startapp blog # blog package is created
+
+# we can use 'tree' command to view the directory structures
+
+2. django_project\blog\views.py
+ - add home()
+ - add about()
+
+3. blog\urls.py
+ - add the relative path '' route to views.home()
+ - add the relative path 'about/' route to views.about()
+ path('', views.home, name='blog-home'), ####<<<<<<<<<<<###
+ #^
+4. add 'blog' urls into project urls #^
+ django_project/urls.py #^
+ - first way: #^
+ path('blog/', include('blog.urls')), ####>>>>>>>>>>>###
+ # Note: the url with blog/ and blog/* would map to this path
+ # --> http://127.0.0.1:8000/blog mapped to 'blog.urls' --> then "views.home"
+ # --> http://127.0.0.1:8000/blog/about mapped to 'blog.urls' --> then "views.about"
+ OR:
+ - second way
+ path('', include('blog.urls')),
+ # --> http://127.0.0.1:8000 mapped to 'blog.urls' --> then "views.home"
+
+5. Test the website
+ # if web server not UP: django_project> python manage.py runserver
+ http://127.0.0.1:8000/blog #(first way)
+ http://127.0.0.1:8000/blog/about #(first way)
+ http://127.0.0.1:8000/ #(second way)
+ http://127.0.0.1:8000/about #(second way)
+
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/blog/__init__.py b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/blog/admin.py b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/admin.py
new file mode 100644
index 000000000..8c38f3f3d
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/blog/apps.py b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/apps.py
new file mode 100644
index 000000000..793058786
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class BlogConfig(AppConfig):
+ name = 'blog'
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/blog/migrations/__init__.py b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/blog/models.py b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/models.py
new file mode 100644
index 000000000..71a836239
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/blog/templates/blog/about.html b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/templates/blog/about.html
new file mode 100644
index 000000000..8a620d248
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/templates/blog/about.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Blog About
+
+
+
Blog About !
+
+
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/blog/templates/blog/home.html b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/templates/blog/home.html
new file mode 100644
index 000000000..8b702f3b4
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/templates/blog/home.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Blog
+
+
+
Blog Home !
+
+
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/blog/tests.py b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/tests.py
new file mode 100644
index 000000000..7ce503c2d
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/blog/urls.py b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/urls.py
new file mode 100644
index 000000000..9b38e211e
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/urls.py
@@ -0,0 +1,12 @@
+# This is blog urls
+
+from django.urls import path
+from . import views
+
+# home() page rounte
+urlpatterns = [
+ #The empty('') path is mapped to home() in views.py
+ path('', views.home, name='blog-home'),
+ #The ('about/') path is mapped to about() in views.py
+ path('about/', views.about, name='blog-about'),
+]
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/blog/views.py b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/views.py
new file mode 100644
index 000000000..e18b7c8cd
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-1/django_project/blog/views.py
@@ -0,0 +1,8 @@
+from django.shortcuts import render
+from django.http import HttpResponse
+
+def home(request) :
+ return render(request, 'blog/home.html')
+
+def about(request) :
+ return render(request, 'blog/about.html')
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/db.sqlite3 b/Django_Blog/00-Practices/C-Templates-1/django_project/db.sqlite3
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/django_project/__init__.py b/Django_Blog/00-Practices/C-Templates-1/django_project/django_project/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/django_project/asgi.py b/Django_Blog/00-Practices/C-Templates-1/django_project/django_project/asgi.py
new file mode 100644
index 000000000..c6cc048ed
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-1/django_project/django_project/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for django_project project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')
+
+application = get_asgi_application()
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/django_project/settings.py b/Django_Blog/00-Practices/C-Templates-1/django_project/django_project/settings.py
new file mode 100644
index 000000000..fbb2b74b1
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-1/django_project/django_project/settings.py
@@ -0,0 +1,121 @@
+"""
+Django settings for django_project project.
+
+Generated by 'django-admin startproject' using Django 3.1.7.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.1/ref/settings/
+"""
+
+from pathlib import Path
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = '5*=-(6^ld0#*l0o)2&b$8m%aitohgehj%o$l%z22@k71ie6*m5'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'blog.apps.BlogConfig',
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'django_project.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'django_project.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': BASE_DIR / 'db.sqlite3',
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.1/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.1/howto/static-files/
+
+STATIC_URL = '/static/'
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/django_project/urls.py b/Django_Blog/00-Practices/C-Templates-1/django_project/django_project/urls.py
new file mode 100644
index 000000000..5a2984ce2
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-1/django_project/django_project/urls.py
@@ -0,0 +1,44 @@
+"""django_project URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/3.1/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+
+# This is project urls
+
+from django.contrib import admin
+from django.urls import path, include
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+
+ ######################################################
+ #In the case of multiple apps in a project,
+ # the app 'blog' as example.
+ #
+ #blog/ and blog/* are mapped to 'blog.urls'
+ #ie. http://127.0.0.1:8000/blog
+ # http://127.0.0.1:8000/blog/about
+ ######################################################
+ #path('blog/', include('blog.urls')),
+
+ ######################################################
+ #In the case of only one app ('blog') in a project,
+ #
+ #/ and /* are mapped to 'blog.urls'
+ #ie. http://127.0.0.1:8000/
+ # http://127.0.0.1:8000/about
+ ######################################################
+ path('', include('blog.urls')),
+
+]
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/django_project/wsgi.py b/Django_Blog/00-Practices/C-Templates-1/django_project/django_project/wsgi.py
new file mode 100644
index 000000000..54bf5eb73
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-1/django_project/django_project/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for django_project project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')
+
+application = get_wsgi_application()
diff --git a/Django_Blog/00-Practices/C-Templates-1/django_project/manage.py b/Django_Blog/00-Practices/C-Templates-1/django_project/manage.py
new file mode 100644
index 000000000..5ffd8de1e
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-1/django_project/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/Django_Blog/00-Practices/C-Templates-1/zreadme.txt b/Django_Blog/00-Practices/C-Templates-1/zreadme.txt
new file mode 100644
index 000000000..fbe0454c7
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-1/zreadme.txt
@@ -0,0 +1,26 @@
+
+1. add 'blog' app into project installed list; so django knows where to look
+ for blog template files, and blog database later.
+ - The recommended way is to add blog configuration to project installed list.
+ ie. add 'BlogConfig' in blog/apps.py to django_project/settings.py
+ INSTALLED_APPS = [
+ 'blog.apps.BlogConfig',
+ :::::::::
+ ]
+
+2. create django_project/blog/templates
+3. create django_project/blog/templates/blog
+
+# Templates directory structure:
+ blog -> templates -> blog -> templates.html
+
+4. create templates:
+ blog/templates/blog/home.html
+ blog/templates/blog/about.html
+
+5. modify blog/views.py to render templates (home.html, about.html)
+
+6. test website
+ http://127.0.0.1:8000/
+ http://127.0.0.1:8000/about
+
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/__init__.py b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/admin.py b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/admin.py
new file mode 100644
index 000000000..8c38f3f3d
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/apps.py b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/apps.py
new file mode 100644
index 000000000..793058786
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class BlogConfig(AppConfig):
+ name = 'blog'
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/migrations/__init__.py b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/models.py b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/models.py
new file mode 100644
index 000000000..71a836239
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/templates/blog/about.html b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/templates/blog/about.html
new file mode 100644
index 000000000..aed3302f3
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/templates/blog/about.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ {% if title %}
+ Django Blog - {{title}}
+ {% else %}
+ Django Blog
+ {% endif %}
+
+
+
Blog About !
+
+
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/templates/blog/home.html b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/templates/blog/home.html
new file mode 100644
index 000000000..ac3949309
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/templates/blog/home.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+ {% if title %}
+ Django Blog - {{title}}
+ {% else %}
+ Django Blog
+ {% endif %}
+
+
+
+ {% for post in posts %}
+
{{ post.title }}
+ By {{ post.author }} on {{ post.date_posted }}
+
{{ post.content }}
+ {% endfor %}
+
+
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/tests.py b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/tests.py
new file mode 100644
index 000000000..7ce503c2d
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/urls.py b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/urls.py
new file mode 100644
index 000000000..9b38e211e
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/urls.py
@@ -0,0 +1,12 @@
+# This is blog urls
+
+from django.urls import path
+from . import views
+
+# home() page rounte
+urlpatterns = [
+ #The empty('') path is mapped to home() in views.py
+ path('', views.home, name='blog-home'),
+ #The ('about/') path is mapped to about() in views.py
+ path('about/', views.about, name='blog-about'),
+]
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/views.py b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/views.py
new file mode 100644
index 000000000..de042db89
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/blog/views.py
@@ -0,0 +1,22 @@
+from django.shortcuts import render
+from django.http import HttpResponse
+
+posts = [
+ { 'author': 'Mike',
+ 'title': 'Blog Post 1',
+ 'content': 'First post content',
+ 'date_posted': 'March 7, 2021'
+ },
+ { 'author': 'Jeff',
+ 'title': 'Blog Post 2',
+ 'content': 'Second post content',
+ 'date_posted': 'March 8, 2021'
+ }
+]
+
+def home(request) :
+ context = { 'posts': posts }
+ return render(request, 'blog/home.html', context)
+
+def about(request) :
+ return render(request, 'blog/about.html', {'title': 'About'})
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/db.sqlite3 b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/db.sqlite3
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/django_project/__init__.py b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/django_project/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/django_project/asgi.py b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/django_project/asgi.py
new file mode 100644
index 000000000..c6cc048ed
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/django_project/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for django_project project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')
+
+application = get_asgi_application()
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/django_project/settings.py b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/django_project/settings.py
new file mode 100644
index 000000000..fbb2b74b1
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/django_project/settings.py
@@ -0,0 +1,121 @@
+"""
+Django settings for django_project project.
+
+Generated by 'django-admin startproject' using Django 3.1.7.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.1/ref/settings/
+"""
+
+from pathlib import Path
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = '5*=-(6^ld0#*l0o)2&b$8m%aitohgehj%o$l%z22@k71ie6*m5'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'blog.apps.BlogConfig',
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'django_project.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'django_project.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': BASE_DIR / 'db.sqlite3',
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.1/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.1/howto/static-files/
+
+STATIC_URL = '/static/'
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/django_project/urls.py b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/django_project/urls.py
new file mode 100644
index 000000000..5a2984ce2
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/django_project/urls.py
@@ -0,0 +1,44 @@
+"""django_project URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/3.1/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+
+# This is project urls
+
+from django.contrib import admin
+from django.urls import path, include
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+
+ ######################################################
+ #In the case of multiple apps in a project,
+ # the app 'blog' as example.
+ #
+ #blog/ and blog/* are mapped to 'blog.urls'
+ #ie. http://127.0.0.1:8000/blog
+ # http://127.0.0.1:8000/blog/about
+ ######################################################
+ #path('blog/', include('blog.urls')),
+
+ ######################################################
+ #In the case of only one app ('blog') in a project,
+ #
+ #/ and /* are mapped to 'blog.urls'
+ #ie. http://127.0.0.1:8000/
+ # http://127.0.0.1:8000/about
+ ######################################################
+ path('', include('blog.urls')),
+
+]
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/django_project/wsgi.py b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/django_project/wsgi.py
new file mode 100644
index 000000000..54bf5eb73
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/django_project/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for django_project project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')
+
+application = get_wsgi_application()
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/manage.py b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/manage.py
new file mode 100644
index 000000000..5ffd8de1e
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-2-fake-posts/django_project/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/Django_Blog/00-Practices/C-Templates-2-fake-posts/zreadme.txt b/Django_Blog/00-Practices/C-Templates-2-fake-posts/zreadme.txt
new file mode 100644
index 000000000..271f4f4fb
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-2-fake-posts/zreadme.txt
@@ -0,0 +1,15 @@
+
+1. add fake 'posts' in blog/views.py to demo
+ how to pass python variables into html templates
+
+2. add 'context = { 'posts': posts }' in home() in blog/views.py
+
+3. - update templates/blog/home.html to view the posts
+ - Add title in templates/blog/home.html
+ - Add title in templates/blog/about.html
+
+4. test website
+ django_project> python manage.py runserver
+ http://127.0.0.1:8000/
+ http://127.0.0.1:8000/about
+
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/__init__.py b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/admin.py b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/admin.py
new file mode 100644
index 000000000..8c38f3f3d
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/apps.py b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/apps.py
new file mode 100644
index 000000000..793058786
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class BlogConfig(AppConfig):
+ name = 'blog'
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/migrations/__init__.py b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/models.py b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/models.py
new file mode 100644
index 000000000..71a836239
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/templates/blog/about.html b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/templates/blog/about.html
new file mode 100644
index 000000000..6fb6380f4
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/templates/blog/about.html
@@ -0,0 +1,4 @@
+{% extends "blog/base.html" %}
+{% block content %}
+
About page coming soon
+{% endblock content %}
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/templates/blog/base.html b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/templates/blog/base.html
new file mode 100644
index 000000000..6018978b7
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/templates/blog/base.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ {% if title %}
+ Django Blog - {{title}}
+ {% else %}
+ Django Blog
+ {% endif %}
+
+
+ {% block content %} {% endblock %}
+
+
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/templates/blog/home.html b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/templates/blog/home.html
new file mode 100644
index 000000000..2c1282475
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/templates/blog/home.html
@@ -0,0 +1,9 @@
+{% extends "blog/base.html" %}
+{% block content %}
+
+ {% for post in posts %}
+
{{ post.title }}
+ By {{ post.author }} on {{ post.date_posted }}
+
{{ post.content }}
+ {% endfor %}
+{% endblock content %}
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/tests.py b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/tests.py
new file mode 100644
index 000000000..7ce503c2d
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/urls.py b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/urls.py
new file mode 100644
index 000000000..9b38e211e
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/urls.py
@@ -0,0 +1,12 @@
+# This is blog urls
+
+from django.urls import path
+from . import views
+
+# home() page rounte
+urlpatterns = [
+ #The empty('') path is mapped to home() in views.py
+ path('', views.home, name='blog-home'),
+ #The ('about/') path is mapped to about() in views.py
+ path('about/', views.about, name='blog-about'),
+]
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/views.py b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/views.py
new file mode 100644
index 000000000..de042db89
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/blog/views.py
@@ -0,0 +1,22 @@
+from django.shortcuts import render
+from django.http import HttpResponse
+
+posts = [
+ { 'author': 'Mike',
+ 'title': 'Blog Post 1',
+ 'content': 'First post content',
+ 'date_posted': 'March 7, 2021'
+ },
+ { 'author': 'Jeff',
+ 'title': 'Blog Post 2',
+ 'content': 'Second post content',
+ 'date_posted': 'March 8, 2021'
+ }
+]
+
+def home(request) :
+ context = { 'posts': posts }
+ return render(request, 'blog/home.html', context)
+
+def about(request) :
+ return render(request, 'blog/about.html', {'title': 'About'})
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/db.sqlite3 b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/db.sqlite3
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/django_project/__init__.py b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/django_project/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/django_project/asgi.py b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/django_project/asgi.py
new file mode 100644
index 000000000..c6cc048ed
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/django_project/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for django_project project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')
+
+application = get_asgi_application()
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/django_project/settings.py b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/django_project/settings.py
new file mode 100644
index 000000000..fbb2b74b1
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/django_project/settings.py
@@ -0,0 +1,121 @@
+"""
+Django settings for django_project project.
+
+Generated by 'django-admin startproject' using Django 3.1.7.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.1/ref/settings/
+"""
+
+from pathlib import Path
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = '5*=-(6^ld0#*l0o)2&b$8m%aitohgehj%o$l%z22@k71ie6*m5'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'blog.apps.BlogConfig',
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'django_project.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'django_project.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': BASE_DIR / 'db.sqlite3',
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.1/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.1/howto/static-files/
+
+STATIC_URL = '/static/'
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/django_project/urls.py b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/django_project/urls.py
new file mode 100644
index 000000000..5a2984ce2
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/django_project/urls.py
@@ -0,0 +1,44 @@
+"""django_project URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/3.1/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+
+# This is project urls
+
+from django.contrib import admin
+from django.urls import path, include
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+
+ ######################################################
+ #In the case of multiple apps in a project,
+ # the app 'blog' as example.
+ #
+ #blog/ and blog/* are mapped to 'blog.urls'
+ #ie. http://127.0.0.1:8000/blog
+ # http://127.0.0.1:8000/blog/about
+ ######################################################
+ #path('blog/', include('blog.urls')),
+
+ ######################################################
+ #In the case of only one app ('blog') in a project,
+ #
+ #/ and /* are mapped to 'blog.urls'
+ #ie. http://127.0.0.1:8000/
+ # http://127.0.0.1:8000/about
+ ######################################################
+ path('', include('blog.urls')),
+
+]
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/django_project/wsgi.py b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/django_project/wsgi.py
new file mode 100644
index 000000000..54bf5eb73
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/django_project/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for django_project project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')
+
+application = get_wsgi_application()
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/manage.py b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/manage.py
new file mode 100644
index 000000000..5ffd8de1e
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-3-base-template/django_project/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/Django_Blog/00-Practices/C-Templates-3-base-template/zreadme.txt b/Django_Blog/00-Practices/C-Templates-3-base-template/zreadme.txt
new file mode 100644
index 000000000..1e834d068
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-3-base-template/zreadme.txt
@@ -0,0 +1,11 @@
+
+1. create blog/templates/blog/base.html
+
+2. modify home.html, about.html to extend (inherit) base.html
+ by adding stuff in {% block content %} {% endblock %}
+
+3. test website
+ django_project> python manage.py runserver (if server not UP)
+ http://127.0.0.1:8000/
+ http://127.0.0.1:8000/about
+
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/__init__.py b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/admin.py b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/admin.py
new file mode 100644
index 000000000..8c38f3f3d
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/apps.py b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/apps.py
new file mode 100644
index 000000000..793058786
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class BlogConfig(AppConfig):
+ name = 'blog'
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/migrations/__init__.py b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/models.py b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/models.py
new file mode 100644
index 000000000..71a836239
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/templates/blog/about.html b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/templates/blog/about.html
new file mode 100644
index 000000000..6fb6380f4
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/templates/blog/about.html
@@ -0,0 +1,4 @@
+{% extends "blog/base.html" %}
+{% block content %}
+
About page coming soon
+{% endblock content %}
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/templates/blog/base.html b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/templates/blog/base.html
new file mode 100644
index 000000000..cb39f46ea
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/templates/blog/base.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+ {% if title %}
+ Django Blog - {{title}}
+ {% else %}
+ Django Blog
+ {% endif %}
+
+
+
+
+ {% block content %} {% endblock %}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/templates/blog/home.html b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/templates/blog/home.html
new file mode 100644
index 000000000..2c1282475
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/templates/blog/home.html
@@ -0,0 +1,9 @@
+{% extends "blog/base.html" %}
+{% block content %}
+
+ {% for post in posts %}
+
{{ post.title }}
+ By {{ post.author }} on {{ post.date_posted }}
+
{{ post.content }}
+ {% endfor %}
+{% endblock content %}
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/tests.py b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/tests.py
new file mode 100644
index 000000000..7ce503c2d
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/urls.py b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/urls.py
new file mode 100644
index 000000000..9b38e211e
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/urls.py
@@ -0,0 +1,12 @@
+# This is blog urls
+
+from django.urls import path
+from . import views
+
+# home() page rounte
+urlpatterns = [
+ #The empty('') path is mapped to home() in views.py
+ path('', views.home, name='blog-home'),
+ #The ('about/') path is mapped to about() in views.py
+ path('about/', views.about, name='blog-about'),
+]
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/views.py b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/views.py
new file mode 100644
index 000000000..de042db89
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/blog/views.py
@@ -0,0 +1,22 @@
+from django.shortcuts import render
+from django.http import HttpResponse
+
+posts = [
+ { 'author': 'Mike',
+ 'title': 'Blog Post 1',
+ 'content': 'First post content',
+ 'date_posted': 'March 7, 2021'
+ },
+ { 'author': 'Jeff',
+ 'title': 'Blog Post 2',
+ 'content': 'Second post content',
+ 'date_posted': 'March 8, 2021'
+ }
+]
+
+def home(request) :
+ context = { 'posts': posts }
+ return render(request, 'blog/home.html', context)
+
+def about(request) :
+ return render(request, 'blog/about.html', {'title': 'About'})
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/db.sqlite3 b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/db.sqlite3
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/django_project/__init__.py b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/django_project/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/django_project/asgi.py b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/django_project/asgi.py
new file mode 100644
index 000000000..c6cc048ed
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/django_project/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for django_project project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')
+
+application = get_asgi_application()
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/django_project/settings.py b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/django_project/settings.py
new file mode 100644
index 000000000..fbb2b74b1
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/django_project/settings.py
@@ -0,0 +1,121 @@
+"""
+Django settings for django_project project.
+
+Generated by 'django-admin startproject' using Django 3.1.7.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.1/ref/settings/
+"""
+
+from pathlib import Path
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = '5*=-(6^ld0#*l0o)2&b$8m%aitohgehj%o$l%z22@k71ie6*m5'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'blog.apps.BlogConfig',
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'django_project.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'django_project.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': BASE_DIR / 'db.sqlite3',
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.1/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.1/howto/static-files/
+
+STATIC_URL = '/static/'
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/django_project/urls.py b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/django_project/urls.py
new file mode 100644
index 000000000..5a2984ce2
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/django_project/urls.py
@@ -0,0 +1,44 @@
+"""django_project URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/3.1/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+
+# This is project urls
+
+from django.contrib import admin
+from django.urls import path, include
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+
+ ######################################################
+ #In the case of multiple apps in a project,
+ # the app 'blog' as example.
+ #
+ #blog/ and blog/* are mapped to 'blog.urls'
+ #ie. http://127.0.0.1:8000/blog
+ # http://127.0.0.1:8000/blog/about
+ ######################################################
+ #path('blog/', include('blog.urls')),
+
+ ######################################################
+ #In the case of only one app ('blog') in a project,
+ #
+ #/ and /* are mapped to 'blog.urls'
+ #ie. http://127.0.0.1:8000/
+ # http://127.0.0.1:8000/about
+ ######################################################
+ path('', include('blog.urls')),
+
+]
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/django_project/wsgi.py b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/django_project/wsgi.py
new file mode 100644
index 000000000..54bf5eb73
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/django_project/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for django_project project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')
+
+application = get_wsgi_application()
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/manage.py b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/manage.py
new file mode 100644
index 000000000..5ffd8de1e
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-4-bootstrap/django_project/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/Django_Blog/00-Practices/C-Templates-4-bootstrap/zreadme.txt b/Django_Blog/00-Practices/C-Templates-4-bootstrap/zreadme.txt
new file mode 100644
index 000000000..603b67ea9
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-4-bootstrap/zreadme.txt
@@ -0,0 +1,17 @@
+
+1. https://getbootstrap.com/docs/4.0/getting-started/introduction/
+ look for "Starter template"
+
+2. add bootstrap css and javascript in base template (blog/template/blog/base.html)
+
+3. test website
+ django_project> python manage.py runserver (if server not UP)
+ http://127.0.0.1:8000/
+ http://127.0.0.1:8000/about
+ Now you can see some margin be added by bootstrap
+
+
+
+
+
+
diff --git a/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/__init__.py b/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/admin.py b/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/admin.py
new file mode 100644
index 000000000..8c38f3f3d
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/apps.py b/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/apps.py
new file mode 100644
index 000000000..793058786
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class BlogConfig(AppConfig):
+ name = 'blog'
diff --git a/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/migrations/__init__.py b/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/models.py b/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/models.py
new file mode 100644
index 000000000..71a836239
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/static/blog/main.css b/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/static/blog/main.css
new file mode 100644
index 000000000..2d7785159
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/static/blog/main.css
@@ -0,0 +1,84 @@
+body {
+ background: #fafafa;
+ color: #333333;
+ margin-top: 5rem;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: #444444;
+}
+
+ul {
+ margin: 0;
+}
+
+.bg-steel {
+ background-color: #5f788a;
+}
+
+.site-header .navbar-nav .nav-link {
+ color: #cbd5db;
+}
+
+.site-header .navbar-nav .nav-link:hover {
+ color: #ffffff;
+}
+
+.site-header .navbar-nav .nav-link.active {
+ font-weight: 500;
+}
+
+.content-section {
+ background: #ffffff;
+ padding: 10px 20px;
+ border: 1px solid #dddddd;
+ border-radius: 3px;
+ margin-bottom: 20px;
+}
+
+.article-title {
+ color: #444444;
+}
+
+a.article-title:hover {
+ color: #428bca;
+ text-decoration: none;
+}
+
+.article-content {
+ white-space: pre-line;
+}
+
+.article-img {
+ height: 65px;
+ width: 65px;
+ margin-right: 16px;
+}
+
+.article-metadata {
+ padding-bottom: 1px;
+ margin-bottom: 4px;
+ border-bottom: 1px solid #e3e3e3
+}
+
+.article-metadata a:hover {
+ color: #333;
+ text-decoration: none;
+}
+
+.article-svg {
+ width: 25px;
+ height: 25px;
+ vertical-align: middle;
+}
+
+.account-img {
+ height: 125px;
+ width: 125px;
+ margin-right: 20px;
+ margin-bottom: 16px;
+}
+
+.account-heading {
+ font-size: 2.5rem;
+}
diff --git a/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/templates/blog/about.html b/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/templates/blog/about.html
new file mode 100644
index 000000000..6fb6380f4
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/templates/blog/about.html
@@ -0,0 +1,4 @@
+{% extends "blog/base.html" %}
+{% block content %}
+
About page coming soon
+{% endblock content %}
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/templates/blog/base.html b/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/templates/blog/base.html
new file mode 100644
index 000000000..b31fecf21
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/templates/blog/base.html
@@ -0,0 +1,70 @@
+{% load static %}
+
+
+
+
+
+
+
+
+
+
+ {% if title %}
+ Django Blog - {{title}}
+ {% else %}
+ Django Blog
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+ {% block content %}{% endblock %}
+
+
+
+
Our Sidebar
+
You can put any information here you'd like.
+
+
Latest Posts
+
Announcements
+
Calendars
+
etc
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/templates/blog/home.html b/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/templates/blog/home.html
new file mode 100644
index 000000000..a5b44d0fa
--- /dev/null
+++ b/Django_Blog/00-Practices/C-Templates-5-navbar/django_project/blog/templates/blog/home.html
@@ -0,0 +1,18 @@
+{% extends "blog/base.html" %}
+
+{% block content %}
+ {% for post in posts %}
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-E-CUD/run.py b/Python/Flask_Blog/00-Practices/00-Practices-E-CUD/run.py
new file mode 100644
index 000000000..0c8390926
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-E-CUD/run.py
@@ -0,0 +1,7 @@
+from flaskblog import app
+
+# flaskblog is package
+# import app from flaskblog package
+
+if __name__ == '__main__':
+ app.run(debug=True)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-E-CUD/zreadme_CUD.txt b/Python/Flask_Blog/00-Practices/00-Practices-E-CUD/zreadme_CUD.txt
new file mode 100644
index 000000000..260b82986
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-E-CUD/zreadme_CUD.txt
@@ -0,0 +1,47 @@
+email: C@demo.com
+password: password
+
+Create, Update, Delete posts
+
+- add post/new in routes05.py
+- create template create_post.html
+- add PostForm class in form05.py
+- pass form into template in route05.py
+- update create_post.html, ie. copy login.html then modify
+- add "New Post" navbar in layout11_flash.html
+- update __init__.py with routes05
+- test website
+- add post into database in routes06.py
+- remove the dummy posts in routes06.py
+- update posts from database in home() route in routes06.py
+- update home12_post.html to display the post
+- update route06 with home12_post.html
+- test website
+
+Edit a single post
+- Add /post/ in route06.py
+- create post.html (copy home.html and modify)
+- update home12_post.html post link with post_id
+- Add /post//update in route06.py
+ add legend in "Update Post" and "New Post"
+- Use the same template(create_post.html) as "/post/new" by adding legend
+- Update /post//update in route06.py to store post into database
+- test website
+ http://localhost:5000/post/1
+ http://localhost:5000/post/1/update
+
+- add "Update" button in post01.html
+- Add "Delete" button in post01.html with bootstrap modal
+ https://getbootstrap.com/docs/4.0/components/modal/
+ Look for "Live demo"
+ Change "exampleModal" to "deleteModal"
+- Edit Model block in post02.html for delete post
+ "Delete" button route to delete_post(), and pass post_id
+ "Delete" sends "POST" request to delete the post
+- Add delete_post route in route06.py
+
+
+
+
+
+
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/__init__.py b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/__init__.py
new file mode 100644
index 000000000..9e7433ce6
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/__init__.py
@@ -0,0 +1,25 @@
+from flask import Flask
+from flask_sqlalchemy import SQLAlchemy
+from flask_bcrypt import Bcrypt
+from flask_login import LoginManager
+
+
+app = Flask(__name__)
+
+# $ python
+# >>> import secrets
+# >>> secrets.token_hex(16)
+app.config['SECRET_KEY'] = 'ada8419b155676bc78c2296aba9c7c7d'
+# /// represents the relative directory from the current file (flaskblog.py).
+# that is, site.db has the same directory as this __init__.py
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
+
+db = SQLAlchemy(app)
+bcrypt = Bcrypt(app) # Used to hash the password
+login_manager = LoginManager(app)
+# This (login_view) tells where is the login route located
+login_manager.login_view = 'login' # which is login() in routes.py
+login_manager.login_message_category = 'info' #add bootstrap class to make nice view for flash message
+
+# import routes from flaskblog package
+from flaskblog import routes07
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/forms05.py b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/forms05.py
new file mode 100644
index 000000000..eacc9ad85
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/forms05.py
@@ -0,0 +1,76 @@
+from flask_wtf import FlaskForm
+from flask_wtf.file import FileField, FileAllowed
+from flask_login import current_user
+from wtforms import StringField, PasswordField, SubmitField, BooleanField, TextAreaField
+from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
+from flaskblog.models import User
+
+# $ pip3 install flask_wtf
+# $ pip3 install email_validator
+
+# This module is imported in flaskblog.py to create forms
+
+class RegistrationForm (FlaskForm) :
+ # 'Username' in html pass objects in validators
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ confirm_password = PasswordField('Confirm Password',
+ validators=[DataRequired(), EqualTo('password')])
+ submit = SubmitField('Sign up')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ def validate_username(self, username) :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ def validate_email(self, email) :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class LoginForm (FlaskForm) :
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ # This (remember) allows users to stay login for certain time
+ # after web browser closes by using security cookies.
+ remember = BooleanField('Remember me')
+ submit = SubmitField('Login')
+
+# Copy RegistrationForm to modify
+class UpdateAccountForm (FlaskForm) :
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ picture = FileField('Update profile picture', validators=[FileAllowed(['jpg','png'])])
+ submit = SubmitField('Update')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ # validate only when username is not current_user's username
+ def validate_username(self, username) :
+ if username.data != current_user.username :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ # validate only when email is not current_user's email
+ def validate_email(self, email) :
+ if email.data != current_user.email :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class PostForm(FlaskForm) :
+ title = StringField('Title', validators=[DataRequired()])
+ content = TextAreaField('Content', validators=[DataRequired()])
+ submit = SubmitField('Post')
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/models.py b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/models.py
new file mode 100644
index 000000000..9beaaa7a2
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/models.py
@@ -0,0 +1,48 @@
+from datetime import datetime
+from flaskblog import db, login_manager
+from flask_login import UserMixin
+
+### !!! Generate database tables structure first
+### !!! by following zreadme_package.txt
+
+@login_manager.user_loader
+def load_user(user_id) :
+ return User.query.get(int(user_id))
+
+# defines User table structure in database
+class User(db.Model, UserMixin) :
+ # primary_key specifies unique id
+ id = db.Column(db.Integer, primary_key=True)
+ # max 20 characters, unique, and required
+ username = db.Column(db.String(20), unique=True, nullable=False)
+ # max 120 characters, unique, and required
+ email = db.Column(db.String(120), unique=True, nullable=False)
+ # profile image
+ image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
+ # password (be hashed)
+ password = db.Column(db.String(60), nullable=False)
+ # This is not a column, but build relationship between post and author
+ # 'Post' class being passed
+ # backref 'author' is reference to the user by post.author
+ posts = db.relationship('Post', backref='author', lazy=True)
+
+ # function to print this class (User)
+ def __repr__(self) :
+ return f"User('{self.username}','{self.email}','{self.image_file}')"
+
+# defines Post table structure in database
+class Post(db.Model) :
+ id = db.Column(db.Integer, primary_key=True)
+ title = db.Column(db.String(100), nullable=False)
+ # type of DateTime, the current time as default (utcnow)
+ # The default is passed as the function name of utcnow. Do not use
+ # utcnow(), which can invoke the function right away.
+ date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+ content = db.Column(db.Text, nullable=False)
+ # point to user who creates this post
+ # 'user' in 'user.id' is reference to an user in the User table
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
+
+ # function to print this class (Post)
+ def __repr__(self) :
+ return f"Post('{self.title}','{self.date_posted}')"
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/routes07.py b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/routes07.py
new file mode 100644
index 000000000..249ea1fa1
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/routes07.py
@@ -0,0 +1,176 @@
+from flask import render_template, url_for, flash, redirect, request, abort
+from flaskblog import app, db, bcrypt
+from flaskblog.forms05 import RegistrationForm, LoginForm, UpdateAccountForm, PostForm
+from flaskblog.models import User, Post
+from flask_login import login_user, current_user, logout_user, login_required
+import os, secrets
+from PIL import Image
+
+# forms.py in flaskblog package
+# models.py in flaskblog package
+
+# http://localhost:5000/
+@app.route("/")
+@app.route("/home")
+def home():
+ # get page number from url, default is first page (1),
+ # the type (int) is used to make sure page is integer.
+ page = request.args.get('page', 1, type=int)
+ posts = Post.query.order_by(Post.date_posted.desc()).paginate(page=page, per_page=2)
+ return render_template('home13_paginate.html', posts=posts)
+
+# http://localhost:5000/about
+@app.route("/about")
+def about():
+ return render_template('about11_flash.html', title='About')
+
+@app.route("/register", methods=['GET', 'POST'])
+def register():
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = RegistrationForm()
+ # once submit successfully, flash message shows in the placehold in layout.html
+ # flash message is one time only, it disappear after refresh web browser
+ if form.validate_on_submit():
+ # hash the password
+ hashed_pwd = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
+ user = User(username=form.username.data, email=form.email.data, password=hashed_pwd)
+ db.session.add(user)
+ db.session.commit()
+ flash('Your account has been created! You are now able to log in', 'success')
+ # 'home' is function of home() above
+ return redirect(url_for('login'))
+ return render_template('register12_error.html', title='Register', form=form)
+
+@app.route("/login", methods=['GET', 'POST'])
+def login():
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = LoginForm()
+ if form.validate_on_submit():
+ user = User.query.filter_by(email=form.email.data).first()
+ if user and bcrypt.check_password_hash(user.password, form.password.data) :
+ login_user(user, remember=form.remember.data)
+ # request.args is dictionary type
+ next_page = request.args.get('next')
+ # 'home' is function of home() above
+ return redirect(next_page) if next_page else redirect(url_for('home'))
+ else :
+ flash('Login failed. Please check email and password.', 'danger')
+ return render_template('login13.html', title='Login', form=form)
+
+
+@app.route("/logout")
+def logout() :
+ logout_user()
+ return redirect(url_for('home'))
+
+# save uploaded image to the specified path
+def save_picture(form_picture) :
+ random_hex = secrets.token_hex(8)
+ # underscore (_) ignoring the value
+ _, f_ext = os.path.splitext(form_picture.filename)
+ picture_fn = random_hex + f_ext
+ # define the path to store profile image
+ picture_path = os.path.join(app.root_path, 'static/profile_pics', picture_fn)
+
+ # save the uploaded image into the specified path
+
+ # do not want to save the original uploaded image, which can be very large
+ #form_picture.save(picture_path)
+
+ #resize the image before store
+ output_size = (125, 125)
+ i = Image.open(form_picture)
+ i.thumbnail(output_size)
+ i.save(picture_path)
+
+ return picture_fn
+
+# login_required decorator makes sure to access the page only
+# when the user log in
+@app.route("/account", methods=['GET', 'POST'])
+@login_required
+def account() :
+ # this form is for user to update the profile
+ form = UpdateAccountForm()
+ if form.validate_on_submit() :
+ if form.picture.data :
+ picture_file = save_picture(form.picture.data)
+ current_user.image_file = picture_file
+ # think of current_user is reference to the User in database
+ current_user.username = form.username.data
+ current_user.email = form.email.data
+ db.session.commit()
+ flash('Your account has been updated!', 'success')
+ return redirect(url_for('account'))
+ elif request.method == 'GET' :
+ # populate the username and email so that
+ # the new username and email can be viewed right after
+ # update form is submitted
+ form.username.data = current_user.username
+ form.email.data = current_user.email
+ # user.image_file is defined in User() in models.py from database
+ image_file = url_for('static', filename='profile_pics/'+current_user.image_file)
+ return render_template('account04.html', title='Account', image_file=image_file, form=form)
+
+@app.route("/post/new", methods=['GET', 'POST'])
+@login_required
+def new_post() :
+ form = PostForm()
+ if form.validate_on_submit():
+ # Construct Post object which defined in models.py
+ post = Post(title=form.title.data, content=form.content.data, author=current_user)
+ db.session.add(post)
+ db.session.commit()
+ flash('Your post has been created!', 'success')
+ return redirect(url_for('home'))
+ return render_template('create_post.html', title='New Post', form=form, legend="New Post")
+
+
+@app.route("/post/")
+def post(post_id) :
+ # find the post or give page not found 404 error
+ post = Post.query.get_or_404(post_id)
+ return render_template('post02.html', title=post.title, post=post)
+
+@app.route("/post//update", methods=['GET', 'POST'])
+@login_required
+def update_post(post_id) :
+ post = Post.query.get_or_404(post_id)
+ if post.author != current_user:
+ abort(403)
+ form = PostForm()
+ if form.validate_on_submit():
+ post.title = form.title.data
+ post.content = form.content.data
+ db.session.commit()
+ flash('Your post has been updated!', 'success')
+ return redirect(url_for('post', post_id=post.id))
+ elif request.method == 'GET' :
+ form.title.data = post.title
+ form.content.data = post.content
+ return render_template('create_post.html', title='Update Post', form=form, legend="Update Post")
+
+# "Delete" button in Modal sends POST request to delete the post
+@app.route("/post//delete", methods=['POST'])
+@login_required
+def delete_post(post_id) :
+ post = Post.query.get_or_404(post_id)
+ if post.author != current_user:
+ abort(403)
+ db.session.delete(post)
+ db.session.commit()
+ flash('Your post has been deleted!', 'success')
+ return redirect(url_for('home'))
+
+
+@app.route("/user/")
+def user_posts(username) :
+ page = request.args.get('page', 1, type=int)
+ # first_or_404: first user or give 404 if no user is found
+ user = User.query.filter_by(username=username).first_or_404()
+ posts = Post.query.filter_by(author=user)\
+ .order_by(Post.date_posted.desc())\
+ .paginate(page=page, per_page=2)
+ return render_template('user_posts.html', posts=posts, user=user)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/site.db b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/site.db
new file mode 100644
index 000000000..29ca75e11
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/site.db differ
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/static/main.css b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/static/main.css
new file mode 100644
index 000000000..730e2bca6
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/static/main.css
@@ -0,0 +1,80 @@
+body {
+ background: #fafafa;
+ color: #333333;
+ margin-top: 5rem;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: #444444;
+}
+
+.bg-steel {
+ background-color: #5f788a;
+}
+
+.site-header .navbar-nav .nav-link {
+ color: #cbd5db;
+}
+
+.site-header .navbar-nav .nav-link:hover {
+ color: #ffffff;
+}
+
+.site-header .navbar-nav .nav-link:active {
+ font-weight: 500;
+}
+
+.content-section {
+ background: #ffffff;
+ padding: 10px 20px;
+ border: 1px solid #dddddd;
+ border-radius: 3px;
+ margin-bottom: 20px;
+}
+
+.article-title {
+ color: #444444;
+}
+
+a.article-title:hover {
+ color: #428bca;
+ text-decoration: none;
+}
+
+.article-content {
+ white-space: pre-line;
+}
+
+.article-img {
+ height: 65px;
+ width: 65px;
+ margin-right: 16px;
+}
+
+.article-metadata {
+ padding-bottom: 1px;
+ margin-bottom: 4px;
+ border-bottom: 1px solid #e3e3e3
+}
+
+.article-metadata a:hover {
+ color: #333;
+ text-decoration: none;
+}
+
+.article-svg {
+ width: 25px;
+ height: 25px;
+ vertical-align: middle;
+}
+
+.account-img {
+ height: 125px;
+ width: 125px;
+ margin-right: 20px;
+ margin-bottom: 16px;
+}
+
+.account-heading {
+ font-size: 2.5rem;
+}
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/static/profile_pics/c6d28c8397cd6efe.png b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/static/profile_pics/c6d28c8397cd6efe.png
new file mode 100644
index 000000000..92221ad6d
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/static/profile_pics/c6d28c8397cd6efe.png differ
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/static/profile_pics/default.jpg b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/static/profile_pics/default.jpg
new file mode 100644
index 000000000..38f286f63
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/static/profile_pics/default.jpg differ
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/static/profile_pics/f3e92b6e0c7dd644.jpg b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/static/profile_pics/f3e92b6e0c7dd644.jpg
new file mode 100644
index 000000000..2fe68efe2
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/static/profile_pics/f3e92b6e0c7dd644.jpg differ
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/templates/about11_flash.html b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/templates/about11_flash.html
new file mode 100644
index 000000000..7bfe8d3af
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/templates/about11_flash.html
@@ -0,0 +1,4 @@
+{% extends "layout11_flash.html" %}
+{% block content %}
+
About page
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/templates/account04.html b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/templates/account04.html
new file mode 100644
index 000000000..c31a821e9
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/templates/account04.html
@@ -0,0 +1,69 @@
+{% extends "layout11_flash.html" %}
+{% block content %}
+
+
+
+
+
{{ current_user.username }}
+
{{ current_user.email }}
+
+
+
+
+
+
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/templates/create_post.html b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/templates/create_post.html
new file mode 100644
index 000000000..117e7f9b7
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/templates/create_post.html
@@ -0,0 +1,40 @@
+{% extends "layout11_flash.html" %}
+{% block content %}
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/templates/home13_paginate.html b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/templates/home13_paginate.html
new file mode 100644
index 000000000..64ea26365
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/flaskblog/templates/home13_paginate.html
@@ -0,0 +1,32 @@
+{% extends "layout11_flash.html" %}
+
+{% block content %}
+ {% for post in posts.items %}
+
+
+
+ {% endfor %}
+
+
+
+
+
+ {% for page_num in posts.iter_pages(left_edge=1,right_edge=1,left_current=1,right_current=2) %}
+ {% if page_num %}
+ {% if posts.page == page_num %}
+ {{page_num}}
+ {% else %}
+ {{page_num}}
+ {% endif %}
+ {% else %}
+ ...
+ {% endif %}
+ {% endfor %}
+{% endblock %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/run.py b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/run.py
new file mode 100644
index 000000000..0c8390926
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/run.py
@@ -0,0 +1,7 @@
+from flaskblog import app
+
+# flaskblog is package
+# import app from flaskblog package
+
+if __name__ == '__main__':
+ app.run(debug=True)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/zreadme_Pagination.txt b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/zreadme_Pagination.txt
new file mode 100644
index 000000000..05a900905
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-F-Pagination/zreadme_Pagination.txt
@@ -0,0 +1,75 @@
+email: C@demo.com
+password: password
+
+- add many posts to demo pagination
+- view pagination demo in below
+- modify home() route by using paginate, 3 posts per page
+- modify home.html
+- update __init__ with rounte07
+- test website. (also try http://localhost:5000?page=2 )
+- Add pages' link in home.html
+- Add style to the current page in pages' link
+- Edit to reorder posts so the latest post on the top in home() route
+- test website
+
+
+- add user_posts() route to show posts by one author
+- create user_posts.html template
+- update href link {{ url_for('user_posts', username=post.author.username) }} for user
+ in user_posts.html, post.html. home.html
+
+
+
+
+
+
+
+---------------------------------------
+- Demo pagination
+---------------------------------------
+$ python
+>>> from flaskblog.models import Post
+>>> posts = Post.query.paginate() # get posts in the first page (20 posts per page by default)
+>>> posts
+
+>>> dir(posts)
+>>> posts.per_page
+20 # default is 20 posts per page
+>>> posts.page
+1 # on the first page
+>>> for post in posts.items :
+... print(post) # would print 20 posts if there are 20 posts or more
+...
+Post('First updated post','2021-02-28 04:56:56.054747')
+Post('second post','2021-03-01 22:50:33.976068')
+Post('third post ','2021-03-01 22:51:01.193460')
+:::::::::::::::
+
+>>> posts = Post.query.paginate(per_page=3)
+>>> for post in posts.items:
+... print(post) # print 3 posts on the first page
+
+>>> posts = Post.query.paginate(per_page=3, page=2)
+>>> for post in posts.items:
+... print(post) # print 3 posts on the second page
+
+>>> posts.total # number of total posts
+
+#########################################################
+# page_data.iter_pages() generator can return None
+# in the view template, replace None to ...
+#########################################################
+>>> posts = Post.query.paginate(per_page=1) # one post per page
+>>> for page in posts.iter_pages() :
+... print(page)
+...
+1
+2
+3
+4
+5
+None
+7
+8
+>>>
+
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/__init__.py b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/__init__.py
new file mode 100644
index 000000000..d9e560308
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/__init__.py
@@ -0,0 +1,41 @@
+import os
+from flask import Flask
+from flask_sqlalchemy import SQLAlchemy
+from flask_bcrypt import Bcrypt
+from flask_login import LoginManager
+from flask_mail import Mail
+
+
+app = Flask(__name__)
+
+# $ python
+# >>> import secrets
+# >>> secrets.token_hex(16)
+app.config['SECRET_KEY'] = 'ada8419b155676bc78c2296aba9c7c7d'
+# /// represents the relative directory from the current file (flaskblog.py).
+# that is, site.db has the same directory as this __init__.py
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
+
+db = SQLAlchemy(app)
+bcrypt = Bcrypt(app) # Used to hash the password
+login_manager = LoginManager(app)
+# This (login_view) tells where is the login route located
+login_manager.login_view = 'login' # which is login() in routes.py
+login_manager.login_message_category = 'info' #add bootstrap class to make nice view for flash message
+
+# Config gmail server to send instruction how to reset password
+app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
+app.config['MAIL_PORT'] = 587
+app.config['MAIL_USE_TLS'] = True
+# environment variables
+# Need to set these variables on Windows to your gmail user and password
+# ie.
+# > set EMAIL_USER xyz@gmail.com (export in linux)
+# > set EMAIL_PASS password_to_xyz (export in linux)
+app.config['MAIL_USERNAME'] = os.environ.get('EMAIL_USER')
+app.config['MAIL_PASSWORD'] = os.environ.get('EMAIL_PASS')
+mail = Mail(app)
+
+
+# import routes from flaskblog package
+from flaskblog import routes07
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/forms05.py b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/forms05.py
new file mode 100644
index 000000000..a02f47e07
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/forms05.py
@@ -0,0 +1,94 @@
+from flask_wtf import FlaskForm
+from flask_wtf.file import FileField, FileAllowed
+from flask_login import current_user
+from wtforms import StringField, PasswordField, SubmitField, BooleanField, TextAreaField
+from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
+from flaskblog.models import User
+
+# $ pip3 install flask_wtf
+# $ pip3 install email_validator
+
+# This module is imported in flaskblog.py to create forms
+
+class RegistrationForm (FlaskForm) :
+ # 'Username' in html pass objects in validators
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ confirm_password = PasswordField('Confirm Password',
+ validators=[DataRequired(), EqualTo('password')])
+ submit = SubmitField('Sign up')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ def validate_username(self, username) :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ def validate_email(self, email) :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class LoginForm (FlaskForm) :
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ # This (remember) allows users to stay login for certain time
+ # after web browser closes by using security cookies.
+ remember = BooleanField('Remember me')
+ submit = SubmitField('Login')
+
+# Copy RegistrationForm to modify
+class UpdateAccountForm (FlaskForm) :
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ picture = FileField('Update profile picture', validators=[FileAllowed(['jpg','png'])])
+ submit = SubmitField('Update')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ # validate only when username is not current_user's username
+ def validate_username(self, username) :
+ if username.data != current_user.username :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ # validate only when email is not current_user's email
+ def validate_email(self, email) :
+ if email.data != current_user.email :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class PostForm(FlaskForm) :
+ title = StringField('Title', validators=[DataRequired()])
+ content = TextAreaField('Content', validators=[DataRequired()])
+ submit = SubmitField('Post')
+
+
+class RequestResetForm(FlaskForm) :
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ submit = SubmitField('Request Password Reset')
+
+ def validate_email(self, email) :
+ user = User.query.filter_by(email=email.data).first()
+ if user is None:
+ raise ValidationError('There is no account with that email. You must register first.')
+
+
+class ResetPasswordForm(FlaskForm) :
+ password = PasswordField('Password', validators=[DataRequired()])
+ confirm_password = PasswordField('Confirm Password',
+ validators=[DataRequired(), EqualTo('password')])
+ submit = SubmitField('Reset Password')
+
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/models.py b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/models.py
new file mode 100644
index 000000000..57cac8ead
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/models.py
@@ -0,0 +1,69 @@
+from datetime import datetime
+from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
+from flaskblog import db, login_manager, app
+from flask_login import UserMixin
+
+### !!! Generate database tables structure first
+### !!! by following zreadme_package.txt
+
+@login_manager.user_loader
+def load_user(user_id) :
+ return User.query.get(int(user_id))
+
+# defines User table structure in database
+class User(db.Model, UserMixin) :
+ # primary_key specifies unique id
+ id = db.Column(db.Integer, primary_key=True)
+ # max 20 characters, unique, and required
+ username = db.Column(db.String(20), unique=True, nullable=False)
+ # max 120 characters, unique, and required
+ email = db.Column(db.String(120), unique=True, nullable=False)
+ # profile image
+ image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
+ # password (be hashed)
+ password = db.Column(db.String(60), nullable=False)
+ # This is not a column, but build relationship between post and author
+ # 'Post' class being passed
+ # backref 'author' is reference to the user by post.author
+ posts = db.relationship('Post', backref='author', lazy=True)
+
+ # token expires after 1800 seconds (30 min)
+ # {'user_id': self.id} as payload
+ def get_reset_token(self, expires_sec=1800) :
+ s = Serializer(app.config['SECRET_KEY'], expires_sec)
+ return s.dumps({'user_id': self.id}).decode('utf-8')
+
+ # @staticmethod decorator tells python this member function
+ # does not need self as parameter
+ # s.loads(token) = {'user_id': self.id} as payload
+ # So, s.loads(token)['user_id'] = self.id
+ @staticmethod
+ def verify_reset_token(token) :
+ s = Serializer(app.config['SECRET_KEY'])
+ try:
+ user_id = s.loads(token)['user_id']
+ except:
+ return None
+ return User.query.get(user_id)
+
+
+ # function to print this class (User)
+ def __repr__(self) :
+ return f"User('{self.username}','{self.email}','{self.image_file}')"
+
+# defines Post table structure in database
+class Post(db.Model) :
+ id = db.Column(db.Integer, primary_key=True)
+ title = db.Column(db.String(100), nullable=False)
+ # type of DateTime, the current time as default (utcnow)
+ # The default is passed as the function name of utcnow. Do not use
+ # utcnow(), which can invoke the function right away.
+ date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+ content = db.Column(db.Text, nullable=False)
+ # point to user who creates this post
+ # 'user' in 'user.id' is reference to an user in the User table
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
+
+ # function to print this class (Post)
+ def __repr__(self) :
+ return f"Post('{self.title}','{self.date_posted}')"
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/routes07.py b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/routes07.py
new file mode 100644
index 000000000..8c864ddda
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/routes07.py
@@ -0,0 +1,230 @@
+from flask import render_template, url_for, flash, redirect, request, abort
+from flaskblog import app, db, bcrypt, mail
+from flaskblog.forms05 import (RegistrationForm, LoginForm, UpdateAccountForm,
+ PostForm, RequestResetForm, ResetPasswordForm)
+from flaskblog.models import User, Post
+from flask_login import login_user, current_user, logout_user, login_required
+from flask_mail import Message
+import os, secrets
+from PIL import Image
+
+# forms.py in flaskblog package
+# models.py in flaskblog package
+
+# http://localhost:5000/
+@app.route("/")
+@app.route("/home")
+def home():
+ # get page number from url, default is first page (1),
+ # the type (int) is used to make sure page is integer.
+ page = request.args.get('page', 1, type=int)
+ posts = Post.query.order_by(Post.date_posted.desc()).paginate(page=page, per_page=2)
+ return render_template('home13_paginate.html', posts=posts)
+
+# http://localhost:5000/about
+@app.route("/about")
+def about():
+ return render_template('about11_flash.html', title='About')
+
+@app.route("/register", methods=['GET', 'POST'])
+def register():
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = RegistrationForm()
+ # once submit successfully, flash message shows in the placehold in layout.html
+ # flash message is one time only, it disappear after refresh web browser
+ if form.validate_on_submit():
+ # hash the password
+ hashed_pwd = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
+ user = User(username=form.username.data, email=form.email.data, password=hashed_pwd)
+ db.session.add(user)
+ db.session.commit()
+ flash('Your account has been created! You are now able to log in', 'success')
+ # 'home' is function of home() above
+ return redirect(url_for('login'))
+ return render_template('register12_error.html', title='Register', form=form)
+
+@app.route("/login", methods=['GET', 'POST'])
+def login():
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = LoginForm()
+ if form.validate_on_submit():
+ user = User.query.filter_by(email=form.email.data).first()
+ if user and bcrypt.check_password_hash(user.password, form.password.data) :
+ login_user(user, remember=form.remember.data)
+ # request.args is dictionary type
+ next_page = request.args.get('next')
+ # 'home' is function of home() above
+ return redirect(next_page) if next_page else redirect(url_for('home'))
+ else :
+ flash('Login failed. Please check email and password.', 'danger')
+ return render_template('login13.html', title='Login', form=form)
+
+
+@app.route("/logout")
+def logout() :
+ logout_user()
+ return redirect(url_for('home'))
+
+# save uploaded image to the specified path
+def save_picture(form_picture) :
+ random_hex = secrets.token_hex(8)
+ # underscore (_) ignoring the value
+ _, f_ext = os.path.splitext(form_picture.filename)
+ picture_fn = random_hex + f_ext
+ # define the path to store profile image
+ picture_path = os.path.join(app.root_path, 'static/profile_pics', picture_fn)
+
+ # save the uploaded image into the specified path
+
+ # do not want to save the original uploaded image, which can be very large
+ #form_picture.save(picture_path)
+
+ #resize the image before store
+ output_size = (125, 125)
+ i = Image.open(form_picture)
+ i.thumbnail(output_size)
+ i.save(picture_path)
+
+ return picture_fn
+
+# login_required decorator makes sure to access the page only
+# when the user log in
+@app.route("/account", methods=['GET', 'POST'])
+@login_required
+def account() :
+ # this form is for user to update the profile
+ form = UpdateAccountForm()
+ if form.validate_on_submit() :
+ if form.picture.data :
+ picture_file = save_picture(form.picture.data)
+ current_user.image_file = picture_file
+ # think of current_user is reference to the User in database
+ current_user.username = form.username.data
+ current_user.email = form.email.data
+ db.session.commit()
+ flash('Your account has been updated!', 'success')
+ return redirect(url_for('account'))
+ elif request.method == 'GET' :
+ # populate the username and email so that
+ # the new username and email can be viewed right after
+ # update form is submitted
+ form.username.data = current_user.username
+ form.email.data = current_user.email
+ # user.image_file is defined in User() in models.py from database
+ image_file = url_for('static', filename='profile_pics/'+current_user.image_file)
+ return render_template('account04.html', title='Account', image_file=image_file, form=form)
+
+@app.route("/post/new", methods=['GET', 'POST'])
+@login_required
+def new_post() :
+ form = PostForm()
+ if form.validate_on_submit():
+ # Construct Post object which defined in models.py
+ post = Post(title=form.title.data, content=form.content.data, author=current_user)
+ db.session.add(post)
+ db.session.commit()
+ flash('Your post has been created!', 'success')
+ return redirect(url_for('home'))
+ return render_template('create_post.html', title='New Post', form=form, legend="New Post")
+
+
+@app.route("/post/")
+def post(post_id) :
+ # find the post or give page not found 404 error
+ post = Post.query.get_or_404(post_id)
+ return render_template('post02.html', title=post.title, post=post)
+
+@app.route("/post//update", methods=['GET', 'POST'])
+@login_required
+def update_post(post_id) :
+ post = Post.query.get_or_404(post_id)
+ if post.author != current_user:
+ abort(403)
+ form = PostForm()
+ if form.validate_on_submit():
+ post.title = form.title.data
+ post.content = form.content.data
+ db.session.commit()
+ flash('Your post has been updated!', 'success')
+ return redirect(url_for('post', post_id=post.id))
+ elif request.method == 'GET' :
+ form.title.data = post.title
+ form.content.data = post.content
+ return render_template('create_post.html', title='Update Post', form=form, legend="Update Post")
+
+# "Delete" button in Modal sends POST request to delete the post
+@app.route("/post//delete", methods=['POST'])
+@login_required
+def delete_post(post_id) :
+ post = Post.query.get_or_404(post_id)
+ if post.author != current_user:
+ abort(403)
+ db.session.delete(post)
+ db.session.commit()
+ flash('Your post has been deleted!', 'success')
+ return redirect(url_for('home'))
+
+
+@app.route("/user/")
+def user_posts(username) :
+ page = request.args.get('page', 1, type=int)
+ # first_or_404: first user or give 404 if no user is found
+ user = User.query.filter_by(username=username).first_or_404()
+ posts = Post.query.filter_by(author=user)\
+ .order_by(Post.date_posted.desc())\
+ .paginate(page=page, per_page=2)
+ return render_template('user_posts.html', posts=posts, user=user)
+
+
+# Send email with the link to reset password
+# The URL link is specified by url_for(), route to reset_token()
+# _external=True tells Flask that it should generate an absolute URL, and not a relative URL.
+def send_reset_email(user) :
+ token = user.get_reset_token()
+ msg = Message('Password Reset Request', sender='noreply@demo.com',
+ recipients=[user.email])
+ msg.body = f'''To reset your password, visit the following link:
+{url_for('reset_token', token=token, _external=True)}
+
+If you did not make this request then simply ignore this email and no changes will be made.
+'''
+ mail.send(msg)
+
+
+# route the form to send instruction email to request password reset
+# when forgot the password
+@app.route("/reset_password", methods=['GET', 'POST'])
+def reset_request() :
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = RequestResetForm()
+ if form.validate_on_submit():
+ user = User.query.filter_by(email=form.email.data).first()
+ send_reset_email(user)
+ flash('An email has been sent with instructions to reset your password', 'info')
+ return redirect(url_for('login'))
+ return render_template('reset_request.html', title='Reset Password', form=form)
+
+
+# route with the form to set new password (the URL link is from email)
+@app.route("/reset_password/", methods=['GET', 'POST'])
+def reset_token(token) :
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ # the token comes with user id. if the user id is valid,
+ # the valid user is returned
+ user = User.verify_reset_token(token)
+ if user is None:
+ flash('That is an invalid or expired token', 'warning')
+ return redirect(url_for('reset_request'))
+ form = ResetPasswordForm()
+ if form.validate_on_submit():
+ # hash the password
+ hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
+ user.password = hashed_password
+ db.session.commit()
+ flash('Your password has been updated! You are now able to log in', 'success')
+ return redirect(url_for('login'))
+ return render_template('reset_token.html', title='Reset Password', form=form)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/site.db b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/site.db
new file mode 100644
index 000000000..84e529264
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/site.db differ
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/static/main.css b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/static/main.css
new file mode 100644
index 000000000..730e2bca6
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/static/main.css
@@ -0,0 +1,80 @@
+body {
+ background: #fafafa;
+ color: #333333;
+ margin-top: 5rem;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: #444444;
+}
+
+.bg-steel {
+ background-color: #5f788a;
+}
+
+.site-header .navbar-nav .nav-link {
+ color: #cbd5db;
+}
+
+.site-header .navbar-nav .nav-link:hover {
+ color: #ffffff;
+}
+
+.site-header .navbar-nav .nav-link:active {
+ font-weight: 500;
+}
+
+.content-section {
+ background: #ffffff;
+ padding: 10px 20px;
+ border: 1px solid #dddddd;
+ border-radius: 3px;
+ margin-bottom: 20px;
+}
+
+.article-title {
+ color: #444444;
+}
+
+a.article-title:hover {
+ color: #428bca;
+ text-decoration: none;
+}
+
+.article-content {
+ white-space: pre-line;
+}
+
+.article-img {
+ height: 65px;
+ width: 65px;
+ margin-right: 16px;
+}
+
+.article-metadata {
+ padding-bottom: 1px;
+ margin-bottom: 4px;
+ border-bottom: 1px solid #e3e3e3
+}
+
+.article-metadata a:hover {
+ color: #333;
+ text-decoration: none;
+}
+
+.article-svg {
+ width: 25px;
+ height: 25px;
+ vertical-align: middle;
+}
+
+.account-img {
+ height: 125px;
+ width: 125px;
+ margin-right: 20px;
+ margin-bottom: 16px;
+}
+
+.account-heading {
+ font-size: 2.5rem;
+}
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/static/profile_pics/c6d28c8397cd6efe.png b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/static/profile_pics/c6d28c8397cd6efe.png
new file mode 100644
index 000000000..92221ad6d
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/static/profile_pics/c6d28c8397cd6efe.png differ
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/static/profile_pics/default.jpg b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/static/profile_pics/default.jpg
new file mode 100644
index 000000000..38f286f63
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/static/profile_pics/default.jpg differ
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/static/profile_pics/f3e92b6e0c7dd644.jpg b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/static/profile_pics/f3e92b6e0c7dd644.jpg
new file mode 100644
index 000000000..2fe68efe2
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/static/profile_pics/f3e92b6e0c7dd644.jpg differ
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/templates/about11_flash.html b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/templates/about11_flash.html
new file mode 100644
index 000000000..7bfe8d3af
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/templates/about11_flash.html
@@ -0,0 +1,4 @@
+{% extends "layout11_flash.html" %}
+{% block content %}
+
About page
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/templates/account04.html b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/templates/account04.html
new file mode 100644
index 000000000..c31a821e9
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/templates/account04.html
@@ -0,0 +1,69 @@
+{% extends "layout11_flash.html" %}
+{% block content %}
+
+
+
+
+
{{ current_user.username }}
+
{{ current_user.email }}
+
+
+
+
+
+
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/templates/create_post.html b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/templates/create_post.html
new file mode 100644
index 000000000..117e7f9b7
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/templates/create_post.html
@@ -0,0 +1,40 @@
+{% extends "layout11_flash.html" %}
+{% block content %}
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/templates/home13_paginate.html b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/templates/home13_paginate.html
new file mode 100644
index 000000000..64ea26365
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-G-Email/flaskblog/templates/home13_paginate.html
@@ -0,0 +1,32 @@
+{% extends "layout11_flash.html" %}
+
+{% block content %}
+ {% for post in posts.items %}
+
+
+
+ {% endfor %}
+
+
+
+
+
+ {% for page_num in posts.iter_pages(left_edge=1,right_edge=1,left_current=1,right_current=2) %}
+ {% if page_num %}
+ {% if posts.page == page_num %}
+ {{page_num}}
+ {% else %}
+ {{page_num}}
+ {% endif %}
+ {% else %}
+ ...
+ {% endif %}
+ {% endfor %}
+{% endblock %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/flaskblog/users/__init__.py b/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/flaskblog/users/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/flaskblog/users/forms.py b/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/flaskblog/users/forms.py
new file mode 100644
index 000000000..a86eaec24
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/flaskblog/users/forms.py
@@ -0,0 +1,84 @@
+from flask_wtf import FlaskForm
+from flask_wtf.file import FileField, FileAllowed
+from wtforms import StringField, PasswordField, SubmitField, BooleanField
+from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
+from flask_login import current_user
+from flaskblog.models import User
+
+
+class RegistrationForm (FlaskForm) :
+ # 'Username' in html pass objects in validators
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ confirm_password = PasswordField('Confirm Password',
+ validators=[DataRequired(), EqualTo('password')])
+ submit = SubmitField('Sign up')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ def validate_username(self, username) :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ def validate_email(self, email) :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class LoginForm (FlaskForm) :
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ # This (remember) allows users to stay login for certain time
+ # after web browser closes by using security cookies.
+ remember = BooleanField('Remember me')
+ submit = SubmitField('Login')
+
+# Copy RegistrationForm to modify
+class UpdateAccountForm (FlaskForm) :
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ picture = FileField('Update profile picture', validators=[FileAllowed(['jpg','png'])])
+ submit = SubmitField('Update')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ # validate only when username is not current_user's username
+ def validate_username(self, username) :
+ if username.data != current_user.username :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ # validate only when email is not current_user's email
+ def validate_email(self, email) :
+ if email.data != current_user.email :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class RequestResetForm(FlaskForm) :
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ submit = SubmitField('Request Password Reset')
+
+ def validate_email(self, email) :
+ user = User.query.filter_by(email=email.data).first()
+ if user is None:
+ raise ValidationError('There is no account with that email. You must register first.')
+
+
+class ResetPasswordForm(FlaskForm) :
+ password = PasswordField('Password', validators=[DataRequired()])
+ confirm_password = PasswordField('Confirm Password',
+ validators=[DataRequired(), EqualTo('password')])
+ submit = SubmitField('Reset Password')
+
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/flaskblog/users/routes.py b/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/flaskblog/users/routes.py
new file mode 100644
index 000000000..fe670a51c
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/flaskblog/users/routes.py
@@ -0,0 +1,129 @@
+from flask import render_template, url_for, flash, redirect, request, Blueprint
+from flask_login import login_user, current_user, logout_user, login_required
+from flaskblog import db, bcrypt
+from flaskblog.models import User, Post
+from flaskblog.users.forms import (RegistrationForm, LoginForm, UpdateAccountForm,
+ RequestResetForm, ResetPasswordForm)
+from flaskblog.users.utils import save_picture, send_reset_email
+
+
+users = Blueprint('users', __name__)
+
+@users.route("/register", methods=['GET', 'POST'])
+def register():
+ if current_user.is_authenticated:
+ return redirect(url_for('main.home'))
+ form = RegistrationForm()
+ # once submit successfully, flash message shows in the placehold in layout.html
+ # flash message is one time only, it disappear after refresh web browser
+ if form.validate_on_submit():
+ # hash the password
+ hashed_pwd = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
+ user = User(username=form.username.data, email=form.email.data, password=hashed_pwd)
+ db.session.add(user)
+ db.session.commit()
+ flash('Your account has been created! You are now able to log in', 'success')
+ # 'home' is function of home() above
+ return redirect(url_for('users.login'))
+ return render_template('register12_error.html', title='Register', form=form)
+
+@users.route("/login", methods=['GET', 'POST'])
+def login():
+ if current_user.is_authenticated:
+ return redirect(url_for('main.home'))
+ form = LoginForm()
+ if form.validate_on_submit():
+ user = User.query.filter_by(email=form.email.data).first()
+ if user and bcrypt.check_password_hash(user.password, form.password.data) :
+ login_user(user, remember=form.remember.data)
+ # request.args is dictionary type
+ next_page = request.args.get('next')
+ # 'home' is function of home() above
+ return redirect(next_page) if next_page else redirect(url_for('main.home'))
+ else :
+ flash('Login failed. Please check email and password.', 'danger')
+ return render_template('login13.html', title='Login', form=form)
+
+
+@users.route("/logout")
+def logout() :
+ logout_user()
+ return redirect(url_for('main.home'))
+
+
+
+# login_required decorator makes sure to access the page only
+# when the user log in
+@users.route("/account", methods=['GET', 'POST'])
+@login_required
+def account() :
+ # this form is for user to update the profile
+ form = UpdateAccountForm()
+ if form.validate_on_submit() :
+ if form.picture.data :
+ picture_file = save_picture(form.picture.data)
+ current_user.image_file = picture_file
+ # think of current_user is reference to the User in database
+ current_user.username = form.username.data
+ current_user.email = form.email.data
+ db.session.commit()
+ flash('Your account has been updated!', 'success')
+ return redirect(url_for('users.account'))
+ elif request.method == 'GET' :
+ # populate the username and email so that
+ # the new username and email can be viewed right after
+ # update form is submitted
+ form.username.data = current_user.username
+ form.email.data = current_user.email
+ # user.image_file is defined in User() in models.py from database
+ image_file = url_for('static', filename='profile_pics/'+current_user.image_file)
+ return render_template('account04.html', title='Account', image_file=image_file, form=form)
+
+
+@users.route("/user/")
+def user_posts(username) :
+ page = request.args.get('page', 1, type=int)
+ # first_or_404: first user or give 404 if no user is found
+ user = User.query.filter_by(username=username).first_or_404()
+ posts = Post.query.filter_by(author=user)\
+ .order_by(Post.date_posted.desc())\
+ .paginate(page=page, per_page=2)
+ return render_template('user_posts.html', posts=posts, user=user)
+
+
+# route the form to send instruction email to request password reset
+# when forgot the password
+@users.route("/reset_password", methods=['GET', 'POST'])
+def reset_request() :
+ if current_user.is_authenticated:
+ return redirect(url_for('main.home'))
+ form = RequestResetForm()
+ if form.validate_on_submit():
+ user = User.query.filter_by(email=form.email.data).first()
+ send_reset_email(user)
+ flash('An email has been sent with instructions to reset your password', 'info')
+ return redirect(url_for('users.login'))
+ return render_template('reset_request.html', title='Reset Password', form=form)
+
+
+
+# route with the form to set new password (the URL link is from email)
+@users.route("/reset_password/", methods=['GET', 'POST'])
+def reset_token(token) :
+ if current_user.is_authenticated:
+ return redirect(url_for('main.home'))
+ # the token comes with user id. if the user id is valid,
+ # the valid user is returned
+ user = User.verify_reset_token(token)
+ if user is None:
+ flash('That is an invalid or expired token', 'warning')
+ return redirect(url_for('users.reset_request'))
+ form = ResetPasswordForm()
+ if form.validate_on_submit():
+ # hash the password
+ hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
+ user.password = hashed_password
+ db.session.commit()
+ flash('Your password has been updated! You are now able to log in', 'success')
+ return redirect(url_for('users.login'))
+ return render_template('reset_token.html', title='Reset Password', form=form)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/flaskblog/users/utils.py b/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/flaskblog/users/utils.py
new file mode 100644
index 000000000..ed57421cc
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/flaskblog/users/utils.py
@@ -0,0 +1,45 @@
+import os
+import secrets
+from PIL import Image
+from flask import url_for, current_app
+from flask_mail import Message
+from flaskblog import mail
+
+# save uploaded image to the specified path
+def save_picture(form_picture) :
+ random_hex = secrets.token_hex(8)
+ # underscore (_) ignoring the value
+ _, f_ext = os.path.splitext(form_picture.filename)
+ picture_fn = random_hex + f_ext
+ # define the path to store profile image
+ picture_path = os.path.join(current_app.root_path, 'static/profile_pics', picture_fn)
+
+ # save the uploaded image into the specified path
+
+ # do not want to save the original uploaded image, which can be very large
+ #form_picture.save(picture_path)
+
+ #resize the image before store
+ output_size = (125, 125)
+ i = Image.open(form_picture)
+ i.thumbnail(output_size)
+ i.save(picture_path)
+
+ return picture_fn
+
+
+# Send email with the link to reset password
+# The URL link is specified by url_for(), route to reset_token()
+# _external=True tells Flask that it should generate an absolute URL, and not a relative URL.
+def send_reset_email(user) :
+ token = user.get_reset_token()
+ msg = Message('Password Reset Request', sender='noreply@demo.com',
+ recipients=[user.email])
+ msg.body = f'''To reset your password, visit the following link:
+{url_for('users.reset_token', token=token, _external=True)}
+
+If you did not make this request then simply ignore this email and no changes will be made.
+'''
+ mail.send(msg)
+
+
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/run.py b/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/run.py
new file mode 100644
index 000000000..286a1e43c
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/run.py
@@ -0,0 +1,11 @@
+from flaskblog import create_app
+
+# flaskblog is package
+# import app from flaskblog package
+
+# create the current application (flask.current_app)
+# by the default Config
+app = create_app()
+
+if __name__ == '__main__':
+ app.run(debug=True)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/zreadme_deploy.txt b/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/zreadme_deploy.txt
new file mode 100644
index 000000000..dbe3c72c7
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/00-Practices-J-Deploy/zreadme_deploy.txt
@@ -0,0 +1,200 @@
+email: C@demo.com
+password: password
+email: zhjohn925@hotmail.com
+password: 1234
+
+------------------------------------------
+- Linode Server (get Ubuntu server)
+------------------------------------------
+# apt update && apt upgrade # do update in the first time log in the server
+# hostnamectl set-hostname flask-server # set host name
+# hostname
+flask-server
+# nano /etc/hosts # add hostname in the file
+127.0.0.1 localhost
+45.33.123.214 flask-server
+::::::
+# adduser star # add a user
+# adduser star sudo # add the user to sudo
+# cd /home/star
+# mkdir .ssh # store public key
+------------------------------------------
+- Local client window: generate ssh key
+------------------------------------------
+$ ssh-keygen -b 4096
+~/.ssh/id_rsa
+~/.ssh/id_rsa.pub
+$ scp ~/.ssh/id_rsa.pub star@:~/.ssh/authorized_keys
+------------------------------------------
+- Sever: Log out from root and the login as user (star)
+------------------------------------------
+$ sudo chmod 700 ~/.ssh/
+$ sudo chmod 600 ~/.ssh/*
+------------------------------------------
+- Local client window: ssh log in server no needs password
+------------------------------------------
+$ ssh star@
+$
+------------------------------------------
+- Sever: modify sshd_config
+------------------------------------------
+$ sudo nano /etc/ssh/sshd_config
+[sudo] password for star:
+PermitRootLogin no # Not allow root to log in to prevent hackers
+PasswordAuthentication no # Use ssh key, no password is needed. Also prevent hackers to use password to log in
+$ sudo systemctl restart sshd # restart ssh server
+------------------------------------------
+- Sever: install firewall
+------------------------------------------
+$ sudo apt install ufw # ufw : uncomplicated firewall
+$ sudo ufw default allow outgoing # allow outgoing traffic
+$ sudo ufw default deny incoming # deny incoming traffic
+$ sudo ufw allow ssh # allow ssh
+$ sudo ufw allow 5000 # allow port 5000
+$ sudo ufw enable # enable firewall
+$ sudo ufw status # view the status
+------------------------------------------
+- Local client window: deploy
+------------------------------------------
+$ pip3 freeze > requirements.txt # list all dependencies
+$ move requirements.txt into Flask_Blog directory want to deploy
+$ scp -r Flask_Blog star@:~/ # copy the whole flask project directory into server
+------------------------------------------
+- Sever: install python
+------------------------------------------
+We do not want to use default python. want to run it in virtual environment.
+$ sudo apt install python3-pip
+$ sudo apt install python3-venv # allow to create virtual environment
+$ python3 -m venv Flask_Blog/venv # create virtual environment in Flask_Blog
+$ cd Flask_Blog
+$ pip install -r requirements.txt # install dependencies for the application (app)
+Set environment variables for the app as defined in config.py
+ ie. SECRET_KEY="ada8419b155676bc78c2296aba9c7c7d"
+ ie. SQLALCHEMY_DATABASE_URI="sqlite:///site.db"
+$ sudo touch /etc/config.json
+$ sudo nano /etc/config.json
+{
+ "SECRET_KEY": "ada8419b155676bc78c2296aba9c7c7d",
+ "SQLALCHEMY_DATABASE_URI": "sqlite:///site.db",
+ "EMAIL_USER": "Your_GMAIL",
+ "EMAIL_PASS": "Your_PASSWORD"
+}
+------------------------------------------
+- Sever: Flask_Blog structure
+------------------------------------------
+(venv) ~Flask_Blog$ ls
+flaskblog __pycache__ requirements.txt run.py venv
+(venv) ~Flask_Blog$ ls flaskblog
+config.py __init__.py models.py __pycache__ static users
+errors main posts site.db templates
+------------------------------------------
+- Sever: Modify flaskblog/config.py
+------------------------------------------
+(venv) ~Flask_Blog$ nano flaskblog/config_in_server.py
+------------------------------------------
+- Sever: Run in development Server
+------------------------------------------
+(venv) ~Flask_Blog$ export FLASK_APP=run.py
+(venv) ~Flask_Blog$ flask run --host=0.0.0.0
+ * Specify the host allows us to access to the server from outside
+ * Running on http://0.0.0.0:5000/
+------------------------------------------
+- Local client window: web browser
+------------------------------------------
+http://:5000
+
+-------------------------------------------------------------
+The ABOVE is to run website in DEVELOPMENT SERVER
+-------------------------------------------------------------
+
+------------------------------------------
+- Sever: install nginx as web server
+------------------------------------------
+(venv) ~Flask_Blog$ cd
+(venv) ~$ sudo apt install nginx #install it in virtual environment
+(venv) ~$ pip install gunicorn #install it in virtual environment
+(venv) ~$ sudo rm /etc/nginx/sites-enabled/default
+(venv) ~$ sudo nano /etc/nginx/sites-enabled/flaskblog
+
+server {
+ listen 80;
+ server_name ;
+ location /static {
+ alias /home/star/Flask_Blog/flaskblog/static;
+ }
+ location / {
+ proxy_pass http://localhost:8000;
+ include /etc/nginx/proxy_params;
+ proxy_redirect off;
+ }
+}
+
+$ sudo ufw allow http/tcp
+$ sudo ufw delete allow 5000
+$ sudo ufw enable
+$ sudo systemctl restart nginx
+
+
+----------------------------------------------------------------------------
+Now nginx server starts (listening the port 80), but the server does not
+know how to handle python.
+Test website: http://
+would get the errors:
+ 502 Bad Gateway
+So, we need to run gunicorn to handle python.
+Number of workers in gunicorn: 2*cores+1
+----------------------------------------------------------------------------
+
+(venv) ~$ nproc --all # find number of cores
+1 # we pick workers = 3 in this case of 1 core
+(venv) ~$ cd Flask_Blog
+(venv) ~/Flask_Blog$ gunicorn -w 3 run:app # -w 3 (3 workers)
+ # run:app (app in run.py)
+ [INFO] Listening at: http://127.0.0.1:8000
+ [INFO] Booting worker with pid: 15901
+ [INFO] Booting worker with pid: 15902
+ [INFO] Booting worker with pid: 15903
+
+------------------------------------------
+- Local client window: web browser
+------------------------------------------
+Test website: http://
+Now it should work.
+
+
+------------------------------------------
+- Sever: install supervisor
+------------------------------------------
+(venv) ~/Flask_Blog$ sudo apt install supervisor
+(venv) ~/Flask_Blog$ sudo nano /etc/supervisor/conf.d/flaskblog.conf
+[progrom:flaskblog]
+directory=/home/star/Flask_Blog
+command=/home/star/Flask_Blog/venv/bin/gunicorn -w 3 run:app
+user=star
+autostart=true
+autorestart=true
+stopasgroup=true
+killasgroup=true
+stderr_logfile=/var/log/flaskblog/flaskblog.err.log
+stdout_logfile=/var/log/flaskblog/flaskblog.out.log
+
+------------------------------------------
+- Sever: generate log
+------------------------------------------
+(venv) ~/Flask_Blog$ sudo mkdir -p /var/log/flaskblog # create flaskblog directory if not exists.
+(venv) ~/Flask_Blog$ sudo touch /var/log/flaskblog/flaskblog.err.log
+(venv) ~/Flask_Blog$ sudo touch /var/log/flaskblog/flaskblog.out.log
+
+(venv) ~/Flask_Blog$ sudo supervisorctl reload
+
+------------------------------------------
+- Local client window: web browser
+------------------------------------------
+Test website: http://
+
+Please note: nginx has default max of 2MB file to upload.
+(venv) ~/Flask_Blog$ sudo nano /etc/nginx/nginx.conf
+ add:
+ client_max_body_size 5M;
+(venv) ~/Flask_Blog$ sudo systemctl restart nginx
+
diff --git a/Python/Flask_Blog/00-Practices/A-Getting-Started/flaskblog.py b/Python/Flask_Blog/00-Practices/A-Getting-Started/flaskblog.py
new file mode 100644
index 000000000..ba65e283f
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/A-Getting-Started/flaskblog.py
@@ -0,0 +1,23 @@
+from flask import Flask
+
+app = Flask(__name__)
+
+# decorator adds additional functionality into the function
+# apply route decorator
+@app.route("/")
+@app.route("/home")
+def hello():
+ return '
'
+
+# __name__ is '__main__' when run this script directly, ie. python flaskblog01.py.
+# otherwise, __name__ is the name of module if imported by other scripts.
+
+# debug=True : run app in debug mode
+# No need to restart the web server, the view is updated
+# when reloading the web browser if the code changes.
+if __name__ == '__main__' :
+ app.run(debug=True)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/A-Getting-Started/zreadme.txt b/Python/Flask_Blog/00-Practices/A-Getting-Started/zreadme.txt
new file mode 100644
index 000000000..5b35c1ce4
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/A-Getting-Started/zreadme.txt
@@ -0,0 +1,49 @@
+
+1. Install flask
+
+ $ pip3 install flask
+ Try to import flask under python to verify the installation
+
+2. Create flaskblog.py
+
+ add root route and home route to the same page:
+ @app.route("/")
+ @app.route("/home")
+ def hello():
+ return '
Hello the world!
'
+
+3. two ways to run Flask apps
+ -- $ export FLASK_APP=flaskblog.py
+ (\> set FLASK_APP=flaskblog.py in windows )
+
+ $ flask run
+ * Debug mode: off
+ * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
+
+ $ export FLASK_DEBUG=1
+ # Run app in debug mode. No need to restart the web server,
+ # the view is updated when reloading the web browser
+ # if the code changes.
+
+ $ flask run
+ * Debug mode: on
+ * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
+
+ -- $ python flaskblog.py
+ * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
+
+4. Test website:
+ http://127.0.0.1:5000/about
+
+ We can see 404 error: page not found
+
+5. add about() route in flaskblog.py
+
+ @app.route("/about")
+ def about():
+ return '
About page
'
+
+6. Test website:
+ http://127.0.0.1:5000/about
+
+ Now we can see about page on the web browser
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-1/flaskblog.py b/Python/Flask_Blog/00-Practices/B-Templates-1/flaskblog.py
new file mode 100644
index 000000000..811bfbcbf
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-1/flaskblog.py
@@ -0,0 +1,23 @@
+from flask import Flask, render_template
+
+app = Flask(__name__)
+
+# decorator adds additional functionality into the function
+# apply route decorator
+@app.route("/")
+@app.route("/home")
+def home():
+ return render_template('home.html')
+
+@app.route("/about")
+def about():
+ return render_template('about.html')
+
+# __name__ is '__main__' when run this script directly, ie. python flaskblog01.py.
+# otherwise, __name__ is the name of module if imported by other scripts.
+
+# debug=True : run app in debug mode
+# No need to restart the web server, the view is updated
+# when reloading the web browser if the code changes.
+if __name__ == '__main__' :
+ app.run(debug=True)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-1/templates/about.html b/Python/Flask_Blog/00-Practices/B-Templates-1/templates/about.html
new file mode 100644
index 000000000..05f7cfa4d
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-1/templates/about.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ About
+
+
+
About page
+
+
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-1/templates/home.html b/Python/Flask_Blog/00-Practices/B-Templates-1/templates/home.html
new file mode 100644
index 000000000..b86f19f10
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-1/templates/home.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Home page
+
+
+
Add templates
+
+
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-1/zreadme.txt b/Python/Flask_Blog/00-Practices/B-Templates-1/zreadme.txt
new file mode 100644
index 000000000..45ba8490f
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-1/zreadme.txt
@@ -0,0 +1,21 @@
+
+1. create templates directory
+
+2. create templates/home.html for home() route
+ create templates/about.html for about() route
+
+3. render home template in flaskblog.py
+ # Flask knows to render html(views) in templates directory
+ # change hello() to home() for meaningful function name
+ def home():
+ return render_template('home.html')
+
+4. render about template in flaskblog.py
+ def about():
+ return render_template('about.html')
+
+5. Test website
+ $ python flaskblog.py
+ http://127.0.0.1:5000
+ http://127.0.0.1:5000/home
+ http://127.0.0.1:5000/about
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-2-fake-posts/flaskblog.py b/Python/Flask_Blog/00-Practices/B-Templates-2-fake-posts/flaskblog.py
new file mode 100644
index 000000000..5ac8c1625
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-2-fake-posts/flaskblog.py
@@ -0,0 +1,30 @@
+from flask import Flask, render_template
+
+app = Flask(__name__)
+
+# add variables, use Jinja2 to pass into html
+posts = [
+ {
+ 'author': 'Corey Schafer', 'title': 'Blog Post 1',
+ 'content': 'First post content', 'date_posted': 'April 20, 2018'
+ },
+ {
+ 'author': 'Jane Doe', 'title': 'Blog Post 2',
+ 'content': 'Second post content', 'date_posted': 'April 21, 2018'
+ }
+]
+
+
+# apply route decorator
+@app.route("/")
+@app.route("/home")
+def home():
+ return render_template('home.html', posts=posts)
+
+@app.route("/about")
+def about():
+ return render_template('about.html', title='About')
+
+
+if __name__ == '__main__' :
+ app.run(debug=True)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-2-fake-posts/templates/about.html b/Python/Flask_Blog/00-Practices/B-Templates-2-fake-posts/templates/about.html
new file mode 100644
index 000000000..e190939d8
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-2-fake-posts/templates/about.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ {% if title %}
+ Flask Blog - {{title}}
+ {% else %}
+ Flask Blog
+ {% endif %}
+
+
+
About page
+
+
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-2-fake-posts/templates/home.html b/Python/Flask_Blog/00-Practices/B-Templates-2-fake-posts/templates/home.html
new file mode 100644
index 000000000..dd9a59b38
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-2-fake-posts/templates/home.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+ {% if title %}
+ Flask Blog - {{title}}
+ {% else %}
+ Flask Blog
+ {% endif %}
+
+
+ {% for post in posts %}
+
{{ post.title }}
+
By {{ post.author }} on {{ post.date_posted }}
+
{{ post.content }}
+ {% endfor %}
+
+
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-2-fake-posts/zreadme.txt b/Python/Flask_Blog/00-Practices/B-Templates-2-fake-posts/zreadme.txt
new file mode 100644
index 000000000..6f710d6a2
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-2-fake-posts/zreadme.txt
@@ -0,0 +1,33 @@
+
+1. create posts variable in flaskblog.py
+
+2. pass 'posts' into home template in flaskblog.py
+
+ def home():
+ return render_template('home.html', posts=posts)
+
+3. update templates/home.html to view the posts by using Jinja2
+ {% for post in posts %}
+ ::::::
+ {% endfor %}
+
+4. Test website
+ $ python flaskblog.py
+ http://127.0.0.1:5000/home
+
+5. pass 'title' into about template in flaskblog.py
+
+ def about():
+ return render_template('about.html', title='About')
+
+6. update tag in home and about template to view the web page title
+
+ {% if title %}
+ ::::::
+ {% endif %}
+
+7. Test website
+ $ python flaskblog.py (if web server is not UP)
+ http://127.0.0.1:5000/home
+ http://127.0.0.1:5000/about
+ Now we can see web page title
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-3-inherit/flaskblog.py b/Python/Flask_Blog/00-Practices/B-Templates-3-inherit/flaskblog.py
new file mode 100644
index 000000000..5ac8c1625
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-3-inherit/flaskblog.py
@@ -0,0 +1,30 @@
+from flask import Flask, render_template
+
+app = Flask(__name__)
+
+# add variables, use Jinja2 to pass into html
+posts = [
+ {
+ 'author': 'Corey Schafer', 'title': 'Blog Post 1',
+ 'content': 'First post content', 'date_posted': 'April 20, 2018'
+ },
+ {
+ 'author': 'Jane Doe', 'title': 'Blog Post 2',
+ 'content': 'Second post content', 'date_posted': 'April 21, 2018'
+ }
+]
+
+
+# apply route decorator
+@app.route("/")
+@app.route("/home")
+def home():
+ return render_template('home.html', posts=posts)
+
+@app.route("/about")
+def about():
+ return render_template('about.html', title='About')
+
+
+if __name__ == '__main__' :
+ app.run(debug=True)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-3-inherit/templates/about.html b/Python/Flask_Blog/00-Practices/B-Templates-3-inherit/templates/about.html
new file mode 100644
index 000000000..95312bed7
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-3-inherit/templates/about.html
@@ -0,0 +1,4 @@
+{% extends "layout.html" %}
+{% block content %}
+
About page
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-3-inherit/templates/home.html b/Python/Flask_Blog/00-Practices/B-Templates-3-inherit/templates/home.html
new file mode 100644
index 000000000..975527628
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-3-inherit/templates/home.html
@@ -0,0 +1,8 @@
+{% extends "layout.html" %}
+{% block content %}
+ {% for post in posts %}
+
{{ post.title }}
+
By {{ post.author }} on {{ post.date_posted }}
+
{{ post.content }}
+ {% endfor %}
+{% endblock %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-3-inherit/templates/layout.html b/Python/Flask_Blog/00-Practices/B-Templates-3-inherit/templates/layout.html
new file mode 100644
index 000000000..7c651bdc3
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-3-inherit/templates/layout.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+ {% if title %}
+ Flask Blog - {{title}}
+ {% else %}
+ Flask Blog
+ {% endif %}
+
+
+
+
+
+ {% block content %}{% endblock %}
+
+
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-3-inherit/zreadme.txt b/Python/Flask_Blog/00-Practices/B-Templates-3-inherit/zreadme.txt
new file mode 100644
index 000000000..52380f8ed
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-3-inherit/zreadme.txt
@@ -0,0 +1,19 @@
+
+1. template inherit.
+ create layout template (layout.html) in templates
+
+
+ {% block content %}{% endblock %}
+
+2. update home and about templates to inherit layout template
+
+ {% extends "layout.html" %}
+ {% block content %}
+ ::::::
+ {% endblock content %}
+
+3. Test website
+ $ python flaskblog.py (if web server is not UP)
+ http://127.0.0.1:5000/home
+ http://127.0.0.1:5000/about
+ Now we can see same views with layout template inherited
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-4-bootstrap/flaskblog.py b/Python/Flask_Blog/00-Practices/B-Templates-4-bootstrap/flaskblog.py
new file mode 100644
index 000000000..5ac8c1625
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-4-bootstrap/flaskblog.py
@@ -0,0 +1,30 @@
+from flask import Flask, render_template
+
+app = Flask(__name__)
+
+# add variables, use Jinja2 to pass into html
+posts = [
+ {
+ 'author': 'Corey Schafer', 'title': 'Blog Post 1',
+ 'content': 'First post content', 'date_posted': 'April 20, 2018'
+ },
+ {
+ 'author': 'Jane Doe', 'title': 'Blog Post 2',
+ 'content': 'Second post content', 'date_posted': 'April 21, 2018'
+ }
+]
+
+
+# apply route decorator
+@app.route("/")
+@app.route("/home")
+def home():
+ return render_template('home.html', posts=posts)
+
+@app.route("/about")
+def about():
+ return render_template('about.html', title='About')
+
+
+if __name__ == '__main__' :
+ app.run(debug=True)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-4-bootstrap/templates/about.html b/Python/Flask_Blog/00-Practices/B-Templates-4-bootstrap/templates/about.html
new file mode 100644
index 000000000..95312bed7
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-4-bootstrap/templates/about.html
@@ -0,0 +1,4 @@
+{% extends "layout.html" %}
+{% block content %}
+
About page
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-4-bootstrap/templates/home.html b/Python/Flask_Blog/00-Practices/B-Templates-4-bootstrap/templates/home.html
new file mode 100644
index 000000000..975527628
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-4-bootstrap/templates/home.html
@@ -0,0 +1,8 @@
+{% extends "layout.html" %}
+{% block content %}
+ {% for post in posts %}
+
{{ post.title }}
+
By {{ post.author }} on {{ post.date_posted }}
+
{{ post.content }}
+ {% endfor %}
+{% endblock %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-4-bootstrap/templates/layout.html b/Python/Flask_Blog/00-Practices/B-Templates-4-bootstrap/templates/layout.html
new file mode 100644
index 000000000..40c507cd4
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-4-bootstrap/templates/layout.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+ {% if title %}
+ Flask Blog - {{title}}
+ {% else %}
+ Flask Blog
+ {% endif %}
+
+
+
+
+
+
+ {% block content %}{% endblock %}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-4-bootstrap/zreadme.txt b/Python/Flask_Blog/00-Practices/B-Templates-4-bootstrap/zreadme.txt
new file mode 100644
index 000000000..32caff926
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-4-bootstrap/zreadme.txt
@@ -0,0 +1,18 @@
+
+1. Update layout template with bootstrap 4 css framework
+
+
+
+
+2. Add div.container (bootstrap css) in templates/layout.html
+
+ {% block content %}{% endblock %}
+
+
+3. Test website
+ $ python flaskblog.py
+ (Must restart the web server to load new css,
+ or CTRL+SHIFT+R to force web browser to reload css)
+ http://127.0.0.1:5000/home
+ http://127.0.0.1:5000/about
+ Now we can see some margin in the views due to bootstrap css
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/flaskblog.py b/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/flaskblog.py
new file mode 100644
index 000000000..44788e5dd
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/flaskblog.py
@@ -0,0 +1,30 @@
+from flask import Flask, render_template, url_for
+
+app = Flask(__name__)
+
+# add variables, use Jinja2 to pass into html
+posts = [
+ {
+ 'author': 'Corey Schafer', 'title': 'Blog Post 1',
+ 'content': 'First post content', 'date_posted': 'April 20, 2018'
+ },
+ {
+ 'author': 'Jane Doe', 'title': 'Blog Post 2',
+ 'content': 'Second post content', 'date_posted': 'April 21, 2018'
+ }
+]
+
+
+# apply route decorator
+@app.route("/")
+@app.route("/home")
+def home():
+ return render_template('home.html', posts=posts)
+
+@app.route("/about")
+def about():
+ return render_template('about.html', title='About')
+
+
+if __name__ == '__main__' :
+ app.run(debug=True)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/static/main.css b/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/static/main.css
new file mode 100644
index 000000000..c05529fe9
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/static/main.css
@@ -0,0 +1,80 @@
+body {
+ background: #fafafa;
+ color: #333333;
+ margin-top: 5rem;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: #444444;
+}
+
+.bg-steel {
+ background-color: #5f788a;
+}
+
+.site-header .navbar-nav .nav-link {
+ color: #cbd5db;
+}
+
+.site-header .navbar-nav .nav-link:hover {
+ color: #ffffff;
+}
+
+.site-header .navbar-nav .nav-link.active {
+ font-weight: 500;
+}
+
+.content-section {
+ background: #ffffff;
+ padding: 10px 20px;
+ border: 1px solid #dddddd;
+ border-radius: 3px;
+ margin-bottom: 20px;
+}
+
+.article-title {
+ color: #444444;
+}
+
+a.article-title:hover {
+ color: #428bca;
+ text-decoration: none;
+}
+
+.article-content {
+ white-space: pre-line;
+}
+
+.article-img {
+ height: 65px;
+ width: 65px;
+ margin-right: 16px;
+}
+
+.article-metadata {
+ padding-bottom: 1px;
+ margin-bottom: 4px;
+ border-bottom: 1px solid #e3e3e3
+}
+
+.article-metadata a:hover {
+ color: #333;
+ text-decoration: none;
+}
+
+.article-svg {
+ width: 25px;
+ height: 25px;
+ vertical-align: middle;
+}
+
+.account-img {
+ height: 125px;
+ width: 125px;
+ margin-right: 20px;
+ margin-bottom: 16px;
+}
+
+.account-heading {
+ font-size: 2.5rem;
+}
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/templates/about.html b/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/templates/about.html
new file mode 100644
index 000000000..95312bed7
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/templates/about.html
@@ -0,0 +1,4 @@
+{% extends "layout.html" %}
+{% block content %}
+
About page
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/templates/home.html b/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/templates/home.html
new file mode 100644
index 000000000..e941cfcf5
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/templates/home.html
@@ -0,0 +1,15 @@
+{% extends "layout.html" %}
+{% block content %}
+ {% for post in posts %}
+
+
+
+ {% endfor %}
+{% endblock %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/templates/layout.html b/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/templates/layout.html
new file mode 100644
index 000000000..c642d284e
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/templates/layout.html
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+ {% if title %}
+ Flask Blog - {{title}}
+ {% else %}
+ Flask Blog
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+ {% block content %}{% endblock %}
+
+
+
+
Our Sidebar
+
You can put any information here you'd like.
+
+
Latest Posts
+
Announcements
+
Calendars
+
etc
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/zreadme.txt b/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/zreadme.txt
new file mode 100644
index 000000000..e36366f25
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/B-Templates-5-navbar/zreadme.txt
@@ -0,0 +1,51 @@
+
+1. Create navbar by
+ Copy snippets/navigation.html into templates/layout.html
+
+2. Create main section by
+ Copy snippets/main.html into templates/layout.html
+ Note the {% block content %}{% endblock %} is moved to main section
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/C-Forms-and-Validation-1/zreadme.txt b/Python/Flask_Blog/00-Practices/C-Forms-and-Validation-1/zreadme.txt
new file mode 100644
index 000000000..1c7369cbe
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/C-Forms-and-Validation-1/zreadme.txt
@@ -0,0 +1,62 @@
+
+1. Install flask-wtf
+ $ pip3 install flask-wtf
+
+2. create forms.py
+ - class RegistrationForm (FlaskForm) :
+ - class LoginForm (FlaskForm) :
+
+ In order to use form, we need to set security key
+ for this application
+
+3. set security key in flaskblog.py
+
+ # $ python
+ # >>> import secrets
+ # >>> secrets.token_hex(16)
+ app.config['SECRET_KEY'] = 'ada8419b155676bc78c2296aba9c7c7d'
+
+4. add register() and login() routes in flaskblog.py
+
+ from forms import RegistrationForm, LoginForm
+
+ @app.route("/register")
+ def register():
+ ::::::
+
+ @app.route("/login")
+ def login():
+ ::::::
+
+5. create register.html template
+
+
+
+
+
+3. test website
+ http://127.0.0.1:5000/
+ http://127.0.0.1:5000/account
+
+ Try to upload a text file to validate the errors. ie.
+ "File does not have an approved extension: jpg, png"
+
+4. Create a function to save the pictures in the specific directory in routes.py
+ - import os, secrets
+ - create
+ def save_picture(form_picture):
+
+5. Update account() route in routes.py to save the uploaded pictures
+ in the specific directory, and store the file name in the database
+
+ !!! Only the file name is stored in database. (current_user.image_file)
+
+ if form.picture.data :
+ picture_file = save_picture(form.picture.data)
+ current_user.image_file = picture_file
+
+6. update save_picture() in routes.py to resize the images before save
+
+ - $ pip3 intall Pillow
+ - from PIL import Image
+ - update save_picture()
+ output_size = (125, 125)
+ i = Image.open(form_picture)
+ i.thumbnail(output_size)
+ i.save(picture_path)
+
+
+7. test website
+ http://127.0.0.1:5000/
+ http://127.0.0.1:5000/account
+
+ Try to upload a new picture
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/__init__.py b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/__init__.py
new file mode 100644
index 000000000..a1bc035a4
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/__init__.py
@@ -0,0 +1,23 @@
+from flask import Flask
+from flask_sqlalchemy import SQLAlchemy
+from flask_bcrypt import Bcrypt
+from flask_login import LoginManager
+
+app = Flask(__name__)
+
+# $ python
+# >>> import secrets
+# >>> secrets.token_hex(16)
+app.config['SECRET_KEY'] = 'ada8419b155676bc78c2296aba9c7c7d'
+# /// represents the relative directory from the current file (flaskblog.py).
+# that is, site.db has the same directory as this __init__.py
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
+
+db = SQLAlchemy(app)
+bcrypt = Bcrypt(app) # Used to hash the password
+login_manager = LoginManager(app)
+login_manager.login_view = 'login'
+login_manager.login_message_category = 'info'
+
+# import routes from flaskblog package
+from flaskblog import routes
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/forms.py b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/forms.py
new file mode 100644
index 000000000..1bf0250f5
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/forms.py
@@ -0,0 +1,77 @@
+from flask_wtf import FlaskForm
+from flask_wtf.file import FileField, FileAllowed
+from flask_login import current_user
+from wtforms import StringField, PasswordField, SubmitField, BooleanField, TextAreaField
+from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
+from flaskblog.models import User
+
+# $ pip3 install flask_wtf
+# $ pip3 install email_validator
+
+# This module is imported in flaskblog.py to create forms
+
+class RegistrationForm (FlaskForm) :
+ # 'Username' in html pass objects in validators
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ confirm_password = PasswordField('Confirm Password',
+ validators=[DataRequired(), EqualTo('password')])
+ submit = SubmitField('Sign up')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ def validate_username(self, username) :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ def validate_email(self, email) :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class LoginForm (FlaskForm) :
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ # This (remember) allows users to stay login for certain time
+ # after web browser closes by using security cookies.
+ remember = BooleanField('Remember me')
+ submit = SubmitField('Login')
+
+
+# Copy RegistrationForm to modify
+class UpdateAccountForm (FlaskForm) :
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ picture = FileField('Update profile picture', validators=[FileAllowed(['jpg','png'])])
+ submit = SubmitField('Update')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ # validate only when username is not current_user's username
+ def validate_username(self, username) :
+ if username.data != current_user.username :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ # validate only when email is not current_user's email
+ def validate_email(self, email) :
+ if email.data != current_user.email :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class PostForm(FlaskForm) :
+ title = StringField('Title', validators=[DataRequired()])
+ content = TextAreaField('Content', validators=[DataRequired()])
+ submit = SubmitField('Post')
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/models.py b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/models.py
new file mode 100644
index 000000000..1e470f4db
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/models.py
@@ -0,0 +1,50 @@
+from datetime import datetime
+from flaskblog import db, login_manager
+from flask_login import UserMixin
+
+### !!! Generate database tables structure first
+### !!! by following zreadme_package.txt
+
+@login_manager.user_loader
+def load_user(user_id) :
+ return User.query.get(int(user_id))
+
+
+# defines User table structure in database
+class User(db.Model, UserMixin) :
+ # primary_key specifies unique id
+ id = db.Column(db.Integer, primary_key=True)
+ # max 20 characters, unique, and required
+ username = db.Column(db.String(20), unique=True, nullable=False)
+ # max 120 characters, unique, and required
+ email = db.Column(db.String(120), unique=True, nullable=False)
+ # profile image
+ image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
+ # password (be hashed)
+ password = db.Column(db.String(60), nullable=False)
+ # --posts is not a column, but a relationship running a query
+ # in background to collect all the posts by this user
+ # --lazy=true, SQLALCHEMY loads the data as necessary in one go
+ # --backref is a simple way to declare a new property on the Post class.
+ # You can then use post.author to get to the user who writes the post.
+ posts = db.relationship('Post', backref='author', lazy=True)
+
+ # function to print this class (User)
+ def __repr__(self) :
+ return f"User('{self.username}','{self.email}','{self.image_file}')"
+
+# defines Post table structure in database
+class Post(db.Model) :
+ id = db.Column(db.Integer, primary_key=True)
+ title = db.Column(db.String(100), nullable=False)
+ # type of DateTime, the current time as default (utcnow)
+ # The default is passed as the function name of utcnow. Do not use
+ # utcnow(), which can invoke the function right away.
+ date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+ content = db.Column(db.Text, nullable=False)
+ # Use a foreign key that references the primary key of User table
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
+
+ # function to print this class (Post)
+ def __repr__(self) :
+ return f"Post('{self.title}','{self.date_posted}')"
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/routes.py b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/routes.py
new file mode 100644
index 000000000..497f0afb5
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/routes.py
@@ -0,0 +1,137 @@
+from flask import render_template, url_for, flash, redirect, request
+from flaskblog import app, db, bcrypt
+from flaskblog.forms import RegistrationForm, LoginForm, UpdateAccountForm, PostForm
+from flaskblog.models import User, Post
+from flask_login import login_user, current_user, logout_user, login_required
+import os, secrets
+from PIL import Image
+
+# forms.py in flaskblog package
+# models.py in flaskblog package
+
+posts = [
+ {
+ 'author': 'Corey Schafer',
+ 'title': 'Blog Post 1',
+ 'content': 'First post content',
+ 'date_posted': 'April 20, 2018'
+ },
+ {
+ 'author': 'Jane Doe',
+ 'title': 'Blog Post 2',
+ 'content': 'Second post content',
+ 'date_posted': 'April 21, 2018'
+ }
+]
+
+
+# http://localhost:5000/
+@app.route("/")
+@app.route("/home")
+def home():
+ return render_template('home.html', posts=posts)
+
+# http://localhost:5000/about
+@app.route("/about")
+def about():
+ return render_template('about.html', title='About')
+
+@app.route("/register", methods=['GET', 'POST'])
+def register():
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = RegistrationForm()
+ # once submit successfully, flash message shows in the placehold in layout.html
+ # flash message is one time only, it disappear after refresh web browser
+ if form.validate_on_submit():
+ hashed_pwd = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
+ user = User(username=form.username.data, email=form.email.data, password=hashed_pwd)
+ db.session.add(user)
+ db.session.commit()
+ flash('Your account has been created! You are now able to log in!', 'success')
+ return redirect(url_for('login'))
+ return render_template('register.html', title='Register', form=form)
+
+@app.route("/login", methods=['GET', 'POST'])
+def login():
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = LoginForm()
+ if form.validate_on_submit():
+ user = User.query.filter_by(email=form.email.data).first()
+ if user and bcrypt.check_password_hash(user.password, form.password.data) :
+ login_user(user, remember=form.remember.data)
+ # request.args is dictionary type
+ next_page = request.args.get('next')
+ # 'home' is function of home() above
+ return redirect(next_page) if next_page else redirect(url_for('home'))
+ else:
+ flash('Login failed. Please check email and password.', 'danger')
+ return render_template('login.html', title='Login', form=form)
+
+
+@app.route("/logout")
+def logout() :
+ logout_user()
+ return redirect(url_for('home'))
+
+# save uploaded image to the specified path
+def save_picture(form_picture) :
+ random_hex = secrets.token_hex(8)
+ # underscore (_) ignoring the value
+ _, f_ext = os.path.splitext(form_picture.filename)
+ picture_fn = random_hex + f_ext
+ # define the path to store profile image
+ picture_path = os.path.join(app.root_path, 'static/profile_pics', picture_fn)
+
+ # save the uploaded image into the specified path
+
+ # do not want to save the original uploaded image, which can be very large
+ # form_picture.save(picture_path)
+
+ #resize the image before store
+ output_size = (125, 125)
+ i = Image.open(form_picture)
+ i.thumbnail(output_size)
+ i.save(picture_path)
+
+ return picture_fn
+
+
+# login_required decorator makes sure to access the page only
+# when the user log in
+@app.route("/account", methods=['GET', 'POST'])
+@login_required
+def account() :
+ form = UpdateAccountForm()
+ if form.validate_on_submit() :
+ if form.picture.data :
+ picture_file = save_picture(form.picture.data)
+ current_user.image_file = picture_file
+ # think of current_user is reference to the user in database
+ current_user.username = form.username.data
+ current_user.email = form.email.data
+ db.session.commit()
+ flash('Your account has been updated!', 'success')
+ # This is must. After POST request, do the GET request
+ # to avoid POST request again
+ return redirect(url_for('account'))
+ elif request.method == 'GET' :
+ # populate the username and email so that
+ # the new username and email can be viewed right after
+ # update form is submitted
+ form.username.data = current_user.username
+ form.email.data = current_user.email
+ image_file = url_for('static', filename='profile_pics/'+current_user.image_file)
+ return render_template('account.html', title='Account',
+ image_file=image_file, form=form)
+
+
+@app.route("/post/new", methods=['GET', 'POST'])
+@login_required
+def new_post() :
+ form = PostForm()
+ if form.validate_on_submit():
+ flash('Your post has been created!', 'success')
+ return redirect(url_for('home'))
+ return render_template('create_post.html', title='New Post', form=form)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/site.db b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/site.db
new file mode 100644
index 000000000..9162861d4
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/site.db differ
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/static/main.css b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/static/main.css
new file mode 100644
index 000000000..730e2bca6
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/static/main.css
@@ -0,0 +1,80 @@
+body {
+ background: #fafafa;
+ color: #333333;
+ margin-top: 5rem;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: #444444;
+}
+
+.bg-steel {
+ background-color: #5f788a;
+}
+
+.site-header .navbar-nav .nav-link {
+ color: #cbd5db;
+}
+
+.site-header .navbar-nav .nav-link:hover {
+ color: #ffffff;
+}
+
+.site-header .navbar-nav .nav-link:active {
+ font-weight: 500;
+}
+
+.content-section {
+ background: #ffffff;
+ padding: 10px 20px;
+ border: 1px solid #dddddd;
+ border-radius: 3px;
+ margin-bottom: 20px;
+}
+
+.article-title {
+ color: #444444;
+}
+
+a.article-title:hover {
+ color: #428bca;
+ text-decoration: none;
+}
+
+.article-content {
+ white-space: pre-line;
+}
+
+.article-img {
+ height: 65px;
+ width: 65px;
+ margin-right: 16px;
+}
+
+.article-metadata {
+ padding-bottom: 1px;
+ margin-bottom: 4px;
+ border-bottom: 1px solid #e3e3e3
+}
+
+.article-metadata a:hover {
+ color: #333;
+ text-decoration: none;
+}
+
+.article-svg {
+ width: 25px;
+ height: 25px;
+ vertical-align: middle;
+}
+
+.account-img {
+ height: 125px;
+ width: 125px;
+ margin-right: 20px;
+ margin-bottom: 16px;
+}
+
+.account-heading {
+ font-size: 2.5rem;
+}
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/static/profile_pics/919b30429a4f5711.jpg b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/static/profile_pics/919b30429a4f5711.jpg
new file mode 100644
index 000000000..2fe68efe2
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/static/profile_pics/919b30429a4f5711.jpg differ
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/static/profile_pics/default.jpg b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/static/profile_pics/default.jpg
new file mode 100644
index 000000000..38f286f63
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/static/profile_pics/default.jpg differ
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/templates/about.html b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/templates/about.html
new file mode 100644
index 000000000..95312bed7
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/templates/about.html
@@ -0,0 +1,4 @@
+{% extends "layout.html" %}
+{% block content %}
+
About page
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/templates/account.html b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/templates/account.html
new file mode 100644
index 000000000..86b047a25
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-1/flaskblog/templates/account.html
@@ -0,0 +1,64 @@
+{% extends "layout.html" %}
+{% block content %}
+
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-2/run.py b/Python/Flask_Blog/00-Practices/H-Posts-2/run.py
new file mode 100644
index 000000000..0c8390926
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-2/run.py
@@ -0,0 +1,7 @@
+from flaskblog import app
+
+# flaskblog is package
+# import app from flaskblog package
+
+if __name__ == '__main__':
+ app.run(debug=True)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-2/zreadme.txt b/Python/Flask_Blog/00-Practices/H-Posts-2/zreadme.txt
new file mode 100644
index 000000000..865627b52
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-2/zreadme.txt
@@ -0,0 +1,32 @@
+
+1. Store post into database in new_post() in routes.py
+ # Construct Post object which defined in models.py
+ post = Post(title=form.title.data, content=form.content.data, author=current_user)
+ db.session.add(post)
+ db.session.commit()
+
+2. Delete the dummy posts data in routes.py
+
+3. Get posts from database in home() route in routes.py
+ posts = Post.query.all()
+
+4. test website
+ http://127.0.0.1:5000/
+ Now we do not have any posts yet
+ - log in the account and click 'New Post'
+ Now we see the post just being added
+
+5. fix username and date view in home.html template
+
+ {{ post.author.username }}
+ {{ post.date_posted.strftime('%Y-%m-%d') }}
+
+6. add user profile image in home.html template
+
+
+
+
+
+
+
+
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/__init__.py b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/__init__.py
new file mode 100644
index 000000000..a1bc035a4
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/__init__.py
@@ -0,0 +1,23 @@
+from flask import Flask
+from flask_sqlalchemy import SQLAlchemy
+from flask_bcrypt import Bcrypt
+from flask_login import LoginManager
+
+app = Flask(__name__)
+
+# $ python
+# >>> import secrets
+# >>> secrets.token_hex(16)
+app.config['SECRET_KEY'] = 'ada8419b155676bc78c2296aba9c7c7d'
+# /// represents the relative directory from the current file (flaskblog.py).
+# that is, site.db has the same directory as this __init__.py
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
+
+db = SQLAlchemy(app)
+bcrypt = Bcrypt(app) # Used to hash the password
+login_manager = LoginManager(app)
+login_manager.login_view = 'login'
+login_manager.login_message_category = 'info'
+
+# import routes from flaskblog package
+from flaskblog import routes
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/forms.py b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/forms.py
new file mode 100644
index 000000000..1bf0250f5
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/forms.py
@@ -0,0 +1,77 @@
+from flask_wtf import FlaskForm
+from flask_wtf.file import FileField, FileAllowed
+from flask_login import current_user
+from wtforms import StringField, PasswordField, SubmitField, BooleanField, TextAreaField
+from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
+from flaskblog.models import User
+
+# $ pip3 install flask_wtf
+# $ pip3 install email_validator
+
+# This module is imported in flaskblog.py to create forms
+
+class RegistrationForm (FlaskForm) :
+ # 'Username' in html pass objects in validators
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ confirm_password = PasswordField('Confirm Password',
+ validators=[DataRequired(), EqualTo('password')])
+ submit = SubmitField('Sign up')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ def validate_username(self, username) :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ def validate_email(self, email) :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class LoginForm (FlaskForm) :
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ # This (remember) allows users to stay login for certain time
+ # after web browser closes by using security cookies.
+ remember = BooleanField('Remember me')
+ submit = SubmitField('Login')
+
+
+# Copy RegistrationForm to modify
+class UpdateAccountForm (FlaskForm) :
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ picture = FileField('Update profile picture', validators=[FileAllowed(['jpg','png'])])
+ submit = SubmitField('Update')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ # validate only when username is not current_user's username
+ def validate_username(self, username) :
+ if username.data != current_user.username :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ # validate only when email is not current_user's email
+ def validate_email(self, email) :
+ if email.data != current_user.email :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class PostForm(FlaskForm) :
+ title = StringField('Title', validators=[DataRequired()])
+ content = TextAreaField('Content', validators=[DataRequired()])
+ submit = SubmitField('Post')
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/models.py b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/models.py
new file mode 100644
index 000000000..1e470f4db
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/models.py
@@ -0,0 +1,50 @@
+from datetime import datetime
+from flaskblog import db, login_manager
+from flask_login import UserMixin
+
+### !!! Generate database tables structure first
+### !!! by following zreadme_package.txt
+
+@login_manager.user_loader
+def load_user(user_id) :
+ return User.query.get(int(user_id))
+
+
+# defines User table structure in database
+class User(db.Model, UserMixin) :
+ # primary_key specifies unique id
+ id = db.Column(db.Integer, primary_key=True)
+ # max 20 characters, unique, and required
+ username = db.Column(db.String(20), unique=True, nullable=False)
+ # max 120 characters, unique, and required
+ email = db.Column(db.String(120), unique=True, nullable=False)
+ # profile image
+ image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
+ # password (be hashed)
+ password = db.Column(db.String(60), nullable=False)
+ # --posts is not a column, but a relationship running a query
+ # in background to collect all the posts by this user
+ # --lazy=true, SQLALCHEMY loads the data as necessary in one go
+ # --backref is a simple way to declare a new property on the Post class.
+ # You can then use post.author to get to the user who writes the post.
+ posts = db.relationship('Post', backref='author', lazy=True)
+
+ # function to print this class (User)
+ def __repr__(self) :
+ return f"User('{self.username}','{self.email}','{self.image_file}')"
+
+# defines Post table structure in database
+class Post(db.Model) :
+ id = db.Column(db.Integer, primary_key=True)
+ title = db.Column(db.String(100), nullable=False)
+ # type of DateTime, the current time as default (utcnow)
+ # The default is passed as the function name of utcnow. Do not use
+ # utcnow(), which can invoke the function right away.
+ date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+ content = db.Column(db.Text, nullable=False)
+ # Use a foreign key that references the primary key of User table
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
+
+ # function to print this class (Post)
+ def __repr__(self) :
+ return f"Post('{self.title}','{self.date_posted}')"
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/routes.py b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/routes.py
new file mode 100644
index 000000000..3845646d6
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/routes.py
@@ -0,0 +1,165 @@
+from flask import render_template, url_for, flash, redirect, request, abort
+from flaskblog import app, db, bcrypt
+from flaskblog.forms import RegistrationForm, LoginForm, UpdateAccountForm, PostForm
+from flaskblog.models import User, Post
+from flask_login import login_user, current_user, logout_user, login_required
+import os, secrets
+from PIL import Image
+
+
+# http://localhost:5000/
+@app.route("/")
+@app.route("/home")
+def home():
+ posts = Post.query.all()
+ return render_template('home.html', posts=posts)
+
+# http://localhost:5000/about
+@app.route("/about")
+def about():
+ return render_template('about.html', title='About')
+
+@app.route("/register", methods=['GET', 'POST'])
+def register():
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = RegistrationForm()
+ # once submit successfully, flash message shows in the placehold in layout.html
+ # flash message is one time only, it disappear after refresh web browser
+ if form.validate_on_submit():
+ hashed_pwd = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
+ user = User(username=form.username.data, email=form.email.data, password=hashed_pwd)
+ db.session.add(user)
+ db.session.commit()
+ flash('Your account has been created! You are now able to log in!', 'success')
+ return redirect(url_for('login'))
+ return render_template('register.html', title='Register', form=form)
+
+@app.route("/login", methods=['GET', 'POST'])
+def login():
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = LoginForm()
+ if form.validate_on_submit():
+ user = User.query.filter_by(email=form.email.data).first()
+ if user and bcrypt.check_password_hash(user.password, form.password.data) :
+ login_user(user, remember=form.remember.data)
+ # request.args is dictionary type
+ next_page = request.args.get('next')
+ # 'home' is function of home() above
+ return redirect(next_page) if next_page else redirect(url_for('home'))
+ else:
+ flash('Login failed. Please check email and password.', 'danger')
+ return render_template('login.html', title='Login', form=form)
+
+
+@app.route("/logout")
+def logout() :
+ logout_user()
+ return redirect(url_for('home'))
+
+# save uploaded image to the specified path
+def save_picture(form_picture) :
+ random_hex = secrets.token_hex(8)
+ # underscore (_) ignoring the value
+ _, f_ext = os.path.splitext(form_picture.filename)
+ picture_fn = random_hex + f_ext
+ # define the path to store profile image
+ picture_path = os.path.join(app.root_path, 'static/profile_pics', picture_fn)
+
+ # save the uploaded image into the specified path
+
+ # do not want to save the original uploaded image, which can be very large
+ # form_picture.save(picture_path)
+
+ #resize the image before store
+ output_size = (125, 125)
+ i = Image.open(form_picture)
+ i.thumbnail(output_size)
+ i.save(picture_path)
+
+ return picture_fn
+
+
+# login_required decorator makes sure to access the page only
+# when the user log in
+@app.route("/account", methods=['GET', 'POST'])
+@login_required
+def account() :
+ form = UpdateAccountForm()
+ if form.validate_on_submit() :
+ if form.picture.data :
+ picture_file = save_picture(form.picture.data)
+ current_user.image_file = picture_file
+ # think of current_user is reference to the user in database
+ current_user.username = form.username.data
+ current_user.email = form.email.data
+ db.session.commit()
+ flash('Your account has been updated!', 'success')
+ # This is must. After POST request, do the GET request
+ # to avoid POST request again
+ return redirect(url_for('account'))
+ elif request.method == 'GET' :
+ # populate the username and email so that
+ # the new username and email can be viewed right after
+ # update form is submitted
+ form.username.data = current_user.username
+ form.email.data = current_user.email
+ image_file = url_for('static', filename='profile_pics/'+current_user.image_file)
+ return render_template('account.html', title='Account',
+ image_file=image_file, form=form)
+
+
+@app.route("/post/new", methods=['GET', 'POST'])
+@login_required
+def new_post() :
+ form = PostForm()
+ if form.validate_on_submit():
+ # Construct Post object which defined in models.py
+ post = Post(title=form.title.data, content=form.content.data, author=current_user)
+ db.session.add(post)
+ db.session.commit()
+ flash('Your post has been created!', 'success')
+ return redirect(url_for('home'))
+ return render_template('create_post.html', title='New Post',
+ form=form, legend='New Post')
+
+
+@app.route("/post/")
+def post(post_id) :
+ # find the post or give page not found 404 error
+ post = Post.query.get_or_404(post_id)
+ return render_template('post.html', title=post.title, post=post)
+
+@app.route("/post//update", methods=['GET', 'POST'])
+@login_required
+def update_post(post_id) :
+ # find the post or give page not found 404 error
+ post = Post.query.get_or_404(post_id)
+ if post.author != current_user:
+ abort(403)
+ form = PostForm()
+ if form.validate_on_submit():
+ post.title = form.title.data
+ post.content = form.content.data
+ db.session.commit()
+ flash('Your post has been update!', 'success')
+ return redirect(url_for('post', post_id=post.id))
+ elif request.method == 'GET':
+ form.title.data = post.title
+ form.content.data = post.content
+ return render_template('create_post.html', title='Update Post',
+ form=form, legend='Update Post')
+
+
+# "Delete" button in Modal sends POST request to delete the post
+@app.route("/post//delete", methods=['POST'])
+@login_required
+def delete_post(post_id) :
+ post = Post.query.get_or_404(post_id)
+ if post.author != current_user:
+ abort(403)
+ db.session.delete(post)
+ db.session.commit()
+ flash('Your post has been deleted!', 'success')
+ return redirect(url_for('home'))
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/site.db b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/site.db
new file mode 100644
index 000000000..aece297f5
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/site.db differ
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/static/main.css b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/static/main.css
new file mode 100644
index 000000000..730e2bca6
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/static/main.css
@@ -0,0 +1,80 @@
+body {
+ background: #fafafa;
+ color: #333333;
+ margin-top: 5rem;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: #444444;
+}
+
+.bg-steel {
+ background-color: #5f788a;
+}
+
+.site-header .navbar-nav .nav-link {
+ color: #cbd5db;
+}
+
+.site-header .navbar-nav .nav-link:hover {
+ color: #ffffff;
+}
+
+.site-header .navbar-nav .nav-link:active {
+ font-weight: 500;
+}
+
+.content-section {
+ background: #ffffff;
+ padding: 10px 20px;
+ border: 1px solid #dddddd;
+ border-radius: 3px;
+ margin-bottom: 20px;
+}
+
+.article-title {
+ color: #444444;
+}
+
+a.article-title:hover {
+ color: #428bca;
+ text-decoration: none;
+}
+
+.article-content {
+ white-space: pre-line;
+}
+
+.article-img {
+ height: 65px;
+ width: 65px;
+ margin-right: 16px;
+}
+
+.article-metadata {
+ padding-bottom: 1px;
+ margin-bottom: 4px;
+ border-bottom: 1px solid #e3e3e3
+}
+
+.article-metadata a:hover {
+ color: #333;
+ text-decoration: none;
+}
+
+.article-svg {
+ width: 25px;
+ height: 25px;
+ vertical-align: middle;
+}
+
+.account-img {
+ height: 125px;
+ width: 125px;
+ margin-right: 20px;
+ margin-bottom: 16px;
+}
+
+.account-heading {
+ font-size: 2.5rem;
+}
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/static/profile_pics/919b30429a4f5711.jpg b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/static/profile_pics/919b30429a4f5711.jpg
new file mode 100644
index 000000000..2fe68efe2
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/static/profile_pics/919b30429a4f5711.jpg differ
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/static/profile_pics/default.jpg b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/static/profile_pics/default.jpg
new file mode 100644
index 000000000..38f286f63
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/static/profile_pics/default.jpg differ
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/templates/about.html b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/templates/about.html
new file mode 100644
index 000000000..95312bed7
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/templates/about.html
@@ -0,0 +1,4 @@
+{% extends "layout.html" %}
+{% block content %}
+
About page
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/templates/account.html b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/templates/account.html
new file mode 100644
index 000000000..86b047a25
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-3/flaskblog/templates/account.html
@@ -0,0 +1,64 @@
+{% extends "layout.html" %}
+{% block content %}
+
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-3/run.py b/Python/Flask_Blog/00-Practices/H-Posts-3/run.py
new file mode 100644
index 000000000..0c8390926
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-3/run.py
@@ -0,0 +1,7 @@
+from flaskblog import app
+
+# flaskblog is package
+# import app from flaskblog package
+
+if __name__ == '__main__':
+ app.run(debug=True)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/H-Posts-3/zreadme.txt b/Python/Flask_Blog/00-Practices/H-Posts-3/zreadme.txt
new file mode 100644
index 000000000..a6d224f20
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/H-Posts-3/zreadme.txt
@@ -0,0 +1,149 @@
+
+1. Create post(post_id) route in routes.py
+
+ @app.route("/post/")
+ def post(post_id) :
+ # find the post or give page not found 404 error
+ post = Post.query.get_or_404(post_id)
+ return render_template('post.html', title=post.title, post=post)
+
+2. Create post.html template.
+ Copy home.html and modify to only a single post
+
+3. Edit home.html post link to post() route with post_id
+
+
+
+4. test website
+ http://127.0.0.1:5000/
+ - Register new user, and add new post
+ - We can see posts with different users on home page
+ - click one post to a single post
+ http://127.0.0.1:5000/post/1
+ - Try a non-existing post id
+ http://127.0.0.1:5000/post/6
+ We can see 404 page "Not Found"
+
+5. Create update_post() route in routes.py
+ - import abort
+ - @app.route("/post//update")
+ @login_required
+ def update_post(post_id) :
+ This is similar in new_post() route, but 'legend' is added
+ return render_template('create_post.html', title='Update Post', form=form)
+
+6. Add legend='New Post' in new_post() route.
+
+7. Update create_post.html template to apply 'legend'.
+
+8. test website
+ http://127.0.0.1:5000/
+ - Log in a user
+
+ - Click a post is not posted by this user
+ ie. http://127.0.0.1:5000/post/1
+ - Try http://127.0.0.1:5000/post/1/update
+ you will see "Forbidden" by abort(403)
+
+ - Click another post is posted by the current user
+ http://127.0.0.1:5000/post/2/update
+ You will see the update form and form fields are empty.
+
+9. Update update_post() route to show form fields.
+ form.title.data = post.title
+ form.content.data = post.content
+
+10. test website
+ http://127.0.0.1:5000/
+ - Log in a user
+ - Click a post is posted by the current user
+ http://127.0.0.1:5000/post/2/update
+ You will see the update form and form fields.
+
+11. Update update_post() route to store post into database,
+ send flash message,
+ and add methods=['GET', 'POST']
+ then populate post for 'GET' request (redirect() sends 'GET' request)
+
+ form = PostForm()
+ if form.validate_on_submit():
+ # store post into database
+ post.title = form.title.data
+ post.content = form.content.data
+ db.session.commit()
+ flash('Your post has been update!', 'success')
+ # send 'GET' request
+ return redirect(url_for('post', post_id=post.id))
+ elif request.method == 'GET':
+ # populate post data
+ form.title.data = post.title
+ form.content.data = post.content
+ return render_template('create_post.html', title='Update Post',
+ form=form, legend='Update Post')
+
+10. test website
+ http://127.0.0.1:5000/
+ - Log in a user
+ - Click a post is posted by the current user
+ http://127.0.0.1:5000/post/2/update
+ Modify title and content, and click Post.
+
+11. Update post.html by adding 'update' and 'delete' buttons
+ - add bootstrap Modal to 'delete' button
+ - https://getbootstrap.com/docs/4.0/components/modal/
+ Look for 'Live demo'
+ - Copy Modal template in above web page and do changes.
+
+ {% if post.author == current_user %}
+
+
+12. add delete_post() route in routes.py for testing website.
+
+ @app.route("/post//delete")
+ @login_required
+ def delete_post(post_id) :
+ pass
+
+13. test website
+ http://127.0.0.1:5000/
+ - Log in a user
+ - Click a post is posted by the current user
+ Now we see 'update' and 'delete' buttons
+ - click Update to update the post
+ - click Delete, we see Modal, then click 'Close'.
+
+14. Update delete_post() route in routes.py
+
+ # "Delete" button in Modal sends POST request to delete the post
+ @app.route("/post//delete", methods=['POST'])
+ @login_required
+ def delete_post(post_id) :
+ post = Post.query.get_or_404(post_id)
+ if post.author != current_user:
+ abort(403)
+ db.session.delete(post)
+ db.session.commit()
+ flash('Your post has been deleted!', 'success')
+ return redirect(url_for('home'))
+
+15. test website
+ http://127.0.0.1:5000/
+ - Log in a user
+ - Click a post is posted by the current user
+ Now we see 'update' and 'delete' buttons
+ - click Update to update the post
+ - click Delete, we see Modal, then click 'Delete' to delete the post
+
+
+
+
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/__init__.py b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/__init__.py
new file mode 100644
index 000000000..a1bc035a4
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/__init__.py
@@ -0,0 +1,23 @@
+from flask import Flask
+from flask_sqlalchemy import SQLAlchemy
+from flask_bcrypt import Bcrypt
+from flask_login import LoginManager
+
+app = Flask(__name__)
+
+# $ python
+# >>> import secrets
+# >>> secrets.token_hex(16)
+app.config['SECRET_KEY'] = 'ada8419b155676bc78c2296aba9c7c7d'
+# /// represents the relative directory from the current file (flaskblog.py).
+# that is, site.db has the same directory as this __init__.py
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
+
+db = SQLAlchemy(app)
+bcrypt = Bcrypt(app) # Used to hash the password
+login_manager = LoginManager(app)
+login_manager.login_view = 'login'
+login_manager.login_message_category = 'info'
+
+# import routes from flaskblog package
+from flaskblog import routes
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/forms.py b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/forms.py
new file mode 100644
index 000000000..1bf0250f5
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/forms.py
@@ -0,0 +1,77 @@
+from flask_wtf import FlaskForm
+from flask_wtf.file import FileField, FileAllowed
+from flask_login import current_user
+from wtforms import StringField, PasswordField, SubmitField, BooleanField, TextAreaField
+from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
+from flaskblog.models import User
+
+# $ pip3 install flask_wtf
+# $ pip3 install email_validator
+
+# This module is imported in flaskblog.py to create forms
+
+class RegistrationForm (FlaskForm) :
+ # 'Username' in html pass objects in validators
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ confirm_password = PasswordField('Confirm Password',
+ validators=[DataRequired(), EqualTo('password')])
+ submit = SubmitField('Sign up')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ def validate_username(self, username) :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ def validate_email(self, email) :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class LoginForm (FlaskForm) :
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ # This (remember) allows users to stay login for certain time
+ # after web browser closes by using security cookies.
+ remember = BooleanField('Remember me')
+ submit = SubmitField('Login')
+
+
+# Copy RegistrationForm to modify
+class UpdateAccountForm (FlaskForm) :
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ picture = FileField('Update profile picture', validators=[FileAllowed(['jpg','png'])])
+ submit = SubmitField('Update')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ # validate only when username is not current_user's username
+ def validate_username(self, username) :
+ if username.data != current_user.username :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ # validate only when email is not current_user's email
+ def validate_email(self, email) :
+ if email.data != current_user.email :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class PostForm(FlaskForm) :
+ title = StringField('Title', validators=[DataRequired()])
+ content = TextAreaField('Content', validators=[DataRequired()])
+ submit = SubmitField('Post')
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/models.py b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/models.py
new file mode 100644
index 000000000..1e470f4db
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/models.py
@@ -0,0 +1,50 @@
+from datetime import datetime
+from flaskblog import db, login_manager
+from flask_login import UserMixin
+
+### !!! Generate database tables structure first
+### !!! by following zreadme_package.txt
+
+@login_manager.user_loader
+def load_user(user_id) :
+ return User.query.get(int(user_id))
+
+
+# defines User table structure in database
+class User(db.Model, UserMixin) :
+ # primary_key specifies unique id
+ id = db.Column(db.Integer, primary_key=True)
+ # max 20 characters, unique, and required
+ username = db.Column(db.String(20), unique=True, nullable=False)
+ # max 120 characters, unique, and required
+ email = db.Column(db.String(120), unique=True, nullable=False)
+ # profile image
+ image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
+ # password (be hashed)
+ password = db.Column(db.String(60), nullable=False)
+ # --posts is not a column, but a relationship running a query
+ # in background to collect all the posts by this user
+ # --lazy=true, SQLALCHEMY loads the data as necessary in one go
+ # --backref is a simple way to declare a new property on the Post class.
+ # You can then use post.author to get to the user who writes the post.
+ posts = db.relationship('Post', backref='author', lazy=True)
+
+ # function to print this class (User)
+ def __repr__(self) :
+ return f"User('{self.username}','{self.email}','{self.image_file}')"
+
+# defines Post table structure in database
+class Post(db.Model) :
+ id = db.Column(db.Integer, primary_key=True)
+ title = db.Column(db.String(100), nullable=False)
+ # type of DateTime, the current time as default (utcnow)
+ # The default is passed as the function name of utcnow. Do not use
+ # utcnow(), which can invoke the function right away.
+ date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+ content = db.Column(db.Text, nullable=False)
+ # Use a foreign key that references the primary key of User table
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
+
+ # function to print this class (Post)
+ def __repr__(self) :
+ return f"Post('{self.title}','{self.date_posted}')"
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/routes.py b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/routes.py
new file mode 100644
index 000000000..c8bb27335
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/routes.py
@@ -0,0 +1,166 @@
+from flask import render_template, url_for, flash, redirect, request, abort
+from flaskblog import app, db, bcrypt
+from flaskblog.forms import RegistrationForm, LoginForm, UpdateAccountForm, PostForm
+from flaskblog.models import User, Post
+from flask_login import login_user, current_user, logout_user, login_required
+import os, secrets
+from PIL import Image
+
+
+# http://localhost:5000/
+@app.route("/")
+@app.route("/home")
+def home():
+ page = request.args.get('page', 1, type=int)
+ posts = Post.query.paginate(page=page, per_page=5)
+ return render_template('home.html', posts=posts)
+
+# http://localhost:5000/about
+@app.route("/about")
+def about():
+ return render_template('about.html', title='About')
+
+@app.route("/register", methods=['GET', 'POST'])
+def register():
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = RegistrationForm()
+ # once submit successfully, flash message shows in the placehold in layout.html
+ # flash message is one time only, it disappear after refresh web browser
+ if form.validate_on_submit():
+ hashed_pwd = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
+ user = User(username=form.username.data, email=form.email.data, password=hashed_pwd)
+ db.session.add(user)
+ db.session.commit()
+ flash('Your account has been created! You are now able to log in!', 'success')
+ return redirect(url_for('login'))
+ return render_template('register.html', title='Register', form=form)
+
+@app.route("/login", methods=['GET', 'POST'])
+def login():
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = LoginForm()
+ if form.validate_on_submit():
+ user = User.query.filter_by(email=form.email.data).first()
+ if user and bcrypt.check_password_hash(user.password, form.password.data) :
+ login_user(user, remember=form.remember.data)
+ # request.args is dictionary type
+ next_page = request.args.get('next')
+ # 'home' is function of home() above
+ return redirect(next_page) if next_page else redirect(url_for('home'))
+ else:
+ flash('Login failed. Please check email and password.', 'danger')
+ return render_template('login.html', title='Login', form=form)
+
+
+@app.route("/logout")
+def logout() :
+ logout_user()
+ return redirect(url_for('home'))
+
+# save uploaded image to the specified path
+def save_picture(form_picture) :
+ random_hex = secrets.token_hex(8)
+ # underscore (_) ignoring the value
+ _, f_ext = os.path.splitext(form_picture.filename)
+ picture_fn = random_hex + f_ext
+ # define the path to store profile image
+ picture_path = os.path.join(app.root_path, 'static/profile_pics', picture_fn)
+
+ # save the uploaded image into the specified path
+
+ # do not want to save the original uploaded image, which can be very large
+ # form_picture.save(picture_path)
+
+ #resize the image before store
+ output_size = (125, 125)
+ i = Image.open(form_picture)
+ i.thumbnail(output_size)
+ i.save(picture_path)
+
+ return picture_fn
+
+
+# login_required decorator makes sure to access the page only
+# when the user log in
+@app.route("/account", methods=['GET', 'POST'])
+@login_required
+def account() :
+ form = UpdateAccountForm()
+ if form.validate_on_submit() :
+ if form.picture.data :
+ picture_file = save_picture(form.picture.data)
+ current_user.image_file = picture_file
+ # think of current_user is reference to the user in database
+ current_user.username = form.username.data
+ current_user.email = form.email.data
+ db.session.commit()
+ flash('Your account has been updated!', 'success')
+ # This is must. After POST request, do the GET request
+ # to avoid POST request again
+ return redirect(url_for('account'))
+ elif request.method == 'GET' :
+ # populate the username and email so that
+ # the new username and email can be viewed right after
+ # update form is submitted
+ form.username.data = current_user.username
+ form.email.data = current_user.email
+ image_file = url_for('static', filename='profile_pics/'+current_user.image_file)
+ return render_template('account.html', title='Account',
+ image_file=image_file, form=form)
+
+
+@app.route("/post/new", methods=['GET', 'POST'])
+@login_required
+def new_post() :
+ form = PostForm()
+ if form.validate_on_submit():
+ # Construct Post object which defined in models.py
+ post = Post(title=form.title.data, content=form.content.data, author=current_user)
+ db.session.add(post)
+ db.session.commit()
+ flash('Your post has been created!', 'success')
+ return redirect(url_for('home'))
+ return render_template('create_post.html', title='New Post',
+ form=form, legend='New Post')
+
+
+@app.route("/post/")
+def post(post_id) :
+ # find the post or give page not found 404 error
+ post = Post.query.get_or_404(post_id)
+ return render_template('post.html', title=post.title, post=post)
+
+@app.route("/post//update", methods=['GET', 'POST'])
+@login_required
+def update_post(post_id) :
+ # find the post or give page not found 404 error
+ post = Post.query.get_or_404(post_id)
+ if post.author != current_user:
+ abort(403)
+ form = PostForm()
+ if form.validate_on_submit():
+ post.title = form.title.data
+ post.content = form.content.data
+ db.session.commit()
+ flash('Your post has been update!', 'success')
+ return redirect(url_for('post', post_id=post.id))
+ elif request.method == 'GET':
+ form.title.data = post.title
+ form.content.data = post.content
+ return render_template('create_post.html', title='Update Post',
+ form=form, legend='Update Post')
+
+
+# "Delete" button in Modal sends POST request to delete the post
+@app.route("/post//delete", methods=['POST'])
+@login_required
+def delete_post(post_id) :
+ post = Post.query.get_or_404(post_id)
+ if post.author != current_user:
+ abort(403)
+ db.session.delete(post)
+ db.session.commit()
+ flash('Your post has been deleted!', 'success')
+ return redirect(url_for('home'))
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/site.db b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/site.db
new file mode 100644
index 000000000..aece297f5
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/site.db differ
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/static/main.css b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/static/main.css
new file mode 100644
index 000000000..730e2bca6
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/static/main.css
@@ -0,0 +1,80 @@
+body {
+ background: #fafafa;
+ color: #333333;
+ margin-top: 5rem;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: #444444;
+}
+
+.bg-steel {
+ background-color: #5f788a;
+}
+
+.site-header .navbar-nav .nav-link {
+ color: #cbd5db;
+}
+
+.site-header .navbar-nav .nav-link:hover {
+ color: #ffffff;
+}
+
+.site-header .navbar-nav .nav-link:active {
+ font-weight: 500;
+}
+
+.content-section {
+ background: #ffffff;
+ padding: 10px 20px;
+ border: 1px solid #dddddd;
+ border-radius: 3px;
+ margin-bottom: 20px;
+}
+
+.article-title {
+ color: #444444;
+}
+
+a.article-title:hover {
+ color: #428bca;
+ text-decoration: none;
+}
+
+.article-content {
+ white-space: pre-line;
+}
+
+.article-img {
+ height: 65px;
+ width: 65px;
+ margin-right: 16px;
+}
+
+.article-metadata {
+ padding-bottom: 1px;
+ margin-bottom: 4px;
+ border-bottom: 1px solid #e3e3e3
+}
+
+.article-metadata a:hover {
+ color: #333;
+ text-decoration: none;
+}
+
+.article-svg {
+ width: 25px;
+ height: 25px;
+ vertical-align: middle;
+}
+
+.account-img {
+ height: 125px;
+ width: 125px;
+ margin-right: 20px;
+ margin-bottom: 16px;
+}
+
+.account-heading {
+ font-size: 2.5rem;
+}
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/static/profile_pics/919b30429a4f5711.jpg b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/static/profile_pics/919b30429a4f5711.jpg
new file mode 100644
index 000000000..2fe68efe2
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/static/profile_pics/919b30429a4f5711.jpg differ
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/static/profile_pics/default.jpg b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/static/profile_pics/default.jpg
new file mode 100644
index 000000000..38f286f63
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/static/profile_pics/default.jpg differ
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/templates/about.html b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/templates/about.html
new file mode 100644
index 000000000..95312bed7
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/templates/about.html
@@ -0,0 +1,4 @@
+{% extends "layout.html" %}
+{% block content %}
+
About page
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/templates/account.html b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/templates/account.html
new file mode 100644
index 000000000..86b047a25
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-1/flaskblog/templates/account.html
@@ -0,0 +1,64 @@
+{% extends "layout.html" %}
+{% block content %}
+
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-1/run.py b/Python/Flask_Blog/00-Practices/I-Pagination-1/run.py
new file mode 100644
index 000000000..0c8390926
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-1/run.py
@@ -0,0 +1,7 @@
+from flaskblog import app
+
+# flaskblog is package
+# import app from flaskblog package
+
+if __name__ == '__main__':
+ app.run(debug=True)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-1/zreadme.txt b/Python/Flask_Blog/00-Practices/I-Pagination-1/zreadme.txt
new file mode 100644
index 000000000..b8e494b05
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-1/zreadme.txt
@@ -0,0 +1,38 @@
+
+1. Add more posts to exercise pagination
+ http://127.0.0.1:5000/
+ - Log in users
+ add more posts for different users
+ we can use 'lorem ipsum'
+
+2. Demo pagination in zreadme_pagination.txt
+
+3. Update home() route in routes.py to query with pagination
+ def home():
+ # 1 tells page default value
+ page = request.args.get('page', 1, type=int)
+ # 5 posts per page
+ posts = Post.query.paginate(page=page, per_page=5)
+
+4. Update home.html by changing posts to posts.items
+ {% for post in posts.items %}
+
+5. test website
+ http://127.0.0.1:5000/ # page has default value of 1
+ http://127.0.0.1:5000/?page=2 # 5 posts per page
+ http://127.0.0.1:5000/?page=3
+
+ http://127.0.0.1:5000/?page=4 # Not Found since we only have total of 14 posts
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-1/zreadme_pagination.txt b/Python/Flask_Blog/00-Practices/I-Pagination-1/zreadme_pagination.txt
new file mode 100644
index 000000000..277c99e83
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-1/zreadme_pagination.txt
@@ -0,0 +1,96 @@
+
+$ cd Flask_Blog\00-Practices\H-Posts-3
+$ python
+
+>>> from flaskblog.models import Post
+>>> posts = Post.query.all()
+
+>>> for post in posts :
+... print(post)
+...
+ Post('Blog 2 Xy by user2','2021-03-28 05:31:26.709109')
+ Post('Blog 1','2021-04-03 17:47:39.443134')
+ ::::::
+
+>>> posts = Post.query.paginate()
+>>> posts
+
+>>> dir(posts)
+ ['__class__', :::, 'has_next', 'has_prev', 'items', 'iter_pages',
+ 'next', 'next_num', 'page', 'pages', 'per_page', 'prev', 'prev_num',
+ 'query', 'total']
+
+>>> posts.per_page
+ 20 # 20 post per page
+
+>>> posts.page
+1 # the current page
+
+>>> posts.total # total 14 posts
+14
+
+>>> for post in posts.items:
+... print(post)
+...
+ Post('Blog 2 Xy by user2','2021-03-28 05:31:26.709109')
+ Post('Blog 1','2021-04-03 17:47:39.443134')
+ Post('Why do we use it?','2021-04-03 17:59:37.163519')
+ ::::::
+
+########################################################
+# get 5 posts in the first page
+########################################################
+>>> posts = Post.query.paginate(per_page=5) # set 5 posts per page
+>>> for post in posts.items:
+... print(post)
+...
+Post('Blog 2 Xy by user2','2021-03-28 05:31:26.709109')
+Post('Blog 1','2021-04-03 17:47:39.443134')
+Post('Why do we use it?','2021-04-03 17:59:37.163519')
+Post('web page editors ','2021-04-03 18:00:12.111351')
+Post('What is Lorem Ipsum?','2021-04-03 18:01:24.645851')
+
+########################################################
+# get 5 posts in the second page
+########################################################
+>>> posts = Post.query.paginate(per_page=5, page=2)
+>>> for post in posts.items:
+... print(post)
+...
+Post('the release of Letraset sheets','2021-04-03 18:01:57.451815')
+Post('using Lorem Ipsum','2021-04-03 18:02:38.718257')
+Post('Where can I get some?','2021-04-03 18:03:25.709345')
+Post('a Latin professor','2021-04-03 18:04:03.121023')
+Post('consectetur, adipisci velit','2021-04-03 18:05:01.839378')
+
+########################################################
+# get posts in the first page, default 20 posts per page
+########################################################
+>>> posts = Post.query.paginate(page=1)
+>>> for post in posts.items:
+... print(post)
+...
+Post('Blog 2 Xy by user2','2021-03-28 05:31:26.709109')
+Post('Blog 1','2021-04-03 17:47:39.443134')
+Post('Why do we use it?','2021-04-03 17:59:37.163519')
+:::::::
+
+########################################################
+# iterate pages
+########################################################
+ # There are total of 14 posts.
+ # One post per page, so we see 14 pages
+ # When there are many pages, we only care about the current page.
+ # The skipped page numbers are represented as 'None'
+>>> posts = Post.query.paginate(per_page=1)
+>>> for page in posts.iter_pages():
+... print(page)
+...
+ 1
+ 2
+ 3
+ 4
+ 5
+ None # pages are skipped
+ 13
+ 14
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/__init__.py b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/__init__.py
new file mode 100644
index 000000000..a1bc035a4
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/__init__.py
@@ -0,0 +1,23 @@
+from flask import Flask
+from flask_sqlalchemy import SQLAlchemy
+from flask_bcrypt import Bcrypt
+from flask_login import LoginManager
+
+app = Flask(__name__)
+
+# $ python
+# >>> import secrets
+# >>> secrets.token_hex(16)
+app.config['SECRET_KEY'] = 'ada8419b155676bc78c2296aba9c7c7d'
+# /// represents the relative directory from the current file (flaskblog.py).
+# that is, site.db has the same directory as this __init__.py
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
+
+db = SQLAlchemy(app)
+bcrypt = Bcrypt(app) # Used to hash the password
+login_manager = LoginManager(app)
+login_manager.login_view = 'login'
+login_manager.login_message_category = 'info'
+
+# import routes from flaskblog package
+from flaskblog import routes
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/forms.py b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/forms.py
new file mode 100644
index 000000000..1bf0250f5
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/forms.py
@@ -0,0 +1,77 @@
+from flask_wtf import FlaskForm
+from flask_wtf.file import FileField, FileAllowed
+from flask_login import current_user
+from wtforms import StringField, PasswordField, SubmitField, BooleanField, TextAreaField
+from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
+from flaskblog.models import User
+
+# $ pip3 install flask_wtf
+# $ pip3 install email_validator
+
+# This module is imported in flaskblog.py to create forms
+
+class RegistrationForm (FlaskForm) :
+ # 'Username' in html pass objects in validators
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ confirm_password = PasswordField('Confirm Password',
+ validators=[DataRequired(), EqualTo('password')])
+ submit = SubmitField('Sign up')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ def validate_username(self, username) :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ def validate_email(self, email) :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class LoginForm (FlaskForm) :
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ # This (remember) allows users to stay login for certain time
+ # after web browser closes by using security cookies.
+ remember = BooleanField('Remember me')
+ submit = SubmitField('Login')
+
+
+# Copy RegistrationForm to modify
+class UpdateAccountForm (FlaskForm) :
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ picture = FileField('Update profile picture', validators=[FileAllowed(['jpg','png'])])
+ submit = SubmitField('Update')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ # validate only when username is not current_user's username
+ def validate_username(self, username) :
+ if username.data != current_user.username :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ # validate only when email is not current_user's email
+ def validate_email(self, email) :
+ if email.data != current_user.email :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class PostForm(FlaskForm) :
+ title = StringField('Title', validators=[DataRequired()])
+ content = TextAreaField('Content', validators=[DataRequired()])
+ submit = SubmitField('Post')
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/models.py b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/models.py
new file mode 100644
index 000000000..1e470f4db
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/models.py
@@ -0,0 +1,50 @@
+from datetime import datetime
+from flaskblog import db, login_manager
+from flask_login import UserMixin
+
+### !!! Generate database tables structure first
+### !!! by following zreadme_package.txt
+
+@login_manager.user_loader
+def load_user(user_id) :
+ return User.query.get(int(user_id))
+
+
+# defines User table structure in database
+class User(db.Model, UserMixin) :
+ # primary_key specifies unique id
+ id = db.Column(db.Integer, primary_key=True)
+ # max 20 characters, unique, and required
+ username = db.Column(db.String(20), unique=True, nullable=False)
+ # max 120 characters, unique, and required
+ email = db.Column(db.String(120), unique=True, nullable=False)
+ # profile image
+ image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
+ # password (be hashed)
+ password = db.Column(db.String(60), nullable=False)
+ # --posts is not a column, but a relationship running a query
+ # in background to collect all the posts by this user
+ # --lazy=true, SQLALCHEMY loads the data as necessary in one go
+ # --backref is a simple way to declare a new property on the Post class.
+ # You can then use post.author to get to the user who writes the post.
+ posts = db.relationship('Post', backref='author', lazy=True)
+
+ # function to print this class (User)
+ def __repr__(self) :
+ return f"User('{self.username}','{self.email}','{self.image_file}')"
+
+# defines Post table structure in database
+class Post(db.Model) :
+ id = db.Column(db.Integer, primary_key=True)
+ title = db.Column(db.String(100), nullable=False)
+ # type of DateTime, the current time as default (utcnow)
+ # The default is passed as the function name of utcnow. Do not use
+ # utcnow(), which can invoke the function right away.
+ date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+ content = db.Column(db.Text, nullable=False)
+ # Use a foreign key that references the primary key of User table
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
+
+ # function to print this class (Post)
+ def __repr__(self) :
+ return f"Post('{self.title}','{self.date_posted}')"
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/routes.py b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/routes.py
new file mode 100644
index 000000000..16314237f
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/routes.py
@@ -0,0 +1,166 @@
+from flask import render_template, url_for, flash, redirect, request, abort
+from flaskblog import app, db, bcrypt
+from flaskblog.forms import RegistrationForm, LoginForm, UpdateAccountForm, PostForm
+from flaskblog.models import User, Post
+from flask_login import login_user, current_user, logout_user, login_required
+import os, secrets
+from PIL import Image
+
+
+# http://localhost:5000/
+@app.route("/")
+@app.route("/home")
+def home():
+ page = request.args.get('page', 1, type=int)
+ posts = Post.query.paginate(page=page, per_page=2)
+ return render_template('home.html', posts=posts)
+
+# http://localhost:5000/about
+@app.route("/about")
+def about():
+ return render_template('about.html', title='About')
+
+@app.route("/register", methods=['GET', 'POST'])
+def register():
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = RegistrationForm()
+ # once submit successfully, flash message shows in the placehold in layout.html
+ # flash message is one time only, it disappear after refresh web browser
+ if form.validate_on_submit():
+ hashed_pwd = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
+ user = User(username=form.username.data, email=form.email.data, password=hashed_pwd)
+ db.session.add(user)
+ db.session.commit()
+ flash('Your account has been created! You are now able to log in!', 'success')
+ return redirect(url_for('login'))
+ return render_template('register.html', title='Register', form=form)
+
+@app.route("/login", methods=['GET', 'POST'])
+def login():
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = LoginForm()
+ if form.validate_on_submit():
+ user = User.query.filter_by(email=form.email.data).first()
+ if user and bcrypt.check_password_hash(user.password, form.password.data) :
+ login_user(user, remember=form.remember.data)
+ # request.args is dictionary type
+ next_page = request.args.get('next')
+ # 'home' is function of home() above
+ return redirect(next_page) if next_page else redirect(url_for('home'))
+ else:
+ flash('Login failed. Please check email and password.', 'danger')
+ return render_template('login.html', title='Login', form=form)
+
+
+@app.route("/logout")
+def logout() :
+ logout_user()
+ return redirect(url_for('home'))
+
+# save uploaded image to the specified path
+def save_picture(form_picture) :
+ random_hex = secrets.token_hex(8)
+ # underscore (_) ignoring the value
+ _, f_ext = os.path.splitext(form_picture.filename)
+ picture_fn = random_hex + f_ext
+ # define the path to store profile image
+ picture_path = os.path.join(app.root_path, 'static/profile_pics', picture_fn)
+
+ # save the uploaded image into the specified path
+
+ # do not want to save the original uploaded image, which can be very large
+ # form_picture.save(picture_path)
+
+ #resize the image before store
+ output_size = (125, 125)
+ i = Image.open(form_picture)
+ i.thumbnail(output_size)
+ i.save(picture_path)
+
+ return picture_fn
+
+
+# login_required decorator makes sure to access the page only
+# when the user log in
+@app.route("/account", methods=['GET', 'POST'])
+@login_required
+def account() :
+ form = UpdateAccountForm()
+ if form.validate_on_submit() :
+ if form.picture.data :
+ picture_file = save_picture(form.picture.data)
+ current_user.image_file = picture_file
+ # think of current_user is reference to the user in database
+ current_user.username = form.username.data
+ current_user.email = form.email.data
+ db.session.commit()
+ flash('Your account has been updated!', 'success')
+ # This is must. After POST request, do the GET request
+ # to avoid POST request again
+ return redirect(url_for('account'))
+ elif request.method == 'GET' :
+ # populate the username and email so that
+ # the new username and email can be viewed right after
+ # update form is submitted
+ form.username.data = current_user.username
+ form.email.data = current_user.email
+ image_file = url_for('static', filename='profile_pics/'+current_user.image_file)
+ return render_template('account.html', title='Account',
+ image_file=image_file, form=form)
+
+
+@app.route("/post/new", methods=['GET', 'POST'])
+@login_required
+def new_post() :
+ form = PostForm()
+ if form.validate_on_submit():
+ # Construct Post object which defined in models.py
+ post = Post(title=form.title.data, content=form.content.data, author=current_user)
+ db.session.add(post)
+ db.session.commit()
+ flash('Your post has been created!', 'success')
+ return redirect(url_for('home'))
+ return render_template('create_post.html', title='New Post',
+ form=form, legend='New Post')
+
+
+@app.route("/post/")
+def post(post_id) :
+ # find the post or give page not found 404 error
+ post = Post.query.get_or_404(post_id)
+ return render_template('post.html', title=post.title, post=post)
+
+@app.route("/post//update", methods=['GET', 'POST'])
+@login_required
+def update_post(post_id) :
+ # find the post or give page not found 404 error
+ post = Post.query.get_or_404(post_id)
+ if post.author != current_user:
+ abort(403)
+ form = PostForm()
+ if form.validate_on_submit():
+ post.title = form.title.data
+ post.content = form.content.data
+ db.session.commit()
+ flash('Your post has been update!', 'success')
+ return redirect(url_for('post', post_id=post.id))
+ elif request.method == 'GET':
+ form.title.data = post.title
+ form.content.data = post.content
+ return render_template('create_post.html', title='Update Post',
+ form=form, legend='Update Post')
+
+
+# "Delete" button in Modal sends POST request to delete the post
+@app.route("/post//delete", methods=['POST'])
+@login_required
+def delete_post(post_id) :
+ post = Post.query.get_or_404(post_id)
+ if post.author != current_user:
+ abort(403)
+ db.session.delete(post)
+ db.session.commit()
+ flash('Your post has been deleted!', 'success')
+ return redirect(url_for('home'))
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/site.db b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/site.db
new file mode 100644
index 000000000..aece297f5
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/site.db differ
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/static/main.css b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/static/main.css
new file mode 100644
index 000000000..730e2bca6
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/static/main.css
@@ -0,0 +1,80 @@
+body {
+ background: #fafafa;
+ color: #333333;
+ margin-top: 5rem;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: #444444;
+}
+
+.bg-steel {
+ background-color: #5f788a;
+}
+
+.site-header .navbar-nav .nav-link {
+ color: #cbd5db;
+}
+
+.site-header .navbar-nav .nav-link:hover {
+ color: #ffffff;
+}
+
+.site-header .navbar-nav .nav-link:active {
+ font-weight: 500;
+}
+
+.content-section {
+ background: #ffffff;
+ padding: 10px 20px;
+ border: 1px solid #dddddd;
+ border-radius: 3px;
+ margin-bottom: 20px;
+}
+
+.article-title {
+ color: #444444;
+}
+
+a.article-title:hover {
+ color: #428bca;
+ text-decoration: none;
+}
+
+.article-content {
+ white-space: pre-line;
+}
+
+.article-img {
+ height: 65px;
+ width: 65px;
+ margin-right: 16px;
+}
+
+.article-metadata {
+ padding-bottom: 1px;
+ margin-bottom: 4px;
+ border-bottom: 1px solid #e3e3e3
+}
+
+.article-metadata a:hover {
+ color: #333;
+ text-decoration: none;
+}
+
+.article-svg {
+ width: 25px;
+ height: 25px;
+ vertical-align: middle;
+}
+
+.account-img {
+ height: 125px;
+ width: 125px;
+ margin-right: 20px;
+ margin-bottom: 16px;
+}
+
+.account-heading {
+ font-size: 2.5rem;
+}
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/static/profile_pics/919b30429a4f5711.jpg b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/static/profile_pics/919b30429a4f5711.jpg
new file mode 100644
index 000000000..2fe68efe2
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/static/profile_pics/919b30429a4f5711.jpg differ
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/static/profile_pics/default.jpg b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/static/profile_pics/default.jpg
new file mode 100644
index 000000000..38f286f63
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/static/profile_pics/default.jpg differ
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/templates/about.html b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/templates/about.html
new file mode 100644
index 000000000..95312bed7
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/templates/about.html
@@ -0,0 +1,4 @@
+{% extends "layout.html" %}
+{% block content %}
+
About page
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/templates/account.html b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/templates/account.html
new file mode 100644
index 000000000..86b047a25
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-2/flaskblog/templates/account.html
@@ -0,0 +1,64 @@
+{% extends "layout.html" %}
+{% block content %}
+
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-2/run.py b/Python/Flask_Blog/00-Practices/I-Pagination-2/run.py
new file mode 100644
index 000000000..0c8390926
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-2/run.py
@@ -0,0 +1,7 @@
+from flaskblog import app
+
+# flaskblog is package
+# import app from flaskblog package
+
+if __name__ == '__main__':
+ app.run(debug=True)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-2/zreadme.txt b/Python/Flask_Blog/00-Practices/I-Pagination-2/zreadme.txt
new file mode 100644
index 000000000..d3c50488c
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-2/zreadme.txt
@@ -0,0 +1,61 @@
+
+1. Update home() route in routes.py to set 1 post per page
+ then we have more pages to show page iterate
+
+ def home():
+ # 1 tells page default value
+ page = request.args.get('page', 1, type=int)
+ # 1 posts per page
+ posts = Post.query.paginate(page=page, per_page=1)
+
+2. Update home.html to view page numbers
+
+ {% for page_num in posts.iter_pages() %}
+ {% if page_num %}
+ {{ page_num }}
+ {% else %}
+ ...
+ {% endif %}
+ {% endfor %}
+
+3. test website
+ http://127.0.0.1:5000/
+ change per_page in home() route to view the page numbers
+
+4. Set parameters in posts.iter_pages() in home.html template
+ to define/tune page numbers view
+
+ # left_edge: number of pages on the far left side
+ # right_edge: number of pages on the far right side
+ # left_current: number of pages on the left of the current page (not include the current page)
+ # right_current: number of pages on the right of the current page (include the current page)
+
+ {% for page_num in posts.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}
+
+5. test website
+ http://127.0.0.1:5000/
+ click the page numbers link
+
+6. Style the current page number different from the other page numbers
+ by updating home.html
+
+ {% if posts.page==page_num %}
+ {{ page_num }}
+ {% else %}
+ {{ page_num }}
+ {% endif %}
+
+7. test website
+ http://127.0.0.1:5000/
+ Now we see the current page number filled with blue
+
+8. Change per_page to 2 for now
+ def home():
+ page = request.args.get('page', 1, type=int)
+ posts = Post.query.paginate(page=page, per_page=2)
+ return render_template('home.html', posts=posts)
+
+9. test website
+ http://127.0.0.1:5000/
+
+
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-2/zreadme_pagination.txt b/Python/Flask_Blog/00-Practices/I-Pagination-2/zreadme_pagination.txt
new file mode 100644
index 000000000..c55e6fb10
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-2/zreadme_pagination.txt
@@ -0,0 +1,96 @@
+
+$ cd Flask_Blog\00-Practices\H-Posts-3
+$ python
+
+>>> from flaskblog.models import Post
+>>> posts = Post.query.all()
+
+>>> for post in posts :
+... print(post)
+...
+ Post('Blog 2 Xy by user2','2021-03-28 05:31:26.709109')
+ Post('Blog 1','2021-04-03 17:47:39.443134')
+ ::::::
+
+>>> posts = Post.query.paginate()
+>>> posts
+
+>>> dir(posts)
+ ['__class__', :::, 'has_next', 'has_prev', 'items', 'iter_pages',
+ 'next', 'next_num', 'page', 'pages', 'per_page', 'prev', 'prev_num',
+ 'query', 'total']
+
+>>> posts.per_page
+ 20 # 20 post per page
+
+>>> posts.page
+1 # the current page
+
+>>> posts.total # total 14 posts
+14
+
+>>> for post in posts.items:
+... print(post)
+...
+ Post('Blog 2 Xy by user2','2021-03-28 05:31:26.709109')
+ Post('Blog 1','2021-04-03 17:47:39.443134')
+ Post('Why do we use it?','2021-04-03 17:59:37.163519')
+ ::::::
+
+########################################################
+# get 5 posts in the first page
+########################################################
+>>> posts = Post.query.paginate(per_page=5) # set 5 posts per page
+>>> for post in posts.items:
+... print(post)
+...
+Post('Blog 2 Xy by user2','2021-03-28 05:31:26.709109')
+Post('Blog 1','2021-04-03 17:47:39.443134')
+Post('Why do we use it?','2021-04-03 17:59:37.163519')
+Post('web page editors ','2021-04-03 18:00:12.111351')
+Post('What is Lorem Ipsum?','2021-04-03 18:01:24.645851')
+
+########################################################
+# get 5 posts in the second page
+########################################################
+>>> posts = Post.query.paginate(per_page=5, page=2)
+>>> for post in posts.items:
+... print(post)
+...
+Post('the release of Letraset sheets','2021-04-03 18:01:57.451815')
+Post('using Lorem Ipsum','2021-04-03 18:02:38.718257')
+Post('Where can I get some?','2021-04-03 18:03:25.709345')
+Post('a Latin professor','2021-04-03 18:04:03.121023')
+Post('consectetur, adipisci velit','2021-04-03 18:05:01.839378')
+
+########################################################
+# get posts in the first page, default 20 posts per page
+########################################################
+>>> posts = Post.query.paginate(page=1)
+>>> for post in posts.items:
+... print(post)
+...
+Post('Blog 2 Xy by user2','2021-03-28 05:31:26.709109')
+Post('Blog 1','2021-04-03 17:47:39.443134')
+Post('Why do we use it?','2021-04-03 17:59:37.163519')
+:::::::
+
+########################################################
+# iterate pages
+########################################################
+ # There are total of 14 posts.
+ # One post per page, so we see 14 pages
+ # When there are many pages, we only care about the current page.
+ # the other pages can be skipped ('None' can be used for this purpose).
+>>> posts = Post.query.paginate(per_page=1)
+>>> for page in posts.iter_pages():
+... print(page)
+...
+ 1
+ 2
+ 3
+ 4
+ 5
+ None #the other pages are skipped
+ 13
+ 14
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/__init__.py b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/__init__.py
new file mode 100644
index 000000000..a1bc035a4
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/__init__.py
@@ -0,0 +1,23 @@
+from flask import Flask
+from flask_sqlalchemy import SQLAlchemy
+from flask_bcrypt import Bcrypt
+from flask_login import LoginManager
+
+app = Flask(__name__)
+
+# $ python
+# >>> import secrets
+# >>> secrets.token_hex(16)
+app.config['SECRET_KEY'] = 'ada8419b155676bc78c2296aba9c7c7d'
+# /// represents the relative directory from the current file (flaskblog.py).
+# that is, site.db has the same directory as this __init__.py
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
+
+db = SQLAlchemy(app)
+bcrypt = Bcrypt(app) # Used to hash the password
+login_manager = LoginManager(app)
+login_manager.login_view = 'login'
+login_manager.login_message_category = 'info'
+
+# import routes from flaskblog package
+from flaskblog import routes
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/forms.py b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/forms.py
new file mode 100644
index 000000000..1bf0250f5
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/forms.py
@@ -0,0 +1,77 @@
+from flask_wtf import FlaskForm
+from flask_wtf.file import FileField, FileAllowed
+from flask_login import current_user
+from wtforms import StringField, PasswordField, SubmitField, BooleanField, TextAreaField
+from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
+from flaskblog.models import User
+
+# $ pip3 install flask_wtf
+# $ pip3 install email_validator
+
+# This module is imported in flaskblog.py to create forms
+
+class RegistrationForm (FlaskForm) :
+ # 'Username' in html pass objects in validators
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ confirm_password = PasswordField('Confirm Password',
+ validators=[DataRequired(), EqualTo('password')])
+ submit = SubmitField('Sign up')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ def validate_username(self, username) :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ def validate_email(self, email) :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class LoginForm (FlaskForm) :
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ # This (remember) allows users to stay login for certain time
+ # after web browser closes by using security cookies.
+ remember = BooleanField('Remember me')
+ submit = SubmitField('Login')
+
+
+# Copy RegistrationForm to modify
+class UpdateAccountForm (FlaskForm) :
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ picture = FileField('Update profile picture', validators=[FileAllowed(['jpg','png'])])
+ submit = SubmitField('Update')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ # validate only when username is not current_user's username
+ def validate_username(self, username) :
+ if username.data != current_user.username :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ # validate only when email is not current_user's email
+ def validate_email(self, email) :
+ if email.data != current_user.email :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class PostForm(FlaskForm) :
+ title = StringField('Title', validators=[DataRequired()])
+ content = TextAreaField('Content', validators=[DataRequired()])
+ submit = SubmitField('Post')
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/models.py b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/models.py
new file mode 100644
index 000000000..1e470f4db
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/models.py
@@ -0,0 +1,50 @@
+from datetime import datetime
+from flaskblog import db, login_manager
+from flask_login import UserMixin
+
+### !!! Generate database tables structure first
+### !!! by following zreadme_package.txt
+
+@login_manager.user_loader
+def load_user(user_id) :
+ return User.query.get(int(user_id))
+
+
+# defines User table structure in database
+class User(db.Model, UserMixin) :
+ # primary_key specifies unique id
+ id = db.Column(db.Integer, primary_key=True)
+ # max 20 characters, unique, and required
+ username = db.Column(db.String(20), unique=True, nullable=False)
+ # max 120 characters, unique, and required
+ email = db.Column(db.String(120), unique=True, nullable=False)
+ # profile image
+ image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
+ # password (be hashed)
+ password = db.Column(db.String(60), nullable=False)
+ # --posts is not a column, but a relationship running a query
+ # in background to collect all the posts by this user
+ # --lazy=true, SQLALCHEMY loads the data as necessary in one go
+ # --backref is a simple way to declare a new property on the Post class.
+ # You can then use post.author to get to the user who writes the post.
+ posts = db.relationship('Post', backref='author', lazy=True)
+
+ # function to print this class (User)
+ def __repr__(self) :
+ return f"User('{self.username}','{self.email}','{self.image_file}')"
+
+# defines Post table structure in database
+class Post(db.Model) :
+ id = db.Column(db.Integer, primary_key=True)
+ title = db.Column(db.String(100), nullable=False)
+ # type of DateTime, the current time as default (utcnow)
+ # The default is passed as the function name of utcnow. Do not use
+ # utcnow(), which can invoke the function right away.
+ date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+ content = db.Column(db.Text, nullable=False)
+ # Use a foreign key that references the primary key of User table
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
+
+ # function to print this class (Post)
+ def __repr__(self) :
+ return f"Post('{self.title}','{self.date_posted}')"
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/routes.py b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/routes.py
new file mode 100644
index 000000000..7a7783da0
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/routes.py
@@ -0,0 +1,176 @@
+from flask import render_template, url_for, flash, redirect, request, abort
+from flaskblog import app, db, bcrypt
+from flaskblog.forms import RegistrationForm, LoginForm, UpdateAccountForm, PostForm
+from flaskblog.models import User, Post
+from flask_login import login_user, current_user, logout_user, login_required
+import os, secrets
+from PIL import Image
+
+
+# http://localhost:5000/
+@app.route("/")
+@app.route("/home")
+def home():
+ page = request.args.get('page', 1, type=int)
+ posts = Post.query.order_by(Post.date_posted.desc()).paginate(page=page, per_page=2)
+ return render_template('home.html', posts=posts)
+
+# http://localhost:5000/about
+@app.route("/about")
+def about():
+ return render_template('about.html', title='About')
+
+@app.route("/register", methods=['GET', 'POST'])
+def register():
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = RegistrationForm()
+ # once submit successfully, flash message shows in the placehold in layout.html
+ # flash message is one time only, it disappear after refresh web browser
+ if form.validate_on_submit():
+ hashed_pwd = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
+ user = User(username=form.username.data, email=form.email.data, password=hashed_pwd)
+ db.session.add(user)
+ db.session.commit()
+ flash('Your account has been created! You are now able to log in!', 'success')
+ return redirect(url_for('login'))
+ return render_template('register.html', title='Register', form=form)
+
+@app.route("/login", methods=['GET', 'POST'])
+def login():
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = LoginForm()
+ if form.validate_on_submit():
+ user = User.query.filter_by(email=form.email.data).first()
+ if user and bcrypt.check_password_hash(user.password, form.password.data) :
+ login_user(user, remember=form.remember.data)
+ # request.args is dictionary type
+ next_page = request.args.get('next')
+ # 'home' is function of home() above
+ return redirect(next_page) if next_page else redirect(url_for('home'))
+ else:
+ flash('Login failed. Please check email and password.', 'danger')
+ return render_template('login.html', title='Login', form=form)
+
+
+@app.route("/logout")
+def logout() :
+ logout_user()
+ return redirect(url_for('home'))
+
+# save uploaded image to the specified path
+def save_picture(form_picture) :
+ random_hex = secrets.token_hex(8)
+ # underscore (_) ignoring the value
+ _, f_ext = os.path.splitext(form_picture.filename)
+ picture_fn = random_hex + f_ext
+ # define the path to store profile image
+ picture_path = os.path.join(app.root_path, 'static/profile_pics', picture_fn)
+
+ # save the uploaded image into the specified path
+
+ # do not want to save the original uploaded image, which can be very large
+ # form_picture.save(picture_path)
+
+ #resize the image before store
+ output_size = (125, 125)
+ i = Image.open(form_picture)
+ i.thumbnail(output_size)
+ i.save(picture_path)
+
+ return picture_fn
+
+
+# login_required decorator makes sure to access the page only
+# when the user log in
+@app.route("/account", methods=['GET', 'POST'])
+@login_required
+def account() :
+ form = UpdateAccountForm()
+ if form.validate_on_submit() :
+ if form.picture.data :
+ picture_file = save_picture(form.picture.data)
+ current_user.image_file = picture_file
+ # think of current_user is reference to the user in database
+ current_user.username = form.username.data
+ current_user.email = form.email.data
+ db.session.commit()
+ flash('Your account has been updated!', 'success')
+ # This is must. After POST request, do the GET request
+ # to avoid POST request again
+ return redirect(url_for('account'))
+ elif request.method == 'GET' :
+ # populate the username and email so that
+ # the new username and email can be viewed right after
+ # update form is submitted
+ form.username.data = current_user.username
+ form.email.data = current_user.email
+ image_file = url_for('static', filename='profile_pics/'+current_user.image_file)
+ return render_template('account.html', title='Account',
+ image_file=image_file, form=form)
+
+
+@app.route("/post/new", methods=['GET', 'POST'])
+@login_required
+def new_post() :
+ form = PostForm()
+ if form.validate_on_submit():
+ # Construct Post object which defined in models.py
+ post = Post(title=form.title.data, content=form.content.data, author=current_user)
+ db.session.add(post)
+ db.session.commit()
+ flash('Your post has been created!', 'success')
+ return redirect(url_for('home'))
+ return render_template('create_post.html', title='New Post',
+ form=form, legend='New Post')
+
+
+@app.route("/post/")
+def post(post_id) :
+ # find the post or give page not found 404 error
+ post = Post.query.get_or_404(post_id)
+ return render_template('post.html', title=post.title, post=post)
+
+@app.route("/post//update", methods=['GET', 'POST'])
+@login_required
+def update_post(post_id) :
+ # find the post or give page not found 404 error
+ post = Post.query.get_or_404(post_id)
+ if post.author != current_user:
+ abort(403)
+ form = PostForm()
+ if form.validate_on_submit():
+ post.title = form.title.data
+ post.content = form.content.data
+ db.session.commit()
+ flash('Your post has been update!', 'success')
+ return redirect(url_for('post', post_id=post.id))
+ elif request.method == 'GET':
+ form.title.data = post.title
+ form.content.data = post.content
+ return render_template('create_post.html', title='Update Post',
+ form=form, legend='Update Post')
+
+
+# "Delete" button in Modal sends POST request to delete the post
+@app.route("/post//delete", methods=['POST'])
+@login_required
+def delete_post(post_id) :
+ post = Post.query.get_or_404(post_id)
+ if post.author != current_user:
+ abort(403)
+ db.session.delete(post)
+ db.session.commit()
+ flash('Your post has been deleted!', 'success')
+ return redirect(url_for('home'))
+
+
+@app.route("/user/")
+def user_posts(username):
+ page = request.args.get('page', 1, type=int)
+ user = User.query.filter_by(username=username).first_or_404()
+ posts = Post.query.filter_by(author=user)\
+ .order_by(Post.date_posted.desc())\
+ .paginate(page=page, per_page=2)
+ return render_template('user_posts.html', posts=posts, user=user)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/site.db b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/site.db
new file mode 100644
index 000000000..aece297f5
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/site.db differ
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/static/main.css b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/static/main.css
new file mode 100644
index 000000000..730e2bca6
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/static/main.css
@@ -0,0 +1,80 @@
+body {
+ background: #fafafa;
+ color: #333333;
+ margin-top: 5rem;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: #444444;
+}
+
+.bg-steel {
+ background-color: #5f788a;
+}
+
+.site-header .navbar-nav .nav-link {
+ color: #cbd5db;
+}
+
+.site-header .navbar-nav .nav-link:hover {
+ color: #ffffff;
+}
+
+.site-header .navbar-nav .nav-link:active {
+ font-weight: 500;
+}
+
+.content-section {
+ background: #ffffff;
+ padding: 10px 20px;
+ border: 1px solid #dddddd;
+ border-radius: 3px;
+ margin-bottom: 20px;
+}
+
+.article-title {
+ color: #444444;
+}
+
+a.article-title:hover {
+ color: #428bca;
+ text-decoration: none;
+}
+
+.article-content {
+ white-space: pre-line;
+}
+
+.article-img {
+ height: 65px;
+ width: 65px;
+ margin-right: 16px;
+}
+
+.article-metadata {
+ padding-bottom: 1px;
+ margin-bottom: 4px;
+ border-bottom: 1px solid #e3e3e3
+}
+
+.article-metadata a:hover {
+ color: #333;
+ text-decoration: none;
+}
+
+.article-svg {
+ width: 25px;
+ height: 25px;
+ vertical-align: middle;
+}
+
+.account-img {
+ height: 125px;
+ width: 125px;
+ margin-right: 20px;
+ margin-bottom: 16px;
+}
+
+.account-heading {
+ font-size: 2.5rem;
+}
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/static/profile_pics/919b30429a4f5711.jpg b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/static/profile_pics/919b30429a4f5711.jpg
new file mode 100644
index 000000000..2fe68efe2
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/static/profile_pics/919b30429a4f5711.jpg differ
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/static/profile_pics/default.jpg b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/static/profile_pics/default.jpg
new file mode 100644
index 000000000..38f286f63
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/static/profile_pics/default.jpg differ
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/templates/about.html b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/templates/about.html
new file mode 100644
index 000000000..95312bed7
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/templates/about.html
@@ -0,0 +1,4 @@
+{% extends "layout.html" %}
+{% block content %}
+
About page
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/templates/account.html b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/templates/account.html
new file mode 100644
index 000000000..86b047a25
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-3/flaskblog/templates/account.html
@@ -0,0 +1,64 @@
+{% extends "layout.html" %}
+{% block content %}
+
+ {{ post.author.username }}
+
+
+6. test website
+ http://127.0.0.1:5000/
+ Click username link to show the posts by this user only
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/I-Pagination-3/zreadme_pagination.txt b/Python/Flask_Blog/00-Practices/I-Pagination-3/zreadme_pagination.txt
new file mode 100644
index 000000000..c55e6fb10
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/I-Pagination-3/zreadme_pagination.txt
@@ -0,0 +1,96 @@
+
+$ cd Flask_Blog\00-Practices\H-Posts-3
+$ python
+
+>>> from flaskblog.models import Post
+>>> posts = Post.query.all()
+
+>>> for post in posts :
+... print(post)
+...
+ Post('Blog 2 Xy by user2','2021-03-28 05:31:26.709109')
+ Post('Blog 1','2021-04-03 17:47:39.443134')
+ ::::::
+
+>>> posts = Post.query.paginate()
+>>> posts
+
+>>> dir(posts)
+ ['__class__', :::, 'has_next', 'has_prev', 'items', 'iter_pages',
+ 'next', 'next_num', 'page', 'pages', 'per_page', 'prev', 'prev_num',
+ 'query', 'total']
+
+>>> posts.per_page
+ 20 # 20 post per page
+
+>>> posts.page
+1 # the current page
+
+>>> posts.total # total 14 posts
+14
+
+>>> for post in posts.items:
+... print(post)
+...
+ Post('Blog 2 Xy by user2','2021-03-28 05:31:26.709109')
+ Post('Blog 1','2021-04-03 17:47:39.443134')
+ Post('Why do we use it?','2021-04-03 17:59:37.163519')
+ ::::::
+
+########################################################
+# get 5 posts in the first page
+########################################################
+>>> posts = Post.query.paginate(per_page=5) # set 5 posts per page
+>>> for post in posts.items:
+... print(post)
+...
+Post('Blog 2 Xy by user2','2021-03-28 05:31:26.709109')
+Post('Blog 1','2021-04-03 17:47:39.443134')
+Post('Why do we use it?','2021-04-03 17:59:37.163519')
+Post('web page editors ','2021-04-03 18:00:12.111351')
+Post('What is Lorem Ipsum?','2021-04-03 18:01:24.645851')
+
+########################################################
+# get 5 posts in the second page
+########################################################
+>>> posts = Post.query.paginate(per_page=5, page=2)
+>>> for post in posts.items:
+... print(post)
+...
+Post('the release of Letraset sheets','2021-04-03 18:01:57.451815')
+Post('using Lorem Ipsum','2021-04-03 18:02:38.718257')
+Post('Where can I get some?','2021-04-03 18:03:25.709345')
+Post('a Latin professor','2021-04-03 18:04:03.121023')
+Post('consectetur, adipisci velit','2021-04-03 18:05:01.839378')
+
+########################################################
+# get posts in the first page, default 20 posts per page
+########################################################
+>>> posts = Post.query.paginate(page=1)
+>>> for post in posts.items:
+... print(post)
+...
+Post('Blog 2 Xy by user2','2021-03-28 05:31:26.709109')
+Post('Blog 1','2021-04-03 17:47:39.443134')
+Post('Why do we use it?','2021-04-03 17:59:37.163519')
+:::::::
+
+########################################################
+# iterate pages
+########################################################
+ # There are total of 14 posts.
+ # One post per page, so we see 14 pages
+ # When there are many pages, we only care about the current page.
+ # the other pages can be skipped ('None' can be used for this purpose).
+>>> posts = Post.query.paginate(per_page=1)
+>>> for page in posts.iter_pages():
+... print(page)
+...
+ 1
+ 2
+ 3
+ 4
+ 5
+ None #the other pages are skipped
+ 13
+ 14
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/__init__.py b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/__init__.py
new file mode 100644
index 000000000..03d4381af
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/__init__.py
@@ -0,0 +1,38 @@
+import os
+from flask import Flask
+from flask_sqlalchemy import SQLAlchemy
+from flask_bcrypt import Bcrypt
+from flask_login import LoginManager
+from flask_mail import Mail
+
+app = Flask(__name__)
+
+# $ python
+# >>> import secrets
+# >>> secrets.token_hex(16)
+app.config['SECRET_KEY'] = 'ada8419b155676bc78c2296aba9c7c7d'
+# /// represents the relative directory from the current file (flaskblog.py).
+# that is, site.db has the same directory as this __init__.py
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
+
+db = SQLAlchemy(app)
+bcrypt = Bcrypt(app) # Used to hash the password
+login_manager = LoginManager(app)
+login_manager.login_view = 'login'
+login_manager.login_message_category = 'info'
+
+# Config gmail server to send instruction how to reset password
+app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
+app.config['MAIL_PORT'] = 587
+app.config['MAIL_USE_TLS'] = True
+# environment variables
+# Need to set these variables on Windows to your gmail user and password
+# ie.
+# > set EMAIL_USER xyz@gmail.com (use 'export' in linux)
+# > set EMAIL_PASS password_to_xyz (use 'export' in linux)
+app.config['MAIL_USERNAME'] = os.environ.get('EMAIL_USER')
+app.config['MAIL_PASSWORD'] = os.environ.get('EMAIL_PASS')
+mail = Mail(app)
+
+# import routes from flaskblog package
+from flaskblog import routes
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/forms.py b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/forms.py
new file mode 100644
index 000000000..5a4750d7e
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/forms.py
@@ -0,0 +1,94 @@
+from flask_wtf import FlaskForm
+from flask_wtf.file import FileField, FileAllowed
+from flask_login import current_user
+from wtforms import StringField, PasswordField, SubmitField, BooleanField, TextAreaField
+from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
+from flaskblog.models import User
+
+# $ pip3 install flask_wtf
+# $ pip3 install email_validator
+
+# This module is imported in flaskblog.py to create forms
+
+class RegistrationForm (FlaskForm) :
+ # 'Username' in html pass objects in validators
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ confirm_password = PasswordField('Confirm Password',
+ validators=[DataRequired(), EqualTo('password')])
+ submit = SubmitField('Sign up')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ def validate_username(self, username) :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ def validate_email(self, email) :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class LoginForm (FlaskForm) :
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ # This (remember) allows users to stay login for certain time
+ # after web browser closes by using security cookies.
+ remember = BooleanField('Remember me')
+ submit = SubmitField('Login')
+
+
+# Copy RegistrationForm to modify
+class UpdateAccountForm (FlaskForm) :
+ username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ picture = FileField('Update profile picture', validators=[FileAllowed(['jpg','png'])])
+ submit = SubmitField('Update')
+
+ # The validate functions Must follow the format in order to get validation
+ # def validate_field(self, field) :
+ # the fields have username, email and etc in above.
+ # The below makes sure no same username and email is registered.
+
+ # validate only when username is not current_user's username
+ def validate_username(self, username) :
+ if username.data != current_user.username :
+ user = User.query.filter_by(username=username.data).first()
+ if user:
+ raise ValidationError('That username is taken. Please choose a different one.')
+
+ # validate only when email is not current_user's email
+ def validate_email(self, email) :
+ if email.data != current_user.email :
+ user = User.query.filter_by(email=email.data).first()
+ if user:
+ raise ValidationError('That email is taken. Please choose a different one.')
+
+
+class PostForm(FlaskForm) :
+ title = StringField('Title', validators=[DataRequired()])
+ content = TextAreaField('Content', validators=[DataRequired()])
+ submit = SubmitField('Post')
+
+
+class RequestResetForm(FlaskForm) :
+ email = StringField('Email', validators=[DataRequired(), Email()])
+ submit = SubmitField('Request Password Reset')
+
+ def validate_email(self, email) :
+ user = User.query.filter_by(email=email.data).first()
+ if user is None:
+ raise ValidationError('There is no account with that email. You must register first.')
+
+
+class ResetPasswordForm(FlaskForm) :
+ password = PasswordField('Password', validators=[DataRequired()])
+ confirm_password = PasswordField('Confirm Password',
+ validators=[DataRequired(), EqualTo('password')])
+ submit = SubmitField('Reset Password')
diff --git a/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/models.py b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/models.py
new file mode 100644
index 000000000..4ca3d4ab1
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/models.py
@@ -0,0 +1,70 @@
+from datetime import datetime
+from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
+from flaskblog import db, login_manager, app
+from flask_login import UserMixin
+
+### !!! Generate database tables structure first
+### !!! by following zreadme_package.txt
+
+@login_manager.user_loader
+def load_user(user_id) :
+ return User.query.get(int(user_id))
+
+
+# defines User table structure in database
+class User(db.Model, UserMixin) :
+ # primary_key specifies unique id
+ id = db.Column(db.Integer, primary_key=True)
+ # max 20 characters, unique, and required
+ username = db.Column(db.String(20), unique=True, nullable=False)
+ # max 120 characters, unique, and required
+ email = db.Column(db.String(120), unique=True, nullable=False)
+ # profile image
+ image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
+ # password (be hashed)
+ password = db.Column(db.String(60), nullable=False)
+ # --posts is not a column, but a relationship running a query
+ # in background to collect all the posts by this user
+ # --lazy=true, SQLALCHEMY loads the data as necessary in one go
+ # --backref is a simple way to declare a new property on the Post class.
+ # You can then use post.author to get to the user who writes the post.
+ posts = db.relationship('Post', backref='author', lazy=True)
+
+ # token expires after 1800 seconds (30 min)
+ # {'user_id': self.id} as payload
+ def get_reset_token(self, expires_sec=1800) :
+ s = Serializer(app.config['SECRET_KEY'], expires_sec)
+ return s.dumps({'user_id': self.id}).decode('utf-8')
+
+ # @staticmethod decorator tells python this member function
+ # does not need self as parameter
+ # s.loads(token) gets payload which is {'user_id': self.id}
+ # So, s.loads(token)['user_id'] returns self.id
+ @staticmethod
+ def verify_reset_token(token) :
+ s = Serializer(app.config['SECRET_KEY'])
+ try:
+ user_id = s.loads(token)['user_id']
+ except:
+ return None
+ return User.query.get(user_id)
+
+ # function to print this class (User)
+ def __repr__(self) :
+ return f"User('{self.username}','{self.email}','{self.image_file}')"
+
+# defines Post table structure in database
+class Post(db.Model) :
+ id = db.Column(db.Integer, primary_key=True)
+ title = db.Column(db.String(100), nullable=False)
+ # type of DateTime, the current time as default (utcnow)
+ # The default is passed as the function name of utcnow. Do not use
+ # utcnow(), which can invoke the function right away.
+ date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+ content = db.Column(db.Text, nullable=False)
+ # Use a foreign key that references the primary key of User table
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
+
+ # function to print this class (Post)
+ def __repr__(self) :
+ return f"Post('{self.title}','{self.date_posted}')"
diff --git a/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/routes.py b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/routes.py
new file mode 100644
index 000000000..416286824
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/routes.py
@@ -0,0 +1,233 @@
+from flask import render_template, url_for, flash, redirect, request, abort
+from flaskblog import app, db, bcrypt, mail
+from flaskblog.forms import (RegistrationForm, LoginForm, UpdateAccountForm,
+ PostForm, RequestResetForm, ResetPasswordForm)
+from flaskblog.models import User, Post
+from flask_login import login_user, current_user, logout_user, login_required
+from flask_mail import Message
+import os, secrets
+from PIL import Image
+
+
+# http://localhost:5000/
+@app.route("/")
+@app.route("/home")
+def home():
+ page = request.args.get('page', 1, type=int)
+ posts = Post.query.order_by(Post.date_posted.desc()).paginate(page=page, per_page=2)
+ return render_template('home.html', posts=posts)
+
+# http://localhost:5000/about
+@app.route("/about")
+def about():
+ return render_template('about.html', title='About')
+
+@app.route("/register", methods=['GET', 'POST'])
+def register():
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = RegistrationForm()
+ # once submit successfully, flash message shows in the placehold in layout.html
+ # flash message is one time only, it disappear after refresh web browser
+ if form.validate_on_submit():
+ hashed_pwd = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
+ user = User(username=form.username.data, email=form.email.data, password=hashed_pwd)
+ db.session.add(user)
+ db.session.commit()
+ flash('Your account has been created! You are now able to log in!', 'success')
+ return redirect(url_for('login'))
+ return render_template('register.html', title='Register', form=form)
+
+@app.route("/login", methods=['GET', 'POST'])
+def login():
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = LoginForm()
+ if form.validate_on_submit():
+ user = User.query.filter_by(email=form.email.data).first()
+ if user and bcrypt.check_password_hash(user.password, form.password.data) :
+ login_user(user, remember=form.remember.data)
+ # request.args is dictionary type
+ next_page = request.args.get('next')
+ # 'home' is function of home() above
+ return redirect(next_page) if next_page else redirect(url_for('home'))
+ else:
+ flash('Login failed. Please check email and password.', 'danger')
+ return render_template('login.html', title='Login', form=form)
+
+
+@app.route("/logout")
+def logout() :
+ logout_user()
+ return redirect(url_for('home'))
+
+# save uploaded image to the specified path
+def save_picture(form_picture) :
+ random_hex = secrets.token_hex(8)
+ # underscore (_) ignoring the value
+ _, f_ext = os.path.splitext(form_picture.filename)
+ picture_fn = random_hex + f_ext
+ # define the path to store profile image
+ picture_path = os.path.join(app.root_path, 'static/profile_pics', picture_fn)
+
+ # save the uploaded image into the specified path
+
+ # do not want to save the original uploaded image, which can be very large
+ # form_picture.save(picture_path)
+
+ #resize the image before store
+ output_size = (125, 125)
+ i = Image.open(form_picture)
+ i.thumbnail(output_size)
+ i.save(picture_path)
+
+ return picture_fn
+
+
+# login_required decorator makes sure to access the page only
+# when the user log in
+@app.route("/account", methods=['GET', 'POST'])
+@login_required
+def account() :
+ form = UpdateAccountForm()
+ if form.validate_on_submit() :
+ if form.picture.data :
+ picture_file = save_picture(form.picture.data)
+ current_user.image_file = picture_file
+ # think of current_user is reference to the user in database
+ current_user.username = form.username.data
+ current_user.email = form.email.data
+ db.session.commit()
+ flash('Your account has been updated!', 'success')
+ # This is must. After POST request, do the GET request
+ # to avoid POST request again
+ return redirect(url_for('account'))
+ elif request.method == 'GET' :
+ # populate the username and email so that
+ # the new username and email can be viewed right after
+ # update form is submitted
+ form.username.data = current_user.username
+ form.email.data = current_user.email
+ image_file = url_for('static', filename='profile_pics/'+current_user.image_file)
+ return render_template('account.html', title='Account',
+ image_file=image_file, form=form)
+
+
+@app.route("/post/new", methods=['GET', 'POST'])
+@login_required
+def new_post() :
+ form = PostForm()
+ if form.validate_on_submit():
+ # Construct Post object which defined in models.py
+ post = Post(title=form.title.data, content=form.content.data, author=current_user)
+ db.session.add(post)
+ db.session.commit()
+ flash('Your post has been created!', 'success')
+ return redirect(url_for('home'))
+ return render_template('create_post.html', title='New Post',
+ form=form, legend='New Post')
+
+
+@app.route("/post/")
+def post(post_id) :
+ # find the post or give page not found 404 error
+ post = Post.query.get_or_404(post_id)
+ return render_template('post.html', title=post.title, post=post)
+
+@app.route("/post//update", methods=['GET', 'POST'])
+@login_required
+def update_post(post_id) :
+ # find the post or give page not found 404 error
+ post = Post.query.get_or_404(post_id)
+ if post.author != current_user:
+ abort(403)
+ form = PostForm()
+ if form.validate_on_submit():
+ post.title = form.title.data
+ post.content = form.content.data
+ db.session.commit()
+ flash('Your post has been update!', 'success')
+ return redirect(url_for('post', post_id=post.id))
+ elif request.method == 'GET':
+ form.title.data = post.title
+ form.content.data = post.content
+ return render_template('create_post.html', title='Update Post',
+ form=form, legend='Update Post')
+
+
+# "Delete" button in Modal sends POST request to delete the post
+@app.route("/post//delete", methods=['POST'])
+@login_required
+def delete_post(post_id) :
+ post = Post.query.get_or_404(post_id)
+ if post.author != current_user:
+ abort(403)
+ db.session.delete(post)
+ db.session.commit()
+ flash('Your post has been deleted!', 'success')
+ return redirect(url_for('home'))
+
+
+@app.route("/user/")
+def user_posts(username):
+ page = request.args.get('page', 1, type=int)
+ user = User.query.filter_by(username=username).first_or_404()
+ posts = Post.query.filter_by(author=user)\
+ .order_by(Post.date_posted.desc())\
+ .paginate(page=page, per_page=2)
+ return render_template('user_posts.html', posts=posts, user=user)
+
+
+# Send email with the link to reset password
+# The URL link is specified by url_for(), route to reset_token()
+# _external=True tells Flask that it should generate an absolute URL, and not a relative URL.
+def send_reset_email(user) :
+ # token with {'user_id': self.id} as payload
+ token = user.get_reset_token()
+ msg = Message('Password Reset Request', sender='noreply@demo.com',
+ recipients=[user.email])
+ msg.body = f'''To reset your password, visit the following link:
+{url_for('reset_token', token=token, _external=True)}
+
+If you did not make this request then simply ignore this email and no changes will be made.
+'''
+ # send the token via email
+ mail.send(msg)
+
+
+# route the form to send instruction email to request password reset
+# when forgot the password
+@app.route("/reset_password", methods=['GET', 'POST'])
+def reset_request() :
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ form = RequestResetForm()
+ if form.validate_on_submit():
+ user = User.query.filter_by(email=form.email.data).first()
+ send_reset_email(user)
+ flash('An email has been sent with instructions to reset your password', 'info')
+ return redirect(url_for('login'))
+ return render_template('reset_request.html', title='Reset Password', form=form)
+
+
+# route with the form to set new password (the URL link is from email)
+@app.route("/reset_password/", methods=['GET', 'POST'])
+def reset_token(token) :
+ if current_user.is_authenticated:
+ return redirect(url_for('home'))
+ # the token comes with user id. if the user id is valid,
+ # the valid user is returned
+ user = User.verify_reset_token(token)
+ if user is None:
+ flash('That is an invalid or expired token', 'warning')
+ return redirect(url_for('reset_request'))
+ form = ResetPasswordForm()
+ if form.validate_on_submit():
+ # hash the password
+ hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
+ user.password = hashed_password
+ db.session.commit()
+ flash('Your password has been updated! You are now able to log in', 'success')
+ return redirect(url_for('login'))
+ return render_template('reset_token.html', title='Reset Password', form=form)
+
diff --git a/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/site.db b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/site.db
new file mode 100644
index 000000000..04f790eab
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/site.db differ
diff --git a/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/static/main.css b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/static/main.css
new file mode 100644
index 000000000..730e2bca6
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/static/main.css
@@ -0,0 +1,80 @@
+body {
+ background: #fafafa;
+ color: #333333;
+ margin-top: 5rem;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: #444444;
+}
+
+.bg-steel {
+ background-color: #5f788a;
+}
+
+.site-header .navbar-nav .nav-link {
+ color: #cbd5db;
+}
+
+.site-header .navbar-nav .nav-link:hover {
+ color: #ffffff;
+}
+
+.site-header .navbar-nav .nav-link:active {
+ font-weight: 500;
+}
+
+.content-section {
+ background: #ffffff;
+ padding: 10px 20px;
+ border: 1px solid #dddddd;
+ border-radius: 3px;
+ margin-bottom: 20px;
+}
+
+.article-title {
+ color: #444444;
+}
+
+a.article-title:hover {
+ color: #428bca;
+ text-decoration: none;
+}
+
+.article-content {
+ white-space: pre-line;
+}
+
+.article-img {
+ height: 65px;
+ width: 65px;
+ margin-right: 16px;
+}
+
+.article-metadata {
+ padding-bottom: 1px;
+ margin-bottom: 4px;
+ border-bottom: 1px solid #e3e3e3
+}
+
+.article-metadata a:hover {
+ color: #333;
+ text-decoration: none;
+}
+
+.article-svg {
+ width: 25px;
+ height: 25px;
+ vertical-align: middle;
+}
+
+.account-img {
+ height: 125px;
+ width: 125px;
+ margin-right: 20px;
+ margin-bottom: 16px;
+}
+
+.account-heading {
+ font-size: 2.5rem;
+}
diff --git a/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/static/profile_pics/919b30429a4f5711.jpg b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/static/profile_pics/919b30429a4f5711.jpg
new file mode 100644
index 000000000..2fe68efe2
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/static/profile_pics/919b30429a4f5711.jpg differ
diff --git a/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/static/profile_pics/default.jpg b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/static/profile_pics/default.jpg
new file mode 100644
index 000000000..38f286f63
Binary files /dev/null and b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/static/profile_pics/default.jpg differ
diff --git a/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/templates/about.html b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/templates/about.html
new file mode 100644
index 000000000..95312bed7
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/templates/about.html
@@ -0,0 +1,4 @@
+{% extends "layout.html" %}
+{% block content %}
+
About page
+{% endblock content %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/templates/account.html b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/templates/account.html
new file mode 100644
index 000000000..86b047a25
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/flaskblog/templates/account.html
@@ -0,0 +1,64 @@
+{% extends "layout.html" %}
+{% block content %}
+
+
+ {% endfor %}
+ {% for page_num in posts.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}
+ {% if page_num %}
+ {% if posts.page==page_num %}
+ {{ page_num }}
+ {% else %}
+ {{ page_num }}
+ {% endif %}
+ {% else %}
+ ...
+ {% endif %}
+ {% endfor %}
+{% endblock %}
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/run.py b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/run.py
new file mode 100644
index 000000000..0c8390926
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/run.py
@@ -0,0 +1,7 @@
+from flaskblog import app
+
+# flaskblog is package
+# import app from flaskblog package
+
+if __name__ == '__main__':
+ app.run(debug=True)
\ No newline at end of file
diff --git a/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/zreadme.txt b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/zreadme.txt
new file mode 100644
index 000000000..70efa8009
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/zreadme.txt
@@ -0,0 +1,108 @@
+
+1. look at the token (payload) demo below.
+
+ $ python
+ >>> from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
+ >>> s = Serializer('secret', 30) # token expires in 30 seconds
+ >>> token = s.dumps({'user_id': 1}).decode('utf-8') # {'user_id': 1} as payload
+ >>> token
+ 'eyJhbGciOiJIUzUxMiIsImlhdCI6MTYxNDY1NTYwMiwiZXhwIjoxNjE0NjU1NjMyfQ.eyJ1c2VyX2lkIjoxfQ.7R8LOCtZJ83OhB6er_0eGf7UxsL3HPI5y0DN2W2cdyXQoiwXDBRgeSSshQCZVlFnlQbbHzajTjSwKQCuE6-ZzA'
+ >>> s.loads(token) # this must be done in 30 seconds to get payload. OR it fails due to token expires.
+ {'user_id': 1} # payload
+ >>> s.loads(token) # it fails if wait for more than 30 seconds
+ itsdangerous.exc.SignatureExpired: Signature expired
+
+
+2. install flask-mail
+ $ pip3 install flask-mail
+
+3. Update User() in models.py with
+ from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
+ from flaskblog import db, login_manager, app
+ get_reset_token()
+ verify_reset_token()
+
+4. Add RequestResetForm(), and ResetPasswordForm() in forms.py
+
+5. create reset_request() route in route.py
+
+ Send email with the link to reset the password once the user requests
+ password reset.
+ The token in the link comes with {'user_id': self.id} as payload.
+
+ - from flaskblog.forms import (RegistrationForm, LoginForm, UpdateAccountForm,
+ PostForm, RequestResetForm, ResetPasswordForm)
+ - from flaskblog import app, db, bcrypt, mail
+ mail is declared in __init__ as below
+ - from flask_mail import Message
+ - send_reset_email()
+
+ Example in email:
+ To reset your password, visit the following link:
+ http://127.0.0.1:5000/reset_password/eyJhb...rTSAYww
+
+6. create reset_request.html template
+ view to request password reset.
+
+7. create reset_token route in route.py
+ The link in the email is routed to reset_token().
+ The token in the link comes with {'user_id': self.id} as payload to query
+ the user to update the new password.
+
+8. create reset_token.html template
+ view to update the new password.
+
+9. add MAIL server in __init__.py
+ import os
+ from flask_mail import Mail
+
+ # Config gmail server to send instruction how to reset password
+ app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
+ app.config['MAIL_PORT'] = 587
+ app.config['MAIL_USE_TLS'] = True
+
+ # environment variables
+ # Need to set these variables on Windows to your gmail user and password
+ # ie.
+ # > set EMAIL_USER xyz@gmail.com (use 'export' in linux)
+ # > set EMAIL_PASS password_to_xyz (use 'export' in linux)
+ app.config['MAIL_USERNAME'] = os.environ.get('EMAIL_USER')
+ app.config['MAIL_PASSWORD'] = os.environ.get('EMAIL_PASS')
+ mail = Mail(app)
+
+10. update the link to "Forgot Password" in login.html
+
+
+ Forgot password ?
+
+
+
+11. test website
+ http://127.0.0.1:5000/login
+ try to reset password
+
+ - Register an user with real email
+ user: zhjohn925
+ email: zhjohn925@hotmail.com
+ password: 1234
+ - The email with the link to reset password is sent via GMAIL
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ In order to send email from gmail, need to
+ 1. enable access for less secure apps in gmail account
+ 2. on Windows environment variables (Use 'export' in linux)
+ > set EMAIL_USER=zhjohn925@gmail.com
+ > set EMAIL_PASS=
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ - http://127.0.0.1:5000/login
+ - click "Forgot the password" to reset password
+ - the link to reset password is sent to hotmail
+ - click the link to set new password
+
+
+
+
+
+
+
+
+
diff --git a/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/zreadme_pagination.txt b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/zreadme_pagination.txt
new file mode 100644
index 000000000..c55e6fb10
--- /dev/null
+++ b/Python/Flask_Blog/00-Practices/J-Email-Reset-Password/zreadme_pagination.txt
@@ -0,0 +1,96 @@
+
+$ cd Flask_Blog\00-Practices\H-Posts-3
+$ python
+
+>>> from flaskblog.models import Post
+>>> posts = Post.query.all()
+
+>>> for post in posts :
+... print(post)
+...
+ Post('Blog 2 Xy by user2','2021-03-28 05:31:26.709109')
+ Post('Blog 1','2021-04-03 17:47:39.443134')
+ ::::::
+
+>>> posts = Post.query.paginate()
+>>> posts
+
+>>> dir(posts)
+ ['__class__', :::, 'has_next', 'has_prev', 'items', 'iter_pages',
+ 'next', 'next_num', 'page', 'pages', 'per_page', 'prev', 'prev_num',
+ 'query', 'total']
+
+>>> posts.per_page
+ 20 # 20 post per page
+
+>>> posts.page
+1 # the current page
+
+>>> posts.total # total 14 posts
+14
+
+>>> for post in posts.items:
+... print(post)
+...
+ Post('Blog 2 Xy by user2','2021-03-28 05:31:26.709109')
+ Post('Blog 1','2021-04-03 17:47:39.443134')
+ Post('Why do we use it?','2021-04-03 17:59:37.163519')
+ ::::::
+
+########################################################
+# get 5 posts in the first page
+########################################################
+>>> posts = Post.query.paginate(per_page=5) # set 5 posts per page
+>>> for post in posts.items:
+... print(post)
+...
+Post('Blog 2 Xy by user2','2021-03-28 05:31:26.709109')
+Post('Blog 1','2021-04-03 17:47:39.443134')
+Post('Why do we use it?','2021-04-03 17:59:37.163519')
+Post('web page editors ','2021-04-03 18:00:12.111351')
+Post('What is Lorem Ipsum?','2021-04-03 18:01:24.645851')
+
+########################################################
+# get 5 posts in the second page
+########################################################
+>>> posts = Post.query.paginate(per_page=5, page=2)
+>>> for post in posts.items:
+... print(post)
+...
+Post('the release of Letraset sheets','2021-04-03 18:01:57.451815')
+Post('using Lorem Ipsum','2021-04-03 18:02:38.718257')
+Post('Where can I get some?','2021-04-03 18:03:25.709345')
+Post('a Latin professor','2021-04-03 18:04:03.121023')
+Post('consectetur, adipisci velit','2021-04-03 18:05:01.839378')
+
+########################################################
+# get posts in the first page, default 20 posts per page
+########################################################
+>>> posts = Post.query.paginate(page=1)
+>>> for post in posts.items:
+... print(post)
+...
+Post('Blog 2 Xy by user2','2021-03-28 05:31:26.709109')
+Post('Blog 1','2021-04-03 17:47:39.443134')
+Post('Why do we use it?','2021-04-03 17:59:37.163519')
+:::::::
+
+########################################################
+# iterate pages
+########################################################
+ # There are total of 14 posts.
+ # One post per page, so we see 14 pages
+ # When there are many pages, we only care about the current page.
+ # the other pages can be skipped ('None' can be used for this purpose).
+>>> posts = Post.query.paginate(per_page=1)
+>>> for page in posts.iter_pages():
+... print(page)
+...
+ 1
+ 2
+ 3
+ 4
+ 5
+ None #the other pages are skipped
+ 13
+ 14
\ No newline at end of file
diff --git a/Python/Flask_Blog/01-Getting-Started/flaskblog.py b/Python/Flask_Blog/01-Getting-Started/flaskblog.py
index 04deb7bd0..1acfdf6cf 100644
--- a/Python/Flask_Blog/01-Getting-Started/flaskblog.py
+++ b/Python/Flask_Blog/01-Getting-Started/flaskblog.py
@@ -1,17 +1,29 @@
from flask import Flask
-app = Flask(__name__)
+# __name__ is __main__ if we run this script directly.
+# if it is imported by somewhere else, __name__ is
+# module name
+app = Flask(__name__)
+# decorator adds the additional functionality into the existing functions.
+# route to http://localhost:5000/ ("/" is root page of the web site).
+# route to http://localhost:5000/home
@app.route("/")
@app.route("/home")
def home():
return "
Home Page
"
-
+# route to http://localhost:5000/about
+# if below is not defined, "404 error" occurs due to the page not found.
@app.route("/about")
def about():
return "
About Page
"
-
+# __name__ is __main__ only when run it directly, then the condition is True,
+# the functions in if block will be called.
+# otherwise, __name__ is the module name when imported by other python scripts, then
+# the condition is False, the functions in if block will not be called.
if __name__ == '__main__':
+ # run in debug mode. That means, anything changes, no need to restart web server,
+ # the web browser can pick up after reload.
app.run(debug=True)
diff --git a/Python/Flask_Blog/02-Templates/flaskblog.py b/Python/Flask_Blog/02-Templates/flaskblog.py
index a54bab8d2..65c5e2bab 100644
--- a/Python/Flask_Blog/02-Templates/flaskblog.py
+++ b/Python/Flask_Blog/02-Templates/flaskblog.py
@@ -20,6 +20,8 @@
@app.route("/")
@app.route("/home")
def home():
+ # pass variable (posts) into html template
+ # use jinja2 in html ie. {% .... %}
return render_template('home.html', posts=posts)
diff --git a/Python/Flask_Blog/02-Templates/static/main.css b/Python/Flask_Blog/02-Templates/static/main.css
index c05529fe9..9f6478df4 100644
--- a/Python/Flask_Blog/02-Templates/static/main.css
+++ b/Python/Flask_Blog/02-Templates/static/main.css
@@ -1,80 +1,82 @@
-body {
+ body {
background: #fafafa;
color: #333333;
margin-top: 5rem;
-}
+ }
-h1, h2, h3, h4, h5, h6 {
+ h1, h2, h3, h4, h5, h6 {
color: #444444;
-}
+ }
-.bg-steel {
+ .bg-steel {
background-color: #5f788a;
-}
+ }
-.site-header .navbar-nav .nav-link {
+
+
+ .site-header .navbar-nav .nav-link {
color: #cbd5db;
-}
+ }
-.site-header .navbar-nav .nav-link:hover {
+ .site-header .navbar-nav .nav-link:hover {
color: #ffffff;
-}
+ }
-.site-header .navbar-nav .nav-link.active {
+ .site-header .navbar-nav .nav-link.active {
font-weight: 500;
-}
+ }
-.content-section {
+ .content-section {
background: #ffffff;
padding: 10px 20px;
border: 1px solid #dddddd;
border-radius: 3px;
margin-bottom: 20px;
-}
+ }
-.article-title {
+ .article-title {
color: #444444;
-}
+ }
-a.article-title:hover {
+ a.article-title:hover {
color: #428bca;
text-decoration: none;
-}
+ }
-.article-content {
+ .article-content {
white-space: pre-line;
-}
+ }
-.article-img {
+ .article-img {
height: 65px;
width: 65px;
margin-right: 16px;
-}
+ }
-.article-metadata {
+ .article-metadata {
padding-bottom: 1px;
margin-bottom: 4px;
border-bottom: 1px solid #e3e3e3
-}
+ }
-.article-metadata a:hover {
+ .article-metadata a:hover {
color: #333;
text-decoration: none;
-}
+ }
-.article-svg {
+ .article-svg {
width: 25px;
height: 25px;
vertical-align: middle;
-}
+ }
-.account-img {
+ .account-img {
height: 125px;
width: 125px;
margin-right: 20px;
margin-bottom: 16px;
-}
+ }
-.account-heading {
+ .account-heading {
font-size: 2.5rem;
-}
+ }
diff --git a/Python/Flask_Blog/02-Templates/templates/about.html b/Python/Flask_Blog/02-Templates/templates/about.html
index edca7c0f4..db21353a2 100644
--- a/Python/Flask_Blog/02-Templates/templates/about.html
+++ b/Python/Flask_Blog/02-Templates/templates/about.html
@@ -1,3 +1,4 @@
+
{% extends "layout.html" %}
{% block content %}
About Page
diff --git a/Python/Flask_Blog/02-Templates/templates/home.html b/Python/Flask_Blog/02-Templates/templates/home.html
index ff37a53ff..b280279d8 100644
--- a/Python/Flask_Blog/02-Templates/templates/home.html
+++ b/Python/Flask_Blog/02-Templates/templates/home.html
@@ -1,3 +1,4 @@
+
{% extends "layout.html" %}
{% block content %}
{% for post in posts %}
diff --git a/Python/Flask_Blog/02-Templates/templates/layout.html b/Python/Flask_Blog/02-Templates/templates/layout.html
index 676abcbf5..10c459ef6 100644
--- a/Python/Flask_Blog/02-Templates/templates/layout.html
+++ b/Python/Flask_Blog/02-Templates/templates/layout.html
@@ -41,6 +41,7 @@
+
{% block content %}{% endblock %}
diff --git a/Python/Flask_Blog/02-Templates/templates/zreadme.txt b/Python/Flask_Blog/02-Templates/templates/zreadme.txt
new file mode 100644
index 000000000..c2f448ead
--- /dev/null
+++ b/Python/Flask_Blog/02-Templates/templates/zreadme.txt
@@ -0,0 +1,9 @@
+
+
+- Bootstrap, select 'Docs', look for "Starter template"
+https://getbootstrap.com/
+https://getbootstrap.com/docs/5.0/getting-started/introduction/
+
+- "Cmd + Shift + R" on web browser to hard reset it and also clear the cache
+
+
diff --git a/Python/Flask_Blog/04-Database/flaskblog.py b/Python/Flask_Blog/04-Database/flaskblog.py
index 8f7f6571d..203c70691 100644
--- a/Python/Flask_Blog/04-Database/flaskblog.py
+++ b/Python/Flask_Blog/04-Database/flaskblog.py
@@ -5,7 +5,7 @@
app = Flask(__name__)
app.config['SECRET_KEY'] = '5791628bb0b13ce0c676dfde280ba245'
-app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site2.db'
db = SQLAlchemy(app)
diff --git a/Python/Flask_Blog/04-Database/site2.db b/Python/Flask_Blog/04-Database/site2.db
new file mode 100644
index 000000000..f9f533e62
Binary files /dev/null and b/Python/Flask_Blog/04-Database/site2.db differ
diff --git a/Python/Flask_Blog/10-Password-Reset-Email/flaskblog/__init__.py b/Python/Flask_Blog/10-Password-Reset-Email/flaskblog/__init__.py
index ff88c7613..4cdbbffbd 100644
--- a/Python/Flask_Blog/10-Password-Reset-Email/flaskblog/__init__.py
+++ b/Python/Flask_Blog/10-Password-Reset-Email/flaskblog/__init__.py
@@ -18,6 +18,11 @@
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('EMAIL_USER')
app.config['MAIL_PASSWORD'] = os.environ.get('EMAIL_PASS')
+#app.config['MAIL_SERVER'] = 'smtp.gmail.com'
+#app.config['MAIL_PORT'] = 465
+#app.config['MAIL_USE_SSL'] = True
+#app.config['MAIL_USERNAME'] = os.environ.get('EMAIL_USER')
+#app.config['MAIL_PASSWORD'] = os.environ.get('EMAIL_PASS')
mail = Mail(app)
from flaskblog import routes
diff --git a/Python/Flask_Blog/10-Password-Reset-Email/flaskblog/models.py b/Python/Flask_Blog/10-Password-Reset-Email/flaskblog/models.py
index 1cee294b8..ae5c26149 100644
--- a/Python/Flask_Blog/10-Password-Reset-Email/flaskblog/models.py
+++ b/Python/Flask_Blog/10-Password-Reset-Email/flaskblog/models.py
@@ -17,10 +17,13 @@ class User(db.Model, UserMixin):
password = db.Column(db.String(60), nullable=False)
posts = db.relationship('Post', backref='author', lazy=True)
+ # return a 30min token with user id encoded
def get_reset_token(self, expires_sec=1800):
s = Serializer(app.config['SECRET_KEY'], expires_sec)
return s.dumps({'user_id': self.id}).decode('utf-8')
+ # decode the given token to get user id
+ # return the user by user id
@staticmethod
def verify_reset_token(token):
s = Serializer(app.config['SECRET_KEY'])
diff --git a/Python/Flask_Blog/10-Password-Reset-Email/flaskblog/site.db b/Python/Flask_Blog/10-Password-Reset-Email/flaskblog/site.db
index 01a9f6f2f..53dd41b90 100644
Binary files a/Python/Flask_Blog/10-Password-Reset-Email/flaskblog/site.db and b/Python/Flask_Blog/10-Password-Reset-Email/flaskblog/site.db differ
diff --git a/Python/Flask_Blog/profile_images/dog01_large.jpg b/Python/Flask_Blog/profile_images/dog01_large.jpg
new file mode 100644
index 000000000..e6104e489
Binary files /dev/null and b/Python/Flask_Blog/profile_images/dog01_large.jpg differ
diff --git a/Python/Flask_Blog/profile_images/dog02_small.jpg b/Python/Flask_Blog/profile_images/dog02_small.jpg
new file mode 100644
index 000000000..5f23fc7a5
Binary files /dev/null and b/Python/Flask_Blog/profile_images/dog02_small.jpg differ
diff --git a/Python/Flask_Blog/profile_images/profile_img1_large.png b/Python/Flask_Blog/profile_images/profile_img1_large.png
new file mode 100644
index 000000000..92221ad6d
Binary files /dev/null and b/Python/Flask_Blog/profile_images/profile_img1_large.png differ
diff --git a/Python/Flask_Blog/profile_images/profile_img2_small.png b/Python/Flask_Blog/profile_images/profile_img2_small.png
new file mode 100644
index 000000000..e1b5bc021
Binary files /dev/null and b/Python/Flask_Blog/profile_images/profile_img2_small.png differ
diff --git a/Python/Flask_Blog/zreadme.txt b/Python/Flask_Blog/zreadme.txt
new file mode 100644
index 000000000..b76e945e9
--- /dev/null
+++ b/Python/Flask_Blog/zreadme.txt
@@ -0,0 +1,27 @@
+https://www.youtube.com/watch?v=MwZwr5Tvyxo&list=PL-osiE80TeTs4UjLw5MM6OjgkjFeUxCYH
+https://rahul1999.medium.com/deploy-a-flask-app-with-a-sqlite-database-on-heroku-22b5402c5c6
+https://roytuts.com/how-to-deploy-python-flask-mysql-based-application-in-heroku-cloud/
+
+- Install flask (try to import flask under python to verify the installation)
+
+$ pip3 install flask
+$ pip3 install flask-wtf # install forms for part 3.
+
+
+There are two ways to run the script
+1. Run flask
+
+$ export FLASK_APP=flaskblog.py #in Linux
+> set FLASK_APP=flaskblog.py #in Windows
+
+$ export FLASK_DEBUG=1 #Enable debugger. anything changes in the code, no need to restart web server,
+> set FLASK_DEBUG=1 #the web browser can pick up after reload.
+
+$ flask run
+Running on http://127.0.0.1:5000/
+(Or http://localhost:5000/)
+
+2. Run python script directly
+$ python flaskblog.py
+
+
diff --git a/Python/Flask_Blog/zreadme_ssl_tls b/Python/Flask_Blog/zreadme_ssl_tls
new file mode 100644
index 000000000..ab3b9dceb
--- /dev/null
+++ b/Python/Flask_Blog/zreadme_ssl_tls
@@ -0,0 +1,19 @@
+// SSL/TLS
+
+https://www.youtube.com/watch?v=Gdys9qPjuKs&list=PL-osiE80TeTs4UjLw5MM6OjgkjFeUxCYH&index=15
+
+https://letsencrypt.org/
+
+$ sudo apt-get update
+$ sudo apt-get install software-properties-common
+$ sudo add-apt-repository universe
+$ sudo add-apt-repository ppa:certbot/certbot
+$ sudo apt-get update
+$ sudo apt-get install python-certbot-nginx
+$ sudo certbot --nginx
+$ sudo certbot renew --dry-run # run this in crontab
+
+
+$ sudo ufw allow https
+$ sudo systemctl restart nginx
+
diff --git a/README.md b/README.md
index 2b135616d..f357bd5e2 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,26 @@
-# code_snippets
\ No newline at end of file
+# code_snippets
+https://github.com/CoreyMSchafer/code_snippets
+
+# youtube
+https://www.youtube.com/watch?v=MwZwr5Tvyxo&list=PL-osiE80TeTs4UjLw5MM6OjgkjFeUxCYH
+
+- Flask tutorials
+ code_snippets/Python/Flask_Blog/
+
+- git checkout one directory in git repo
+
+$ git clone \
+ --depth 1 \
+ --filter=blob:none \
+ --sparse \
+ https://github.com/zhjohn925/code_snippets \
+;
+
+#cd test-git-partial-clone
+
+#git sparse-checkout init --cone
+
+git sparse-checkout set Python/Flask_Blog # Flask tutorial
+
+git sparse-checkout set Django_Blog # Django tutorial
+