refactor: prefix every table with its module

Two tables were named after the concept they came from rather than the module
they belong to, so the schema read as if the template tables were part of the
paper module. Renamed (data preserved, `RENAME TABLE` moves rows in place):

    paper_template  -> template                 the 模板 module
    section_field   -> template_field_library   the 字段库 the 模板 module owns

The paper tables and `template_field` already followed the rule. The rename
carries through everything that named a module:

  models   Template, TemplateField, TemplateFieldLibrary
  schemas  Template*, TemplateFieldLibrary*
  crud     app/crud/template.py, app/crud/template_field_library.py
  API      /template-field-library (was /section-fields); handlers are now
           named after library entries, which removes the ambiguity with
           TemplateField — a placement, a different thing entirely
  client   src/api/templateFieldLibrary.ts

`paper_template_filed_sort` is deliberately untouched: it is a column of the
paper module, spelled as the feature was specified.

TiDB v8.5 with tidb_enable_foreign_key on — as this cluster runs — enforces
foreign keys rather than ignoring them, so the docs' "TiDB does not enforce
foreign keys" was wrong. Corrected, with what actually follows from it: the
rename was rehearsed (RENAME TABLE carries a referencing constraint along), the
API keeps checking first so a violation names the row instead of surfacing a
driver error, and the ORM cascades stay so behaviour does not depend on a
cluster setting.

Revision f27a1c6d9e04 verified both ways; 40 smoke checks, type-check and build
all pass.
This commit is contained in:
2026-09-18 17:48:11 +08:00
parent 2d113f9f6b
commit a5f884f440
25 changed files with 371 additions and 260 deletions
@@ -0,0 +1,82 @@
"""The reusable template-field library (字段库).
A *library entry* is one heading a paper can contain — "1. Introduction",
"2.1 Dataset", "0 Abstract". Fields live in this one global library and are
never owned by a template.
Why there is no ``parent_id``
-----------------------------
A tree would make a field usable under exactly one parent, so a level-2 heading
such as "Background" could not sit under both "1. Introduction" and
"2. Related Work". Hierarchy is therefore expressed only by
:attr:`TemplateFieldLibrary.level` (1, 2, 3 ...), which is a *rendering hint* — it
drives indentation and numbering semantics in the UI — while a field stays
free to be attached to any number of templates and any number of parents
within them.
The number that the reader sees is part of :attr:`name` and is written by the
user ("1. Introduction", "0 Abstract"). Nothing derives or rewrites it.
Ordering inside a template is *not* stored here: it lives in
``template_field.sort``. This table has no ``sort`` column on purpose, so that
one library field can occupy a different position in every template.
Naming: the table is ``template_field_library`` and one row is one entry of it
— the class is named after the table, as everywhere else in this package. The
library is global, but it belongs to the 模板 module: it is the raw material a
template is built from, and it is managed from the same menu.
"""
from decimal import Decimal
from sqlalchemy import Integer, Numeric, String, text
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
from app.models.mixins import TimestampMixin
class TemplateFieldLibrary(TimestampMixin, Base):
"""A single reusable heading, with the typography it should render in."""
__tablename__ = "template_field_library"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
#: Display name, numbering included. Stored and rendered verbatim.
name: Mapped[str] = mapped_column(String(255), nullable=False)
#: Heading depth: 1 for "1.", 2 for "1.1", 3 for "1.1.1". Rendering hint
#: only — it does not link fields to each other.
level: Mapped[int] = mapped_column(
Integer,
nullable=False,
default=1,
server_default=text("1"),
)
#: Font size in points. ``Numeric`` rather than ``Integer`` because the
#: conventional Chinese sizes are fractional (五号 = 10.5pt,
#: 小四 = 12pt).
font_size: Mapped[Decimal] = mapped_column(
Numeric(4, 1),
nullable=False,
default=Decimal("12.0"),
server_default=text("12.0"),
)
#: Font colour as ``#RRGGBB`` — the canonical RGB encoding. The API
#: normalises ``rgb(20, 30, 40)``, ``#abc`` and bare ``aabbcc`` to it on
#: write, so the column always holds one comparable format.
font_color: Mapped[str] = mapped_column(
String(7),
nullable=False,
default="#000000",
server_default=text("'#000000'"),
)
def __repr__(self) -> str: # pragma: no cover - debugging aid
return (
f"<TemplateFieldLibrary id={self.id} name={self.name!r} "
f"level={self.level} color={self.font_color}>"
)