diff --git a/.gitignore b/.gitignore index fe88a36..aefcdef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ __pycache__/ *.pyc .venv/ +.env CONTEXT.md CONTEXT.local.md backups/ diff --git a/docker-compose.yml b/docker-compose.yml index 7d664c0..2428810 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,8 +6,16 @@ services: SECRET_KEY: ${SECRET_KEY:-change-me-please} WEB_CONCURRENCY: ${WEB_CONCURRENCY:-4} GUNICORN_THREADS: ${GUNICORN_THREADS:-5} + TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-} + TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-} + NEWS_API_TOKEN: ${NEWS_API_TOKEN:-} + TZ: ${TZ:-Europe/Moscow} volumes: - ./matrix.db:/app/matrix.db + - ./static/news_images:/app/static/news_images + - ./static/events_images:/app/static/events_images + - ./static/css:/app/static/css + - ./templates:/app/templates restart: unless-stopped nginx: @@ -16,7 +24,7 @@ services: depends_on: - app ports: - - "5000:80" + - "5002:80" volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro restart: unless-stopped diff --git a/mont_logo.png b/mont_logo.png index 7c4ec2f..49805c4 100644 Binary files a/mont_logo.png and b/mont_logo.png differ diff --git a/mont_scraper.py b/mont_scraper.py new file mode 100644 index 0000000..62f3c95 --- /dev/null +++ b/mont_scraper.py @@ -0,0 +1,717 @@ +#!/usr/bin/env python3 +""" +Парсер новостей с mont.ru → публикует в ZKART БД. +Запуск: python3 mont_scraper.py [--all10] +""" +import re, os, sys, secrets, datetime, sqlite3, time, json +from urllib.request import urlopen, Request, build_opener, HTTPCookieProcessor +from http.cookiejar import CookieJar +from urllib.parse import urlencode, urlparse +from html import unescape + +DB_PATH = "/home/ruslan/docker/ZKART#/matrix.db" +IMG_DIR = "/home/ruslan/docker/ZKART#/static/news_images" +BASE_URL = "https://www.mont.ru" +LIST_URL = "https://www.mont.ru/ru-ru/news?period=1" +SITE_BASE = "https://maps.4mont.ru" +TG_TOKEN = "8181219074:AAGvqWqb6t10YP4xpMOQnBq_6LrUqAFm5hM" +TG_CHAT_ID = "54986411" +MONT_EMAIL = "rgalyaviev@mont.com" +MONT_PASS = "utOgbZ09mont" + +HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", + "Accept": "text/html,application/xhtml+xml,*/*;q=0.9"} + +os.makedirs(IMG_DIR, exist_ok=True) + + +# ── Auth ────────────────────────────────────────────────────────────────────── + +def make_authenticated_opener() -> build_opener: + """Login to mont.ru via OIDC and return an opener with auth cookies.""" + jar = CookieJar() + opener = build_opener(HTTPCookieProcessor(jar)) + + # Step 1: GET login → redirected to passport.mont.ru + req = Request(f"{BASE_URL}/ru-ru/account/login", headers=HEADERS) + with opener.open(req, timeout=20) as r: + html = r.read().decode("utf-8", errors="replace") + login_url = r.url + + form_action = re.search(r']+action="([^"]+)"', html) + xsrf_m = re.search(r'name="idsrv\.xsrf"[^>]+value="([^"]+)"', html) + if not form_action or not xsrf_m: + raise RuntimeError("Login form not found") + + parsed = urlparse(login_url) + action_url = f"{parsed.scheme}://{parsed.netloc}{form_action.group(1)}" + + # Step 2: POST credentials + post_data = urlencode({ + "username": MONT_EMAIL, "password": MONT_PASS, "idsrv.xsrf": xsrf_m.group(1) + }).encode() + req2 = Request(action_url, data=post_data, + headers={**HEADERS, "Content-Type": "application/x-www-form-urlencoded", + "Referer": login_url}, + method="POST") + with opener.open(req2, timeout=20) as r: + html2 = r.read().decode("utf-8", errors="replace") + final_url = r.url + + # Step 3: form_post with id_token back to www.mont.ru + form_action2 = re.search(r']+action="([^"]+)"', html2) + if form_action2: + action2 = form_action2.group(1) + hidden = re.findall(r']+type="hidden"[^>]+name="([^"]+)"[^>]+value="([^"]*)"', html2) + if not hidden: + hidden = re.findall(r']+name="([^"]+)"[^>]+type="hidden"[^>]+value="([^"]*)"', html2) + post_data3 = urlencode(dict(hidden)).encode() + req3 = Request(action2, data=post_data3, + headers={**HEADERS, "Content-Type": "application/x-www-form-urlencoded", + "Referer": final_url}, + method="POST") + with opener.open(req3, timeout=20) as r: + r.read() + + return opener + + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +def tg_notify(text: str): + try: + payload = json.dumps({"chat_id": TG_CHAT_ID, "text": text, "parse_mode": "HTML"}).encode() + req = Request(f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage", + data=payload, + headers={"Content-Type": "application/json"}, + method="POST") + with urlopen(req, timeout=10): + pass + except Exception as e: + print(f" [WARN] Telegram notify failed: {e}") + + +def strip_tags(html): + return unescape(re.sub(r"<[^>]+>", "", html)).strip() + + +ALLOWED_TAGS = re.compile( + r'<(/?)(' + r'p|br|strong|b|em|i|u|s|ul|ol|li|a|h2|h3|h4|h5|blockquote|table|thead|tbody|tr|td|th' + r')(\b[^>]*)?>', re.IGNORECASE +) +ALLOWED_ATTRS = re.compile(r'\s+(href|target|rel)="([^"]*)"', re.IGNORECASE) +DANGEROUS_PROTOCOLS = re.compile(r'^(javascript|vbscript|data):', re.IGNORECASE) + + +def sanitize_html(html_body: str) -> str: + """Keep formatting tags (bold, links, lists etc.) but strip everything unsafe.""" + # Remove script/style blocks entirely + html_body = re.sub(r'<(script|style)[^>]*>.*?', '', html_body, flags=re.IGNORECASE | re.DOTALL) + # Remove HTML comments + html_body = re.sub(r'', '', html_body, flags=re.DOTALL) + + result = [] + pos = 0 + for m in re.finditer(r'<[^>]+>', html_body): + # Text before this tag — escape it + result.append(unescape(html_body[pos:m.start()])) + pos = m.end() + tag = m.group(0) + tag_m = ALLOWED_TAGS.match(tag) + if not tag_m: + continue # strip unknown/dangerous tags + slash, name, attrs_raw = tag_m.group(1), tag_m.group(2).lower(), tag_m.group(3) or "" + if slash: # closing tag + result.append(f'') + continue + # Build safe attribute string + safe_attrs = "" + if name == "a": + href_m = re.search(r'\bhref="([^"]*)"', attrs_raw, re.IGNORECASE) + if href_m: + href = href_m.group(1) + if not DANGEROUS_PROTOCOLS.match(href.strip()): + # Make relative mont.ru links absolute + if href.startswith("/"): + href = "https://www.mont.ru" + href + safe_attrs = f' href="{href}" target="_blank" rel="noopener"' + if name in ("br",): + result.append(f'<{name} />') + else: + result.append(f'<{name}{safe_attrs}>') + result.append(unescape(html_body[pos:])) + return "".join(result).strip() + + +def download_image(opener, img_src: str): + """Download image from mont.ru, return local relative path or None.""" + try: + from urllib.parse import quote + safe_path = quote(img_src, safe="/:.-_") if img_src.startswith("/") else img_src + url = BASE_URL + safe_path if img_src.startswith("/") else safe_path + ext = os.path.splitext(img_src.split("?")[0])[1].lower() or ".png" + if ext not in (".jpg", ".jpeg", ".png", ".webp", ".gif"): + ext = ".png" + fname = f"news_{secrets.token_hex(8)}{ext}" + path = os.path.join(IMG_DIR, fname) + req = Request(url, headers=HEADERS) + with opener.open(req, timeout=15) as resp: + with open(path, "wb") as f: + f.write(resp.read()) + return f"news_images/{fname}" + except Exception as e: + print(f" [WARN] Image download failed: {e}") + return None + + +def slug_from(title, slug_id): + slug = re.sub(r"[^a-z0-9а-яё]+", "-", title.lower()) + slug = re.sub(r"[а-яё]", "", slug) + slug = slug.strip("-")[:50] or f"mont-news-{slug_id}" + return f"{slug}-{slug_id}" + + +# ── News listing ────────────────────────────────────────────────────────────── + +def get_news_ids_from_listing(opener) -> tuple[list[str], dict[str, str]]: + """Return (list of IDs, dict of id→img_src) from the listing page.""" + req = Request(LIST_URL, headers=HEADERS) + with opener.open(req, timeout=20) as r: + html = r.read().decode("utf-8", errors="replace") + + # Pair images with the nearest following news link (within 2000 chars) + imgs = [(m.start(), m.group(1)) for m in re.finditer(r'src="(/Content/Images/[^"]+)"', html)] + links = [(m.start(), m.group(1)) for m in re.finditer(r'href="/ru-ru/news/(\d+)"', html)] + + id_to_img = {} + for img_pos, img_src in imgs: + for link_pos, art_id in links: + if link_pos > img_pos and link_pos - img_pos < 2000: + if art_id not in id_to_img: + id_to_img[art_id] = img_src + break + + # Full ordered list of IDs + ids = list(dict.fromkeys(art_id for _, art_id in links)) + return ids, id_to_img + + +def get_max_slug_id() -> int: + """Return the highest mont.ru article ID already in our DB.""" + try: + conn = sqlite3.connect(DB_PATH, timeout=10) + rows = conn.execute("SELECT slug FROM news ORDER BY id DESC LIMIT 50").fetchall() + conn.close() + ids = [] + for (slug,) in rows: + m = re.search(r"-(\d{4,})$", slug) + if m: + ids.append(int(m.group(1))) + return max(ids) if ids else 0 + except Exception: + return 0 + + +def is_already_saved(slug_id: str) -> bool: + conn = sqlite3.connect(DB_PATH, timeout=10) + row = conn.execute("SELECT id FROM news WHERE slug LIKE ?", (f"%-{slug_id}",)).fetchone() + conn.close() + return row is not None + + +# ── Fetch & save one article ────────────────────────────────────────────────── + +def fetch_and_save_article(opener, slug_id: str, listing_img: str = "") -> tuple[bool, str, str]: + """ + Fetch article from API, save to DB. + Returns (saved: bool, title: str, slug: str) + """ + if is_already_saved(slug_id): + print(f" [SKIP] Already exists: {slug_id}") + return False, "", "" + + # Fetch article data via authenticated API + api_url = f"{BASE_URL}/ru-ru/apiMvc/news/{slug_id}" + req = Request(api_url, headers={**HEADERS, "Accept": "application/json, text/plain, */*"}) + try: + with opener.open(req, timeout=20) as r: + data = json.loads(r.read().decode("utf-8", errors="replace")) + except Exception as e: + print(f" [WARN] API fetch failed for {slug_id}: {e}") + return False, "", "" + + title = strip_tags(data.get("title", "")).strip() + text_html = data.get("text", "") or "" + body = sanitize_html(text_html) + + if not title or len(title) < 5: + print(f" [SKIP] No title for {slug_id}") + return False, "", "" + + # Check not a 404 page + if "страница не найдена" in title.lower() or "404" in title: + print(f" [SKIP] 404 page for {slug_id}") + return False, "", "" + + print(f" [FETCH] {title[:70]}...") + + # Image: prefer listing image (most reliable), then API fields, then article page + img_src = listing_img or data.get("image") or data.get("img") or data.get("previewImage") or "" + image_path = None + if img_src: + image_path = download_image(opener, img_src) + if not image_path: + # Try scraping the article HTML page for an image + try: + req2 = Request(f"{BASE_URL}/ru-ru/news/{slug_id}", headers=HEADERS) + with opener.open(req2, timeout=15) as r: + pg = r.read().decode("utf-8", errors="replace") + img_m = re.search(r'src="(/Content/Images/[^"]+)"', pg) + if img_m: + image_path = download_image(opener, img_m.group(1)) + except Exception: + pass + + slug = slug_from(title, slug_id) + created_at = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + conn = sqlite3.connect(DB_PATH, timeout=15) + try: + conn.execute( + "INSERT INTO news(title, body, slug, image, published, created_at) VALUES (?,?,?,?,1,?)", + (title, body, slug, image_path, created_at) + ) + conn.commit() + print(f" [OK] Published: {title[:70]}") + except sqlite3.IntegrityError: + slug = f"{slug}-{secrets.token_hex(3)}" + conn.execute( + "INSERT INTO news(title, body, slug, image, published, created_at) VALUES (?,?,?,?,1,?)", + (title, body, slug, image_path, created_at) + ) + conn.commit() + print(f" [OK] Published (alt slug): {title[:70]}") + finally: + conn.close() + + time.sleep(0.5) + return True, title, slug + + +# ── Main ────────────────────────────────────────────────────────────────────── + +def main(): + all10 = "--all10" in sys.argv + print(f"[{datetime.datetime.now():%Y-%m-%d %H:%M}] Logging in to mont.ru...") + errors = [] + saved_count = 0 + published = [] + + try: + opener = make_authenticated_opener() + except Exception as e: + msg = f"Ошибка авторизации на mont.ru: {e}" + print(f"Auth error: {e}") + tg_notify(f"🚨 MONT парсер\n{msg}") + return + + try: + print("Fetching news listing...") + listing_ids, id_to_img = get_news_ids_from_listing(opener) + known_max = get_max_slug_id() + probe_ids = [str(i) for i in range(known_max + 1, known_max + 6)] + all_ids = list(dict.fromkeys(listing_ids + probe_ids)) + + if all10: + candidate_ids = all_ids[:15] + else: + candidate_ids = [sid for sid in all_ids if not is_already_saved(sid)] + + if candidate_ids: + print(f"Candidates: {candidate_ids}") + for sid in candidate_ids: + ok, title, slug = fetch_and_save_article(opener, sid, listing_img=id_to_img.get(sid, "")) + if ok: + saved_count += 1 + published.append((title, slug)) + + if saved_count > 0: + _, refreshed_imgs = get_news_ids_from_listing(opener) + conn = sqlite3.connect(DB_PATH, timeout=15) + for sid in candidate_ids: + img_src = refreshed_imgs.get(sid) + if img_src: + row = conn.execute( + "SELECT id, image FROM news WHERE slug LIKE ?", (f"%-{sid}",) + ).fetchone() + if row and not row[1]: + path = download_image(opener, img_src) + if path: + conn.execute("UPDATE news SET image=? WHERE id=?", (path, row[0])) + conn.commit() + conn.close() + else: + print("No new news.") + except Exception as e: + msg = f"Ошибка парсинга новостей: {e}" + print(f"News error: {e}") + errors.append(msg) + + print(f"Done. News saved: {saved_count}") + + # Hide outdated events + hidden_count = hide_outdated_events() + if hidden_count: + print(f"Hidden outdated events: {hidden_count}") + + # Scrape events + ev_count, ev_published, ev_error = scrape_events(opener) + if ev_error: + errors.append(ev_error) + + # Telegram: send only if something new OR errors + tg_lines = [] + if saved_count > 0: + suffix = "ь" if saved_count == 1 else "и" if 2 <= saved_count <= 4 else "ей" + tg_lines.append(f"✅ Новости: {saved_count} новост{suffix}:") + for title, slug in published: + tg_lines.append(f' • {title}') + if ev_count > 0: + suffix = "е" if ev_count == 1 else "я" if 2 <= ev_count <= 4 else "й" + tg_lines.append(f"📅 Мероприятия: {ev_count} мероприяти{suffix}:") + for title, slug in ev_published: + tg_lines.append(f' • {title}') + for err in errors: + tg_lines.append(f"🚨 {err}") + if tg_lines: + tg_notify("\n".join(tg_lines)) + + +# ── Events scraper ──────────────────────────────────────────────────────────── + +EVENTS_LIST_URL = "https://www.mont.ru/ru-ru/events?eventPeriod=1" +EVENTS_IMAGES_DIR = "/home/ruslan/docker/ZKART#/static/events_images" +os.makedirs(EVENTS_IMAGES_DIR, exist_ok=True) + + +def parse_event_date(raw: str) -> str | None: + """Parse various date formats to YYYY-MM-DD, return None if unparseable.""" + if not raw: + return None + raw = raw.strip() + # ISO format + m = re.match(r"(\d{4})-(\d{2})-(\d{2})", raw) + if m: + return f"{m.group(1)}-{m.group(2)}-{m.group(3)}" + # DD.MM.YYYY or DD/MM/YYYY + m = re.match(r"(\d{1,2})[./](\d{1,2})[./](\d{4})", raw) + if m: + return f"{m.group(3)}-{m.group(2).zfill(2)}-{m.group(1).zfill(2)}" + # D Month YYYY (Russian) + months_ru = {"января":"01","февраля":"02","марта":"03","апреля":"04","мая":"05","июня":"06", + "июля":"07","августа":"08","сентября":"09","октября":"10","ноября":"11","декабря":"12"} + m = re.match(r"(\d{1,2})\s+([а-яё]+)\s+(\d{4})", raw.lower()) + if m: + mon = months_ru.get(m.group(2)) + if mon: + return f"{m.group(3)}-{mon}-{m.group(1).zfill(2)}" + return None + + +def download_event_image(opener, img_src: str) -> str | None: + try: + from urllib.parse import quote + safe_path = quote(img_src, safe="/:.-_") if img_src.startswith("/") else img_src + url = BASE_URL + safe_path if img_src.startswith("/") else safe_path + ext = os.path.splitext(img_src.split("?")[0])[1].lower() or ".png" + if ext not in (".jpg", ".jpeg", ".png", ".webp", ".gif"): + ext = ".png" + fname = f"event_{secrets.token_hex(8)}{ext}" + path = os.path.join(EVENTS_IMAGES_DIR, fname) + req = Request(url, headers=HEADERS) + with opener.open(req, timeout=15) as resp: + with open(path, "wb") as f: + f.write(resp.read()) + return f"events_images/{fname}" + except Exception as e: + print(f" [WARN] Event image download failed: {e}") + return None + + +def get_event_ids_from_listing(opener) -> tuple[list[str], dict]: + """Use JSON API to get all upcoming events — returns more than the HTML listing.""" + import json as _json + api_url = "https://www.mont.ru/ru-ru/apiMvc/events?eventPeriod=1&perPageCount=100" + req = Request(api_url, headers=HEADERS) + with opener.open(req, timeout=20) as r: + data = _json.loads(r.read().decode("utf-8", errors="replace")) + + ids = [] + id_to_img = {} + id_to_date = {} + for ev in data.get("events", []): + eid = str(ev.get("eventId", "")) + if not eid: + continue + ids.append(eid) + img = ev.get("backgroundImageUrl", "") + if img: + id_to_img[eid] = img + start = ev.get("start", "") + if start: + id_to_date[eid] = start[:10] # "2026-06-09T10:00:00" → "2026-06-09" + + return ids, id_to_img, id_to_date + + +def fetch_and_save_event(opener, eid: str, listing_img: str = "", listing_date: str = "") -> tuple[bool, str, str]: + from zkart_db_shim import is_event_saved, create_event + if is_event_saved(eid): + print(f" [SKIP] Event already exists: {eid}") + return False, "", "" + + # Try API first + api_url = f"{BASE_URL}/ru-ru/apiMvc/events/{eid}" + req = Request(api_url, headers={**HEADERS, "Accept": "application/json, text/plain, */*"}) + data = {} + try: + with opener.open(req, timeout=20) as r: + data = json.loads(r.read().decode("utf-8", errors="replace")) + except Exception: + pass + + title = strip_tags(data.get("title", "") or data.get("name", "")).strip() + body_html = data.get("text", "") or data.get("description", "") or "" + body = sanitize_html(body_html) + + # Fallback: scrape article page + if not title: + try: + req2 = Request(f"{BASE_URL}/ru-ru/events/{eid}", headers=HEADERS) + with opener.open(req2, timeout=20) as r: + pg = r.read().decode("utf-8", errors="replace") + h1 = re.search(r']*>(.*?)', pg, re.DOTALL) + if h1: + title = strip_tags(h1.group(1)).strip() + if not body: + content_m = re.search(r']+class="[^"]*content[^"]*"[^>]*>(.*?)', pg, re.DOTALL | re.IGNORECASE) + if content_m: + body = sanitize_html(content_m.group(1)) + # Try to get date from page + if not listing_date: + dm = re.search(r'(\d{1,2}[./]\d{1,2}[./]\d{4}|\d{1,2}\s+[а-яё]+\s+\d{4})', pg, re.IGNORECASE) + if dm: + listing_date = parse_event_date(dm.group(1)) or "" + except Exception as e: + print(f" [WARN] Event page fetch failed: {e}") + + if not title or len(title) < 4: + print(f" [SKIP] No title for event {eid}") + return False, "", "" + + print(f" [FETCH] Event: {title[:70]}...") + + # Date + event_date = listing_date + if not event_date: + for field in ("date", "startDate", "start_date", "eventDate", "dateStart"): + raw = data.get(field, "") + if raw: + event_date = parse_event_date(str(raw)) or "" + if event_date: + break + if not event_date: + event_date = datetime.date.today().strftime("%Y-%m-%d") + + # Image + img_src = listing_img or data.get("image") or data.get("img") or data.get("previewImage") or "" + image_path = None + if img_src: + image_path = download_event_image(opener, img_src) + + slug_base = slug_from(title, eid) + + conn = sqlite3.connect(DB_PATH, timeout=15) + try: + conn.execute( + "INSERT INTO events(title, body, slug, image, event_date, published) VALUES (?,?,?,?,?,1)", + (title, body, slug_base, image_path, event_date) + ) + conn.commit() + print(f" [OK] Event saved: {title[:60]} ({event_date})") + except sqlite3.IntegrityError: + slug_base = f"{slug_base}-{secrets.token_hex(3)}" + conn.execute( + "INSERT INTO events(title, body, slug, image, event_date, published) VALUES (?,?,?,?,?,1)", + (title, body, slug_base, image_path, event_date) + ) + conn.commit() + finally: + conn.close() + + time.sleep(0.4) + return True, title, slug_base + + +def hide_outdated_events() -> int: + """Set published=0 for events where event_date <= today.""" + conn = sqlite3.connect(DB_PATH, timeout=10) + cur = conn.execute( + "UPDATE events SET published=0 WHERE published=1 AND event_date <= date('now','localtime')" + ) + count = cur.rowcount + conn.commit() + conn.close() + return count + + + +def parse_event_page(html: str) -> dict: + """Extract body, register_url, image_src from events-details page HTML.""" + import re as _re + from html import unescape as _u + + # Description: events-details__about block + body = "" + about_m = _re.search( + r'class="events-details__about[^"]*"[^>]*>.*?]*>(.*?)\s*\s*', + html, _re.DOTALL + ) + if about_m: + body = sanitize_html(about_m.group(1)) + + # Registration URL + reg_m = _re.search(r'class="[^"]*register-btn[^"]*"[^>]+href="([^"]+)"', html, _re.IGNORECASE) + _raw_reg = reg_m.group(1) if reg_m else "" + if _raw_reg.startswith("/"): + _raw_reg = "https://www.mont.ru" + _raw_reg + register_url = _raw_reg + + # Cover background image + cover_m = _re.search(r'events-details__background[^>]+style="background-image:\s*url\("([^&]+)"\)', html) + img_src = cover_m.group(1) if cover_m else "" + + # Fallback: vendor logo + if not img_src: + logo_m = _re.search(r'events-details__logo[^>]*>.*?]+src="([^"]+)"', html, _re.DOTALL) + if logo_m: + img_src = logo_m.group(1) + + # Fallback: any /Content/Images + if not img_src: + ci_m = _re.search(r'src="(/Content/Images/[^"]+)"', html) + if ci_m: + img_src = ci_m.group(1) + + # Date from events-details__dates + date_m = _re.search(r'events-details__dates[^>]*>.*?(\d{1,2}\.\d{2}\.\d{4})', html, _re.DOTALL) + date_str = parse_event_date(date_m.group(1)) if date_m else "" + + return {"body": body, "register_url": register_url, "img_src": img_src, "date_str": date_str} + + +def scrape_events(opener=None): + print(f"[{datetime.datetime.now():%Y-%m-%d %H:%M}] Scraping events...") + + def is_event_saved(eid): + conn = sqlite3.connect(DB_PATH, timeout=10) + row = conn.execute("SELECT id FROM events WHERE slug LIKE ?", (f"%-{eid}",)).fetchone() + conn.close() + return row is not None + + def save_event(title, body, slug, image_path, event_date, register_url): + conn = sqlite3.connect(DB_PATH, timeout=15) + try: + conn.execute( + "INSERT INTO events(title, body, slug, image, event_date, published, register_url) VALUES (?,?,?,?,?,1,?)", + (title, body, slug, image_path, event_date, register_url) + ) + conn.commit() + return slug + except sqlite3.IntegrityError: + s2 = f"{slug}-{secrets.token_hex(3)}" + conn.execute( + "INSERT INTO events(title, body, slug, image, event_date, published, register_url) VALUES (?,?,?,?,?,1,?)", + (title, body, s2, image_path, event_date, register_url) + ) + conn.commit() + return s2 + finally: + conn.close() + + if opener is None: + try: + opener = make_authenticated_opener() + except Exception as e: + msg = f"Ошибка авторизации (events): {e}" + print(f" Auth error: {e}") + return 0, [], msg + + try: + ids, id_to_img, id_to_date = get_event_ids_from_listing(opener) + except Exception as e: + msg = f"Ошибка листинга мероприятий: {e}" + print(f" Listing error: {e}") + return 0, [], msg + + candidates = [eid for eid in ids if not is_event_saved(eid)] + if not candidates: + print(" No new events.") + return 0, [], None + + print(f" Event candidates: {candidates}") + saved_count = 0 + published = [] + + for eid in candidates: + # Fetch full event page HTML (contains all data) + try: + req = Request(f"{BASE_URL}/ru-ru/events/{eid}", headers=HEADERS) + with opener.open(req, timeout=20) as r: + pg = r.read().decode("utf-8", errors="replace") + except Exception as e: + print(f" [WARN] Could not fetch event page {eid}: {e}") + continue + + parsed = parse_event_page(pg) + body = parsed["body"] + register_url = parsed["register_url"] + img_src = parsed["img_src"] or id_to_img.get(eid, "") + event_date = parsed["date_str"] or id_to_date.get(eid, "") + + # Title from h1 + h1_m = re.search(r']*>(.*?)', pg, re.DOTALL) + title = strip_tags(h1_m.group(1)).strip() if h1_m else "" + if not title: + title = strip_tags(re.search(r'events-details__title[^>]*>(.*?)]+>', pg, re.DOTALL).group(1)).strip() if re.search(r'events-details__title[^>]*>(.*?)]+>', pg, re.DOTALL) else "" + + if not title or len(title) < 4: + print(f" [SKIP] No title for event {eid}") + continue + if "страница не найдена" in title.lower() or "404" in title: + print(f" [SKIP] 404 for event {eid}") + continue + if not body: + body = title # at minimum use title as body + + if not event_date: + event_date = datetime.date.today().strftime("%Y-%m-%d") + + # Download image + image_path = None + if img_src: + image_path = download_event_image(opener, img_src) + + slug = slug_from(title, eid) + final_slug = save_event(title, body, slug, image_path, event_date, register_url) + print(f" [OK] Event: {title[:60]} ({event_date}){' +reg' if register_url else ''}") + saved_count += 1 + published.append((title, final_slug)) + time.sleep(0.4) + + return saved_count, published, None + + +if __name__ == "__main__": + main() diff --git a/nginx.conf b/nginx.conf index abba34f..4569882 100644 --- a/nginx.conf +++ b/nginx.conf @@ -6,7 +6,7 @@ server { location / { proxy_pass http://app:8000; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; diff --git a/static/css/admin.css b/static/css/admin.css index e5caecc..6f839f6 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -1,7 +1,7 @@ :root { --b:#1f4ea3; --line:#cfe0ff; } * { box-sizing: border-box; } body { margin:0; font-family:Manrope,sans-serif; background:#f0f5ff; color:#1a2746; } - body.ib { background:#fff1f1; } + body.ib { background:#eefaf3; } .wrap { width:min(1600px, calc(100% - 24px)); margin:12px auto 24px; } .top { background: linear-gradient(130deg, #1f4ea3, #3977df); @@ -13,7 +13,7 @@ align-items:center; gap:10px; } - body.ib .top { background: linear-gradient(130deg, #9b2f3a, #c24a56); } + body.ib .top { background: linear-gradient(130deg, #1f7a4a, #37a96b); } .scope-switch { display:flex; gap:8px; } .scope-chip { display:inline-block; @@ -26,6 +26,9 @@ border:1px solid #ccdcff; } .scope-chip.active { background:#fff; color:#112847; } + .top-actions { display:flex; gap:8px; align-items:center; } + .top-actions a, + .top-actions form { margin:0; } .grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap:10px; margin:12px 0; } .box { background:#fff; @@ -54,12 +57,44 @@ .pri { background:#1f4ea3; color:#fff; } .warn { background:#e8eefc; color:#223963; } .danger { background:#ffefef; color:#8e1d1d; } + .alerts { display:grid; gap:8px; margin:12px 0; } + .alert { border-radius:10px; padding:10px 12px; font-weight:700; border:1px solid #cfe0ff; background:#fff; } + .alert.ok { color:#14532d; background:#ecfdf3; border-color:#bde9cb; } + .alert.error { color:#8e1d1d; background:#ffefef; border-color:#ffcaca; } .lists { display:grid; grid-template-columns:1fr 1fr 1fr; gap:10px; margin-bottom:10px; } .list-box { max-height: 430px; overflow-y: auto; padding-right: 4px; } .list-box::-webkit-scrollbar { width:12px; } .list-box::-webkit-scrollbar-thumb { background:#bfd4ff; border-radius:10px; } .list-item { display:flex; justify-content:space-between; align-items:center; gap:8px; border:1px solid var(--line); border-radius:10px; padding:6px 8px; margin-bottom:6px; background:#fff; min-height: 36px; } + .product-item { border:1px solid var(--line); border-radius:10px; padding:6px 8px; margin-bottom:6px; background:#fff; } + .product-row { display:flex; justify-content:space-between; align-items:center; gap:8px; min-height:36px; } + .product-row span { min-width:0; overflow-wrap:anywhere; } + .product-actions { display:flex; gap:8px; align-items:center; flex-shrink:0; } + .product-actions form { margin:0; } + .product-edit { display:grid; grid-template-columns: minmax(120px, 1fr) minmax(150px, 1.2fr) auto; gap:8px; margin-top:8px; } + .product-edit[hidden] { display:none; } + .pending-box { margin:12px 0; } + .pending-head { display:flex; justify-content:space-between; align-items:center; gap:10px; margin-bottom:10px; } + .pending-head h3 { margin:0; } + .pending-head form { margin:0; } + .pending-list { display:grid; gap:8px; } + .pending-item { display:grid; grid-template-columns:minmax(0, 1fr) auto; gap:10px; align-items:start; border:1px solid var(--line); border-radius:10px; padding:8px; } + .pending-item span { display:block; color:#49638f; font-size:12px; margin-top:2px; } + .pending-desc { margin-top:8px; padding:8px; border-radius:8px; background:#f6f9ff; white-space:pre-wrap; font-size:13px; line-height:1.45; } + .pending-actions { display:flex; gap:8px; } + .pending-actions form { margin:0; } + .admin-users-box { margin:12px 0; } + .created-admin-card { display:grid; gap:6px; margin:0 0 10px; padding:10px 12px; border:1px solid #9fd7b1; border-radius:10px; background:#ecfdf3; color:#14532d; } + .created-admin-card code { display:inline-block; padding:3px 6px; border-radius:6px; background:#fff; color:#102a1a; font-weight:800; } + .created-admin-share { display:grid; grid-template-columns:minmax(0, 1fr) auto; gap:8px; align-items:stretch; margin-top:4px; } + .created-admin-share textarea { width:100%; min-height:84px; resize:vertical; padding:9px 10px; border:1px solid #9fd7b1; border-radius:9px; font:700 13px/1.45 Manrope,sans-serif; color:#102a1a; background:#fff; } + .admin-create { display:grid; grid-template-columns:minmax(160px, 1fr) auto auto auto; gap:8px; align-items:center; } + .admin-create label { display:flex; align-items:center; gap:6px; font-weight:700; white-space:nowrap; } + .admin-users-list { display:grid; gap:6px; margin-top:10px; } + .admin-user-item { display:flex; justify-content:space-between; align-items:center; gap:8px; border:1px solid var(--line); border-radius:10px; padding:7px 8px; } + .admin-user-item span { display:grid; gap:2px; } + small { color:#60759d; font-size:11px; } .matrix-wrap { background:#fff; border:1px solid #d4e3ff; border-radius:12px; padding:10px; } .matrix-scroll { overflow:auto; max-height:72vh; border:1px solid #dce7ff; border-radius:10px; } .matrix-scroll::-webkit-scrollbar, @@ -75,3 +110,103 @@ th:first-child { z-index: 3; } td input { transform: scale(1.05); } .matrix-tip { margin:0 0 6px; font-size:12px; color:#37507d; } + + @media (max-width: 980px) { + .wrap { + width: calc(100% - 16px); + margin: 8px auto 16px; + } + .top { + flex-direction: column; + align-items: stretch; + padding: 12px; + } + .top-actions { + width: 100%; + display: grid !important; + grid-template-columns: 1fr 1fr 1fr; + } + .top-actions a, + .top-actions form, + .top-actions button { + width: 100%; + } + .scope-switch { + flex-wrap: wrap; + } + .scope-chip { + flex: 1 1 auto; + text-align: center; + } + .grid { + grid-template-columns: 1fr; + } + .lists { + grid-template-columns: 1fr; + } + .list-box { + max-height: 300px; + } + .list-item { + align-items: flex-start; + flex-wrap: wrap; + } + .product-row, + .product-actions, + .pending-head, + .pending-item, + .pending-actions, + .admin-create, + .admin-user-item, + .created-admin-share { + display:grid; + grid-template-columns:1fr; + width:100%; + } + .product-edit { + grid-template-columns:1fr; + } + .matrix-wrap { + padding: 8px; + } + .matrix-scroll { + max-height: 62vh; + } + th, td { + font-size: 11px; + padding: 5px; + } + th:first-child, + td:first-child { + min-width: 170px; + } + input[type="text"], + select, + button { + min-height: 40px; + } + } + + @media (max-width: 600px) { + .top-actions { + grid-template-columns: 1fr; + } + .inline-product { + grid-template-columns: 1fr; + } + .matrix-h-scroll { + height: 24px; + } + .matrix-scroll::-webkit-scrollbar, + .matrix-h-scroll::-webkit-scrollbar { + height: 18px; + width: 12px; + } + th:first-child, + td:first-child { + min-width: 145px; + } + .matrix-tip { + font-size: 11px; + } + } diff --git a/static/css/index.css b/static/css/index.css index 77f91f0..5d85011 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -183,8 +183,163 @@ font-size: 16px; } - .board { + /* ── Page layout: main content + news sidebar ── */ + .page-layout { margin-top: 18px; + display: grid; + grid-template-columns: 1fr 300px; + gap: 20px; + align-items: start; + } + + .main-col { + min-width: 0; + } + + .news-sidebar { + position: sticky; + top: 18px; + } + + .news-widget { + background: #fff; + border-radius: var(--radius); + border: 1px solid #dfebff; + box-shadow: 0 10px 30px rgba(24, 56, 116, .08); + overflow: hidden; + } + + .news-widget-head { + padding: 14px 16px 10px; + border-bottom: 1px solid #edf3ff; + display: flex; + align-items: center; + gap: 8px; + } + + .news-all-link { + font-size: 12px; font-weight: 700; color: var(--brand-2); + text-decoration: none; white-space: nowrap; + padding: 3px 10px; border-radius: 999px; + background: #eef4ff; border: 1px solid #c8d8f7; + transition: .15s; + } + .news-all-link:hover { background: #dbe8ff; } + + .news-widget-head h2 { + margin: 0; + font-size: 13px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 1px; + color: #234782; + display: flex; + align-items: center; + gap: 7px; + } + + .news-widget-head h2::before { + content: ""; + display: inline-block; + width: 3px; + height: 14px; + border-radius: 2px; + background: linear-gradient(180deg, #3978e0, #1f4ea3); + flex-shrink: 0; + } + + .news-list { + display: flex; + flex-direction: column; + } + + .news-card { + border-bottom: 1px solid #f0f5ff; + transition: background .15s; + overflow: hidden; + display: flex; + align-items: center; + text-decoration: none; + } + + .news-card:last-child { + border-bottom: none; + } + + .news-card:hover { + background: #f8fbff; + } + + .news-card-img-wrap { + flex-shrink: 0; + width: 62px; height: 62px; + margin: 10px 0 10px 12px; + border-radius: 8px; + overflow: hidden; + background: #e8f0ff; + display: flex; align-items: center; justify-content: center; + } + + .news-card-img { + width: 62px; height: 62px; + object-fit: cover; + display: block; + } + + .news-card-no-img { + font-size: 22px; + line-height: 1; + } + + .news-card-body { + padding: 10px 12px 10px 10px; + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; + } + + .news-card-date { + font-size: 10px; + font-weight: 700; + color: #b0c4df; + letter-spacing: .3px; + text-transform: uppercase; + } + + .news-card-title { + margin: 0; + font-size: 12.5px; + font-weight: 700; + color: #1a3e79; + line-height: 1.4; + } + + .news-card-btn { + display: inline-flex; + align-items: center; + font-size: 11px; + font-weight: 700; + color: var(--brand-2); + text-decoration: none; + margin-top: 2px; + transition: .15s; + } + + .news-card-btn:hover { + color: var(--brand); + transform: translateX(2px); + } + + .news-empty { + padding: 20px 16px; + font-size: 13px; + color: #9ab0d0; + margin: 0; + } + + .board { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; @@ -538,12 +693,24 @@ transform: scale(1.04); } + @media (max-width: 1100px) { + .page-layout { grid-template-columns: 1fr 260px; } + } + @media (max-width: 980px) { .brand-logo { max-width: 160px; } .board { grid-template-columns: 1fr; } .hero { padding: 20px; } .credit { right: 8px; bottom: 6px; } #btn-contact-ruslan { font-size: 14px; } + .page-layout { grid-template-columns: 1fr; } + .news-sidebar { position: static; } + .news-list { flex-direction: column; } + } + + @media (max-width: 640px) { + .news-list { flex-direction: column; } + .news-card { border-right: none; border-bottom: 1px solid #f0f5ff; } } @media (max-width: 768px) { diff --git a/static/events_images/event_0c98293921faf7af.png b/static/events_images/event_0c98293921faf7af.png new file mode 100644 index 0000000..016ef5b Binary files /dev/null and b/static/events_images/event_0c98293921faf7af.png differ diff --git a/static/events_images/event_26ec3a4a9e0db0b1.png b/static/events_images/event_26ec3a4a9e0db0b1.png new file mode 100644 index 0000000..97c8fd3 Binary files /dev/null and b/static/events_images/event_26ec3a4a9e0db0b1.png differ diff --git a/static/events_images/event_379c313a039d79b9.png b/static/events_images/event_379c313a039d79b9.png new file mode 100644 index 0000000..bdb2306 Binary files /dev/null and b/static/events_images/event_379c313a039d79b9.png differ diff --git a/static/events_images/event_3b60ef1d9b4538f4.png b/static/events_images/event_3b60ef1d9b4538f4.png new file mode 100644 index 0000000..af70046 Binary files /dev/null and b/static/events_images/event_3b60ef1d9b4538f4.png differ diff --git a/static/events_images/event_53bd17d01c23f641.png b/static/events_images/event_53bd17d01c23f641.png new file mode 100644 index 0000000..5239b58 Binary files /dev/null and b/static/events_images/event_53bd17d01c23f641.png differ diff --git a/static/events_images/event_5d4c1c63052e6265.png b/static/events_images/event_5d4c1c63052e6265.png new file mode 100644 index 0000000..ea11392 Binary files /dev/null and b/static/events_images/event_5d4c1c63052e6265.png differ diff --git a/static/events_images/event_67232ea3634d2a65.png b/static/events_images/event_67232ea3634d2a65.png new file mode 100644 index 0000000..ddf8671 Binary files /dev/null and b/static/events_images/event_67232ea3634d2a65.png differ diff --git a/static/events_images/event_6aedda2c209bf527.png b/static/events_images/event_6aedda2c209bf527.png new file mode 100644 index 0000000..c30c2bd Binary files /dev/null and b/static/events_images/event_6aedda2c209bf527.png differ diff --git a/static/events_images/event_7721e000bfe283aa.png b/static/events_images/event_7721e000bfe283aa.png new file mode 100644 index 0000000..1da85f7 Binary files /dev/null and b/static/events_images/event_7721e000bfe283aa.png differ diff --git a/static/events_images/event_823766ff2361336f.png b/static/events_images/event_823766ff2361336f.png new file mode 100644 index 0000000..ea11392 Binary files /dev/null and b/static/events_images/event_823766ff2361336f.png differ diff --git a/static/events_images/event_843c78fc9a096581.png b/static/events_images/event_843c78fc9a096581.png new file mode 100644 index 0000000..971cc9d Binary files /dev/null and b/static/events_images/event_843c78fc9a096581.png differ diff --git a/static/events_images/event_93b3f64f44141ea6.png b/static/events_images/event_93b3f64f44141ea6.png new file mode 100644 index 0000000..3eda1fc Binary files /dev/null and b/static/events_images/event_93b3f64f44141ea6.png differ diff --git a/static/events_images/event_9a184b802ba34a89.png b/static/events_images/event_9a184b802ba34a89.png new file mode 100644 index 0000000..ea11392 Binary files /dev/null and b/static/events_images/event_9a184b802ba34a89.png differ diff --git a/static/events_images/event_aa98deba2ea326d3.png b/static/events_images/event_aa98deba2ea326d3.png new file mode 100644 index 0000000..c58d01d Binary files /dev/null and b/static/events_images/event_aa98deba2ea326d3.png differ diff --git a/static/events_images/event_ab1b001b4f13ba42.png b/static/events_images/event_ab1b001b4f13ba42.png new file mode 100644 index 0000000..cddbf7b Binary files /dev/null and b/static/events_images/event_ab1b001b4f13ba42.png differ diff --git a/static/events_images/event_c13ad61a90334871.png b/static/events_images/event_c13ad61a90334871.png new file mode 100644 index 0000000..ea22f42 Binary files /dev/null and b/static/events_images/event_c13ad61a90334871.png differ diff --git a/static/events_images/event_c86b7abfa6562d03.png b/static/events_images/event_c86b7abfa6562d03.png new file mode 100644 index 0000000..016ef5b Binary files /dev/null and b/static/events_images/event_c86b7abfa6562d03.png differ diff --git a/static/events_images/event_cbd632f7b7594dc8.png b/static/events_images/event_cbd632f7b7594dc8.png new file mode 100644 index 0000000..27317b1 Binary files /dev/null and b/static/events_images/event_cbd632f7b7594dc8.png differ diff --git a/static/events_images/event_cceafb01735daa6f.png b/static/events_images/event_cceafb01735daa6f.png new file mode 100644 index 0000000..f7acc1c Binary files /dev/null and b/static/events_images/event_cceafb01735daa6f.png differ diff --git a/static/events_images/event_d49754a43821362c.png b/static/events_images/event_d49754a43821362c.png new file mode 100644 index 0000000..85b521b Binary files /dev/null and b/static/events_images/event_d49754a43821362c.png differ diff --git a/static/events_images/event_e5be98c3906e9233.png b/static/events_images/event_e5be98c3906e9233.png new file mode 100644 index 0000000..c30c2bd Binary files /dev/null and b/static/events_images/event_e5be98c3906e9233.png differ diff --git a/static/events_images/event_eb59c962d99ff3ea.png b/static/events_images/event_eb59c962d99ff3ea.png new file mode 100644 index 0000000..5e3ff63 Binary files /dev/null and b/static/events_images/event_eb59c962d99ff3ea.png differ diff --git a/static/events_images/event_f5ff06607d3f2a90.png b/static/events_images/event_f5ff06607d3f2a90.png new file mode 100644 index 0000000..c30c2bd Binary files /dev/null and b/static/events_images/event_f5ff06607d3f2a90.png differ diff --git a/static/events_images/event_fa00d06e57275e36.png b/static/events_images/event_fa00d06e57275e36.png new file mode 100644 index 0000000..f7acc1c Binary files /dev/null and b/static/events_images/event_fa00d06e57275e36.png differ diff --git a/static/favicon.svg b/static/favicon.svg new file mode 100644 index 0000000..08ab93c --- /dev/null +++ b/static/favicon.svg @@ -0,0 +1,25 @@ + + 4MONT favicon + A compact favicon inspired by the 4MONT logo: a blue geometric 4 and bold black M on a clean rounded square. + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/news_images/news_00273bfc77cb3f2b.png b/static/news_images/news_00273bfc77cb3f2b.png new file mode 100644 index 0000000..bd695e0 Binary files /dev/null and b/static/news_images/news_00273bfc77cb3f2b.png differ diff --git a/static/news_images/news_0181cb77115980db.png b/static/news_images/news_0181cb77115980db.png new file mode 100644 index 0000000..c6317ec Binary files /dev/null and b/static/news_images/news_0181cb77115980db.png differ diff --git a/static/news_images/news_037422b264ae6454.jpg b/static/news_images/news_037422b264ae6454.jpg new file mode 100644 index 0000000..ff704b9 Binary files /dev/null and b/static/news_images/news_037422b264ae6454.jpg differ diff --git a/static/news_images/news_03d4f3b0c289faa2.png b/static/news_images/news_03d4f3b0c289faa2.png new file mode 100644 index 0000000..c6317ec Binary files /dev/null and b/static/news_images/news_03d4f3b0c289faa2.png differ diff --git a/static/news_images/news_072feccd35c4fec7.jpg b/static/news_images/news_072feccd35c4fec7.jpg new file mode 100644 index 0000000..6b059fd --- /dev/null +++ b/static/news_images/news_072feccd35c4fec7.jpg @@ -0,0 +1,551 @@ + + + + + + 404 - материал не найден » Техноновости + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ +
+
+ +
+

