返回顶部

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),
)

 

联调

 

posted @ 2018-12-28 00:28  Crazymagic  阅读(272)  评论(0编辑  收藏  举报