printer_location.py 3.6 KB

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