diff --git a/article/migrations/0003_category_alter_article_options_alter_article_id_and_more.py b/article/migrations/0003_category_alter_article_options_alter_article_id_and_more.py new file mode 100644 index 0000000..65e3e10 --- /dev/null +++ b/article/migrations/0003_category_alter_article_options_alter_article_id_and_more.py @@ -0,0 +1,40 @@ +# Generated by Django 4.1.1 on 2022-09-23 16:26 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('article', '0002_article_author'), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=100)), + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ], + options={ + 'ordering': ['-created'], + }, + ), + migrations.AlterModelOptions( + name='article', + options={'ordering': ['-created']}, + ), + migrations.AlterField( + model_name='article', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AddField( + model_name='article', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='articles', to='article.category'), + ), + ] diff --git a/article/migrations/0004_tag_article_tag.py b/article/migrations/0004_tag_article_tag.py new file mode 100644 index 0000000..9c29c53 --- /dev/null +++ b/article/migrations/0004_tag_article_tag.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1.1 on 2022-09-23 18:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('article', '0003_category_alter_article_options_alter_article_id_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.CharField(max_length=30)), + ], + options={ + 'ordering': ['-id'], + }, + ), + migrations.AddField( + model_name='article', + name='tag', + field=models.ManyToManyField(blank=True, related_name='articles', to='article.tag'), + ), + ] diff --git a/article/migrations/0005_rename_tag_article_tags.py b/article/migrations/0005_rename_tag_article_tags.py new file mode 100644 index 0000000..e7f8ea9 --- /dev/null +++ b/article/migrations/0005_rename_tag_article_tags.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.1 on 2022-09-23 18:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('article', '0004_tag_article_tag'), + ] + + operations = [ + migrations.RenameField( + model_name='article', + old_name='tag', + new_name='tags', + ), + ] diff --git a/article/models.py b/article/models.py index 2181105..7cc78d4 100644 --- a/article/models.py +++ b/article/models.py @@ -1,10 +1,39 @@ from django.db import models from django.utils import timezone from django.contrib.auth.models import User +from markdown import Markdown + + +class Category(models.Model): + """文章分类""" + # 分类名称 + title = models.CharField(max_length=100) + created = models.DateTimeField(default=timezone.now) + + class Meta: + ordering = ['-created'] + + def __str__(self): + return self.title + + +class Tag(models.Model): + """文章标签""" + text = models.CharField(max_length=30) + + class Meta: + ordering = ['-id'] + + def __str__(self): + return self.text # 博客文章 model class Article(models.Model): + class Meta: + # 按创建时间降序排列 + ordering = ['-created'] + # 作者 author = models.ForeignKey( User, @@ -12,6 +41,24 @@ class Article(models.Model): on_delete=models.CASCADE, related_name='articles' ) + + # 分类 + category = models.ForeignKey( + Category, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='articles' + ) + + # 标签 + # 一篇文章可以有多个tag,一个tag可以属于多篇文章 + tags = models.ManyToManyField( + Tag, + blank=True, + related_name='articles', + ) + # 标题 title = models.CharField(max_length=100) # 正文 @@ -23,3 +70,18 @@ class Article(models.Model): def __str__(self): return self.title + + # 新增方法,将body转换为带html标签的正文 + def get_md(self): + md = Markdown( + extensions=[ + 'markdown.extensions.extra', + 'markdown.extensions.codehilite', + 'markdown.extensions.toc', + ] + ) + md_body = md.convert(self.body) + return md_body, md.toc + + + diff --git a/article/serializers.py b/article/serializers.py index 79831fb..26d33cb 100644 --- a/article/serializers.py +++ b/article/serializers.py @@ -1,29 +1,128 @@ from rest_framework import serializers -from article.models import Article +from article.models import Article, Category, Tag from user_info.serializers import UserDescSerializer -# 返回文章列表或创建一篇文章 -class ArticleListSerializer(serializers.ModelSerializer): +class CategorySerializer(serializers.ModelSerializer): + """所有分类的序列化器""" + # 将路由间的表示转换为超链接 + # category-detail是自动注册路由时, Router默认帮你设置的详情页面的名称 + url = serializers.HyperlinkedIdentityField(view_name='category-detail') + + class Meta: + model = Category + fields = '__all__' + # 创建时间不能修改 + read_only_fields = ['created'] + + +class ArticleBaseSerializer(serializers.HyperlinkedModelSerializer): + """将原来的ArticleSerializer抽象出一个父类""" author = UserDescSerializer(read_only=True) - # 使用article的url.py中的view --> detail - url = serializers.HyperlinkedIdentityField(view_name="article:detail") + # 希望文章接口不仅仅只返回分类的id而已,所以需要显式指定category,将其变成一个嵌套数据, + # 分类的嵌套序列化字段 + category = CategorySerializer(read_only=True) + # 显示指定category的id字段,用于创建/更新category外键 + category_id = serializers.IntegerField(write_only=True, allow_null=True, required=False) + # 新增tag字段, 直接显示Tag的text字段 + tags = serializers.SlugRelatedField( + queryset=Tag.objects.all(), + many=True, + required=False, + slug_field='text', + ) + + # 验证category_id是否正确 + def validate_category_id(self, value): + if not Category.objects.filter(id=value).exists() and value is not None: + raise serializers.ValidationError("Category with id {} not exist.".format(value)) + return value + + def to_internal_value(self, data): + tags_data = data.get('tags') + if isinstance(tags_data, list): + for text in tags_data: + # 不存在该标签则创建它 + if not Tag.objects.filter(text=text).exists(): + Tag.objects.create(text=text) + return super().to_internal_value(data) + + +class ArticleSerializer(ArticleBaseSerializer): + """文章序列化器""" + class Meta: + model = Article + fields = '__all__' + # body字段只可写不可见 + extra_kwargs = {'body': {'write_only': True}} + + +class ArticleDetailSerializer(ArticleBaseSerializer): + # 渲染后的正文 + body_html = serializers.SerializerMethodField() + # 渲染后的目录 + toc_html = serializers.SerializerMethodField() + + def get_body_html(self, obj): + return obj.get_md()[0] + + def get_toc_html(self, obj): + return obj.get_md()[1] + + class Meta: + model = Article + fields = '__all__' + + +class ArticleCategoryDetailSerializer(serializers.ModelSerializer): + """分类详情的嵌套序列化器""" + url = serializers.HyperlinkedIdentityField(view_name='article-detail') class Meta: model = Article fields = [ 'url', 'title', + ] + + +class CategoryDetailSerializer(serializers.ModelSerializer): + """具体的分类详情页不显示url""" + # 显示某个分类下的所有文章 + articles = ArticleCategoryDetailSerializer(many=True, read_only=True) + + class Meta: + model = Category + fields = [ + 'id', + 'title', 'created', - 'body', - 'author', + 'articles', ] - # read_only_fields = ['author'] - # 返回文章详情 +class TagSerializer(serializers.ModelSerializer): + # 显示url + # url = serializers.HyperlinkedIdentityField(view_name='tag-detail') + + """所有标签序列化器""" -class ArticleDetailSerializer(serializers.ModelSerializer): class Meta: - model = Article - fields = '__all__' \ No newline at end of file + model = Tag + fields = '__all__' + + # 创建或者更新前检查是否存在该tag + def check_tag_obj_exists(self, validated_data): + text = validated_data.get('text') + if Tag.objects.filter(text=text).exists(): + raise serializers.ValidationError('Tag with text {} exists.'.format(text)) + + def create(self, validated_data): + self.check_tag_obj_exists(validated_data) + return super(TagSerializer, self).create(validated_data) + + def update(self, instance, validated_data): + self.check_tag_obj_exists(validated_data) + return super(TagSerializer, self).update(instance, validated_data) + + diff --git a/article/views.py b/article/views.py index 4e16769..f8d9faf 100644 --- a/article/views.py +++ b/article/views.py @@ -1,101 +1,50 @@ -from article.models import Article -from article.serializers import ArticleListSerializer, ArticleDetailSerializer -from rest_framework.decorators import api_view -from rest_framework.response import Response -from rest_framework.views import APIView -from django.http import Http404 -from rest_framework import mixins -from rest_framework import generics -from rest_framework import status +from rest_framework import viewsets +from article.serializers import ArticleSerializer, CategorySerializer, CategoryDetailSerializer +from article.serializers import TagSerializer,ArticleDetailSerializer +from article.models import Article, Category, Tag from article.permissions import IsAdminUserOrReadOnly +from rest_framework.filters import SearchFilter -@api_view(['GET', 'POST']) -def article_list(request): - # 获取所有文章列表 - if request.method == 'GET': - articles = Article.objects.all() - serializer = ArticleListSerializer(articles, many=True) - return Response(serializer.data) - # 创建新的文章 - elif request.method == 'POST': - serializer = ArticleListSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +class ArticleViewSet(viewsets.ModelViewSet): + """文章视图集""" + queryset = Article.objects.all() + serializer_class = ArticleSerializer + permission_classes = [IsAdminUserOrReadOnly] + filter_backends = [SearchFilter, ] + # http://127.0.0.1:8000/api/article/?author__username=admin&title=第一篇文章 + # filter_fields = ['title', 'author__username',] # 精确查询 -# 和article_list()功能相同 -class ArticleList(generics.ListCreateAPIView): - # 添加用户权限 - permission_classes = [IsAdminUserOrReadOnly] - queryset = Article.objects.all() - serializer_class = ArticleListSerializer + # 下面两种字段都可以使用模糊查询 + search_fields = ('title', 'author__username') def perform_create(self, serializer): - # 在序列化数据真正保存之前调用 + # 在创建文章前,提供了视图集无法自行推断的用户外键字段。 serializer.save(author=self.request.user) + def get_serializer_class(self): + if self.action == 'list': + return ArticleSerializer + else: + return ArticleDetailSerializer -# 第一种版本 -# class ArticleDetail(APIView): -# """文章详情视图""" -# def get_object(self, pk): -# """获取单个文章对象""" -# try: -# # pk代表主键 -# return Article.objects.get(pk=pk) -# except: -# raise Http404 -# -# def get(self, request, pk): -# article = self.get_object(pk) -# serializer = ArticleDetailSerializer(article) -# # 返回Json数据 -# return Response(serializer.data) -# -# def put(self, request, pk): -# article = self.get_object(pk) -# serializer = ArticleDetailSerializer(article, data=request.data) -# # 验证提交的数据是否合法 -# # 不合法就返回400 -# if serializer.is_valid(): -# # 序列化器将持有的数据反序列化后 -# # 保存到数据库 -# serializer.save() -# return Response(serializer.data) -# return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -# -# def delete(self, request, pk): -# article = self.get_object(pk=pk) -# article.delete() -# # 删除成功后返回204 -# return Response(status=status.HTTP_204_NO_CONTENT) +class CategoryViewSet(viewsets.ModelViewSet): + """分类视图集""" + queryset = Category.objects.all() + serializer_class = CategorySerializer + permission_classes = [IsAdminUserOrReadOnly] -# 第二种版本 + def get_serializer_class(self): + if self.action == 'list': + return CategorySerializer + else: + return CategoryDetailSerializer -# class ArticleDetail(mixins.RetrieveModelMixin, -# mixins.UpdateModelMixin, -# mixins.DestroyModelMixin, -# generics.GenericAPIView): -# """文章详情视图""" -# queryset = Article.objects.all() -# # 序列化类 -# serializer_class = ArticleDetailSerializer -# -# def get(self, request, *args, **kwargs): -# return self.retrieve(request, *args, **kwargs) -# -# def put(self, request, *args, **kwargs): -# return self.update(request, *args, **kwargs) -# -# def delete(self, request, *args, **kwargs): -# return self.destroy(request, *args, **kwargs) -# ArticleDetail类还可以简化为下面的代码 -class ArticleDetail(generics.RetrieveUpdateDestroyAPIView): - permission_classes = [IsAdminUserOrReadOnly] - queryset = Article.objects.all() - serializer_class = ArticleDetailSerializer +class TagViewSet(viewsets.ModelViewSet): + """标签视图集""" + queryset = Tag.objects.all() + serializer_class = TagSerializer + permission_classes = [IsAdminUserOrReadOnly] \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 index cbb0937..88deac7 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/drf_vue_blog/settings.py b/drf_vue_blog/settings.py index 2cf0d36..54baff8 100644 --- a/drf_vue_blog/settings.py +++ b/drf_vue_blog/settings.py @@ -28,6 +28,7 @@ INSTALLED_APPS = [ 'rest_framework', 'article', 'user_info', + 'django_filters', ] MIDDLEWARE = [ @@ -114,5 +115,11 @@ STATIC_URL = '/static/' # 使用django_rest_framework中的分页功能 REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', - 'PAGE_SIZE': 3, # 每页3条记录 -} \ No newline at end of file + # 每页3条记录 + 'PAGE_SIZE': 3, + # 使用django-filter后端过滤引擎 + 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], +} + + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/drf_vue_blog/urls.py b/drf_vue_blog/urls.py index 0069fb0..973784d 100644 --- a/drf_vue_blog/urls.py +++ b/drf_vue_blog/urls.py @@ -1,8 +1,17 @@ from django.contrib import admin from django.urls import path, include +from rest_framework.routers import DefaultRouter +from article import views + +router = DefaultRouter() +router.register(r'article', views.ArticleViewSet) +router.register(r'category', views.CategoryViewSet) +router.register(r'tag', views.TagViewSet) + urlpatterns = [ path('admin/', admin.site.urls), path('api-auth/', include('rest_framework.urls')), - path('api/article/', include('article.urls', namespace='article')), + path('api/', include(router.urls)) + # path('api/article/', include('article.urls', namespace='article')), ]