Corp/docs/reports/API_ENDPOINTS_VALIDATION.md
Игорь Надеин 28e2f1615f feat: реализован drag & drop для перемещения событий между календарями
- Создан модуль calendarEventDragDrop.js с классами DragDropState и CalendarUtils
- Реализовано перетаскивание событий между календарями через модальное окно
- Добавлена кнопка 'Переместить все' для массового перемещения
- Модальное окно отображает источник и цель перемещения
- Поддержка legacy-календарей (company/department/personal)
- Добавлены стили для подсветки drop-target (папки и календари)
- Backend: исправлена обработка null значений в serializer
- Backend: добавлен endpoint list_raw для получения немодерируемых событий
- Использованы стандартные Bootstrap стили для модалов
- Исправлено всплытие событий dragover/drop (stopPropagation)
- Эмодзи иконки для типов календарей (����)
- Toast уведомления об успехе/ошибках перемещения
2026-02-12 15:59:53 +03:00

16 KiB
Raw Blame History

Отчет о проверке правильности использования API endpoints

Дата: 2025-02-12 Статус: ✅ ИСПРАВЛЕНО Файлы изменены: 2


🔍 Обнаруженные проблемы

Критичность: ВЫСОКАЯ

Проблема: Фронтенд использовал хардкод URL'ов вместо централизованных констант из apiUrls.js.

Последствия:

  1. ❌ Дублирование кода - URL'ы прописаны в 11+ местах
  2. ❌ Сложность поддержки - при изменении endpoint'а нужно править везде
  3. ❌ Риск опечаток - легко ошибиться при ручном вводе URL
  4. ❌ Неконсистентность - некоторые функции используют константы, другие - хардкод

📊 Сравнение с бэкендом

Backend Endpoints (Django REST Framework)

# backend/api/v1/urls.py
router.register(r"calendar/events", CalendarEventsViewSet, basename="events")
router.register(r"calendar/calendars", CalendarViewSet, basename="calendars")
router.register(r"calendar/subscriptions", CalendarSubscriptionViewSet, basename="subscriptions")

CalendarViewSet Actions:

Метод Endpoint Описание
GET /api/v1/calendar/calendars/ Список всех календарей
POST /api/v1/calendar/calendars/ Создать календарь
GET /api/v1/calendar/calendars/{id}/ Получить календарь
PATCH /api/v1/calendar/calendars/{id}/ Обновить календарь
DELETE /api/v1/calendar/calendars/{id}/ Удалить календарь
GET /api/v1/calendar/calendars/my-calendars/ Мои календари (custom action)
POST /api/v1/calendar/calendars/{id}/subscribe/ Подписаться
POST /api/v1/calendar/calendars/{id}/unsubscribe/ Отписаться
POST /api/v1/calendar/calendars/{id}/invite/ Пригласить пользователя
POST /api/v1/calendar/calendars/{id}/invite-bulk/ Массовое приглашение

CalendarEventsViewSet:

Метод Endpoint Описание
GET /api/v1/calendar/events/ Список событий
POST /api/v1/calendar/events/ Создать событие
GET /api/v1/calendar/events/{id}/ Получить событие
PATCH /api/v1/calendar/events/{id}/ Обновить событие
DELETE /api/v1/calendar/events/{id}/ Удалить событие

CalendarSubscriptionViewSet:

Метод Endpoint Описание
GET /api/v1/calendar/subscriptions/ Мои подписки
PATCH /api/v1/calendar/subscriptions/{id}/ Обновить подписку

🛠️ Внесенные исправления

1. Обновлен backend/static/js/constants/apiUrls.js

До:

