数据库查询
重构 session 的使用
- ReuestHandler 的 prepare 和 on_finish 使用
class AuthBaseHandler(tornado.web.RequestHandler, SessionMixin):
def get_current_user(self):
return self.session.get('tudo_user_info')
def prepare(self):
self.db_session = DBSession()
self.orm = HandlerDRM(self.db_session)
def on_finish(self):
self.db_session.close()
- db session 的独立
self.db_session = DBSession()
- 使用 ORM 工具类
相关代码
- main.py
- service.py
- utlis.account ( auth 相关的也可以做类似改进)
/room 的相关改进
- user 的获取
- 数据库的使用
服务器维护
-
自动代码部署(Fabric,git pull)
-
启动管理(重启之后,自动启动 supervisord 和 nginx 等)
作业
改进项目,使得 handler 里面的数据库查询支持更好,上传支持多进程的服务
code
├── tudo
│ ├── alembic
│ │ ├── env.py
│ │ ├── README
│ │ ├── script.py.mako
│ │ └── versions
│ │ ├── b9faebdbef40_add_post_model.py
│ │ ├── d868e3279547_create_users_table.py
│ │ ├── f7cb9fac4444_add_like_model.py
│ ├── alembic.ini
│ ├── app.py
│ ├── deploy
│ │ ├── nginx.conf
│ │ ├── supervisord.conf
│ │ ├── tudo_nginx
│ │ └── tudo_super.conf
│ ├── handlers
│ │ ├── auth.py
│ │ ├── chat.py
│ │ ├── __init__.py
│ │ ├── main.py
│ │ └── service.py
│ ├── models
│ │ ├── account.py
│ │ ├── db.py
│ │ ├── __init__.py
│ ├── static
│ │ ├── css
│ │ │ ├── bootstrap.css
│ │ │ ├── bootstrap.css.map
│ │ │ ├── bootstrap-grid.css
│ │ │ ├── bootstrap-grid.css.map
│ │ │ ├── bootstrap-grid.min.css
│ │ │ ├── bootstrap-grid.min.css.map
│ │ │ ├── bootstrap.min.css
│ │ │ ├── bootstrap.min.css.map
│ │ │ ├── bootstrap-reboot.css
│ │ │ ├── bootstrap-reboot.css.map
│ │ │ ├── bootstrap-reboot.min.css
│ │ │ └── bootstrap-reboot.min.css.map
│ │ ├── font-awesome-4.7.0
│ │ │ ├── css
│ │ │ │ ├── font-awesome.css
│ │ │ │ └── font-awesome.min.css
│ │ │ └── fonts
│ │ │ ├── FontAwesome.otf
│ │ │ ├── fontawesome-webfont.eot
│ │ │ ├── fontawesome-webfont.svg
│ │ │ ├── fontawesome-webfont.ttf
│ │ │ ├── fontawesome-webfont.woff
│ │ │ └── fontawesome-webfont.woff2
│ │ ├── images
│ │ │ ├── 1.jpg
│ │ │ └── error.jpg
│ │ ├── js
│ │ │ ├── bootstrap.bundle.js
│ │ │ ├── bootstrap.bundle.js.map
│ │ │ ├── bootstrap.bundle.min.js
│ │ │ ├── bootstrap.bundle.min.js.map
│ │ │ ├── bootstrap.js
│ │ │ ├── bootstrap.js.map
│ │ │ ├── bootstrap.min.js
│ │ │ ├── bootstrap.min.js.map
│ │ │ ├── chat.js
│ │ │ ├── jquery-3.3.1.slim.min.js
│ │ │ └── popper.min.js
│ │ └── uploads
│ │ ├── 0370e9cf7a9c46e495a7a21e74ec21ef.jpg
│ │ └── thumbs
│ │ ├── 0370e9cf7a9c46e495a7a21e74ec21ef_200x200.jpg
│ ├── templates
│ │ ├── base.html
│ │ ├── error.html
│ │ ├── explore.html
│ │ ├── index.html
│ │ ├── login.html
│ │ ├── message.html
│ │ ├── post.html
│ │ ├── profile.html
│ │ ├── room.html
│ │ ├── signup.html
│ │ └── upload.html
│ └── utils
│ ├── account.py
│ ├── photo.py
│ └── __pycache__
│ ├── account.cpython-35.pyc
│ └── photo.cpython-35.pyc
auth.py
from .main import AuthBaseHandler
import tornado.web
class LoginHandler(AuthBaseHandler):
"""
登录
"""
def get(self, *args, **kwargs):
if self.current_user: # 如果已经登录 访问/login自动跳转到/
self.redirect('/')
nextname = self.get_argument('next', '/') # 注意是/ 没有next (从logout跳转过来)就跳转到首页
self.render('login.html', nextname=nextname, error=None)
def post(self, *args, **kwargs):
username = self.get_argument('username')
password = self.get_argument('password')
nextname = self.get_argument('next', '/')
passed = self.orm.authenticate(username, password) # 登录认证
if passed:
self.session.set('tudo_user_info', username)
# 保存最后登录时间
self.orm.username = username
self.orm.save_last_login()
self.redirect(nextname)
else:
self.render('login.html', nextname=nextname, 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', msg='')
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 password2:
if password1 != password2:
self.render('signup.html', msg='两次输入的密码不一致')
else:
ret = self.orm.register(username, password2, email) # 注册函数
# 注册成功
if ret['msg'] == 'ok':
self.session.set('tudo_user_info', username) # 注册成功后直接设置session(自动登录)
self.redirect('/') # 注册成功后自动访问首页
else:
self.render('signup.html', msg=ret['msg'])
else:
self.render('signup.html', msg={'sign failed'})
chat.py
import tornado.web
import tornado.websocket
import tornado.httpclient
import tornado.escape
from tornado.ioloop import IOLoop
from pycket.session import SessionMixin
from .main import AuthBaseHandler
import uuid
def make_chat(msg, img_url=None):
"""生成一个用来格式化message.html的dict"""
chat = {
"id": str(uuid.uuid4()),
"body": msg,
"img_url": img_url # 注意 展示数据库中的url 传给message.html 的 img的url 是 thumb_url
}
return chat
class RoomHandler(AuthBaseHandler):
"""
聊天室页面
"""
def get(self, *args, **kwargs):
self.render('room.html', messages=ChatSocketHandler.cache)
class ChatSocketHandler(tornado.websocket.WebSocketHandler, SessionMixin):
"""
处理响应websocket连接
"""
waiters = set() # 等待接受信息的用户
cache = [] # 存放历史信息的列表
cache_size = 200 # 消息列表的大小
def get_current_user(self):
return self.session.get('tudo_user_info')
def open(self, *args, **kwargs):
"""新的websocket连接打开时 自动调用此函数"""
print('new connection:%s' % self)
ChatSocketHandler.waiters.add(self)
def on_close(self):
"""websocketa连接关闭时 自动调用此函数"""
print('close connection: %s' % self)
ChatSocketHandler.waiters.remove(self)
@classmethod
def update_cache(cls, message):
"""# 更新历史消息列表 加入新的消息"""
cls.cache.append(message)
if len(cls.cache) > cls.cache_size:
cls.cache = cls.cache[-cls.cache_size:]
@classmethod
def send_updates(cls, chat):
"""# 向所有在线用户发送消息"""
for waiter in cls.waiters:
waiter.write_message(chat)
def on_message(self, message):
"""websocket服务端接收到消息 自动调用此函数"""
print('got message %s' % message) # got message {"body":"2"}
parsed = tornado.escape.json_decode(message) # {"body":"2"}
# 用户发送的消息是一个链接
if parsed['body'] and parsed["body"].startswith("http://"):
url = parsed['body']
client = tornado.httpclient.AsyncHTTPClient() # 异步客户端
# http://127.0.0.1:8000/async?url=http://source.unsplash.com/random
save_api_url = "http://127.0.0.1:8000/async?url={}&username={}&from=room".\
format(url, self.current_user)
# 非阻塞调用内部接口(/async) client.fetch(http://127.0.0.1:8000/async?url=http://source.unsplash.com/random)
IOLoop.current().spawn_callback(client.fetch, save_api_url)
chat = make_chat("user[{}],url({}) is processing".format(self.current_user, url))
chat["html"] = tornado.escape.to_basestring(
self.render_string("message.html", message=chat))
self.write_message(chat) # 消息仅发送给自己
else:
chat = make_chat("user[{}],say: {}".format(self.current_user, parsed['body']))
chat["html"] = tornado.escape.to_basestring(
self.render_string("message.html", message=chat))
ChatSocketHandler.update_cache(chat) # 更新历史消息列表
ChatSocketHandler.send_updates(chat) # 向所有在线用户发送消息
main.py
import tornado.web
from models.db import DBSession
from pycket.session import SessionMixin
from utils.account import HandlerDRM
from utils.photo import ImageSave
class AuthBaseHandler(tornado.web.RequestHandler, SessionMixin):
def get_current_user(self):
return self.session.get('tudo_user_info')
def prepare(self):
self.db_session = DBSession()
self.orm = HandlerDRM(self.db_session, self.current_user)
def on_finish(self):
self.db_session.close()
class IndexHandler(AuthBaseHandler):
"""
首页
"""
@tornado.web.authenticated
def get(self, *args, **kwargs):
post_list = self.orm.get_post_for_user()
self.render('index.html', post_list=post_list)
class ExploreHandler(AuthBaseHandler):
"""
发现页
"""
@tornado.web.authenticated
def get(self, *args, **kwargs):
post_list = self.orm.get_post_all()
self.render('explore.html', post_list=post_list)
class PostHandler(AuthBaseHandler):
"""
详情页
"""
@tornado.web.authenticated
def get(self, post_id):
post = self.orm.get_post(post_id)
if not post:
self.write('post id is not exists')
else:
like_count = self.orm.get_like_count(post)
self.render('post.html', post=post, like_count=like_count)
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')
if img_files:
post_id = 0
for img in img_files:
# 保存的图片的目录 名字
image_saver = ImageSave(self.settings['static_path'], img['filename'])
# 保存图片
image_saver.save_image(img['body'])
# 生成缩略图 ./ static / uploads /thumbs/ 701728_200x200.jpg
image_saver.make_thumbs()
# 保存图片的用户 大图地址 缩略图地址 把url存到数据库
post = self.orm.add_post_for_user(image_saver.image_url, image_saver.thumb_url)
post_id = post.id
self.redirect('post/{}'.format(post_id))
else:
self.write({'msg': 'empty form data'})
class ProfileHandler(AuthBaseHandler):
"""
显示用户上传的图片 和 喜欢的图片列表
"""
@tornado.web.authenticated
def get(self, *args, **kwargs):
username = self.get_argument('name', None) # url传参
if not username: # name=空
username = self.current_user
self.orm.username = username
user = self.orm.get_user()
if user:
like_post_list = self.orm.get_like_post(user)
self.render('profile.html', user=user, like_post_list=like_post_list)
else:
self.render('error.html')
service.py
from datetime import datetime
import tornado.web
import tornado.httpclient
import tornado.gen
import tornado.escape
from .main import AuthBaseHandler
from utils.photo import ImageSave
from .chat import ChatSocketHandler, make_chat
class URLSaveHandler(AuthBaseHandler):
"""保存指定url的图片 同步方法"""
@tornado.web.authenticated
def get(self, *args, **kwargs):
url = self.get_argument('url', None)
response = self.fetch_image(url) # 获取指定url的图片
if not response.body: # 数据被封装在响应对象的body属性中
self.write('empty data')
return
image_saver = ImageSave(self.settings['static_path'], 'x.jpg')
image_saver.save_image(response.body) # body 就是图片数据 保存图片
image_saver.make_thumbs() # 做缩略图
# 添加到数据库,拿到 post 实例
post = self.orm.add_post_for_user(image_saver.image_url, image_saver.thumb_url)
print("-- {} -end fetch:#{}".format(datetime.now(), post.id))
self.redirect('/post/{}'.format(post.id)) # 跳转到 post 页面
def fetch_image(self, url):
"""获取指定url的图片"""
client = tornado.httpclient.HTTPClient() # 获取同步操作对象
print("-- {} -going to fetch:{}".format(datetime.now(), url))
response = client.fetch(url) # 获取url对应的内容 得到响应对象
return response
class AsyncURLSaveHandler(AuthBaseHandler):
"""保存指定url的图片 异步方法"""
# @tornado.web.authenticated
@tornado.gen.coroutine
def get(self, *args, **kwargs):
url = self.get_argument('url', None)
print(url)
response = yield self.fetch_image(url) # 获取指定url的图片
if not response.body: # 数据被封装在响应对象的body属性中
self.write('empty data')
return
image_saver = ImageSave(self.settings['static_path'], 'x.jpg')
image_saver.save_image(response.body) # 保存图片
image_saver.make_thumbs() # 缩略图
# http://127.0.0.1: 8000/async?url=http://source.unsplash.com/random&username=2&from=room
username = self.get_argument("username", None) # user可以在url中获取
is_from_room = self.get_argument('from',"")=='room'
if username and is_from_room:
self.orm.username = username
post = self.orm.add_post_for_user(image_saver.image_url, image_saver.thumb_url) # 添加到数据库
print("-- {} -end fetch:#{}".format(datetime.now(), post.id))
# 注意 展示数据库中的url 传给message.html 的 img的url 是 thumb_url
chat = make_chat("username[{}] post:127.0.0.1:8000/{}".\
format(username, post.id), image_saver.thumb_url)
chat["html"] = tornado.escape.to_basestring(
self.render_string("message.html", message=chat))
ChatSocketHandler.update_cache(chat) # 将抓取到的图片更新到历史消息列表
ChatSocketHandler.send_updates(chat) # 向所有在线用户发送图片消息
else:
self.write('username error')
@tornado.gen.coroutine
def fetch_image(self, url):
"""获取指定url的图片"""
client = tornado.httpclient.AsyncHTTPClient() # 获取异步操作对象
print("-- {} -going to fetch:{}".format(datetime.now(), url))
# yield tornado.gen.sleep(6)
response = yield client.fetch(url) # 获取url对应的内容 得到响应对象
return response
models/account.py
from sqlalchemy import (Column, Integer, String, DateTime, ForeignKey)
from sqlalchemy import exists
from sqlalchemy.orm import relationship
from sqlalchemy.sql import exists
from .db import Base
from datetime import datetime
# session = DBSession()
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)
email = Column(String(50))
last_login = Column(DateTime)
def __repr__(self):
return '<User(#{}:{})>'.format(self.id, self.name)
# 增加用户到数据库中
@classmethod
def add_user(cls, username, password, db_session, email=''):
user = Users(name=username, password=password, email=email, last_login=datetime.now())
db_session.add(user)
db_session.commit()
# 查询用户username对应的密码
@classmethod
def get_passwd(cls, username, db_session):
user = db_session.query(Users).filter_by(name=username).first()
# 如果用户存在 返回密码
if user:
return user.password
# 用户不存在 返回空
else:
return ''
# 判断用户是否存在于数据库中
@classmethod
def is_exists(cls, username, db_session):
ret = db_session.query(exists().where(Users.name == username)).scalar()
return ret
class Posts(Base):
"""
用户图片信息
"""
__tablename__ = 'posts'
id = Column(Integer, primary_key=True, autoincrement=True)
image_url = Column(String(100)) # 大图路径
thumb_url = Column(String(100)) # 缩略图路径
user_id = Column(Integer, ForeignKey('users.id'))
# Post 有user属性 存放Users对象 , Users有posts属性 存放Posts对象(多对一)
user = relationship('Users', backref='posts', uselist=False, cascade='all')
class Like(Base):
"""
记录用户喜欢的图片信息
"""
__tablename__ = 'likes'
user_id = Column(Integer, ForeignKey('users.id'), nullable=False, primary_key=True)
post_id = Column(Integer, ForeignKey('posts.id'), nullable=False, primary_key=True)
if __name__ == '__main__':
Base.metadata.create_all()
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 = 'tudo'
USERNAME = 'admin'
PASSWORD = 'Root110qwe'
db_uri = 'mysql+pymysql://{}:{}@{}:{}/{}?charset=utf8'.format(
USERNAME,
PASSWORD,
HOSTNAME,
PORT,
DATABASE
)
engin = create_engine(db_uri)
DBSession = sessionmaker(engin)
Base = declarative_base(engin)
if __name__ == '__main__':
connection = engin.connect()
result = connection.execute('select 1')
print(result.fetchone())
chat.js
// 浏览器在页面加载完成后调用此方法
$(document).ready(function () {
// 创建空console对象,避免JS报错 //兼容Firefox/IE使用console.log
if (!window.console)
window.console = {};
if (!window.console.log)
window.console.log = function () {
};
$("#messageform").on("submit", function () { // 点击提交时执行
newMessage($(this)); // 发送新消息给服务器
return false;
});
$("#messageform").on("keypress", function (e) { // 回车提交时执行
if (e.keyCode == 13) {
newMessage($(this)); // 发送新消息给服务器
return false;
}
});
$("#message").select(); // 选中输入框
updater.start(); // 开始 WebSocket 连接服务器 获取信息 展示在页面上
});
// 发送新消息给服务器
function newMessage(form) {
var message = form.formToDict(); // 将提取的数据转化成字典 {body: "2"}
updater.socket.send(JSON.stringify(message)); // 向服务器发送json形式的新的消息 {"body":"2"}
$("input[name='body']").val("").select(); //清空并选中输入框
}
// 将提取的数据转化成字典
jQuery.fn.formToDict = function () {
var fields = this.serializeArray(); // [{name: "body", value: "2"}]
var json = {};
for (var i = 0; i < fields.length; i++) {
json[fields[i].name] = fields[i].value; // json["body"]="2"
}
if (json.next)
delete json.next;
return json; // {body: "2"}
};
var updater = {
socket: null,
start: function () {
var url = "ws://" + location.host + "/ws"; //ws://127.0.0.1:8000/ws
updater.socket = new WebSocket(url); // 初始化 WebSocket 客户端与服务器建立连接
// 收到服务器数据event.data后的回调函数
updater.socket.onmessage = function (event) {
updater.showMessage(JSON.parse(event.data));//信息展示在页面上
}
},
showMessage: function (message) {
var existing = $("#m" + message.id);
if (existing.length > 0)
return;
var node = $(message.html);
// node.hide();
$("#inbox").append(node); // 添加消息 DIV 到页面
// node.slideDown();
}
};
base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ static_url('css/bootstrap.css') }}">
<link rel="stylesheet" href="{{ static_url('font-awesome-4.7.0/css/font-awesome.css') }}">
<title>{% block title %}base{% end %}</title>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">
<i class="fa fa-camera"></i>
Tudo 图片
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="/">首页<span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/explore">发现</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
用户中心
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="/profile">个人信息</a>
<a class="dropdown-item" href="#">收藏</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">Something else here</a>
</div>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="/logout">{{ current_user }}登出</a>
</li>
</ul>
<a class="btn btn-info" href="/upload">
<i class="fa fa-upload"></i>
上传
</a>
</div>
</nav>
<div class="container">
{% block content %}
base
{% end %}
</div>
<script src="{{ static_url('js/jquery-3.3.1.slim.min.js') }}"></script>
<script src="{{ static_url('js/popper.min.js') }}"></script>
<script src="{{ static_url('js/bootstrap.js') }}"></script>
{% block extra_scripts %}{% end %}
</body>
</html>
error.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<img src="{{ static_url('images/error.jpg') }}">
<h3>您访问的页面不存在</h3>
</body>
</html>
explore.html
{% extends 'base.html' %}
{% block title %}
explore page
{% end %}
{% block content %}
{% for post in post_list %}
<a href="post/{{ post.id }}">
<img src="{{ static_url(post.thumb_url) }}">
</a>
{% end %}
{% end %}
index.html
{% extends 'base.html' %}
{% block title %}
index page
{% end %}
{% block content %}
<div class="row">
<div class="col col-md-8">
{% for post in post_list %}
<a href="post/{{ post.id }}">
<img src="{{ static_url(post.image_url) }}" class="img-fluid" width="666">
</a>
{% end %}
</div>
<div class="d-none d-sm-none d-md-block col-md-4">
4col
</div>
</div>
{% end %}
login.html
{% extends 'base.html' %}
{% block title %}
login page
{% end %}
{% block content %}
<div class="row justify-content-center">
<div class="col-sm-6">
{% if error %}
{{ error }}
{% end %}
<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="btn btn-outline-primary btn-lg btn-block">登录</button>
<br>
<div>
还没有账号 需要<a href="/signup">注册</a>一个
</div>
</form>
</div>
</div>
{% end %}
message.html
<div class="message" id="m{{ message["id"] }}">
{% module linkify(message["body"]) %}
<br>
{% if message["img_url"] %}
<img src="{{ static_url(message["img_url"]) }}">
{% end %}
</div>
post.html
{% extends 'base.html' %}
{% block title %}
post page
{% end %}
{% block content %}
<div class="row">
<div class="col-sm-8">
<img src="{{ static_url(post.image_url)}}" width="560">
</div>
<div class="col-sm-4">
upload by {{ post.user.name }}<br>
{% if like_count %}
<i class="fa fa-heart text-danger"></i> {{ like_count }}
{% end %}
</div>
</div>
{% end %}
profile.html
{% extends 'base.html' %}
{% block title %}
profile page
{% end %}
{% block content %}
<div class="card">
<div class="card-header"><h3>{{ user.name }}用户上传</h3></div>
<div class="row">
{% for post in user.posts %}
<div class="col-6 col-sm-3">
<a href="/post/{{ post.id }}">
<img src="{{ static_url(post.thumb_url) }}" class="img-fluid">
</a>
</div>
{% end %}
</div>
</div>
<div class="card">
<div class="card-header"><h3>用户喜欢</h3></div>
<div class="row">
{% for post in like_post_list %}
<div class="col-6 col-sm-3">
<a href="/post/{{ post.id }}">
<img src="{{ static_url(post.thumb_url) }}" class="img-fluid">
</a>
</div>
{% end%}
</div>
</div>
{% end %}
room.html
{% extends 'base.html' %}
{% block title %}
room page
{% end %}
{% block content %}
<div id="body">
<div id="inbox">
{% for message in messages %}
{% include "message.html" %}
{% end %}
</div>
<div id="input">
<form action="#" method="post" id="messageform">
<table>
<tr>
<td><input name="body" id="message" style="width:500px"></td>
<td style="padding-left:5px">
<input type="submit" value="提交">
<input type="hidden" name="next" value="{{ request.path }}">
</td>
</tr>
</table>
</form>
</div>
</div>
{% end %}
{% block extra_scripts %}
<script src="{{ static_url('js/chat.js') }}" type="text/javascript"></script>
{% end %}
signup.html
{% extends 'base.html' %}
{% block title %}
signup page
{% end %}
{% block content %}
<div class="row justify-content-center">
<div class="col-sm-6">
{% if msg %}
{{ msg }}
{% end %}
<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-outline-primary btn-lg btn-block">立即注册</button>
<br>
<div class="text-center help-text">
已有账号请 <a href="/login">登录</a>
</div>
</form>
</div>
</div>
{% end %}
upload.html
{% extends base.html %}
{% block title %}
upload
{% end %}
{% block content %}
<form action="/upload" method="post" enctype="multipart/form-data">
<div class="form-group">
<input type="file" name="newimg" class="form-control-file" id="exampleFormControlFile1">
<br>
<input type="submit">
</div>
</form>
{% end %}
utils/account.py
import hashlib
from models.account import Users, Posts, Like
from datetime import datetime
class HandlerDRM:
"""和RequestHandler配合使用的数据库连接操作工具类 """
def __init__(self, db_session, username):
"""
初始化参数
:param db_session: 由RequestHandler来调用 初始化 传入 关闭 的session
:param username: pycket记录的用户名
"""
self.db_session = db_session
self.username = username
def hashed(self, password):
return hashlib.md5(password.encode('utf8')).hexdigest()
def authenticate(self, username, password):
"""
登录认证
:param username:
:param password:
:return:
"""
if username and password:
# 获取数据库中username对应的密码
user_passwd = Users.get_passwd(username, self.db_session)
# 如果用户存在 密码匹配
if user_passwd and user_passwd == self.hashed(password):
return True
return False
def register(self, username, password, email):
"""
注册
:param username:
:param password:
:param email:
:return:
"""
# 查看用户是否存在于数据库中
if Users.is_exists(username, self.db_session):
return {'msg': '用户已存在'}
hash_passwd = self.hashed(password)
# 添加用户到数据库
Users.add_user(username, hash_passwd, self.db_session, email)
return {'msg': 'ok'}
def add_post_for_user(self, image_url, thumb_url):
"""
保存用户上传的图片信息 图片和特定的用户建立关系(这张图片是由这个用户传上来的)
:param image_url:
:param thumb_url:
:return:
"""
user = self.get_user()
post = Posts(image_url=image_url, thumb_url=thumb_url, user=user)
self.db_session.add(post)
self.db_session.commit()
return post
def get_post_for_user(self):
"""
获取用户上传的图片信息
:return:
"""
user = self.get_user()
if user:
return user.posts
else:
return []
def get_post_all(self):
"""
获取所有图片信息(降序)
:return:
"""
post_list = self.db_session.query(Posts).order_by(Posts.id.desc()).all()
return post_list
def get_post(self, post_id):
"""
获取用户的特定图片
:param post_id:
:return:
"""
post = self.db_session.query(Posts).filter_by(id=post_id).first()
return post
def get_user(self):
"""
查询用户user
:return:
"""
user = self.db_session.query(Users).filter_by(name=self.username).first()
return user
def get_like_post(self, user):
"""
查询用户user喜欢的图片的post对象
:param user: Users的实例
:return: Posts的实例
"""
# 去除用户自己上传的图片
posts = self.db_session.query(Posts).filter(Like.user_id == user.id, Posts.id == Like.post_id,
Posts.user_id != user.id).all()
return posts
def get_like_count(self, post):
"""
获取喜欢的(♥)数量
:param post:
:return:
"""
like_count = self.db_session.query(Like).filter_by(post_id=post.id).count()
return like_count
def save_last_login(self):
"""
# 保存用户username最后登录时间
:return:
"""
t = datetime.now()
print("user {} login at {}".format(self.username, t))
self.db_session.query(Users).filter_by(name=self.username).update({Users.last_login: t})
self.db_session.commit()
photo.py
import os
import glob
from PIL import Image
import uuid
class ImageSave(object):
"""
保存图片
"""
upload_dir = 'uploads'
thumb_dir = 'thumbs'
size = (200, 200)
def __init__(self, static_path, name):
self.static_path = static_path # static
self.oldname = name # 701728.jpg
self.newname = self.gen_new_name()
def gen_new_name(self):
"""
生成随机唯一的字符串(文件名)
:return:
"""
_, ext = os.path.splitext(self.oldname)
return uuid.uuid4().hex + ext
# 图片的url: uploads / 1202983.jpg
@property
def image_url(self):
return os.path.join(self.upload_dir, self.newname)
# 图片上传后的保存路径 static / uploads / 1202983.jpg
@property
def upload_path(self):
return os.path.join(self.static_path, self.image_url)
# 缩略图的url:uploads / thumbs / 1202983_200x200.jpg
@property
def thumb_url(self):
filename, ext = os.path.splitext(self.newname)
thumb_name = '{}_{}x{}{}'.format(filename, *self.size, ext)
return os.path.join(self.upload_dir, self.thumb_dir, thumb_name)
# 保存图片
def save_image(self, content):
with open(self.upload_path, 'wb') as f:
f.write(content)
# 生成缩略图
def make_thumbs(self):
im = Image.open(self.upload_path)
im.thumbnail(self.size)
# static / uploads / thumbs / 1202983.jpg
save_thumb_to_ = os.path.join(self.static_path, self.thumb_url)
im.save(save_thumb_to_, 'JPEG')
app.py
import tornado.web
import tornado.options
import tornado.ioloop
from tornado.options import define, options
from handlers import main,auth,chat,service
define(name='port', default='8000', type=int, help='run 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'/profile', main.ProfileHandler),
(r'/login', auth.LoginHandler),
(r'/logout', auth.LogoutHandler),
(r'/signup', auth.SignupHandler),
(r'/room', chat.RoomHandler),
(r'/ws', chat.ChatSocketHandler),
(r'/save', service.URLSaveHandler),
(r'/async', service.AsyncURLSaveHandler),
]
settings = dict(
debug=True,
template_path='templates',
static_path='static',
login_url='/login',
cookie_secret='bZJc2sWbQLKos6GkHn/VB9oXwQt8S0R0kRvJ5/xJ89E=',
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()