Merge pull request 'production' (#12) from production into main

This commit is contained in:
vladislav 2026-09-10 18:46:44 +00:00
commit cad8cb4998
22 changed files with 274 additions and 164 deletions

View File

@ -3842,6 +3842,8 @@
" update_doc[\"status\"] = -1\n", " update_doc[\"status\"] = -1\n",
" if payload.date_finish is not None:\n", " if payload.date_finish is not None:\n",
" update_doc[\"date_finish\"] = payload.date_finish\n", " update_doc[\"date_finish\"] = payload.date_finish\n",
" if datetime.now(timezone.utc) + timedelta(hours=3) > payload.datetime_start and datetime.now(timezone.utc) + timedelta(hours=3) < payload.date_finish:\n",
" update_doc[\"status\"] = 0\n",
" if payload.address is not None:\n", " if payload.address is not None:\n",
" await users_collection.update_one(\n", " await users_collection.update_one(\n",
" { \"_id\": ObjectId(current_user[\"_id\"]) },\n", " { \"_id\": ObjectId(current_user[\"_id\"]) },\n",
@ -4155,6 +4157,54 @@
], ],
"display_name": ".venv" "display_name": ".venv"
}, },
{
"cell_type": "code",
"execution_count": null,
"id": "30acea7c",
"metadata": {},
"outputs": [],
"source": [
"@app.get(\"/b/{bucket_name}/{file_id}/{new_w}/{quality}/{GaussianBlur_radius}\")\n",
"async def get_file_with_config(\n",
" bucket_name: str,\n",
" file_id: str,\n",
" new_w: int,\n",
" quality: int,\n",
" GaussianBlur_radius: int,\n",
"):\n",
" res = await stream_gridfs_file(file_id, bucket_name)\n",
" if res is None:\n",
" raise HTTPException(status_code=404, detail=\"File not found\")\n",
"\n",
" filename, content_type, file_length, iterator = res\n",
"\n",
" # Собираем все байты файла\n",
" data = b\"\".join([chunk async for chunk in iterator()])\n",
"\n",
" # Обрабатываем\n",
" processed = await build_composite(\n",
" data,\n",
" new_w=new_w,\n",
" quality=quality,\n",
" GaussianBlur_radius=GaussianBlur_radius,\n",
" )\n",
"\n",
" headers = {\n",
" \"Content-Disposition\": content_disposition_header(filename, inline=True),\n",
" \"Cache-Control\": \"public, max-age=3600\",\n",
" \"ETag\": f'\"{bucket_name}:{file_id}:{new_w}:{quality}:{GaussianBlur_radius}\"',\n",
" \"Vary\": \"Accept-Encoding\",\n",
" \"Content-Length\": str(len(processed.getvalue())),\n",
" }\n",
"\n",
" return StreamingResponse(\n",
" processed,\n",
" media_type=\"image/webp\",\n",
" headers=headers,\n",
" )"
],
"display_name": ".venv"
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
@ -4258,7 +4308,7 @@
" users = [doc async for doc in users_collection.find().sort({'_id':-1})]\n", " users = [doc async for doc in users_collection.find().sort({'_id':-1})]\n",
" return templates.TemplateResponse(\n", " return templates.TemplateResponse(\n",
" request=request, name=\"profile/admin/users.html\", context={\"user\": user,\n", " request=request, name=\"profile/admin/users.html\", context={\"user\": user,\n",
" 'users': users \n", " 'users': users\n",
" }\n", " }\n",
" )" " )"
], ],

43
main.py
View File

