关于博客的记数器,最常见的做法是在主表里加一个view_number字段就可以了,每访问一次数据加1就好。
不优雅!而且扩展性不好。
因为以后除了blog以外的其它类型的应用,也需要记数器,又得加一堆boring的代码,所以本文是为整个项目增加一个全局计数器,为今后新增的应用服务。
具体是通过python的装饰器,以及Django的ContentType,tamplate tags来实现的
首先,利用django生成一个通用的阅读计数器app,就叫view_record:
python3 manage.py startapp view_record
创建计数器model
要统计什么时候谁访问了哪篇博文,那么就需要一个明细表记录和总表记录总数。
当然可以不用总表记录阅读总数,为了提高网站的访问效率,每次得到博文的阅读总数如果是直接获取到表中的数据,要比每次都用明细表的数据求和要快很多。
打开view_record应用的models.py文件:
#coding:utf-8
from django.db import models
#引用ContentType相关模块
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
#引用系统自带的用户模型
#from django.contrib.auth.models import User
from myusers.models import UserProfile #后续我扩展了系统的User类,后续再讲
class Recorder(models.Model):
"""阅读明细记录"""
#ContentType设置
content_type = models.ForeignKey(ContentType,on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey(
ct_field="content_type",
fk_field="object_id"
)
#记录IP地址
ip_address = models.CharField(max_length=15)
#记录User,这里可能没有登录用户,所以要允许为空
#user = models.ForeignKey(User, blank=True, null=True,on_delete=models.CASCADE)
user = models.ForeignKey(UserProfile, blank=True, null=True,on_delete=models.CASCADE)
#阅读的时间
view_time = models.DateTimeField(auto_now=True)
class ViewNum(models.Model):
"""阅读数量记录"""
#ContentType设置
content_type = models.ForeignKey(ContentType,on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey(
ct_field="content_type",
fk_field="object_id"
)
#普通字段,阅读总数量
view_num = models.IntegerField(default=0)
def __unicode__(self):
return u'<%s:%s> %s' % (self.content_type, self.object_id, self.view_num)
修改计数器admin
顺手把它加到后台管理中,打开该应用的admin.py:
from django.contrib import admin
from view_record.models import Recorder, ViewNum
# Register your models here.
class RecorderAdmin(admin.ModelAdmin):
"""view recorder admin"""
list_display=('content_type','object_id','ip_address','user','view_time')
ordering=('-view_time',)
class ViewNumAdmin(admin.ModelAdmin):
"""view num admin"""
list_display=('content_type','object_id','view_num')
admin.site.register(Recorder, RecorderAdmin)
admin.site.register(ViewNum, ViewNumAdmin)
同时也打开settings.py文件,加入该应用:
INSTALLED_APPS = (
#... 其他应用,
'view_record',
)
记得同步一下数据库,本来这些都是细枝末节(自己应该懂的,注意的事情)。为了避免出现低级错误,还是提一下。
python3 manage.py makemigrations
python3 manage.py migrate
创建计数器的decorator
在view_record应用创建一个decorator.py文件,用于放置装饰器方法:
#coding:utf-8
from django.contrib.contenttypes import models
from django.http import Http404
from django.contrib.contenttypes.models import ContentType
from view_record.models import Recorder, ViewNum
# 记录Entry_Detail页面访问量,只在get上生效
def entry_detail_view_counter(func):
def wrapper(self, request, *args, **kwargs):
print('自定义装饰器entry_detail_view_counter被调用了 请求路径%s' % request.path)
#print('request meta = ' ,request.META)
#print('request COOKIES = ' ,request.COOKIES)
#print('self = ' ,self.get_object().id)
#print('kwargs = ' ,kwargs)
'''
for key, value in kwargs.items():
setattr(self, key, value)
print(self.year,self.month,self.day,self.slug)
'''
#得到view里面的object参数
obj = self.get_object()
recorder = Recorder(content_object = obj)
recorder.ip_address = request.META.get("HTTP_X_FORWARDED_FOR", request.META.get("REMOTE_ADDR", None))
#print('in decorator request.user = = = ',request.user)
recorder.user = request.user if request.user.is_authenticated else None
recorder.save()
#总记录+1
obj_type = ContentType.objects.get_for_model(obj)
viewers = ViewNum.objects.filter(content_type = obj_type, object_id = obj.id)
if viewers.count() > 0:
viewer = viewers[0]
else:
viewer = ViewNum(content_type = obj_type, object_id = obj.id)
viewer.view_num += 1
viewer.save()
return func(self, request, *args, **kwargs)
return wrapper
若你对装饰器不太了解的话,可以看看《我对Python装饰器的理解》。
该装饰器是带一个参数,需要转递模型类进来。
由于zinnia代码框架高度简约,需要在EntryProtectionMixin的get方法上调用装饰器,才能得到每一次访问的计数值
from view_record.decorator import entry_detail_view_counter
class EntryProtectionMixin(LoginMixin, PasswordMixin):
"""
Mixin returning a login view if the current
entry need authentication and password view
if the entry is protected by a password.
"""
session_key = 'zinnia_entry_%s_password'
@entry_detail_view_counter
def get(self, request, *args, **kwargs):
"""
Do the login and password protection.
"""
response = super(EntryProtectionMixin, self).get(request, *args, **kwargs)
if self.object.login_required and not request.user.is_authenticated:
return self.login()
if (self.object.password and self.object.password !=
self.request.session.get(self.session_key % self.object.pk)):
return self.password()
return response
给装饰器传递博文的模型类,就可以实现对博文阅读计数。
修改博客的admin
为了方便在entry后台管理看到计数器,需要修改models_bases/entry.py文件:
view_num = GenericRelation(ViewNum)
再打开blog应用的admin/entry.py文件:
#coding:utf-8
from django.contrib import admin
class EntryAdmin(admin.ModelAdmin):
"""blog admin"""
list_display=('id','view_num_count')
def view_num_count(self, obj):
"""自定义显示字段"""
return sum(map(lambda x: x.view_num,obj.view_num.all()))
实现这个代码之后,就可以在后台管理中查看对应博文的阅读次数。
自定义计数器的模板标签
在view_record应用的目录下,创建文件夹templatetags。该文件夹django可以自动识别。
在templatetags文件夹,新建两个文件:__init__.py和view_num.py。打开view_num.py,写入如下代码:
#coding:utf-8
from django import template
from django.contrib.contenttypes.models import ContentType
from view_record.models import ViewNum
#得到自定义标签库,用于注册标签
register = template.Library()
#由于view_record使用contentType,所以更利于代码的扩展,自动侦测对象的类型,找到相关的数字
#本方法是专为列表的cell对象写的标签
@register.simple_tag
def get_view_cell_nums(*args, **kwargs):
obj = kwargs['data_in_cell']
obj_type = ContentType.objects.get_for_model(obj)
views = ViewNum.objects.filter(content_type = obj_type, object_id = obj.id)
view_num_all = sum(map(lambda x: x.view_num, views))
return str(view_num_all)
这个自定义标签实现的模式和admin差不多,需要处理的类和注册的方法。一般处理的类是返回结果;注册的方法是判断使用自定义标签的格式是否正确。
标签定义好之后,主要在博客列表中使用
{# 导入阅读器标签 #}
{% load view_nums %}
#根据每一条数据去抓取view_nums
{% get_view_cell_nums data_in_cell=object%}
全部完成以后,文章列表就可以显示每个博客的访问数字了。大概效果可以看下图:
结语
至此,这个功能从前端到后端都讲完了,一通疯狂操作以后,发现对django的理解更深了一些。
各位观众老爷,如果觉得对你有用的话。
请把“优雅”打在公屏上!