404 — материал не найден

+ + +
+ +
+

Новость не найдена на сайте, возможно она удалена или перемещена по новому адресу.

+ + + +

Воспользуйтесь поиском по сайту.

+ + +
+
+ + +
+ + + + +
+ + + +

+
+ +
+
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/news_images/news_0b4a2c70bceb24e1.jpg b/static/news_images/news_0b4a2c70bceb24e1.jpg new file mode 100644 index 0000000..6b059fd --- /dev/null +++ b/static/news_images/news_0b4a2c70bceb24e1.jpg @@ -0,0 +1,551 @@ + + + + + + 404 - материал не найден » Техноновости + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ +
+
+ +
+

404 — материал не найден

+ + +
+ +
+

Новость не найдена на сайте, возможно она удалена или перемещена по новому адресу.

+ + + +

Воспользуйтесь поиском по сайту.

+ + +
+
+ + +
+ + + + +
+ + + +

+
+ +
+
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/news_images/news_0bbf8d541fc1d255.png b/static/news_images/news_0bbf8d541fc1d255.png new file mode 100644 index 0000000..19e70da Binary files /dev/null and b/static/news_images/news_0bbf8d541fc1d255.png differ diff --git a/static/news_images/news_0c9b98ea191873a6.png b/static/news_images/news_0c9b98ea191873a6.png new file mode 100644 index 0000000..f4eeade Binary files /dev/null and b/static/news_images/news_0c9b98ea191873a6.png differ diff --git a/static/news_images/news_0fb3747d6c1ecebc.png b/static/news_images/news_0fb3747d6c1ecebc.png new file mode 100644 index 0000000..e2f0ffd Binary files /dev/null and b/static/news_images/news_0fb3747d6c1ecebc.png differ diff --git a/static/news_images/news_10dcf574a4f17a9f.jpg b/static/news_images/news_10dcf574a4f17a9f.jpg new file mode 100644 index 0000000..41cac5c Binary files /dev/null and b/static/news_images/news_10dcf574a4f17a9f.jpg differ diff --git a/static/news_images/news_1360c0dca3af6577.jpg b/static/news_images/news_1360c0dca3af6577.jpg new file mode 100644 index 0000000..3c6625b Binary files /dev/null and b/static/news_images/news_1360c0dca3af6577.jpg differ diff --git a/static/news_images/news_175295b3cf950d20.png b/static/news_images/news_175295b3cf950d20.png new file mode 100644 index 0000000..f3752aa Binary files /dev/null and b/static/news_images/news_175295b3cf950d20.png differ diff --git a/static/news_images/news_17facf281b4f1349.jpg b/static/news_images/news_17facf281b4f1349.jpg new file mode 100644 index 0000000..40b3df8 Binary files /dev/null and b/static/news_images/news_17facf281b4f1349.jpg differ diff --git a/static/news_images/news_20a1dde3b0ebc4b0.jpg b/static/news_images/news_20a1dde3b0ebc4b0.jpg new file mode 100644 index 0000000..ee7fb8f Binary files /dev/null and b/static/news_images/news_20a1dde3b0ebc4b0.jpg differ diff --git a/static/news_images/news_2593882fd44f72c2.jpg b/static/news_images/news_2593882fd44f72c2.jpg new file mode 100644 index 0000000..1062a90 Binary files /dev/null and b/static/news_images/news_2593882fd44f72c2.jpg differ diff --git a/static/news_images/news_28e23559a59bcf77.jpg b/static/news_images/news_28e23559a59bcf77.jpg new file mode 100644 index 0000000..78c627b Binary files /dev/null and b/static/news_images/news_28e23559a59bcf77.jpg differ diff --git a/static/news_images/news_2bf1abed99635844.jpg b/static/news_images/news_2bf1abed99635844.jpg new file mode 100644 index 0000000..f1e8b6e Binary files /dev/null and b/static/news_images/news_2bf1abed99635844.jpg differ diff --git a/static/news_images/news_2e98d976f91405ae.png b/static/news_images/news_2e98d976f91405ae.png new file mode 100644 index 0000000..21640d3 Binary files /dev/null and b/static/news_images/news_2e98d976f91405ae.png differ diff --git a/static/news_images/news_30b0905e5ee1f4b3.jpg b/static/news_images/news_30b0905e5ee1f4b3.jpg new file mode 100644 index 0000000..952dc7f Binary files /dev/null and b/static/news_images/news_30b0905e5ee1f4b3.jpg differ diff --git a/static/news_images/news_365c0a375e75f50a.png b/static/news_images/news_365c0a375e75f50a.png new file mode 100644 index 0000000..b64c8a7 Binary files /dev/null and b/static/news_images/news_365c0a375e75f50a.png differ diff --git a/static/news_images/news_55b57a253d19ad55.jpg b/static/news_images/news_55b57a253d19ad55.jpg new file mode 100644 index 0000000..3c6625b Binary files /dev/null and b/static/news_images/news_55b57a253d19ad55.jpg differ diff --git a/static/news_images/news_5c645cff337cd188.jpg b/static/news_images/news_5c645cff337cd188.jpg new file mode 100644 index 0000000..db4fcee Binary files /dev/null and b/static/news_images/news_5c645cff337cd188.jpg differ diff --git a/static/news_images/news_5f769cbff413442d.jpg b/static/news_images/news_5f769cbff413442d.jpg new file mode 100644 index 0000000..414a56a --- /dev/null +++ b/static/news_images/news_5f769cbff413442d.jpg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/news_images/news_66a28424d07d5a78.png b/static/news_images/news_66a28424d07d5a78.png new file mode 100644 index 0000000..c6317ec Binary files /dev/null and b/static/news_images/news_66a28424d07d5a78.png differ diff --git a/static/news_images/news_6b218d24061b4331.jpg b/static/news_images/news_6b218d24061b4331.jpg new file mode 100644 index 0000000..dd12f86 Binary files /dev/null and b/static/news_images/news_6b218d24061b4331.jpg differ diff --git a/static/news_images/news_6c9f27e38d8625e3.png b/static/news_images/news_6c9f27e38d8625e3.png new file mode 100644 index 0000000..c8071d4 Binary files /dev/null and b/static/news_images/news_6c9f27e38d8625e3.png differ diff --git a/static/news_images/news_6d570994a642d94a.png b/static/news_images/news_6d570994a642d94a.png new file mode 100644 index 0000000..7885662 Binary files /dev/null and b/static/news_images/news_6d570994a642d94a.png differ diff --git a/static/news_images/news_7083f891ddc4026a.png b/static/news_images/news_7083f891ddc4026a.png new file mode 100644 index 0000000..84dda69 Binary files /dev/null and b/static/news_images/news_7083f891ddc4026a.png differ diff --git a/static/news_images/news_715299bed9d57b76.png b/static/news_images/news_715299bed9d57b76.png new file mode 100644 index 0000000..d2fa610 Binary files /dev/null and b/static/news_images/news_715299bed9d57b76.png differ diff --git a/static/news_images/news_76bc65b0f2c34073.png b/static/news_images/news_76bc65b0f2c34073.png new file mode 100644 index 0000000..044ffc7 Binary files /dev/null and b/static/news_images/news_76bc65b0f2c34073.png differ diff --git a/static/news_images/news_79bfa26a3e7d6b90.jpg b/static/news_images/news_79bfa26a3e7d6b90.jpg new file mode 100644 index 0000000..3a503fa Binary files /dev/null and b/static/news_images/news_79bfa26a3e7d6b90.jpg differ diff --git a/static/news_images/news_7a3ea325ac6ed8aa.jpg b/static/news_images/news_7a3ea325ac6ed8aa.jpg new file mode 100644 index 0000000..3a503fa Binary files /dev/null and b/static/news_images/news_7a3ea325ac6ed8aa.jpg differ diff --git a/static/news_images/news_7eb80a1c89867eee.png b/static/news_images/news_7eb80a1c89867eee.png new file mode 100644 index 0000000..94d2a5f Binary files /dev/null and b/static/news_images/news_7eb80a1c89867eee.png differ diff --git a/static/news_images/news_803dcbbb4c63853c.jpg b/static/news_images/news_803dcbbb4c63853c.jpg new file mode 100644 index 0000000..78c627b Binary files /dev/null and b/static/news_images/news_803dcbbb4c63853c.jpg differ diff --git a/static/news_images/news_8a1099c2fc08eb97.jpg b/static/news_images/news_8a1099c2fc08eb97.jpg new file mode 100644 index 0000000..6271e9a Binary files /dev/null and b/static/news_images/news_8a1099c2fc08eb97.jpg differ diff --git a/static/news_images/news_8b34874b1dc865a3.jpg b/static/news_images/news_8b34874b1dc865a3.jpg new file mode 100644 index 0000000..41cac5c Binary files /dev/null and b/static/news_images/news_8b34874b1dc865a3.jpg differ diff --git a/static/news_images/news_8c339a287bf1c40c.jpg b/static/news_images/news_8c339a287bf1c40c.jpg new file mode 100644 index 0000000..472bd6a Binary files /dev/null and b/static/news_images/news_8c339a287bf1c40c.jpg differ diff --git a/static/news_images/news_8cbba93f9fee9e89.jpg b/static/news_images/news_8cbba93f9fee9e89.jpg new file mode 100644 index 0000000..1062a90 Binary files /dev/null and b/static/news_images/news_8cbba93f9fee9e89.jpg differ diff --git a/static/news_images/news_979768e1aa88ed41.png b/static/news_images/news_979768e1aa88ed41.png new file mode 100644 index 0000000..3031282 Binary files /dev/null and b/static/news_images/news_979768e1aa88ed41.png differ diff --git a/static/news_images/news_9bc7034fe5ebce8d.jpg b/static/news_images/news_9bc7034fe5ebce8d.jpg new file mode 100644 index 0000000..21d5d7b Binary files /dev/null and b/static/news_images/news_9bc7034fe5ebce8d.jpg differ diff --git a/static/news_images/news_a1b937bd60a333f4.png b/static/news_images/news_a1b937bd60a333f4.png new file mode 100644 index 0000000..f90487c Binary files /dev/null and b/static/news_images/news_a1b937bd60a333f4.png differ diff --git a/static/news_images/news_a3b58810458dcfaa.jpg b/static/news_images/news_a3b58810458dcfaa.jpg new file mode 100644 index 0000000..2167e97 Binary files /dev/null and b/static/news_images/news_a3b58810458dcfaa.jpg differ diff --git a/static/news_images/news_a498b5289fd00a60.jpg b/static/news_images/news_a498b5289fd00a60.jpg new file mode 100644 index 0000000..97b1d34 Binary files /dev/null and b/static/news_images/news_a498b5289fd00a60.jpg differ diff --git a/static/news_images/news_a5daba5586120af0.jpg b/static/news_images/news_a5daba5586120af0.jpg new file mode 100644 index 0000000..f708aa7 Binary files /dev/null and b/static/news_images/news_a5daba5586120af0.jpg differ diff --git a/static/news_images/news_ab8f6e4b258068d7.jpg b/static/news_images/news_ab8f6e4b258068d7.jpg new file mode 100644 index 0000000..236d9af Binary files /dev/null and b/static/news_images/news_ab8f6e4b258068d7.jpg differ diff --git a/static/news_images/news_ac563b280d95f8dc.jpeg b/static/news_images/news_ac563b280d95f8dc.jpeg new file mode 100644 index 0000000..bf86a7d Binary files /dev/null and b/static/news_images/news_ac563b280d95f8dc.jpeg differ diff --git a/static/news_images/news_b049dabde95614c0.png b/static/news_images/news_b049dabde95614c0.png new file mode 100644 index 0000000..c6317ec Binary files /dev/null and b/static/news_images/news_b049dabde95614c0.png differ diff --git a/static/news_images/news_b14a61c1f11d38ab.jpg b/static/news_images/news_b14a61c1f11d38ab.jpg new file mode 100644 index 0000000..3a503fa Binary files /dev/null and b/static/news_images/news_b14a61c1f11d38ab.jpg differ diff --git a/static/news_images/news_bb0dc68e0723f410.jpg b/static/news_images/news_bb0dc68e0723f410.jpg new file mode 100644 index 0000000..1062a90 Binary files /dev/null and b/static/news_images/news_bb0dc68e0723f410.jpg differ diff --git a/static/news_images/news_bc18ae7677845a44.png b/static/news_images/news_bc18ae7677845a44.png new file mode 100644 index 0000000..a211693 Binary files /dev/null and b/static/news_images/news_bc18ae7677845a44.png differ diff --git a/static/news_images/news_c19fc7e7461b15a7.png b/static/news_images/news_c19fc7e7461b15a7.png new file mode 100644 index 0000000..ff63ecf Binary files /dev/null and b/static/news_images/news_c19fc7e7461b15a7.png differ diff --git a/static/news_images/news_c566295d67694894.jpg b/static/news_images/news_c566295d67694894.jpg new file mode 100644 index 0000000..f708aa7 Binary files /dev/null and b/static/news_images/news_c566295d67694894.jpg differ diff --git a/static/news_images/news_c96237b43c079ae8.jpg b/static/news_images/news_c96237b43c079ae8.jpg new file mode 100644 index 0000000..952dc7f Binary files /dev/null and b/static/news_images/news_c96237b43c079ae8.jpg differ diff --git a/static/news_images/news_cc94a202ada1a71f.png b/static/news_images/news_cc94a202ada1a71f.png new file mode 100644 index 0000000..ccd3d11 Binary files /dev/null and b/static/news_images/news_cc94a202ada1a71f.png differ diff --git a/static/news_images/news_cd090df0f852fc15.png b/static/news_images/news_cd090df0f852fc15.png new file mode 100644 index 0000000..2adfb0e Binary files /dev/null and b/static/news_images/news_cd090df0f852fc15.png differ diff --git a/static/news_images/news_ce76b31170383d85.jpg b/static/news_images/news_ce76b31170383d85.jpg new file mode 100644 index 0000000..cfda436 Binary files /dev/null and b/static/news_images/news_ce76b31170383d85.jpg differ diff --git a/static/news_images/news_ce90f1b83a44c9e5.jpg b/static/news_images/news_ce90f1b83a44c9e5.jpg new file mode 100644 index 0000000..fbc3cd9 Binary files /dev/null and b/static/news_images/news_ce90f1b83a44c9e5.jpg differ diff --git a/static/news_images/news_ced61e1dd3f8bc1e.jpg b/static/news_images/news_ced61e1dd3f8bc1e.jpg new file mode 100644 index 0000000..a940386 Binary files /dev/null and b/static/news_images/news_ced61e1dd3f8bc1e.jpg differ diff --git a/static/news_images/news_d064ab6c4b8988ec.jpg b/static/news_images/news_d064ab6c4b8988ec.jpg new file mode 100644 index 0000000..472bd6a Binary files /dev/null and b/static/news_images/news_d064ab6c4b8988ec.jpg differ diff --git a/static/news_images/news_d76ee917bca5de85.jpg b/static/news_images/news_d76ee917bca5de85.jpg new file mode 100644 index 0000000..fa98f59 Binary files /dev/null and b/static/news_images/news_d76ee917bca5de85.jpg differ diff --git a/static/news_images/news_da8f2e0bd2575574.jpg b/static/news_images/news_da8f2e0bd2575574.jpg new file mode 100644 index 0000000..3c6625b Binary files /dev/null and b/static/news_images/news_da8f2e0bd2575574.jpg differ diff --git a/static/news_images/news_de1a11c937affb34.png b/static/news_images/news_de1a11c937affb34.png new file mode 100644 index 0000000..f8f0857 Binary files /dev/null and b/static/news_images/news_de1a11c937affb34.png differ diff --git a/static/news_images/news_e1668c95db99dc92.jpg b/static/news_images/news_e1668c95db99dc92.jpg new file mode 100644 index 0000000..8ac642e Binary files /dev/null and b/static/news_images/news_e1668c95db99dc92.jpg differ diff --git a/static/news_images/news_e2966ce72a9ffdf9.jpg b/static/news_images/news_e2966ce72a9ffdf9.jpg new file mode 100644 index 0000000..78c627b Binary files /dev/null and b/static/news_images/news_e2966ce72a9ffdf9.jpg differ diff --git a/static/news_images/news_e40745bd4a3faf92.jpg b/static/news_images/news_e40745bd4a3faf92.jpg new file mode 100644 index 0000000..236d9af Binary files /dev/null and b/static/news_images/news_e40745bd4a3faf92.jpg differ diff --git a/static/news_images/news_ea8f96842eca600c.png b/static/news_images/news_ea8f96842eca600c.png new file mode 100644 index 0000000..fb7ad79 Binary files /dev/null and b/static/news_images/news_ea8f96842eca600c.png differ diff --git a/static/news_images/news_f0c8f7d7a488eb53.jpg b/static/news_images/news_f0c8f7d7a488eb53.jpg new file mode 100644 index 0000000..cc47e02 Binary files /dev/null and b/static/news_images/news_f0c8f7d7a488eb53.jpg differ diff --git a/static/news_images/news_f32ee003f770a407.jpg b/static/news_images/news_f32ee003f770a407.jpg new file mode 100644 index 0000000..6f8d265 Binary files /dev/null and b/static/news_images/news_f32ee003f770a407.jpg differ diff --git a/static/news_images/news_f3b32df3e892f16e.png b/static/news_images/news_f3b32df3e892f16e.png new file mode 100644 index 0000000..2fd6210 Binary files /dev/null and b/static/news_images/news_f3b32df3e892f16e.png differ diff --git a/static/news_images/news_f54a77eec3ee35af.png b/static/news_images/news_f54a77eec3ee35af.png new file mode 100644 index 0000000..4c047b2 Binary files /dev/null and b/static/news_images/news_f54a77eec3ee35af.png differ diff --git a/static/news_images/news_f6702e5208751de1.png b/static/news_images/news_f6702e5208751de1.png new file mode 100644 index 0000000..a1b45b5 Binary files /dev/null and b/static/news_images/news_f6702e5208751de1.png differ diff --git a/static/news_images/news_f715657b87d18e81.png b/static/news_images/news_f715657b87d18e81.png new file mode 100644 index 0000000..70afb85 Binary files /dev/null and b/static/news_images/news_f715657b87d18e81.png differ diff --git a/static/news_images/news_fc98b4d4b8065e87.jpg b/static/news_images/news_fc98b4d4b8065e87.jpg new file mode 100644 index 0000000..78c627b Binary files /dev/null and b/static/news_images/news_fc98b4d4b8065e87.jpg differ diff --git a/static/news_images/news_fe0ab9a9e228166c.png b/static/news_images/news_fe0ab9a9e228166c.png new file mode 100644 index 0000000..044ffc7 Binary files /dev/null and b/static/news_images/news_fe0ab9a9e228166c.png differ diff --git a/templates/admin.html b/templates/admin.html index 517bc08..af93385 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -106,11 +106,20 @@
- - - + + + +
+
{% for user in admin_users %}
diff --git a/templates/events_article.html b/templates/events_article.html new file mode 100644 index 0000000..3f701ff --- /dev/null +++ b/templates/events_article.html @@ -0,0 +1,115 @@ + + + + + + {{ event.title }} — MONT + + + + + + + {% if event.image %}{% endif %} + + + + + + + + +
+ ← Все мероприятия +
+
+ {% if event.image %} + {{ event.title }} + {% endif %} +
+ 📅 {{ event.event_date[8:10] }}.{{ event.event_date[5:7] }}.{{ event.event_date[:4] }} +

