朋友这两天看了我的网站,提出来能否加一个点赞功能,于是抽了点时间,把这个功能加上了。开始并没有急着去做,而是先去研究各类网站和APP的点赞功能,实现效果真是百花齐放,有的甚至是花里胡哨。
做为一个偏技术和运营类的博客站点,重点还是内容本身,点赞功能可能起不到互动和玩耍的效果,于是定了一个很素的功能。大致如下:
- 功能边界:可以点赞文章,点赞评论,点赞回复
- 功能限制:点赞必须登录,记录每一个用户是否点赞,或取消点赞
- 操作功能:显示点赞数,点赞数字+1,取消赞数字-1
- 界面效果:点赞后要更换按钮显示样式,给使用者明显的操作回馈
技术方面和《飞仙锅建站日志第5篇-增加文章阅读计数器》所使用的技术类似:
- 利用django的contenttype,动态记录是文章点赞,还是评论回复的点赞
- 装饰器来检查是否登录,参数检查
- 模板标签用来展示点赞数
- 利用Jquery来做前端动态效果控制,使用ajax做请求异步处理
- 利用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回复