diff --git a/models.py b/models.py new file mode 100644 index 0000000..2a148ca --- /dev/null +++ b/models.py @@ -0,0 +1,553 @@ +#zf:导入日志模块,用于记录日志信息 +import logging +#zf:导入正则表达式模块,用于匹配文章中的图片 +import re +#zf:从abc模块导入abstractmethod装饰器 +from abc import abstractmethod + +#zf:导入Django配置模块 +from django.conf import settings +#zf:导入Django验证错误异常 +from django.core.exceptions import ValidationError +#zf:导入Django数据库模型模块 +from django.db import models +#zf:导入Django URL反向解析函数 +from django.urls import reverse +#zf:导入Django时区工具 +from django.utils.timezone import now +#zf:导入Django国际化翻译函数 +from django.utils.translation import gettext_lazy as _ +#zf:导入Markdown编辑器字段 +from mdeditor.fields import MDTextField +#zf:导入slugify函数,用于生成slug +from uuslug import slugify + +#zf:从djangoblog.utils导入缓存装饰器和缓存工具 +from djangoblog.utils import cache_decorator, cache +#zf:从djangoblog.utils导入获取当前站点的函数 +from djangoblog.utils import get_current_site + +#zf:创建日志记录器 +logger = logging.getLogger(__name__) + + +#zf:定义链接显示类型枚举类,继承自TextChoices +class LinkShowType(models.TextChoices): + #zf:首页显示 + I = ('i', _('index')) + #zf:列表页显示 + L = ('l', _('list')) + #zf:文章页显示 + P = ('p', _('post')) + #zf:全站显示 + A = ('a', _('all')) + #zf:幻灯片显示 + S = ('s', _('slide')) + + +#zf:定义基础模型类,继承自Django的Model类 +class BaseModel(models.Model): + #zf:主键字段 + id = models.AutoField(primary_key=True) + #zf:创建时间字段 + creation_time = models.DateTimeField(_('creation time'), default=now) + #zf:最后修改时间字段 + last_modify_time = models.DateTimeField(_('modify time'), default=now) + + #zf:重写save方法 + def save(self, *args, **kwargs): + #zf:检查是否是更新文章浏览量的特殊情况 + is_update_views = isinstance( + self, + Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] + if is_update_views: + #zf:如果是更新浏览量,则直接更新数据库,避免触发其他逻辑 + Article.objects.filter(pk=self.pk).update(views=self.views) + else: + #zf:如果有slug字段,则生成slug + if 'slug' in self.__dict__: + slug = getattr( + self, 'title') if 'title' in self.__dict__ else getattr( + self, 'name') + setattr(self, 'slug', slugify(slug)) + #zf:调用父类的save方法 + super().save(*args, **kwargs) + + #zf:获取完整URL方法 + def get_full_url(self): + #zf:获取当前站点域名 + site = get_current_site().domain + #zf:拼接完整URL + url = "https://{site}{path}".format(site=site, + path=self.get_absolute_url()) + return url + + #zf:设置为抽象类 + class Meta: + abstract = True + + #zf:定义抽象方法,子类必须实现 + @abstractmethod + def get_absolute_url(self): + pass + + +#zf:定义文章模型类,继承自BaseModel +class Article(BaseModel): + """文章""" + #zf:文章状态选项 + STATUS_CHOICES = ( + #zf:草稿 + ('d', _('Draft')), + #zf:已发布 + ('p', _('Published')), + ) + #zf:评论状态选项 + COMMENT_STATUS = ( + #zf:开启评论 + ('o', _('Open')), + #zf:关闭评论 + ('c', _('Close')), + ) + #zf:文章类型选项 + TYPE = ( + #zf:文章 + ('a', _('Article')), + #zf:页面 + ('p', _('Page')), + ) + + #zf:标题字段 + title = models.CharField(_('title'), max_length=200, unique=True) + #zf:正文字段,使用Markdown编辑器 + body = MDTextField(_('body')) + #zf:发布时间字段 + pub_time = models.DateTimeField( + _('publish time'), blank=False, null=False, default=now) + #zf:状态字段 + status = models.CharField( + _('status'), + max_length=1, + choices=STATUS_CHOICES, + default='p') + #zf:评论状态字段 + comment_status = models.CharField( + _('comment status'), + max_length=1, + choices=COMMENT_STATUS, + default='o') + #zf:类型字段 + type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') + #zf:浏览量字段 + views = models.PositiveIntegerField(_('views'), default=0) + #zf:作者字段,外键关联到用户模型 + author = models.ForeignKey( + settings.AUTH_USER_MODEL, + verbose_name=_('author'), + blank=False, + null=False, + on_delete=models.CASCADE) + #zf:文章排序字段 + article_order = models.IntegerField( + _('order'), blank=False, null=False, default=0) + #zf:是否显示目录字段 + show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) + #zf:分类字段,外键关联到Category模型 + category = models.ForeignKey( + 'Category', + verbose_name=_('category'), + on_delete=models.CASCADE, + blank=False, + null=False) + #zf:标签字段,多对多关联到Tag模型 + tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) + + #zf:将文章正文转换为字符串 + def body_to_string(self): + return self.body + + #zf:字符串表示方法 + def __str__(self): + return self.title + + #zf:模型元数据 + class Meta: + #zf:排序规则 + ordering = ['-article_order', '-pub_time'] + #zf:单数形式显示名称 + verbose_name = _('article') + #zf:复数形式显示名称 + verbose_name_plural = verbose_name + #zf:latest()方法使用的字段 + get_latest_by = 'id' + + #zf:获取绝对URL方法 + def get_absolute_url(self): + return reverse('blog:detailbyid', kwargs={ + 'article_id': self.id, + 'year': self.creation_time.year, + 'month': self.creation_time.month, + 'day': self.creation_time.day + }) + + #zf:获取分类树方法,使用缓存装饰器缓存10小时 + @cache_decorator(60 * 60 * 10) + def get_category_tree(self): + #zf:获取分类的分类树 + tree = self.category.get_category_tree() + #zf:将分类名称和URL组成元组列表 + names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) + + return names + + #zf:保存方法 + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + + #zf:增加浏览量方法 + def viewed(self): + self.views += 1 + self.save(update_fields=['views']) + + #zf:获取评论列表方法 + def comment_list(self): + #zf:构造缓存键名 + cache_key = 'article_comments_{id}'.format(id=self.id) + #zf:从缓存中获取评论列表 + value = cache.get(cache_key) + if value: + #zf:如果缓存中有数据,记录日志并返回 + logger.info('get article comments:{id}'.format(id=self.id)) + return value + else: + #zf:如果缓存中没有数据,从数据库查询并缓存 + comments = self.comment_set.filter(is_enable=True).order_by('-id') + cache.set(cache_key, comments, 60 * 100) + logger.info('set article comments:{id}'.format(id=self.id)) + return comments + + #zf:获取管理后台URL方法 + def get_admin_url(self): + #zf:获取模型的app_label和model_name + info = (self._meta.app_label, self._meta.model_name) + #zf:生成管理后台编辑页面的URL + return reverse('admin:%s_%s_change' % info, args=(self.pk,)) + + #zf:获取下一篇文章方法,使用缓存装饰器缓存100分钟 + @cache_decorator(expiration=60 * 100) + def next_article(self): + #zf:下一篇 + return Article.objects.filter( + id__gt=self.id, status='p').order_by('id').first() + + #zf:获取上一篇文章方法,使用缓存装饰器缓存100分钟 + @cache_decorator(expiration=60 * 100) + def prev_article(self): + #zf:前一篇 + return Article.objects.filter(id__lt=self.id, status='p').first() + + #zf:获取文章中第一张图片的URL方法 + def get_first_image_url(self): + """ + Get the first image url from article.body. + :return: + """ + #zf:使用正则表达式匹配Markdown图片语法 + match = re.search(r'!\[.*?\]\((.+?)\)', self.body) + if match: + #zf:如果匹配到图片,返回图片URL + return match.group(1) + #zf:如果没有匹配到图片,返回空字符串 + return "" + + +#zf:定义分类模型类,继承自BaseModel +class Category(BaseModel): + """文章分类""" + #zf:分类名称字段 + name = models.CharField(_('category name'), max_length=30, unique=True) + #zf:父级分类字段,外键关联到自身 + parent_category = models.ForeignKey( + 'self', + verbose_name=_('parent category'), + blank=True, + null=True, + on_delete=models.CASCADE) + #zf:slug字段 + slug = models.SlugField(default='no-slug', max_length=60, blank=True) + #zf:索引字段,用于排序 + index = models.IntegerField(default=0, verbose_name=_('index')) + + #zf:模型元数据 + class Meta: + #zf:按索引降序排列 + ordering = ['-index'] + #zf:单数形式显示名称 + verbose_name = _('category') + #zf:复数形式显示名称 + verbose_name_plural = verbose_name + + #zf:获取绝对URL方法 + def get_absolute_url(self): + return reverse( + 'blog:category_detail', kwargs={ + 'category_name': self.slug}) + + #zf:字符串表示方法 + def __str__(self): + return self.name + + #zf:获取分类树方法,使用缓存装饰器缓存10小时 + @cache_decorator(60 * 60 * 10) + def get_category_tree(self): + """ + 递归获得分类目录的父级 + :return: + """ + #zf:初始化分类列表 + categorys = [] + + #zf:递归解析分类树的内部函数 + def parse(category): + #zf:将当前分类添加到列表 + categorys.append(category) + #zf:如果有父级分类,递归处理父级分类 + if category.parent_category: + parse(category.parent_category) + + #zf:从当前分类开始解析 + parse(self) + return categorys + + #zf:获取子分类方法,使用缓存装饰器缓存10小时 + @cache_decorator(60 * 60 * 10) + def get_sub_categorys(self): + """ + 获得当前分类目录所有子集 + :return: + """ + #zf:初始化分类列表 + categorys = [] + #zf:获取所有分类 + all_categorys = Category.objects.all() + + #zf:递归解析子分类的内部函数 + def parse(category): + #zf:如果分类不在列表中,添加到列表 + if category not in categorys: + categorys.append(category) + #zf:获取当前分类的子分类 + childs = all_categorys.filter(parent_category=category) + #zf:遍历子分类 + for child in childs: + #zf:如果子分类不在列表中,添加到列表 + if category not in categorys: + categorys.append(child) + #zf:递归处理子分类 + parse(child) + + #zf:从当前分类开始解析 + parse(self) + return categorys + + +#zf:定义标签模型类,继承自BaseModel +class Tag(BaseModel): + """文章标签""" + #zf:标签名称字段 + name = models.CharField(_('tag name'), max_length=30, unique=True) + #zf:slug字段 + slug = models.SlugField(default='no-slug', max_length=60, blank=True) + + #zf:字符串表示方法 + def __str__(self): + return self.name + + #zf:获取绝对URL方法 + def get_absolute_url(self): + return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) + + #zf:获取文章数量方法,使用缓存装饰器缓存10小时 + @cache_decorator(60 * 60 * 10) + def get_article_count(self): + #zf:统计包含该标签的文章数量 + return Article.objects.filter(tags__name=self.name).distinct().count() + + #zf:模型元数据 + class Meta: + #zf:按名称升序排列 + ordering = ['name'] + #zf:单数形式显示名称 + verbose_name = _('tag') + #zf:复数形式显示名称 + verbose_name_plural = verbose_name + + +#zf:定义友情链接模型类 +class Links(models.Model): + """友情链接""" + + #zf:链接名称字段 + name = models.CharField(_('link name'), max_length=30, unique=True) + #zf:链接地址字段 + link = models.URLField(_('link')) + #zf:排序字段 + sequence = models.IntegerField(_('order'), unique=True) + #zf:是否显示字段 + is_enable = models.BooleanField( + _('is show'), default=True, blank=False, null=False) + #zf:显示类型字段 + show_type = models.CharField( + _('show type'), + max_length=1, + choices=LinkShowType.choices, + default=LinkShowType.I) + #zf:创建时间字段 + creation_time = models.DateTimeField(_('creation time'), default=now) + #zf:最后修改时间字段 + last_mod_time = models.DateTimeField(_('modify time'), default=now) + + #zf:模型元数据 + class Meta: + #zf:按排序字段升序排列 + ordering = ['sequence'] + #zf:单数形式显示名称 + verbose_name = _('link') + #zf:复数形式显示名称 + verbose_name_plural = verbose_name + + #zf:字符串表示方法 + def __str__(self): + return self.name + + +#zf:定义侧边栏模型类 +class SideBar(models.Model): + """侧边栏,可以展示一些html内容""" + #zf:标题字段 + name = models.CharField(_('title'), max_length=100) + #zf:内容字段 + content = models.TextField(_('content')) + #zf:排序字段 + sequence = models.IntegerField(_('order'), unique=True) + #zf:是否启用字段 + is_enable = models.BooleanField(_('is enable'), default=True) + #zf:创建时间字段 + creation_time = models.DateTimeField(_('creation time'), default=now) + #zf:最后修改时间字段 + last_mod_time = models.DateTimeField(_('modify time'), default=now) + + #zf:模型元数据 + class Meta: + #zf:按排序字段升序排列 + ordering = ['sequence'] + #zf:单数形式显示名称 + verbose_name = _('sidebar') + #zf:复数形式显示名称 + verbose_name_plural = verbose_name + + #zf:字符串表示方法 + def __str__(self): + return self.name + + +#zf:定义博客设置模型类 +class BlogSettings(models.Model): + """blog的配置""" + #zf:网站名称字段 + site_name = models.CharField( + _('site name'), + max_length=200, + null=False, + blank=False, + default='') + #zf:网站描述字段 + site_description = models.TextField( + _('site description'), + max_length=1000, + null=False, + blank=False, + default='') + #zf:网站SEO描述字段 + site_seo_description = models.TextField( + _('site seo description'), max_length=1000, null=False, blank=False, default='') + #zf:网站关键词字段 + site_keywords = models.TextField( + _('site keywords'), + max_length=1000, + null=False, + blank=False, + default='') + #zf:文章摘要长度字段 + article_sub_length = models.IntegerField(_('article sub length'), default=300) + #zf:侧边栏文章数量字段 + sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) + #zf:侧边栏评论数量字段 + sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) + #zf:文章页面默认显示评论数量字段 + article_comment_count = models.IntegerField(_('article comment count'), default=5) + #zf:是否显示谷歌广告字段 + show_google_adsense = models.BooleanField(_('show adsense'), default=False) + #zf:谷歌广告代码字段 + google_adsense_codes = models.TextField( + _('adsense code'), max_length=2000, null=True, blank=True, default='') + #zf:是否开启网站评论功能字段 + open_site_comment = models.BooleanField(_('open site comment'), default=True) + #zf:全局头部内容字段 + global_header = models.TextField("公共头部", null=True, blank=True, default='') + #zf:全局尾部内容字段 + global_footer = models.TextField("公共尾部", null=True, blank=True, default='') + #zf:备案号字段 + beian_code = models.CharField( + '备案号', + max_length=2000, + null=True, + blank=True, + default='') + #zf:网站统计代码字段 + analytics_code = models.TextField( + "网站统计代码", + max_length=1000, + null=False, + blank=False, + default='') + #zf:是否显示公安备案号字段 + show_gongan_code = models.BooleanField( + '是否显示公安备案号', default=False, null=False) + #zf:公安备案号字段 + gongan_beiancode = models.TextField( + '公安备案号', + max_length=2000, + null=True, + blank=True, + default='') + #zf:评论是否需要审核字段 + comment_need_review = models.BooleanField( + '评论是否需要审核', default=False, null=False) + + #zf:模型元数据 + class Meta: + #zf:单数形式显示名称 + verbose_name = _('Website configuration') + #zf:复数形式显示名称 + verbose_name_plural = verbose_name + + #zf:字符串表示方法 + def __str__(self): + return self.site_name + + #zf:数据验证方法 + def clean(self): + #zf:检查是否已存在其他配置记录 + if BlogSettings.objects.exclude(id=self.id).count(): + #zf:如果已存在其他配置记录,抛出验证错误 + raise ValidationError(_('There can only be one configuration')) + + #zf:保存方法 + def save(self, *args, **kwargs): + #zf:调用父类的保存方法 + super().save(*args, **kwargs) + #zf:导入缓存工具 + from djangoblog.utils import cache + #zf:清除所有缓存 + cache.clear() \ No newline at end of file