export const API_URLS = {
  CALENDARS: "/api/v1/calendar/calendars/",
  CALENDAR_DETAIL: (id) => `/api/v1/calendar/calendars/${id}/`,
  CALENDAR_SUBSCRIBE: (id) => `/api/v1/calendar/calendars/${id}/subscribe/`,
  CALENDAR_UNSUBSCRIBE: (id) => `/api/v1/calendar/calendars/${id}/unsubscribe/`,
  // Отсутствуют: CALENDARS_MY, CALENDAR_INVITE, CALENDAR_INVITE_BULK
  // Отсутствуют: SUBSCRIPTIONS, SUBSCRIPTION_DETAIL
};

После:

export const API_URLS = {
  // Calendar Events API
  EVENTS: "/api/v1/calendar/events/",
  EVENT_DETAIL: (id) => `/api/v1/calendar/events/${id}/`,

  // Calendars API
  CALENDARS: "/api/v1/calendar/calendars/",
  CALENDARS_MY: "/api/v1/calendar/calendars/my-calendars/",  // ✅ ДОБАВЛЕНО
  CALENDAR_DETAIL: (id) => `/api/v1/calendar/calendars/${id}/`,
  CALENDAR_SUBSCRIBE: (id) => `/api/v1/calendar/calendars/${id}/subscribe/`,
  CALENDAR_UNSUBSCRIBE: (id) => `/api/v1/calendar/calendars/${id}/unsubscribe/`,
  CALENDAR_INVITE: (id) => `/api/v1/calendar/calendars/${id}/invite/`,  // ✅ ДОБАВЛЕНО
  CALENDAR_INVITE_BULK: (id) => `/api/v1/calendar/calendars/${id}/invite-bulk/`,  // ✅ ДОБАВЛЕНО

  // Calendar Subscriptions API  // ✅ ДОБАВЛЕНО
  SUBSCRIPTIONS: "/api/v1/calendar/subscriptions/",
  SUBSCRIPTION_DETAIL: (id) => `/api/v1/calendar/subscriptions/${id}/`,

  // Departments API
  MY_DEPARTMENTS: "/api/v1/departments/my-departments/",
  DEPARTMENT_DETAIL: (id) => `/api/v1/departments/${id}/`,

  // Employees API
  EMPLOYEES: "/api/v1/employees/",
  EMPLOYEE_DETAIL: (id) => `/api/v1/employees/${id}/`,
};

2. Рефакторинг backend/static/js/api/calendarsApi.js

Заменены 11 хардкод URL'ов на использование констант из API_URLS:

Функция До После
getMyCalendars() "/api/v1/calendar/calendars/" API_URLS.CALENDARS_MY ⚠️
createCalendar() "/api/v1/calendar/calendars/" API_URLS.CALENDARS
updateCalendar() `/api/v1/calendar/calendars/${id}/` API_URLS.CALENDAR_DETAIL(id)
deleteCalendar() `/api/v1/calendar/calendars/${id}/` API_URLS.CALENDAR_DETAIL(id)
inviteUserToCalendar() `/api/v1/calendar/calendars/${id}/invite/` API_URLS.CALENDAR_INVITE(id)
inviteBulkToCalendar() `/api/v1/calendar/calendars/${id}/invite-bulk/` API_URLS.CALENDAR_INVITE_BULK(id)
getMySubscriptions() "/api/v1/calendar/subscriptions/" API_URLS.SUBSCRIPTIONS
updateSubscription() `/api/v1/calendar/subscriptions/${id}/` API_URLS.SUBSCRIPTION_DETAIL(id)
getEvents() "/api/v1/calendar/events/" API_URLS.EVENTS
getEvent() `/api/v1/calendar/events/${id}/` API_URLS.EVENT_DETAIL(id)
createEvent() "/api/v1/calendar/events/" API_URLS.EVENTS
updateEvent() `/api/v1/calendar/events/${id}/` API_URLS.EVENT_DETAIL(id)
deleteEvent() `/api/v1/calendar/events/${id}/` API_URLS.EVENT_DETAIL(id)

⚠️ Важное исправление: getMyCalendars() теперь использует правильный endpoint my-calendars, а не базовый список.


