feat: ofm 完善1

This commit is contained in:
caoqianming 2025-06-25 17:29:46 +08:00
parent 31f4e2869d
commit ab22415e0f
8 changed files with 257 additions and 4 deletions

14
apps/ofm/filters.py Normal file
View File

@ -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"]
}

View File

@ -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')},
},
),
]

View File

@ -15,8 +15,8 @@ class MroomBooking(CommonADModel):
class MroomSlot(BaseModel): class MroomSlot(BaseModel):
"""TN: 会议室时段""" """TN: 会议室时段"""
mroom = models.ForeignKey(Mroom, on_delete=models.CASCADE) mroom = models.ForeignKey(Mroom, on_delete=models.CASCADE, related_name="slot_m")
booking = models.ForeignKey(MroomBooking, on_delete=models.CASCADE) booking = models.ForeignKey(MroomBooking, on_delete=models.CASCADE, related_name="slot_b")
mdate = models.DateField('会议日期', db_index=True) mdate = models.DateField('会议日期', db_index=True)
slot = models.PositiveIntegerField('时段', help_text='0-47') slot = models.PositiveIntegerField('时段', help_text='0-47')

64
apps/ofm/serializers.py Normal file
View File

@ -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__'

14
apps/ofm/urls.py Normal file
View File

@ -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)),
]

View File

@ -1,4 +1,97 @@
from django.shortcuts import render 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"]

View File

@ -84,7 +84,8 @@ INSTALLED_APPS = [
'apps.dpm', 'apps.dpm',
'apps.cm', 'apps.cm',
'apps.cms', 'apps.cms',
'apps.wpmw' 'apps.wpmw',
'apps.ofm'
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@ -74,6 +74,7 @@ urlpatterns = [
path('', include('apps.cm.urls')), path('', include('apps.cm.urls')),
path('', include('apps.cms.urls')), path('', include('apps.cms.urls')),
path('', include('apps.wpmw.urls')), path('', include('apps.wpmw.urls')),
path('', include('apps.ofm.urls')),
# 前端页面入口 # 前端页面入口
path('', TemplateView.as_view(template_name="index.html")), path('', TemplateView.as_view(template_name="index.html")),