Files

1616 lines
58 KiB
Python

from datetime import datetime, timezone
from sqlalchemy import Boolean, DateTime, Float, Integer, String, Text, delete, func, or_, select, text
from sqlalchemy.dialects.mysql import MEDIUMTEXT
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Mapped, mapped_column
from app.db import Base
def _mysql_args() -> dict:
return {"mysql_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}
TECH = "科技蓝"
BRAND = "LOGO蓝"
TEMPLATE_NAMES = (TECH, BRAND)
SLOGAN_EMPTY = "协会定位整理中"
VALUE_EMPTY = "价值主张整理中"
EMPTY_REGIONS = {
"carousel": "轮播内容准备中",
"news": "新闻动态即将发布",
"notices": "暂无新的通知公告",
"policies": "政策法规整理中",
"dashboard": "数据看板整理中",
"audiences": "估价师、机构与公众入口即将开放",
"hotLinks": "报告查询、报告审核与投诉举报入口即将开放",
"honors": "协会荣誉与资质整理中",
"members": "会员单位名录整理中",
"compliance": "隐私政策、使用条款与网站声明整理中",
"annual_reports": "暂无年度报告或工作计划",
"work_briefs": "暂无工作简报",
"qualifications": "暂无资质与荣誉",
"tech_seminars": "暂无技术研讨",
"tech_exchange": "暂无技术交流",
"party_activities": "暂无党建活动",
"appraisal_guides": "暂无估价指南",
"institution_directories": "暂无机构名录",
"policy_analyses": "暂无政策分析",
"case_library": "暂无案例库",
"edu_notices": "暂无教育公告",
"warning_cases": "暂无警示案例",
"integrity_records": "暂无诚信档案",
"discipline_results": "暂无结果公示",
}
HOT_LINK_SLOTS = (
("reportQuery", "报告查询", "/reports/query"),
("reportReview", "报告审核", "/reports/review"),
("complaint", "投诉举报", "/discipline"),
)
FIXED_HOT_URLS = {
"reportQuery": "/reports/query",
"reportReview": "/reports/review",
}
HOT_LINK_KEYS = {key for key, _label, _url in HOT_LINK_SLOTS}
class HomeSlide(Base):
__tablename__ = "home_slide"
__table_args__ = _mysql_args()
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(200), nullable=False, default="")
image_file_id: Mapped[int] = mapped_column(Integer, nullable=False)
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
interval_seconds: Mapped[int] = mapped_column(Integer, nullable=False)
published: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
body_html: Mapped[str] = mapped_column(Text, nullable=False, default="")
class HomeSlideAttachment(Base):
__tablename__ = "home_slide_attachment"
__table_args__ = _mysql_args()
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
slide_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
file_id: Mapped[int] = mapped_column(Integer, nullable=False)
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
HOME_ARTICLE_CATEGORIES = ("news", "notices", "policies", "dashboard")
COLUMN_ARTICLE_CATEGORIES = (
"annual_reports",
"work_briefs",
"qualifications",
"tech_seminars",
"tech_exchange",
"party_activities",
"appraisal_guides",
"institution_directories",
"policy_analyses",
"case_library",
"edu_notices",
"warning_cases",
"integrity_records",
"discipline_results",
)
ARTICLE_CATEGORIES = HOME_ARTICLE_CATEGORIES + COLUMN_ARTICLE_CATEGORIES
def ensure_article_body_capacity(db) -> None:
bind = db.get_bind()
if bind.dialect.name != "mysql":
return
db.execute(text("ALTER TABLE home_article MODIFY body_html MEDIUMTEXT NOT NULL"))
db.commit()
RELATED_CATEGORIES = ("主管部门", "相关部门", "合作协会")
AUDIENCE_NAMES = ("估价师", "估价机构", "公众")
MENU_KEYS = ("member",)
EDU_PLATFORM_EMPTY = "暂无在线教育"
DEFAULT_DISPLAY_COUNT = 8
COLUMN_LIST_PAGE_SIZES = (10, 20, 100)
DEFAULT_COLUMN_PAGE_SIZE = 10
APPRAISAL_GUIDE_KINDS = ("residential", "commercial", "industrial", "specialty")
APPRAISAL_GUIDE_KIND_LABELS = {
"residential": "住宅",
"commercial": "商业",
"industrial": "工业",
"specialty": "特色地产",
}
POLICY_ANALYSIS_KINDS = ("national", "local", "industry", "expert", "digest")
POLICY_ANALYSIS_KIND_LABELS = {
"national": "国家级",
"local": "地方级",
"industry": "行业级",
"expert": "专家解读",
"digest": "政策汇编",
}
POLICY_SEARCH_EMPTY = "未找到相关政策"
class HomeArticleList(Base):
__tablename__ = "home_article_list"
__table_args__ = _mysql_args()
category: Mapped[str] = mapped_column(String(32), primary_key=True)
display_count: Mapped[int] = mapped_column(Integer, nullable=False, default=DEFAULT_DISPLAY_COUNT)
class HomeArticle(Base):
__tablename__ = "home_article"
__table_args__ = _mysql_args()
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
category: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
title: Mapped[str] = mapped_column(String(200), nullable=False, default="")
body_html: Mapped[str] = mapped_column(
Text().with_variant(MEDIUMTEXT(), "mysql"), nullable=False, default=""
)
image_file_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
published: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
published_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
class HomeArticleAttachment(Base):
__tablename__ = "home_article_attachment"
__table_args__ = _mysql_args()
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
article_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
file_id: Mapped[int] = mapped_column(Integer, nullable=False)
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
class HomeArticleKind(Base):
"""栏目文章的附加分类。估价指南与政策分析使用。"""
__tablename__ = "home_article_kind"
__table_args__ = _mysql_args()
article_id: Mapped[int] = mapped_column(Integer, primary_key=True)
kind: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
class HomeAudienceLink(Base):
__tablename__ = "home_audience_link"
__table_args__ = _mysql_args()
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
category: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
label: Mapped[str] = mapped_column(String(200), nullable=False)
url: Mapped[str] = mapped_column(Text, nullable=False, default="")
visible: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
class HomeHotLink(Base):
__tablename__ = "home_hot_link"
__table_args__ = _mysql_args()
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
key: Mapped[str] = mapped_column(String(32), nullable=False, default="", unique=True)
label: Mapped[str] = mapped_column(String(200), nullable=False)
url: Mapped[str] = mapped_column(Text, nullable=False, default="")
visible: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
class HomeMenuItem(Base):
__tablename__ = "home_menu_item"
__table_args__ = _mysql_args()
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
key: Mapped[str] = mapped_column(String(32), unique=True, nullable=False)
label: Mapped[str] = mapped_column(String(200), nullable=False, default="")
url: Mapped[str] = mapped_column(Text, nullable=False, default="")
visible: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
class HomeMarquee(Base):
__tablename__ = "home_marquee"
__table_args__ = _mysql_args()
kind: Mapped[str] = mapped_column(String(16), primary_key=True)
interval_seconds: Mapped[int] = mapped_column(Integer, nullable=False)
class HomeHonor(Base):
__tablename__ = "home_honor"
__table_args__ = _mysql_args()
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
image_file_id: Mapped[int] = mapped_column(Integer, nullable=False)
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
class HomeMember(Base):
__tablename__ = "home_member"
__table_args__ = _mysql_args()
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(200), nullable=False)
url: Mapped[str] = mapped_column(Text, nullable=False, default="")
image_file_id: Mapped[int] = mapped_column(Integer, nullable=False)
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
class EduPlatform(Base):
__tablename__ = "edu_platform"
__table_args__ = _mysql_args()
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(200), nullable=False)
url: Mapped[str] = mapped_column(Text, nullable=False, default="")
image_file_id: Mapped[int] = mapped_column(Integer, nullable=False)
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
class HomeContact(Base):
__tablename__ = "home_contact"
__table_args__ = _mysql_args()
template_name: Mapped[str] = mapped_column(String(32), primary_key=True)
address: Mapped[str] = mapped_column(Text, nullable=False, default="")
phone: Mapped[str] = mapped_column(Text, nullable=False, default="")
email: Mapped[str] = mapped_column(Text, nullable=False, default="")
latitude: Mapped[float | None] = mapped_column(Float, nullable=True)
longitude: Mapped[float | None] = mapped_column(Float, nullable=True)
class HomeRelatedLink(Base):
__tablename__ = "home_related_link"
__table_args__ = _mysql_args()
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
template_name: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
category: Mapped[str] = mapped_column(String(32), nullable=False)
label: Mapped[str] = mapped_column(String(200), nullable=False)
url: Mapped[str] = mapped_column(Text, nullable=False, default="")
visible: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
class HomeComplianceLink(Base):
__tablename__ = "home_compliance_link"
__table_args__ = _mysql_args()
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
template_name: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
label: Mapped[str] = mapped_column(String(200), nullable=False)
url: Mapped[str] = mapped_column(Text, nullable=False, default="")
visible: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
class HomeTemplate(Base):
__tablename__ = "home_template"
__table_args__ = _mysql_args()
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(32), unique=True, nullable=False)
slogan: Mapped[str] = mapped_column(Text, nullable=False, default="")
value_proposition: Mapped[str] = mapped_column(Text, nullable=False, default="")
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
def ensure_home_templates(db) -> None:
existing = set(db.scalars(select(HomeTemplate.name)))
missing = [
HomeTemplate(name=name, slogan="", value_proposition="", is_active=name == TECH)
for name in TEMPLATE_NAMES
if name not in existing
]
if missing:
db.add_all(missing)
try:
db.commit()
except IntegrityError:
db.rollback()
rows = db.scalars(select(HomeTemplate)).all()
if rows and not any(row.is_active for row in rows):
for row in rows:
row.is_active = row.name == TECH
db.commit()
def _text(value: str, empty: str) -> str:
text = "" if value is None else str(value).strip()
return text or empty
def public_home(db) -> dict:
from app.models.content import settings_view
ensure_home_templates(db)
rows = db.scalars(select(HomeTemplate)).all()
active = next((row for row in rows if row.is_active), None)
if active is None and rows:
active = next(row for row in rows if row.name == TECH)
settings = settings_view(db)
payload = {
"templateName": active.name if active else TECH,
"slogan": _text(active.slogan if active else "", SLOGAN_EMPTY),
"valueProposition": _text(active.value_proposition if active else "", VALUE_EMPTY),
"icpText": settings["icp_text"],
"policeText": settings["police_text"],
}
template_name = active.name if active else TECH
for key, sentence in EMPTY_REGIONS.items():
if key == "carousel" or key in HOME_ARTICLE_CATEGORIES or key in COLUMN_ARTICLE_CATEGORIES or key in {
"audiences",
"hotLinks",
"honors",
"members",
"compliance",
}:
continue
payload[key] = {"placeholder": sentence}
payload["carousel"] = _carousel(db)
for key in HOME_ARTICLE_CATEGORIES:
payload[key] = _articles(db, key, EMPTY_REGIONS[key])
payload["audiences"] = _public_audiences(db)
payload["hotLinks"] = _public_hot_links(db)
payload["shortcutMenu"] = _shortcut_menu(db)
payload["honors"] = _public_honors(db)
payload["members"] = _public_members(db)
payload["contact"] = _public_contact(db, template_name)
payload["relatedLinks"] = _public_related(db, template_name)
payload["compliance"] = _public_compliance(db, template_name)
return payload
def _public_file(db, file_id: int):
from app.models.content import AdminFile
if isinstance(file_id, bool) or not isinstance(file_id, int) or file_id <= 0:
return None
row = db.get(AdminFile, file_id)
if row is None or row.bucket != "public" or not row.object_key:
return None
return row
def _carousel(db) -> dict:
from app.services.storage import public_object_url
rows = db.scalars(
select(HomeSlide)
.where(HomeSlide.published.is_(True), HomeSlide.enabled.is_(True))
.order_by(HomeSlide.sort_order.asc(), HomeSlide.id.asc())
).all()
items = []
for row in rows:
image = _public_file(db, row.image_file_id)
if image is None:
continue
items.append(
{
"id": row.id,
"title": row.title,
"imageUrl": public_object_url(image.object_key),
"sortOrder": row.sort_order,
"intervalSeconds": row.interval_seconds,
}
)
if not items:
return {"placeholder": EMPTY_REGIONS["carousel"]}
return {"items": items}
def _prepare_slide(db, payload: dict) -> tuple[dict, list]:
from app.core.config import get_settings
from app.models.content import AdminFile
from app.services.html_filter import filter_html
image = _public_file(db, payload["imageFileId"])
if image is None:
raise ValueError("轮播图必须是已上传的公开图片")
interval = payload["intervalSeconds"]
if isinstance(interval, bool) or not isinstance(interval, int) or interval <= 0:
raise ValueError("间隔秒数必须是正整数")
attachments = []
for file_id in payload["attachmentFileIds"]:
row = _public_file(db, file_id)
if row is None:
raise ValueError("附件必须是已上传的公开文件")
attachments.append(row)
keys = list(db.scalars(select(AdminFile.object_key).where(AdminFile.bucket == "public")))
cleaned = filter_html(payload.get("bodyHtml") or "", list(keys), get_settings().minio_base_url)
fields = {
"title": "" if payload.get("title") is None else str(payload["title"]),
"image_file_id": image.id,
"sort_order": int(payload.get("sortOrder") or 0),
"enabled": bool(payload.get("enabled", True)),
"interval_seconds": interval,
"published": bool(payload.get("published", False)),
"body_html": cleaned,
}
return fields, attachments
def _replace_slide_attachments(db, slide_id: int, attachments: list) -> None:
links = db.scalars(
select(HomeSlideAttachment).where(HomeSlideAttachment.slide_id == slide_id)
).all()
for link in links:
db.delete(link)
db.flush()
for index, row in enumerate(attachments):
db.add(HomeSlideAttachment(slide_id=slide_id, file_id=row.id, position=index))
def create_slide(db, payload: dict) -> int:
fields, attachments = _prepare_slide(db, payload)
slide = HomeSlide(**fields)
db.add(slide)
db.flush()
_replace_slide_attachments(db, slide.id, attachments)
db.commit()
db.refresh(slide)
return slide.id
def update_slide(db, slide_id: int, payload: dict) -> None:
slide = db.get(HomeSlide, slide_id)
if slide is None:
raise ValueError("轮播不存在")
fields, attachments = _prepare_slide(db, payload)
for key, value in fields.items():
setattr(slide, key, value)
_replace_slide_attachments(db, slide.id, attachments)
db.commit()
def public_slide(db, slide_id: int) -> dict | None:
from app.services.storage import public_object_url
row = db.get(HomeSlide, slide_id)
if row is None or not row.published or not row.enabled:
return None
image = _public_file(db, row.image_file_id)
links = db.scalars(
select(HomeSlideAttachment)
.where(HomeSlideAttachment.slide_id == row.id)
.order_by(HomeSlideAttachment.position.asc(), HomeSlideAttachment.id.asc())
).all()
attachments = []
for link in links:
file_row = _public_file(db, link.file_id)
if file_row is None:
continue
attachments.append(
{"filename": file_row.filename, "url": public_object_url(file_row.object_key)}
)
return {
"id": row.id,
"title": row.title,
"bodyHtml": row.body_html,
"imageUrl": public_object_url(image.object_key) if image else "",
"intervalSeconds": row.interval_seconds,
"attachments": attachments,
}
def delete_slide(db, slide_id: int) -> None:
row = db.get(HomeSlide, slide_id)
if row is None:
raise ValueError("轮播不存在")
links = db.scalars(
select(HomeSlideAttachment).where(HomeSlideAttachment.slide_id == row.id)
).all()
for link in links:
db.delete(link)
db.delete(row)
db.commit()
def _display_count(db, category: str) -> int:
row = db.get(HomeArticleList, category)
if row is None or row.display_count <= 0:
return DEFAULT_DISPLAY_COUNT
return row.display_count
def set_article_display(db, limits: dict) -> None:
unknown = [key for key in limits if key not in HOME_ARTICLE_CATEGORIES]
if unknown:
raise ValueError("文章类别不正确")
for category, count in limits.items():
if count is None:
continue
if isinstance(count, bool) or not isinstance(count, int) or count <= 0:
raise ValueError("展示条数必须是正整数")
row = db.get(HomeArticleList, category)
if row is None:
db.add(HomeArticleList(category=category, display_count=count))
else:
row.display_count = count
db.commit()
def _published_at(value) -> datetime | None:
if value is None or value == "":
return None
if not isinstance(value, str):
raise ValueError("发布时间格式不正确")
try:
parsed = datetime.fromisoformat(value)
except ValueError as exc:
raise ValueError("发布时间格式不正确") from exc
if parsed.tzinfo is not None:
parsed = parsed.astimezone(timezone.utc).replace(tzinfo=None)
return parsed
def _articles(db, category: str, placeholder: str, *, limit: int | None = None) -> dict:
from app.services.storage import public_object_url
query = (
select(HomeArticle)
.where(HomeArticle.category == category, HomeArticle.published.is_(True))
.order_by(HomeArticle.published_at.desc(), HomeArticle.id.desc())
)
if limit is None:
query = query.limit(_display_count(db, category))
else:
query = query.limit(limit)
rows = db.scalars(query).all()
items = []
for row in rows:
item = {"id": row.id, "title": row.title}
image = _public_file(db, row.image_file_id) if row.image_file_id else None
if image is not None:
item["imageUrl"] = public_object_url(image.object_key)
items.append(item)
if not items:
return {"placeholder": placeholder}
return {"items": items}
def _like_contains(value: str) -> str:
escaped = value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
return f"%{escaped}%"
def public_column_articles(
db,
category: str,
*,
current: int = 1,
size: int = DEFAULT_COLUMN_PAGE_SIZE,
kind: str | None = None,
q: str | None = None,
) -> dict | None:
if category not in COLUMN_ARTICLE_CATEGORIES:
return None
from app.services.storage import public_object_url
page_size = size if size in COLUMN_LIST_PAGE_SIZES else DEFAULT_COLUMN_PAGE_SIZE
page = current if current >= 1 else 1
placeholder = EMPTY_REGIONS[category]
filters = [HomeArticle.category == category, HomeArticle.published.is_(True)]
kind_filter = (kind or "").strip()
kind_sets = {
"appraisal_guides": APPRAISAL_GUIDE_KINDS,
"policy_analyses": POLICY_ANALYSIS_KINDS,
}
allowed_kinds = kind_sets.get(category)
if allowed_kinds is not None and kind_filter and kind_filter != "all":
if kind_filter not in allowed_kinds:
return None
filters.append(
HomeArticle.id.in_(
select(HomeArticleKind.article_id).where(HomeArticleKind.kind == kind_filter)
)
)
keyword = (q or "").strip()[:100]
if keyword and category == "policy_analyses":
needle = _like_contains(keyword)
filters.append(
or_(
HomeArticle.title.like(needle, escape="\\"),
HomeArticle.body_html.like(needle, escape="\\"),
)
)
total = db.scalar(select(func.count()).select_from(HomeArticle).where(*filters)) or 0
if total == 0:
if keyword and category == "policy_analyses":
placeholder = POLICY_SEARCH_EMPTY
return {"placeholder": placeholder, "items": [], "total": 0, "current": page, "size": page_size}
pages = max(1, (total + page_size - 1) // page_size)
if page > pages:
page = pages
rows = db.scalars(
select(HomeArticle)
.where(*filters)
.order_by(HomeArticle.published_at.desc(), HomeArticle.id.desc())
.offset((page - 1) * page_size)
.limit(page_size)
).all()
kinds = _article_kinds(db, [row.id for row in rows])
items = []
for row in rows:
item = {"id": row.id, "title": row.title}
image = _public_file(db, row.image_file_id) if row.image_file_id else None
if image is not None:
item["imageUrl"] = public_object_url(image.object_key)
article_kind = kinds.get(row.id)
if article_kind:
item["kind"] = article_kind
item["kindLabel"] = _kind_label(category, article_kind)
items.append(item)
return {"items": items, "total": total, "current": page, "size": page_size}
def admin_column_articles(db, category: str) -> dict | None:
if category not in COLUMN_ARTICLE_CATEGORIES:
return None
from app.services.storage import public_object_url
rows = db.scalars(
select(HomeArticle)
.where(HomeArticle.category == category)
.order_by(HomeArticle.published_at.desc(), HomeArticle.id.desc())
).all()
kinds = _article_kinds(db, [row.id for row in rows])
items = []
for row in rows:
image_id, image_url = _editor_image(db, row.image_file_id)
attachments = db.scalars(
select(HomeArticleAttachment)
.where(HomeArticleAttachment.article_id == row.id)
.order_by(HomeArticleAttachment.position.asc(), HomeArticleAttachment.id.asc())
).all()
files = _editor_attachments(db, attachments)
item = {
"id": row.id,
"category": row.category,
"title": row.title,
"publishedAt": row.published_at.isoformat(timespec="seconds") if row.published_at else "",
"published": bool(row.published),
"bodyHtml": row.body_html,
"imageFileId": image_id,
"imageUrl": image_url,
"attachmentFileIds": [item["id"] for item in files],
"attachments": files,
}
article_kind = kinds.get(row.id)
if article_kind:
item["kind"] = article_kind
item["kindLabel"] = _kind_label(category, article_kind)
items.append(item)
return {"category": category, "items": items}
def _kind_label(category: str, kind: str) -> str:
labels = {
"appraisal_guides": APPRAISAL_GUIDE_KIND_LABELS,
"policy_analyses": POLICY_ANALYSIS_KIND_LABELS,
}
return labels.get(category, {}).get(kind, kind)
def _article_kinds(db, article_ids: list[int]) -> dict[int, str]:
if not article_ids:
return {}
rows = db.scalars(select(HomeArticleKind).where(HomeArticleKind.article_id.in_(article_ids))).all()
return {row.article_id: row.kind for row in rows}
def _normalize_article_kind(category: str, payload: dict) -> str | None:
raw = payload.get("kind")
kind = "" if raw is None else str(raw).strip()
if category == "appraisal_guides":
if kind not in APPRAISAL_GUIDE_KINDS:
raise ValueError("估价指南分类不正确")
return kind
if category == "policy_analyses":
if kind not in POLICY_ANALYSIS_KINDS:
raise ValueError("政策分类不正确")
return kind
if raw not in (None, ""):
raise ValueError("该类文章不支持分类")
return None
def _set_article_kind(db, article_id: int, kind: str | None) -> None:
existing = db.get(HomeArticleKind, article_id)
if kind is None:
if existing is not None:
db.delete(existing)
return
if existing is None:
db.add(HomeArticleKind(article_id=article_id, kind=kind))
return
existing.kind = kind
def _prepare_article(db, payload: dict) -> tuple[dict, list, str | None]:
from app.core.config import get_settings
from app.models.content import AdminFile
from app.services.html_filter import filter_html
category = payload.get("category")
if category not in ARTICLE_CATEGORIES:
raise ValueError("文章类别不正确")
kind = _normalize_article_kind(category, payload)
image_id = payload.get("imageFileId")
image = None
if image_id is not None:
image = _public_file(db, image_id)
if image is None:
raise ValueError("配图必须是已上传的公开图片")
attachments = []
for file_id in payload.get("attachmentFileIds") or []:
row = _public_file(db, file_id)
if row is None:
raise ValueError("附件必须是已上传的公开文件")
attachments.append(row)
keys = list(db.scalars(select(AdminFile.object_key).where(AdminFile.bucket == "public")))
cleaned = filter_html(payload.get("bodyHtml") or "", list(keys), get_settings().minio_base_url)
fields = {
"category": category,
"title": "" if payload.get("title") is None else str(payload["title"]),
"body_html": cleaned,
"image_file_id": image.id if image else None,
"published": bool(payload.get("published", False)),
"published_at": _published_at(payload.get("publishedAt")),
}
return fields, attachments, kind
def _replace_article_attachments(db, article_id: int, attachments: list) -> None:
links = db.scalars(
select(HomeArticleAttachment).where(HomeArticleAttachment.article_id == article_id)
).all()
for link in links:
db.delete(link)
db.flush()
for index, row in enumerate(attachments):
db.add(HomeArticleAttachment(article_id=article_id, file_id=row.id, position=index))
def create_article(db, payload: dict) -> int:
fields, attachments, kind = _prepare_article(db, payload)
article = HomeArticle(**fields)
db.add(article)
db.flush()
_replace_article_attachments(db, article.id, attachments)
_set_article_kind(db, article.id, kind)
db.commit()
db.refresh(article)
return article.id
def update_article(db, article_id: int, payload: dict) -> None:
article = db.get(HomeArticle, article_id)
if article is None:
raise ValueError("文章不存在")
fields, attachments, kind = _prepare_article(db, payload)
for key, value in fields.items():
setattr(article, key, value)
_replace_article_attachments(db, article.id, attachments)
_set_article_kind(db, article.id, kind)
db.commit()
def public_article(db, article_id: int) -> dict | None:
from app.services.storage import public_object_url
row = db.get(HomeArticle, article_id)
if row is None or not row.published:
return None
image = _public_file(db, row.image_file_id) if row.image_file_id else None
links = db.scalars(
select(HomeArticleAttachment)
.where(HomeArticleAttachment.article_id == row.id)
.order_by(HomeArticleAttachment.position.asc(), HomeArticleAttachment.id.asc())
).all()
attachments = []
for link in links:
file_row = _public_file(db, link.file_id)
if file_row is None:
continue
attachments.append(
{"filename": file_row.filename, "url": public_object_url(file_row.object_key)}
)
body = {
"id": row.id,
"category": row.category,
"title": row.title,
"bodyHtml": row.body_html,
"attachments": attachments,
}
if image is not None:
body["imageUrl"] = public_object_url(image.object_key)
kinds = _article_kinds(db, [row.id])
article_kind = kinds.get(row.id)
if article_kind:
body["kind"] = article_kind
body["kindLabel"] = APPRAISAL_GUIDE_KIND_LABELS.get(article_kind, article_kind)
return body
def delete_article(db, article_id: int) -> None:
row = db.get(HomeArticle, article_id)
if row is None:
raise ValueError("文章不存在")
links = db.scalars(
select(HomeArticleAttachment).where(HomeArticleAttachment.article_id == row.id)
).all()
for link in links:
db.delete(link)
kind_row = db.get(HomeArticleKind, row.id)
if kind_row is not None:
db.delete(kind_row)
db.delete(row)
db.commit()
def _link_label(link: dict) -> str:
label = link.get("label")
if not isinstance(label, str) or not label.strip():
raise ValueError("链接文字不能为空")
return label.strip()
def _link_url(value) -> str:
from app.services.html_filter import is_javascript_url
if value is None:
return ""
if not isinstance(value, str):
raise ValueError("链接地址不正确")
text = value.strip()
if is_javascript_url(text):
raise ValueError("链接地址不正确")
return text
def _sort_order(link: dict) -> int:
value = link.get("sortOrder", 0)
if isinstance(value, bool) or not isinstance(value, int):
raise ValueError("顺序必须是整数")
return value
def _navigation(url: str) -> str:
if not url:
return "none"
if url.startswith("/") and not url.startswith("//"):
return "sameWindow"
return "newWindow"
def _public_link(row) -> dict:
return {"label": row.label, "url": row.url, "navigation": _navigation(row.url)}
def _public_audiences(db) -> dict:
rows = db.scalars(
select(HomeAudienceLink)
.where(HomeAudienceLink.visible.is_(True))
.order_by(HomeAudienceLink.sort_order.asc(), HomeAudienceLink.id.asc())
).all()
if not rows:
return {"placeholder": EMPTY_REGIONS["audiences"]}
grouped = {name: [] for name in AUDIENCE_NAMES}
for row in rows:
if row.category in grouped:
grouped[row.category].append(_public_link(row))
return {
"categories": [{"name": name, "links": grouped[name]} for name in AUDIENCE_NAMES]
}
def _public_hot_link(row: HomeHotLink) -> dict:
url = FIXED_HOT_URLS.get(row.key, row.url)
return {"label": row.label, "url": url, "navigation": _navigation(url)}
def _public_hot_links(db) -> dict:
rows = db.scalars(
select(HomeHotLink)
.where(HomeHotLink.visible.is_(True))
.order_by(HomeHotLink.sort_order.asc(), HomeHotLink.id.asc())
).all()
if not rows:
return {"placeholder": EMPTY_REGIONS["hotLinks"]}
return {"links": [_public_hot_link(row) for row in rows]}
def _shortcut_menu(db) -> dict:
rows = db.scalars(
select(HomeMenuItem)
.where(HomeMenuItem.visible.is_(True), HomeMenuItem.key.in_(MENU_KEYS))
.order_by(HomeMenuItem.sort_order.asc(), HomeMenuItem.id.asc())
).all()
items = [{"label": "首页", "url": "/", "navigation": "sameWindow"}]
items.extend(_public_link(row) for row in rows)
return {"items": items}
def save_audiences(db, categories: list[dict]) -> None:
names = [item.get("name") for item in categories]
if names != list(AUDIENCE_NAMES):
raise ValueError("服务对象类别必须是估价师、估价机构、公众")
prepared: list[HomeAudienceLink] = []
for category in categories:
for link in category.get("links") or []:
prepared.append(
HomeAudienceLink(
category=category["name"],
label=_link_label(link),
url=_link_url(link.get("url")),
visible=bool(link.get("visible", True)),
sort_order=_sort_order(link),
)
)
db.execute(delete(HomeAudienceLink))
db.add_all(prepared)
db.commit()
def save_hot_links(db, links: list[dict]) -> None:
if len(links) != len(HOT_LINK_SLOTS):
raise ValueError("高频功能必须是报告查询、报告审核和投诉举报")
seen: set[str] = set()
prepared: list[HomeHotLink] = []
for link in links:
key = link.get("key")
if key not in HOT_LINK_KEYS or key in seen:
raise ValueError("高频功能必须是报告查询、报告审核和投诉举报")
seen.add(key)
url = FIXED_HOT_URLS.get(key)
if url is None:
url = _link_url(link.get("url"))
prepared.append(
HomeHotLink(
key=key,
label=_link_label(link),
url=url,
visible=bool(link.get("visible", True)),
sort_order=_sort_order(link),
)
)
db.execute(delete(HomeHotLink))
db.add_all(prepared)
db.commit()
def ensure_hot_link_slots(db) -> None:
bind = db.get_bind()
if bind.dialect.name == "mysql":
from sqlalchemy import inspect
columns = {column["name"] for column in inspect(bind).get_columns("home_hot_link")}
if "key" not in columns:
db.execute(text("ALTER TABLE home_hot_link ADD COLUMN `key` VARCHAR(32) NOT NULL DEFAULT ''"))
db.commit()
label_to_key = {label: key for key, label, _url in HOT_LINK_SLOTS}
rows = db.scalars(select(HomeHotLink).order_by(HomeHotLink.sort_order.asc(), HomeHotLink.id.asc())).all()
used: set[str] = set()
for row in rows:
if row.key in HOT_LINK_KEYS:
if row.key in used:
db.delete(row)
continue
used.add(row.key)
if row.key in FIXED_HOT_URLS:
row.url = FIXED_HOT_URLS[row.key]
continue
mapped = label_to_key.get(row.label)
if mapped and mapped not in used:
row.key = mapped
if mapped in FIXED_HOT_URLS:
row.url = FIXED_HOT_URLS[mapped]
used.add(mapped)
continue
db.delete(row)
db.commit()
if bind.dialect.name != "mysql":
return
from sqlalchemy import inspect
indexes = inspect(bind).get_indexes("home_hot_link")
has_unique = any(item.get("unique") and "key" in (item.get("column_names") or []) for item in indexes)
if not has_unique:
db.execute(text("ALTER TABLE home_hot_link ADD UNIQUE INDEX uq_home_hot_link_key (`key`)"))
db.commit()
def save_menu(db, items: list[dict]) -> None:
seen: set[str] = set()
for item in items:
key = item.get("key")
if key not in MENU_KEYS or key in seen:
raise ValueError("快捷菜单只能配置会员服务")
seen.add(key)
row = db.scalar(select(HomeMenuItem).where(HomeMenuItem.key == key))
if row is None:
row = HomeMenuItem(key=key)
db.add(row)
row.label = _link_label(item)
row.url = _link_url(item.get("url"))
row.visible = bool(item.get("visible", False))
row.sort_order = _sort_order(item)
db.commit()
def retire_removed_shortcut_keys(db) -> None:
db.execute(delete(HomeMenuItem).where(HomeMenuItem.key.notin_(MENU_KEYS)))
db.commit()
def _interval(value) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
raise ValueError("间隔秒数必须是正整数")
return value
def _image_id(db, file_id, message: str) -> int:
image = _public_file(db, file_id)
if image is None:
raise ValueError(message)
return image.id
def _store_interval(db, kind: str, seconds: int) -> None:
row = db.get(HomeMarquee, kind)
if row is None:
db.add(HomeMarquee(kind=kind, interval_seconds=seconds))
else:
row.interval_seconds = seconds
def _public_honors(db) -> dict:
from app.services.storage import public_object_url
rows = db.scalars(select(HomeHonor).order_by(HomeHonor.sort_order.asc(), HomeHonor.id.asc())).all()
items = []
for row in rows:
image = _public_file(db, row.image_file_id)
if image is None:
continue
items.append({"imageUrl": public_object_url(image.object_key)})
if not items:
return {"placeholder": EMPTY_REGIONS["honors"]}
setting = db.get(HomeMarquee, "honors")
return {"intervalSeconds": setting.interval_seconds if setting else 0, "items": items}
def _public_members(db) -> dict:
from app.services.storage import public_object_url
rows = db.scalars(
select(HomeMember).order_by(HomeMember.sort_order.asc(), HomeMember.id.asc())
).all()
items = []
for row in rows:
image = _public_file(db, row.image_file_id)
if image is None:
continue
items.append(
{
"name": row.name,
"url": row.url,
"navigation": _navigation(row.url),
"imageUrl": public_object_url(image.object_key),
}
)
if not items:
return {"placeholder": EMPTY_REGIONS["members"]}
setting = db.get(HomeMarquee, "members")
return {"intervalSeconds": setting.interval_seconds if setting else 0, "items": items}
def save_honors(db, interval_seconds: int, items: list[dict]) -> None:
seconds = _interval(interval_seconds)
prepared = [
HomeHonor(
image_file_id=_image_id(db, item.get("imageFileId"), "荣誉图片必须是已上传的公开图片"),
sort_order=_sort_order(item),
)
for item in items
]
db.execute(delete(HomeHonor))
db.add_all(prepared)
_store_interval(db, "honors", seconds)
db.commit()
def save_members(db, interval_seconds: int, items: list[dict]) -> None:
seconds = _interval(interval_seconds)
prepared = []
for item in items:
name = item.get("name")
if not isinstance(name, str) or not name.strip():
raise ValueError("会员单位名称不能为空")
prepared.append(
HomeMember(
name=name.strip(),
url=_link_url(item.get("url")),
image_file_id=_image_id(db, item.get("imageFileId"), "会员标志必须是已上传的公开图片"),
sort_order=_sort_order(item),
)
)
db.execute(delete(HomeMember))
db.add_all(prepared)
_store_interval(db, "members", seconds)
db.commit()
def _edu_site_name(item: dict) -> str:
name = item.get("name")
if not isinstance(name, str) or not name.strip():
raise ValueError("教育网站名称不能为空")
return name.strip()
def _edu_site_url(value) -> str:
text = _link_url(value)
if not text:
raise ValueError("教育网站地址不能为空")
if not text.lower().startswith(("http://", "https://")):
raise ValueError("教育网站地址不正确")
return text
def public_edu_platforms(db) -> dict:
from app.services.storage import public_object_url
rows = db.scalars(
select(EduPlatform).order_by(EduPlatform.sort_order.asc(), EduPlatform.id.asc())
).all()
items = []
for row in rows:
image = _public_file(db, row.image_file_id)
if image is None:
continue
items.append(
{
"name": row.name,
"url": row.url,
"navigation": "newWindow",
"imageUrl": public_object_url(image.object_key),
}
)
if not items:
return {"placeholder": EDU_PLATFORM_EMPTY}
return {"items": items}
def admin_edu_platforms(db) -> dict:
rows = db.scalars(
select(EduPlatform).order_by(EduPlatform.sort_order.asc(), EduPlatform.id.asc())
).all()
items = []
for row in rows:
image_id, image_url = _editor_image(db, row.image_file_id)
items.append(
{
"name": row.name,
"url": row.url,
"imageFileId": image_id,
"imageUrl": image_url,
"sortOrder": row.sort_order,
}
)
return {"items": items}
def save_edu_platforms(db, items: list[dict]) -> None:
prepared = [
EduPlatform(
name=_edu_site_name(item),
url=_edu_site_url(item.get("url")),
image_file_id=_image_id(db, item.get("imageFileId"), "教育网站图片必须是已上传的公开图片"),
sort_order=_sort_order(item),
)
for item in items
]
db.execute(delete(EduPlatform))
db.add_all(prepared)
db.commit()
def _coordinate(value, low: float, high: float, message: str) -> float | None:
if value is None or value == "":
return None
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise ValueError(message)
number = float(value)
if number < low or number > high:
raise ValueError(message)
return number
def _coordinate_pair(latitude, longitude) -> tuple[float | None, float | None]:
lat = _coordinate(latitude, -90, 90, "纬度不正确")
lng = _coordinate(longitude, -180, 180, "经度不正确")
if (lat is None) != (lng is None):
raise ValueError("纬度和经度需要同时填写")
return lat, lng
def staff_map_picker() -> dict | None:
from app.core.config import get_settings
settings = get_settings()
key = settings.gaode_key.strip()
secret = settings.gaode_security_js_code.strip()
if not key or not secret:
return None
return {"key": key, "securityJsCode": secret}
def _public_map(row) -> str | dict:
from app.core.config import get_settings
settings = get_settings()
key = settings.gaode_key.strip()
secret = settings.gaode_security_js_code.strip()
if row is None or row.latitude is None or row.longitude is None or not key or not secret:
return "地图位置整理中"
return {
"latitude": float(row.latitude),
"longitude": float(row.longitude),
"key": key,
"securityJsCode": secret,
}
def _public_contact(db, template_name: str) -> dict:
row = db.get(HomeContact, template_name)
return {
"address": _text(row.address if row else "", "办公地址整理中"),
"phone": _text(row.phone if row else "", "联系电话整理中"),
"email": _text(row.email if row else "", "联系邮箱整理中"),
"map": _public_map(row),
}
def _visible_links(db, model, template_name: str, category: str | None = None) -> list:
stmt = select(model).where(model.template_name == template_name, model.visible.is_(True))
if category is not None:
stmt = stmt.where(model.category == category)
stmt = stmt.order_by(model.sort_order.asc(), model.id.asc())
return list(db.scalars(stmt).all())
def _public_related(db, template_name: str) -> dict:
result = {}
for category in RELATED_CATEGORIES:
rows = _visible_links(db, HomeRelatedLink, template_name, category)
if not rows:
result[category] = {"placeholder": f"{category}链接整理中"}
else:
result[category] = {"links": [_public_link(row) for row in rows]}
return result
def _public_compliance(db, template_name: str) -> dict:
# 页脚固定三项,正文由 legal_page 管理;不再使用外链列表占位。
del db, template_name
from app.models.legal import public_compliance_links
return public_compliance_links()
def _template_names(items: list[dict]) -> None:
names = [item.get("name") for item in items]
if sorted(names) != sorted(TEMPLATE_NAMES):
raise ValueError("模板必须是科技蓝和LOGO蓝")
def save_contact(db, templates: list[dict]) -> None:
_template_names(templates)
for item in templates:
latitude, longitude = _coordinate_pair(item.get("latitude"), item.get("longitude"))
row = db.get(HomeContact, item["name"])
if row is None:
row = HomeContact(template_name=item["name"])
db.add(row)
row.address = "" if item.get("address") is None else str(item.get("address"))
row.phone = "" if item.get("phone") is None else str(item.get("phone"))
row.email = "" if item.get("email") is None else str(item.get("email"))
row.latitude = latitude
row.longitude = longitude
db.commit()
def _prepared_links(db, model, template_name: str, links: list[dict], category: str | None = None):
prepared = []
for link in links:
values = {
"template_name": template_name,
"label": _link_label(link),
"url": _link_url(link.get("url")),
"visible": bool(link.get("visible", True)),
"sort_order": _sort_order(link),
}
if category is not None:
values["category"] = category
prepared.append(model(**values))
return prepared
def save_related_links(db, template_name: str, categories: list[dict]) -> None:
if template_name not in TEMPLATE_NAMES:
raise ValueError("模板必须是科技蓝和LOGO蓝")
names = [item.get("name") for item in categories]
if names != list(RELATED_CATEGORIES):
raise ValueError("相关链接类别必须是主管部门、相关部门、合作协会")
prepared = []
for category in categories:
prepared.extend(
_prepared_links(db, HomeRelatedLink, template_name, category.get("links") or [], category["name"])
)
db.execute(delete(HomeRelatedLink).where(HomeRelatedLink.template_name == template_name))
db.add_all(prepared)
db.commit()
def save_compliance(db, template_name: str, links: list[dict]) -> None:
if template_name not in TEMPLATE_NAMES:
raise ValueError("模板必须是科技蓝和LOGO蓝")
prepared = _prepared_links(db, HomeComplianceLink, template_name, links)
db.execute(delete(HomeComplianceLink).where(HomeComplianceLink.template_name == template_name))
db.add_all(prepared)
db.commit()
def save_home(db, active_name: str, templates: list[dict]) -> None:
ensure_home_templates(db)
names = [item["name"] for item in templates]
if active_name not in TEMPLATE_NAMES or sorted(names) != sorted(TEMPLATE_NAMES):
raise ValueError("模板必须是科技蓝和LOGO蓝")
by_name = {item["name"]: item for item in templates}
rows = db.scalars(select(HomeTemplate)).all()
found = {row.name: row for row in rows}
for name in TEMPLATE_NAMES:
row = found[name]
incoming = by_name[name]
row.slogan = "" if incoming["slogan"] is None else str(incoming["slogan"])
row.value_proposition = (
"" if incoming["valueProposition"] is None else str(incoming["valueProposition"])
)
row.is_active = name == active_name
db.commit()
def _editor_attachments(db, links) -> list[dict]:
from app.services.storage import public_object_url
items = []
for link in links:
row = _public_file(db, link.file_id)
if row is None:
continue
items.append(
{
"id": row.id,
"filename": row.filename,
"url": public_object_url(row.object_key),
}
)
return items
def _editor_image(db, file_id: int | None) -> tuple[int | None, str]:
from app.services.storage import public_object_url
row = _public_file(db, file_id) if file_id else None
if row is None:
return None, ""
return row.id, public_object_url(row.object_key)
def _link_editor(row) -> dict:
return {
"label": row.label,
"url": row.url,
"visible": bool(row.visible),
"sortOrder": row.sort_order,
}
def _hot_link_editor(rows: list[HomeHotLink]) -> list[dict]:
present: list[dict] = []
seen: set[str] = set()
for row in rows:
if row.key not in HOT_LINK_KEYS or row.key in seen:
continue
seen.add(row.key)
present.append(
{
"key": row.key,
"label": row.label,
"url": FIXED_HOT_URLS.get(row.key, row.url),
"visible": bool(row.visible),
"sortOrder": row.sort_order,
"urlLocked": row.key in FIXED_HOT_URLS,
}
)
for index, (key, label, url) in enumerate(HOT_LINK_SLOTS):
if key in seen:
continue
present.append(
{
"key": key,
"label": label,
"url": url,
"visible": False,
"sortOrder": index,
"urlLocked": key in FIXED_HOT_URLS,
}
)
return present
def admin_home_view(db) -> dict:
ensure_home_templates(db)
templates = db.scalars(select(HomeTemplate).order_by(HomeTemplate.id.asc())).all()
active = next((row.name for row in templates if row.is_active), TECH)
slides = []
for row in db.scalars(select(HomeSlide).order_by(HomeSlide.sort_order.asc(), HomeSlide.id.asc())).all():
image_id, image_url = _editor_image(db, row.image_file_id)
attachments = db.scalars(
select(HomeSlideAttachment)
.where(HomeSlideAttachment.slide_id == row.id)
.order_by(HomeSlideAttachment.position.asc(), HomeSlideAttachment.id.asc())
).all()
files = _editor_attachments(db, attachments)
slides.append(
{
"id": row.id,
"title": row.title,
"imageFileId": image_id,
"imageUrl": image_url,
"sortOrder": row.sort_order,
"enabled": bool(row.enabled),
"intervalSeconds": row.interval_seconds,
"published": bool(row.published),
"bodyHtml": row.body_html,
"attachmentFileIds": [item["id"] for item in files],
"attachments": files,
}
)
articles = []
for row in db.scalars(
select(HomeArticle)
.where(HomeArticle.category.in_(HOME_ARTICLE_CATEGORIES))
.order_by(HomeArticle.published_at.desc(), HomeArticle.id.desc())
).all():
image_id, image_url = _editor_image(db, row.image_file_id)
attachments = db.scalars(
select(HomeArticleAttachment)
.where(HomeArticleAttachment.article_id == row.id)
.order_by(HomeArticleAttachment.position.asc(), HomeArticleAttachment.id.asc())
).all()
files = _editor_attachments(db, attachments)
articles.append(
{
"id": row.id,
"category": row.category,
"title": row.title,
"publishedAt": row.published_at.isoformat(timespec="seconds") if row.published_at else "",
"published": bool(row.published),
"bodyHtml": row.body_html,
"imageFileId": image_id,
"imageUrl": image_url,
"attachmentFileIds": [item["id"] for item in files],
"attachments": files,
}
)
audience_rows = db.scalars(
select(HomeAudienceLink).order_by(HomeAudienceLink.sort_order.asc(), HomeAudienceLink.id.asc())
).all()
grouped = {name: [] for name in AUDIENCE_NAMES}
for row in audience_rows:
if row.category in grouped:
grouped[row.category].append(_link_editor(row))
hot_rows = db.scalars(select(HomeHotLink).order_by(HomeHotLink.sort_order.asc(), HomeHotLink.id.asc())).all()
menu_rows = db.scalars(select(HomeMenuItem).order_by(HomeMenuItem.sort_order.asc(), HomeMenuItem.id.asc())).all()
honors = db.scalars(select(HomeHonor).order_by(HomeHonor.sort_order.asc(), HomeHonor.id.asc())).all()
members = db.scalars(select(HomeMember).order_by(HomeMember.sort_order.asc(), HomeMember.id.asc())).all()
honor_setting = db.get(HomeMarquee, "honors")
member_setting = db.get(HomeMarquee, "members")
contacts = []
for name in TEMPLATE_NAMES:
row = db.get(HomeContact, name)
contacts.append(
{
"name": name,
"address": row.address if row else "",
"phone": row.phone if row else "",
"email": row.email if row else "",
"latitude": float(row.latitude) if row and row.latitude is not None else None,
"longitude": float(row.longitude) if row and row.longitude is not None else None,
}
)
related = []
compliance = []
for name in TEMPLATE_NAMES:
categories = []
for category in RELATED_CATEGORIES:
rows = db.scalars(
select(HomeRelatedLink)
.where(HomeRelatedLink.template_name == name, HomeRelatedLink.category == category)
.order_by(HomeRelatedLink.sort_order.asc(), HomeRelatedLink.id.asc())
).all()
categories.append({"name": category, "links": [_link_editor(item) for item in rows]})
related.append({"templateName": name, "categories": categories})
rows = db.scalars(
select(HomeComplianceLink)
.where(HomeComplianceLink.template_name == name)
.order_by(HomeComplianceLink.sort_order.asc(), HomeComplianceLink.id.asc())
).all()
compliance.append({"templateName": name, "links": [_link_editor(item) for item in rows]})
return {
"activeTemplate": active,
"templates": [
{
"name": row.name,
"slogan": row.slogan,
"valueProposition": row.value_proposition,
}
for row in templates
if row.name in TEMPLATE_NAMES
],
"slides": slides,
"articleDisplay": {key: _display_count(db, key) for key in HOME_ARTICLE_CATEGORIES},
"articles": articles,
"audiences": {
"categories": [{"name": name, "links": grouped[name]} for name in AUDIENCE_NAMES]
},
"hotLinks": {"links": _hot_link_editor(hot_rows)},
"menu": {
"items": [
{
"key": row.key,
"label": row.label,
"url": row.url,
"visible": bool(row.visible),
"sortOrder": row.sort_order,
}
for row in menu_rows
if row.key in MENU_KEYS
]
},
"honors": {
"intervalSeconds": honor_setting.interval_seconds if honor_setting else 8,
"items": [
{
"imageFileId": _editor_image(db, row.image_file_id)[0],
"imageUrl": _editor_image(db, row.image_file_id)[1],
"sortOrder": row.sort_order,
}
for row in honors
],
},
"members": {
"intervalSeconds": member_setting.interval_seconds if member_setting else 8,
"items": [
{
"name": row.name,
"url": row.url,
"imageFileId": _editor_image(db, row.image_file_id)[0],
"imageUrl": _editor_image(db, row.image_file_id)[1],
"sortOrder": row.sort_order,
}
for row in members
],
},
"contacts": contacts,
"relatedLinks": related,
"compliance": compliance,
"mapPicker": staff_map_picker(),
}