用户登陆 登出 注册
所有 pip install 需要 workon env 之后执行
用户登录
login.html
{% extends 'base.html' %}
{% block title %}
login page
{% end %}
{% block content %}
{% if error %}
{{ error }}
{% end %}
<div class="">
<form action="/login?next={{ nextname }}" method="post" enctype="multipart/form-data">
<div class="form-group">
Username
<input autofocus="" class="form-control" id="id_username" maxlength="254" name="username" type="text"
required="">
</div>
<div class="form-group">
Password
<input class="form-control" id="id_password" name="password" type="password" required="">
</div>
<button class="">Login</button>
<div>
还没有账号 需要<a href="/signup">注册</a>一个
</div>
</form>
</div>
{% end %}
app.py
class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r'/login', auth.LoginHandler),
]
settings = dict(
debug=True,
template_path='template',
static_path='static', # 相对路径
cookie_secret='bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=',
login_url='/login',
pycket={
'engine': 'redis',
'storage': {
'host': 'localhost',
'port': 6379,
# 'password': '',
'db_sessions': 5, # redis db index
'db_notifications': 11,
'max_connections': 2 ** 30,
},
'cookies': {
'expires_days': 30
}
}
)
super(Application, self).__init__(handlers, **settings)
使用 pycket 和 cookie_secret
pip install pycket
pip install redis
pycket={
'engine': 'redis',
'storage': {
'host': 'localhost',
'port': 6379,
# 'password': '',
'db_sessions': 5, # redis db index
'db_notifications': 11,
'max_connections': 2 ** 30,
},
'cookies': {
'expires_days': 30,
},
}
account.py
import hashlib
def hashed(passwd):
return hashlib.md5(passwd.encode('utf8')).hexdigest()
USER_DATA = {
'name': '1',
'password': hashed('1')
}
def authenticate(username, password):
if username and password:
if username == USER_DATA['name'] and hashed(password) == USER_DATA['password']:
return True
return False
auth.py
class LoginHandler(AuthBaseHandler):
"""
登录
"""
def get(self, *args, **kwargs):
next = self.get_argument('next', '/') # 注意是/ 没有next(从logout跳转过来)就跳转到首页
self.render('login.html', nextname=next, error=None)
def post(self, *args, **kwargs):
username = self.get_argument('username')
password = self.get_argument('password')
next = self.get_argument('next', '/')
passed = authenticate(username, password)
if passed:
self.session.set('tudo_user_info', username)
self.redirect(next)
else:
self.render('login.html', nextname=next, error='用户名或密码错误')
main.py
import tornado.web
import os
from utils import photo
from pycket.session import SessionMixin
class AuthBaseHandler(tornado.web.RequestHandler,SessionMixin):
def get_current_user(self):
return self.session.get('tudo_user_info')
# 图片展示
class IndexHandler(AuthBaseHandler):
"""
Home page for user,photo feeds.
"""
@tornado.web.authenticated
def get(self, *args, **kwargs):
images_path = os.path.join(self.settings.get('static_path'), 'uploads')
images = photo.get_images(images_path)
print(images)
self.render('index.html', images=images)
用户登出(注销)
base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}Title{% end %}</title>
</head>
<body>
<div style="width: 200px; height:100px;display: block;">
<a href="/logout">登出</a>
</div>
<div style="width: auto;text-align: center">
{% block content %}
base content
{% end %}
</div>
</body>
</html>
main.py
class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r'/logout', auth.LogoutHandler),
]
settings = dict(
debug=True,
template_path='template',
static_path='static', # 相对路径
cookie_secret='bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=',
login_url='/login',
pycket={
'engine': 'redis',
'storage': {
'host': 'localhost',
'port': 6379,
# 'password': '',
'db_sessions': 5, # redis db index
'db_notifications': 11,
'max_connections': 2 ** 30,
},
'cookies': {
'expires_days': 30
}
}
)
super(Application, self).__init__(handlers, **settings)
auth.py
class LogoutHandler(AuthBaseHandler):
@tornado.web.authenticated
def get(self, *args, **kwargs):
self.session.set('tudo_user_info', '')
self.redirect('/login') # 直接跳转到 login 没有next 默认是/
用户注册
signup.html
{% extends 'base.html' %}
{% block title %}
signup page
{% end %}
{% block content %}
<form action="/signup" enctype="multipart/form-data" method="post">
<div class="form-group">
Username
<input autofocus="" class="form-control" id="id_username" maxlength="150" name="username" type="text" required="">
</div>
<div class="form-group">
Email
<input class="form-control" id="id_email" name="email" type="email" required="">
</div>
<div class="form-group">
Password
<input class="form-control" id="id_password1" name="password1" type="password" required="">
</div>
<div class="form-group">
Password confirmation
<input class="form-control" id="id_password2" name="password2" type="password" required="">
</div>
<div class="text-center help-text">
已有账号请 <a href="/login">登录</a>
</div>
</form>
{% end %}
auth.py
class SignupHandler(AuthBaseHandler):
def get(self, *args, **kwargs):
self.render('signup.html')
def post(self, *args, **kwargs):
username = self.get_argument('username', 'no')
email = self.get_argument('email', 'no')
password1 = self.get_argument('password1', 'no')
password2 = self.get_argument('password2', 'no')
# 用户名 密码不能为空 两次密码需一致
if username and password1 and password1:
if password1 != password2:
self.write({'msg': '两次输入的密码不匹配'})
else:
pass
app.py
class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r'/signup', auth.SignupHandler),
]
settings = dict(
debug=True,
template_path='template',
static_path='static', # 相对路径
cookie_secret='bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=',
login_url='/login',
pycket={
'engine': 'redis',
'storage': {
'host': 'localhost',
'port': 6379,
# 'password': '',
'db_sessions': 5, # redis db index
'db_notifications': 11,
'max_connections': 2 ** 30,
},
'cookies': {
'expires_days': 30
}
}
)
super(Application, self).__init__(handlers, **settings)
application = Application()
SQLalchemy 版本迁移
pip install pymysql
pip install sqlalchemy
pip install alembic
完成 pip 安装之后
-
在 shell 里面 cd 到项目根目录执行
alembic init alembic
-
用 pycharm 把生成的文件 download 回来(包括
alembic
目录和alembic.ini
) -
修改
alembic.ini
设置数据库连接。sqlalchemy.url = driver://user:pass@localhost/dbname
变成
sqlalchemy.url = mysql+pymysql://admin:Root110qwe@127.0.0.1:3306/tudo
-
在
env.py
中设置,将target_metadata赋值成数据库的元数据(metadata)from models.account import Base target_metadata = Base.metadata
-
如果执行 revision 有 import 报错,注意是否正确将当前项目目录添加到 sys.path 路径
import sys
from os.path import abspath, dirname
root = dirname(dirname(abspath(__file__)))
print(root)
sys.path.append(root)
from __future__ import with_statement
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
import os, sys
from os.path import abspath, dirname
root = dirname(dirname(abspath(__file__)))
print(root)
sys.path.append(root)
from models.account import Base
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
-
配置完成执行
alembic revision --autogenerate -m ‘create users table’
这里可以看到虚拟机目录在 alembic/versions 里生成了 py 文件,然后执行
alembic upgrade head
这样就会更新 mysql 数据库了
-
删除配置
rm alembic/versions/…
-
若想增加一个属性
先在Users类写好 再在终端执行
alembic revision --autogenerate -m ‘add created for users’
or
(alembic revision --autogenerate -m ‘add field for users’)
alembic upgrade head
(alembic revision --autogenerate -m ‘add thumb url’)
-
若想增加一个类
alembic revision --autogenerate -m ‘add post model’
alembic upgrade head
-
回退一个版本
alembic downgrade -1
如果报这个错误
Can't locate revision identified by
将alembic_version表中的数据删掉即可
作业:
实现 登陆/登出页面,session 的设置,login 加上 next 跳转回原来访问页面。(提交截图)
hashlib
廖大神
import hashlib
md5 = hashlib.md5()
md5.update('how to use md5 in python hashlib?')
print md5.hexdigest()
d26a53750bc40b38b65a520292f69306
附加:
了解数据库迁移工具 alembic 的使用
参考文档:
Python数据模型构建和迁移方案:SQLAlchemy&Alembic
完整代码
app.py
import os
import tornado.ioloop
import tornado.options
import tornado.web
from tornado.options import define, options
from handlers import main
from handlers import auth
define('port', default='8000', type=int, help='Listening port')
class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r'/', main.IndexHandler),
(r'/explore', main.ExploreHandler),
(r'/post/(?P<post_id>[0-9]+)', main.PostHandler),
(r'/upload', main.UploadHandler),
(r'/login', auth.LoginHandler),
(r'/logout', auth.LogoutHandler),
(r'/signup', auth.SignupHandler),
]
settings = dict(
debug=True,
template_path='template',
static_path='static', # 相对路径
# static_path=os.path.join(os.path.dirname(__file__), 'static'), # 绝对路径
cookie_secret='bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=',
login_url='/login',
pycket={
'engine': 'redis',
'storage': {
'host': 'localhost',
'port': 6379,
# 'password': '',
'db_sessions': 5, # redis db index
'db_notifications': 11,
'max_connections': 2 ** 30,
},
'cookies': {
'expires_days': 30
}
}
)
super(Application, self).__init__(handlers, **settings)
application = Application()
if __name__ == '__main__':
tornado.options.parse_command_line()
application.listen(options.port)
print("Server start on port {}".format(str(options.port)))
tornado.ioloop.IOLoop.current().start()
main.py
import tornado.web
import os
from utils import photo
from pycket.session import SessionMixin
class AuthBaseHandler(tornado.web.RequestHandler, SessionMixin):
def get_current_user(self):
return self.session.get('tudo_user_info')
# 图片展示
class IndexHandler(AuthBaseHandler):
"""
Home page for user,photo feeds.
"""
@tornado.web.authenticated
def get(self, *args, **kwargs):
# os.path.join组合成新目录
images_path = os.path.join(self.settings.get('static_path'), 'uploads') # static/uploads
# 获取该路径下所有的图片路径
images = photo.get_images(images_path) # ['static/uploads/1106628.jpg', 'static/uploads/701728.jpg',...]
print(images)
self.render('index.html', images=images)
# 缩略图展示
class ExploreHandler(AuthBaseHandler):
"""
Explore page ,photo of other users.
"""
@tornado.web.authenticated
def get(self, *args, **kwargs):
# 获取该路径下所有的缩略图的路径
images_url = photo.get_images('./static/uploads/thumbs')
# ['./static/uploads/thumbs/1106620_200x200.jpg', './static/uploads/thumbs/1106622_200x200.jpg',...]
self.render('explore.html', images=images_url)
class PostHandler(AuthBaseHandler):
"""
Single photo page,and maybe comments
"""
@tornado.web.authenticated
def get(self, *args, **kwargs):
self.render('post.html', post_id=kwargs['post_id'])
class UploadHandler(AuthBaseHandler):
"""
接受图片上传
"""
@tornado.web.authenticated
def get(self, *args, **kwargs):
self.render('upload.html')
def post(self, *args, **kwargs):
# 提取表单中‘name’为‘newimg’的文件元数据 获取上传文件信息
img_files = self.request.files.get('newimg')
print(img_files) # [{"filename": ..., "content_type": ..., "body": ...},]
print(img_files[0]['filename']) # 例:1178645.jpg
file_size = self.request.headers.get('Content-Length') # 18539
print(file_size)
if img_files:
for img_file in img_files:
# img_file['filename']获取文件的名称 有些文件需要以二进制的形式存储
with open('./static/uploads/' + img_file['filename'], 'wb') as f:
f.write(img_file['body']) # img_file['body']获取文件的内容
photo.make_thumb('./static/uploads/' + img_file['filename']) # 生成缩略图
self.write({'msg': 'got file: {}'.format(img_files[0]['filename'])})
else:
self.write({'msg': 'empty form data'})
photo.py
import glob
import os
from PIL import Image
# 获取图片路径
def get_images(path):
print(path) # static/uploads
images = []
for file in glob.glob(path + '/*.jpg'):
images.append(file) # static/uploads/1172020.jpg
return images
# 生成缩略图
def make_thumb(path):
print(path) # ./static/uploads/1172020.jpg
file, ext = os.path.splitext(os.path.basename(path))
print(os.path.basename(path)) # 1172020.jpg
print(file) # 1172020
print(ext) # .jpg
im = Image.open(path) # 打开./static/uploads/1172020.jpg图片
im.thumbnail((200, 200)) # 按长200高200缩放
# 保存./static/uploads/thumbs/1172020_200x200.jpg
im.save('./static/uploads/thumbs/{}_{}x{}.jpg'.format(file, 200, 200), "JPEG")
base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}Title{% end %}</title>
</head>
<body>
<div style="width: 200px; height:100px;display: block;">
<a href="/logout">登出</a>
</div>
<div style="width: auto;text-align: center">
{% block content %}
base content
{% end %}
</div>
</body>
</html>
index.html
{% extends 'base.html' %}
{% block title %}
index page
{% end %}
{% block content %}
{% for img in images %}
<img src="{{ img }} " width="666">
{% end %}
{% end %}
explore.html
{% extends 'base.html' %}
{% block title %}
explore page
{% end %}
{% block content %}
{% for img in images %}
<img src="{{ img }} " width="333">
{% end %}
{% end %}
post.html
{% extends 'base.html' %}
{% block title %}
post page
{% end %}
{% block content %}
<img src="{{ static_url("images/{}.jpg".format(post_id)) }} " width=666">
{% end %}
upload.html
{% extends 'base.html' %}
{% block title %}
list post page
{% end %}
{% block content %}
{#entype 设置编码格式 上传文件:multipart/form-data#}
<form action="/upload" enctype="multipart/form-data" method="post">
<input type="file" name="newimg">
<input type="submit">
</form>
{% end %}
signup.html
{% extends 'base.html' %}
{% block title %}
signup page
{% end %}
{% block content %}
<div class="">
{% if msg %}
{{ msg }}
{% end %}
</div>
<form action="/signup" enctype="multipart/form-data" method="post">
<div class="form-group">
Username
<input autofocus="" class="form-control" id="id_username" maxlength="150" name="username" type="text" required="">
</div>
<div class="form-group">
Email
<input class="form-control" id="id_email" name="email" type="email" required="">
</div>
<div class="form-group">
Password
<input class="form-control" id="id_password1" name="password1" type="password" required="">
</div>
<div class="form-group">
Password confirmation
<input class="form-control" id="id_password2" name="password2" type="password" required="">
</div>
<button class="btn btn-default">注册</button>
<div class="text-center help-text">
已有账号请 <a href="/login">登录</a>
</div>
</form>
{% end %}
login.html
{% extends 'base.html' %}
{% block title %}
login page
{% end %}
{% block content %}
{% if error %}
{{ error }}
{% end %}
<div class="">
<form action="/login?next={{ nextname }}" method="post" enctype="multipart/form-data">
<div class="form-group">
Username
<input autofocus="" class="form-control" id="id_username" maxlength="254" name="username" type="text"
required="">
</div>
<div class="form-group">
Password
<input class="form-control" id="id_password" name="password" type="password" required="">
</div>
<button class="">Login</button>
<div>
还没有账号 需要<a href="/signup">注册</a>一个
</div>
</form>
</div>
{% end %}
auth.py
import tornado.web
from utils.account import authenticate
from .main import AuthBaseHandler
class LoginHandler(AuthBaseHandler):
"""
登录
"""
def get(self, *args, **kwargs):
if self.current_user: # 如果已经登录 访问/login自动跳转到/
self.redirect('/')
next = self.get_argument('next', '/') # 注意是/ 没有next (从logout跳转过来)就跳转到首页
self.render('login.html', nextname=next, error=None)
def post(self, *args, **kwargs):
username = self.get_argument('username')
password = self.get_argument('password')
next = self.get_argument('next', '/')
passed = authenticate(username, password)
if passed:
self.session.set('tudo_user_info', username)
self.redirect(next)
else:
self.render('login.html', nextname=next, error='用户名或密码错误')
class LogoutHandler(AuthBaseHandler):
@tornado.web.authenticated
def get(self, *args, **kwargs):
self.session.set('tudo_user_info', '')
self.redirect('/login') # 直接跳转到 login 没有next 默认是/
class SignupHandler(AuthBaseHandler):
def get(self, *args, **kwargs):
self.render('signup.html')
def post(self, *args, **kwargs):
username = self.get_argument('username', 'no')
email = self.get_argument('email', 'no')
password1 = self.get_argument('password1', 'no')
password2 = self.get_argument('password2', 'no')
# 用户名 密码不能为空 两次密码需一致
if username and password1 and password1:
if password1 != password2:
self.write({'msg': '两次输入的密码不匹配'})
else:
pass
account.py
import hashlib
USER_DATA = {
'name': 'wu',
'password': hashlib.md5('123qwe'.encode('utf8')).hexdigest(),
}
def authenticate(username, password):
if username and password:
hash_pw = hashlib.md5(password.encode()).hexdigest()
if username == USER_DATA['name'] and hash_pw == USER_DATA['password']:
return True
return False
数据库迁移代码
db.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
HOSTNAME = '127.0.0.1'
PORT = '3306'
DATABASE = 'mydb'
USERNAME = 'admin'
PASSWORD = 'Root110qwe'
DB_URI = 'mysql+pymysql://{}:{}@{}:{}/{}?charset=utf8'.format(
USERNAME,
PASSWORD,
HOSTNAME,
PORT,
DATABASE
)
engin = create_engine(DB_URI)
DBSession = sessionmaker(bind=engin)
Base = declarative_base(engin)
if __name__ == '__main__':
connection = engin.connect()
result = connection.execute('select 1')
print(result.fetchone())
account.py
from sqlalchemy import (Column, Integer, String, DateTime)
from .db import Base
from datetime import datetime
class Users(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(50), unique=True, nullable=False)
password = Column(String(50), nullable=False)
created = Column(DateTime, default=datetime.now)
def __repr__(self):
return '<User(#{}:{})>'.format(self.id, self.name)
if __name__ == '__main__':
Base.metadata.create_all()
目录结构
tornado2项目
├── alembic
│ ├── env.py
│ ├── pycache
│ │ └── env.cpython-35.pyc
│ ├── README
│ ├── script.py.mako
│ └── versions
│ ├── 084b87695f74_create_user_table.py
│ ├── cf918abbb966_add_created_for_users.py
│ └── pycache
│ ├── 084b87695f74_create_user_table.cpython-35.pyc
│ ├── 16354e8f04e7_create_user_table.cpython-35.pyc
│ └── cf918abbb966_add_created_for_users.cpython-35.pyc
├── alembic.ini
├── app.py
├── handlers
│ ├── auth.py
│ ├── init.py
│ ├── main.py
│ └── pycache
│ ├── auth.cpython-35.pyc
│ ├── init.cpython-35.pyc
│ └── main.cpython-35.pyc
├── models
│ ├── account.py
│ ├── db.py
│ ├── init.py
│ └── pycache
│ ├── account.cpython-35.pyc
│ ├── db.cpython-35.pyc
│ └── init.cpython-35.pyc
├── static
│ ├── images
│ │ ├── 1.jpg
│ │ ├── 2.jpg
│ │ ├── 3.jpg
│ │ └── 4.jpg
│ └── uploads
│ ├── 1106620.jpg
│ ├── 1106622.jpg
│ ├── 1178645.jpg
│ └── thumbs
│ ├── 1106620_200x200.jpg
│ ├── 1106622_200x200.jpg
│ ├── 1178645_200x200.jpg
│ └── 200200
│ └── 2_200200.jpg
├── template
│ ├── base.html
│ ├── explore.html
│ ├── index.html
│ ├── login.html
│ ├── post.html
│ ├── signup.html
│ └── upload.html
└── utils
├── account.py
├── init.py
├── photo.py
└── pycache
├── account.cpython-35.pyc
├── init.cpython-35.pyc
└── photo.cpython-35.pyc