diff --git a/src/backend/memo/__init__.py b/src/backend/memo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/backend/memo/admin.py b/src/backend/memo/admin.py new file mode 100644 index 0000000..96014b4 --- /dev/null +++ b/src/backend/memo/admin.py @@ -0,0 +1,28 @@ +from django.contrib import admin +from .models import Memo, MemoImage + + +class MemoImageInline(admin.TabularInline): + model = MemoImage + extra = 0 + + +@admin.register(Memo) +class MemoAdmin(admin.ModelAdmin): + list_display = ['title', 'created_by', 'created_at', 'updated_at', 'is_active'] + list_filter = ['is_active', 'created_at', 'updated_at'] + search_fields = ['title', 'content'] + readonly_fields = ['created_at', 'updated_at'] + inlines = [MemoImageInline] + + def save_model(self, request, obj, form, change): + if not change: + obj.created_by = request.user + super().save_model(request, obj, form, change) + + +@admin.register(MemoImage) +class MemoImageAdmin(admin.ModelAdmin): + list_display = ['memo', 'alt_text', 'uploaded_at'] + list_filter = ['uploaded_at'] + search_fields = ['memo__title', 'alt_text'] diff --git a/src/backend/memo/apps.py b/src/backend/memo/apps.py new file mode 100644 index 0000000..abd08f6 --- /dev/null +++ b/src/backend/memo/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MemoConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "memo" diff --git a/src/backend/memo/models.py b/src/backend/memo/models.py new file mode 100644 index 0000000..fb1cad0 --- /dev/null +++ b/src/backend/memo/models.py @@ -0,0 +1,40 @@ +from django.db import models +from django.contrib.auth.models import User + + +class Memo(models.Model): + title = models.CharField(max_length=200, verbose_name="标题") + content = models.TextField(verbose_name="内容", blank=True) + created_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="创建者") + created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") + is_active = models.BooleanField(default=True, verbose_name="是否启用") + + class Meta: + verbose_name = "备忘录" + verbose_name_plural = "备忘录" + ordering = ['-updated_at'] + + def __str__(self): + return self.title + + @property + def content_preview(self): + """返回内容的缩略预览""" + if len(self.content) > 100: + return self.content[:100] + "..." + return self.content + + +class MemoImage(models.Model): + memo = models.ForeignKey(Memo, on_delete=models.CASCADE, related_name='images', verbose_name="备忘录") + image = models.ImageField(upload_to='memo_images/', verbose_name="图片") + uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间") + alt_text = models.CharField(max_length=200, blank=True, verbose_name="图片描述") + + class Meta: + verbose_name = "备忘录图片" + verbose_name_plural = "备忘录图片" + + def __str__(self): + return f"{self.memo.title} - 图片" diff --git a/src/backend/memo/serializers.py b/src/backend/memo/serializers.py new file mode 100644 index 0000000..fa56771 --- /dev/null +++ b/src/backend/memo/serializers.py @@ -0,0 +1,33 @@ +from rest_framework import serializers +from .models import Memo, MemoImage + + +class MemoImageSerializer(serializers.ModelSerializer): + class Meta: + model = MemoImage + fields = ['id', 'image', 'uploaded_at', 'alt_text'] + + +class MemoSerializer(serializers.ModelSerializer): + images = MemoImageSerializer(many=True, read_only=True) + content_preview = serializers.ReadOnlyField() + created_by_name = serializers.CharField(source='created_by.username', read_only=True) + + class Meta: + model = Memo + fields = ['id', 'title', 'content', 'content_preview', 'created_by', 'created_by_name', + 'created_at', 'updated_at', 'is_active', 'images'] + read_only_fields = ['created_by', 'created_at', 'updated_at'] + + def create(self, validated_data): + validated_data['created_by'] = self.context['request'].user + return super().create(validated_data) + + +class MemoListSerializer(serializers.ModelSerializer): + content_preview = serializers.ReadOnlyField() + created_by_name = serializers.CharField(source='created_by.username', read_only=True) + + class Meta: + model = Memo + fields = ['id', 'title', 'content_preview', 'created_by_name', 'updated_at'] \ No newline at end of file diff --git a/src/backend/memo/tests.py b/src/backend/memo/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/src/backend/memo/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/backend/memo/urls.py b/src/backend/memo/urls.py new file mode 100644 index 0000000..555ebb0 --- /dev/null +++ b/src/backend/memo/urls.py @@ -0,0 +1,10 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import MemoViewSet + +router = DefaultRouter() +router.register(r'memos', MemoViewSet) + +urlpatterns = [ + path('api/', include(router.urls)), +] \ No newline at end of file diff --git a/src/backend/memo/views.py b/src/backend/memo/views.py new file mode 100644 index 0000000..609f471 --- /dev/null +++ b/src/backend/memo/views.py @@ -0,0 +1,61 @@ +from rest_framework import viewsets, filters, status +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.parsers import MultiPartParser, FormParser +from django_filters.rest_framework import DjangoFilterBackend +from django.db.models import Q +from .models import Memo, MemoImage +from .serializers import MemoSerializer, MemoListSerializer, MemoImageSerializer + + +class MemoViewSet(viewsets.ModelViewSet): + queryset = Memo.objects.filter(is_active=True) + serializer_class = MemoSerializer + filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] + search_fields = ['title', 'content'] + ordering_fields = ['created_at', 'updated_at'] + ordering = ['-updated_at'] + + def get_serializer_class(self): + if self.action == 'list': + return MemoListSerializer + return MemoSerializer + + def get_queryset(self): + queryset = super().get_queryset() + search = self.request.query_params.get('search', None) + if search: + queryset = queryset.filter( + Q(title__icontains=search) | Q(content__icontains=search) + ) + return queryset + + @action(detail=True, methods=['post'], parser_classes=[MultiPartParser, FormParser]) + def upload_image(self, request, pk=None): + memo = self.get_object() + image = request.FILES.get('image') + alt_text = request.data.get('alt_text', '') + + if not image: + return Response({'error': '请选择图片文件'}, status=status.HTTP_400_BAD_REQUEST) + + memo_image = MemoImage.objects.create( + memo=memo, + image=image, + alt_text=alt_text + ) + + serializer = MemoImageSerializer(memo_image) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + @action(detail=True, methods=['delete']) + def delete_image(self, request, pk=None): + memo = self.get_object() + image_id = request.data.get('image_id') + + try: + memo_image = MemoImage.objects.get(id=image_id, memo=memo) + memo_image.delete() + return Response({'message': '图片删除成功'}, status=status.HTTP_200_OK) + except MemoImage.DoesNotExist: + return Response({'error': '图片不存在'}, status=status.HTTP_404_NOT_FOUND)