🐛 Обнаруженная логическая ошибка

Проблема в getMyCalendars()

Было:

export async function getMyCalendars(ttl = API_DEFAULTS.TTL.CALENDARS) {
  const url = new URL(API_URLS.CALENDARS, window.location.origin);
  // Запрашивал: GET /api/v1/calendar/calendars/
  // Ожидал: data.results (pagination)
}

Проблема:

  • Endpoint /api/v1/calendar/calendars/ возвращает ВСЕ календари (с пагинацией)
  • Нужен endpoint /api/v1/calendar/calendars/my-calendars/ для календарей пользователя

Стало:

export async function getMyCalendars(ttl = API_DEFAULTS.TTL.CALENDARS) {
  const url = new URL(API_URLS.CALENDARS_MY, window.location.origin);
  // Запрашивает: GET /api/v1/calendar/calendars/my-calendars/
  // Возвращает: массив напрямую (без pagination)

  const data = await response.json();
  return Array.isArray(data) ? data : data.results || [];
}

Backend код (для справки):

# backend/api/v1/calendar/views.py
@rest_action(
    detail=False,
    methods=["get"],
    permission_classes=[IsAuthenticated],
    url_path="my-calendars",
)
def my_calendars(self, request):
    """Список всех календарей, доступных текущему пользователю."""
    user = request.user
    calendars = Calendar.objects.get_available_for_user(user)
    serializer = CalendarSerializer(calendars, many=True, context={"request": request})
    return Response(serializer.data, status=status.HTTP_200_OK)

✅ Итоговая таблица соответствия

Frontend функция Backend endpoint Метод Статус
getMyCalendars() /calendars/my-calendars/ GET ✅ Исправлено
getCalendar() /calendars/{id}/ GET ✅ Корректно
createCalendar() /calendars/ POST ✅ Корректно
updateCalendar() /calendars/{id}/ PATCH ✅ Корректно
deleteCalendar() /calendars/{id}/ DELETE ✅ Корректно
subscribeToCalendar() /calendars/{id}/subscribe/ POST ✅ Корректно
unsubscribeFromCalendar() /calendars/{id}/unsubscribe/ POST ✅ Корректно
inviteUserToCalendar() /calendars/{id}/invite/ POST ✅ Добавлено
inviteBulkToCalendar() /calendars/{id}/invite-bulk/ POST ✅ Добавлено
getMySubscriptions() /subscriptions/ GET ✅ Корректно
updateSubscription() /subscriptions/{id}/ PATCH ✅ Корректно
getEvents() /events/ GET ✅ Корректно
getEvent() /events/{id}/ GET ✅ Корректно
createEvent() /events/ POST ✅ Корректно
updateEvent() /events/{id}/ PATCH ✅ Корректно
deleteEvent() /events/{id}/ DELETE ✅ Корректно

Отсутствуют в frontend:

  • ❌ getCalendarSubscriptions(calendarId) - список участников календаря
  • ❌ removeUserFromCalendar(calendarId, userId) - удалить участника

📈 Преимущества исправлений

До исправления:

// 11 мест с хардкод URL'ами
const url = new URL("/api/v1/calendar/calendars/", ...);
const url = new URL(`/api/v1/calendar/calendars/${id}/`, ...);
const url = new URL(`/api/v1/calendar/calendars/${id}/subscribe/`, ...);
// ... и т.д.

После исправления:

// Централизованные константы
const url = new URL(API_URLS.CALENDARS, ...);
const url = new URL(API_URLS.CALENDAR_DETAIL(id), ...);
const url = new URL(API_URLS.CALENDAR_SUBSCRIBE(id), ...);

Плюсы:

  1. ✅ Single Source of Truth - все URL'ы в одном месте
  2. ✅ Легкость рефакторинга - изменение в одном файле
  3. ✅ Отсутствие опечаток - автодополнение IDE
  4. ✅ Консистентность - единый стиль во всем коде
  5. ✅ Документирование - JSDoc в константах

