diff --git a/article/migrations/0006_avatar_article_avatar.py b/article/migrations/0006_avatar_article_avatar.py new file mode 100644 index 0000000..92154f3 --- /dev/null +++ b/article/migrations/0006_avatar_article_avatar.py @@ -0,0 +1,26 @@ +# Generated by Django 4.1.1 on 2022-09-23 22:51 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('article', '0005_rename_tag_article_tags'), + ] + + operations = [ + migrations.CreateModel( + name='Avatar', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.ImageField(upload_to='avatar/%Y%m%d')), + ], + ), + migrations.AddField( + model_name='article', + name='avatar', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='articles', to='article.avatar'), + ), + ] diff --git a/article/migrations/0007_alter_article_avatar.py b/article/migrations/0007_alter_article_avatar.py new file mode 100644 index 0000000..aac4e71 --- /dev/null +++ b/article/migrations/0007_alter_article_avatar.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1.1 on 2022-09-23 23:57 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('article', '0006_avatar_article_avatar'), + ] + + operations = [ + migrations.AlterField( + model_name='article', + name='avatar', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article', to='article.avatar'), + ), + ] diff --git a/article/models.py b/article/models.py index 7cc78d4..0e4e054 100644 --- a/article/models.py +++ b/article/models.py @@ -28,6 +28,11 @@ class Tag(models.Model): return self.text +class Avatar(models.Model): + """标题图""" + content = models.ImageField(upload_to='avatar/%Y%m%d') + + # 博客文章 model class Article(models.Model): class Meta: @@ -59,6 +64,15 @@ class Article(models.Model): related_name='articles', ) + # 标题图 + avatar = models.ForeignKey( + Avatar, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='article' # 这里是1对1关系 + ) + # 标题 title = models.CharField(max_length=100) # 正文 diff --git a/article/permissions.py b/article/permissions.py index c64cf88..4e52cb9 100644 --- a/article/permissions.py +++ b/article/permissions.py @@ -1,4 +1,5 @@ from rest_framework import permissions +from rest_framework.permissions import BasePermission, SAFE_METHODS class IsAdminUserOrReadOnly(permissions.BasePermission): @@ -14,3 +15,6 @@ class IsAdminUserOrReadOnly(permissions.BasePermission): # 仅管理员可进行其他操作 return request.user.is_superuser + + + diff --git a/article/serializers.py b/article/serializers.py index 26d33cb..df3fbcd 100644 --- a/article/serializers.py +++ b/article/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers -from article.models import Article, Category, Tag +from article.models import Article, Category, Tag, Avatar from user_info.serializers import UserDescSerializer - +from comment.serializers import CommentSerializer class CategorySerializer(serializers.ModelSerializer): """所有分类的序列化器""" @@ -16,6 +16,14 @@ class CategorySerializer(serializers.ModelSerializer): read_only_fields = ['created'] +class AvatarSerializer(serializers.ModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='avatar-detail') + + class Meta: + model = Avatar + fields = '__all__' + + class ArticleBaseSerializer(serializers.HyperlinkedModelSerializer): """将原来的ArticleSerializer抽象出一个父类""" author = UserDescSerializer(read_only=True) @@ -32,10 +40,45 @@ class ArticleBaseSerializer(serializers.HyperlinkedModelSerializer): slug_field='text', ) + # 图片字段 + avatar = AvatarSerializer(read_only=True) + avatar_id = serializers.IntegerField( + write_only=True, + allow_null=True, + required=False, + ) + + # 自定义错误信息 + default_error_messages = { + 'incorrect_avatar_id': 'Avatar with id {value} not exists.', + 'incorrect_category_id': 'Category with id {value} not exists.', + 'default': 'No more message here..' + } + + def check_obj_exists_or_fail(self, model, value, message='default'): + if not self.default_error_messages.get(message, None): + message = 'default' + # 不为None但是不存在该id返回错误信息 + if not model.objects.filter(id=value).exists() and value is not None: + # 若不存在则调用钩子方法fail()引发错误 + self.fail(message, value=value) + + # 验证图片id是否正确 + def validate_avatar_id(self, value): + self.check_obj_exists_or_fail( + model=Avatar, + value=value, + message='incorrect_avatar_id' + ) + return value + # 验证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)) + self.check_obj_exists_or_fail( + model=Category, + value=value, + message='incorrect_category_id' + ) return value def to_internal_value(self, data): @@ -58,6 +101,8 @@ class ArticleSerializer(ArticleBaseSerializer): class ArticleDetailSerializer(ArticleBaseSerializer): + id = serializers.IntegerField(read_only=True) + comments = CommentSerializer(many=True, read_only=True) # 渲染后的正文 body_html = serializers.SerializerMethodField() # 渲染后的目录 @@ -102,10 +147,10 @@ class CategoryDetailSerializer(serializers.ModelSerializer): class TagSerializer(serializers.ModelSerializer): + """所有标签序列化器""" # 显示url # url = serializers.HyperlinkedIdentityField(view_name='tag-detail') - """所有标签序列化器""" class Meta: model = Tag @@ -126,3 +171,4 @@ class TagSerializer(serializers.ModelSerializer): return super(TagSerializer, self).update(instance, validated_data) + diff --git a/article/views.py b/article/views.py index f8d9faf..91387a9 100644 --- a/article/views.py +++ b/article/views.py @@ -1,7 +1,7 @@ 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.serializers import TagSerializer,ArticleDetailSerializer, AvatarSerializer +from article.models import Article, Category, Tag , Avatar from article.permissions import IsAdminUserOrReadOnly from rest_framework.filters import SearchFilter @@ -47,4 +47,11 @@ class TagViewSet(viewsets.ModelViewSet): """标签视图集""" queryset = Tag.objects.all() serializer_class = TagSerializer - permission_classes = [IsAdminUserOrReadOnly] \ No newline at end of file + permission_classes = [IsAdminUserOrReadOnly] + + +class AvatarViewSet(viewsets.ModelViewSet): + """标题图片视图集""" + queryset = Avatar.objects.all() + serializer_class = AvatarSerializer + permission_classes = [IsAdminUserOrReadOnly] diff --git a/comment/__init__.py b/comment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/comment/admin.py b/comment/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/comment/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/comment/apps.py b/comment/apps.py new file mode 100644 index 0000000..c364f39 --- /dev/null +++ b/comment/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CommentConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'comment' diff --git a/comment/migrations/0001_initial.py b/comment/migrations/0001_initial.py new file mode 100644 index 0000000..703dd26 --- /dev/null +++ b/comment/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# Generated by Django 4.1.1 on 2022-09-23 23:57 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('article', '0007_alter_article_avatar'), + ] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.TextField()), + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='article.article')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created'], + }, + ), + ] diff --git a/comment/migrations/__init__.py b/comment/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/comment/models.py b/comment/models.py new file mode 100644 index 0000000..8d1d5c5 --- /dev/null +++ b/comment/models.py @@ -0,0 +1,30 @@ +from django.db import models +from django.utils import timezone +from article.models import Article +from django.contrib.auth.models import User + + +class Comment(models.Model): + # 评论人 + author = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='comments', + ) + # 文章 + article = models.ForeignKey( + Article, + on_delete=models.CASCADE, + related_name='comments', + ) + # 评论内容 + content = models.TextField() + # 评论时间 + created = models.DateTimeField(default=timezone.now) + + class Meta: + ordering = ['-created'] + + def __str__(self): + # 展示前20个字符 + return self.content[:20] diff --git a/comment/permissions.py b/comment/permissions.py new file mode 100644 index 0000000..1da9b33 --- /dev/null +++ b/comment/permissions.py @@ -0,0 +1,26 @@ +from rest_framework.permissions import BasePermission, SAFE_METHODS + + +class IsOwnerOrReadOnly(BasePermission): + """ + 只有作者本人可以修改,其他人只能查看 + """ + message = "You must be the owner to update" + + def safe_methods_or_owner(self, request, func): + if request.method in SAFE_METHODS: + return True + return func() + + def has_permission(self, request, view): + return self.safe_methods_or_owner( + request, + lambda: request.user.is_authenticated + ) + + def has_object_permission(self, request, view, obj): + return self.safe_methods_or_owner( + request, + lambda: obj.author == request.user # 验证当前评论的作者和当前登录的用户是否为同一个人 + ) + diff --git a/comment/serializers.py b/comment/serializers.py new file mode 100644 index 0000000..fd9dca0 --- /dev/null +++ b/comment/serializers.py @@ -0,0 +1,15 @@ +from rest_framework import serializers +from user_info.serializers import UserDescSerializer +from comment.models import Comment + + +class CommentSerializer(serializers.ModelSerializer): + """评论的序列化器""" + url = serializers.HyperlinkedIdentityField(view_name='comment-detail') + author = UserDescSerializer(read_only=True) + + class Meta: + model = Comment + fields = '__all__' + extra_kwargs = {'created': {'read_only': True}} + diff --git a/comment/tests.py b/comment/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/comment/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/comment/views.py b/comment/views.py new file mode 100644 index 0000000..472e968 --- /dev/null +++ b/comment/views.py @@ -0,0 +1,14 @@ +from rest_framework import viewsets + +from comment.models import Comment +from comment.serializers import CommentSerializer +from comment.permissions import IsOwnerOrReadOnly + + +class CommentViewSet(viewsets.ModelViewSet): + queryset = Comment.objects.all() + serializer_class = CommentSerializer + permission_classes = [IsOwnerOrReadOnly] + + def perform_create(self, serializer): + serializer.save(author=self.request.user) \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 index 88deac7..5a31d26 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 54baff8..f319c73 100644 --- a/drf_vue_blog/settings.py +++ b/drf_vue_blog/settings.py @@ -1,5 +1,5 @@ from pathlib import Path - +import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -29,6 +29,7 @@ INSTALLED_APPS = [ 'article', 'user_info', 'django_filters', + 'comment', ] MIDDLEWARE = [ @@ -115,11 +116,14 @@ STATIC_URL = '/static/' # 使用django_rest_framework中的分页功能 REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', - # 每页3条记录 - 'PAGE_SIZE': 3, + # 每页5条记录 + 'PAGE_SIZE': 5, # 使用django-filter后端过滤引擎 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], } +# 媒体文件 +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/drf_vue_blog/urls.py b/drf_vue_blog/urls.py index 973784d..15b91da 100644 --- a/drf_vue_blog/urls.py +++ b/drf_vue_blog/urls.py @@ -2,12 +2,16 @@ from django.contrib import admin from django.urls import path, include from rest_framework.routers import DefaultRouter from article import views +from django.conf import settings +from django.conf.urls.static import static +from comment.views import CommentViewSet router = DefaultRouter() router.register(r'article', views.ArticleViewSet) router.register(r'category', views.CategoryViewSet) router.register(r'tag', views.TagViewSet) - +router.register(r'avatar', views.AvatarViewSet) +router.register(r'comment', CommentViewSet) urlpatterns = [ path('admin/', admin.site.urls), @@ -15,3 +19,7 @@ urlpatterns = [ path('api/', include(router.urls)) # path('api/article/', include('article.urls', namespace='article')), ] + +# 把媒体文件的路由注册了 +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/media/avatar/20220923/apple_00.png b/media/avatar/20220923/apple_00.png new file mode 100644 index 0000000..9a5156d Binary files /dev/null and b/media/avatar/20220923/apple_00.png differ diff --git a/media/avatar/20220923/apple_01.png b/media/avatar/20220923/apple_01.png new file mode 100644 index 0000000..75ef057 Binary files /dev/null and b/media/avatar/20220923/apple_01.png differ diff --git a/media/avatar/20220923/apple_02.png b/media/avatar/20220923/apple_02.png new file mode 100644 index 0000000..e9bdffb Binary files /dev/null and b/media/avatar/20220923/apple_02.png differ diff --git a/media/avatar/20220923/apple_03.png b/media/avatar/20220923/apple_03.png new file mode 100644 index 0000000..73df7cb Binary files /dev/null and b/media/avatar/20220923/apple_03.png differ diff --git a/media/avatar/20220923/apple_04.png b/media/avatar/20220923/apple_04.png new file mode 100644 index 0000000..29ce42e Binary files /dev/null and b/media/avatar/20220923/apple_04.png differ diff --git a/media/avatar/20220923/apple_05.png b/media/avatar/20220923/apple_05.png new file mode 100644 index 0000000..9746f29 Binary files /dev/null and b/media/avatar/20220923/apple_05.png differ diff --git a/media/avatar/20220923/apple_06.png b/media/avatar/20220923/apple_06.png new file mode 100644 index 0000000..d856b29 Binary files /dev/null and b/media/avatar/20220923/apple_06.png differ diff --git a/media/avatar/20220923/apple_07.png b/media/avatar/20220923/apple_07.png new file mode 100644 index 0000000..0341b65 Binary files /dev/null and b/media/avatar/20220923/apple_07.png differ diff --git a/media/avatar/20220923/apple_08.png b/media/avatar/20220923/apple_08.png new file mode 100644 index 0000000..83ac857 Binary files /dev/null and b/media/avatar/20220923/apple_08.png differ diff --git a/media/avatar/20220923/apple_09.png b/media/avatar/20220923/apple_09.png new file mode 100644 index 0000000..79ca66b Binary files /dev/null and b/media/avatar/20220923/apple_09.png differ diff --git a/media/avatar/20220923/apple_10.png b/media/avatar/20220923/apple_10.png new file mode 100644 index 0000000..73df7cb Binary files /dev/null and b/media/avatar/20220923/apple_10.png differ diff --git a/media/avatar/20220923/apple_20.png b/media/avatar/20220923/apple_20.png new file mode 100644 index 0000000..97be381 Binary files /dev/null and b/media/avatar/20220923/apple_20.png differ