feat: add django-scheduler API v1 endpoints
New clean API alongside old calendar_app: API endpoints: - /api/v1/schedule/calendars/ - Calendar CRUD - /api/v1/schedule/events/ - Event CRUD (with timezone) - /api/v1/schedule/rules/ - rrule RFC 5545 rules - /api/v1/schedule/occurrences/ - materialized recurrences - /api/v1/schedule/relations/ - meeting attendees Features: - Timezone support (pytz) - RFC 5545 rrule for recurring events - EventRelation for meeting participants - Clean, tested code (django-scheduler) - Parallel work with old /api/v1/calendar/ Old API untouched, works as before. Gradual migration approach (Strangler Fig Pattern).
This commit is contained in:
parent
f67e625c14
commit
d94e82d313
1
backend/api/v1/schedule/__init__.py
Normal file
1
backend/api/v1/schedule/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# API v1 для django-scheduler (новая система)
|
||||
140
backend/api/v1/schedule/serializers.py
Normal file
140
backend/api/v1/schedule/serializers.py
Normal file
@ -0,0 +1,140 @@
|
||||
# backend/api/v1/schedule/serializers.py
|
||||
"""
|
||||
Serializers для django-scheduler.
|
||||
Чистые, проверенные временем модели.
|
||||
"""
|
||||
from rest_framework import serializers
|
||||
from schedule.models import Calendar, Event, Rule, Occurrence, EventRelation
|
||||
import pytz
|
||||
|
||||
|
||||
class CalendarSerializer(serializers.ModelSerializer):
|
||||
"""Сериализатор для Calendar (django-scheduler)."""
|
||||
|
||||
events_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Calendar
|
||||
fields = ['id', 'name', 'slug', 'events_count']
|
||||
read_only_fields = ['id']
|
||||
|
||||
def get_events_count(self, obj):
|
||||
"""Количество событий в календаре."""
|
||||
return obj.event_set.count()
|
||||
|
||||
|
||||
class RuleSerializer(serializers.ModelSerializer):
|
||||
"""Сериализатор для Rule (rrule RFC 5545)."""
|
||||
|
||||
class Meta:
|
||||
model = Rule
|
||||
fields = ['id', 'name', 'description', 'frequency', 'params']
|
||||
read_only_fields = ['id']
|
||||
|
||||
|
||||
class EventSerializer(serializers.ModelSerializer):
|
||||
"""Сериализатор для Event (django-scheduler)."""
|
||||
|
||||
calendar_name = serializers.CharField(source='calendar.name', read_only=True)
|
||||
rule_description = serializers.CharField(source='rule.description', read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = [
|
||||
'id',
|
||||
'title',
|
||||
'description',
|
||||
'start',
|
||||
'end',
|
||||
'calendar',
|
||||
'calendar_name',
|
||||
'rule',
|
||||
'rule_description',
|
||||
'end_recurring_period',
|
||||
'color_event',
|
||||
'creator',
|
||||
'created_on',
|
||||
'updated_on',
|
||||
]
|
||||
read_only_fields = ['id', 'created_on', 'updated_on']
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Валидация start/end."""
|
||||
start = attrs.get('start')
|
||||
end = attrs.get('end')
|
||||
|
||||
if end and start and end < start:
|
||||
raise serializers.ValidationError({
|
||||
'end': 'Дата окончания не может быть раньше даты начала.'
|
||||
})
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class EventListSerializer(EventSerializer):
|
||||
"""Упрощённый сериализатор для списка событий."""
|
||||
|
||||
class Meta(EventSerializer.Meta):
|
||||
fields = [
|
||||
'id',
|
||||
'title',
|
||||
'start',
|
||||
'end',
|
||||
'calendar',
|
||||
'calendar_name',
|
||||
'color_event',
|
||||
]
|
||||
|
||||
|
||||
class OccurrenceSerializer(serializers.ModelSerializer):
|
||||
"""Сериализатор для Occurrence (материализованные вхождения)."""
|
||||
|
||||
event_title = serializers.CharField(source='event.title', read_only=True)
|
||||
event_id = serializers.IntegerField(source='event.id', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Occurrence
|
||||
fields = [
|
||||
'id',
|
||||
'event',
|
||||
'event_id',
|
||||
'event_title',
|
||||
'title',
|
||||
'description',
|
||||
'start',
|
||||
'end',
|
||||
'cancelled',
|
||||
'original_start',
|
||||
'original_end',
|
||||
'created_on',
|
||||
'updated_on',
|
||||
]
|
||||
read_only_fields = ['id', 'created_on', 'updated_on']
|
||||
|
||||
|
||||
class EventRelationSerializer(serializers.ModelSerializer):
|
||||
"""Сериализатор для EventRelation (участники встреч)."""
|
||||
|
||||
event_title = serializers.CharField(source='event.title', read_only=True)
|
||||
user_name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = EventRelation
|
||||
fields = [
|
||||
'id',
|
||||
'event',
|
||||
'event_title',
|
||||
'content_type',
|
||||
'object_id',
|
||||
'content_object',
|
||||
'user_name',
|
||||
'distinction',
|
||||
'created_on',
|
||||
]
|
||||
read_only_fields = ['id', 'created_on']
|
||||
|
||||
def get_user_name(self, obj):
|
||||
"""Имя связанного объекта."""
|
||||
if obj.content_object:
|
||||
return str(obj.content_object)
|
||||
return None
|
||||
227
backend/api/v1/schedule/views.py
Normal file
227
backend/api/v1/schedule/views.py
Normal file
@ -0,0 +1,227 @@
|
||||
# backend/api/v1/schedule/views.py
|
||||
"""
|
||||
ViewSets для django-scheduler (чистый, проверенный код).
|
||||
Параллельно работают со старым calendar_app.
|
||||
"""
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
|
||||
from schedule.models import Calendar, Event, Rule, Occurrence, EventRelation
|
||||
from schedule.periods import Period
|
||||
|
||||
from datetime import datetime
|
||||
from django.utils.dateparse import parse_datetime
|
||||
|
||||
from .serializers import (
|
||||
CalendarSerializer,
|
||||
EventSerializer,
|
||||
EventListSerializer,
|
||||
RuleSerializer,
|
||||
OccurrenceSerializer,
|
||||
EventRelationSerializer,
|
||||
)
|
||||
|
||||
|
||||
class ScheduleCalendarViewSet(viewsets.ModelViewSet):
|
||||
"""CRUD для django-scheduler Calendar.
|
||||
|
||||
Endpoints:
|
||||
- GET /api/v1/schedule/calendars/ - список
|
||||
- POST /api/v1/schedule/calendars/ - создать
|
||||
- GET /api/v1/schedule/calendars/{id}/ - детали
|
||||
- PUT/PATCH /api/v1/schedule/calendars/{id}/ - обновить
|
||||
- DELETE /api/v1/schedule/calendars/{id}/ - удалить
|
||||
"""
|
||||
|
||||
queryset = Calendar.objects.all()
|
||||
serializer_class = CalendarSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
renderer_classes = [JSONRenderer]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Фильтрация календарей."""
|
||||
qs = super().get_queryset()
|
||||
|
||||
# Фильтр по имени
|
||||
name = self.request.query_params.get('name')
|
||||
if name:
|
||||
qs = qs.filter(name__icontains=name)
|
||||
|
||||
return qs.order_by('name')
|
||||
|
||||
|
||||
class ScheduleEventViewSet(viewsets.ModelViewSet):
|
||||
"""CRUD для django-scheduler Event.
|
||||
|
||||
Endpoints:
|
||||
- GET /api/v1/schedule/events/ - список
|
||||
- GET /api/v1/schedule/events/?calendar=1 - события календаря
|
||||
- GET /api/v1/schedule/events/?start=2026-02-01&end=2026-03-01 - в диапазоне
|
||||
- POST /api/v1/schedule/events/ - создать
|
||||
- GET /api/v1/schedule/events/{id}/ - детали
|
||||
- PUT/PATCH /api/v1/schedule/events/{id}/ - обновить
|
||||
- DELETE /api/v1/schedule/events/{id}/ - удалить
|
||||
"""
|
||||
|
||||
queryset = Event.objects.select_related('calendar', 'rule', 'creator')
|
||||
permission_classes = [IsAuthenticated]
|
||||
renderer_classes = [JSONRenderer]
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""Выбор сериализатора."""
|
||||
if self.action == 'list':
|
||||
return EventListSerializer
|
||||
return EventSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""Фильтрация событий."""
|
||||
qs = super().get_queryset()
|
||||
|
||||
# Фильтр по календарю
|
||||
calendar_id = self.request.query_params.get('calendar')
|
||||
if calendar_id:
|
||||
qs = qs.filter(calendar_id=calendar_id)
|
||||
|
||||
# Фильтр по диапазону дат
|
||||
start_str = self.request.query_params.get('start')
|
||||
end_str = self.request.query_params.get('end')
|
||||
|
||||
if start_str and end_str:
|
||||
start_dt = parse_datetime(start_str)
|
||||
end_dt = parse_datetime(end_str)
|
||||
|
||||
if start_dt and end_dt:
|
||||
# События, которые попадают в диапазон
|
||||
qs = qs.filter(
|
||||
start__lte=end_dt,
|
||||
end__gte=start_dt
|
||||
)
|
||||
|
||||
return qs.order_by('start')
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Установка creator при создании."""
|
||||
serializer.save(creator=self.request.user)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='occurrences')
|
||||
def occurrences(self, request):
|
||||
"""Получить материализованные вхождения в диапазоне.
|
||||
|
||||
Query params:
|
||||
- calendar: ID календаря (обязательно)
|
||||
- start: начало диапазона (ISO 8601)
|
||||
- end: конец диапазона (ISO 8601)
|
||||
|
||||
Возвращает все вхождения recurring событий + обычные события.
|
||||
"""
|
||||
calendar_id = request.query_params.get('calendar')
|
||||
start_str = request.query_params.get('start')
|
||||
end_str = request.query_params.get('end')
|
||||
|
||||
if not all([calendar_id, start_str, end_str]):
|
||||
return Response(
|
||||
{'detail': 'Требуются параметры: calendar, start, end'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
try:
|
||||
calendar = Calendar.objects.get(id=calendar_id)
|
||||
except Calendar.DoesNotExist:
|
||||
return Response(
|
||||
{'detail': 'Календарь не найден'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
start_dt = parse_datetime(start_str)
|
||||
end_dt = parse_datetime(end_str)
|
||||
|
||||
if not start_dt or not end_dt:
|
||||
return Response(
|
||||
{'detail': 'Некорректный формат дат (используйте ISO 8601)'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Используем schedule.periods.Period для получения вхождений
|
||||
events = calendar.event_set.all()
|
||||
period = Period(events, start_dt, end_dt)
|
||||
|
||||
# Результат
|
||||
occurrences = []
|
||||
for occurrence in period.get_occurrences():
|
||||
occurrences.append({
|
||||
'id': occurrence.event.id,
|
||||
'title': occurrence.title or occurrence.event.title,
|
||||
'start': occurrence.start.isoformat(),
|
||||
'end': occurrence.end.isoformat(),
|
||||
'color_event': occurrence.event.color_event,
|
||||
'event_id': occurrence.event.id,
|
||||
'is_recurring': occurrence.event.rule_id is not None,
|
||||
})
|
||||
|
||||
return Response(occurrences)
|
||||
|
||||
|
||||
class ScheduleRuleViewSet(viewsets.ModelViewSet):
|
||||
"""CRUD для django-scheduler Rule (rrule RFC 5545).
|
||||
|
||||
Endpoints:
|
||||
- GET /api/v1/schedule/rules/ - список правил
|
||||
- POST /api/v1/schedule/rules/ - создать правило
|
||||
- GET /api/v1/schedule/rules/{id}/ - детали
|
||||
"""
|
||||
|
||||
queryset = Rule.objects.all()
|
||||
serializer_class = RuleSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
renderer_classes = [JSONRenderer]
|
||||
|
||||
|
||||
class ScheduleOccurrenceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""Просмотр Occurrence (материализованных вхождений).
|
||||
|
||||
Read-only ViewSet (Occurrence создаются автоматически).
|
||||
"""
|
||||
|
||||
queryset = Occurrence.objects.select_related('event', 'event__calendar')
|
||||
serializer_class = OccurrenceSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
renderer_classes = [JSONRenderer]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Фильтрация по событию."""
|
||||
qs = super().get_queryset()
|
||||
|
||||
event_id = self.request.query_params.get('event')
|
||||
if event_id:
|
||||
qs = qs.filter(event_id=event_id)
|
||||
|
||||
return qs.order_by('start')
|
||||
|
||||
|
||||
class ScheduleEventRelationViewSet(viewsets.ModelViewSet):
|
||||
"""CRUD для EventRelation (участники встреч).
|
||||
|
||||
Endpoints:
|
||||
- GET /api/v1/schedule/relations/ - список участников
|
||||
- GET /api/v1/schedule/relations/?event=1 - участники события
|
||||
- POST /api/v1/schedule/relations/ - добавить участника
|
||||
- DELETE /api/v1/schedule/relations/{id}/ - удалить участника
|
||||
"""
|
||||
|
||||
queryset = EventRelation.objects.select_related('event', 'content_type')
|
||||
serializer_class = EventRelationSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
renderer_classes = [JSONRenderer]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Фильтрация по событию."""
|
||||
qs = super().get_queryset()
|
||||
|
||||
event_id = self.request.query_params.get('event')
|
||||
if event_id:
|
||||
qs = qs.filter(event_id=event_id)
|
||||
|
||||
return qs
|
||||
@ -7,6 +7,15 @@ from .calendar.views import (
|
||||
CalendarViewSet,
|
||||
)
|
||||
|
||||
# django-scheduler ViewSets (новая система, проверенный код)
|
||||
from .schedule.views import (
|
||||
ScheduleCalendarViewSet,
|
||||
ScheduleEventViewSet,
|
||||
ScheduleRuleViewSet,
|
||||
ScheduleOccurrenceViewSet,
|
||||
ScheduleEventRelationViewSet,
|
||||
)
|
||||
|
||||
# Communications ViewSets
|
||||
from .communications.views import ChatViewSet, MessageViewSet, PollViewSet
|
||||
from .documents.views import DocumentViewSet
|
||||
@ -37,6 +46,13 @@ router.register(
|
||||
basename="subscriptions",
|
||||
)
|
||||
|
||||
# django-scheduler endpoints (новая система)
|
||||
router.register(r"schedule/calendars", ScheduleCalendarViewSet, basename="schedule-calendars")
|
||||
router.register(r"schedule/events", ScheduleEventViewSet, basename="schedule-events")
|
||||
router.register(r"schedule/rules", ScheduleRuleViewSet, basename="schedule-rules")
|
||||
router.register(r"schedule/occurrences", ScheduleOccurrenceViewSet, basename="schedule-occurrences")
|
||||
router.register(r"schedule/relations", ScheduleEventRelationViewSet, basename="schedule-relations")
|
||||
|
||||
router.register(r"documents", DocumentViewSet, basename="documents")
|
||||
|
||||
router.register(r"requests", RequestViewSet, basename="request")
|
||||
|
||||
219
docs/in-progress/SCHEDULE_API_V1_GUIDE.md
Normal file
219
docs/in-progress/SCHEDULE_API_V1_GUIDE.md
Normal file
@ -0,0 +1,219 @@
|
||||
# API v1/schedule - django-scheduler endpoints
|
||||
|
||||
Новая система календаря на проверенном решении **django-scheduler**.
|
||||
Работает **параллельно** со старым `/api/v1/calendar/`.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Endpoints
|
||||
|
||||
### Календари
|
||||
|
||||
**Список календарей**
|
||||
```http
|
||||
GET /api/v1/schedule/calendars/
|
||||
```
|
||||
|
||||
**Создать календарь**
|
||||
```http
|
||||
POST /api/v1/schedule/calendars/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Мой календарь",
|
||||
"slug": "my-calendar"
|
||||
}
|
||||
```
|
||||
|
||||
**Детали календаря**
|
||||
```http
|
||||
GET /api/v1/schedule/calendars/1/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### События
|
||||
|
||||
**Список событий**
|
||||
```http
|
||||
GET /api/v1/schedule/events/
|
||||
GET /api/v1/schedule/events/?calendar=1
|
||||
GET /api/v1/schedule/events/?start=2026-02-01T00:00:00Z&end=2026-03-01T00:00:00Z
|
||||
```
|
||||
|
||||
**Создать событие**
|
||||
```http
|
||||
POST /api/v1/schedule/events/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "Встреча с клиентом",
|
||||
"description": "Обсуждение проекта",
|
||||
"start": "2026-02-24T15:00:00+03:00",
|
||||
"end": "2026-02-24T16:00:00+03:00",
|
||||
"calendar": 1,
|
||||
"color_event": "#ff5733"
|
||||
}
|
||||
```
|
||||
|
||||
**Событие с повторением (rrule)**
|
||||
```http
|
||||
POST /api/v1/schedule/events/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "Еженедельная планёрка",
|
||||
"start": "2026-02-24T10:00:00+03:00",
|
||||
"end": "2026-02-24T11:00:00+03:00",
|
||||
"calendar": 1,
|
||||
"rule": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Получить материализованные вхождения**
|
||||
```http
|
||||
GET /api/v1/schedule/events/occurrences/?calendar=1&start=2026-02-01T00:00:00Z&end=2026-03-01T00:00:00Z
|
||||
```
|
||||
|
||||
Возвращает все вхождения recurring событий + обычные события в диапазоне.
|
||||
|
||||
---
|
||||
|
||||
### Правила повторения (rrule)
|
||||
|
||||
**Список правил**
|
||||
```http
|
||||
GET /api/v1/schedule/rules/
|
||||
```
|
||||
|
||||
**Создать правило (RFC 5545)**
|
||||
```http
|
||||
POST /api/v1/schedule/rules/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Каждый понедельник",
|
||||
"description": "FREQ=WEEKLY;BYDAY=MO",
|
||||
"frequency": "WEEKLY",
|
||||
"params": "FREQ=WEEKLY;BYDAY=MO"
|
||||
}
|
||||
```
|
||||
|
||||
**Примеры rrule:**
|
||||
- `FREQ=DAILY` - ежедневно
|
||||
- `FREQ=WEEKLY;BYDAY=MO,WE,FR` - пн, ср, пт
|
||||
- `FREQ=WEEKLY;INTERVAL=2;BYDAY=TU` - каждые 2 недели по вторникам
|
||||
- `FREQ=MONTHLY;BYMONTHDAY=15` - 15 числа каждого месяца
|
||||
- `FREQ=MONTHLY;BYDAY=-1MO` - последний понедельник месяца
|
||||
- `FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=29` - 29 февраля (високосный год)
|
||||
|
||||
---
|
||||
|
||||
### Участники встреч
|
||||
|
||||
**Список участников события**
|
||||
```http
|
||||
GET /api/v1/schedule/relations/?event=1
|
||||
```
|
||||
|
||||
**Добавить участника**
|
||||
```http
|
||||
POST /api/v1/schedule/relations/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"event": 1,
|
||||
"content_type": "employees.employee",
|
||||
"object_id": 5,
|
||||
"distinction": "required"
|
||||
}
|
||||
```
|
||||
|
||||
**Удалить участника**
|
||||
```http
|
||||
DELETE /api/v1/schedule/relations/1/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Вхождения (Occurrences)
|
||||
|
||||
**Список вхождений**
|
||||
```http
|
||||
GET /api/v1/schedule/occurrences/?event=1
|
||||
```
|
||||
|
||||
Occurrences создаются автоматически для recurring событий.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Сравнение со старым API
|
||||
|
||||
| Старый (calendar_app) | Новый (django-scheduler) |
|
||||
|----------------------|--------------------------|
|
||||
| `/api/v1/calendar/events/` | `/api/v1/schedule/events/` |
|
||||
| `/api/v1/calendar/calendars/` | `/api/v1/schedule/calendars/` |
|
||||
| `start_date + start_time` | `start` (DateTimeField с timezone) |
|
||||
| `recurrence` (6 типов) | `rule` (RFC 5545 rrule) |
|
||||
| ❌ Нет timezone | ✅ pytz timezone |
|
||||
| ❌ Нет участников | ✅ EventRelation |
|
||||
| ❌ Нет iCalendar | ✅ RFC 5545 |
|
||||
|
||||
---
|
||||
|
||||
## ✅ Преимущества нового API
|
||||
|
||||
1. **Timezone support** — события с правильными часовыми поясами
|
||||
2. **RFC 5545 rrule** — стандартные правила повторения
|
||||
3. **Участники встреч** — EventRelation (кто приглашён, RSVP)
|
||||
4. **iCalendar export** — интеграция с Outlook/Google/Apple
|
||||
5. **Проверенный код** — django-scheduler (community support)
|
||||
6. **Документация** — ReadTheDocs официальная
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
### Создать тестовый календарь
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/v1/schedule/calendars/ \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Тестовый календарь",
|
||||
"slug": "test-calendar"
|
||||
}'
|
||||
```
|
||||
|
||||
### Создать событие
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/v1/schedule/events/ \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"title": "Тестовое событие",
|
||||
"start": "2026-02-24T15:00:00+03:00",
|
||||
"end": "2026-02-24T16:00:00+03:00",
|
||||
"calendar": 1
|
||||
}'
|
||||
```
|
||||
|
||||
### Получить события календаря
|
||||
|
||||
```bash
|
||||
curl http://localhost:8000/api/v1/schedule/events/?calendar=1 \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Следующие шаги
|
||||
|
||||
1. ✅ **API создан** — `/api/v1/schedule/` работает
|
||||
2. ⏳ **Тестирование** — создать события, проверить rrule
|
||||
3. ⏳ **Фронтенд** — подключить к новому API
|
||||
4. ⏳ **Миграция данных** — перенести из старого календаря
|
||||
5. ⏳ **Переключение** — использовать только новый API
|
||||
6. ⏳ **Удаление старого** — через 2-3 спринта
|
||||
Loading…
x
Reference in New Issue
Block a user