飞仙锅建站日志第10篇-增加点赞功能

朋友这两天看了我的网站,提出来能否加一个点赞功能,于是抽了点时间,把这个功能加上了。开始并没有急着去做,而是先去研究各类网站和APP的点赞功能,实现效果真是百花齐放,有的甚至是花里胡哨。

做为一个偏技术和运营类的博客站点,重点还是内容本身,点赞功能可能起不到互动和玩耍的效果,于是定了一个很素的功能。大致如下:

  1. 功能边界:可以点赞文章,点赞评论,点赞回复
  2. 功能限制:点赞必须登录,记录每一个用户是否点赞,或取消点赞
  3. 操作功能:显示点赞数,点赞数字+1,取消赞数字-1
  4. 界面效果:点赞后要更换按钮显示样式,给使用者明显的操作回馈

技术方面和《飞仙锅建站日志第5篇-增加文章阅读计数器》所使用的技术类似:

  1. 利用django的contenttype,动态记录是文章点赞,还是评论回复的点赞
  2. 装饰器来检查是否登录,参数检查
  3. 模板标签用来展示点赞数
  4. 利用Jquery来做前端动态效果控制,使用ajax做请求异步处理
  5. 利用boostrap来做样式控制

下面是具体的代码和说明:

Models

使用数据库设计的老套路,一个主表记录总点赞数,一个从表记录用户的点赞情况

