zoukankan      html  css  js  c++  java
  • 一个web应用的诞生(9)--回到用户

    在开始之前,我们首先根据之前的内容想象一个场景,用户张三在网上浏览,看到了这个轻博客,发现了感兴趣的内容,于是想要为大家分享一下心情,恩?发现需要注册,好,输入用户名,密码,邮箱,并上传头像后,就可以愉快的和大家进行分享互动了。

    这是一个很好的场景,不是么,下面我们就要来实现它,首先来说,存储一张图片有多重方法,服务器本地存储,db中存储二进制,但是这些都会或多或少的占用服务器的空间,并且,图片的读写还会占用空间宝贵的流量,对于我来说,一个穷coder,用的服务器是最便宜的一款阿里云,所以空间能省就省,而流量,更是节约到底,毕竟阿里云的流量比空间还要贵。

    最节省的方式当然是使用免费的专有空间来存储图片了,幸运的是,确实有这样一种看上去很天方夜谭的方式,那就是使用七牛云,当然了,免费使用七牛云的话,比如不能绑定域名,单ip访问频次限制等,但现阶段来说已经是够用了。

    使用七牛云的方法看上去和之前没什么区别,第一项当然还是安装:

    pip3.6 install qiniu 
    

    然后进行注册:

    from qiniu import Auth
    ...
    
    qn=Auth(access_key,secret_key)
    

    很简单,其实这里使用的只是一个获取token,而文件上传的部分使用js-jdk来实现,现在增加一个获取token的视图:

    #获取七牛凭证
    @main.route("/qiniuuptoken",methods=["GET","POST"])
    def qiniuuptoken():
        bucket_name="python-nblog"
        key=str(uuid.uuid1())
        token=qn.upload_token(bucket_name,key)
        return  jsonify({
            "uptoken":token,
            "key":key
    	})
    

    使用一个uuid作为云端的文件名,并且将此uuid与用户绑定存入db中作为用户的头像使用

    然后修改用户对象,新增headimg字段(存储文件key):

    class User(UserMixin,db.Model):
        __tablename__="users"
    	...
        headimg=db.column(db.String(50))
        ...
    

    好了,还记得之前实现的功能么,下面要修改RegisterForm类,在表单中新增一个上传头像的file域,以及一个用于记录图片key的隐藏域

    class RegisterForm(Form):
        ...
        headimg=FileField("上传头像")
        headkey=HiddenField("头像上传后生成的key")
        ...
        submit=SubmitField("提交")
    

    修改register.html模板,增加js文件的引用块:

    {% block scripts %}
    {{super()}}
    <script src="http://cdn.bootcss.com/plupload/2.1.9/moxie.min.js"></script>
    <script src="http://cdn.bootcss.com/plupload/2.1.9/plupload.min.js"></script>
    <script src="http://cdn.bootcss.com/plupload/2.1.9/i18n/zh_CN.js"></script>
    <script src="http://cdn.bootcss.com/qiniu-js/1.0.17.1/qiniu.min.js"></script>
    <script type="text/javascript" src="{{ url_for('static', filename='js/qiniuupload.js',key=12) }}"></script>
    {% endblock %}
    

    引用的js文件貌似还不少,可能也看到了,自己使用的就是qiniuupload.js,代码如下:

    $(function () {
        var tempurl="http://on4ag3uf5.bkt.clouddn.com";//常量 七牛临时域名地址
      
        var token={
            key:"",
            uptoken:""
        }
        //img回写
        if($("#headkey").val()!=""){
            reSetImg(tempurl)
        }
        var uploader = Qiniu.uploader({
        runtimes: 'html5',      // 上传模式,依次退化
        browse_button: 'headimg',         // 上传选择的点选按钮,必需
         uptoken_func: function(file){    // 在需要获取uptoken时,该方法会被调用
             $.getJSON({url:"/qiniuuptoken",type:"POST",async:false,success:function (d) {
                token.up= d.uptoken;
                token.key=d.key;
             }})
            return  token.up;
        },
        get_new_uptoken: false,             // 设置上传文件的时候是否每次都重新获取新的uptoken
        domain: 'python-nblog',     // bucket域名,下载资源时用到,必需
        //container: 'container',             // 上传区域DOM ID,默认是browser_button的父元素
        max_file_size: '5mb',             // 最大文件体积限制
        flash_swf_url: 'http://cdn.bootcss.com/plupload/3.1.0/Moxie.swf',  //引入flash,相对路径
        max_retries: 3,                     // 上传失败最大重试次数
        dragdrop: false,                     // 开启可拖曳上传
        //drop_element: 'container',          // 拖曳上传区域元素的ID,拖曳文件或文件夹后可触发上传
        chunk_size: '1mb',                  // 分块上传时,每块的体积
        auto_start: true,                   // 选择文件后自动上传,若关闭需要自己绑定事件触发上传
        init: {
            'FileUploaded': function(up, file, info) {
                setImg(tempurl, $.parseJSON(info).key)
            },
            'Key': function(up, file) {
                // do something with key here
                return token.key
            }
        }
    });
    });
    
    
    function setImg( tempurl,imgKey){
          var temphtml="<div class='form-group'><label class='control-label'>头像预览</label>"
            temphtml+="<div><img src='"+tempurl+"/"+imgKey+"'  class='img-thumbnail' style='200px;height:200px;'></div>";
            temphtml+="</div>";
         
            //修改key
            $("#headkey").val(imgKey)
            //增加预览图
            $("#headimg").parent().after(temphtml);
            $("#headimg").hide();
    }
    

    代码不难懂,除了七牛部分,都是基本的jq代码,并且七牛的js-sdk都有很完善的demo和文档

    七牛的使用步骤
    1 注册七牛账户
    2 点击新建存储空间如图示:

    4 输入存储空间名称,必填,对应sdk中的domain字段
    5 点击确定 即可

    注意,由于使用的为免费用户,所以不能绑定域名,使用的为七牛分配域名。

    然后,修改注册视图:

     if form.validate_on_submit():
      	...
        user.headimg=form.headkey.data
        ...
        user.role_id=1          #暂时约定公开用户角色为1
        db.session.add(user)
    

    最后修改base.html模板,将注册页的导航加入:

     <ul class="nav navbar-nav navbar-right">
        {% if current_user.is_authenticated %}
            <li><p class="navbar-text"><a href="#" class="navbar-link">{{current_user.username}}</a>  您好</p></li>
            <li><a href="{{url_for('auth.logout')}}">登出</a></li>
        {% else %}
            <li><a href="{{url_for('auth.login')}}">登录</a></li>
            <li><a href="{{url_for('auth.register')}}">注册</a></li>
        {% endif %}
      </ul>
    

    功能宣告完成。

    与这个功能类似的功能是用户资料的功能,即对用户资料的查看和修改,但这个功能需要用户权限来进行支撑,所以先来完成用户权限。

    下面让我们回看之前的代码,user.role_id=1很扎眼对不对,下面完成一下权限系统,说是权限系统,其实只有三个角色:

    1. 匿名用户,即未登录用户,只有阅读权限
    2. 普通用户,具有发布文章,评论文章已经关注他人的权限
    3. 管理员,除普通用户外,还有删除及修改文章权限

    这三个角色,对应到db中需要两条记录,即User和Administrator,下面对角色类进行适当的修改并增加初始化方法

    class Role(db.Model):
        __tablename__="roles"
        id=db.Column(db.Integer,primary_key=True)
        name=db.Column(db.String(50),unique=True)
        users=db.relationship("User",backref='role')
        default=db.Column(db.Boolean)
        @staticmethod
        def init_roles():
            roles={
                "User":('普通用户',True),
                "Administrator":("管理员用户",False)
            }
            for r in roles:
                print(r)
                role=Role.query.filter_by(name=r[0]).first()
                if role is None:
                    role=Role()
                role.name=roles[r][0]
                role.default=roles[r][1]
                db.session.add(role)
            db.session.commit()
    

    增加了一个default字段,以绝定用户注册时使用此角色,并且增加了初始化方法,新增两个角色,执行初始化脚本:

    python manage.py shell
    >>>Role.init_roles()
    

    为用户定义默认角色:

    class User(UserMixin,db.Model):
        def __init__(self,**kwargs):
            super(User,self).__init__(**kwargs)
            if self.role is None:
                self.role=Role.query.filter_by(default=True).first();
    

    通过User类的构造函数,来发现创建user类中是否已经定义了角色,如果没有定义则设置为默认角色。

    然后继续创建一个匿名用户类:

    class AnonymousUser(AnonymousUserMixin):
        def is_administrator(self):
            return self.role.admin
    

    可以看到,此匿名用户类继承了Flask_login的AnonymousUserMixin类,并将其设置为匿名用的current_user的值,即未登录用户的current_user,以便程序中使用。

    如果某些视图函数只对登录用户或管理员开发,当让可以在视图内判断,但更好的方式则是使用一个自定义的装饰器。

    from functools import wraps
    from flask import abort
    from flask_login import current_user
    
    def admin_required(f):
        @wraps(f)
        def decorated_function(*args,**kwargs):
            if not current_user.is_administrator():
                abort(403)
            return f(*args,**kwargs)
        return decorated_function
    

    装饰器使用了functools包,功能为如果用户不为管理员,则返回403错误,下面演示一下如何使用这个装饰器:

    @main.route("/admin",methods=["GET","POST"])
    @admin_required
    def for_admin_only():
        return "您好 管理员"
    

    运行一下,还记得之前注册过的用户么,就使用zhangji这个用户好了,登录后直接在url中输入/admin,显示:

    为了方便测试,直接将db中zhangji这个用户的role_id字段修改为管理员id,刷新页面:

    ok,非常完美,接下来根据权限,完成首页内容:
    首先,头像改为实际内容:

    {% for post in posts %}
      <div class="bs-callout
              {% if loop.index % 2 ==0 %}
               bs-callout-d
              {% endif %}
              {% if loop.last %}
                bs-callout-last
              {% endif %}" >
          <div class="row">
              <div class="col-sm-2 col-md-2">
                    <!--使用测试域名-->
                    <img src="http://on4ag3uf5.bkt.clouddn.com/{{post.author.headimg}}" alt="...">
              </div>
              <div class="col-sm-10 col-md-10">
               <div>
                <p>
                   {% if post.body_html%}
                      {{post.body_html|safe}}
                    {% else %}
                   {{post.body}}
                   {% endif %}
                </p>
                </div>
               <div>
                <a class="text-left" href="#">李四</a>
                <span class="text-right">发表于&nbsp;{{ moment( post.createtime).fromNow(refresh=True)}}</span>
               </div>
              </div>
          </div>
      </div>
      {% endfor %}
    

    以及:

     <div class="col-md-4 col-md-4 col-lg-4">
         <!--这里 当没有用户登录的时候 显示热门分享列表 稍后实现-->
         {% if current_user.is_authenticated %}
        <img src="http://on4ag3uf5.bkt.clouddn.com/{{current_user.headimg}}" alt="..." class="headimg img-thumbnail">
         <br><br>
         <p class="text-muted">我已经分享<span class="text-danger">55</span>条心情</p>
         <p class="text-muted">我已经关注了<span class="text-danger">7</span>名好友</p>
         <p class="text-muted">我已经被<span class="text-danger">8</span>名好友关注</p>
         {%endif%}
     </div>
    

    关注部分稍后完成。

    而如果没有登录,则是不能分享心情的,这时将表单隐藏即可

      <div>
          {% if current_user.is_authenticated %}
          {{ wtf.quick_form(form) }}
          {% endif %}
      </div>
    

    最后,点击头像或姓名,还可以查看作者的资料,这个功能点分为三种情况:

    1. 其他人观看,会有一个样式美观的名片页
    2. 自己观看,则会产生名片页,并可以修改内容
    3. 管理员观看,则会产生名片页并可以修改内容

    我们先来看其他人的个人资料页,首先,需要创建一个视图:

    @main.route("/user/<username>")
    def user(username):
        user=User.query.filter_by(username=username).first()
        if(user is None):
            abort(404)
        posts = Post.query.filter_by(author_id=user.id)
        return render_template("user.html",user=user,posts=posts)
    

    然后创建模板:

    {% extends "base.html" %}
    {% block main %}
    <div class="container">
        <div class="row">
            <p>
             <img src="http://on4ag3uf5.bkt.clouddn.com/{{user.headimg}}" alt="..." class="headimg img-thumbnail" style="300px; height: 300px">
            </p>
            <p>
                {% if user.nickname%}{{user.nickname}}{%elif user.username %}{{ user.username }}{% endif %}
            </p>
            {% if user.username %}
            <p>用户名:{{user.username}}</p>
            {% endif %}
            {% if user.username %}
            <p>昵称:{{user.nickname}}</p>
            {% endif %}
             {% if user.email %}
            <p>联系方式:<a href="mailto:{{user.email}}">{{user.email}}</a></p>
            {% endif %}
            {% if user.remark %}
            <p>自我简介:{{user.remark}}</p>
            {% endif %}
            <p>
                注册时间:{{moment(user.createtime).format('LL')}}
                最终登录时间:{{moment(user.lastseen).format('LL')}}
            </p>
        </div>
    </div>
    {% endblock %}
    

    你可能注意到createtime和lastseen两个字段,是基于一般的博客网站,新增加的内容:

    class User(UserMixin,db.Model):
    	...
    	lastseen=db.Column(db.DateTime,default=datetime.utcnow)
    	createtime=db.Column(db.DateTime,default=datetime.utcnow)
    	...	
    

    分别在定义了注册时间和最后访问的时间

    最后,为头像和作者的位置增加超链接(index.html):

      ...
      <a class="text-left" href="{{url_for('main.user',username=post.author.username)}}">
        <img src="http://on4ag3uf5.bkt.clouddn.com/{{post.author.headimg}}" alt="...">
      </a>
      ...
      <a class="text-left" href="{{url_for('main.user',username=post.author.username)}}">{{post.author.nickname}}</a>
    

    接下来是自己进入和管理员进入,这时候如果还同样在这个页面进行操作,就会显得复杂,所以比较好的办法是如果是本用户或管理员的话,显示一个编辑的超链接,进行一下跳转进行编辑,同时,由于本用户进行编辑的话,只可以编辑有限几个字段,如生日,真实姓名,自我简介等,但是如果是管理员的话,显然会编辑很多自动,如用户名,权限配置等,所以,会创建两个超链接分别对应本用户的表单和管理员的表单(user.html)。

     <p>
      	{% if current_user.is_authenticated and current_user.username==user.username %}
        	<a href="#">修改个人信息</a>
        {% endif %}
        {% if current_user.is_administrator() %}
        	<a href="#">修改该用户信息</a>
        {% endif %}
    </p>
    

    下面创建修改个人信息表单:

    from flask_wtf import FlaskForm
    from wtforms import FileField,HiddenField,StringField,DateField,RadioField,TextAreaField,SubmitField
    from wtforms.validators import Email
    class EditProfileForm(FlaskForm):
        headimg = FileField("上传头像")
        headkey = HiddenField("头像上传后生成的key")
        nickname = StringField("昵称")
        birthday = DateField("出生日期")
        email = StringField("邮箱地址", validators=[Email()])
        gender = RadioField("性别", choices=[("0", "男"), ("1", "女")], default=0,coerce=int)
        remark = TextAreaField("自我简介")
        submit = SubmitField("提交")
    

    当修改的时候,头像要能够回写,在qiniuupload.js文件中的$(function(){})方法中增加如下方法:

    //img回写
    if($("#headkey").val()!=""){
        reSetImg(tempurl)
    }
    

    并且添加reSetImg方法:

    function reSetImg(tempurl) {
       var temphtml="<div class='form-group'><label class='control-label'>头像预览</label>"
            temphtml+="<div><img src='"+tempurl+"/"+$("#headkey").val()+"'  class='img-thumbnail' style='200px;height:200px;'></div>";
            temphtml+="</div>";
        $("#headimg").parent().after(temphtml);
    }
    

    之前的头像还要删除掉:

    function setImg( tempurl,imgKey){
      	var temphtml="<div class='form-group'><label class='control-label'>头像预览</label>"
        temphtml+="<div><img src='"+tempurl+"/"+imgKey+"'  class='img-thumbnail' style='200px;height:200px;'></div>";
        temphtml+="</div>";
        //删除之前的预览图
        if($("#headimg").parent().next().find("img"))
        {
           $("#headimg").parent().next().remove()
        }
        //修改key
        $("#headkey").val(imgKey)
        //增加预览图
        $("#headimg").parent().after(temphtml);
        $("#headimg").hide();
    }
    

    注意这里删除仅仅是删除html中的dom,七牛中的文件并没有删除,毕竟不是专门针对七牛的blog 所以这个功能不打算实现,各位可以自己来实现此功能。

    而html模板与注册模板基本一样:

    {% extends "base.html"%}
    {% block content %} <!--具体内容-->
    {% import "bootstrap/wtf.html" as wtf %}
    <div class="container">
        <div class="row"></div>
        <div class="row">
    
            <div>
                <div class="page-header">
                    <h1>修改个人信息</h1>
                </div>
                {% for message in get_flashed_messages() %}
                <div class="alert alert-warning">
                  <button type="button" class="close" data-dismiss="alter">&times</button>
                  {{message}}
                </div>
                {% endfor %}
                {{ wtf.quick_form(form)}}
            </div>
        </div>
    </div>
    {% endblock %}
    
    {% block scripts %}
    {{super()}}
    <script src="http://cdn.bootcss.com/plupload/2.1.9/moxie.min.js"></script>
    <script src="http://cdn.bootcss.com/plupload/2.1.9/plupload.min.js"></script>
    <script src="http://cdn.bootcss.com/plupload/2.1.9/i18n/zh_CN.js"></script>
    <script src="http://cdn.bootcss.com/qiniu-js/1.0.17.1/qiniu.min.js"></script>
    <script type="text/javascript" src="{{ url_for('static', filename='js/qiniuupload.js',key=01) }}"></script>
    {% endblock %}
    

    简单测试一下,非常完美,限于篇幅就不贴图,下面完成一下管理员对于普通用户的资料修改,相对于普通用户来说,管理员要能修改的项就要多一些了,下面创建一个用于管理员使用的表单:

    from flask_wtf import FlaskForm
    from wtforms import FileField,HiddenField,StringField,DateField,RadioField,TextAreaField,SubmitField,SelectField
    from wtforms.validators import Email,ValidationError,DataRequired
    from ..models.User import User
    from ..models.Role import Role
    class EditProfileAdminForm(FlaskForm):
        headimg = FileField("上传头像")
        headkey = HiddenField("头像上传后生成的key")
        username=StringField("用户名",validators=[DataRequired()])
        role=SelectField("用户角色",coerce=int)
        nickname = StringField("昵称")
        birthday = DateField("出生日期")
        email = StringField("邮箱地址", validators=[Email()])
        gender = RadioField("性别", choices=[(0, "男"), (1, "女")], default=0,coerce=int)
        remark = TextAreaField("自我简介")
        submit = SubmitField("提交")
    
        def __init__(self,user,*args,**kwargs):
            super(EditProfileAdminForm,self).__init__(*args,**kwargs)
            self.role.choices=[(role.id,role.name) for role in Role.query.all()]
            self.user=user;
    
        def validate_username(self,field):
            if(field.data!=self.username and User.query.filter_by(username=field.data).first()):
                raise ValidationError("此用户名已经使用!")
    

    可以看到,就是在普通的修改页进行了一些修改,增加用户名和角色两个字段,并在构造函数中为角色下拉菜单注入值,主语注入的写法:

    [(role.id,role.name) for role in Role.query.all()]
    

    这种表达式的写法是我决定python中最帅的写法,虽然复杂的看着有点晕:(,和java中的拉姆达一样,其实应该说java中的拉姆达和他一样。还需要注意的一个就是自定义验证的写法,这个验证的功能是如果用户名进行了修改,并且与db中已有值相同,则会抛出异常,页面会提示此用户名已经使用,你一定想到了,其实注册的时候就应该做此验证的,同时对注册表单进行修改, 这里就不贴代码。

    剩下的就非常简单,和本用户编辑几乎相同,甚至使用相同的模板,下面是视图控制器的代码:

    @main.route("/edit-profile/<int:id>",methods=["GET","POST"])
    @admin_required
    @login_required
    def edit_profile_admin(id):
        user=User.query.get_or_404(id);
        form=EditProfileAdminForm(user=user);
        if form.validate_on_submit():
            user.nickname=form.nickname.data
            user.remark=form.remark.data
            user.birthday=form.birthday.data
            user.email=form.email.data
            user.gender=form.gender.data
            user.headimg=form.headkey.data
            user.role=Role.query.get(form.role.data)
            user.username=form.username.data
            db.session.add(user)
            return redirect(url_for("main.user",username=user.username))
        form.nickname.data=user.nickname
        form.remark.data=user.remark
        form.birthday.data=user.birthday
        form.email.data=user.email
        form.gender.data=user.gender
        form.headkey.data=user.headimg
        form.role.data=user.role_id
        form.username.data=user.username
        return render_template("edit_profile.html",form=form,user=user);
    

    注意此时使用id进行用户检索,则可以使用get_or_404方法,当查询失败直接报404错误

    ok,这个功能宣告完成,是不是很简单,发现这篇博文写的有点长了,但是最后还有一个地方要思考一下,就是用户的lastseen字段,在什么时候更新合适呢,最简单的方式当然是登录的时候进行更新,但这样真的好吗,想象一下,我在登录后如果进行频繁的操作,那么时间势必会不准确,所以最好的方法是在条件允许的情况下每次request的时候都进行更新,当然这样也不可避免的会消耗资源,如何取舍由自己来决定,下面这个例子中实现一下这个功能:

    首先在用户模型中添加方法:

    class User(UserMixin,db.Model):
    	...
    	def visit(self):
            self.lastseen=datetime.utcnow()
            db.session.add(self);
    

    然后在试图控制器中:

    @auth.before_app_request
    def before_request():
        if(current_user.is_authenticated):
            current_user.visit()
    

    添加这个方法即可。

  • 相关阅读:
    delphi.数据结构.链表
    delphi.指针.PChar
    delphi.指针.应用
    delphi.memory.分配及释放---New/Dispose, GetMem/FreeMem及其它函数的区别与相同
    Delphi系统变量:IsMultiThread对MM的影响
    安装文件制作工具Wix概念快速入门
    [转]JUnit-4.11使用报java.lang.NoClassDefFoundError: org/hamcrest/SelfDescribing错误
    Xiaohe-LeetCode 288 Unique Word Abbreviation
    Xiaohe-LeetCode 100 Same Tree
    Xiaohe-LeetCode 237 Delete Node in a Linked List
  • 原文地址:https://www.cnblogs.com/jiangchao226/p/6629441.html
Copyright © 2011-2022 走看看