групповая привязка по тэгам
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2025-09-29 19:51:02 +03:00
parent f40dbbf41d
commit 7602c60bd6
2 changed files with 291 additions and 0 deletions
+83
View File
@@ -1283,6 +1283,89 @@ def api_venue_events(venue_name):
finally:
db.close()
# API endpoints для работы с тэгами
@bp.route('/api/tags')
@login_required
def api_tags():
"""Получает список всех уникальных тэгов с количеством событий"""
db = get_db()
try:
with db.conn.cursor() as cursor:
# Получаем все тэги и разбиваем их по пробелам
cursor.execute("""
SELECT tags
FROM events
WHERE tags IS NOT NULL AND tags != ''
""")
events = cursor.fetchall()
# Собираем все уникальные тэги
tag_counts = {}
for event in events:
tags = event['tags'].split()
for tag in tags:
tag = tag.strip()
if tag:
tag_counts[tag] = tag_counts.get(tag, 0) + 1
# Сортируем по количеству событий (убывание), затем по названию
sorted_tags = sorted(tag_counts.items(), key=lambda x: (-x[1], x[0]))
tags_list = []
for tag, count in sorted_tags:
tags_list.append({
'name': tag,
'event_count': count
})
return jsonify(tags_list)
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
db.close()
@bp.route('/api/tags/<tag_name>/events')
@login_required
def api_tag_events(tag_name):
"""Получает список событий для указанного тэга с информацией о привязке к посту"""
post_id = request.args.get('post_id')
if not post_id:
return jsonify({'error': 'post_id parameter is required'}), 400
db = get_db()
try:
with db.conn.cursor() as cursor:
# Получаем события с указанным тэгом с информацией о привязке к посту
cursor.execute("""
SELECT e.id, e.name, e.number, e.unit_name, e.tags,
CASE WHEN i.post_id IS NOT NULL THEN 1 ELSE 0 END as linked
FROM events e
LEFT JOIN interlinks i ON e.id = i.event_id AND i.post_id = %s
WHERE e.tags LIKE %s
ORDER BY e.name
""", (post_id, f'%{tag_name}%'))
events = cursor.fetchall()
events_list = []
for event in events:
# Проверяем, что тэг действительно присутствует (не подстрока другого тэга)
tags = event['tags'].split() if event['tags'] else []
if tag_name in tags:
events_list.append({
'id': event['id'],
'name': event['name'],
'number': event['number'],
'unit_name': event['unit_name'],
'tags': event['tags'],
'linked': bool(event['linked'])
})
return jsonify(events_list)
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
db.close()
@bp.route('/image_proxy')
@login_required
def image_proxy():
+208
View File
@@ -434,6 +434,9 @@
<button class="btn btn-sm btn-outline-success ms-2" id="post-venues-btn" style="display: none;">
<i class="fas fa-map-marker-alt me-1"></i> Площадки
</button>
<button class="btn btn-sm btn-outline-info ms-2" id="post-tags-btn" style="display: none;">
<i class="fas fa-tags me-1"></i> Тэги
</button>
</div>
<div class="row">
<!-- Левая панель - список постов -->
@@ -674,6 +677,52 @@
</div>
</div>
<!-- Модальное окно для выбора тэгов -->
<div class="modal fade" id="tagsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Выбор тэга</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="list-group" id="tags-list" style="max-height: 60vh; overflow-y: auto;">
<!-- Список тэгов будет загружен динамически -->
</div>
</div>
</div>
</div>
</div>
<!-- Модальное окно для выбора событий по тэгу -->
<div class="modal fade" id="tagEventsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">События с тэгом: <span id="selected-tag-name"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<button type="button" class="btn btn-sm btn-outline-primary me-2" id="select-all-tag-events">
<i class="fas fa-check-square me-1"></i> Выбрать все
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="deselect-all-tag-events">
<i class="fas fa-square me-1"></i> Снять все
</button>
</div>
<div class="list-group" id="tag-events-list" style="max-height: 50vh; overflow-y: auto;">
<!-- Список событий будет загружен динамически -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" id="confirm-tag-events-link">ОК</button>
</div>
</div>
</div>
</div>
<!-- Сообщение о процессе выполнения -->
<div id="processing-message" class="alert alert-info">
<div class="d-flex align-items-center">
@@ -1938,10 +1987,13 @@
// Обновление видимости кнопки "Площадки"
function updateVenuesButtonVisibility() {
const venuesBtn = document.getElementById('post-venues-btn');
const tagsBtn = document.getElementById('post-tags-btn');
if (selectedPostId) {
venuesBtn.style.display = 'inline-block';
tagsBtn.style.display = 'inline-block';
} else {
venuesBtn.style.display = 'none';
tagsBtn.style.display = 'none';
}
}
// Добавляем функции для отвязывания постов от событий и наоборот
@@ -2826,6 +2878,162 @@ document.addEventListener('DOMContentLoaded', () => {
throw new Error('Ошибка отвязки события от поста');
}
}
// Функции для работы с тэгами
let selectedTagName = '';
let selectedTagEvents = [];
// Обработчик клика по кнопке "Тэги"
document.getElementById('post-tags-btn').addEventListener('click', async () => {
if (!selectedPostId) {
alert('Сначала выберите пост');
return;
}
currentPostId = selectedPostId;
await loadTags();
const tagsModal = new bootstrap.Modal(document.getElementById('tagsModal'));
tagsModal.show();
});
// Загрузка списка тэгов
async function loadTags() {
try {
const response = await fetch(`${PREFIX}/api/tags`);
const tags = await response.json();
const tagsList = document.getElementById('tags-list');
tagsList.innerHTML = '';
if (tags.length === 0) {
tagsList.innerHTML = '<div class="list-group-item text-center text-muted">Тэги не найдены</div>';
return;
}
tags.forEach(tag => {
const tagItem = document.createElement('div');
tagItem.className = 'list-group-item list-group-item-action';
tagItem.innerHTML = `
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">${tag.name}</h6>
<small class="text-muted">${tag.event_count} событий</small>
</div>
`;
tagItem.addEventListener('click', () => {
selectedTagName = tag.name;
loadTagEvents(tag.name);
});
tagsList.appendChild(tagItem);
});
} catch (error) {
console.error('Ошибка загрузки тэгов:', error);
alert('Ошибка загрузки тэгов');
}
}
// Загрузка событий выбранного тэга
async function loadTagEvents(tagName) {
try {
const response = await fetch(`${PREFIX}/api/tags/${encodeURIComponent(tagName)}/events?post_id=${currentPostId}`);
const events = await response.json();
selectedTagEvents = events;
// Закрываем модальное окно тэгов
const tagsModal = bootstrap.Modal.getInstance(document.getElementById('tagsModal'));
tagsModal.hide();
// Показываем модальное окно событий
document.getElementById('selected-tag-name').textContent = tagName;
renderTagEventsList(events);
const tagEventsModal = new bootstrap.Modal(document.getElementById('tagEventsModal'));
tagEventsModal.show();
} catch (error) {
console.error('Ошибка загрузки событий тэга:', error);
alert('Ошибка загрузки событий тэга');
}
}
// Отображение списка событий с чекбоксами для тэгов
function renderTagEventsList(events) {
const eventsList = document.getElementById('tag-events-list');
eventsList.innerHTML = '';
if (events.length === 0) {
eventsList.innerHTML = '<div class="list-group-item text-center text-muted">События не найдены</div>';
return;
}
events.forEach(event => {
const eventItem = document.createElement('div');
eventItem.className = 'list-group-item';
eventItem.innerHTML = `
<div class="form-check">
<input class="form-check-input" type="checkbox" value="${event.id}" id="tag-event-${event.id}" ${event.linked ? 'checked' : ''}>
<label class="form-check-label" for="tag-event-${event.id}">
<strong>${event.name}</strong>
${event.number ? `<br><small class="text-muted">№ ${event.number}</small>` : ''}
${event.unit_name ? `<br><small class="text-muted">Площадка: ${event.unit_name}</small>` : ''}
</label>
</div>
`;
eventsList.appendChild(eventItem);
});
}
// Обработчики кнопок "Выбрать все" и "Снять все" для тэгов
document.getElementById('select-all-tag-events').addEventListener('click', () => {
document.querySelectorAll('#tag-events-list input[type="checkbox"]').forEach(checkbox => {
checkbox.checked = true;
});
});
document.getElementById('deselect-all-tag-events').addEventListener('click', () => {
document.querySelectorAll('#tag-events-list input[type="checkbox"]').forEach(checkbox => {
checkbox.checked = false;
});
});
// Обработчик кнопки "ОК" для подтверждения привязки/отвязки событий по тэгу
document.getElementById('confirm-tag-events-link').addEventListener('click', async () => {
const checkboxes = document.querySelectorAll('#tag-events-list input[type="checkbox"]');
const promises = [];
for (const checkbox of checkboxes) {
const eventId = parseInt(checkbox.value);
const isChecked = checkbox.checked;
const wasLinked = selectedTagEvents.find(e => e.id === eventId)?.linked || false;
// Если состояние изменилось, выполняем соответствующее действие
if (isChecked && !wasLinked) {
// Привязываем событие к посту
promises.push(linkEventToPost(currentPostId, eventId));
} else if (!isChecked && wasLinked) {
// Отвязываем событие от поста
promises.push(unlinkEventFromPost(currentPostId, eventId));
}
}
try {
await Promise.all(promises);
alert('Изменения сохранены');
// Закрываем модальное окно
const tagEventsModal = bootstrap.Modal.getInstance(document.getElementById('tagEventsModal'));
tagEventsModal.hide();
// Обновляем информацию о связях
updateInterlinkInfo();
// Обновляем детали выбранного поста, если он открыт
if (currentPostId) {
await showPostDetails(currentPostId);
}
} catch (error) {
console.error('Ошибка при сохранении изменений:', error);
alert('Ошибка при сохранении изменений');
}
});
});
// Убедимся, что функция доступна глобально, если потребуется вызвать из HTML
window.resetLayout = resetLayout;