diff --git a/README.md b/README.md index 31bfddb6f1ba67d096c12cd779f2056b4549434b..fc63b46152e7277245dbc506017ab65b7f12fa8a 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ bugs? Also: - - Please treat all code as production level code. - - Please don’t spend more than 4 hours on this. If time runs short, an +- Please treat all code as production level code. +- Please don’t spend more than 4 hours on this. If time runs short, an explanation of what you would do given more time is fine. ## Original `readme.txt` diff --git a/db.sqlite3 b/db.sqlite3 index 42db1a76d95f421c660bd03c45f143554cb13386..bf13b657204c51cbee84bab72f1d5958e2d734c4 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/django_rest_sample/settings.py b/django_rest_sample/settings.py index a6dc00d37c5b630e939cc8df79bf81cf1f3b86d5..179237b5c54118fb83853014c665bb8cb4fed475 100644 --- a/django_rest_sample/settings.py +++ b/django_rest_sample/settings.py @@ -108,7 +108,7 @@ AUTH_PASSWORD_VALIDATORS = [ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +TIME_ZONE = 'Asia/Hong_Kong' USE_I18N = True @@ -125,5 +125,13 @@ STATIC_URL = '/static/' REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', - 'PAGE_SIZE': 100 + 'PAGE_SIZE': 10, + 'DEFAULT_THROTTLE_CLASSES': [ + 'rest_framework.throttling.AnonRateThrottle', + 'rest_framework.throttling.UserRateThrottle' + ], + 'DEFAULT_THROTTLE_RATES':{ + 'anon': '3/minute', # 匿名用户一分钟可以访问3次,秒(s)、分(m)、时(h)、天(d) + 'user': '10/minute', # 登录用户一分钟可以访问10次 + }, } diff --git a/django_rest_sample/urls.py b/django_rest_sample/urls.py index 05fb841da371ccc55cbe81f3dacc6687bba81312..e37c2a44ac0c1c5c396c09a58fa636ea547b243b 100644 --- a/django_rest_sample/urls.py +++ b/django_rest_sample/urls.py @@ -14,7 +14,7 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include from rest_framework import routers from library_app.views import BookViewSet, CategoryViewSet, AuthorViewSet @@ -27,4 +27,5 @@ urlpatterns = router.urls urlpatterns += [ path('admin/', admin.site.urls), + path('api-auth/', include('rest_framework.urls')), ] diff --git a/library_app/serializers.py b/library_app/serializers.py index 849aea1f7cfdf51d36afe7743319ac57b0688bc5..f2e0c7e1a18e40f4ad7abde7ddd34c0086c06bb4 100644 --- a/library_app/serializers.py +++ b/library_app/serializers.py @@ -2,7 +2,10 @@ from rest_framework import serializers from library_app.models import Category, Book, Author - +class CategorySerializer(serializers.ModelSerializer): + class Meta: + model = Category + fields = "__all__" class AuthorSerializer(serializers.ModelSerializer): class Meta: @@ -18,11 +21,28 @@ class BookReadSerializer(serializers.ModelSerializer): fields = '__all__' class BookWriteSerializer(serializers.ModelSerializer): + category = CategorySerializer() + author = AuthorSerializer() + class Meta: model = Book fields = "__all__" - -class CategorySerializer(serializers.ModelSerializer): - class Meta: - model = Category - fields = "__all__" + + def create(self, validated_data): + category = validated_data.pop('category') + category_obj = Category.objects.get(title=category['title']) + author = validated_data.pop('author') + author_obj = Author.objects.get(name=author['name']) + return Book.objects.create(category=category_obj, author=author_obj, **validated_data) + + def update(self, instance, validated_data): + instance.title = validated_data.get('title', instance.title) + instance.owned = validated_data.get('owned', instance.owned) + instance.price = validated_data.get('price', instance.price) + instance.publish_date = validated_data.get('publish_date', instance.publish_date) + author = validated_data.pop('author') + instance.author = Author.objects.get(name=author['name']) + category = validated_data.pop('category') + instance.category = Category.objects.get(title=category['title']) + instance.save() + return instance diff --git a/library_app/utils.py b/library_app/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..3fc43711435bd15bc47568a8e6e6a2cfdaae2027 --- /dev/null +++ b/library_app/utils.py @@ -0,0 +1,14 @@ +from rest_framework.throttling import SimpleRateThrottle + +class VisitThrottle(SimpleRateThrottle): + scope = 'AnonymousUser' + + def get_cache_key(self, request, view): + return self.get_ident(request) + + +class UserThrottle(SimpleRateThrottle): + scope = 'LoginUser' + + def get_cache_key(self, request, view): + return request.user.user_id diff --git a/library_app/views.py b/library_app/views.py index da56ab7fe376dc3d778ee7e3d280b4971270631d..3713d8c2a5151f807c350e88b7c68ee0b9b0dfaa 100644 --- a/library_app/views.py +++ b/library_app/views.py @@ -1,23 +1,91 @@ +from typing import Type, Tuple + +from django.db import models +from django.db.models import Sum, F +from django.http import JsonResponse + +from rest_framework import permissions from rest_framework import viewsets +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticatedOrReadOnly from library_app.models import Category, Author, Book from library_app.serializers import CategorySerializer, AuthorSerializer, BookReadSerializer, BookWriteSerializer class BookViewSet(viewsets.ModelViewSet): + permission_classes: tuple[Type[IsAuthenticatedOrReadOnly]] = (permissions.IsAuthenticatedOrReadOnly,) queryset = Book.objects.all() + def list(self, request, *args, **kwargs): + query = self.get_queryset() + if request.query_params.get('totalnumber') == 'true': + total = query.aggregate(total=Sum('owned')) + return JsonResponse({'result of Total number of books owned:': total}) + if request.query_params.get('totalcost') == 'true': + total = query.aggregate( + total=Sum(F('price') * F('owned'), output_field=models.FloatField())) + return JsonResponse({'result of Total cost of all books:': total}) + queryset = self.filter_queryset(query) + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + def get_serializer_class(self): if self.request.method in ['GET', 'HEAD']: return BookReadSerializer else: return BookWriteSerializer + class CategoryViewSet(viewsets.ModelViewSet): + permission_classes: tuple[Type[IsAuthenticatedOrReadOnly]] = (permissions.IsAuthenticatedOrReadOnly,) queryset = Category.objects.all() - serializer_class = CategorySerializer + + def list(self, request, *args, **kwargs): + if request.query_params.get('bookstop') == 'true': + from django.db import connection + cursor = connection.cursor() + cursor.execute( + 'select category_id,max(totalbook) as total from (select category_id,sum(owned) as totalbook from library_app_book group by category_id)') + ret = cursor.fetchall() + return JsonResponse({'result of Category with most books[category_id,ownedbookstotal]': ret}) + query = self.get_queryset() + queryset = self.filter_queryset(query) + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + def get_serializer_class(self): + return CategorySerializer class AuthorViewSet(viewsets.ModelViewSet): + permission_classes: tuple[Type[IsAuthenticatedOrReadOnly]] = (permissions.IsAuthenticatedOrReadOnly,) queryset = Author.objects.all() - serializer_class = AuthorSerializer + + def list(self, request, *args, **kwargs): + if request.query_params.get('bookstop') == 'true': + from django.db import connection + cursor = connection.cursor() + cursor.execute( + 'select author_id,max(totalbook) as total from (select id,author_id,sum(owned) as totalbook from library_app_book group by author_id) ') + ret = cursor.fetchall() + return JsonResponse({'result of Author with most books[author_id,ownedbookstotal]': ret}) + query = self.get_queryset() + queryset = self.filter_queryset(query) + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + def get_serializer_class(self): + return AuthorSerializer diff --git "a/\345\274\200\345\217\221\347\254\224\350\256\260.md" "b/\345\274\200\345\217\221\347\254\224\350\256\260.md" new file mode 100644 index 0000000000000000000000000000000000000000..e7dd2b12455be8299811f364e93d0b6bcde73393 --- /dev/null +++ "b/\345\274\200\345\217\221\347\254\224\350\256\260.md" @@ -0,0 +1,76 @@ +# 开发笔记 -朱兴林 + +## 原有 bug 修复 +### 运行 makemigrations 时报错:NameError: name ‘CategorySerializer’ is not defined +修复方式:将 serializers.py 文件中的 CategorySerializer 类定义代码移动到 BookReadSerializer 之前 +### 使用浏览器访问 books 时报类型错误,JSON 格式输出正常 +在 serializers.py 文件的 BookWriteSerializer 类代码中添加以下代码: +``` +category = CategorySerializer() +author = AuthorSerializer() +``` +继续添加 create 方法和 update 方法,实现 book 的添加和修改功能 + +## 功能添加 +### 在库的图书总数 +访问链接:http://127.0.0.1:8000/books?totalnumber=true +实现方式:在 views.py 文件的 BookViewSet 类中添加 list 方法,获取查询参数后(totalnumber),使用聚合统计功能,将结果以 JSON 形式返回 +``` +代码片段: query.aggregate(total=Sum(‘owned’)) +``` +### 所有的图书总价 +访问链接:http://127.0.0.1:8000/books?totalcost=true +实现方式:在 views.py 文件的 BookViewSet 类中增加判断,获取查询参数后(totalcost),使用聚合统计功能,将结果以 JSON 形式返回 +``` +代码片段: query.aggregate(total=Sum(F(‘price’) * F(‘owned’), output_field=models.FloatField())) +``` +### 拥有图书最多的作者列表 +访问链接:http://127.0.0.1:8000/author?bookstop=true +实现方式:在 views.py 文件的 AuthorViewSet 类中添加 list 方法,获取查询参数后,因为使用了复杂 SQL 语句,采取直连数据库的方式,获取分组后的最大记录,以 JSON 形式返回 +``` +SQL 语句: +select category_id,max(totalbook) as total from (select category_id,sum(owned) as totalbook from library_app_book group by category_id) +代码片段: +if request.query_params.get(‘bookstop’) == ‘true’: + from django.db import connection + cursor = connection.cursor() + cursor.execute( + ‘select author_id,max(totalbook) as total from (select id,author_id,sum(owned) as totalbook from library_app_book group by author_id) ‘) + ret = cursor.fetchall() + return JsonResponse({‘result of Author with most books[author_id,ownedbookstotal]’: ret}) +``` +待解决 bug:如果多个作者的图书数量相等,无法显示出此结果集,只能有一条记录,是使用 max 函数导致 +### 图书最多的类别列表 +访问链接:http://127.0.0.1:8000/category?bookstop=true +实现方式:在 views.py 文件的 CategoryViewSet 类中添加 list 方法,获取查询参数后,因为使用了复杂 SQL 语句,采取直连数据库的方式,获取分组后的最大记录,以 JSON 形式返回 +``` +SQL 语句: +select category_id,max(totalbook) as total from (select category_id,sum(owned) as totalbook from library_app_book group by category_id) +代码片段: +if request.query_params.get(‘bookstop’) == ‘true’: + from django.db import connection + cursor = connection.cursor() + cursor.execute( + ‘select category_id,max(totalbook) as total from (select category_id,sum(owned) as totalbook from library_app_book group by category_id)’) + ret = cursor.fetchall() + return JsonResponse({‘result of Category with most books[category_id,ownedbookstotal]’: ret}) +``` +### 添加安全认证和访问控制功能 +#### 未登录用户只能浏览,不能添加和修改数据 +在 views.py 文件的 3 个 ViewSet 类中添加以下代码: +`permission_classes = (permissions.IsAuthenticatedOrReadOnly,)` +在 url.py 文件中添加以下代码: +` path(‘api-auth/‘, include(‘rest_framework.urls’)), ` +#### 控制未登录用户和登录用户的访问频率 +在配置文件的“REST_FRAMEWORK”配置节中,增加访问频率配置项,例如: +``` +‘DEFAULT_THROTTLE_CLASSES’: [ + ‘rest_framework.throttling.AnonRateThrottle’, + ‘rest_framework.throttling.UserRateThrottle’ + ], + ‘DEFAULT_THROTTLE_RATES’:{ + ‘Anon’: ‘3/minute’, /# 匿名用户一分钟可以访问3次,秒(s)、分(m)、时(h)、天(d)/ + ‘User’: ’10/minute’, /# 登录用户一分钟可以访问10次/ + }, +``` +