Фикс: корректный placeholder для фото сотрудника при пустом avatar, форма отправляет POST на фронтовую вьюху, убрана JS-валидация и кастомный submit.
This commit is contained in:
parent
2131ffd920
commit
68352580e8
@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import base64
|
||||
import json
|
||||
import mimetypes
|
||||
from io import BytesIO
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
from api.client import get_api_client
|
||||
@ -16,6 +17,7 @@ from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.views.decorators.csrf import csrf_protect
|
||||
from django.views.decorators.http import require_http_methods, require_POST
|
||||
from django.http.multipartparser import MultiPartParser, MultiPartParserError
|
||||
|
||||
|
||||
from .forms_front import DepartmentEditForm, SetHeadForm
|
||||
@ -45,6 +47,41 @@ def _file_to_data_uri(uploaded) -> str:
|
||||
return f"data:{mime};base64,{b64}"
|
||||
|
||||
|
||||
def _parse_multipart_request(
|
||||
request: HttpRequest, logger
|
||||
) -> tuple[Dict[str, Any], Any]:
|
||||
"""Возвращает (POST, FILES) даже для PATCH с multipart/form-data."""
|
||||
content_type = request.content_type or ""
|
||||
if request.method == "POST" or "multipart/form-data" not in content_type:
|
||||
return request.POST, request.FILES
|
||||
|
||||
try:
|
||||
stream = BytesIO(request.body)
|
||||
parser = MultiPartParser(
|
||||
request.META,
|
||||
stream,
|
||||
request.upload_handlers,
|
||||
request.encoding,
|
||||
)
|
||||
post_data, files_data = parser.parse()
|
||||
logger.info(
|
||||
"[multipart_parser] Parsed PATCH payload: %s fields, %s files",
|
||||
len(post_data.keys()),
|
||||
len(files_data.keys()),
|
||||
)
|
||||
return post_data, files_data
|
||||
except MultiPartParserError as exc:
|
||||
logger.error(
|
||||
"[multipart_parser] Failed to parse multipart body: %s",
|
||||
exc,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"[multipart_parser] Unexpected error while parsing multipart body"
|
||||
)
|
||||
return request.POST, request.FILES
|
||||
|
||||
|
||||
def _extract_items(payload: Any):
|
||||
"""
|
||||
Универсальный парсер: пагинированный ответ DRF или «сырой» список.
|
||||
@ -444,8 +481,15 @@ def employee_edit(request, pk: int):
|
||||
# Проверяем Content-Type
|
||||
content_type = request.content_type or ''
|
||||
logger.info(f"[employee_edit] pk={pk}, Content-Type: {content_type}")
|
||||
logger.info(f"[employee_edit] request.POST keys: {list(request.POST.keys())}")
|
||||
logger.info(f"[employee_edit] request.FILES keys: {list(request.FILES.keys())}")
|
||||
parsed_post, parsed_files = _parse_multipart_request(request, logger)
|
||||
logger.info(
|
||||
"[employee_edit] request.POST keys: %s",
|
||||
list(parsed_post.keys()),
|
||||
)
|
||||
logger.info(
|
||||
"[employee_edit] request.FILES keys: %s",
|
||||
list(parsed_files.keys()),
|
||||
)
|
||||
|
||||
if 'multipart/form-data' in content_type:
|
||||
# Обрабатываем multipart/form-data (файлы)
|
||||
@ -453,21 +497,28 @@ def employee_edit(request, pk: int):
|
||||
payload = {}
|
||||
|
||||
# Собираем обычные поля из POST
|
||||
for key, value in request.POST.items():
|
||||
for key, value in parsed_post.items():
|
||||
if key == 'csrfmiddlewaretoken':
|
||||
continue
|
||||
# Если это массив (например skills_ids)
|
||||
if key.endswith('_ids') or key == 'skills':
|
||||
payload[key] = request.POST.getlist(key)
|
||||
payload[key] = parsed_post.getlist(key)
|
||||
else:
|
||||
payload[key] = value
|
||||
|
||||
logger.info(f"[employee_edit] Payload before avatar: {list(payload.keys())}")
|
||||
logger.info(
|
||||
"[employee_edit] Payload before avatar: %s",
|
||||
list(payload.keys()),
|
||||
)
|
||||
|
||||
# Обрабатываем файлы (аватар)
|
||||
if 'avatar' in request.FILES:
|
||||
avatar_file = request.FILES['avatar']
|
||||
logger.info(f"[employee_edit] Processing avatar: {avatar_file.name}, size: {avatar_file.size}")
|
||||
if 'avatar' in parsed_files:
|
||||
avatar_file = parsed_files['avatar']
|
||||
logger.info(
|
||||
"[employee_edit] Processing avatar: %s, size: %s",
|
||||
avatar_file.name,
|
||||
avatar_file.size,
|
||||
)
|
||||
# Конвертируем в data URI для Base64ImageField
|
||||
data_uri = _file_to_data_uri(avatar_file)
|
||||
payload['avatar'] = data_uri
|
||||
@ -494,7 +545,30 @@ def employee_edit(request, pk: int):
|
||||
data = resp.json
|
||||
except Exception:
|
||||
data = {"detail": resp.text}
|
||||
return JsonResponse(data or {}, status=resp.status)
|
||||
|
||||
accepts = (request.headers.get("Accept") or "").lower()
|
||||
is_ajax = request.headers.get("X-Requested-With") == "XMLHttpRequest"
|
||||
wants_json = "application/json" in accepts or is_ajax
|
||||
|
||||
if wants_json:
|
||||
return JsonResponse(data or {}, status=resp.status)
|
||||
|
||||
if resp.ok:
|
||||
messages.success(request, "Профиль сотрудника обновлён")
|
||||
else:
|
||||
detail = ""
|
||||
if isinstance(data, dict):
|
||||
detail = data.get("detail") or data.get("error") or ""
|
||||
messages.error(
|
||||
request,
|
||||
detail or f"Не удалось обновить сотрудника (HTTP {resp.status})",
|
||||
)
|
||||
|
||||
redirect_url = request.META.get("HTTP_REFERER") or reverse(
|
||||
"employees:employee_profile",
|
||||
args=(pk,),
|
||||
)
|
||||
return redirect(redirect_url)
|
||||
|
||||
|
||||
# ---------- EDIT ME ----------
|
||||
@ -517,8 +591,15 @@ def employee_edit_me(request):
|
||||
# Проверяем Content-Type
|
||||
content_type = request.content_type or ''
|
||||
logger.info(f"[employee_edit_me] Content-Type: {content_type}")
|
||||
logger.info(f"[employee_edit_me] request.POST keys: {list(request.POST.keys())}")
|
||||
logger.info(f"[employee_edit_me] request.FILES keys: {list(request.FILES.keys())}")
|
||||
parsed_post, parsed_files = _parse_multipart_request(request, logger)
|
||||
logger.info(
|
||||
"[employee_edit_me] request.POST keys: %s",
|
||||
list(parsed_post.keys()),
|
||||
)
|
||||
logger.info(
|
||||
"[employee_edit_me] request.FILES keys: %s",
|
||||
list(parsed_files.keys()),
|
||||
)
|
||||
|
||||
if 'multipart/form-data' in content_type:
|
||||
# Обрабатываем multipart/form-data (файлы)
|
||||
@ -526,29 +607,43 @@ def employee_edit_me(request):
|
||||
payload = {}
|
||||
|
||||
# Собираем обычные поля из POST
|
||||
for key, value in request.POST.items():
|
||||
for key, value in parsed_post.items():
|
||||
if key == 'csrfmiddlewaretoken':
|
||||
continue
|
||||
# Если это массив (например skills_ids)
|
||||
if key.endswith('_ids') or key == 'skills':
|
||||
payload[key] = request.POST.getlist(key)
|
||||
payload[key] = parsed_post.getlist(key)
|
||||
else:
|
||||
payload[key] = value
|
||||
|
||||
logger.info(f"[employee_edit_me] Payload before avatar: {list(payload.keys())}")
|
||||
logger.info(
|
||||
"[employee_edit_me] Payload before avatar: %s",
|
||||
list(payload.keys()),
|
||||
)
|
||||
|
||||
# Обрабатываем файлы (аватар)
|
||||
if 'avatar' in request.FILES:
|
||||
avatar_file = request.FILES['avatar']
|
||||
logger.info(f"[employee_edit_me] Processing avatar: {avatar_file.name}, size: {avatar_file.size}, content_type: {avatar_file.content_type}")
|
||||
if 'avatar' in parsed_files:
|
||||
avatar_file = parsed_files['avatar']
|
||||
logger.info(
|
||||
"[employee_edit_me] Processing avatar: %s, size: %s, content_type: %s",
|
||||
avatar_file.name,
|
||||
avatar_file.size,
|
||||
avatar_file.content_type,
|
||||
)
|
||||
# Конвертируем в data URI для Base64ImageField
|
||||
data_uri = _file_to_data_uri(avatar_file)
|
||||
payload['avatar'] = data_uri
|
||||
logger.info(f"[employee_edit_me] Avatar converted to data URI, length: {len(data_uri)}")
|
||||
logger.info(
|
||||
"[employee_edit_me] Avatar converted to data URI, length: %s",
|
||||
len(data_uri),
|
||||
)
|
||||
else:
|
||||
logger.warning("[employee_edit_me] No avatar in request.FILES")
|
||||
|
||||
logger.info(f"[employee_edit_me] Final payload keys: {list(payload.keys())}")
|
||||
logger.info(
|
||||
"[employee_edit_me] Final payload keys: %s",
|
||||
list(payload.keys()),
|
||||
)
|
||||
|
||||
# Отправляем в DRF как JSON (с base64 аватаром)
|
||||
resp = api.patch("v1/employees/me/", json=payload)
|
||||
@ -567,7 +662,27 @@ def employee_edit_me(request):
|
||||
data = resp.json
|
||||
except Exception:
|
||||
data = {"detail": resp.text}
|
||||
return JsonResponse(data or {}, status=resp.status)
|
||||
|
||||
accepts = (request.headers.get("Accept") or "").lower()
|
||||
is_ajax = request.headers.get("X-Requested-With") == "XMLHttpRequest"
|
||||
wants_json = "application/json" in accepts or is_ajax
|
||||
|
||||
if wants_json:
|
||||
return JsonResponse(data or {}, status=resp.status)
|
||||
|
||||
if resp.ok:
|
||||
messages.success(request, "Профиль обновлён")
|
||||
else:
|
||||
detail = ""
|
||||
if isinstance(data, dict):
|
||||
detail = data.get("detail") or data.get("error") or ""
|
||||
messages.error(
|
||||
request,
|
||||
detail or f"Не удалось обновить профиль (HTTP {resp.status})",
|
||||
)
|
||||
|
||||
redirect_url = request.META.get("HTTP_REFERER") or reverse("employees:profile")
|
||||
return redirect(redirect_url)
|
||||
|
||||
|
||||
# ---------- CREATE (staff) ----------
|
||||
|
||||
@ -1,296 +1,24 @@
|
||||
/**
|
||||
* employeeFormHandler.js
|
||||
* Обработчик формы редактирования сотрудника через API
|
||||
*
|
||||
* @module employeeFormHandler
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Инициализация обработчика формы редактирования сотрудника
|
||||
* @param {Object} options - Параметры инициализации
|
||||
* @param {string} options.formId - ID формы (по умолчанию 'apiEditForm')
|
||||
* @param {string[]} options.contactFields - Список полей контактов для валидации
|
||||
* @returns {Object} API формы { refresh, submit, clearErrors }
|
||||
* Минимальный стаб, оставленный ради обратной совместимости после
|
||||
* отказа от кастомного JS-сабмита. Теперь форма отправляется браузером
|
||||
* напрямую в фронтовую Django-вьюху, а модуль лишь возвращает
|
||||
* совместимое API без вмешательства в submit.
|
||||
*/
|
||||
export function initEmployeeForm(options = {}) {
|
||||
const config = {
|
||||
formId: options.formId || 'apiEditForm',
|
||||
contactFields: options.contactFields || ['email', 'phone_number', 'telegram', 'whatsapp', 'wechat']
|
||||
};
|
||||
const formId = options.formId || 'apiEditForm';
|
||||
const form = document.getElementById(formId);
|
||||
|
||||
const form = document.getElementById(config.formId);
|
||||
if (!form) {
|
||||
return null; // Тихий выход, если форма не найдена
|
||||
}
|
||||
|
||||
const endpoint = form.dataset.endpoint || '';
|
||||
if (!endpoint) {
|
||||
console.warn('Employee form: endpoint not found');
|
||||
console.warn('[EmployeeForm] Форма не найдена, инициализация пропущена');
|
||||
return null;
|
||||
}
|
||||
|
||||
const CONTACT_FIELDS = config.contactFields;
|
||||
const btn = form.querySelector('[type="submit"]');
|
||||
const btnLabel = btn?.querySelector('.btn-label');
|
||||
const btnSpin = btn?.querySelector('.btn-spinner');
|
||||
console.info('[EmployeeForm] JS-обработчик отключён, используется нативная отправка формы');
|
||||
|
||||
/**
|
||||
* Установить состояние отправки формы
|
||||
*/
|
||||
function setSubmitting(on) {
|
||||
if (!btn) return;
|
||||
if (on) {
|
||||
btn.setAttribute('disabled', '');
|
||||
btnSpin?.classList.remove('d-none');
|
||||
btnLabel?.classList.add('visually-hidden');
|
||||
} else {
|
||||
btn.removeAttribute('disabled');
|
||||
btnSpin?.classList.add('d-none');
|
||||
btnLabel?.classList.remove('visually-hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистить все ошибки валидации
|
||||
*/
|
||||
function clearErrors() {
|
||||
form.querySelectorAll('.is-invalid').forEach((el) => el.classList.remove('is-invalid'));
|
||||
form.querySelectorAll('.invalid-feedback').forEach((el) => (el.textContent = ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Показать ошибку для конкретного поля
|
||||
*/
|
||||
function fieldError(name, message) {
|
||||
const el = form.querySelector(`[name="${CSS.escape(name)}"]`);
|
||||
if (!el) return;
|
||||
el.classList.add('is-invalid');
|
||||
const container = el.closest('[class^="col-"]') || el.parentElement;
|
||||
let fb = container?.querySelector('.invalid-feedback');
|
||||
if (!fb) {
|
||||
fb = document.createElement('div');
|
||||
fb.className = 'invalid-feedback';
|
||||
el.after(fb);
|
||||
}
|
||||
fb.textContent = message || 'Некорректное значение';
|
||||
}
|
||||
|
||||
/**
|
||||
* Валидация наличия хотя бы одного контакта
|
||||
*/
|
||||
function ensureOneContact() {
|
||||
const hasAny = CONTACT_FIELDS.some((n) => (form.elements[n]?.value || '').trim().length > 0);
|
||||
if (hasAny) return true;
|
||||
CONTACT_FIELDS.forEach((n) => fieldError(n, 'Укажите хотя бы один контакт'));
|
||||
form.elements[CONTACT_FIELDS[0]]?.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Прочитать файл в Base64
|
||||
*/
|
||||
function readFileAsDataURL(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fr = new FileReader();
|
||||
fr.onload = () => resolve(fr.result);
|
||||
fr.onerror = reject;
|
||||
fr.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Собрать skills_ids (массив) и сравнить с начальным значением
|
||||
*/
|
||||
function collectSkillsPayload() {
|
||||
const sel = form.querySelector('select[name="skills_ids"]');
|
||||
if (!sel) return null;
|
||||
const selected = Array.from(sel.selectedOptions)
|
||||
.map((o) => o.value)
|
||||
.filter(Boolean);
|
||||
const initCsv = sel.dataset.init || '';
|
||||
const initArr = initCsv
|
||||
? initCsv
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
// Сравнение (без учёта порядка)
|
||||
const norm = (a) => a.slice().sort().join(',');
|
||||
if (norm(selected) === norm(initArr)) return null;
|
||||
return selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Собрать общие поля (кроме skills_ids и avatar)
|
||||
*/
|
||||
function buildScalarPayload() {
|
||||
const payload = {};
|
||||
const fields = form.querySelectorAll(
|
||||
'input[name]:not([type="file"]):not([name="clear_avatar"]):not(:disabled), select[name]:not([name="skills_ids"]):not(:disabled)'
|
||||
);
|
||||
|
||||
fields.forEach((el) => {
|
||||
const name = el.name;
|
||||
const raw = (el.value ?? '').toString().trim();
|
||||
const init = (el.dataset.init ?? el.defaultValue ?? '').toString();
|
||||
|
||||
// Ничего не менялось
|
||||
if (raw === init) return;
|
||||
|
||||
// Пустое значение: для некоторых полей нужно слать null
|
||||
const shouldNull = raw === '' && ['gender', 'birth_date', 'position_id'].includes(name);
|
||||
|
||||
if (shouldNull) {
|
||||
// Меняем непустое init -> null (очистка)
|
||||
if (init !== '') payload[name] = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Типизация gender в число
|
||||
if (name === 'gender' && raw !== '') {
|
||||
payload[name] = Number(raw);
|
||||
return;
|
||||
}
|
||||
|
||||
// Остальные — как есть (пустые строки допустимы для текстовых полей)
|
||||
payload[name] = raw;
|
||||
});
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработчик отправки формы
|
||||
*/
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
clearErrors();
|
||||
|
||||
if (!ensureOneContact()) return;
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const payload = buildScalarPayload();
|
||||
|
||||
// skills_ids (массив)
|
||||
const skills = collectSkillsPayload();
|
||||
if (skills) payload.skills_ids = skills;
|
||||
|
||||
// avatar (файл через FormData)
|
||||
const avatarInput = document.getElementById('avatarInput');
|
||||
const avatarFile = avatarInput?.files?.[0] || null;
|
||||
|
||||
console.log('[EmployeeForm] Avatar input:', avatarInput);
|
||||
console.log('[EmployeeForm] Avatar file:', avatarFile);
|
||||
console.log('[EmployeeForm] Payload:', payload);
|
||||
|
||||
let body;
|
||||
let headers = {
|
||||
'X-CSRFToken': form.querySelector('[name="csrfmiddlewaretoken"]')?.value || ''
|
||||
};
|
||||
|
||||
if (avatarFile) {
|
||||
// Если есть файл - используем FormData
|
||||
console.log('[EmployeeForm] Using FormData with avatar:', avatarFile.name, avatarFile.size);
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', avatarFile);
|
||||
|
||||
// Добавляем остальные поля
|
||||
Object.entries(payload).forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => formData.append(key, v));
|
||||
} else {
|
||||
formData.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
if (skills) {
|
||||
skills.forEach(id => formData.append('skills_ids', id));
|
||||
}
|
||||
|
||||
body = formData;
|
||||
console.log('[EmployeeForm] FormData entries:');
|
||||
for (let [key, value] of formData.entries()) {
|
||||
console.log(` ${key}:`, value);
|
||||
}
|
||||
// Не устанавливаем Content-Type для FormData - браузер сам добавит boundary
|
||||
} else {
|
||||
// Если файла нет - используем JSON
|
||||
console.log('[EmployeeForm] Using JSON (no avatar)');
|
||||
headers['Content-Type'] = 'application/json';
|
||||
body = JSON.stringify(payload);
|
||||
}
|
||||
|
||||
// Если нечего отправлять — просто обновим UI
|
||||
if (Object.keys(payload).length === 0 && !avatarFile) {
|
||||
console.log('[EmployeeForm] Nothing to save, reloading...');
|
||||
setSubmitting(false);
|
||||
if (window.bootstrap) {
|
||||
try {
|
||||
new bootstrap.Collapse(document.getElementById('editFormWrap'), { toggle: false }).hide();
|
||||
} catch (_) {}
|
||||
}
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[EmployeeForm] Sending PATCH request to:', endpoint);
|
||||
console.log('[EmployeeForm] Headers:', headers);
|
||||
console.log('[EmployeeForm] Body type:', body instanceof FormData ? 'FormData' : typeof body);
|
||||
|
||||
const resp = await fetch(endpoint, {
|
||||
method: 'PATCH',
|
||||
headers: headers,
|
||||
body: body,
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
console.log('[EmployeeForm] Response status:', resp.status, resp.statusText);
|
||||
|
||||
if (!resp.ok) {
|
||||
let body = null;
|
||||
try {
|
||||
body = await resp.json();
|
||||
} catch {}
|
||||
if (body && typeof body === 'object') {
|
||||
let shown = false;
|
||||
for (const [k, v] of Object.entries(body)) {
|
||||
if (k === 'detail') continue;
|
||||
const msg = Array.isArray(v) ? v.join(' ') : String(v);
|
||||
fieldError(k, msg);
|
||||
shown = true;
|
||||
}
|
||||
if (body.detail) alert(body.detail);
|
||||
else if (!shown) alert('Не удалось сохранить изменения (ошибка валидации).');
|
||||
} else {
|
||||
const t = await resp.text();
|
||||
alert('Не удалось сохранить изменения.\n' + (t || `HTTP ${resp.status}`));
|
||||
}
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Успех
|
||||
try {
|
||||
if (window.bootstrap)
|
||||
new bootstrap.Collapse(document.getElementById('editFormWrap'), { toggle: false }).hide();
|
||||
} catch (_) {}
|
||||
location.reload();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Не удалось сохранить изменения. Попробуйте ещё раз.');
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
form.addEventListener('submit', handleSubmit);
|
||||
|
||||
// API возвращаем для внешнего использования
|
||||
const noop = () => {};
|
||||
return {
|
||||
clearErrors,
|
||||
setSubmitting,
|
||||
fieldError,
|
||||
setSubmitting: noop,
|
||||
refresh: () => location.reload()
|
||||
};
|
||||
}
|
||||
|
||||
@ -3,8 +3,13 @@
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/components/modal-overrides.css' %}">
|
||||
|
||||
<form id="apiEditForm" class="row g-3" data-endpoint="{{ endpoint }}"
|
||||
action="{% if is_me %}{% url 'employees:employee_set_position_me' %}{% else %}{% url 'employees:employee_set_position' emp.id %}{% endif %}">
|
||||
<form
|
||||
id="apiEditForm"
|
||||
class="row g-3"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
data-endpoint="{{ endpoint }}"
|
||||
action="{{ endpoint }}">
|
||||
{% csrf_token %}
|
||||
|
||||
{# === ЛИЧНЫЕ ДАННЫЕ === #}
|
||||
|
||||
@ -14,15 +14,25 @@
|
||||
<label class="form-label">Фотография</label>
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<div class="position-relative">
|
||||
{% if emp.avatar %}
|
||||
<img id="avatarPreview" src="{{ emp.avatar.url }}" alt="Фото"
|
||||
class="rounded" style="width: 120px; height: 120px; object-fit: cover; border: 2px solid #dee2e6;">
|
||||
{% else %}
|
||||
<div id="avatarPreview" class="rounded d-flex align-items-center justify-content-center bg-light text-muted"
|
||||
style="width: 120px; height: 120px; border: 2px solid #dee2e6;">
|
||||
<i class="bi-person fs-1"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% with emp.avatar.url|default:emp.avatar as avatar_url %}
|
||||
{% if avatar_url %}
|
||||
<img
|
||||
id="avatarPreview"
|
||||
src="{{ avatar_url }}"
|
||||
alt="Фото"
|
||||
class="rounded"
|
||||
style="width: 120px; height: 120px; object-fit: cover; border: 2px solid #dee2e6;"
|
||||
>
|
||||
{% else %}
|
||||
<div
|
||||
id="avatarPreview"
|
||||
class="rounded d-flex align-items-center justify-content-center bg-light text-muted"
|
||||
style="width: 120px; height: 120px; border: 2px solid #dee2e6;"
|
||||
>
|
||||
<i class="bi-person fs-1"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<input type="file" class="form-control mb-2" id="avatarInput" name="avatar" accept="image/*">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user