Django网站(三):博客APP——创建与models设计
使用python manage.py startapp blog
创建一个名为blog的app。之后将在这个app下面完成网站的第一个功能:发布文章。
网站结构
整个网站是一个树形结构,如下所示。
- 首页
- 分类一
- 文章一
- 资源
- 子分类一
- 文章二
- 文章一
首页是一个单独设计的页面,要求有各种炫酷的效果来吸引人。分类是网站结构的主干,分类的子节点可以是分类或者文章。文章是网站的基础展示页面,包含了大段文字和图片文件等资源。
首页 子分类文章的父节点必须是分类。隶属于顶级分类的文章和隶属于次级分类的文章在显示效果上有所不同。
文章 文章models设计
由上文分析的网站结构结构,主要有三种对象:分类(Category),文章(Article),资源(Resource)。这三种对象有一些共有的信息,如名称、创建者等,可以通过抽象类的方法创建一个基础信息(CommonInfo)类,来减少代码量。
基础信息(抽象类)
为了方便管理,我们需要确定三种对象的创建时间、创建者、修改时间、修改者等,所以基础信息(CommonInfo)中主要有以下几个字段。
字段名称 | 字段类型 | verbose_name | 说明 |
---|---|---|---|
name | CharField | 名称 | 便于理解的名字 |
timeModify | DateTimeField | 修改时间 | 修改时间 |
userModify | ForeignKey(to=User) | 修改者 | 修改者 |
timeCreate | DateTimeField | 创建时间 | 创建时间 |
userCreate | ForeignKey(to=User) | 创建者 | 创建者 |
需要注意的是,userModify和userCreate都是指向User的ForeignKey,所以需要指定related_name。因为CommonInfo是抽象类,所以related_name不能使用固定的字符串,而需要加入%(app_label)s
和%(class)s
,根据具体的类名动态地生成related_name。
这个抽象model的具体代码如下。
class CommonInfo(models.Model):
# editable
name = models.CharField(max_length=25,
blank=False,
verbose_name='名称',
help_text='不允许与其他条目重复。')
# not editable (used to record)
timeModify = models.DateTimeField(default=timezone.now,
editable=False,
verbose_name='修改时间')
userModify = models.ForeignKey(to=User,
on_delete=models.SET_NULL,
null=True,
editable=False,
related_name='%(app_label)s_%(class)s_modified',
verbose_name='修改者')
timeCreate = models.DateTimeField(default=timezone.now,
editable=False,
verbose_name='创建时间')
userCreate = models.ForeignKey(to=User,
on_delete=models.SET_NULL,
null=True,
editable=False,
related_name='%(app_label)s_%(class)s_created',
verbose_name='创建者')
class Meta:
abstract = True
db_table = '%(app_label)s_%(class)s'
分类
分类组成了网站结构的主干,所有的文章都隶属于某个分类。分类应该采用树的结构实现。幸运的是,django-mptt库已经为我们准备好了一切。想要了解具体原理的,可以看残阳似血的博客。
除了继承自CommonInfo的字段外,分类(Category)的字段只有一个父节点。
字段名称 | 字段类型 | verbose_name | 说明 |
---|---|---|---|
parent | TreeForeignKey | 上级目录 | 上级目录 |
分类model的具体代码如下。
class Category(MPTTModel, CommonInfo):
# Tree
parent = TreeForeignKey(to='self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='children',
verbose_name='上级目录',
help_text='若上级目录为空,则此目录为顶级目录。')
class MPTTMeta:
order_insertion_by = []
level_attr = 'level'
class Meta:
verbose_name = '目录'
verbose_name_plural = '目录'
文章
如果说分类是网站的骨架,那么文章就是网站的血肉。文章的主体内容使用Markdown编写,使用markdown库转换成HTML格式。用Markdown格式存储有利于反复修改,用HTML格式存储有利于快速响应request。所以我在设计的时候选择了牺牲空间来换取时间,将这两种格式的文本都存储在数据库中。
除了继承自CommonInfo的字段外,文章(Article)的字段如下。
字段名称 | 字段类型 | verbose_name | 说明 |
---|---|---|---|
cateogry | TreeForeignKey | 类别 | 文章所属分类 |
markdownBody | TextField | 正文 | 用Markdown格式记录的正文 |
body | TextField | (None) | 用HTML格式记录的正文 |
timePublish | DateTimeField | 发布时间 | 在这个时间后文章才可以被看到 |
status | BooleanField | 发布状态 | 是否已经发布 |
为了便于管理,文章类还需要在增删查改四种权限之外额外增加一个发布权限。
文章model的具体代码如下。
class Article(CommonInfo):
category = TreeForeignKey(to=Category,
on_delete=models.CASCADE,
null=True,
verbose_name='类别')
markdownBody = models.TextField(default=' ',
blank=True,
verbose_name='正文')
timePublish = models.DateTimeField(blank=True,
null=True,
verbose_name='发布时间')
status = models.BooleanField(default=False,
verbose_name='发布状态')
body = models.TextField(default=' ',
blank=True,
editable=False)
class Meta:
verbose_name_plural = '文章'
verbose_name = '文章'
permissions = (
('publish_article', 'Can publish 文章'),
)
资源
一篇文章中通常会包含各种图片、附件,这些文件我统称为资源(Resource)。在我的设计中,文章和资源是一对多的关系。虽然在某些情况下文章与图片是多对多的关系,例如两篇文章可能会使用到相同的图片,但是多对多关系的管理太过复杂。所以,在这里我牺牲存储空间来换取操作的简易性。
除了继承自CommonInfo的字段外,资源(Resource)的字段如下。
字段名称 | 字段类型 | verbose_name | 说明 |
---|---|---|---|
article | ForeignKey | 文章 | 资源所属的文章 |
file | FileField | 文件 | 资源的具体内容 |
需要注意的是,FileField有一个名为upload_to的参数,用于控制上传的文件保存到哪里。这个参数可以是一个字符串,也可以是一个函数func(instance, filename): str
。如果使用函数,则函数应当接收两个参数,返回值是文件存储位置相对于MEDIA_ROOT的相对路径。注意,这个函数返回值给出的是文件的相对路径而非文件所在文件夹的相对路径。具体细节参照Django官方FileField文档。
为了便于管理,我所设计的上传文件存储路径是blog/category_id/subcategory_id/.../article_id/file_hash.ext
。具体的函数如下。首先,将文件名替换为文件内容的hash,同时保持文件扩展名不变。然后,获取资源所有祖先节点的id。最后,在前面加上blog,按顺序拼接成路径。
def storage_redirect(instance, name):
name = '%x' % hash(instance.file) + '.' + name.split('.')[-1]
ids = list(map(str, instance.article.category.get_ancestors(include_self=True).values_list('id', flat=True)))
path = os.path.join(*ids)
return os.path.join('blog', path, str(instance.article_id), name)
资源model的具体代码如下。
class Resource(CommonInfo):
file = models.FileField(upload_to=storage_redirect,
verbose_name='文件')
article = models.ForeignKey(to=Article,
on_delete=models.CASCADE,
verbose_name='文章')
def delete(self, using=None, keep_parents=False):
path = os.path.join(settings.MEDIA_ROOT, self.file.name)
if os.path.exists(path):
os.remove(path)
return super().delete(using, keep_parents)
class Meta:
verbose_name_plural = '资源'
verbose_name = '资源'
以上就是blog app中models.py的主要内容。
网友评论