🧪 Тестирование

Рекомендуется проверить:

  1. Загрузка календарей:

    // Должен запрашивать /api/v1/calendar/calendars/my-calendars/
    const calendars = await getMyCalendars();
    console.log("Мои календари:", calendars);
    
  2. CRUD операции:

    // Создание
    const newCal = await createCalendar({ title: "Test", calendar_type: "personal" });
    
    // Обновление
    await updateCalendar(newCal.id, { title: "Updated" });
    
    // Удаление
    await deleteCalendar(newCal.id);
    
  3. Приглашения:

    // Пригласить пользователя
    await inviteUserToCalendar(calendarId, {
      user_id: 123,
      can_edit: true,
      notify: true
    });
    
    // Массовое приглашение
    await inviteBulkToCalendar(calendarId, {
      user_ids: [123, 456, 789],
      can_edit: false,
      can_manage: false
    });
    
  4. Подписки:

    // Подписаться
    await subscribeToCalendar(calendarId);
    
    // Обновить подписку
    const subs = await getMySubscriptions();
    await updateSubscription(subs[0].id, { is_visible: false });
    
    // Отписаться
    await unsubscribeFromCalendar(calendarId);
    

🚀 Следующие шаги

Backend endpoints, которые еще нужно добавить:

  1. Получить участников календаря:

    @action(detail=True, methods=['get'], url_path='subscriptions')
    def get_calendar_subscriptions(self, request, pk=None):
        """Список участников календаря (только для владельца)"""
        # Вернуть CalendarSubscription.objects.filter(calendar_id=pk)
    
  2. Удалить участника:

    @action(detail=True, methods=['delete'], url_path='subscriptions/(?P<user_id>[^/.]+)')
    def remove_participant(self, request, pk=None, user_id=None):
        """Удалить участника из календаря (только владелец)"""
        # Удалить подписку
    
  3. Изменить права участника:

    @action(detail=True, methods=['patch'], url_path='subscriptions/(?P<user_id>[^/.]+)')
    def update_participant_permissions(self, request, pk=None, user_id=None):
        """Изменить права участника (только владелец)"""
        # Обновить can_edit, can_manage
    

Frontend функции для новых endpoints:

// backend/static/js/api/calendarsApi.js

export async function getCalendarParticipants(calendarId) {
  const url = new URL(
    `/api/v1/calendar/calendars/${calendarId}/subscriptions/`,
    window.location.origin
  );
  // ...
}

export async function removeParticipant(calendarId, userId) {
  const url = new URL(
    `/api/v1/calendar/calendars/${calendarId}/subscriptions/${userId}/`,
    window.location.origin
  );
  // DELETE
}

export async function updateParticipantPermissions(calendarId, userId, permissions) {
  const url = new URL(
    `/api/v1/calendar/calendars/${calendarId}/subscriptions/${userId}/`,
    window.location.origin
  );
  // PATCH with {can_edit, can_manage}
}

📝 Заключение

Что сделано:

✅ Добавлены недостающие константы в apiUrls.js (5 новых) ✅ Заменены 13 хардкод URL'ов на константы ✅ Исправлена логическая ошибка в getMyCalendars() (использовался неверный endpoint) ✅ Добавлена поддержка обработки как массива, так и пагинированного ответа

Текущий статус:

  • Базовые CRUD операции: 100% соответствие бэкенду ✅
  • Приглашения: API полностью готов ✅
  • Управление участниками: Требуется добавление backend endpoints ⚠️

Рекомендации:

  1. Собрать frontend (npm run build:app)
  2. Протестировать все endpoint'ы вручную
  3. Добавить недостающие backend endpoints для управления участниками
  4. Создать UI для работы с приглашениями

Статус: ✅ Критические проблемы устранены Риски: Минимальные - изменения backward-compatible Следующая задача: UI для управления участниками календаря