#主表
class Likes(models.Model):
	#content type
    content_type = models.ForeignKey(ContentType,on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey(
        ct_field="content_type",
        fk_field="object_id"
    )

    likes_num = models.IntegerField(default = 0)

    def __unicode__(self):
        return u'%s:%s(%s)' % (self.content_type, self.object_id, self.likes_num)

#从表
class LikesDetail(models.Model):
    likes = models.ForeignKey(Likes,on_delete=models.CASCADE)
    #user = models.ForeignKey(User)
    user = models.ForeignKey(UserProfile, blank=True, null=True,on_delete=models.CASCADE)
    is_like = models.BooleanField(default = False)
    pub_date = models.DateTimeField(auto_now=True)

    class Meta:
        ordering=['-pub_date']

Views:

利用一个方法来实现点赞和取消点赞,检查是否登录,检查参数。处理完毕后,给前端返回json数据。

@check_login   #点赞必估登录
@check_request('type', 'id', 'is_like') #检查传参
def likes_change(request):
    data = {}
    data['status'] = 200
    data['message'] = u'ok'
    data['nums'] = 0

    obj_type = request.GET.get('type')
    obj_id = request.GET.get('id')
    #user = request.user
    user = request.user if request.user.is_authenticated else None

    #print(request.GET.get('is_like'))
    is_like = True if request.GET.get('is_like') == 'true' else False
    c = ContentType.objects.get(model = obj_type)

    try:
        l = Likes.objects.get(content_type = c, object_id = obj_id)
    except Exception as e:
        l = Likes(content_type = c, object_id = obj_id)
    data['nums'] = l.likes_num

    try:
        detail = LikesDetail.objects.get(likes = l, user = user)
    except Exception as e:
        detail = LikesDetail(likes = l, user = user, is_like = False)

    try:
        #这一句是为了兼容前段传参异常造成的问题
        if detail.is_like is not is_like:
            if is_like:
                l.likes_num += 1
            else:
                l.likes_num += -1

            if l.likes_num < 0:
                l.likes_num = 0
            l.save()
            data['nums'] = l.likes_num

            detail.is_like = is_like
            detail.save()
    except Exception as e:
        data['status'] = 404 
        data['message'] = str(e)

    return HttpResponse(json.dumps(data), content_type="application/json")

装饰器

一共两个装饰器:一个是检查是否有登录用户的;一个是检查GET请求提供的参数是否齐全。处理完毕后,给前端返回json数据。

#装饰器,检测是否有登录用户
def check_login(func):
    def warpper(request):
        try:
            if request.user.is_authenticated:
                return func(request)
            else:
                data = {}
                data['status'] = 401
                data['message'] = u'没有登录'
                return HttpResponse(json.dumps(data), content_type="application/json")
        except Exception as e:
            print(e)
            data = {}
            data['status'] = 402
            data['message'] = str(e)
            return HttpResponse(json.dumps(data), content_type="application/json")
    return warpper

#装饰器,检查request参数是否齐全
def check_request(*params):
    def __check_request(func):
        def warpper(request):
            for param in params:
                if not request.GET.__contains__(param):
                    data = {}
                    data['status'] = 403
                    data['message'] = u'no params:%s' % param
                    return HttpResponse(json.dumps(data), content_type="application/json")
            return func(request)
        return warpper
    return __check_request

自定义模板标签

由于文章和评论的点赞数要在列表中显示,最好的办法还是使用模板标签最合适,即插即用。

写到这,突然想起来刚编程那会,写一堆sql语句关联查询,一次性把评论数、点赞数这类统计数字都一股脑塞到一个sql语句中,再向前端返回,然后就是无穷尽的调试sql语句。

也不知道这是好事还是坏事,新一波的程序员估计都不知道关联查询是个什么东东,数据库存储过程为何物,mysql和oracle、DB2之间的sql语句之间语法差异。

因为这些工作都被java的hibernate,ruby on rails,python的django给做了,程序员只需要熟练的使用这些ORM框架就好了。

不感慨了,能用就好,不是吗?

继续看代码:

@register.simple_tag
def get_entry_likes_nums(*args, **kwargs):
    obj = kwargs['data_in_cell']
    obj_type = ContentType.objects.get_for_model(obj)
    views = Likes.objects.filter(content_type = obj_type, object_id = obj.id)
    entry_likes_all = sum(map(lambda x: x.likes_num, views))
    return str(entry_likes_all)

Admin

在django后台加上admin的代码

class LikesDetailAdmin(admin.ModelAdmin):
    list_display=('likes','user','is_like','pub_date')
    ordering=('-pub_date',)
    
class LikesAdmin(admin.ModelAdmin):
    list_display=('content_type','object_id','likes_num')
    
admin.site.register(LikesDetail, LikesDetailAdmin)
admin.site.register(Likes, LikesAdmin)

Urls

urlpatterns = [
    url(r'^likes_change$',views.likes_change,name='likes_change'),
]

模板

在文章列表里,每一个文章下面放一个按钮,利用模板标签显示点赞数,利用javascript点击按钮实现点击效果

<a href="javascript:void(0);" onclick="likes_change(this,'entry','{{ object.id }}');"
        title="点赞数" class="btn btn-default btn-xs pull-right" style="margin-right: 10px;">
        <span class="glyphicon glyphicon-thumbs-up">
          {% get_entry_likes_nums  data_in_cell=object%}
        </span>
</a>

Javascript

具体实现逻辑已经在代码注释里写了,看代码就好了

function likes_change(obj, type, id) {
    // 判断obj中是否包含active的元素,用于判断当前状态是否为激活状态
    var like_css = 'btn-success'
    var like_num_css = 'glyphicon'
    var has_like_css = $(obj).hasClass(like_css)
    console.log(obj.className)
    //如果页面显示喜欢,就变把数据库变成不喜欢迎,反之同理
    var is_like = !has_like_css
    
    $.ajax({
        url: '/view_record/likes_change',
        // 为了避免加入csrf_token令牌,所以使用GET请求
        type: 'GET',
        // 返回的数据用于创建一个点赞记录
        data: {
            type: type,
            id: id,
            is_like: is_like,
        },
        cache: false,
        success: function (data) {
            console.log(data);
            if (data['status'] == '200'){
                // 更新点赞状态
                if (has_like_css){
                    $(obj).removeClass(like_css)
                }
                else {
                    $(obj).addClass(like_css)
                }
                // 更新点赞数量
                var like_num = $(obj.getElementsByClassName(like_num_css))
                like_num.text(' '+ data['nums']+ ' ')

            }
            else if(data["status"]=='401'){//需要登录
                $(location).attr('href', '/accounts/login/');
            }else {
                // 以弹窗的形式显示错误信息
                alert(data['message'])
            }
        },
        error: function (xhr) {
            console.log(xhr)
        }
    });
    return false;
};

结语

每次写完技术贴都比较累,和写代码不一样的地方在于写文章要讲清楚代码的来龙去脉,不仅自己要明白,还要向大家讲明白。

而且,能写出给大家展示的东西,一定是比较优雅的代码。

在写的过程中,也是对代码的一次回顾,也建议各位看官在写完代码,将过程心得,设计思想写下来,百益无一害。

DONE

评论列表

annli1980@163.com

腻害腻害~

2021年11月2日 14:40回复

新的评论

清空