{{ event.title }}

+
+
+
+
+ {% if '<' in event.body %} + {{ event.body | safe }} + {% else %} + {% for paragraph in event.body.split('\n\n') %} +

{{ paragraph }}

+ {% endfor %} + {% endif %} +
+ {% if event.register_url %} + + {% endif %} +
+ По вопросам участия: mont.ru/events +
+
+
+
+ + diff --git a/templates/events_list.html b/templates/events_list.html new file mode 100644 index 0000000..f52970b --- /dev/null +++ b/templates/events_list.html @@ -0,0 +1,129 @@ + + + + + + Мероприятия MONT + + + + + + + + + + + +
+ ← На главную +

Мероприятия MONT

+ + {% if events %} + {% set top3 = events[:3] %} + {% set rest = events[3:] %} + + + + {% if rest %} +
+
Все мероприятия
+ {% for e in rest %} + + {% if e.image %} + {{ e.title }} + {% else %} +
+ {% endif %} +
+
📅 {{ e.event_date[8:10] }}.{{ e.event_date[5:7] }}.{{ e.event_date[:4] }}
+
{{ e.title }}
+

{{ e.body | striptags | truncate(160, true, '...') }}

+
+
+ {% endfor %} + {% endif %} + + {% else %} +
Актуальных мероприятий нет
+ {% endif %} +
+ + diff --git a/templates/index.html b/templates/index.html index 4e27c36..1c27071 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3,17 +3,17 @@ - Вендоры МОНТ — матрица продуктов и категорий дистрибьютора MONT - - + Вендоры MONT — матрица продуктов и категорий дистрибьютора MONT + + - - + + @@ -22,7 +22,7 @@ { "@context": "https://schema.org", "@type": "Organization", - "name": "МОНТ", + "name": "MONT", "alternateName": "MONT", "description": "Дистрибьютор программного обеспечения. Корзина продуктов: вендоры, категории, продуктовые линейки в сфере инфраструктуры и информационной безопасности.", "url": "{{ canonical_url }}", @@ -31,6 +31,7 @@ } + @@ -41,21 +42,21 @@
-

