dana-medical-site/migrations/import/export_content.py
2026-10-14 13:33:13 +00:00

225 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""Перенос контента: филиалы, FAQ, отзывы, документы, партнёры, посты, вакансии, страницы."""
import sqlread, io, os, re, html, zipfile
P = 'db.sql'
ZIP = r'C:\Users\naym4\Downloads\public_html.zip'
MEDIA_DIR = r'D:\dana\media'
OUT = io.open('seed_content.sql', 'w', encoding='utf-8')
MIME = {'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
'.gif': 'image/gif', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.pdf': 'application/pdf'}
def esc(s):
if s is None:
return "NULL"
# В PostgreSQL со standard_conforming_strings обратный слэш — обычный символ,
# удваивать его нельзя: имена приезжали в базу с лишним слэшем.
return "'" + str(s).replace("'", "''") + "'"
def clean(s):
# Парсер дампа отдаёт SQL NULL строкой 'NULL'. Без этой проверки clean()
# возвращал непустую строку "NULL", из-за чего конструкции вида
# `clean(DETAIL_TEXT) or clean(PREVIEW_TEXT)` не доходили до запасного
# поля — так у отзывов потерялся текст (он лежит в PREVIEW_TEXT).
if not s or s == 'NULL':
return ''
s = re.sub(r'(?is)<[^>]+>', '', s)
return html.unescape(s).strip()
files = {r['ID']: r for r in sqlread.rows(P, 'b_file')}
zf = zipfile.ZipFile(ZIP); names = set(zf.namelist()); extracted = {}
def ensure(fid):
if not fid or fid in ('', 'NULL'):
return None
if fid in extracted:
return extracted[fid]
f = files.get(fid)
if not f:
return None
sub = (f.get('SUBDIR') or '').strip(); nm = (f.get('FILE_NAME') or '').strip()
arc = 'public_html/upload/%s/%s' % (sub, nm)
if not sub or not nm or arc not in names:
return None
dd = os.path.join(MEDIA_DIR, sub.replace('/', os.sep)); os.makedirs(dd, exist_ok=True)
with zf.open(arc) as s, open(os.path.join(dd, nm), 'wb') as d:
d.write(s.read())
web = '/media/%s/%s' % (sub, nm); extracted[fid] = web
return web
def media_sql(fid, web, alt=''):
f = files[fid]; ext = os.path.splitext(web)[1].lower()
OUT.write("INSERT INTO media (path,mime,size,width,height,alt) VALUES (%s,%s,%s,%s,%s,%s) "
"ON CONFLICT (path) DO NOTHING;\n" % (
esc(web), esc(MIME.get(ext, 'application/octet-stream')),
f.get('FILE_SIZE') or '0', f.get('WIDTH') or '0', f.get('HEIGHT') or '0', esc(alt)))
# элементы и их свойства (нужные инфоблоки)
NEED = {'23', '17', '13', '25', '14', '28', '27', '26', '15', '19', '21', '18'}
elems = [r for r in sqlread.rows(P, 'b_iblock_element') if r['IBLOCK_ID'] in NEED]
props = {}
for r in sqlread.rows(P, 'b_iblock_element_property'):
props.setdefault((r.get('IBLOCK_ELEMENT_ID'), r.get('IBLOCK_PROPERTY_ID')), []).append(r.get('VALUE'))
def p1(eid, pid):
v = props.get((eid, pid))
return v[0] if v else None
def pN(eid, pid):
return [x for x in props.get((eid, pid), []) if x not in (None, '', 'NULL')]
def blocks_of(e):
d = e.get('DETAIL_TEXT') or ''
if not d.strip():
return "'[]'::jsonb"
return "jsonb_build_array(jsonb_build_object('type','html','html'," + esc(d) + "))"
OUT.write('BEGIN;\n\n')
# ---------- ФИЛИАЛЫ (iblock 19, элемент с объединённым адресом) ----------
OUT.write('-- Филиалы\n')
e = next((x for x in elems if x['IBLOCK_ID'] == '19' and x.get('ACTIVE') == 'Y'), None)
if e:
addr = clean(p1(e['ID'], '140'))
phone = clean(p1(e['ID'], '142'))
sched_raw = p1(e['ID'], '144') or ''
m = re.search(r'"TEXT";s:\d+:"(.*?)";', sched_raw)
hours = clean(m.group(1)) if m else 'ПН – ВС: 08:00 – 20:00'
coord = (p1(e['ID'], '146') or '').split(',')
lat = coord[0].strip() if len(coord) == 2 else None
lon = coord[1].strip() if len(coord) == 2 else None
parts = [a.strip() for a in re.split(r'[;]', addr) if a.strip()] or [addr]
for i, a in enumerate(parts):
slug = 'lyahova-%s' % (re.search(r'(\d+)', a).group(1) if re.search(r'(\d+)', a) else i + 1)
la = lat if i == 0 else 'NULL'
lo = lon if i == 0 else 'NULL'
OUT.write("INSERT INTO branches (slug,name,address,phones,work_hours,lat,lon,sort) "
"VALUES (%s,%s,%s,jsonb_build_array(%s),jsonb_build_array(%s),%s,%s,%s) "
"ON CONFLICT (slug) DO NOTHING;\n" % (
esc(slug), esc('г. Махачкала, ' + a), esc('г. Махачкала, ' + a),
esc(phone), esc(hours), la or 'NULL', lo or 'NULL', i + 1))
OUT.write('\n')
# ---------- FAQ (iblock 15) ----------
OUT.write('-- FAQ\n')
for e in elems:
if e['IBLOCK_ID'] != '15' or e.get('ACTIVE') != 'Y':
continue
q = clean(e.get('NAME')); a = clean(e.get('PREVIEW_TEXT')) or clean(e.get('DETAIL_TEXT'))
if not q:
continue
OUT.write("INSERT INTO faq (question,answer,sort,published,legacy_id) VALUES (%s,%s,%s,true,%s);\n" % (
esc(q), esc(a), e.get('SORT') or '100', e['ID']))
OUT.write('\n')
# ---------- ОТЗЫВЫ (iblock 23) ----------
OUT.write('-- Отзывы (ACTIVE=Y -> published, иначе pending)\n')
RATING = {'143': 1, '144': 2, '145': 3, '146': 4, '147': 5}
for e in elems:
if e['IBLOCK_ID'] != '23':
continue
name = clean(e.get('NAME')) or 'Аноним'
text = clean(e.get('DETAIL_TEXT')) or clean(e.get('PREVIEW_TEXT'))
if not text:
continue
rating = RATING.get(p1(e['ID'], '172'))
status = 'published' if e.get('ACTIVE') == 'Y' else 'pending'
photo = ensure(e.get('PREVIEW_PICTURE') or e.get('DETAIL_PICTURE'))
photo_sql = 'NULL'
if photo:
media_sql(e.get('PREVIEW_PICTURE') or e.get('DETAIL_PICTURE'), photo, name)
photo_sql = "(SELECT id FROM media WHERE path=%s)" % esc(photo)
doc = pN(e['ID'], '173') or pN(e['ID'], '170')
doc_sql = "(SELECT id FROM doctors WHERE legacy_id=%s)" % doc[0] if doc else 'NULL'
OUT.write("INSERT INTO reviews (author_name,author_photo_id,rating,text,doctor_id,source,status,legacy_id) "
"VALUES (%s,%s,%s,%s,%s,'manual',%s,%s);\n" % (
esc(name), photo_sql, rating if rating else 'NULL', esc(text), doc_sql, esc(status), e['ID']))
OUT.write('\n')
# ---------- ДОКУМЕНТЫ: лицензии(17)+надзор(13) ----------
OUT.write('-- Документы (лицензии, надзорные органы)\n')
for e in elems:
if e['IBLOCK_ID'] not in ('17', '13'):
continue
kind = 'license' if e['IBLOCK_ID'] == '17' else 'supervisor'
title = clean(e.get('NAME'))
body = clean(e.get('DETAIL_TEXT')) or clean(e.get('PREVIEW_TEXT'))
scans = pN(e['ID'], '136') if e['IBLOCK_ID'] == '17' else pN(e['ID'], '117')
prev = ensure(e.get('DETAIL_PICTURE') or e.get('PREVIEW_PICTURE') or (scans[0] if scans else None))
prev_sql = 'NULL'
if prev:
fid = e.get('DETAIL_PICTURE') or e.get('PREVIEW_PICTURE') or scans[0]
media_sql(fid, prev, title)
prev_sql = "(SELECT id FROM media WHERE path=%s)" % esc(prev)
OUT.write("INSERT INTO documents (kind,title,body,preview_id,sort,published,legacy_id) "
"VALUES (%s,%s,%s,%s,%s,true,%s);\n" % (
esc(kind), esc(title), esc(body), prev_sql, e.get('SORT') or '100', e['ID']))
OUT.write('\n')
# ---------- ПАРТНЁРЫ (iblock 25) ----------
OUT.write('-- Партнёры\n')
for e in elems:
if e['IBLOCK_ID'] != '25' or e.get('ACTIVE') != 'Y':
continue
logo = ensure(e.get('PREVIEW_PICTURE') or e.get('DETAIL_PICTURE') or (pN(e['ID'], '726')[:1] or [None])[0])
logo_sql = 'NULL'
if logo:
fid = e.get('PREVIEW_PICTURE') or e.get('DETAIL_PICTURE') or pN(e['ID'], '726')[0]
media_sql(fid, logo, clean(e.get('NAME')))
logo_sql = "(SELECT id FROM media WHERE path=%s)" % esc(logo)
OUT.write("INSERT INTO partners (name,url,logo_id,sort,published,legacy_id) VALUES (%s,%s,%s,%s,true,%s);\n" % (
esc(clean(e.get('NAME'))), esc(clean(p1(e['ID'], '210'))), logo_sql, e.get('SORT') or '100', e['ID']))
OUT.write('\n')
# ---------- ПОСТЫ: новости(27)+статьи(26)+акции(28) ----------
OUT.write('-- Новости, статьи, акции\n')
for e in elems:
if e['IBLOCK_ID'] == '27':
kind = 'news'
elif e['IBLOCK_ID'] == '26':
kind = 'article'
elif e['IBLOCK_ID'] == '28':
kind = 'promo'
else:
continue
if e.get('ACTIVE') != 'Y':
continue
slug = e.get('CODE') or (kind + '-' + e['ID'])
img = ensure(e.get('PREVIEW_PICTURE') or e.get('DETAIL_PICTURE'))
img_sql = 'NULL'
if img:
media_sql(e.get('PREVIEW_PICTURE') or e.get('DETAIL_PICTURE'), img, clean(e.get('NAME')))
img_sql = "(SELECT id FROM media WHERE path=%s)" % esc(img)
af = e.get('ACTIVE_FROM')
starts = esc(af[:10]) if kind == 'promo' and af and af != 'NULL' else 'NULL'
OUT.write("INSERT INTO posts (kind,slug,title,lead,blocks,image_id,starts_at,published,legacy_id) "
"VALUES (%s,%s,%s,%s,%s,%s,%s,true,%s) ON CONFLICT (kind,slug) WHERE deleted_at IS NULL DO NOTHING;\n" % (
esc(kind), esc(slug), esc(clean(e.get('NAME'))), esc(clean(e.get('PREVIEW_TEXT'))),
blocks_of(e), img_sql, starts, e['ID']))
OUT.write('\n')
# ---------- ВАКАНСИИ (iblock 14) ----------
OUT.write('-- Вакансии\n')
for e in elems:
if e['IBLOCK_ID'] != '14' or e.get('ACTIVE') != 'Y':
continue
slug = e.get('CODE') or ('vac-' + e['ID'])
OUT.write("INSERT INTO vacancies (slug,title,description,salary,sort,published,legacy_id) "
"VALUES (%s,%s,%s,%s,%s,true,%s) ON CONFLICT (slug) DO NOTHING;\n" % (
esc(slug), esc(clean(e.get('NAME'))), esc(clean(e.get('DETAIL_TEXT')) or clean(e.get('PREVIEW_TEXT'))),
esc(clean(p1(e['ID'], '122'))), e.get('SORT') or '100', e['ID']))
OUT.write('\n')
# ---------- СТРАНИЦЫ: о клинике(21)+история(18) ----------
OUT.write('-- Страницы (о клинике, история)\n')
page_map = {'21': ('about', 'О клинике'), '18': ('history', 'История')}
for e in elems:
if e['IBLOCK_ID'] not in page_map:
continue
slug, title = page_map[e['IBLOCK_ID']]
OUT.write("INSERT INTO pages (slug,title,blocks) VALUES (%s,%s,%s) ON CONFLICT (slug) DO NOTHING;\n" % (
esc(slug), esc(title), blocks_of(e)))
OUT.write('\nCOMMIT;\n')
OUT.close()
print('extracted media:', len(extracted))