printer_location.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. """Printer locations (groups) and their appearance (#2962)."""
  2. from pydantic import BaseModel, Field, field_validator
  3. from backend.app.schemas.not_null import IgnoresNullForRequiredColumns
  4. # printers.location, print_queue.target_location and printer_locations.name are
  5. # all VARCHAR(100). PostgreSQL refuses a longer value with an error mid-write,
  6. # so it is refused here instead.
  7. LOCATION_NAME_MAX_LENGTH = 100
  8. _ICON_PATTERN = r"^[a-z0-9-]{1,50}$"
  9. _COLOR_PATTERN = r"^#[0-9a-fA-F]{6}$"
  10. def normalize_location_name(value: str | None) -> str | None:
  11. """A location as stored: trimmed, and None for blank (no location).
  12. "" and None both meant "no location" before this existed; folding them into
  13. one value keeps the exact-match filters from treating them as two groups.
  14. """
  15. if value is None:
  16. return None
  17. if not isinstance(value, str):
  18. raise ValueError("location must be a string")
  19. value = value.strip()
  20. if not value:
  21. return None
  22. if len(value) > LOCATION_NAME_MAX_LENGTH:
  23. raise ValueError(f"location must be at most {LOCATION_NAME_MAX_LENGTH} characters")
  24. return value
  25. def _required_name(value: str) -> str:
  26. name = normalize_location_name(value)
  27. if name is None:
  28. raise ValueError("location name is required")
  29. return name
  30. def _existing_name(value: str) -> str:
  31. """The name of a location that already exists, as stored.
  32. Not length-checked: SQLite never enforced the column width, so a stored
  33. location can be longer than a new one may be, and it must still be possible
  34. to rename or delete it.
  35. """
  36. if not isinstance(value, str) or not value.strip():
  37. raise ValueError("location name is required")
  38. return value.strip()
  39. def _blank_to_none(value: str | None) -> str | None:
  40. if isinstance(value, str) and not value.strip():
  41. return None
  42. return value
  43. class PrinterLocationResponse(BaseModel):
  44. # None for a location that only exists on printers and has no row yet.
  45. id: int | None = None
  46. name: str
  47. icon: str | None = None
  48. color: str | None = None
  49. printer_count: int = 0
  50. class PrinterLocationCreate(BaseModel):
  51. name: str
  52. icon: str | None = Field(default=None, pattern=_ICON_PATTERN)
  53. color: str | None = Field(default=None, pattern=_COLOR_PATTERN)
  54. _name = field_validator("name")(_required_name)
  55. _blank = field_validator("icon", "color", mode="before")(_blank_to_none)
  56. class PrinterLocationUpdate(IgnoresNullForRequiredColumns):
  57. """Rename and/or restyle the location called ``name``.
  58. ``icon`` and ``color`` are only changed when sent; sending null or "" clears
  59. them.
  60. """
  61. __orm_table__ = "printer_locations"
  62. name: str
  63. new_name: str | None = None
  64. icon: str | None = Field(default=None, pattern=_ICON_PATTERN)
  65. color: str | None = Field(default=None, pattern=_COLOR_PATTERN)
  66. _name = field_validator("name")(_existing_name)
  67. _new_name = field_validator("new_name")(_required_name)
  68. _blank = field_validator("icon", "color", mode="before")(_blank_to_none)
  69. class PrinterLocationDelete(BaseModel):
  70. names: list[str] = Field(min_length=1, max_length=500)
  71. @field_validator("names")
  72. @classmethod
  73. def _names(cls, values: list[str]) -> list[str]:
  74. return list(dict.fromkeys(_existing_name(v) for v in values))
  75. class PrinterLocationDeleteResult(BaseModel):
  76. deleted: int
  77. printers_ungrouped: int
  78. class PrinterLocationAssign(BaseModel):
  79. """Move printers into ``location``, or out of any location with null."""
  80. printer_ids: list[int] = Field(min_length=1, max_length=1000)
  81. location: str | None = None
  82. _location = field_validator("location")(normalize_location_name)
  83. class PrinterLocationAssignResult(BaseModel):
  84. moved: int