feat: migrate frontend to django-scheduler API
- Updated API endpoints from /api/v1/calendar/ to /api/v1/schedule/ - Removed subscriptions concept (django-scheduler uses simpler approach) - Adapted CalendarSerializer to accept 'title' field for EUSRR compatibility - All calendar visibility now stored in localStorage - Removed deprecated subscribe/unsubscribe functions
This commit is contained in:
parent
d94e82d313
commit
63b6fff1ef
@ -9,18 +9,45 @@ import pytz
|
||||
|
||||
|
||||
class CalendarSerializer(serializers.ModelSerializer):
|
||||
"""Сериализатор для Calendar (django-scheduler)."""
|
||||
"""Сериализатор для Calendar (django-scheduler) с совместимостью EUSRR."""
|
||||
|
||||
# Совместимость с EUSRR API - принимаем title, конвертируем в name
|
||||
title = serializers.CharField(source='name', required=False)
|
||||
events_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Calendar
|
||||
fields = ['id', 'name', 'slug', 'events_count']
|
||||
fields = ['id', 'name', 'title', 'slug', 'events_count']
|
||||
read_only_fields = ['id']
|
||||
extra_kwargs = {
|
||||
'slug': {'required': False}, # Сделаем auto-generated если не указан
|
||||
}
|
||||
|
||||
def get_events_count(self, obj):
|
||||
"""Количество событий в календаре."""
|
||||
return obj.event_set.count()
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Создать календарь с автогенерацией slug из name/title."""
|
||||
from django.utils.text import slugify
|
||||
|
||||
# Если slug не указан - генерируем из name
|
||||
if 'slug' not in validated_data or not validated_data.get('slug'):
|
||||
name = validated_data.get('name', 'calendar')
|
||||
base_slug = slugify(name)
|
||||
if not base_slug:
|
||||
base_slug = 'calendar'
|
||||
|
||||
# Проверяем уникальность slug
|
||||
slug = base_slug
|
||||
counter = 1
|
||||
while Calendar.objects.filter(slug=slug).exists():
|
||||
slug = f"{base_slug}-{counter}"
|
||||
counter += 1
|
||||
|
||||
validated_data['slug'] = slug
|
||||
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class RuleSerializer(serializers.ModelSerializer):
|
||||
|
||||
@ -173,217 +173,11 @@ export async function deleteCalendar(calendarId) {
|
||||
dataManager.invalidate("calendars:my");
|
||||
}
|
||||
|
||||
/**
|
||||
* Подписаться на календарь
|
||||
* @param {number} calendarId - ID календаря
|
||||
* @param {Object} [options] - Опции подписки
|
||||
* @param {boolean} [options.can_edit] - Право редактирования
|
||||
* @param {boolean} [options.can_manage] - Право управления
|
||||
* @returns {Promise<Object>} Данные подписки
|
||||
*/
|
||||
export async function subscribeToCalendar(calendarId, options = {}) {
|
||||
const url = new URL(
|
||||
API_URLS.CALENDAR_SUBSCRIBE(calendarId),
|
||||
window.location.origin,
|
||||
);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify(options),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response
|
||||
.json()
|
||||
.catch(() => ({ detail: response.statusText }));
|
||||
throw new Error(error.detail || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
// Инвалидируем кеш
|
||||
dataManager.invalidate(`calendars:${calendarId}`);
|
||||
dataManager.invalidate("calendars:my");
|
||||
dataManager.invalidate("subscriptions:my");
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Отписаться от календаря
|
||||
* @param {number} calendarId - ID календаря
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function unsubscribeFromCalendar(calendarId) {
|
||||
const url = new URL(
|
||||
API_URLS.CALENDAR_UNSUBSCRIBE(calendarId),
|
||||
window.location.origin,
|
||||
);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: authHeaders(false),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response
|
||||
.json()
|
||||
.catch(() => ({ detail: response.statusText }));
|
||||
throw new Error(error.detail || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
// Инвалидируем кеш
|
||||
dataManager.invalidate(`calendars:${calendarId}`);
|
||||
dataManager.invalidate("calendars:my");
|
||||
dataManager.invalidate("subscriptions:my");
|
||||
}
|
||||
|
||||
/**
|
||||
* Пригласить пользователя в календарь (только владелец)
|
||||
* @param {number} calendarId - ID календаря
|
||||
* @param {Object} inviteData - Данные приглашения
|
||||
* @param {number} [inviteData.user_id] - ID пользователя
|
||||
* @param {string} [inviteData.username] - Username пользователя
|
||||
* @param {boolean} [inviteData.can_edit=false] - Право редактирования событий
|
||||
* @param {boolean} [inviteData.can_manage=false] - Право управления календарем
|
||||
* @param {boolean} [inviteData.notify=true] - Отправить уведомление
|
||||
* @returns {Promise<Object>} Созданная подписка
|
||||
*/
|
||||
export async function inviteUserToCalendar(calendarId, inviteData) {
|
||||
// ИСПРАВЛЕНО: используем константу вместо хардкода
|
||||
const url = new URL(
|
||||
API_URLS.CALENDAR_INVITE(calendarId),
|
||||
window.location.origin,
|
||||
);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify(inviteData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response
|
||||
.json()
|
||||
.catch(() => ({ detail: response.statusText }));
|
||||
throw new Error(error.detail || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
// Инвалидируем кеш
|
||||
dataManager.invalidate(`calendars:${calendarId}`);
|
||||
dataManager.invalidate("calendars:my");
|
||||
dataManager.invalidate("subscriptions:my");
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Массовое приглашение пользователей в календарь (только владелец)
|
||||
* @param {number} calendarId - ID календаря
|
||||
* @param {Object} inviteData - Данные приглашения
|
||||
* @param {number[]} [inviteData.user_ids] - Список ID пользователей
|
||||
* @param {string[]} [inviteData.usernames] - Список usernames
|
||||
* @param {boolean} [inviteData.can_edit=false] - Право редактирования событий
|
||||
* @param {boolean} [inviteData.can_manage=false] - Право управления календарем
|
||||
* @param {boolean} [inviteData.notify=true] - Отправить уведомления
|
||||
* @returns {Promise<Object>} Результат массового приглашения
|
||||
* @returns {Promise<{created: Array, already_subscribed: Array, errors: Array, total_created: number, total_already_subscribed: number, total_errors: number}>}
|
||||
*/
|
||||
export async function inviteBulkToCalendar(calendarId, inviteData) {
|
||||
// ИСПРАВЛЕНО: используем константу вместо хардкода
|
||||
const url = new URL(
|
||||
API_URLS.CALENDAR_INVITE_BULK(calendarId),
|
||||
window.location.origin,
|
||||
);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify(inviteData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response
|
||||
.json()
|
||||
.catch(() => ({ detail: response.statusText }));
|
||||
throw new Error(error.detail || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
// Инвалидируем кеш
|
||||
dataManager.invalidate(`calendars:${calendarId}`);
|
||||
dataManager.invalidate("calendars:my");
|
||||
dataManager.invalidate("subscriptions:my");
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить мои подписки на календари
|
||||
* @param {number} [ttl=60000] - Time to live кеша в мс
|
||||
* @returns {Promise<Array>} Массив подписок
|
||||
*/
|
||||
export async function getMySubscriptions(ttl = 60000) {
|
||||
const key = "subscriptions:my";
|
||||
|
||||
return dataManager.fetch(
|
||||
key,
|
||||
async () => {
|
||||
// ИСПРАВЛЕНО: используем константу вместо хардкода
|
||||
const url = new URL(API_URLS.SUBSCRIPTIONS, window.location.origin);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: authHeaders(false),
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
console.warn("[CalendarsAPI] 401 Unauthorized");
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.results || [];
|
||||
},
|
||||
ttl,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить подписку на календарь
|
||||
* @param {number} subscriptionId - ID подписки
|
||||
* @param {Object} updates - Обновления
|
||||
* @param {boolean} [updates.is_visible] - Видимость
|
||||
* @param {string} [updates.color_override] - Свой цвет
|
||||
* @returns {Promise<Object>} Обновленная подписка
|
||||
*/
|
||||
export async function updateSubscription(subscriptionId, updates) {
|
||||
// ИСПРАВЛЕНО: используем константу вместо хардкода
|
||||
const url = new URL(
|
||||
API_URLS.SUBSCRIPTION_DETAIL(subscriptionId),
|
||||
window.location.origin,
|
||||
);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "PATCH",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response
|
||||
.json()
|
||||
.catch(() => ({ detail: response.statusText }));
|
||||
throw new Error(error.detail || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
// Инвалидируем кеш
|
||||
dataManager.invalidate("subscriptions:my");
|
||||
dataManager.invalidate("calendars:my");
|
||||
|
||||
return response.json();
|
||||
}
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// ПРИМЕЧАНИЕ: Функции subscriptions, invite удалены
|
||||
// Django-scheduler не имеет концепции "подписок"
|
||||
// Видимость календарей управляется через localStorage
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Events API
|
||||
@ -537,7 +331,6 @@ export function invalidateEventsCache() {
|
||||
*/
|
||||
export function invalidateCalendarsCache() {
|
||||
dataManager.invalidatePattern(/^calendars:/);
|
||||
dataManager.invalidatePattern(/^subscriptions:/);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -5,10 +5,6 @@
|
||||
|
||||
import {
|
||||
getMyCalendars,
|
||||
getMySubscriptions,
|
||||
updateSubscription,
|
||||
subscribeToCalendar,
|
||||
unsubscribeFromCalendar,
|
||||
invalidateCalendarsCache,
|
||||
invalidateCalendarEventsCache,
|
||||
} from "../api/calendarsApi.js";
|
||||
@ -42,7 +38,6 @@ export function initCalendarManager(options = {}) {
|
||||
|
||||
// Состояние
|
||||
let calendars = [];
|
||||
let subscriptions = []; // Подписки пользователя
|
||||
let visibleCalendarIds = new Set();
|
||||
let eventCounts = new Map(); // Счётчики событий для папок: folderId -> count
|
||||
let dragDropInstance = null;
|
||||
@ -114,13 +109,8 @@ export function initCalendarManager(options = {}) {
|
||||
*/
|
||||
async function loadCalendars() {
|
||||
try {
|
||||
// Загрузить новые Calendar записи и подписки
|
||||
const [newCalendars, userSubscriptions] = await Promise.all([
|
||||
getMyCalendars(),
|
||||
getMySubscriptions(),
|
||||
]);
|
||||
|
||||
subscriptions = userSubscriptions;
|
||||
// Загрузить календари из django-scheduler API
|
||||
const newCalendars = await getMyCalendars();
|
||||
|
||||
// Добавить legacy календари
|
||||
const legacyCalendars = createLegacyCalendars();
|
||||
@ -141,39 +131,19 @@ export function initCalendarManager(options = {}) {
|
||||
newCalendars.map((c) => ({
|
||||
id: c.id,
|
||||
title: c.title,
|
||||
is_global: c.is_global,
|
||||
is_personal: c.is_personal,
|
||||
is_department: c.is_department,
|
||||
owner_user_id: c.owner_user_id,
|
||||
owner_department_id: c.owner_department_id,
|
||||
visibility: c.visibility,
|
||||
name: c.name,
|
||||
slug: c.slug,
|
||||
})),
|
||||
);
|
||||
|
||||
// Загрузить видимость календарей из подписок и localStorage
|
||||
// Загрузить видимость календарей из localStorage
|
||||
visibleCalendarIds = new Set();
|
||||
|
||||
calendars.forEach((cal) => {
|
||||
let isVisible = true;
|
||||
|
||||
if (cal.is_legacy || !cal.user_can_edit) {
|
||||
// Legacy календари или календари без прав редактирования - используем localStorage
|
||||
const storageKey = `calendar_visible_${cal.id}`;
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
isVisible = stored === null ? true : stored === "true";
|
||||
} else {
|
||||
// Новые календари с правами редактирования - используем подписки
|
||||
const subscription = subscriptions.find((s) => s.calendar === cal.id);
|
||||
if (subscription) {
|
||||
// Если есть подписка - используем её is_visible
|
||||
isVisible = subscription.is_visible;
|
||||
} else {
|
||||
// Если подписки нет - проверяем localStorage (fallback)
|
||||
const storageKey = `calendar_visible_${cal.id}`;
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
isVisible = stored === null ? true : stored === "true";
|
||||
}
|
||||
}
|
||||
// Используем localStorage для всех календарей
|
||||
const storageKey = `calendar_visible_${cal.id}`;
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
const isVisible = stored === null ? true : stored === "true";
|
||||
|
||||
if (isVisible) {
|
||||
visibleCalendarIds.add(cal.id);
|
||||
@ -296,65 +266,13 @@ export function initCalendarManager(options = {}) {
|
||||
visibleCalendarIds.delete(calendarId);
|
||||
}
|
||||
|
||||
// Найти календарь
|
||||
const calendar = calendars.find((c) => c.id === calendarId);
|
||||
|
||||
// Сохранить изменение
|
||||
// Сохранить в localStorage (django-scheduler не имеет subscriptions)
|
||||
try {
|
||||
// Проверяем, можно ли использовать API для этого календаря
|
||||
const canUseAPI =
|
||||
calendar && !calendar.is_legacy && calendar.user_can_edit;
|
||||
|
||||
if (!canUseAPI) {
|
||||
// Legacy календари или календари без прав редактирования - сохраняем в localStorage
|
||||
const storageKey = `calendar_visible_${calendarId}`;
|
||||
localStorage.setItem(storageKey, String(isVisible));
|
||||
console.log(
|
||||
`[CalendarManager] Saved calendar visibility to localStorage: ${calendarId} = ${isVisible}`,
|
||||
);
|
||||
} else {
|
||||
// Новые календари с правами редактирования - обновляем подписку через API
|
||||
const subscription = subscriptions.find(
|
||||
(s) => s.calendar === calendarId,
|
||||
);
|
||||
|
||||
if (subscription) {
|
||||
await updateSubscription(subscription.id, { is_visible: isVisible });
|
||||
console.log(
|
||||
`[CalendarManager] Updated subscription ${subscription.id} visibility: ${isVisible}`,
|
||||
);
|
||||
} else {
|
||||
// Если подписки нет - создаём её автоматически
|
||||
try {
|
||||
// Сначала создаём подписку
|
||||
const newSubscription = await subscribeToCalendar(calendarId);
|
||||
|
||||
// Если visibility не по умолчанию (false), обновляем её
|
||||
if (!isVisible) {
|
||||
await updateSubscription(newSubscription.id, {
|
||||
is_visible: isVisible,
|
||||
});
|
||||
}
|
||||
|
||||
// Обновляем локальный список подписок
|
||||
subscriptions.push({ ...newSubscription, is_visible: isVisible });
|
||||
console.log(
|
||||
`[CalendarManager] Created subscription ${newSubscription.id} for calendar ${calendarId} with visibility: ${isVisible}`,
|
||||
);
|
||||
} catch (subError) {
|
||||
// Если создание подписки не удалось - сохраняем в localStorage как fallback
|
||||
console.warn(
|
||||
`[CalendarManager] Failed to create subscription for calendar ${calendarId}:`,
|
||||
subError,
|
||||
);
|
||||
const storageKey = `calendar_visible_${calendarId}`;
|
||||
localStorage.setItem(storageKey, String(isVisible));
|
||||
console.log(
|
||||
`[CalendarManager] Saved to localStorage as fallback: ${isVisible}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const storageKey = `calendar_visible_${calendarId}`;
|
||||
localStorage.setItem(storageKey, String(isVisible));
|
||||
console.log(
|
||||
`[CalendarManager] Saved calendar visibility to localStorage: ${calendarId} = ${isVisible}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[CalendarManager] Failed to save visibility:", error);
|
||||
// Откатить изменение при ошибке
|
||||
@ -370,32 +288,38 @@ export function initCalendarManager(options = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Подписаться на календарь
|
||||
* Подписаться на календарь (заглушка для совместимости)
|
||||
* @deprecated django-scheduler не использует subscriptions
|
||||
* @param {number} calendarId
|
||||
*/
|
||||
async function subscribe(calendarId) {
|
||||
try {
|
||||
await subscribeToCalendar(calendarId);
|
||||
await loadCalendars(); // Перезагрузить список
|
||||
} catch (error) {
|
||||
console.error("[CalendarManager] Subscribe failed:", error);
|
||||
alert("Не удалось подписаться на календарь");
|
||||
console.warn(
|
||||
"[CalendarManager] subscribe() deprecated - django-scheduler doesn't use subscriptions",
|
||||
);
|
||||
const calendar = calendars.find((c) => c.id === calendarId);
|
||||
if (calendar) {
|
||||
// Просто показываем календарь
|
||||
if (!visibleCalendarIds.has(calendarId)) {
|
||||
await toggleCalendarVisibility(calendarId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отписаться от календаря
|
||||
* Отписаться от календаря (заглушка для совместимости)
|
||||
* @deprecated django-scheduler не использует subscriptions
|
||||
* @param {number} calendarId
|
||||
*/
|
||||
async function unsubscribe(calendarId) {
|
||||
if (!confirm("Отписаться от этого календаря?")) return;
|
||||
|
||||
try {
|
||||
await unsubscribeFromCalendar(calendarId);
|
||||
await loadCalendars(); // Перезагрузить список
|
||||
} catch (error) {
|
||||
console.error("[CalendarManager] Unsubscribe failed:", error);
|
||||
alert("Не удалось отписаться от календаря");
|
||||
console.warn(
|
||||
"[CalendarManager] unsubscribe() deprecated - django-scheduler doesn't use subscriptions",
|
||||
);
|
||||
const calendar = calendars.find((c) => c.id === calendarId);
|
||||
if (calendar) {
|
||||
// Просто скрываем календарь
|
||||
if (visibleCalendarIds.has(calendarId)) {
|
||||
await toggleCalendarVisibility(calendarId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,26 +4,31 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Базовые URL API эндпоинтов
|
||||
* Базовые URL API эндпоинтов (django-scheduler)
|
||||
* @enum {string}
|
||||
*/
|
||||
export const API_URLS = {
|
||||
// Calendar Events API
|
||||
EVENTS: "/api/v1/calendar/events/",
|
||||
EVENT_DETAIL: (id) => `/api/v1/calendar/events/${id}/`,
|
||||
// Schedule Events API (django-scheduler)
|
||||
EVENTS: "/api/v1/schedule/events/",
|
||||
EVENT_DETAIL: (id) => `/api/v1/schedule/events/${id}/`,
|
||||
EVENT_OCCURRENCES: "/api/v1/schedule/events/occurrences/",
|
||||
|
||||
// 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/`,
|
||||
// Schedule Calendars API (django-scheduler)
|
||||
CALENDARS: "/api/v1/schedule/calendars/",
|
||||
CALENDARS_MY: "/api/v1/schedule/calendars/", // django-scheduler не требует отдельного endpoint
|
||||
CALENDAR_DETAIL: (id) => `/api/v1/schedule/calendars/${id}/`,
|
||||
|
||||
// Calendar Subscriptions API
|
||||
SUBSCRIPTIONS: "/api/v1/calendar/subscriptions/",
|
||||
SUBSCRIPTION_DETAIL: (id) => `/api/v1/calendar/subscriptions/${id}/`,
|
||||
// Schedule Rules API (RFC 5545 rrule)
|
||||
RULES: "/api/v1/schedule/rules/",
|
||||
RULE_DETAIL: (id) => `/api/v1/schedule/rules/${id}/`,
|
||||
|
||||
// Schedule Occurrences API (materialized recurring events)
|
||||
OCCURRENCES: "/api/v1/schedule/occurrences/",
|
||||
OCCURRENCE_DETAIL: (id) => `/api/v1/schedule/occurrences/${id}/`,
|
||||
|
||||
// Schedule Relations API (event attendees)
|
||||
RELATIONS: "/api/v1/schedule/relations/",
|
||||
RELATION_DETAIL: (id) => `/api/v1/schedule/relations/${id}/`,
|
||||
|
||||
// Departments API
|
||||
MY_DEPARTMENTS: "/api/v1/departments/my-departments/",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user