Вендоры в корзине МОНТ

+

Вендоры в портфеле MONT

Актуальная матрица вендоров, продуктов и категорий. Выбирайте вендоров или категории, чтобы видеть релевантные продуктовые линейки в Инфраструктуре и ИБ.

@@ -63,39 +64,108 @@ -
-
-

Вендоры

...
- -
-
+
+
+
+
+

Вендоры

...
+ +
+
-
-

Категории

...
- -
-
-
+
+

Категории

...
+ +
+
+
- + +
-
-
-

Вендоры и продукты (после фильтрации)

-
-
-
-
@@ -224,10 +294,43 @@ } } - document.getElementById('btn-contact-ruslan').addEventListener('click', openModal); + document.getElementById('btn-contact-ruslan').addEventListener('click', () => { window.location.href = 'mailto:ruslan@ipcom.su'; }); overlay.addEventListener('click', e => { if (e.target === overlay) window._closeContact(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') window._closeContact(); }); })(); + + + diff --git a/templates/news_admin.html b/templates/news_admin.html new file mode 100644 index 0000000..7a087f3 --- /dev/null +++ b/templates/news_admin.html @@ -0,0 +1,306 @@ + + + + + + Редактор новостей — MONT + + + + + + + +
+ + {% if not is_news_editor %} + + + {% else %} +
+
+ Редактор новостей MONT
+ {{ admin_login }} +
+
+ +
+ + +
+
+
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% for cat, msg in messages %} +
{{ msg }}
+ {% endfor %} + {% endwith %} + +
+

Добавить новость

+
+ +
+
+ +
+ +
Перетащите файл, выберите или кликните в «Заголовок» и нажмите Ctrl+V
+
+ +
+
+ + +
+ +
+
+ +
+

Все новости

+ {% if all_news %} +
+ {% for n in all_news %} +
+
+
+
{{ n.title }}
+
{{ n.created_at[:10] }} · /news/{{ n.slug }}
+
+
+
+ + + + +
+
+ + + +
+
+
+
+ Редактировать +
+
+ + +
+
+ + {% if n.image %} + + {% endif %} +
+ +
Перетащите файл, выберите или кликните в «Заголовок» и нажмите Ctrl+V
+
+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ {% endfor %} +
+ {% else %} +

Новостей пока нет.

+ {% endif %} + + {% if total_adm_pages > 1 %} + + {% endif %} +
+ {% endif %} + +
+ + + diff --git a/templates/news_article.html b/templates/news_article.html new file mode 100644 index 0000000..cf98c54 --- /dev/null +++ b/templates/news_article.html @@ -0,0 +1,252 @@ + + + + + + {{ article.title }} — MONT + + + + + + + + {% if article.image %}{% endif %} + + + + + + + + +
+ ← На главную + {% if is_news_editor %} + ✏ Редактировать +
+ {% endif %} +
+
+ {% if article.image %} + {{ article.title }} + {% endif %} +
+ +

{{ article.title }}

+
+
+
+
+ {% if '<' in article.body %} + {{ article.body | safe }} + {% else %} + {% for paragraph in article.body.split('\n\n') %} +

{{ paragraph }}

+ {% endfor %} + {% endif %} +
+
+ По вопросам сотрудничества: mshamov@mont.com +
+
+
+
+ +
+
+ + + + + + + diff --git a/templates/news_list.html b/templates/news_list.html new file mode 100644 index 0000000..ebf03ae --- /dev/null +++ b/templates/news_list.html @@ -0,0 +1,191 @@ + + + + + + Новости MONT + + + + + + + + + + + +
+ ← На главную +

Новости MONT

+ + {% if articles %} + + {% if page == 1 %} + {% set top3 = articles[:3] %} + {% set rest = articles[3:] %} + + {% if rest %} +
+
Ещё новости
+ {% for a in rest %} + + {% if a.image %} + {{ a.title }} + {% else %} +
+ {% endif %} +
+
{{ a.created_at[8:10] }}.{{ a.created_at[5:7] }}.{{ a.created_at[:4] }}
+
{{ a.title }}
+

{{ a.body | striptags | truncate(160, true, '...') }}

+
+
+ {% endfor %} + {% endif %} + + {% else %} +
+
Новости — страница {{ page }}
+ {% for a in articles %} + + {% if a.image %} + {{ a.title }} + {% else %} +
+ {% endif %} +
+
{{ a.created_at[8:10] }}.{{ a.created_at[5:7] }}.{{ a.created_at[:4] }}
+
{{ a.title }}
+

{{ a.body | striptags | truncate(160, true, '...') }}

+
+
+ {% endfor %} + {% endif %} + + {% else %} +
Новостей пока нет
+ {% endif %} + + {% if total_pages > 1 %} + + {% endif %} + +
+ + diff --git a/templates/vendor.html b/templates/vendor.html index 6436c9e..775457e 100644 --- a/templates/vendor.html +++ b/templates/vendor.html @@ -3,15 +3,15 @@ - {{ vendor.name }} — вендор МОНТ | матрица продуктов MONT - + {{ vendor.name }} — вендор MONT | матрица продуктов MONT + - - + + {% if vendor.logo %}{% endif %} + + + @@ -127,6 +169,36 @@ } .back-btn:hover { background: #eef4ff; transform: translateX(-2px); } + .seo-block { + background: #fff; + border-radius: 16px; + border: 1px solid #dae6ff; + padding: 24px 28px; + margin-top: 20px; + } + .seo-block h2 { font-size: 17px; font-weight: 800; color: #1a3e79; margin: 0 0 10px; } + .seo-block p { font-size: 14px; color: #3a4f6e; line-height: 1.7; margin: 0 0 10px; } + .seo-block p:last-child { margin-bottom: 0; } + .cities-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; } + .city-tag { + font-size: 12px; font-weight: 600; padding: 4px 12px; + border-radius: 999px; background: #f0f6ff; color: #22427a; + border: 1px solid #c8dcff; + } + .faq-list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 10px; } + .faq-item { border: 1px solid #dae6ff; border-radius: 12px; overflow: hidden; } + .faq-q { + width: 100%; text-align: left; background: #f5f9ff; + border: none; padding: 14px 18px; cursor: pointer; + font-size: 14px; font-weight: 700; color: #1a3e79; + font-family: Manrope, sans-serif; display: flex; justify-content: space-between; align-items: center; + } + .faq-q:hover { background: #eaf2ff; } + .faq-q .arr { transition: transform .2s; font-size: 12px; color: #7a9bd0; } + .faq-q.open .arr { transform: rotate(180deg); } + .faq-a { display: none; padding: 12px 18px 16px; font-size: 14px; color: #3a4f6e; line-height: 1.7; } + .faq-a.open { display: block; } + @media (max-width: 640px) { .vendor-card { flex-direction: column; padding: 20px; } .vendor-logo-box { width: 100%; height: 100px; } @@ -136,10 +208,10 @@
- ← Все вендоры МОНТ + ← Все вендоры MONT
@@ -157,7 +229,7 @@ {% endif %}