08 | 小组相关功能开发
重写authenticated装饰器
之前完成了登录注册功能,有一个新的问题也随之而来
因为采用的是jwt的模式,所以没用到seesion,那么如果有一个页面需要用户登录怎么办呢
其实如果使用原来的seesion模式的话,tornado是提供了一个装饰器的
就是authenticated,在tornaodo.web里面
这个装饰器逻辑其实比较简单,就是去获取self._current_user
如果获取不到就直接跳转到登录页面,前提是你需要在setting里设置了login_url
那么如何重写的主要思路是:通过从header中获取token 赋值给self._current_user,最后通过协程的方式调用请求方法
apps/utils/mxform_decorators
import functools import jwt from apps.users.models import User def authenticated_async(method): @functools.wraps(method) async def wrapper(self, *args, **kwargs): tsessionid = self.request.headers.get("tsessionid", None) if tsessionid: try: send_data = jwt.decode(tsessionid, self.settings["secret_key"], leeway=self.settings["jwt_expire"], options={"verify_exp": True}) user_id = send_data["id"] #从数据库中获取到user并设置给_current_user try: user = await self.application.objects.get(User, id=user_id) self._current_user = user #此处很关键 await method(self, *args, **kwargs) except User.DoesNotExist as e: self.set_status(401) except jwt.ExpiredSignatureError as e: self.set_status(401) else: self.set_status(401) self.finish({}) return wrapper
首先先获取jwt_token,如果获取不到,抛异常,如果获取到,再用我们的密钥进行解密,
解密失败,抛异常,如果解密成功,取出里面的user_id,并且通过协程的方式去数据库取出该用户并且设置到self._current_user里
注意前面有下划线,最后运行被装饰的函数,注意因为装饰的函数是协程,所以需要用await的方式来调用
还有一个小点需要注意一下
token = jwt.decode(jwt_token, self.settings['secret_key'], algorithm='HS256', leeway=7*24*3600, options={'verify_exp':True})
这里的leeway代表的是过期时间,因为前端的jwt里面设置了exp字段为datetime.utcnow(),相当于在exp字段后加上这个设置的时间,然后options代表验证这个字段。
总结:
1.jwt.decode()这个方法的过期时间的设置,需要leeway和options
2.取到用户后一定要设置self._current_user
3.被装饰的方法如果是协程的话需要用await来调用
创建小组功能开发
现在论坛需要新增一个功能,就是创建小组
首先,form = CommunityGroupForm(self.request.body_arguments)
这个里面因为有文件存在,所以不能用之前的form_json.# 另如果前端传递过来的是json数据。需要用self.request.body获取并且需要decode(utf8)然后json.loads()
上传的文件存放在self.request.files属性里面,格式需要看前端怎么放
文件名字在file[filename]里,文件内容file[body]
最后就是aiofiles里,github里面的开源项目。
文件路径需要用,项目路径加上文件名称,而项目路径可以用
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
使用 aiofiles 异步写入文件
安装
pip install aiofiles
github
https://github.com/Tinche/aiofiles
数据表如下
apps/community/models.py
from datetime import datetime from peewee import * from MxForm.models import BaseModel from apps.users.models import User class CommunityGroup(BaseModel): creator = ForeignKeyField(User, verbose_name="创建者") name = CharField(max_length=100, null=True, verbose_name="名称") category = CharField(max_length=20, verbose_name="分类", null=True) front_image = CharField(max_length=200, null=True, verbose_name="封面图") desc = TextField(verbose_name="简介") notice = TextField(verbose_name="公告") #小组的信息 member_nums = IntegerField(default=0, verbose_name="成员数") post_nums = IntegerField(default=0, verbose_name="帖子数") @classmethod def extend(cls): return cls.select(cls, User.id, User.nick_name).join(User) HANDLE_STATUS = ( ("agree", "同意"), ("refuse", "拒绝") ) class CommunityGroupMember(BaseModel): user = ForeignKeyField(User, verbose_name="用户") community = ForeignKeyField(CommunityGroup, verbose_name="社区") status = CharField(choices=HANDLE_STATUS, max_length=10, null=True, verbose_name="处理状态") handle_msg = CharField(max_length=200, null=True, verbose_name="处理内容") apply_reason = CharField(max_length=200, verbose_name="申请理由") handle_time = DateTimeField(default=datetime.now(), verbose_name="加入时间")
apps/community/forms.py 创建小组参数验证
from wtforms_tornado import Form from wtforms import StringField, TextAreaField, IntegerField from wtforms.validators import DataRequired, Regexp, AnyOf, Length class CommunityGroupForm(Form): name = StringField("名称", validators=[DataRequired("请输入小组名称")]) category = StringField("类别", validators=[AnyOf(values=["教育同盟", "同城交易", "程序设计", "生活兴趣"])]) desc = TextAreaField("简介", validators=[DataRequired(message="请输入简介")]) notice = TextAreaField("简介", validators=[DataRequired(message="请输入公告")])
apps/community/handler.py 创建小组处理器
import os import uuid import json from tornado.web import authenticated import aiofiles from playhouse.shortcuts import model_to_dict from MxForm.handler import * from apps.utils.mxform_decorators import authenticated_async from apps.community.forms import * from apps.community.models import * from apps.utils.util_func import json_serial class GroupHandler(BaseHandler): async def get(self, *args, **kwargs): pass @authenticated_async async def post(self, *args, **kwargs): re_data = {} #不能使用jsonform group_form = CommunityGroupForm(self.request.body_arguments) if group_form.validate(): #自己完成图片字段的验证 files_meta = self.request.files.get("front_image", None) if not files_meta: self.set_status(400) re_data["front_image"] = "请上传图片" else: #完成图片保存并将值设置给对应的记录 #通过aiofiles写文件 #1. 文件名 new_filename = "" for meta in files_meta: filename = meta["filename"] new_filename = "{uuid}_{filename}".format(uuid=uuid.uuid1(), filename=filename) file_path = os.path.join(self.settings["MEDIA_ROOT"], new_filename) async with aiofiles.open(file_path, 'wb') as f: await f.write(meta['body']) group = await self.application.objects.create(CommunityGroup, creator = self.current_user, name=group_form.name.data, category=group_form.category.data, desc=group_form.desc.data, notice=group_form.notice.data, front_image = new_filename) re_data["id"] = group.id else: self.set_status(400) for field in group_form.errors: re_data[field] = group_form.errors[field][0] self.write(re_data)
apps/community/urls.py 创建小组 url
from tornado.web import url from apps.community.handler import * urlpattern = ( url("/groups/", GroupHandler),)
MxForm/urls.py 添加到 跟路由
from MxForm.settings import settings from apps.users import urls as user_urls from apps.community import urls as community_urls from tornado.web import url from tornado.web import StaticFileHandler urlpattern = [ (url("/media/(.*)", StaticFileHandler, {'path':settings["MEDIA_ROOT"]})) ] urlpattern += user_urls.urlpattern urlpattern += community_urls.urlpattern
联调
查询数据库
小组列表页功能开发
现在来完善小组的列表展示页面
主要的思路就是从数据库取出所有数据(使用的是懒加载,只有在执行execute的时候才会查询数据库),然后根据条件进行筛选,然后返回给前端
apps/community/handler.py
class GroupHandler(BaseHandler): async def get(self, *args, **kwargs): #获取小组列表 re_data = [] community_query = CommunityGroup.extend() # 需要自己组装外键的语句,不然会报错 #根据类别进行过滤 c = self.get_argument("c", None) if c: community_query = community_query.filter(CommunityGroup.category==c) #根据参数进行排序 order = self.get_argument("o", None) if order: if order == "new": community_query = community_query.order_by(CommunityGroup.add_time.desc()) elif order == "hot": community_query = community_query.order_by(CommunityGroup.member_nums.desc()) limit = self.get_argument("limit", None) if limit: community_query = community_query.limit(int(limit)) groups = await self.application.objects.execute(community_query) for group in groups: group_dict = model_to_dict(group) group_dict["front_image"] = "{}/media/{}/".format(self.settings["SITE_URL"], group_dict["front_image"]) re_data.append(group_dict) self.finish(json.dumps(re_data, default=json_serial))
逻辑虽然简单但是里面坑还是不少的
1.首先是peewee的坑,在生成有外键的的表的查询语句时,需要自己手动组装查询外键的语句,可以放在类里面
例如:
class CommunityGroup(BaseModel): creator = ForeignKeyField(User, verbose_name="创建者") name = CharField(max_length=100, null=True, verbose_name="名称") category = CharField(max_length=20, verbose_name="分类", null=True) front_image = CharField(max_length=200, null=True, verbose_name="封面图") desc = TextField(verbose_name="简介") notice = TextField(verbose_name="公告") #小组的信息 member_nums = IntegerField(default=0, verbose_name="成员数") post_nums = IntegerField(default=0, verbose_name="帖子数") @classmethod def extend(cls): return cls.select(cls, User.id, User.nick_name).join(User)
2.传递少量数据可以手动生成json,但是大量数据可以用model_to_dict方法
from playhouse.shortcuts import model_to_dict group_dict = model_to_dict(group)
3.json不能dumps datetime和date类型
需要额外写个函数解决,利用obj.isoformat函数
apps/utils/util_func.py
from datetime import datetime, date def json_time(obj): if isinstance(obj , (date,datetime)): return obj.isoformat() else: raise TypeError
前端联调
申请加入小组功能开发
apps/community/forms.py 参数校验
class GroupApplyForm(Form): apply_reason = StringField("申请理由", validators=[DataRequired("请输入申请理由")])
apps/community/handler.py 申请加入小组处理器
# 申请小组功能开发 class GroupMemberHandler(RedisHandler): @authenticated_async async def post(self, group_id, *args, **kwargs): #申请加入小组 re_data = {} param = self.request.body.decode("utf8") param = json.loads(param) form = GroupApplyForm.from_json(param) if form.validate(): try: group = await self.application.objects.get(CommunityGroup, id=int(group_id)) existed = await self.application.objects.get(CommunityGroupMember, community=group, user=self.current_user) self.set_status(400) re_data["non_fields"] = "用户已经加入" except CommunityGroup.DoesNotExist as e: self.set_status(404) except CommunityGroupMember.DoesNotExist as e: community_member = await self.application.objects.create(CommunityGroupMember, community=group, user=self.current_user, apply_reason=form.apply_reason.data) re_data["id"] = community_member.id else: self.set_status(400) for field in form.errors: re_data[field] = form.errors[field][0] self.finish(re_data)
apps/community/urls.py 路由
urlpattern = ( url("/groups/", GroupHandler), url("/groups/([0-9]+)/members/", GroupMemberHandler), )
联调
查询数据库
小组详情功能开发
apps/community/handler.py 小组详情处理器
class GroupDetailHanlder(RedisHandler): @authenticated_async async def get(self, group_id, *args, **kwargs): #获取小组的基本信息 re_data = {} try: group = await self.application.objects.get(CommunityGroup, id=int(group_id)) item_dict = {} item_dict["name"] = group.name item_dict["id"] = group.id item_dict["desc"] = group.desc item_dict["notice"] = group.notice item_dict["member_nums"] = group.member_nums item_dict["post_nums"] = group.post_nums item_dict["front_image"] = "{}/media/{}/".format(self.settings["SITE_URL"], group.front_image) re_data = item_dict except CommunityGroup.DoesNotExist as e: self.set_status(404) self.finish(re_data)
apps/community/urls.py 路由
urlpattern = ( url("/groups/", GroupHandler), url("/groups/([0-9]+)/members/", GroupMemberHandler), url("/groups/([0-9]+)/", GroupDetailHanlder), )
联调