This commit is contained in:
Vladislav 2026-07-19 23:05:59 -04:00
parent 75a072bcdf
commit 8f312bc038
5 changed files with 72 additions and 137 deletions

View File

@ -454,7 +454,7 @@
"metadata": {},
"outputs": [],
"source": [
"!ssh kultmost 'systemctl restart vk-celery.service'\n"
"!ssh kultmost 'systemctl restart vk-celery.service'"
]
},
{

View File

@ -11,20 +11,12 @@
{{ header.section(user) }}
<form class="c_sbrxk">
<div class="c_dtjoh">
<!-- Афиша (файл) -->
<div class="c_aihm6 floating-group file-group poster-group">
<!-- Отображение имени файла с обрезкой -->
<div class="file-name-display" id="poster_name">
Выберите изображение
</div>
<!-- Скрытый input -->
<input type="file" id="poster" name="poster" class="c_xhd5k hidden-file" accept="image/*">
<!-- Плавающий лейбл -->
<label for="poster" class="c_z3ybu floating-label">Афиша</label>
<!-- Иконка справа -->
<svg class="file-icon" width="2237" height="24" viewBox="0 0 2237 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
@ -37,15 +29,11 @@
d="M2217.5 21C2216.95 21 2216.48 20.8042 2216.09 20.4125C2215.7 20.0208 2215.5 19.55 2215.5 19V5C2215.5 4.45 2215.7 3.97917 2216.09 3.5875C2216.48 3.19583 2216.95 3 2217.5 3H2231.5C2232.05 3 2232.52 3.19583 2232.91 3.5875C2233.3 3.97917 2233.5 4.45 2233.5 5V19C2233.5 19.55 2233.3 20.0208 2232.91 20.4125C2232.52 20.8042 2232.05 21 2231.5 21H2217.5ZM2217.5 19H2231.5V5H2217.5V19ZM2218.5 17H2230.5L2226.75 12L2223.75 16L2221.5 13L2218.5 17Z"
fill="#5F1798" />
</svg>
</div>
<!-- Название -->
<div class="c_aihm6 floating-group">
<input type="text" id="title" name="title" class="c_xhd5k" >
<label for="title" class="c_z3ybu floating-label">Название выставки</label>
</div>
<!-- Тема -->
<div class="c_aihm6 floating-group">
<input type="text" id="topic" name="topic" class="c_xhd5k" >
<label for="topic" class="c_z3ybu floating-label">Тема выставки</label>
@ -54,13 +42,10 @@
<input type="datetime-local" id="datetime_start" name="datetime_start" class="c_xhd5k" >
<label for="datetime_start" class="c_z3ybu floating-label">Дата открытия (дата и время)</label>
</div>
<div class="c_aihm6 floating-group">
<input type="date" id="date_finish" name="date_finish" class="c_xhd5k" >
<label for="date_finish" class="c_z3ybu floating-label">Дата закрытия</label>
</div>
<!-- Переключатель -->
<div class="toggle-group">
<label class="toggle-option t1 active" data-type="file">
<input type="radio" name="rules_type" value="file" checked>
@ -71,13 +56,10 @@
Ссылка
</label>
</div>
<!-- Блок файла -->
<div class="c_nb6gy floating-group file-group poster-group" id="file-block-1">
<div class="file-name-display" id="doc_name_1">Выберите файл</div>
<input type="file" id="tizer" name="tizer" class="c_4ykpn hidden-file" accept=".pdf,.doc,.docx" >
<label for="tizer" class="c_z3ybu floating-label">Тизер (PDF, DOC)</label>
<svg class="file-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2H6C4.89543 2 4 2.89543 4 4V20C4 21.1046 4.89543 22 6 22H18C19.1046 22 20 21.1046 20 20V8L14 2Z" stroke="#5F1798" stroke-width="2" stroke-linejoin="round"/>
<path d="M14 2V8H20" stroke="#5F1798" stroke-width="2" stroke-linejoin="round"/>
@ -85,15 +67,11 @@
<path d="M9 15H15" stroke="#5F1798" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<!-- Блок ссылки -->
<div class="c_aihm6 floating-group" id="link-block-1" style="display: none;">
<input type="url" id="tizer_link" name="tizer_link" class="c_xhd5k" placeholder="https://">
<label for="tizer_link" class="c_z3ybu floating-label">Тизер (ссылка)</label>
<div id="link-error-1" class="error-message" style="display:none; color:#e74c3c; font-size:0.85em; margin-top:4px;"></div>
</div>
<!-- Переключатель -->
<div class="toggle-group">
<label class="toggle-option t2 active" data-type="file">
<input type="radio" name="rules_type" value="file" checked>
@ -104,13 +82,10 @@
Ссылка
</label>
</div>
<!-- Блок файла -->
<div class="c_nb6gy floating-group file-group poster-group" id="file-block-2">
<div class="file-name-display" id="doc_name_2">Выберите файл</div>
<input type="file" id="rules" name="rules" class="c_4ykpn hidden-file" accept=".pdf,.doc,.docx" >
<label for="rules" class="c_z3ybu floating-label">Условия конкурса (PDF, DOC)</label>
<svg class="file-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2H6C4.89543 2 4 2.89543 4 4V20C4 21.1046 4.89543 22 6 22H18C19.1046 22 20 21.1046 20 20V8L14 2Z" stroke="#5F1798" stroke-width="2" stroke-linejoin="round"/>
<path d="M14 2V8H20" stroke="#5F1798" stroke-width="2" stroke-linejoin="round"/>
@ -118,26 +93,21 @@
<path d="M9 15H15" stroke="#5F1798" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<!-- Блок ссылки -->
<div class="c_aihm6 floating-group" id="link-block-2" style="display: none;">
<input type="url" id="rules_link" name="rules_link" class="c_xhd5k" placeholder="https://">
<label for="rules_link" class="c_z3ybu floating-label">Условия конкурса (ссылка)</label>
<div id="link-error-2" class="error-message" style="display:none; color:#e74c3c; font-size:0.85em; margin-top:4px;"></div>
</div>
<div class="c_aihm6 floating-group">
<input
type="text"
id="address"
name="address"
class="c_xhd5k"
list="address_list"
>
<label for="address" class="c_z3ybu floating-label">Адресс для отправки картин</label>
</div>
<datalist id="address_list">
{% for doc in user.send_address %}
<option value="{{ doc }}"></option>
@ -149,27 +119,22 @@
id="place"
name="place"
class="c_xhd5k"
list="place_list"
>
<label for="place" class="c_z3ybu floating-label">Место проведения</label>
</div>
<datalist id="place_list">
{% for doc in user.places %}
<option value="{{ doc }}"></option>
{% endfor %}
</datalist>
<button class="c_aq9et" id="createBtn">
<span class="c_som9s">Сохранить изменения</span>
</button>
</div>
<!-- Превью остаётся без изменений -->
<div class="c_rtopc">
<h2 class="c_1gai0">Карточка выставки</h2>
<article class="c_9fgdh b_ax8vz">
<div class="b_ornr9">
<div id="preview_poster_back"
@ -202,21 +167,16 @@
</div>
</form>
<script defer>
/* ---------- Utility helpers (removed) ---------- */
/* Toggle visibility of a block (flex / none) */
function toggleBlock(id, show) {
document.querySelector(id).style.display = show ? 'flex' : 'none';
document.querySelector(id).querySelector('input').required = show ? true : false;
}
/* Reset an input field and hide its error */
function resetInput(inputId, errorId) {
document.querySelector(inputId).value = '';
document.querySelector(errorId).style.display = 'none';
}
/* Show an error message */
function showError(errorId, msg) {
const el = document.querySelector(errorId);
el.textContent = msg;
@ -225,12 +185,10 @@ function showError(errorId, msg) {
el.style.bottom = '-1.6em';
}
/* ---------- Toggle between file / link ---------- */
function setupToggle(toggleClass, fileBlockId, linkBlockId, fileInputId, linkInputId, linkErrorId) {
const buttons = document.querySelectorAll(toggleClass);
buttons.forEach(btn => {
btn.addEventListener('click', () => {
// clear previous active state
buttons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
@ -247,7 +205,6 @@ function setupToggle(toggleClass, fileBlockId, linkBlockId, fileInputId, linkInp
});
}
/* ---------- File‑name display ---------- */
function setupFileNameDisplay(inputId, outputId) {
const input = document.querySelector(inputId);
input.addEventListener('change', () => {
@ -256,10 +213,9 @@ function setupFileNameDisplay(inputId, outputId) {
});
}
/* ---------- Link validation ---------- */
function validateHttpsLink(inputId, errorId) {
const value = document.querySelector(inputId).value.trim();
if (!value) return true; // empty is allowed (handled elsewhere)
if (!value) return true;
if (!value.startsWith('https://')) {
showError(errorId, 'Ссылка должна начинаться с https://');
@ -276,48 +232,39 @@ function validateHttpsLink(inputId, errorId) {
}
}
/* ---------- Initialization ---------- */
// 1️⃣ First toggle set
setupToggle(
setupToggle(
'.t1',
'#file-block-1',
'#link-block-1',
'#tizer',
'#tizer_link',
'#link-error-1'
);
);
// 2️⃣ Second toggle set
setupToggle(
setupToggle(
'.t2',
'#file-block-2',
'#link-block-2',
'#rules',
'#rules_link',
'#link-error-2'
);
);
// 3️⃣ Show selected file name
setupFileNameDisplay('#rules', '#doc_name_2');
setupFileNameDisplay('#tizer', '#doc_name_1');
setupFileNameDisplay('#rules', '#doc_name_2');
setupFileNameDisplay('#tizer', '#doc_name_1');
/* -------------------------------------------------
Attach link‑field validation & form‑submit handling
for an arbitrary toggle group.
------------------------------------------------- */
function bindLinkValidation({
linkInputSelector, // selector for the <input type="url">
linkErrorSelector, // selector for the error <div/span>
formSelector = 'form'// form to listen on (default = first form)
linkInputSelector,
linkErrorSelector,
formSelector = 'form'
}) {
const linkInput = document.querySelector(linkInputSelector);
const linkError = document.querySelector(linkErrorSelector);
const form = document.querySelector(formSelector);
if (!linkInput || !linkError || !form) return; // safety guard
if (!linkInput || !linkError || !form) return;
// --- live feedback -------------------------------------------------
linkInput.addEventListener('blur', () =>
validateHttpsLink(linkInputSelector, linkErrorSelector)
);
@ -326,35 +273,27 @@ function bindLinkValidation({
linkError.style.display = 'none';
});
// --- submit validation ---------------------------------------------
form.addEventListener('submit', e => {
const activeBtn = document.querySelector('.toggle-option.active');
const active = activeBtn ? activeBtn.dataset.type : null;
// only run validation when the *current* active option is a link
if (active === 'link') {
const valid = validateHttpsLink(linkInputSelector, linkErrorSelector);
if (!valid) e.preventDefault();
}
// extra file‑validation could be added here
});
}
/* -------------------------------------------------
Initialise both toggle groups (tizer & rules)
------------------------------------------------- */
bindLinkValidation({
bindLinkValidation({
linkInputSelector: '#rules_link',
linkErrorSelector: '#link-error-2'
});
});
bindLinkValidation({
bindLinkValidation({
linkInputSelector: '#tizer_link',
linkErrorSelector: '#link-error-1'
});
});
</script>
<script defer>
function setTomorrowAtNoon() {
const input = document.getElementById('datetime_start');
@ -375,70 +314,69 @@ function bindLinkValidation({
setTomorrowAtNoon();
</script>
<script defer>
const exhibition = JSON.parse({{exhibition|tojson}})
const exhibitionId = exhibition._id
document.getElementById('title').value = exhibition.title;
function setDateOrDateTimeLocal(elId, isoString) {
if (!isoString) return;
function setDateOrDateTimeLocal(elId, isoString) {
if (!isoString) return;
const s = isoString.replace(/^"|"$/g, ""); // на случай двойных кавычек
const [datePart, timePart = "00:00:00"] = s.split("T");
const s = isoString.replace(/^"|"$/g, ""); // на случай двойных кавычек
const [datePart, timePart = "00:00:00"] = s.split("T");
const [y, m, d] = datePart.split("-").map(Number);
const [hh, mm, ss = "0"] = timePart.split(":");
const [y, m, d] = datePart.split("-").map(Number);
const [hh, mm, ss = "0"] = timePart.split(":");
// Date constructor: month is 0-based
const dt = new Date(y, m - 1, d, Number(hh), Number(mm), Number(ss));
// Date constructor: month is 0-based
const dt = new Date(y, m - 1, d, Number(hh), Number(mm), Number(ss));
// Подготовим строку для input (локальное время)
const pad = (n) => String(n).padStart(2, "0");
const value = `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}` +
`T${pad(dt.getHours())}:${pad(dt.getMinutes())}:${pad(dt.getSeconds())}`;
// Подготовим строку для input (локальное время)
const pad = (n) => String(n).padStart(2, "0");
const value = `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}` +
`T${pad(dt.getHours())}:${pad(dt.getMinutes())}:${pad(dt.getSeconds())}`;
document.getElementById(elId).value = value;
}
document.getElementById(elId).value = value;
}
function setDateOnly(elId, isoString) {
if (!isoString) return;
const s = isoString.replace(/^"|"$/g, "");
const datePart = s.split("T")[0];
document.getElementById(elId).value = datePart; // YYYY-MM-DD
}
function setDateOnly(elId, isoString) {
if (!isoString) return;
const s = isoString.replace(/^"|"$/g, "");
const datePart = s.split("T")[0];
document.getElementById(elId).value = datePart; // YYYY-MM-DD
}
setDateOnly("date_finish", exhibition.date_finish);
setDateOrDateTimeLocal("datetime_start", exhibition.datetime_start);
setDateOnly("date_finish", exhibition.date_finish);
setDateOrDateTimeLocal("datetime_start", exhibition.datetime_start);
setDateOrDateTimeLocal("datetime_start", exhibition.datetime_start);
document.getElementById('topic').value = exhibition.topic;
document.getElementById('address').value = exhibition.address;
document.getElementById('place').value = exhibition.place;
document.getElementById('preview_poster').src = `/b/exhibition/${exhibition.poster_file_id}`
if (exhibition.rules_link){
console.log(exhibition.rules_link)
document.getElementById('rules_link').value = exhibition.rules_link;
document.getElementById('file-block-2').style.display = 'none';
document.getElementById('link-block-2').style.display = 'flex';
const select = document.querySelectorAll('.t2')
select.forEach(opt => opt.classList.remove('active'));
select[1].classList.add('active');
select[0].querySelector('input').checked=false
select[1].querySelector('input').checked=true
}
if (exhibition.tizer_link){
console.log(exhibition.tizer_link)
document.getElementById('file-block-1').style.display = 'none';
document.getElementById('link-block-1').style.display = 'flex';
const select = document.querySelectorAll('.t1')
select.forEach(opt => opt.classList.remove('active'));
select[1].classList.add('active');
select[0].querySelector('input').checked=false
select[1].querySelector('input').checked=true
document.getElementById('tizer_link').value = exhibition.tizer_link;
}
setDateOrDateTimeLocal("datetime_start", exhibition.datetime_start);
document.getElementById('topic').value = exhibition.topic;
document.getElementById('address').value = exhibition.address;
document.getElementById('place').value = exhibition.place;
document.getElementById('preview_poster').src = `/b/exhibition/${exhibition.poster_file_id}`
if (exhibition.rules_link){
console.log(exhibition.rules_link)
document.getElementById('rules_link').value = exhibition.rules_link;
document.getElementById('file-block-2').style.display = 'none';
document.getElementById('link-block-2').style.display = 'flex';
const select = document.querySelectorAll('.t2')
select.forEach(opt => opt.classList.remove('active'));
select[1].classList.add('active');
select[0].querySelector('input').checked=false
select[1].querySelector('input').checked=true
}
if (exhibition.tizer_link){
console.log(exhibition.tizer_link)
document.getElementById('file-block-1').style.display = 'none';
document.getElementById('link-block-1').style.display = 'flex';
const select = document.querySelectorAll('.t1')
select.forEach(opt => opt.classList.remove('active'));
select[1].classList.add('active');
select[0].querySelector('input').checked=false
select[1].querySelector('input').checked=true
document.getElementById('tizer_link').value = exhibition.tizer_link;
}
const titleInput = document.getElementById('title');

View File

@ -71,8 +71,6 @@
inputEl.dataset.list = JSON.stringify(list);
inputEl.dataset.value = post.grade;
document.getElementById('title').value = post.title;
document.getElementById('city').value = post.city;
document.getElementById('size').value = post.size;
@ -80,8 +78,8 @@
document.getElementById('genre').value = post.genre;
document.getElementById('price').value = post.price;
document.getElementById('description').value = post.description;
let filesOrdered = []; // теперь храним строки (имена файлов) + существующие медиа
console.log(post.medias_original) // ["6a5c33dcc10bbb64e654ba63"]
let filesOrdered = [];
console.log(post.medias_original)
const existingMedias = (post?.medias_original ?? []).map((media) => ({ url: `/b/posts/${media}`, name: `${media}`, isExisting: true, type: "image",}));
async function uploadMediaFiles(files) {
@ -98,7 +96,7 @@
});
if (!res.ok) throw new Error('Ошибка загрузки медиа');
return await res.json(); // ожидается массив имён файлов
return await res.json();
}
function loadExistingMedias() {
@ -220,7 +218,6 @@
renderPreviews();
}
// === Основная загрузка новых файлов ===
fileInput.addEventListener('change', async (e) => {
const newFiles = Array.from(e.target.files);
if (newFiles.length === 0) return;
@ -228,12 +225,11 @@
try {
const uploadedNames = await uploadMediaFiles(newFiles);
// Добавляем загруженные файлы в массив
newFiles.forEach((file, i) => {
filesOrdered.push({
name: uploadedNames['post_medias'][i], // имя с сервера
name: uploadedNames['post_medias'][i],
url: `/b/posts/${uploadedNames['post_medias'][i]}`,
file: file, // для превью
file: file,
type: file.type,
isExisting: false
});

View File

@ -129,7 +129,7 @@
"@router.get(\"/token\")\n",
"async def oauth_callback(code: str, state: str, device_id: str = None):\n",
" token_data = await token_manager.get_token(code=code, state=state, device_id=device_id)\n",
" return RedirectResponse(url='/profile/social', status_code=status.HTTP_303_SEE_OTHER)"
" return RedirectResponse(url='/profile/social', status_code=status.HTTP_303_SEE_OTHER)\n"
]
},
{

1
vk.py
View File

@ -75,6 +75,7 @@ async def oauth_callback(code: str, state: str, device_id: str = None):
token_data = await token_manager.get_token(code=code, state=state, device_id=device_id)
return RedirectResponse(url='/profile/social', status_code=status.HTTP_303_SEE_OTHER)
async def get_images(db_name: str, file_ids: list, sign_avatar: bool = True):
images = {}
photo_bytes_list= []