import logging from abc import abstractmethod from django.conf import settings from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from mdeditor.fields import MDTextField from uuslug import slugify from djangoblog.utils import cache_decorator, cache from djangoblog.utils import get_current_site logger = logging.getLogger(__name__) class LinkShowType(models.TextChoices): I = ('i', '首页') L = ('l', '列表页') P = ('p', '文章页面') A = ('a', '全站') S = ('s', '友情链接页面') class BaseModel(models.Model): id = models.AutoField(primary_key=True) created_time = models.DateTimeField('创建时间', default=now) last_mod_time = models.DateTimeField('修改时间', default=now) def save(self, *args, **kwargs): is_update_views = isinstance( self, Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] if is_update_views: Article.objects.filter(pk=self.pk).update(views=self.views) else: if 'slug' in self.__dict__: slug = getattr( self, 'title') if 'title' in self.__dict__ else getattr( self, 'name') setattr(self, 'slug', slugify(slug)) super().save(*args, **kwargs) def get_full_url(self): site = get_current_site().domain url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) return url class Meta: abstract = True @abstractmethod def get_absolute_url(self): pass class Article(BaseModel): """文章""" STATUS_CHOICES = ( ('d', '草稿'), ('p', '发表'), ) COMMENT_STATUS = ( ('o', '打开'), ('c', '关闭'), ) TYPE = ( ('a', '文章'), ('p', '页面'), ) title = models.CharField('标题', max_length=200, unique=True) body = MDTextField('正文') pub_time = models.DateTimeField( '发布时间', blank=False, null=False, default=now) status = models.CharField( '文章状态', max_length=1, choices=STATUS_CHOICES, default='p') comment_status = models.CharField( '评论状态', max_length=1, choices=COMMENT_STATUS, default='o') type = models.CharField('类型', max_length=1, choices=TYPE, default='a') views = models.PositiveIntegerField('浏览量', default=0) author = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name='作者', blank=False, null=False, on_delete=models.CASCADE) article_order = models.IntegerField( '排序,数字越大越靠前', blank=False, null=False, default=0) show_toc = models.BooleanField("是否显示toc目录", blank=False, null=False, default=False) category = models.ForeignKey( 'Category', verbose_name='分类', on_delete=models.CASCADE, blank=False, null=False) tags = models.ManyToManyField('Tag', verbose_name='标签集合', blank=True) def body_to_string(self): return self.body def __str__(self): return self.title class Meta: ordering = ['-article_order', '-pub_time'] verbose_name = "文章" verbose_name_plural = verbose_name get_latest_by = 'id' def get_absolute_url(self): return reverse('blog:detailbyid', kwargs={ 'article_id': self.id, 'year': self.created_time.year, 'month': self.created_time.month, 'day': self.created_time.day }) @cache_decorator(60 * 60 * 10) def get_category_tree(self): tree = self.category.get_category_tree() names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) return names def save(self, *args, **kwargs): super().save(*args, **kwargs) def viewed(self): self.views += 1 self.save(update_fields=['views']) def comment_list(self): cache_key = 'article_comments_{id}'.format(id=self.id) value = cache.get(cache_key) if value: logger.info('get article comments:{id}'.format(id=self.id)) return value else: 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 def get_admin_url(self): info = (self._meta.app_label, self._meta.model_name) return reverse('admin:%s_%s_change' % info, args=(self.pk,)) @cache_decorator(expiration=60 * 100) def next_article(self): # 下一篇 return Article.objects.filter( id__gt=self.id, status='p').order_by('id').first() @cache_decorator(expiration=60 * 100) def prev_article(self): # 前一篇 return Article.objects.filter(id__lt=self.id, status='p').first() class Category(BaseModel): """文章分类""" name = models.CharField('分类名', max_length=30, unique=True) parent_category = models.ForeignKey( 'self', verbose_name="父级分类", blank=True, null=True, on_delete=models.CASCADE) slug = models.SlugField(default='no-slug', max_length=60, blank=True) index = models.IntegerField(default=0, verbose_name="权重排序-越大越靠前") class Meta: ordering = ['-index'] verbose_name = "分类" verbose_name_plural = verbose_name def get_absolute_url(self): return reverse( 'blog:category_detail', kwargs={ 'category_name': self.slug}) def __str__(self): return self.name @cache_decorator(60 * 60 * 10) def get_category_tree(self): """ 递归获得分类目录的父级 :return: """ categorys = [] def parse(category): categorys.append(category) if category.parent_category: parse(category.parent_category) parse(self) return categorys @cache_decorator(60 * 60 * 10) def get_sub_categorys(self): """ 获得当前分类目录所有子集 :return: """ categorys = [] all_categorys = Category.objects.all() def parse(category): if category not in categorys: categorys.append(category) childs = all_categorys.filter(parent_category=category) for child in childs: if category not in categorys: categorys.append(child) parse(child) parse(self) return categorys class Tag(BaseModel): """文章标签""" name = models.CharField('标签名', max_length=30, unique=True) slug = models.SlugField(default='no-slug', max_length=60, blank=True) def __str__(self): return self.name def get_absolute_url(self): return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) @cache_decorator(60 * 60 * 10) def get_article_count(self): return Article.objects.filter(tags__name=self.name).distinct().count() class Meta: ordering = ['name'] verbose_name = "标签" verbose_name_plural = verbose_name class Links(models.Model): """友情链接""" name = models.CharField('链接名称', max_length=30, unique=True) link = models.URLField('链接地址') sequence = models.IntegerField('排序', unique=True) is_enable = models.BooleanField( '是否显示', default=True, blank=False, null=False) show_type = models.CharField( '显示类型', max_length=1, choices=LinkShowType.choices, default=LinkShowType.I) created_time = models.DateTimeField('创建时间', default=now) last_mod_time = models.DateTimeField('修改时间', default=now) class Meta: ordering = ['sequence'] verbose_name = '友情链接' verbose_name_plural = verbose_name def __str__(self): return self.name class SideBar(models.Model): """侧边栏,可以展示一些html内容""" name = models.CharField('标题', max_length=100) content = models.TextField("内容") sequence = models.IntegerField('排序', unique=True) is_enable = models.BooleanField('是否启用', default=True) created_time = models.DateTimeField('创建时间', default=now) last_mod_time = models.DateTimeField('修改时间', default=now) class Meta: ordering = ['sequence'] verbose_name = '侧边栏' verbose_name_plural = verbose_name def __str__(self): return self.name class BlogSettings(models.Model): """blog的配置""" sitename = models.CharField( "网站名称", max_length=200, null=False, blank=False, default='') site_description = models.TextField( "网站描述", max_length=1000, null=False, blank=False, default='') site_seo_description = models.TextField( "网站SEO描述", max_length=1000, null=False, blank=False, default='') site_keywords = models.TextField( "网站关键字", max_length=1000, null=False, blank=False, default='') article_sub_length = models.IntegerField("文章摘要长度", default=300) sidebar_article_count = models.IntegerField("侧边栏文章数目", default=10) sidebar_comment_count = models.IntegerField("侧边栏评论数目", default=5) article_comment_count = models.IntegerField("文章评论数目", default=5) show_google_adsense = models.BooleanField('是否显示谷歌广告', default=False) google_adsense_codes = models.TextField( '广告内容', max_length=2000, null=True, blank=True, default='') open_site_comment = models.BooleanField('是否打开网站评论功能', default=True) beiancode = models.CharField( '备案号', max_length=2000, null=True, blank=True, default='') analyticscode = models.TextField( "网站统计代码", max_length=1000, null=False, blank=False, default='') show_gongan_code = models.BooleanField( '是否显示公安备案号', default=False, null=False) gongan_beiancode = models.TextField( '公安备案号', max_length=2000, null=True, blank=True, default='') resource_path = models.CharField( "静态文件保存地址", max_length=300, null=False, default='/var/www/resource/') class Meta: verbose_name = '网站配置' verbose_name_plural = verbose_name def __str__(self): return self.sitename def clean(self): if BlogSettings.objects.exclude(id=self.id).count(): raise ValidationError(_('只能有一个配置')) def save(self, *args, **kwargs): super().save(*args, **kwargs) from djangoblog.utils import cache cache.clear()