diff --git a/apps/ofm/filters.py b/apps/ofm/filters.py new file mode 100644 index 00000000..17088aa2 --- /dev/null +++ b/apps/ofm/filters.py @@ -0,0 +1,14 @@ +from django_filters import rest_framework as filters +from apps.ofm.models import MroomBooking + + +class MroomBookingFilterset(filters.FilterSet): + class Meta: + model = MroomBooking + fields = { + 'slot_b__mroom': ['exact', 'in'], + 'slot_b__booking': ['exact'], + 'slot_b__mdate': ['exact', 'gte', 'lte'], + 'create_by': ['exact'], + "id": ["exact"] + } \ No newline at end of file diff --git a/apps/ofm/migrations/0001_initial.py b/apps/ofm/migrations/0001_initial.py new file mode 100644 index 00000000..84ca7469 --- /dev/null +++ b/apps/ofm/migrations/0001_initial.py @@ -0,0 +1,66 @@ +# Generated by Django 3.2.12 on 2025-06-25 09:29 + +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), + ] + + operations = [ + migrations.CreateModel( + name='Mroom', + fields=[ + ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('name', models.CharField(max_length=50, unique=True, verbose_name='会议室名称')), + ('location', models.CharField(max_length=100, verbose_name='位置')), + ('capacity', models.PositiveIntegerField(verbose_name='容纳人数')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroom_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroom_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='MroomBooking', + fields=[ + ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('title', models.CharField(max_length=100, verbose_name='会议主题')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroombooking_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroombooking_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='MroomSlot', + fields=[ + ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('mdate', models.DateField(db_index=True, verbose_name='会议日期')), + ('slot', models.PositiveIntegerField(help_text='0-47', verbose_name='时段')), + ('booking', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slot_b', to='ofm.mroombooking')), + ('mroom', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slot_m', to='ofm.mroom')), + ], + options={ + 'unique_together': {('mroom', 'mdate', 'slot')}, + }, + ), + ] diff --git a/apps/ofm/models.py b/apps/ofm/models.py index 0c7be57f..7cf1f869 100644 --- a/apps/ofm/models.py +++ b/apps/ofm/models.py @@ -15,8 +15,8 @@ class MroomBooking(CommonADModel): class MroomSlot(BaseModel): """TN: 会议室时段""" - mroom = models.ForeignKey(Mroom, on_delete=models.CASCADE) - booking = models.ForeignKey(MroomBooking, on_delete=models.CASCADE) + mroom = models.ForeignKey(Mroom, on_delete=models.CASCADE, related_name="slot_m") + booking = models.ForeignKey(MroomBooking, on_delete=models.CASCADE, related_name="slot_b") mdate = models.DateField('会议日期', db_index=True) slot = models.PositiveIntegerField('时段', help_text='0-47') diff --git a/apps/ofm/serializers.py b/apps/ofm/serializers.py new file mode 100644 index 00000000..95cc65d5 --- /dev/null +++ b/apps/ofm/serializers.py @@ -0,0 +1,64 @@ +from .models import Mroom, MroomBooking, MroomSlot +from apps.utils.serializers import CustomModelSerializer +from rest_framework import serializers +from django.db import transaction +from rest_framework.exceptions import ParseError +from apps.utils.constants import EXCLUDE_FIELDS + + +class MroomSerializer(CustomModelSerializer): + class Meta: + model = Mroom + fields = '__all__' + + +class MroomBookingSerializer(CustomModelSerializer): + mroom = serializers.PrimaryKeyRelatedField(queryset=Mroom.objects.all(), write_only=True, label="会议室") + mdate = serializers.DateField(write_only=True, label="预订日期") + slots = serializers.ListField(child=serializers.IntegerField(), write_only=True, label="时段索引") + create_by_name = serializers.CharField(source='create_by.username', read_only=True) + class Meta: + model = MroomBooking + fields = '__all__' + read_only_fields = EXCLUDE_FIELDS + + @transaction.atomic + def create(self, validated_data): + mroom = validated_data.pop('mroom') + slots = validated_data.pop('slots') + mdate = validated_data.pop('mdate') + booking = MroomBooking.objects.create(**validated_data) + MroomSlot.objects.filter(booking=booking).delete() + for slot in slots: + if slot < 0 or slot > 47: + raise ParseError("时段索引超出范围") + try: + MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom) + except Exception as e: + raise ParseError(f"时段已预订,请刷新重选-{e}") + return booking + + @transaction.atomic + def update(self, instance, validated_data): + mroom = validated_data.pop('mroom') + slots = validated_data.pop('slots') + mdate = validated_data.pop('mdate') + booking = super().update(instance, validated_data) + MroomSlot.objects.filter(booking=instance).delete() + for slot in slots: + if slot < 0 or slot > 47: + raise ParseError("时段索引超出范围") + try: + MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom) + except Exception as e: + raise ParseError(f"时段已预订,请刷新重选-{e}") + return booking + + + +class MroomSlotSerializer(CustomModelSerializer): + booking_title = serializers.CharField(source='booking.title', read_only=True) + class Meta: + model = MroomSlot + fields = '__all__' + diff --git a/apps/ofm/urls.py b/apps/ofm/urls.py new file mode 100644 index 00000000..3f0e8960 --- /dev/null +++ b/apps/ofm/urls.py @@ -0,0 +1,14 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from apps.ofm.views import (MroomViewSet, MroomBookingViewSet, MroomSlotViewSet) + +API_BASE_URL = 'api/ofm/' +HTML_BASE_URL = 'dhtml/ofm/' + +router = DefaultRouter() +router.register('mroom', MroomViewSet, basename='mroom') +router.register('mroombooking', MroomBookingViewSet, basename='mroombooking') +router.register('mroomslot', MroomSlotViewSet, basename='mroomslot') +urlpatterns = [ + path(API_BASE_URL, include(router.urls)), +] diff --git a/apps/ofm/views.py b/apps/ofm/views.py index 3aa941e4..f9e21ed0 100644 --- a/apps/ofm/views.py +++ b/apps/ofm/views.py @@ -1,4 +1,97 @@ from django.shortcuts import render -from apps.utils.viewsets import CustomModelViewSet +from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet +from .models import Mroom, MroomBooking, MroomSlot +from .serializers import MroomSerializer, MroomBookingSerializer, MroomSlotSerializer +from rest_framework.decorators import action +from apps.utils.mixins import CustomListModelMixin +from rest_framework.exceptions import ParseError +from apps.ofm.filters import MroomBookingFilterset +class MroomViewSet(CustomModelViewSet): + """list: 会议室 + + 会议室 + """ + queryset = Mroom.objects.all() + serializer_class = MroomSerializer + + +class MroomBookingViewSet(CustomModelViewSet): + """list: 会议室预订 + + 会议室预订 + """ + queryset = MroomBooking.objects.all() + serializer_class = MroomBookingSerializer + select_related_fields = ["create_by"] + filterset_class = MroomBookingFilterset + + def add_info_for_list(self, data): + booking_ids = [d["id"] for d in data] + slots = MroomSlot.objects.filter(booking__in=booking_ids).order_by("booking", "mroom", "mdate", "slot") + booking_info = {} + for slot in slots: + booking_id = slot.booking.id + + if booking_id not in booking_info: + booking_info[booking_id] = { + "mdate": slot.mdate.strftime("%Y-%m-%d"), # 格式化日期 + "mroom": slot.mroom.id, + "mroom_name": slot.mroom.name, # 会议室名称 + "time_ranges": [], # 存储时间段(如 ["8:00-9:00", "10:00-11:30"]) + "current_slots": [], # 临时存储连续的slot(用于合并) + } + + # 检查是否连续(当前slot是否紧接上一个slot) + current_slots = booking_info[booking_id]["current_slots"] + if not current_slots or slot.slot == current_slots[-1] + 1: + current_slots.append(slot.slot) + else: + # 如果不连续,先把当前连续的slot转换成时间段 + if current_slots: + start_time = self._slot_to_time(current_slots[0]) + end_time = self._slot_to_time(current_slots[-1] + 1) + booking_info[booking_id]["time_ranges"].append(f"{start_time}-{end_time}") + current_slots.clear() + current_slots.append(slot.slot) + + # 处理最后剩余的连续slot + for info in booking_info.values(): + if info["current_slots"]: + start_time = self._slot_to_time(info["current_slots"][0]) + end_time = self._slot_to_time(info["current_slots"][-1] + 1) + info["time_ranges"].append(f"{start_time}-{end_time}") + info.pop("current_slots") # 清理临时数据 + + for item in data: + item.update(booking_info.get(item["id"], {})) + return data + + @staticmethod + def _slot_to_time(slot): + """将slot (0-47) 转换为 HH:MM 格式的时间字符串""" + hours = slot // 2 + minutes = (slot % 2) * 30 + return f"{hours:02d}:{minutes:02d}" + + def perform_update(self, serializer): + ins:MroomBooking = self.get_object() + if ins.create_by != self.request.user: + raise ParseError("只允许创建者修改") + return super().perform_update(serializer) + + def perform_destroy(self, instance): + if instance.create_by != self.request.user: + raise ParseError("只允许创建者删除") + return super().perform_destroy(instance) + + +class MroomSlotViewSet(CustomListModelMixin, CustomGenericViewSet): + """list: 会议室预订时段 + + 会议室预订时段 + """ + queryset = MroomSlot.objects.all() + serializer_class = MroomSlotSerializer + filterset_fields = ["mroom", "mdate", "booking"] \ No newline at end of file diff --git a/server/settings.py b/server/settings.py index 3dc73675..99c15c5d 100755 --- a/server/settings.py +++ b/server/settings.py @@ -84,7 +84,8 @@ INSTALLED_APPS = [ 'apps.dpm', 'apps.cm', 'apps.cms', - 'apps.wpmw' + 'apps.wpmw', + 'apps.ofm' ] MIDDLEWARE = [ diff --git a/server/urls.py b/server/urls.py index 0eb2954c..56fcf7f9 100755 --- a/server/urls.py +++ b/server/urls.py @@ -74,6 +74,7 @@ urlpatterns = [ path('', include('apps.cm.urls')), path('', include('apps.cms.urls')), path('', include('apps.wpmw.urls')), + path('', include('apps.ofm.urls')), # 前端页面入口 path('', TemplateView.as_view(template_name="index.html")),