本博文将一步步带领你实现抽屉官网的各种功能:包括登陆、注册、发送邮箱验证码、登陆验证码、页面登陆验证、发布文章、上传图片、form验证、点赞、评论、文章分页处理以及基于tornado的后端和ajax的前端数据处理。
转载请注明出处http://www.cnblogs.com/wanghzh/p/5806514.html
抽屉官网:http://dig.chouti.com/
一、配置(settings)
1
2
3
4
5
6
7
|
settings = { 'template_path' : 'views' , #模板文件路径 'static_path' : 'statics' , #静态文件路径 'static_url_prefix' : '/statics/' , #静态文件前缀 'autoreload' : True , 'ui_methods' : mt } |
二、路由配置
1
2
3
4
5
6
7
8
9
10
|
application = tornado.web.Application([ (r "/index" , home.IndexHandler), #主页 (r "/check_code" , account.CheckCodeHandler), #验证码 (r "/send_msg" , account.SendMsgHandler), #邮箱验证码 (r "/register" , account.RegisterHandler), #注册 (r "/login" , account.LoginHandler), #登陆 (r "/upload_image" , home.UploadImageHandler), #上传图片 (r "/comment" , home.CommentHandler), #评论 (r "/favor" , home.FavorHandler), #点赞 ], * * settings) |
三、文件夹分类
下面我们将根据上图文件目录由上到下做一一分析:
四、准备工作
本项目所有前端反馈均是通过BaseResponse类实现的:
1
2
3
4
5
6
7
8
|
class BaseResponse: def __init__( self ): self .status = False #状态信息,是否注册成功,是否登陆成功,是否点赞成功、是否评论成功等 self .code = StatusCodeEnum.Success self .data = None #前端需要展示的数据 self .summary = None #错误信息 self .message = {} #字典类型的错误信息 |
本文参考了大量前端和后端基础知识,从入门到精通的链接如下:
前端:
后端:
数据库:
缓存:
五、core:业务处理类handler需要继承的父类
1
2
3
4
5
6
7
8
9
|
import tornado.web from backend.session.session import SessionFactory class BaseRequestHandler(tornado.web.RequestHandler): def initialize( self ): self .session = SessionFactory.get_session_obj( self ) |
六、form:用于form验证的文件,这是一个自定义的tornado form验证模块
fields:包含字符串、邮箱、数字、checkbox、文件类型验证
forms:核心验证处理,返回验证是否成功self._valid_status、成功后的数据提self._value_dict、错误信息self._error_dict
class Field: def __init__(self): self.is_valid = False self.name = None self.value = None self.error = None def match(self, name, value): self.name = name if not self.required: self.is_valid = True self.value = value else: if not value: if self.custom_error_dict.get('required', None): self.error = self.custom_error_dict['required'] else: self.error = "%s is required" % name else: ret = re.match(self.REGULAR, value) if ret: self.is_valid = True self.value = value else: if self.custom_error_dict.get('valid', None): self.error = self.custom_error_dict['valid'] else: self.error = "%s is invalid" % name
class StringField(Field): REGULAR = "^.*$" def __init__(self, custom_error_dict=None, required=True): self.custom_error_dict = {} # {'required': 'IP不能为空', 'valid': 'IP格式错误'} if custom_error_dict: self.custom_error_dict.update(custom_error_dict) self.required = required super(StringField, self).__init__()
class IPField(Field): REGULAR = "^(25[0-5]|2[0-4]d|[0-1]?d?d)(.(25[0-5]|2[0-4]d|[0-1]?d?d)){3}$" def __init__(self, custom_error_dict=None, required=True): self.custom_error_dict = {} # {'required': 'IP不能为空', 'valid': 'IP格式错误'} if custom_error_dict: self.custom_error_dict.update(custom_error_dict) self.required = required super(IPField, self).__init__()
class EmailField(Field): REGULAR = "^w+([-+.']w+)*@w+([-.]w+)*.w+([-.]w+)*$" def __init__(self, custom_error_dict=None, required=True): self.custom_error_dict = {} # {'required': 'IP不能为空', 'valid': 'IP格式错误'} if custom_error_dict: self.custom_error_dict.update(custom_error_dict) self.required = required super(EmailField, self).__init__()
class IntegerField(Field): REGULAR = "^d+$" def __init__(self, custom_error_dict=None, required=True): self.custom_error_dict = {} # {'required': 'IP不能为空', 'valid': 'IP格式错误'} if custom_error_dict: self.custom_error_dict.update(custom_error_dict) self.required = required super(IntegerField, self).__init__()
class CheckBoxField(Field): REGULAR = "^d+$" def __init__(self, custom_error_dict=None, required=True): self.custom_error_dict = {} # {'required': 'IP不能为空', 'valid': 'IP格式错误'} if custom_error_dict: self.custom_error_dict.update(custom_error_dict) self.required = required super(CheckBoxField, self).__init__() def match(self, name, value): self.name = name if not self.required: self.is_valid = True self.value = value else: if not value: if self.custom_error_dict.get('required', None): self.error = self.custom_error_dict['required'] else: self.error = "%s is required" % name else: if isinstance(name, list): self.is_valid = True self.value = value else: if self.custom_error_dict.get('valid', None): self.error = self.custom_error_dict['valid'] else: self.error = "%s is invalid" % name
class FileField(Field): REGULAR = "^(w+.pdf)|(w+.mp3)|(w+.py)$" def __init__(self, custom_error_dict=None, required=True): self.custom_error_dict = {} # {'required': 'IP不能为空', 'valid': 'IP格式错误'} if custom_error_dict: self.custom_error_dict.update(custom_error_dict) self.required = required super(FileField, self).__init__() def match(self, name, file_name_list): flag = True self.name = name if not self.required: self.is_valid = True self.value = file_name_list else: if not file_name_list: if self.custom_error_dict.get('required', None): self.error = self.custom_error_dict['required'] else: self.error = "%s is required" % name flag = False else: for file_name in file_name_list: if not file_name or not file_name.strip(): if self.custom_error_dict.get('required', None): self.error = self.custom_error_dict['required'] else: self.error = "%s is required" % name flag = False break else: ret = re.match(self.REGULAR, file_name) if not ret: if self.custom_error_dict.get('valid', None): self.error = self.custom_error_dict['valid'] else: self.error = "%s is invalid" % name flag = False break self.is_valid = flag def save(self, request, upload_to=""): file_metas = request.files[self.name] for meta in file_metas: file_name = meta['filename'] file_path_name = os.path.join(upload_to, file_name) with open(file_path_name, 'wb') as up: up.write(meta['body']) upload_file_path_list = map(lambda path: os.path.join(upload_to, path), self.value) self.value = list(upload_file_path_list)
核心验证处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
from backend.form import fields class BaseForm: def __init__( self ): self ._value_dict = {} #数据字典 self ._error_dict = {} #错误信息字典 self ._valid_status = True #是否验证成功 def valid( self , handler): for field_name, field_obj in self .__dict__.items(): if field_name.startswith( '_' ): #过滤私有字段 continue if type (field_obj) = = fields.CheckBoxField: #checkbox处理 post_value = handler.get_arguments(field_name, None ) elif type (field_obj) = = fields.FileField: #文件处理 post_value = [] file_list = handler.request.files.get(field_name, []) for file_item in file_list: post_value.append(file_item[ 'filename' ]) else : post_value = handler.get_argument(field_name, None ) field_obj.match(field_name, post_value) #匹配 if field_obj.is_valid: #如果验证成功 self ._value_dict[field_name] = field_obj.value #提取数据 else : self ._error_dict[field_name] = field_obj.error #错误信息 self ._valid_status = False return self ._valid_status #返回是否验证成功 |
七、如何应用上述form验证模块:
以注册为例:
前端:
<div class="header"> <span>注册</span> <div class="dialog-close" onclick="CloseDialog('#accountDialog');">X</div> </div> <div class="content"> <div style="padding: 0 70px"> <div class="tips"> <span>输入注册信息</span> </div> <div id="register_error_summary" class="error-msg"> </div> <div class="inp"> <input name="username" type="text" placeholder="请输入用户名" /> </div> <div class="inp"> <input name="email" id="email" type="text" placeholder="请输入邮箱" /> </div> <div class="inp"> <input name="email_code" class="email-code" type="text" placeholder="请输入邮箱验证码" /> <a id="fetch_code" class="fetch-code" href="javascript:void(0);">获取验证码</a> </div> <div class="inp"> <input name="password" type="password" placeholder="请输入密码" /> </div> <div class="inp"> <div class="submit" onclick="SubmitRegister(this);"> <span>注册</span> <span class="hide"> <img src="/statics/images/loader.gif" style="height: 16px; 16px"> <span>正在注册</span> </span> </div> </div> </div> </div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
/* 点击注册按钮 */ function SubmitRegister(ths){ $( '#register_error_summary' ).empty(); $( '#model_register .inp .error' ).remove(); $(ths).children( ':eq(0)' ).addClass( 'hide' ); $(ths).addClass( 'not-allow' ).children( ':eq(1)' ).removeClass( 'hide' ); var post_dict = {}; $( '#model_register input' ).each( function (){ post_dict[$( this ).attr( "name" )] = $( this ).val(); #将所有input标签内容提取出来,以对应name为key,值为value放入post_dict字典 }); $.ajax({ url: '/register' , #提交的url type: 'POST' , #提交方式 data: post_dict, #提交数据 dataType: 'json' , #数据格式 success: function (arg){ if (arg.status){ window.location.href = '/index' ; #验证成功跳转至主页 } else { $.each(arg.message, function (k,v){ //<span class="error">s</span> var tag = document.createElement( 'span' ); #验证失败创建标签 tag.className = 'error' ; tag.innerText = v; #存入错误信息 $( '#model_register input[name="' + k + '"]' ).after(tag); #将标签加入html中 }) } } }); $(ths).removeClass( 'not-allow' ).children( ':eq(1)' ).addClass( 'hide' ); $(ths).children( ':eq(0)' ).removeClass( 'hide' ); } |
后台处理:
首先需要编写RegisterForm:
1
2
3
4
5
6
7
8
9
|
class RegisterForm(BaseForm): #需要继承上面的form验证核心处理类 def __init__( self ): #初始化每一个input标签的name self .username = StringField() #input标签name=对应类型的类 self .email = EmailField() self .password = StringField() self .email_code = StringField() super (RegisterForm, self ).__init__() |
后台RegisterHandler:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
class RegisterHandler(BaseRequestHandler): def post( self , * args, * * kwargs): rep = BaseResponse() #总的返回前端的类,包含是否注册成功的状态、错误信息 form = account.RegisterForm() #实例化RegisterForm if form.valid( self ): #调用baseform核心验证处理函数valid,返回是否验证成功 current_date = datetime.datetime.now() limit_day = current_date - datetime.timedelta(minutes = 1 ) conn = ORM.session() #获取数据库session对象<br> #查看验证码是否过期 is_valid_code = conn.query(ORM.SendMsg). filter (ORM.SendMsg.email = = form._value_dict[ 'email' ], ORM.SendMsg.code = = form._value_dict[ 'email_code' ], ORM.SendMsg.ctime > limit_day).count() if not is_valid_code: rep.message[ 'email_code' ] = '邮箱验证码不正确或过期' self .write(json.dumps(rep.__dict__)) return has_exists_email = conn.query(ORM.UserInfo). filter (ORM.UserInfo.email = = form._value_dict[ 'email' ]).count() #邮箱是否存在 if has_exists_email: rep.message[ 'email' ] = '邮箱已经存在' self .write(json.dumps(rep.__dict__)) return has_exists_username = conn.query(ORM.UserInfo). filter ( ORM.UserInfo.username = = form._value_dict[ 'username' ]).count() #用户名是否存在 if has_exists_username: rep.message[ 'email' ] = '用户名已经存在' self .write(json.dumps(rep.__dict__)) return <br> #按数据库表的列订制form._value_dict |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
form._value_dict[ 'ctime' ] = current_date form._value_dict.pop( 'email_code' ) obj = ORM.UserInfo( * * form._value_dict) conn.add(obj)<br> conn.flush() conn.refresh(obj) #将自增id也提取出来 user_info_dict = { 'nid' : obj.nid, 'email' : obj.email, 'username' : obj.username} conn.query(ORM.SendMsg).filter_by(email = form._value_dict[ 'email' ]).delete() #删除本次邮箱验证码 conn.commit() conn.close() self .session[ 'is_login' ] = True #注册成功后定义登陆成功 self .session[ 'user_info' ] = user_info_dict 用户信息写入session rep.status = True else : rep.message = form._error_dict #错误信息 self .write(json.dumps(rep.__dict__)) #返回给前端,前端ajax success接收并处理,在前端页面展示 |
八、session ,本session是基于tornado的自定义session
1.应用工厂方法模式定义session保存的位置,用户只需在配置文件修改即可
1
2
3
4
5
6
7
8
9
|
class SessionFactory: @staticmethod def get_session_obj(handler): obj = None if config.SESSION_TYPE = = "cache" : #缓存 obj = CacheSession(handler) elif config.SESSION_TYPE = = "memcached" : #memcached |
1
|
obj = MemcachedSession(handler) <br> elif config.SESSION_TYPE = = "redis" : #radis<br> obj = RedisSession(handler) <br> return obj |
2.缓存session
class CacheSession: session_container = {} session_id = "__sessionId__" def __init__(self, handler): self.handler = handler client_random_str = handler.get_cookie(CacheSession.session_id, None) if client_random_str and client_random_str in CacheSession.session_container: self.random_str = client_random_str else: self.random_str = create_session_id() CacheSession.session_container[self.random_str] = {} expires_time = time.time() + config.SESSION_EXPIRES handler.set_cookie(CacheSession.session_id, self.random_str, expires=expires_time) def __getitem__(self, key): ret = CacheSession.session_container[self.random_str].get(key, None) return ret def __setitem__(self, key, value): CacheSession.session_container[self.random_str][key] = value def __delitem__(self, key): if key in CacheSession.session_container[self.random_str]: del CacheSession.session_container[self.random_str][key]
3.memcache session
import memcache conn = memcache.Client(['192.168.11.119:12000'], debug=True, cache_cas=True) class MemcachedSession: session_id = "__sessionId__" def __init__(self, handler): self.handler = handler # 从客户端获取随机字符串 client_random_str = handler.get_cookie(CacheSession.session_id, None) # 如果从客户端获取到了随机字符串 # if client_random_str and conn.get(client_random_str): self.random_str = client_random_str else: self.random_str = create_session_id() conn.set(self.random_str, json.dumps({}), config.SESSION_EXPIRES) #CacheSession.session_container[self.random_str] = {} conn.set(self.random_str, conn.get(self.random_str), config.SESSION_EXPIRES) expires_time = time.time() + config.SESSION_EXPIRES handler.set_cookie(MemcachedSession.session_id, self.random_str, expires=expires_time) def __getitem__(self, key): # ret = CacheSession.session_container[self.random_str].get(key, None) ret = conn.get(self.random_str) ret_dict = json.loads(ret) result = ret_dict.get(key,None) return result def __setitem__(self, key, value): ret = conn.get(self.random_str) ret_dict = json.loads(ret) ret_dict[key] = value conn.set(self.random_str, json.dumps(ret_dict), config.SESSION_EXPIRES) # CacheSession.session_container[self.random_str][key] = value def __delitem__(self, key): ret = conn.get(self.random_str) ret_dict = json.loads(ret) del ret_dict[key] conn.set(self.random_str, json.dumps(ret_dict), config.SESSION_EXPIRES)
4.radis session
import redis pool = redis.ConnectionPool(host='192.168.11.119', port=6379) r = redis.Redis(connection_pool=pool) class RedisSession: session_id = "__sessionId__" def __init__(self, handler): self.handler = handler # 从客户端获取随机字符串 client_random_str = handler.get_cookie(CacheSession.session_id, None) # 如果从客户端获取到了随机字符串 if client_random_str and r.exists(client_random_str): self.random_str = client_random_str else: self.random_str = create_session_id() r.hset(self.random_str,None,None) # conn.set(self.random_str, json.dumps({}), config.SESSION_EXPIRES) # CacheSession.session_container[self.random_str] = {} r.expire(self.random_str, config.SESSION_EXPIRES) # conn.set(self.random_str, conn.get(self.random_str), config.SESSION_EXPIRES) expires_time = time.time() + config.SESSION_EXPIRES handler.set_cookie(RedisSession.session_id, self.random_str, expires=expires_time) def __getitem__(self, key): # ret = CacheSession.session_container[self.random_str].get(key, None) result = r.hget(self.random_str,key) if result: ret_str = str(result, encoding='utf-8') try: result = json.loads(ret_str) except: result = ret_str return result else: return result def __setitem__(self, key, value): if type(value) == dict: r.hset(self.random_str, key, json.dumps(value)) else: r.hset(self.random_str, key, value) # CacheSession.session_container[self.random_str][key] = value def __delitem__(self, key): r.hdel(self.random_str,key)
九、验证码:
注:验证码需要依赖session。
#!/usr/bin/env python #coding:utf-8 import random from PIL import Image, ImageDraw, ImageFont, ImageFilter _letter_cases = "abcdefghjkmnpqrstuvwxy" # 小写字母,去除可能干扰的i,l,o,z _upper_cases = _letter_cases.upper() # 大写字母 _numbers = ''.join(map(str, range(3, 10))) # 数字 init_chars = ''.join((_letter_cases, _upper_cases, _numbers)) def create_validate_code(size=(120, 30), chars=init_chars, img_type="GIF", mode="RGB", bg_color=(255, 255, 255), fg_color=(0, 0, 255), font_size=18, font_type="Monaco.ttf", length=4, draw_lines=True, n_line=(1, 2), draw_points=True, point_chance = 2): ''' @todo: 生成验证码图片 @param size: 图片的大小,格式(宽,高),默认为(120, 30) @param chars: 允许的字符集合,格式字符串 @param img_type: 图片保存的格式,默认为GIF,可选的为GIF,JPEG,TIFF,PNG @param mode: 图片模式,默认为RGB @param bg_color: 背景颜色,默认为白色 @param fg_color: 前景色,验证码字符颜色,默认为蓝色#0000FF @param font_size: 验证码字体大小 @param font_type: 验证码字体,默认为 ae_AlArabiya.ttf @param length: 验证码字符个数 @param draw_lines: 是否划干扰线 @param n_lines: 干扰线的条数范围,格式元组,默认为(1, 2),只有draw_lines为True时有效 @param draw_points: 是否画干扰点 @param point_chance: 干扰点出现的概率,大小范围[0, 100] @return: [0]: PIL Image实例 @return: [1]: 验证码图片中的字符串 ''' width, height = size # 宽, 高 img = Image.new(mode, size, bg_color) # 创建图形 draw = ImageDraw.Draw(img) # 创建画笔 def get_chars(): '''生成给定长度的字符串,返回列表格式''' return random.sample(chars, length) def create_lines(): '''绘制干扰线''' line_num = random.randint(*n_line) # 干扰线条数 for i in range(line_num): # 起始点 begin = (random.randint(0, size[0]), random.randint(0, size[1])) #结束点 end = (random.randint(0, size[0]), random.randint(0, size[1])) draw.line([begin, end], fill=(0, 0, 0)) def create_points(): '''绘制干扰点''' chance = min(100, max(0, int(point_chance))) # 大小限制在[0, 100] for w in range(width): for h in range(height): tmp = random.randint(0, 100) if tmp > 100 - chance: draw.point((w, h), fill=(0, 0, 0)) def create_strs(): '''绘制验证码字符''' c_chars = get_chars() strs = ' %s ' % ' '.join(c_chars) # 每个字符前后以空格隔开 font = ImageFont.truetype(font_type, font_size) font_width, font_height = font.getsize(strs) draw.text(((width - font_width) / 3, (height - font_height) / 3), strs, font=font, fill=fg_color) return ''.join(c_chars) if draw_lines: create_lines() if draw_points: create_points() strs = create_strs() # 图形扭曲参数 params = [1 - float(random.randint(1, 2)) / 100, 0, 0, 0, 1 - float(random.randint(1, 10)) / 100, float(random.randint(1, 2)) / 500, 0.001, float(random.randint(1, 2)) / 500 ] img = img.transform(size, Image.PERSPECTIVE, params) # 创建扭曲 img = img.filter(ImageFilter.EDGE_ENHANCE_MORE) # 滤镜,边界加强(阈值更大) return img, strs
1
2
3
4
5
6
7
|
class CheckCodeHandler(BaseRequestHandler): def get( self , * args, * * kwargs): stream = io.BytesIO() img, code = check_code.create_validate_code() img.save(stream, "png" ) self .session[ "CheckCode" ] = code #利用session保存验证码 self .write(stream.getvalue()) |
路由配置:(r"/check_code", account.CheckCodeHandler),
前端:
1
|
< img class="check-img" src="/check_code" alt="验证码" onclick="ChangeCode(this);"> |
js:
1
2
3
4
5
|
<script> function ChangeCode(ths) { ths.src += '?' ; } </script> |
十、发送邮箱验证码
前端:
1
2
3
4
|
< div class="inp"> < input class="regiter-temp" name="code" class="email-code" type="text" placeholder="请输入邮箱验证码" /> < a onclick="SendCode(this);" class="fetch-code" >获取验证码</ a > </ div > |
js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
function SendCode(ths) { // var email = $(ths).prev().val(); var email = $( '#email' ).val(); $.ajax({ url: '/send_code' , type: 'POST' , data: {em: email}, success: function (arg) { console.log(arg); }, error: function () { } }); } |
路由配置:
1
|
(r "/send_code" , account.SendCodeHandler), |
后台handler:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class SendCodeHandler(BaseRequestHandler): def post( self , * args, * * kwargs): ret = { 'status' : True , "data" : " ", " error ": " "} email = self .get_argument( 'em' , None ) if email: code = commons.random_code() #获取随机验证码 message.email([email,], code) #发送验证码到邮箱 conn = chouti_orm.session() #获取数据库session对象 obj = chouti_orm.SendCode(email = email,code = code, stime = datetime.datetime.now()) #写入数据库 conn.add(obj) conn.commit() else : ret[ 'status' ] = False ret[ 'error' ] = "邮箱格式错误" self .write(json.dumps(ret)) |
发送邮箱验证码函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import smtplib from email.mime.text import MIMEText from email.utils import formataddr def email(email_list, content, subject = "抽屉新热榜-用户注册" ): #email_list邮件列表,content邮件内容,subject:发送标题 msg = MIMEText(content, 'plain' , 'utf-8' ) msg[ 'From' ] = formataddr([ "抽屉新热榜" , 'wptawy@126.com' ]) msg[ 'Subject' ] = subject server = smtplib.SMTP( "smtp.126.com" , 25 ) 邮箱引擎 server.login( "wptawy@126.com" , "JUEmimima" ) #邮箱名,密码 server.sendmail( 'wptawy@126.com' , email_list, msg.as_string()) server.quit() |
十一、邮箱验证码之过期时间
案例:
html:
1
|
< a id="fetch_code" class="fetch-code" href="javascript:void(0);">获取验证码</ a > |
js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
function BindSendMsg(){ $( "#fetch_code" ).click( function (){ $( '#register_error_summary' ).empty(); #清空错误信息 var email = $( '#email' ).val(); #获取邮箱地址 if (email.trim().length == 0){ #判断是否输入邮箱 $( '#register_error_summary' ).text( '请输入注册邮箱' ); return ; } if ($( this ).hasClass( 'sending' )){ #判断是否已经发送 return ; } var ths = $( this ); var time = 60; 设置倒计时时间为60s $.ajax({ url: "/send_msg" , type: 'POST' , data: {email: email}, dataType: 'json' , success: function (arg){ if (!arg.status){ #是否发送成功 $( '#register_error_summary' ).text(arg.summary); #不成功显示错误信息 } else { ths.addClass( 'sending' ); #成功后显示已发送状态 var interval = setInterval( function (){ ths.text( "已发送(" + time + ")" ); time -= 1; #定时器每运行一次,计数器减1 if (time <= 0){ clearInterval(interval); #一分钟过完,清除定时器 ths.removeClass( 'sending' ); # 移除已发送状态 ths.text( "获取验证码" ); # 恢复未发送状态 } }, 1000); #定时器每隔1s运行一次 } } }); }); } |
附:一些常见模块:
1.随机验证码获取:
def random_code(): code = '' for i in range(4): current = random.randrange(0,4) if current != i: temp = chr(random.randint(65,90)) else: temp = random.randint(0,9) code += str(temp) return code
2.md5加密
def generate_md5(value): r = str(time.time()) obj = hashlib.md5(r.encode('utf-8')) obj.update(value.encode('utf-8')) return obj.hexdigest()
十二、分页功能,该功能是基于tornado的自定义分页功能
案例:
前端:
1
2
3
|
< div class="pagination"> {% raw str_page%} #展示原生html </ div > |
url配置:
1
|
(r "/index/(?P<page>d*)" , IndexHandler), |
分页模块:
#!/usr/bin/env python # -*- coding:utf-8 -*- class Pagination: def __init__(self, current_page, all_item): try: page = int(current_page) except: page = 1 if page < 1: page = 1 all_pager, c = divmod(all_item, 10) if c > 0: all_pager += 1 self.current_page = page self.all_pager = all_pager @property def start(self): return (self.current_page - 1) * 10 @property def end(self): return self.current_page * 10 def string_pager(self, base_url="/index/"): list_page = [] if self.all_pager < 11: s = 1 t = self.all_pager + 1 else: # 总页数大于11 if self.current_page < 6: s = 1 t = 12 else: if (self.current_page + 5) < self.all_pager: s = self.current_page - 5 t = self.current_page + 5 + 1 else: s = self.all_pager - 11 t = self.all_pager + 1 # 首页 # first = '<a href="%s1">首页</a>' % base_url # list_page.append(first) # 上一页 # 当前页 page if self.current_page == 1: prev = '<a href="javascript:void(0);">上一页</a>' else: prev = '<a href="%s%s">上一页</a>' % (base_url, self.current_page - 1,) list_page.append(prev) for p in range(s, t): # 1-11 if p == self.current_page: temp = '<a class="active" href="%s%s">%s</a>' % (base_url,p, p) else: temp = '<a href="%s%s">%s</a>' % (base_url,p, p) list_page.append(temp) if self.current_page == self.all_pager: nex = '<a href="javascript:void(0);">下一页</a>' else: nex = '<a href="%s%s">下一页</a>' % (base_url, self.current_page + 1,) list_page.append(nex) # 尾页 last = '<a href="%s%s">尾页</a>' % (base_url, self.all_pager,) list_page.append(last) # 跳转 jump = """<input type='text' /><a onclick="Jump('%s',this);">GO</a>""" % ('/index/', ) script = """<script> function Jump(baseUrl,ths){ var val = ths.previousElementSibling.value; if(val.trim().length>0){ location.href = baseUrl + val; } } </script>""" list_page.append(jump) list_page.append(script) str_page = "".join(list_page) return str_page
注:Pagination实例化接收两个参数:当前页current_page、新闻总数all_item,其中current_page一般通过url分组元素直接获取
后台handler:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class IndexHandler(BaseRequestHandler): def get( self , page = 1 ): conn = ORM.session() #获取数据库session对象 all_count = conn.query(ORM.News).count() #计算新闻总数 obj = Pagination(page, all_count) #实例化pagination对象 current_user_id = self .session[ 'user_info' ][ 'nid' ] if self .session[ 'is_login' ] else 0 #如果登陆获取用户id,否则,用户id=0,下面的查询结果也为空 result = conn.query(ORM.News.nid, ORM.News.title, ORM.News.url, ORM.News.content, ORM.News.ctime, ORM.UserInfo.username, ORM.NewsType.caption, ORM.News.favor_count, ORM.News.comment_count, ORM.Favor.nid.label( 'has_favor' )).join(ORM.NewsType, isouter = True ).join(ORM.UserInfo, isouter = True ).join(ORM.Favor, and_(ORM.Favor.user_info_id = = current_user_id, ORM.News.nid = = ORM.Favor.news_id), isouter = True )[obj.start: 10 ] #从每页开始向下取10条,即每页显示10条新闻 conn.close() str_page = obj.string_pager( '/index/' ) 获取页码的字符串格式html self .render( 'home/index.html' , str_page = str_page, news_list = result) |
十三、页面登陆验证(装饰器方式实现)
1.普通登陆验证
1
2
3
4
5
6
7
8
|
def auth_login_redirect(func): def inner( self , * args, * * kwargs): if not self .session[ 'is_login' ]: self .redirect(config.LOGIN_URL) return func( self , * args, * * kwargs) return inner |
2.ajax提交数据的登陆验证
1
2
3
4
5
6
7
8
9
10
|
def auth_login_json(func): def inner( self , * args, * * kwargs): if not self .session[ 'is_login' ]: rep = BaseResponse() rep.summary = "auth failed" self .write(json.dumps(rep.__dict__)) return func( self , * args, * * kwargs) return inner |
十四、上传文件
前端:
1
2
3
4
5
6
7
8
|
< form style="display: inline-block" id="upload_img_form" name="form" action="/upload_image" method="POST" enctype="multipart/form-data" > < a id="fakeFile" class="fake-file"> < span >上传图片</ span > < input type="file" name="img" onchange="UploadImage(this);"/> < input type="text" name="url" class="hide" /> </ a > < iframe id='upload_img_iframe' name='upload_img_iframe' src="" class="hide"></ iframe > </ form > |
js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
function UploadImage(ths){ document.getElementById( 'upload_img_iframe' ).onload = UploadImageComplete; #页面加载完成后执行UploadImageComplete函数 document.getElementById( 'upload_img_form' ).target = 'upload_img_iframe' ; 设置form提交到iframe document.getElementById( 'upload_img_form' ).submit(); #提交到iframe } /* 上传图片之后回掉函数 */ function UploadImageComplete(){ var origin = $( "#upload_img_iframe" ).contents().find( "body" ).text();#获取图片数据 var obj = JSON.parse(origin); #转换成JavaScript对象 if (obj.status){ #如果上传成功 var img = document.createElement( 'img' ); #创建img标签 img.src = obj.data; 图片地址 img.style.width = "200px" ; img.style.height = "180px" ; $( "#upload_img_form" ).append(img);添加图片 $( '#fakeFile' ).addClass( 'hide' ); $( '#reUploadImage' ).removeClass( 'hide' ); $( '#fakeFile' ).find( 'input[type="text"]' ).val(obj.data);#保存图片地址到隐藏的input标签中 } else { alert(obj.summary); #否则显示错误信息 } } |
路由配置:
1
|
(r "/upload_image" , home.UploadImageHandler), |
后台handler:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class UploadImageHandler(BaseRequestHandler): @decrator .auth_login_json #上传前登陆验证 def post( self , * args, * * kwargs): rep = BaseResponse() 前端回应类 try : file_metas = self .request.files[ "img" ] 获取图片列表 for meta in file_metas: file_name = meta[ 'filename' ] #图片名 file_path = os.path.join( 'statics' , 'upload' , commons.generate_md5(file_name)) #保存地址 with open (file_path, 'wb' ) as up: up.write(meta[ 'body' ]) #在服务器写入图片 rep.status = True #写入成功 rep.data = file_path except Exception as ex: rep.summary = str (ex) #错误信息 self .write(json.dumps(rep.__dict__)) #反馈给前端 |
十五、文章发布
1.定义需要验证的form类
1
2
3
4
5
6
7
8
9
|
class IndexForm(BaseForm): def __init__( self ): self .title = StringField() #标题 self .content = StringField(required = False ) 内容 self .url = StringField(required = False ) 图片url self .news_type_id = IntegerField() 新闻类型 super (IndexForm, self ).__init__() |
2.前端html:
1
2
3
4
|
< div class="f4"> < a class="submit right" id="submit_img">提交</ a > < span class="error-msg right"></ span > </ div > |
3.js
function BindPublishSubmit(){
$('#submit_link,#submit_text,#submit_img').click(function(){
// 获取输入内容并提交
var container = $(this).parent().parent();
var post_dict = {};
container.find('input[type="text"],textarea').each(function(){
post_dict[$(this).attr('name')] =$(this).val();
});
post_dict['news_type_id'] = container.find('.news-type .active').attr('value');
$.ajax({
url: '/index',
type: 'POST',
data: post_dict,
dataType: 'json',
success: function (arg) {
if(arg.status){
window.location.href = '/index';
}else{
console.log(arg);
}
}
})
});
}
后台handler:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@decrator .auth_login_json #发布前登陆验证 def post( self , * args, * * kwargs): rep = BaseResponse() form = IndexForm() #实例化Indexform if form.valid( self ): # title,content,href,news_type,user_info_id <br> #写入数据库 input_dict = copy.deepcopy(form._value_dict) input_dict[ 'ctime' ] = datetime.datetime.now() input_dict[ 'user_info_id' ] = self .session[ 'user_info' ][ 'nid' ] conn = ORM.session() conn.add(ORM.News( * * input_dict)) conn.commit() conn.close() rep.status = True #写入成功 else : rep.message = form._error_dict #错误信息 self .write(json.dumps(rep.__dict__)) |
十六、点赞功能
前端html:
1
2
3
4
5
6
7
|
< a href="javascript:void(0);" class="digg-a" title="推荐" onclick="DoFavor(this,{{item[0]}});"> {% if item[9] %} #是否已点过赞 < span class="hand-icon icon-digg active"></ span > {% else %} < span class="hand-icon icon-digg"></ span > {% end %} < b id="favor_count_{{item[0]}}">{{item[7]}}</ b > #点赞数量< br ></ a > |
js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
function DoFavor(ths, nid) { if ($( '#action_nav' ).attr( 'is-login' ) = = 'true' ){ #登陆状态才能点赞 $.ajax({ url: '/favor' , type : 'POST' , data: {news_id: nid}, #携带当前新闻id dataType: 'json' , success: function(arg){ if (arg.status){ var $favorCount = $( '#favor_count_' + nid); var c = parseInt($favorCount.text()); #获取当前点赞数量 if (arg.code = = 2301 ){ #当前用户以前没点过赞 $favorCount.text(c + 1 ); #点赞数量加1 $(ths).find( 'span' ).addClass( 'active' ); 已经点过赞变深颜色 AddFavorAnimation(ths); #+1动态效果 } else if (arg.code = = 2302 ){ #该用户以前对该新闻点过赞 $favorCount.text(c - 1 ); #点赞数量减1 $(ths).find( 'span' ).removeClass( 'active' ); 取消点赞颜色变浅 MinusFavorAnimation(ths); #-1动态效果 } else { } } else { } } }) } else { $( '#accountDialog' ).removeClass( 'hide' ); $( '.shadow' ).removeClass( 'hide' ); } } |
后台handler:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
class FavorHandler(BaseRequestHandler): @decrator .auth_login_json #点赞前登陆验证 def post( self , * args, * * kwargs): rep = BaseResponse() news_id = self .get_argument( 'news_id' , None ) #获取新闻id if not news_id: rep.summary = "新闻ID不能为空." else : user_info_id = self .session[ 'user_info' ][ 'nid' ] #获取当前用户id conn = ORM.session()<br> #查询当前用户是否对该新闻点过赞 has_favor = conn.query(ORM.Favor). filter (ORM.Favor.user_info_id = = user_info_id, ORM.Favor.news_id = = news_id).count() if has_favor: #如果已经点过赞,删除数据库点赞表favor点赞数据,更新数据库新闻表news点赞数量-1 conn.query(ORM.Favor). filter (ORM.Favor.user_info_id = = user_info_id, ORM.Favor.news_id = = news_id).delete() conn.query(ORM.News). filter (ORM.News.nid = = news_id).update( { "favor_count" : ORM.News.favor_count - 1 }, synchronize_session = "evaluate" ) rep.code = StatusCodeEnum.FavorMinus #返回已经点过赞的状态吗 else : conn.add(ORM.Favor(user_info_id = user_info_id, news_id = news_id, ctime = datetime.datetime.now())) conn.query(ORM.News). filter (ORM.News.nid = = news_id).update( { "favor_count" : ORM.News.favor_count + 1 }, synchronize_session = "evaluate" ) rep.code = StatusCodeEnum.FavorPlus conn.commit() conn.close() rep.status = True #操作成功 self .write(json.dumps(rep.__dict__)) #返回给前端 |
点赞+1和-1动态效果js:
/* 点赞+1效果 */ function AddFavorAnimation(ths){ var offsetTop = -10; var offsetLeft = 20; var fontSize = 24; var opacity = 1; var tag = document.createElement('i'); tag.innerText = "+1"; tag.style.position = 'absolute'; tag.style.top = offsetTop + 'px'; tag.style.left = offsetLeft + 'px'; tag.style.fontSize = fontSize + "px"; tag.style.color = "#5cb85c"; $(ths).append(tag); var addInterval = setInterval(function(){ fontSize += 5; offsetTop -= 15; offsetLeft += 5; opacity -= 0.1; tag.style.top = offsetTop+ 'px'; tag.style.left = offsetLeft+ 'px'; tag.style.fontSize = fontSize + 'px'; tag.style.opacity = opacity; if(opacity <= 0.5){ tag.remove(); clearInterval(addInterval); } },40) }
/* 点赞-1效果 */ function MinusFavorAnimation(ths){ var offsetTop = -10; var offsetLeft = 20; var fontSize = 24; var opacity = 1; var tag = document.createElement('i'); tag.innerText = "-1"; tag.style.position = 'absolute'; tag.style.top = offsetTop + 'px'; tag.style.left = offsetLeft + 'px'; tag.style.fontSize = fontSize + "px"; tag.style.color = "#787878"; $(ths).append(tag); var addInterval = setInterval(function(){ fontSize += 5; offsetTop -= 15; offsetLeft += 5 ; opacity -= 0.1; tag.style.top = offsetTop+ 'px'; tag.style.left = offsetLeft+ 'px'; tag.style.fontSize = fontSize + 'px'; tag.style.opacity = opacity; if(opacity <= 0.5){ tag.remove(); clearInterval(addInterval); } },40) }
十七、评论功能
案例:
前端html:
1
2
3
4
|
< div class="box-r"> < a href="javascript:void(0);" class="pub-icons add-pub-btn add-pub-btn-unvalid" onclick="DoComment({{item[0]}})">评论</ a > #携带新闻id < a href="javascript:void(0);" class="loading-ico loading-ico-top pub-loading-top hide">发布中...</ a > </ div > |
1
|
{% raw tree(comment_tree) %} |
创建评论树字典函数:
def build_tree(comment_list): comment_dic = collections.OrderedDict() for comment_obj in comment_list: if comment_obj[2] is None: # 如果是根评论,添加到comment_dic[评论对象] = {} comment_dic[comment_obj] = collections.OrderedDict() else: # 如果是回复的评论,则需要在 comment_dic 中找到其回复的评论 tree_search(comment_dic, comment_obj) return comment_dic
递归生成评论树函数:
def tree_search(d_dic, comment_obj): # 在comment_dic中一个一个的寻找其回复的评论 # 检查当前评论的 reply_id 和 comment_dic中已有评论的nid是否相同, # 如果相同,表示就是回复的此信息 # 如果不同,则需要去 comment_dic 的所有子元素中寻找,一直找,如果一系列中未找,则继续向下找 for k, v_dic in d_dic.items(): # 找回复的评论,将自己添加到其对应的字典中,例如: {评论一: {回复一:{},回复二:{}}} if k[0] == comment_obj[2]: d_dic[k][comment_obj] = collections.OrderedDict() return else: # 在当前第一个跟元素中递归的去寻找父亲 tree_search(d_dic[k], comment_obj)
前端调用uimethod,生成评论html:
TEMP1 = """ <li class="items" style='padding:8px 0 0 %spx;'> <span class="folder" id='comment_folder_%s'> <div class="comment-L comment-L-top"> <a href="#" class="icons zhan-ico"></a> <a href="/user/moyujian/submitted/1"> <img src="/statics/images/1.jpg"> </a> </div> <div class="comment-R comment-R-top" style="background-color: rgb(246, 246, 246);"> <div class="pp"> <a class="name" href="/user/moyujian/submitted/1">%s</a> <span class="p3">%s</span> <span class="into-time into-time-top">%s</span> </div> <div class="comment-line-top"> <div class="comment-state"> <a class="ding" href="javascript:void(0);"> <b>顶</b> <span class="ding-num">[0]</span> </a> <a class="cai" href="javascript:void(0);"> <b>踩</b> <span class="cai-num">[0]</span> </a> <span class="line-huifu">|</span> <a class="see-a jubao" href="javascript:void(0);">举报</a> <span class="line-huifu">|</span> <a class="see-a huifu-a" href="javascript:void(0);" onclick="reply(%s,%s,'%s')" id='comment_reply_%s' >回复</a> </div> </div> </div> </span> """ def tree(self, comment_dic): html = '' for k, v in comment_dic.items(): html += TEMP1 %(0,k[0], k[3],k[1],k[4],k[7],k[0], k[3],k[0]) html += generate_comment_html(v, 16) html += "</li>" return html def generate_comment_html(sub_comment_dic, margin_left_val): # html = '<ul style="background: url("/statics/images/pinglun_line.gif") 0px -10px no-repeat scroll transparent;margin-left:3px;">' html = '<ul>' for k, v_dic in sub_comment_dic.items(): html += TEMP1 %(margin_left_val,k[0], k[3],k[1],k[4],k[7],k[0], k[3],k[0]) if v_dic: html += generate_comment_html(v_dic, margin_left_val) html += "</li>" html += "</ul>" return html
后台handler:
class CommentHandler(BaseRequestHandler):
def get(self, *args, **kwargs): #展示评论信息
# comment_list需要按照时间从小到大排列
nid = self.get_argument('nid', 0)
conn = ORM.session()
comment_list = conn.query(
ORM.Comment.nid,
ORM.Comment.content,
ORM.Comment.reply_id,
ORM.UserInfo.username,
ORM.Comment.ctime,
ORM.Comment.up,
ORM.Comment.down,
ORM.Comment.news_id
).join(ORM.UserInfo, isouter=True).filter(ORM.Comment.news_id == nid).all()
conn.close()
"""
comment_list = [
(1, '111',None), #评论id,评论内容,回复id,如果是None,则代表回复新闻
(2, '222',None),
(3, '33',None),
(9, '999',5),
(4, '444',2),
(5, '555',1),
(6, '666',4),
(7, '777',2),
(8, '888',4),
]
"""
comment_tree = commons.build_tree(comment_list) #最终传递给前端的是一个字典
self.render('include/comment.html', comment_tree=comment_tree)
@decrator.auth_login_json #提交评论前做登陆验证
def post(self, *args, **kwargs): #提交评论信息
rep = BaseResponse() #前端反馈类
form = CommentForm() #评论的form验证
if form.valid(self): #如果验证成功
form._value_dict['ctime'] = datetime.datetime.now() #获取当前时间
conn = ORM.session() #获取数据库session对象
# 将评论写入数据库
obj = ORM.Comment(user_info_id=self.session['user_info']['nid'],
news_id=form._value_dict['news_id'],
reply_id=form._value_dict['reply_id'],
content=form._value_dict['content'],
up=0,
down=0,
ctime=datetime.datetime.now())
conn.add(obj)
conn.flush()
conn.refresh(obj) #同时获取评论的自增id
rep.data = {
'user_info_id': self.session['user_info']['nid'],
'username': self.session['user_info']['username'],
'nid': obj.nid,
'news_id': obj.news_id,
'ctime': obj.ctime.strftime("%Y-%m-%d %H:%M:%S"),
'reply_id': obj.reply_id,
'content': obj.content,
}
#更新数据库的评论数量
conn.query(ORM.News).filter(ORM.News.nid == form._value_dict['news_id']).update(
{"comment_count": ORM.News.comment_count + 1}, synchronize_session="evaluate")
conn.commit()
conn.close()
rep.status = True #评论成功的状态信息
else:
rep.message = form._error_dict #错误信息
print(rep.__dict__)
self.write(json.dumps(rep.__dict__)) #反馈给前端
js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
function DoComment(nid){ var content = $( "#comment_content_" +nid).val(); var reply_id = $( "#reply_id_" +nid).attr( 'target' ); if ($( '#action_nav' ).attr( 'is-login' ) == 'true' ){ #已经登录才发ajax请求 $.ajax({ url: '/comment' , type: 'POST' , data: {content: content, reply_id:reply_id, news_id: nid}, #发送评论内容,回复的评论id,新闻id dataType: 'json' , success: function (arg){ // 获取评论信息,将内容添加到指定位置 console.log(arg); if (arg.status){ $( '#comment_content_' + arg.data.news_id).val( '' ); var a = '<ul><li class="items" style="padding:8px 0 0 16px;"><span class="folder" id="comment_folder_' ; var b = arg.data.nid; var c = '"><div class="comment-L comment-L-top"><a href="#" class="icons zhan-ico"></a><a href="/user/moyujian/submitted/1"><img src="/statics/images/1.jpg"></a></div><div class="comment-R comment-R-top" style=""><div class="pp"><a class="name" href="/user/moyujian/submitted/1">' ; var d = arg.data.username; var e = '</a><span class="p3">' ; var f = arg.data.content; var g= '</span><span class="into-time into-time-top">' ; var h = arg.data.ctime; var i = '</span></div><div class="comment-line-top"><div class="comment-state"><a class="ding" href="javascript:void(0);"><b>顶</b><span class="ding-num">[0]</span></a><a class="cai" href="javascript:void(0);"><b>踩</b><span class="cai-num">[0]</span></a><span class="line-huifu">|</span> <a class="see-a jubao" href="javascript:void(0);">举报</a> <span class="line-huifu">|</span> <a class="see-a huifu-a" href="javascript:void(0);" onclick="' ; var j = "reply( " + arg.data.news_id + " , " +arg.data.nid+" , '"+arg.data.username+"' ) "; var k = '" >回复</a></div></div></div></span></li></ul>'; var tag = a+b+c+d+e+f+g+h+i+j+k; console.log(arg,tag); if (arg.data.reply_id){ $comment_folder = $( '#comment_folder_' + arg.data.reply_id); $comment_folder.after(tag); } else { $( '#comment_list_' +arg.data.news_id).append(tag); } } else { alert( 'error' ); } } }) } else { $( '#accountDialog' ).removeClass( 'hide' ); $( '.shadow' ).removeClass( 'hide' ); } } |
总结:本博客是基于tornado实现的较完整版web开发项目,汇集了form验证、点赞、评论等高级功能,欢迎大家参考并提出相关问题,本人会在看到的第一时间回复,最后,如果您觉得本文对您有参考价值,欢迎推荐,谢谢!