本博文將一步步帶領你實現抽屜官網的各類功能:包括登錄、註冊、發送郵箱驗證碼、登錄驗證碼、頁面登錄驗證、發佈文章、上傳圖片、form驗證、點贊、評論、文章分頁處理以及基於tornado的後端和ajax的前端數據處理。javascript
轉載請註明出處http://www.cnblogs.com/wanghzh/p/5806514.html
css
抽屜官網:http://dig.chouti.com/html
settings = { 'template_path': 'views', #模板文件路徑 'static_path': 'statics', #靜態文件路徑 'static_url_prefix': '/statics/', #靜態文件前綴 'autoreload': True, 'ui_methods': mt }
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類實現的:java
class BaseResponse: def __init__(self): self.status = False #狀態信息,是否註冊成功,是否登錄成功,是否點同意功、是否評論成功等 self.code = StatusCodeEnum.Success self.data = None #前端須要展現的數據 self.summary = None #錯誤信息 self.message = {} #字典類型的錯誤信息
本文參考了大量前端和後端基礎知識,從入門到精通的連接以下:python
前端:mysql
後端:jquery
數據庫:git
緩存:web
import tornado.web from backend.session.session import SessionFactory class BaseRequestHandler(tornado.web.RequestHandler): def initialize(self): self.session = SessionFactory.get_session_obj(self)
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)
核心驗證處理:
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 #返回是否驗證成功
以註冊爲例:
前端:
<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;width: 16px"> <span>正在註冊</span> </span> </div> </div> </div> </div>
/* 點擊註冊按鈕 */ 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:
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:
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對象
#查看驗證碼是否過時 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
#按數據庫表的列訂製form._value_dict
form._value_dict['ctime'] = current_date form._value_dict.pop('email_code') obj = ORM.UserInfo(**form._value_dict) conn.add(obj)
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接收並處理,在前端頁面展現
1.應用工廠方法模式定義session保存的位置,用戶只需在配置文件修改便可
class SessionFactory: @staticmethod def get_session_obj(handler): obj = None if config.SESSION_TYPE == "cache": #緩存 obj = CacheSession(handler) elif config.SESSION_TYPE == "memcached": #memcached
obj = MemcachedSession(handler)
elif config.SESSION_TYPE == "redis": #radis
obj = RedisSession(handler)
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
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),
前端:
<img class="check-img" src="/check_code" alt="驗證碼" onclick="ChangeCode(this);">
js:
<script> function ChangeCode(ths) { ths.src += '?'; } </script>
前端:
<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:
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 () { } }); }
路由配置:
(r"/send_code", account.SendCodeHandler),
後臺handler:
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))
發送郵箱驗證碼函數:
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:
<a id="fetch_code" class="fetch-code" href="javascript:void(0);">獲取驗證碼</a>
js:
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()
案例:
前端:
<div class="pagination"> {% raw str_page%} #展現原生html </div>
url配置:
(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:
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.普通登錄驗證
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提交數據的登錄驗證
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
前端:
<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:
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);#不然顯示錯誤信息 } }
路由配置:
(r"/upload_image", home.UploadImageHandler),
後臺handler:
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類
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:
<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:
@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
#寫入數據庫 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:
<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> #點贊數量
</a>
js:
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:
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()
#查詢當前用戶是否對該新聞點過贊 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:
<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>
{% 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:
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驗證、點贊、評論等高級功能,歡迎你們參考並提出相關問題,本人會在看到的第一時間回覆,最後,若是您以爲本文對您有參考價值,歡迎推薦,謝謝!