@ -2918,6 +2918,8 @@ async def put_competition(
update_doc["status"] = -1 update_doc["status"] = -1
if payload.date_finish is not None: if payload.date_finish is not None:
update_doc["date_finish"] = payload.date_finish update_doc["date_finish"] = payload.date_finish
if datetime.now(timezone.utc) + timedelta(hours=3) > payload.datetime_start and datetime.now(timezone.utc) + timedelta(hours=3) < payload.date_finish:
update_doc["status"] = 0
if payload.address is not None: if payload.address is not None:
await users_collection.update_one( await users_collection.update_one(
{ "_id": ObjectId(current_user["_id"]) }, { "_id": ObjectId(current_user["_id"]) },
@ -3195,6 +3197,45 @@ async def get_avatar(user_id: str):
return StreamingResponse(iterator(), media_type=content_type, headers=headers) return StreamingResponse(iterator(), media_type=content_type, headers=headers)
@app.get("/b/{bucket_name}/{file_id}/{new_w}/{quality}/{GaussianBlur_radius}")
async def get_file_with_config(
bucket_name: str,
file_id: str,
new_w: int,
quality: int,
GaussianBlur_radius: int,
):
res = await stream_gridfs_file(file_id, bucket_name)
if res is None:
raise HTTPException(status_code=404, detail="File not found")
filename, content_type, file_length, iterator = res
# Собираем все байты файла
data = b"".join([chunk async for chunk in iterator()])
# Обрабатываем
processed = await build_composite(
data,
new_w=new_w,
quality=quality,
GaussianBlur_radius=GaussianBlur_radius,
)
headers = {
"Content-Disposition": content_disposition_header(filename, inline=True),
"Cache-Control": "public, max-age=3600",
"ETag": f'"{bucket_name}:{file_id}:{new_w}:{quality}:{GaussianBlur_radius}"',
"Vary": "Accept-Encoding",
"Content-Length": str(len(processed.getvalue())),
}
return StreamingResponse(
processed,
media_type="image/webp",
headers=headers,
)
async def unsubscribe_user( async def unsubscribe_user(
email: str, email: str,
token: Optional[str] = None token: Optional[str] = None
@ -3275,7 +3316,7 @@ async def profile_admin_statistics(
users = [doc async for doc in users_collection.find().sort({'_id':-1})] users = [doc async for doc in users_collection.find().sort({'_id':-1})]
return templates.TemplateResponse( return templates.TemplateResponse(
request=request, name="profile/admin/users.html", context={"user": user, request=request, name="profile/admin/users.html", context={"user": user,
'users': users 'users': users
} }
) )

View File

@ -214,7 +214,8 @@
</div> </div>
</form> </form>
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
/* ---------- Utility helpers (removed) ---------- */ /* ---------- Utility helpers (removed) ---------- */
/* Toggle visibility of a block (flex / none) */ /* Toggle visibility of a block (flex / none) */
@ -359,7 +360,8 @@ document.getElementById('address').value = '{{ user.send_address[-1] }}'
// @license-end // @license-end
</script> </script>
{% endif %} {% endif %}
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
function setTomorrowAtNoon() { function setTomorrowAtNoon() {
const input = document.getElementById('datetime_start'); const input = document.getElementById('datetime_start');
const date_finish = document.getElementById('date_finish'); const date_finish = document.getElementById('date_finish');

View File

@ -107,7 +107,7 @@
<div class="b_1tx4m"> <div class="b_1tx4m">
<picture> <picture>
<img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ post.title }}" class="b_1tx4m" <img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ post.title }}" class="b_1tx4m"
src="/b/posts/{{ post.medias[0] }}" data-src="/b/posts/{{ post.medias_original[0] }}"> src="/b/posts/{{ post.medias_original[0] }}/500/90/80" data-src="/b/posts/{{ post.medias_original[0] }}">
</picture> </picture>
</div> </div>
<figcaption class="c_fzh03 b_bp0ws"> <figcaption class="c_fzh03 b_bp0ws">

View File

@ -54,7 +54,8 @@
</a> </a>
</div> </div>
</div> </div>
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
async function logout() { async function logout() {
try { try {
const resp = await fetch('/logout', { const resp = await fetch('/logout', {
@ -73,7 +74,8 @@
} }
// @license-end // @license-end
</script> </script>
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
async function switchProfile() { async function switchProfile() {
try { try {
const resp = await fetch('/api/switch-profile', { const resp = await fetch('/api/switch-profile', {
@ -89,7 +91,8 @@
} }
// @license-end // @license-end
</script> </script>
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const avatarInput = document.getElementById('avatar'); const avatarInput = document.getElementById('avatar');
const preview = document.getElementById('preview'); const preview = document.getElementById('preview');

View File

@ -215,7 +215,8 @@ document.getElementById('place').value = '{{ user.places[-1] }}'
// @license-end // @license-end
</script> </script>
{% endif %} {% endif %}
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
/* ---------- Utility helpers (removed) ---------- */ /* ---------- Utility helpers (removed) ---------- */
/* Toggle visibility of a block (flex / none) */ /* Toggle visibility of a block (flex / none) */
@ -370,7 +371,8 @@ function bindLinkValidation({
// @license-end // @license-end
</script> </script>
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
function setTomorrowAtNoon() { function setTomorrowAtNoon() {
const input = document.getElementById('datetime_start'); const input = document.getElementById('datetime_start');
const date_finish = document.getElementById('date_finish'); const date_finish = document.getElementById('date_finish');
@ -391,7 +393,8 @@ function bindLinkValidation({
setTomorrowAtNoon(); setTomorrowAtNoon();
// @license-end // @license-end
</script> </script>
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
const titleInput = document.getElementById('title'); const titleInput = document.getElementById('title');
const topicInput = document.getElementById('topic'); const topicInput = document.getElementById('topic');

View File

@ -166,7 +166,8 @@
</article> </article>
</div> </div>
</form> </form>
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
function toggleBlock(id, show) { function toggleBlock(id, show) {
document.querySelector(id).style.display = show ? 'flex' : 'none'; document.querySelector(id).style.display = show ? 'flex' : 'none';
document.querySelector(id).querySelector('input').required = show ? true : false; document.querySelector(id).querySelector('input').required = show ? true : false;
@ -295,7 +296,8 @@ bindLinkValidation({
}); });
// @license-end // @license-end
</script> </script>
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
function setTomorrowAtNoon() { function setTomorrowAtNoon() {
const input = document.getElementById('datetime_start'); const input = document.getElementById('datetime_start');
const date_finish = document.getElementById('date_finish'); const date_finish = document.getElementById('date_finish');
@ -316,7 +318,8 @@ bindLinkValidation({
setTomorrowAtNoon(); setTomorrowAtNoon();
// @license-end // @license-end
</script> </script>
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
const exhibition = JSON.parse({{exhibition|tojson}}) const exhibition = JSON.parse({{exhibition|tojson}})
const exhibitionId = exhibition._id const exhibitionId = exhibition._id

View File

@ -27,7 +27,8 @@
{{ articles.section(news) }} {{ articles.section(news) }}
{{ footer.section(user) }} {{ footer.section(user) }}
{% if pwa %} {% if pwa %}
<script async> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
fetch('/api/me') fetch('/api/me')
.then(response => { .then(response => {
if (response.ok) { if (response.ok) {

View File

@ -6,7 +6,7 @@
<a href="/paintings/{{post._id}}" class="b_kbsvf"> <a href="/paintings/{{post._id}}" class="b_kbsvf">
<picture> <picture>
<img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ post.title }}" class="b_ya0t5" <img crossorigin="anonymous" fetchpriority="high" loading="eager" alt="{{ post.title }}" class="b_ya0t5"
src="/b/posts/{{ post.medias[0] }}" data-src="/b/posts/{{ post.medias_original[0] }}"> src="/b/posts/{{ post.medias_original[0] }}/500/90/80" data-src="/b/posts/{{ post.medias_original[0] }}">
</picture> </picture>
</a> </a>
</div> </div>
@ -84,7 +84,7 @@ picture>img{ width: 100%; object-fit: scale-down; }
.b_rhbo8 { color: #5F1798; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; } .b_rhbo8 { color: #5F1798; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
.b_tbfd3 { color: rgba(58, 58, 58, 0.75); font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; text-align: right; } .b_tbfd3 { color: rgba(58, 58, 58, 0.75); font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; text-align: right; }
.b_gagrr { align-items: center; align-self: stretch; display: flex; justify-content: space-between; } .b_gagrr { align-items: center; align-self: stretch; display: flex; justify-content: space-between; }
.b_kbsvf { align-items: flex-start; align-self: stretch; display: flex; flex-direction: column; gap: 10px; justify-content: center; } .b_kbsvf { align-items: center; align-self: stretch; display: flex; flex-direction: column; gap: 10px; justify-content: center; }
.b_h57rq { align-items: flex-start; background: #FDFAFF; border-radius: 5px; border: 1px solid #5F1798; box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25), 0 4px 20px 0 rgba(0, 0, 0, 0.20); display: flex; flex-direction: column; width: 335px; } .b_h57rq { align-items: flex-start; background: #FDFAFF; border-radius: 5px; border: 1px solid #5F1798; box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25), 0 4px 20px 0 rgba(0, 0, 0, 0.20); display: flex; flex-direction: column; width: 335px; }
.c_jy9j2{padding: 10px 20px; height: 226px;} .c_jy9j2{padding: 10px 20px; height: 226px;}
/* Mobile */ /* Mobile */

View File

@ -7,7 +7,6 @@
{% block title %}Пользователи{% endblock %} {% block title %}Пользователи{% endblock %}
{% block content %} {% block content %}
{{ header.section(user) }} {{ header.section(user) }}
<section class="c_dg3k4"> <section class="c_dg3k4">
@ -16,22 +15,7 @@
<div class="c_ru432"> <div class="c_ru432">
{{ profile_nav.profile_nav(user, 'users') }} {{ profile_nav.profile_nav(user, 'users') }}
<div class="c_tihtk" id="main"> <div class="c_tihtk" id="main">
<style>
input { padding: 8px; width: 300px; margin-bottom: 10px; border-bottom: 1px solid #5f1798;position: sticky;
left: 10px;}
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
th { background-color: #f4f4f4; }
tr.hidden { display: none; }
.highlight { background-color: #ffeb3b; padding: 2px 4px; border-radius: 3px; }
</style>
<input type="text" id="searchInput" placeholder="Поиск ..." onkeyup="searchTable()"> <input type="text" id="searchInput" placeholder="Поиск ..." onkeyup="searchTable()">
<table id="dataTable"> <table id="dataTable">
<thead> <thead>
<tr> <tr>
@ -85,7 +69,7 @@ async function deleteUser(params) {
} }
// @license-end // @license-end
</script> </script>
<script> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause // @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
function normalizeText(str) { function normalizeText(str) {
return str return str
@ -134,15 +118,10 @@ function normalizeText(str) {
} }
// @license-end // @license-end
</script> </script>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
{{ footer.section(user) }} {{ footer.section(user) }}
{% endblock %} {% endblock %}
@ -150,6 +129,42 @@ function normalizeText(str) {
{{ header.style() }} {{ header.style() }}
{{ profile_nav.style() }} {{ profile_nav.style() }}
{{ footer.style() }} {{ footer.style() }}
<style>
input {
padding: 8px;
width: 300px;
margin-bottom: 10px;
border-bottom: 1px solid #5f1798;
position: sticky;
left: 10px;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
border: 1px solid #ccc;
padding: 8px;
text-align: left;
}
th {
background-color: #f4f4f4;
}
tr.hidden {
display: none;
}
.highlight {
background-color: #ffeb3b;
padding: 2px 4px;
border-radius: 3px;
}
</style>
<style> <style>
@media (max-width: 699px) { @media (max-width: 699px) {
.c_tihtk { .c_tihtk {

View File

@ -139,7 +139,8 @@ document.querySelectorAll('.post').forEach(postEl => {
// @license-end // @license-end
</script> </script>
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
const min_paintings = parseInt('{{competition.min_paintings}}') const min_paintings = parseInt('{{competition.min_paintings}}')
const max_paintings = parseInt('{{competition.max_paintings}}') const max_paintings = parseInt('{{competition.max_paintings}}')
const competition_id = '{{competition._id}}' const competition_id = '{{competition._id}}'
@ -373,7 +374,8 @@ function loadAndClick() {
</div> </div>
</div> </div>
</section> </section>
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
loadAndClick(); loadAndClick();
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const avatarInput = document.getElementById('avatar'); const avatarInput = document.getElementById('avatar');

View File

@ -228,7 +228,7 @@ picture > img {
} }
.b_kbsvf { .b_kbsvf {
padding: 10px 20px; padding: 10px 20px;
align-items: flex-start; align-items: center;
align-self: stretch; align-self: stretch;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -150,7 +150,7 @@ picture > img {
} }
.b_kbsvf { .b_kbsvf {
padding: 10px 20px; padding: 10px 20px;
align-items: flex-start; align-items: center;
align-self: stretch; align-self: stretch;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -539,7 +539,8 @@
}); });
// @license-end // @license-end
</script> </script>
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
function toggleTrackField() { function toggleTrackField() {
const deliveryType = document.getElementById('delivery_type'); const deliveryType = document.getElementById('delivery_type');
const trackGroup = document.getElementById('track-group'); const trackGroup = document.getElementById('track-group');
@ -643,7 +644,7 @@ picture > img {
} }
.b_kbsvf { .b_kbsvf {
padding: 10px 20px; padding: 10px 20px;
align-items: flex-start; align-items: center;
align-self: stretch; align-self: stretch;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -7,39 +7,37 @@
{% block title %}Анкета{% endblock %} {% block title %}Анкета{% endblock %}
{% block content %} {% block content %}
{{ header.section(user) }} {{ header.section(user) }}
<section class="c_dg3k4"> <section class="c_dg3k4">
<div class="c_gtoe9"> <div class="c_gtoe9">
{{ profile_card.profile_card(user) }} {{ profile_card.profile_card(user) }}
<div class="c_ru432"> <div class="c_ru432">
{{ profile_nav.profile_nav(user, 'form') }} {{ profile_nav.profile_nav(user, 'form') }}
<div class="c_tihtk" id="main"> <div class="c_tihtk" id="main">
<div class="c_aihm6 floating-group"> <div class="c_aihm6 floating-group">
<label for="female_name_lastname" class="c_z3ybu floating-label">ФИО<span style="color:red">*</span></label> <label for="female_name_lastname" class="c_z3ybu floating-label">ФИО<span style="color:red">*</span></label>
<input name="female_name_lastname" id="female_name_lastname" class="c_xhd5k" required /> <input name="female_name_lastname" id="female_name_lastname" class="c_xhd5k" required />
<span></span> <span></span>
</div> </div>
<label for="birthday" class="c_z3ybu floating-label">Дата рождения<span style="color:red">*</span></label>
<label for="birthday" class="c_z3ybu floating-label">Дата рождения<span style="color:red">*</span></label>
<div class="c_aihm6 floating-group birthday"> <div class="c_aihm6 floating-group birthday">
<select id="day"> <select id="day">
<option>1</option> <option>1</option>
<option>2</option> <option>2</option>
</select> </select>
<select id="month"> <select id="month">
<option>Январь</option> <option>Январь</option>
<option>Февраль</option> <option>Февраль</option>
</select> </select>
<select id="year"> <select id="year">
<option>2000</option> <option>2000</option>
<option>2001</option> <option>2001</option>
</select> </select>
<input type="date" id="birthday" name="birthday" class="c_xhd5k" required data-toast="Поле Дата рождения должно быть заполнено"> <input type="date" id="birthday" name="birthday" class="c_xhd5k" required data-toast="Поле Дата рождения должно быть заполнено">
</div> </div>
<div class="c_aihm6 floating-group"> <div class="c_aihm6 floating-group">
<label for="living_place" class="c_z3ybu floating-label">Место проживания<span style="color:red">*</span></label> <label for="living_place" class="c_z3ybu floating-label">Место проживания<span style="color:red">*</span></label>
@ -258,82 +256,77 @@ document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById(
</script> </script>
<script> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause // @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const createBtn = document.getElementById('createBtn'); const createBtn = document.getElementById('createBtn');
createBtn.addEventListener('click', async (e) => { createBtn.addEventListener('click', async (e) => {
e.preventDefault(); e.preventDefault();
payload = { payload = {
'female_name_lastname': document.getElementById('female_name_lastname').value, 'female_name_lastname': document.getElementById('female_name_lastname').value,
'birthday': document.getElementById('birthday').value, 'birthday': document.getElementById('birthday').value,
'living_place': document.getElementById('living_place').value, 'living_place': document.getElementById('living_place').value,
'social_media': document.getElementById('social_media').value, 'social_media': document.getElementById('social_media').value,
'marketplaces': document.getElementById('marketplaces').value, 'marketplaces': document.getElementById('marketplaces').value,
'email': document.getElementById('email').value, 'email': document.getElementById('email').value,
'phone': document.getElementById('phone').value, 'phone': document.getElementById('phone').value,
'messengers': document.getElementById('messengers').value, 'messengers': document.getElementById('messengers').value,
'specialization': document.getElementById('specialization').value, 'specialization': document.getElementById('specialization').value,
'grade': document.getElementById('grade').value, 'grade': document.getElementById('grade').value,
'style': document.getElementById('style').value, 'style': document.getElementById('style').value,
'art_types': document.getElementById('art_types').value, 'art_types': document.getElementById('art_types').value,
'genres': document.getElementById('genres').value, 'genres': document.getElementById('genres').value,
'education': document.getElementById('education').value, 'education': document.getElementById('education').value,
'unions': document.getElementById('unions').value, 'unions': document.getElementById('unions').value,
'rewards': document.getElementById('rewards').value, 'rewards': document.getElementById('rewards').value,
'projects': document.getElementById('projects').value, 'projects': document.getElementById('projects').value,
'publications': document.getElementById('publications').value, 'publications': document.getElementById('publications').value,
'site': document.getElementById('site').value, 'site': document.getElementById('site').value,
'material_manufacturer': document.getElementById('material_manufacturer').value, 'material_manufacturer': document.getElementById('material_manufacturer').value,
'material_use': document.getElementById('material_use').value, 'material_use': document.getElementById('material_use').value,
'buying_materials': document.getElementById('buying_materials').value, 'buying_materials': document.getElementById('buying_materials').value,
'collaborate': document.getElementById('collaborate').value, 'collaborate': document.getElementById('collaborate').value,
'meta_data.wishes': document.getElementById('meta_data.wishes').value, 'meta_data.wishes': document.getElementById('meta_data.wishes').value,
'master_classes': document.getElementById('master_classes').value, 'master_classes': document.getElementById('master_classes').value,
'materials_use_in_master_classes': document.getElementById('materials_use_in_master_classes').value, 'materials_use_in_master_classes': document.getElementById('materials_use_in_master_classes').value,
'print_merch': document.getElementById('print_merch').value, 'print_merch': document.getElementById('print_merch').value,
'own_workshop': document.getElementById('own_workshop').value, 'own_workshop': document.getElementById('own_workshop').value,
'workshop_tour': document.getElementById('workshop_tour').value, 'workshop_tour': document.getElementById('workshop_tour').value,
'biography': document.getElementById('biography').value} 'biography': document.getElementById('biography').value
}
try { try {
const res = await fetch(`/api/user/form`, { const res = await fetch(`/api/user/form`, {
method: 'PUT', method: 'PUT',
// headers:{ // headers:{
// } // }
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
if (res.ok) { if (res.ok) {
showToast({message: 'Анкета сохранена!', duration:2500}); showToast({ message: 'Анкета сохранена!', duration: 2500 });
{% if notify %} {% if notify %}
const targetHash = window.location.hash; const targetHash = window.location.hash;
if (targetHash) { if (targetHash) {
const el = document.querySelector(targetHash); const el = document.querySelector(targetHash);
if (el) { if (el) {
el.scrollIntoView({ behavior: 'smooth' }); el.scrollIntoView({ behavior: 'smooth' });
} }
} }
window.location.reload(); window.location.reload();
{% endif %} {% endif %}
} else { } else {
showToast({message: 'Ошибка при сохранение!', status: 'error', duration:2500}); showToast({ message: 'Ошибка при сохранение!', status: 'error', duration: 2500 });
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
showToast({message: 'Ошибка соединения с сервером!', status: 'error', duration:2500}); showToast({ message: 'Ошибка соединения с сервером!', status: 'error', duration: 2500 });
} }
}); });
}); });
// @license-end
// @license-end
</script> </script>
{{ footer.section(user) }} {{ footer.section(user) }}
{% endblock %} {% endblock %}

View File

@ -790,7 +790,7 @@ picture > img {
} }
.b_kbsvf { .b_kbsvf {
padding: 10px 20px; padding: 10px 20px;
align-items: flex-start; align-items: center;
align-self: stretch; align-self: stretch;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -173,7 +173,7 @@ picture > img {
} }
.b_kbsvf { .b_kbsvf {
padding: 10px 20px; padding: 10px 20px;
align-items: flex-start; align-items: center;
align-self: stretch; align-self: stretch;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -75,7 +75,7 @@ picture>img{ width: 100%; object-fit: scale-down; }
.b_rhbo8 { color: #5F1798; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; } .b_rhbo8 { color: #5F1798; font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; }
.b_tbfd3 { color: rgba(58, 58, 58, 0.75); font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; text-align: right; } .b_tbfd3 { color: rgba(58, 58, 58, 0.75); font-family: Inter; font-size: 14px; font-style: normal; font-weight: 400; line-height: normal; text-align: right; }
.b_gagrr { align-items: center; align-self: stretch; display: flex; justify-content: space-between; } .b_gagrr { align-items: center; align-self: stretch; display: flex; justify-content: space-between; }
.b_kbsvf { align-items: flex-start; align-self: stretch; display: flex; flex-direction: column; gap: 10px; justify-content: center; } .b_kbsvf { align-items: center; align-self: stretch; display: flex; flex-direction: column; gap: 10px; justify-content: center; }
.b_h57rq { align-items: flex-start; background: #FDFAFF; border-radius: 5px; border: 1px solid #5F1798; box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25), 0 4px 20px 0 rgba(0, 0, 0, 0.20); display: flex; flex-direction: column; width: 335px; } .b_h57rq { align-items: flex-start; background: #FDFAFF; border-radius: 5px; border: 1px solid #5F1798; box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25), 0 4px 20px 0 rgba(0, 0, 0, 0.20); display: flex; flex-direction: column; width: 335px; }
.c_jy9j2{padding: 10px 20px; height: 226px;} .c_jy9j2{padding: 10px 20px; height: 226px;}
/* Mobile */ /* Mobile */

View File

@ -206,7 +206,8 @@
</div> </div>
</div> </div>
</section> </section>
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
document.querySelectorAll('.c_6cezu.b_6spaf').forEach(toggle => { document.querySelectorAll('.c_6cezu.b_6spaf').forEach(toggle => {
toggle.addEventListener('click', () => { toggle.addEventListener('click', () => {
const parent = toggle.parentElement; // ваш parent const parent = toggle.parentElement; // ваш parent

View File

@ -208,7 +208,8 @@ failBtn.addEventListener('click', () => selectMember('fail'));
</div> </div>
</div> </div>
</section> </section>
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
document.querySelectorAll('.c_6cezu.b_6spaf').forEach(toggle => { document.querySelectorAll('.c_6cezu.b_6spaf').forEach(toggle => {
toggle.addEventListener('click', () => { toggle.addEventListener('click', () => {
const parent = toggle.parentElement; // ваш parent const parent = toggle.parentElement; // ваш parent

View File

@ -88,7 +88,8 @@
</div> </div>
</section> </section>
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
const galerists = [ const galerists = [
{% for p in galerists %} {% for p in galerists %}
{ {
@ -100,7 +101,8 @@
document.getElementById('filter-galerists').dataset.list=JSON.stringify(galerists) document.getElementById('filter-galerists').dataset.list=JSON.stringify(galerists)
// @license-end // @license-end
</script> </script>
<script defer> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
const living_places = [ const living_places = [
{% for p in living_places %} {% for p in living_places %}
{ {
@ -112,7 +114,8 @@ document.getElementById('filter-galerists').dataset.list=JSON.stringify(galerist
document.getElementById('filter-city').dataset.list=JSON.stringify(living_places) document.getElementById('filter-city').dataset.list=JSON.stringify(living_places)
// @license-end // @license-end
</script> </script>
<script async> <script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const genres_count = {{genres|tojson}}; const genres_count = {{genres|tojson}};
let genres = [ let genres = [

View File

@ -352,81 +352,72 @@ def is_safe_redirect(next_url: str) -> bool:
return False return False
async def build_composite(data, scale: float = 1.5, new_w: int = 500, quality: int = 90, GaussianBlur_radius: int = 80): async def build_composite(
src_img = Image.open(BytesIO(data)).convert('RGB') data: bytes,
scale: float = 1.5,
new_w: int = 500,
quality: int = 90,
GaussianBlur_radius: int = 80,
) -> BytesIO:
src_img = Image.open(BytesIO(data)).convert("RGB")
# -------------------------------------------------------------- # --------------------------------------------------------------
# 2️⃣ Параметры конечного результата # 2️⃣ Параметры конечного результата
# -------------------------------------------------------------- # --------------------------------------------------------------
new_h = int(new_w / scale) # высота для соотношения 3:2 (≈333) new_h = int(new_w / scale) # высота для соотношения 3:2 (≈333)
canvas_sz = (new_w, new_h) # размер «фонового» холста canvas_sz = (new_w, new_h)
# --------------------------------------------------------------
# 3️⃣ Фоновое изображение – cover + blur(3)
# --------------------------------------------------------------
# - «cover» = заполняем весь холст, обрезая лишнее,
# сохраняем пропорции оригинала.
# - Затем делаем размытие радиусом 3.
# --------------------------------------------------------------
# Сначала масштабируем так, чтобы одна из сторон полностью покрывала холст
src_ratio = src_img.width / src_img.height src_ratio = src_img.width / src_img.height
canvas_ratio = new_w / new_h canvas_ratio = new_w / new_h
if src_ratio > canvas_ratio: # изображение более «широкое» # --------------------------------------------------------------
# масштабируем по высоте, ширина будет больше холста → обрезаем по горизонтали # 3️⃣ Фоновое изображение – cover + blur (optional)
# --------------------------------------------------------------
if src_ratio > canvas_ratio:
scale_h = new_h scale_h = new_h
scale_w = int(scale_h * src_ratio) scale_w = int(scale_h * src_ratio)
else: # изображение более «высокое» или точное соотношение else:
scale_w = new_w scale_w = new_w
scale_h = int(scale_w / src_ratio) scale_h = int(scale_w / src_ratio)
# масштабируем
bg_resized = src_img.resize((scale_w, scale_h), Image.LANCZOS) bg_resized = src_img.resize((scale_w, scale_h), Image.LANCZOS)
# обрезаем центр, чтобы получить точно new_w × new_h
left = (scale_w - new_w) // 2 left = (scale_w - new_w) // 2
top = (scale_h - new_h) // 2 top = (scale_h - new_h) // 2
right = left + new_w right = left + new_w
bottom = top + new_h bottom = top + new_h
bg_cropped = bg_resized.crop((left, top, right, bottom)) bg_cropped = bg_resized.crop((left, top, right, bottom))
# размытие
bg_blurred = bg_cropped.filter(ImageFilter.GaussianBlur(radius=GaussianBlur_radius))
# -------------------------------------------------------------- # --------------------------------------------------------------
# 4️⃣ Переднее изображение – contain + центрировать # 4️⃣ Переднее изображение – contain + центрировать
# -------------------------------------------------------------- # --------------------------------------------------------------
# - «contain» = вписать изображение в границы canvas,
# полностью показывая его, оставляя пустые поля (прозрачные)
# --------------------------------------------------------------
# Находим масштаб, чтобы изображение полностью поместилось
if src_ratio > canvas_ratio: if src_ratio > canvas_ratio:
# ограничиваем ширину
fore_w = new_w fore_w = new_w
fore_h = int(fore_w / src_ratio) fore_h = int(fore_w / src_ratio)
else: else:
# ограничиваем высоту
fore_h = new_h fore_h = new_h
fore_w = int(fore_h * src_ratio) fore_w = int(fore_h * src_ratio)
fore_resized = src_img.resize((fore_w, fore_h), Image.LANCZOS) fore_resized = src_img.resize((fore_w, fore_h), Image.LANCZOS)
# Создаём прозрачный холст того же размера, что и фон # --------------------------------------------------------------
final_img = Image.new('RGB', canvas_sz, (0, 0, 0)) # 5️⃣ Сборка
# --------------------------------------------------------------
final_img = Image.new("RGB", canvas_sz, (255, 255, 255))
# Сначала помещаем размытый фон if GaussianBlur_radius:
final_img.paste(bg_blurred, (0, 0)) bg_blurred = bg_cropped.filter(ImageFilter.GaussianBlur(radius=GaussianBlur_radius))
final_img.paste(bg_blurred, (0, 0))
# else: оставляем чёрный фон (или можно вставить bg_cropped без blur)
# Затем — переднее изображение, центрированное
offset_x = (new_w - fore_w) // 2 offset_x = (new_w - fore_w) // 2
offset_y = (new_h - fore_h) // 2 offset_y = (new_h - fore_h) // 2
final_img.paste(fore_resized, (offset_x, offset_y)) final_img.paste(fore_resized, (offset_x, offset_y))
# -------------------------------------------------------------- # --------------------------------------------------------------
# 5️⃣ Возврат / сохранение # 6️⃣ Возврат
# -------------------------------------------------------------- # --------------------------------------------------------------
out_bytes = BytesIO() out_bytes = BytesIO()
# final_img.save(out_bytes, format='JPEG', quality=85)
final_img.save(out_bytes, format="WEBP", quality=quality, method=6) final_img.save(out_bytes, format="WEBP", quality=quality, method=6)
out_bytes.seek(0) out_bytes.seek(0)
return out_bytes # JPEG‑байты готового композитного изображения return out_bytes