04_文章模型和评论模型

本文主要完成文章模型和评论模型的设计和开发,文章和评论都是一个博客程序的核心内容。所以,这一章的 model 代码会比之前的稍微多一些。 同样,在本节内容开始之前,我们来思考几个问题: 文章和页面应该怎么存储? 查询的时候能否有快捷的获取某一类型的所有文章或页面? 在不用登录就可以评论的情况下,怎么防止垃圾评论的产生? 本文目标 将 model 里面的 choices 统一写到 docspace/choices.py 文件里 设计一个可以根据文章类型动态应用模板的模型 运用 Django 模型管理器,写一些常调用的方法 博客评论接入 akismet 来防止垃圾评论 涉及知识点 django 模型管理器 akismet 防垃圾评论 Step1、将 choices 统一管理 由于模型里面的 choices 字段可能越来越多,而且 choices 被其他地方调用的次数也会增加,所以我们把它们写到同一个文件里面去更方便管理 新建 docspace/choices.py 文件并写入如下代码: # `docspace/choices.py` from django.db import models class MarkItems(models.TextChoices): TAG = "tag", _("Tag") CATEGORY = "category", _("Category") LINK = "link", _("Link") FILE = "file", _("File") PHOTO = "photo", _("Photo") AUDIO = "audio", _("Audio") VIDEO = "video", _("Video") SPECIAL = "special", _("Special") TUTORIAL = "tutorial", _("Tutorial") COURSE = "course", _("Course") class ArticleStatusItems(models.TextChoices): DRAFT = 'draft', _("Draft") TRASH = 'trash', _("Trash") PUBLISHED = 'published', _("Published") PENDING = 'pending', _("Pending") INHERIT = 'inherit', _("Inherit") AUTODRAFT = 'auto-draft', _("Auto draft") class ArticleTypeItems(models.TextChoices): ARTICLE = 'article', _("Article") PAGE = 'page', _("Page") COVER = 'cover', _("Cover") PHOTO = "photo", _("Photo") AUDIO = "audio", _("Audio") VIDEO = "video", _("Video") SPECIAL = "special", _("Special") TUTORIAL = "tutorial", _("Tutorial") COURSE = "course", _("Course") ATTACHMENT = 'attachment', _("Attachment") class CommentStatusItems(models.TextChoices): OPEN = 'open', _("Open") CLOSED = 'closed', _("Closed") 调用的时候只需要从这个文件 import 相应的类即可,例如: from docspace.choices import MarkItems Step2、文章模型和模型管理器 Django 默认的模型管理器 models.Manager,可通过继承该类得到新的管理器类 # models.Manager 是每个模型类默认的名为 objects 的 Manager # `docspace/models.py`.`ArticleManager` class ArticleManager(models.Manager): def get_published(self): """ 该方法执行 `Article.objects.get_puslished()` 直接获取已发布的所有文章 需要你重新指定模型类的 objects = ArticleManager() 到新的管理器类 """ query = dict( post_type=ArticleTypeItems.ARTICLE, status=ArticleStatusItems.PUBLISHED, ) return super(ArticleManager, self).get_queryset().filter(**query) class Article(models.Model): . . . objects = ArticleManager() 上面是给默认管理器 objects 添加额外的管理器方法 Article 模型类的完整代码如下: # `docspace/models.py`.`Article` class Article(Created, Updated, Parent, SlugAble): title = models.CharField( max_length=255, verbose_name=_("Article title"), ) content = models.TextField( null=True, blank=True, verbose_name=_("Article content"), ) author = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Article author"), ) comment_status = models.SlugField( default=CommentStatusItems.OPEN, choices=CommentStatusItems.choices, verbose_name=_("Comment status"), ) mime_type = models.CharField( max_length=64, null=True, blank=True, verbose_name=_("MIME type"), ) post_type = models.SlugField( default=ArticleTypeItems.ARTICLE, choices=ArticleTypeItems.choices, verbose_name=_("Article type"), ) status = models.SlugField( null=True, blank=True, default=ArticleStatusItems.AUTODRAFT, choices=ArticleStatusItems.choices, verbose_name=_("Article status"), ) category = models.ManyToManyField( 'Taxonomy', blank=True, limit_choices_to={'mark': 'category'}, verbose_name=_("Article category"), related_name="%(app_label)s_%(class)s_category", ) tags = models.ManyToManyField( 'Taxonomy', blank=True, limit_choices_to={'mark': 'tag'}, verbose_name=_("Article tags"), related_name="%(app_label)s_%(class)s_tags", ) read_num = models.PositiveIntegerField( default=0, verbose_name=_("Read number"), ) comment_num = models.PositiveIntegerField( default=0, verbose_name=_("Comment number"), ) claps_num = models.PositiveIntegerField( default=0, verbose_name=_("Claps number"), ) objects = ArticleManager() def __str__(self): return self.title class Meta: verbose_name = _("Article") verbose_name_plural = _("Articles") objects = ArticleManager() 指定了默认新的模型管理器 mime_type 字段用于区分图片的 mime_type,同时可通过 MIME_TYPE 的值, markdown 或 rich_text,通过判断文档类型来渲染html post_type 文章类型可以用来动态选择要渲染的模板 分类和标签字段的 limit_choices_to 限制了模型外键查询集的范围 Step3、评论模型与垃圾评论 现在很多个人博客都接入了第三方评论或者强制要求用户登录之后再发言,这样做的好处能减少 90% 的垃圾评论甚至更多。 但如果做一个通过评论昵称和邮箱地址加上发言内容就直接可以发言,又会招来很多很多的垃圾评论。 所以,我们要在用户评论这个业务上接入 Akismet 来拦截大部分的垃圾评论。 Akismet 会根据我们的全球垃圾评论数据库检查您的评论和联系表单提交,以保护您和您的站点免受恶意内容的侵害。您可以在站点的“评论”管理屏幕上查看垃圾评论。 Comment 模型代码: # `docspace/models.py`.`Comment` class Comment(Created, Parent): article = models.ForeignKey( 'Article', on_delete=models.CASCADE, verbose_name=_("Article"), related_name="%(app_label)s_%(class)s_article", ) author_name = models.CharField( max_length=12, verbose_name=_("Author name"), ) author_email = models.EmailField( max_length=36, blank=True, null=True, verbose_name=_("Author email"), ) author_url = models.URLField( blank=True, null=True, verbose_name=_("Author url"), ) author_ip = models.GenericIPAddressField( blank=True, null=True, verbose_name=_("Author ip address"), ) content = models.TextField(verbose_name=_("Comment content")) approved = models.SlugField( null=True, blank=True, default='yes', verbose_name=_("Approved"), ) agent = models.TextField( null=True, blank=True, verbose_name=_("User agent"), ) notify = models.SlugField( null=True, blank=True, default='push', verbose_name=_("Notify"), ) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_("User"), ) def __str__(self): return str(self.article) def children(self): obj = self._meta.model.objects.filter( approved='yes', parent_id=self.pk ) return obj class Meta: verbose_name = _("Comment") verbose_name_plural = _("Comments") @receiver(models.signals.post_save, sender=Comment) def update_article_comment_count(instance, **kwargs): article = instance.article model_class = instance._meta.model count = model_class.objects.filter( approved='yes', article=article ).count() if article.comment_num != count: article.comment_num = count article.save() @receiver(models.signals.post_delete, sender=Comment) def delete_article_comment_count(instance, **kwargs): article = instance.article model_class = instance._meta.model count = model_class.objects.filter( approved='yes', article=article ).count() if article.comment_num != count: article.comment_num = count article.save() 接入两个信号用来更新文章评论数 Akismet python3 的开源库 pykismet3 用法很简单,但需要注册一个账户,根据博客 blog_url 来生成一个 api_key 。 下面是 pykismet3 的用法: # https://github.com/grundleborg/pykismet3#example from pykismet3 import Akismet import os a = Akismet(blog_url="http://your.blog/url", user_agent="My Awesome Web App/0.0.1") a.api_key="YOUR_AKISMET_API_KEY" # Comment Check,检查评论是否为垃圾评论 a.check({'user_ip': os.environ['REMOTE_ADDR'], 'user_agent': os.environ['HTTP_USER_AGENT'], 'referrer': os.environ.get('HTTP_REFERER', 'unknown'), 'comment_content': 'I LIEK YOUR WEB SITE', 'comment_author': 'Comment Author', 'is_test': 1, }) # Submit Ham,纠正 Akismet 的错误:垃圾评论/非垃圾评论 a.submit_ham({'user_ip': os.environ['REMOTE_ADDR'], 'user_agent': os.environ['HTTP_USER_AGENT'], 'referrer': os.environ.get('HTTP_REFERER', 'unknown'), 'comment_content': 'I LIEK YOUR WEB SITE', 'comment_author': 'Comment Author', 'is_test': 1, }) # Submit Spam,提交为垃圾评论 a.submit_spam({'user_ip': os.environ['REMOTE_ADDR'], 'user_agent': os.environ['HTTP_USER_AGENT'], 'referrer': os.environ.get('HTTP_REFERER', 'unknown'), 'comment_content': 'I LIEK YOUR WEB SITE', 'comment_author': 'Comment Author', 'is_test': 1, }) 这一块,我们写在非登录用户评论的业务逻辑里面即可,通过 proj/settings.py 来配置 Akismet 的 blog_url 和 api_key。 通过3篇文章,终于把重要的模型类都讲完,模型的设计特别重要,合理的模型设计能够很大程度的减少你的代码量。 下一篇文章将继续我们的教程,主要讲述博客的URLs设计:《Django 个人博客教程-05:管理站点与博客URLs》。 系列文章: 《Django 个人博客教程-00:开篇》 《Django 个人博客教程-01:开发环境与项目初始化》 《Django 个人博客教程-02:自定义字段与模型抽象类》 《Django 个人博客教程-03:用户模型和博客链接》 《Django 个人博客教程-04:文章模型和评论模型》当前文章 《Django 个人博客教程-05:管理站点与博客URLs》下一篇 如果觉得文章对你有用,请分享给其他人或点击在看